1+ #!/usr/bin/env python3
2+ # __ _
3+ # \/imana 2016
4+ # [|-ramewørk
5+ #
6+ #
7+ # Author: s4dhu
8+ # Email: <s4dhul4bs[at]prontonmail[dot]ch
9+ # Git: @s4dhulabs
10+ # Mastodon: @s4dhu
11+ #
12+ # This file is part of Vimana Framework Project.
13+
14+ import re
15+ from typing import Dict , List , Any , Optional
16+ from urllib .parse import urljoin
17+
18+ from .base import BaseDetector
19+
20+
21+ class Web2pyDetector (BaseDetector ):
22+ """Web2py-specific detection methods"""
23+
24+ FRAMEWORK = "Web2py"
25+
26+ # Common Web2py paths to check
27+ COMMON_PATHS = [
28+ '/admin/' ,
29+ '/appadmin/' ,
30+ '/static/' ,
31+ '/welcome/' ,
32+ '/default/' ,
33+ '/_admin/' ,
34+ '/admin/default/' ,
35+ '/admin/default/index' ,
36+ '/admin/default/login' ,
37+ ]
38+
39+ # Web2py error patterns
40+ ERROR_PATTERNS = [
41+ # Pattern, Description, Confidence
42+ (r'web2py_error:' , 'Web2py error header' , 15 ),
43+ (r'web2py_error: invalid application' , 'Web2py invalid application error' , 20 ),
44+ (r'web2py_error: ticket' , 'Web2py ticket error' , 15 ),
45+ (r'web2py_error: ticket invalid' , 'Web2py invalid ticket error' , 18 ),
46+ (r'web2py_error: application' , 'Web2py application error' , 12 ),
47+ ]
48+
49+ # Web2py server patterns
50+ SERVER_PATTERNS = [
51+ # Pattern, Description, Confidence
52+ (r'Rocket\d+' , 'Rocket server (Web2py default)' , 25 ),
53+ (r'Rocket3' , 'Rocket3 server (Web2py default)' , 30 ),
54+ (r'web2py' , 'Web2py server reference' , 20 ),
55+ ]
56+
57+ # Web2py session cookie patterns
58+ SESSION_PATTERNS = [
59+ # Pattern, Description, Confidence
60+ (r'session_id_\w+=' , 'Web2py session cookie pattern' , 25 ),
61+ (r'session_id_admin=' , 'Web2py admin session cookie' , 30 ),
62+ (r'session_id_welcome=' , 'Web2py welcome app session cookie' , 25 ),
63+ ]
64+
65+ # Web2py content patterns
66+ CONTENT_PATTERNS = [
67+ # Pattern, Description, Confidence
68+ (r'web2py' , 'Web2py reference in content' , 5 ),
69+ (r'{{=' , 'Web2py template syntax' , 15 ),
70+ (r'{{extend' , 'Web2py template extend' , 20 ),
71+ (r'{{include' , 'Web2py template include' , 20 ),
72+ (r'{{block' , 'Web2py template block' , 20 ),
73+ (r'{{pass' , 'Web2py template pass' , 20 ),
74+ (r'{{if' , 'Web2py template if' , 15 ),
75+ (r'{{for' , 'Web2py template for' , 15 ),
76+ (r'{{try' , 'Web2py template try' , 15 ),
77+ (r'{{except' , 'Web2py template except' , 15 ),
78+ (r'{{finally' , 'Web2py template finally' , 15 ),
79+ (r'{{def' , 'Web2py template def' , 20 ),
80+ (r'{{return' , 'Web2py template return' , 15 ),
81+ ]
82+
83+ # Web2py header patterns
84+ HEADER_PATTERNS = [
85+ # Header name, Pattern, Description, Confidence
86+ ('X-Powered-By' , r'web2py' , 'X-Powered-By header contains web2py' , 30 ),
87+ ('Server' , r'Rocket\d+' , 'Server header contains Rocket' , 25 ),
88+ ('Server' , r'web2py' , 'Server header contains web2py' , 20 ),
89+ ]
90+
91+ def detect (self ) -> None :
92+ """Run Web2py detection methods"""
93+ self ._check_headers ()
94+ self ._check_common_paths ()
95+ self ._check_error_patterns ()
96+ self ._check_server_patterns ()
97+ self ._check_session_patterns ()
98+ self ._check_content_patterns ()
99+ self ._check_admin_interface ()
100+ self .detect_version ()
101+
102+ def _add_score (self ,
103+ points : int ,
104+ evidence_type : str ,
105+ detail : str ,
106+ raw_data : Optional [Dict [str , Any ]] = None ) -> None :
107+ """Add score for Web2py"""
108+ self .result_manager .add_score (self .FRAMEWORK , points , evidence_type , detail , raw_data )
109+
110+ def _add_version_hint (self ,
111+ version : str ,
112+ confidence : int ,
113+ evidence : str ) -> None :
114+ """Add version hint for Web2py"""
115+ self .result_manager .add_version_hint (self .FRAMEWORK , version , confidence , evidence )
116+
117+ def _add_component (self ,
118+ component : str ,
119+ evidence : str ) -> None :
120+ """Add component for Web2py"""
121+ self .result_manager .add_component (self .FRAMEWORK , component , evidence )
122+
123+ def _check_headers (self ) -> None :
124+ """Check for Web2py-specific headers"""
125+ response = self .request_manager .make_request ()
126+ if not response :
127+ return
128+
129+ headers = response .headers
130+
131+ # Check header patterns
132+ for header_name , pattern , description , confidence in self .HEADER_PATTERNS :
133+ if header_name in headers :
134+ header_value = headers [header_name ]
135+ if re .search (pattern , header_value , re .IGNORECASE ):
136+ self ._add_score (
137+ confidence ,
138+ 'Header' ,
139+ f"{ description } : { header_name } : { header_value } "
140+ )
141+
142+ # Check for web2py in any header
143+ for name , value in headers .items ():
144+ if 'web2py' in value .lower ():
145+ self ._add_score (
146+ 15 ,
147+ 'Header' ,
148+ f"{ name } header contains web2py: { value } "
149+ )
150+
151+ def _check_common_paths (self ) -> None :
152+ """Check for Web2py-specific paths"""
153+ base_url = self .request_manager .target_url .rstrip ('/' )
154+
155+ for path in self .COMMON_PATHS :
156+ url = urljoin (base_url , path )
157+ response = self .request_manager .make_request (url )
158+
159+ if response :
160+ # Check for web2py error patterns in response
161+ if 'web2py_error:' in response .text :
162+ self ._add_score (
163+ 20 ,
164+ 'Endpoint' ,
165+ f"{ path } returns web2py error response"
166+ )
167+
168+ # Check for successful admin access
169+ if path == '/admin/' and response .status_code == 200 :
170+ self ._add_score (
171+ 25 ,
172+ 'Endpoint' ,
173+ f"{ path } returns 200 OK (Web2py admin interface)"
174+ )
175+
176+ # Check for web2py session cookies
177+ if 'session_id_' in response .headers .get ('Set-Cookie' , '' ):
178+ self ._add_score (
179+ 20 ,
180+ 'Endpoint' ,
181+ f"{ path } sets web2py session cookie"
182+ )
183+
184+ def _check_error_patterns (self ) -> None :
185+ """Check for Web2py error patterns"""
186+ response = self .request_manager .make_request ()
187+ if not response :
188+ return
189+
190+ # Check error patterns in response text
191+ for pattern , description , confidence in self .ERROR_PATTERNS :
192+ if re .search (pattern , response .text , re .IGNORECASE ):
193+ self ._add_score (
194+ confidence ,
195+ 'Error' ,
196+ f"{ description } : { pattern } "
197+ )
198+
199+ # Check error patterns in headers
200+ for name , value in response .headers .items ():
201+ for pattern , description , confidence in self .ERROR_PATTERNS :
202+ if re .search (pattern , value , re .IGNORECASE ):
203+ self ._add_score (
204+ confidence ,
205+ 'Error' ,
206+ f"{ description } in { name } header: { value } "
207+ )
208+
209+ def _check_server_patterns (self ) -> None :
210+ """Check for Web2py server patterns"""
211+ response = self .request_manager .make_request ()
212+ if not response :
213+ return
214+
215+ server_header = response .headers .get ('Server' , '' )
216+
217+ for pattern , description , confidence in self .SERVER_PATTERNS :
218+ if re .search (pattern , server_header , re .IGNORECASE ):
219+ self ._add_score (
220+ confidence ,
221+ 'Server' ,
222+ f"{ description } : { server_header } "
223+ )
224+
225+ def _check_session_patterns (self ) -> None :
226+ """Check for Web2py session patterns"""
227+ response = self .request_manager .make_request ()
228+ if not response :
229+ return
230+
231+ # Check Set-Cookie header
232+ set_cookie = response .headers .get ('Set-Cookie' , '' )
233+
234+ for pattern , description , confidence in self .SESSION_PATTERNS :
235+ if re .search (pattern , set_cookie , re .IGNORECASE ):
236+ self ._add_score (
237+ confidence ,
238+ 'Session' ,
239+ f"{ description } : { set_cookie } "
240+ )
241+
242+ def _check_content_patterns (self ) -> None :
243+ """Check for Web2py content patterns"""
244+ response = self .request_manager .make_request ()
245+ if not response :
246+ return
247+
248+ # Check content patterns
249+ for pattern , description , confidence in self .CONTENT_PATTERNS :
250+ if re .search (pattern , response .text , re .IGNORECASE ):
251+ self ._add_score (
252+ confidence ,
253+ 'Content' ,
254+ f"{ description } : { pattern } "
255+ )
256+
257+ def _check_admin_interface (self ) -> None :
258+ """Check for Web2py admin interface"""
259+ base_url = self .request_manager .target_url .rstrip ('/' )
260+ admin_url = urljoin (base_url , '/admin/' )
261+
262+ response = self .request_manager .make_request (admin_url )
263+ if response and response .status_code == 200 :
264+ # Check for web2py admin characteristics
265+ if 'session_id_admin=' in response .headers .get ('Set-Cookie' , '' ):
266+ self ._add_score (
267+ 30 ,
268+ 'Admin' ,
269+ "Web2py admin interface detected with admin session cookie"
270+ )
271+ self ._add_component (
272+ "Admin Interface" ,
273+ "Web2py admin interface accessible at /admin/"
274+ )
275+
276+ def detect_version (self ) -> None :
277+ """Attempt to detect Web2py version"""
278+ response = self .request_manager .make_request ()
279+ if not response :
280+ return
281+
282+ # Check server header for Rocket version
283+ server_header = response .headers .get ('Server' , '' )
284+ rocket_match = re .search (r'Rocket(\d+\.\d+\.\d+)' , server_header )
285+ if rocket_match :
286+ rocket_version = rocket_match .group (1 )
287+ self ._add_version_hint (
288+ f"Rocket { rocket_version } " ,
289+ 70 ,
290+ f"Rocket server version detected: { rocket_version } "
291+ )
292+
293+ # Check for web2py version in headers or content
294+ # This would require more specific version detection logic
295+ # based on web2py's versioning scheme
0 commit comments