Skip to content

Commit 42f539a

Browse files
committed
Update Fmwk.CherryPy detector
1 parent c306215 commit 42f539a

File tree

1 file changed

+238
-0
lines changed

1 file changed

+238
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#!/usr/bin/env python3
2+
# __ _
3+
# \/imana 2016
4+
# [|-ramewørk
5+
#
6+
# Author: s4dhu
7+
# Email: <s4dhul4bs[at]prontonmail[dot]ch
8+
# Git: @s4dhulabs
9+
# Mastodon: @s4dhu
10+
#
11+
# This file is part of Vimana Framework Project.
12+
13+
import re
14+
from typing import Dict, List, Any, Optional
15+
from urllib.parse import urljoin
16+
17+
from .base import BaseDetector
18+
19+
class CherryPyDetector(BaseDetector):
20+
"""CherryPy-specific detection methods"""
21+
22+
FRAMEWORK = "CherryPy"
23+
24+
COMMON_PATHS = [
25+
'/admin',
26+
'/api/status',
27+
'/about',
28+
'/error',
29+
'/tools',
30+
'/config',
31+
'/sessions',
32+
]
33+
34+
ERROR_PATTERNS = [
35+
(r'cherrypy\.HTTPError', 'CherryPy HTTPError in error page', 40),
36+
(r'cherrypy\.lib\.cptools', 'CherryPy tools in error traceback', 35),
37+
(r'cherrypy\.wsgiserver', 'CherryPy WSGI server in error', 30),
38+
(r'cherrypy\._cperror', 'CherryPy error module in traceback', 30),
39+
(r'cherrypy\.expose', 'CherryPy expose decorator in error', 25),
40+
(r'cherrypy\.quickstart', 'CherryPy quickstart in error', 25),
41+
(r'cherrypy\.config', 'CherryPy config in error', 20),
42+
(r'(?i)cherrypy', 'CherryPy reference in error', 15),
43+
]
44+
45+
CONTENT_PATTERNS = [
46+
(r'Hello from CherryPy!', 'CherryPy greeting in content', 30),
47+
(r'<title>CherryPy Test App</title>', 'CherryPy test app title', 25),
48+
(r'<h1>Hello from CherryPy!</h1>', 'CherryPy heading in content', 25),
49+
(r'CherryPy Framework Detection Test', 'CherryPy detection test string', 25),
50+
(r'CherryPy is a pythonic, object-oriented HTTP framework', 'CherryPy description', 20),
51+
(r'CherryPy Admin Interface', 'CherryPy admin interface title', 30),
52+
(r'cherrypy\.quickstart', 'CherryPy quickstart in content', 20),
53+
(r'(?i)cherrypy', 'CherryPy reference in HTML', 15),
54+
]
55+
56+
HEADER_PATTERNS = [
57+
('server', r'cherrypy', 'CherryPy server header', 40),
58+
('x-powered-by', r'cherrypy', 'CherryPy X-Powered-By header', 35),
59+
('content-type', r'application/json', 'JSON API response', 5),
60+
]
61+
62+
API_PATTERNS = [
63+
(r'"framework": ?"cherrypy"', 'CherryPy framework in API response', 40),
64+
(r'"status": ?"running"', 'Status running in API response', 10),
65+
(r'"version": ?"[0-9.]+"', 'Version in API response', 10),
66+
]
67+
68+
SESSION_PATTERNS = [
69+
(r'session_id', 'CherryPy session cookie pattern', 25),
70+
(r'cherrypy_session', 'CherryPy session cookie', 30),
71+
]
72+
73+
def detect(self) -> None:
74+
evidence = set()
75+
evidence_types = set()
76+
77+
# Check error page for CherryPy-specific patterns
78+
if self._check_error_patterns(evidence, evidence_types):
79+
pass
80+
81+
# Check API status endpoint for CherryPy-specific JSON
82+
if self._check_api_status(evidence, evidence_types):
83+
pass
84+
85+
# Check main page and about for CherryPy content
86+
if self._check_content_patterns(evidence, evidence_types):
87+
pass
88+
89+
# Check headers for CherryPy server and X-Powered-By
90+
if self._check_headers(evidence, evidence_types):
91+
pass
92+
93+
# Check for CherryPy session cookies
94+
if self._check_session_patterns(evidence, evidence_types):
95+
pass
96+
97+
# Check common paths for 200 OK (low confidence)
98+
self._check_common_paths(evidence, evidence_types)
99+
100+
# Assign confidence based on evidence
101+
self._score_evidence(evidence, evidence_types)
102+
self.detect_version()
103+
104+
def _add_score(self, points: int, evidence_type: str, detail: str, raw_data: Optional[Dict[str, Any]] = None) -> None:
105+
self.result_manager.add_score(self.FRAMEWORK, points, evidence_type, detail, raw_data)
106+
107+
def _add_version_hint(self, version: str, confidence: int, evidence: str) -> None:
108+
self.result_manager.add_version_hint(self.FRAMEWORK, version, confidence, evidence)
109+
110+
def _add_component(self, component: str, evidence: str) -> None:
111+
self.result_manager.add_component(self.FRAMEWORK, component, evidence)
112+
113+
def _check_headers(self, evidence, evidence_types) -> bool:
114+
response = self.request_manager.make_request()
115+
if not response:
116+
return False
117+
headers = response.headers
118+
found = False
119+
for header_name, pattern, description, confidence in self.HEADER_PATTERNS:
120+
if header_name in headers:
121+
header_value = headers[header_name]
122+
if re.search(pattern, header_value, re.IGNORECASE):
123+
evidence.add(f"Header: {description}")
124+
evidence_types.add('header')
125+
found = True
126+
return found
127+
128+
def _check_content_patterns(self, evidence, evidence_types) -> bool:
129+
response = self.request_manager.make_request()
130+
if not response:
131+
return False
132+
found = False
133+
for pattern, description, confidence in self.CONTENT_PATTERNS:
134+
if re.search(pattern, response.text, re.IGNORECASE):
135+
evidence.add(f"Content: {description}")
136+
evidence_types.add('content')
137+
found = True
138+
return found
139+
140+
def _check_error_patterns(self, evidence, evidence_types) -> bool:
141+
base_url = self.request_manager.target_url.rstrip('/')
142+
error_url = urljoin(base_url, '/error')
143+
response = self.request_manager.make_request(error_url)
144+
if not response:
145+
return False
146+
found = False
147+
for pattern, description, confidence in self.ERROR_PATTERNS:
148+
if re.search(pattern, response.text, re.IGNORECASE):
149+
evidence.add(f"Error: {description}")
150+
evidence_types.add('error')
151+
found = True
152+
return found
153+
154+
def _check_api_status(self, evidence, evidence_types) -> bool:
155+
base_url = self.request_manager.target_url.rstrip('/')
156+
api_url = urljoin(base_url, '/api/status')
157+
response = self.request_manager.make_request(api_url)
158+
if not response:
159+
return False
160+
found = False
161+
for pattern, description, confidence in self.API_PATTERNS:
162+
if re.search(pattern, response.text, re.IGNORECASE):
163+
evidence.add(f"API: {description}")
164+
evidence_types.add('api')
165+
found = True
166+
if response.headers.get('content-type', '').startswith('application/json'):
167+
evidence.add("API: JSON API response detected")
168+
evidence_types.add('api')
169+
found = True
170+
return found
171+
172+
def _check_session_patterns(self, evidence, evidence_types) -> bool:
173+
response = self.request_manager.make_request()
174+
if not response:
175+
return False
176+
found = False
177+
cookies = response.cookies
178+
for name, value in cookies.items():
179+
for pattern, description, confidence in self.SESSION_PATTERNS:
180+
if re.search(pattern, name, re.IGNORECASE):
181+
evidence.add(f"Session: {description}")
182+
evidence_types.add('session')
183+
found = True
184+
return found
185+
186+
def _check_common_paths(self, evidence, evidence_types):
187+
base_url = self.request_manager.target_url.rstrip('/')
188+
for path in self.COMMON_PATHS:
189+
url = urljoin(base_url, path)
190+
response = self.request_manager.make_request(url)
191+
if response and response.status_code == 200:
192+
# Only add as low confidence evidence
193+
evidence.add(f"Endpoint: {path} returns 200 OK")
194+
evidence_types.add('endpoint')
195+
196+
def _score_evidence(self, evidence, evidence_types):
197+
# High confidence: at least 2 unique types of strong evidence
198+
strong_types = {'error', 'api', 'content', 'header', 'session'}
199+
strong_evidence = strong_types.intersection(evidence_types)
200+
201+
if len(strong_evidence) >= 3:
202+
self._add_score(95, 'Composite', f"CherryPy detected by: {sorted(strong_evidence)} | {sorted(evidence)}")
203+
elif len(strong_evidence) >= 2:
204+
self._add_score(85, 'Composite', f"CherryPy detected by: {sorted(strong_evidence)} | {sorted(evidence)}")
205+
elif 'header' in evidence_types and 'content' in evidence_types:
206+
self._add_score(75, 'Composite', f"CherryPy header and content evidence: {sorted(evidence)}")
207+
elif 'error' in evidence_types:
208+
self._add_score(40, 'Error', f"CherryPy error evidence: {sorted(evidence)}")
209+
elif 'header' in evidence_types:
210+
self._add_score(35, 'Header', f"CherryPy header evidence: {sorted(evidence)}")
211+
elif 'session' in evidence_types:
212+
self._add_score(30, 'Session', f"CherryPy session evidence: {sorted(evidence)}")
213+
elif 'api' in evidence_types:
214+
self._add_score(25, 'API', f"CherryPy API evidence: {sorted(evidence)}")
215+
elif 'content' in evidence_types:
216+
self._add_score(20, 'Content', f"CherryPy content evidence: {sorted(evidence)}")
217+
elif 'endpoint' in evidence_types:
218+
self._add_score(10, 'Endpoint', f"CherryPy endpoint evidence: {sorted(evidence)}")
219+
220+
def detect_version(self) -> None:
221+
# Try to detect version from server header
222+
response = self.request_manager.make_request()
223+
if response and 'server' in response.headers:
224+
server_header = response.headers['server']
225+
version_match = re.search(r'cherrypy[/\s]+(\d+\.\d+\.\d+)', server_header, re.IGNORECASE)
226+
if version_match:
227+
version = version_match.group(1)
228+
self._add_version_hint(version, 80, f"CherryPy version detected in server header: {version}")
229+
230+
# Try to detect version from API response
231+
base_url = self.request_manager.target_url.rstrip('/')
232+
api_url = urljoin(base_url, '/api/status')
233+
response = self.request_manager.make_request(api_url)
234+
if response:
235+
version_match = re.search(r'"version": ?"([0-9.]+)"', response.text)
236+
if version_match:
237+
version = version_match.group(1)
238+
self._add_version_hint(version, 85, f"CherryPy version detected in API response: {version}")

0 commit comments

Comments
 (0)