Skip to content

Commit 2800afb

Browse files
committed
feat: 提高VulnShop靶场鲁棒性,防止SQLMap扫描时崩溃
1 parent 88d824b commit 2800afb

2 files changed

Lines changed: 179 additions & 75 deletions

File tree

src/vulnTestServer/handlers/base.py

Lines changed: 114 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,20 @@ class BaseHandlerMixin:
3737

3838
def send_json_response(self, data, status=200):
3939
"""发送JSON响应"""
40-
self.send_response(status)
41-
self.send_header('Content-Type', 'application/json; charset=utf-8')
42-
self.send_header('Access-Control-Allow-Origin', '*')
43-
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
44-
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
45-
self.end_headers()
46-
response = json.dumps(data, ensure_ascii=False)
47-
self.wfile.write(response.encode('utf-8'))
40+
try:
41+
self.send_response(status)
42+
self.send_header('Content-Type', 'application/json; charset=utf-8')
43+
self.send_header('Access-Control-Allow-Origin', '*')
44+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
45+
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
46+
self.end_headers()
47+
response = json.dumps(data, ensure_ascii=False)
48+
self.wfile.write(response.encode('utf-8'))
49+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
50+
# 客户端断开连接,静默处理
51+
pass
52+
except Exception as e:
53+
error_logger.debug("Error sending JSON response: %s", str(e))
4854

4955
def send_error_response(self, message, status=400, sql_error=None):
5056
"""发送错误响应(可能包含SQL错误信息用于演示)"""
@@ -63,37 +69,43 @@ def send_error_response(self, message, status=400, sql_error=None):
6369

6470
def send_xml_response(self, data, status=200):
6571
"""发送XML响应"""
66-
self.send_response(status)
67-
self.send_header('Content-Type', 'application/xml; charset=utf-8')
68-
self.send_header('Access-Control-Allow-Origin', '*')
69-
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
70-
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Id, X-Token')
71-
self.end_headers()
72-
73-
# 将dict转换为XML
74-
def dict_to_xml(d, root_name='response'):
75-
root = ET.Element(root_name)
76-
for key, value in d.items():
77-
child = ET.SubElement(root, key)
78-
if isinstance(value, dict):
79-
for k, v in value.items():
80-
sub = ET.SubElement(child, k)
81-
sub.text = str(v) if v is not None else ''
82-
elif isinstance(value, list):
83-
for item in value:
84-
item_el = ET.SubElement(child, 'item')
85-
if isinstance(item, dict):
86-
for k, v in item.items():
87-
sub = ET.SubElement(item_el, k)
88-
sub.text = str(v) if v is not None else ''
89-
else:
90-
item_el.text = str(item)
91-
else:
92-
child.text = str(value) if value is not None else ''
93-
return ET.tostring(root, encoding='unicode')
94-
95-
xml_str = '<?xml version="1.0" encoding="UTF-8"?>\n' + dict_to_xml(data)
96-
self.wfile.write(xml_str.encode('utf-8'))
72+
try:
73+
self.send_response(status)
74+
self.send_header('Content-Type', 'application/xml; charset=utf-8')
75+
self.send_header('Access-Control-Allow-Origin', '*')
76+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
77+
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Id, X-Token')
78+
self.end_headers()
79+
80+
# 将dict转换为XML
81+
def dict_to_xml(d, root_name='response'):
82+
root = ET.Element(root_name)
83+
for key, value in d.items():
84+
child = ET.SubElement(root, key)
85+
if isinstance(value, dict):
86+
for k, v in value.items():
87+
sub = ET.SubElement(child, k)
88+
sub.text = str(v) if v is not None else ''
89+
elif isinstance(value, list):
90+
for item in value:
91+
item_el = ET.SubElement(child, 'item')
92+
if isinstance(item, dict):
93+
for k, v in item.items():
94+
sub = ET.SubElement(item_el, k)
95+
sub.text = str(v) if v is not None else ''
96+
else:
97+
item_el.text = str(item)
98+
else:
99+
child.text = str(value) if value is not None else ''
100+
return ET.tostring(root, encoding='unicode')
101+
102+
xml_str = '<?xml version="1.0" encoding="UTF-8"?>\n' + dict_to_xml(data)
103+
self.wfile.write(xml_str.encode('utf-8'))
104+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
105+
# 客户端断开连接,静默处理
106+
pass
107+
except Exception as e:
108+
error_logger.debug("Error sending XML response: %s", str(e))
97109

98110
def send_static_file(self, filepath):
99111
"""发送静态文件"""
@@ -120,42 +132,78 @@ def send_static_file(self, filepath):
120132
self.send_header('Content-Length', len(content))
121133
self.end_headers()
122134
self.wfile.write(content)
135+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
136+
# 客户端断开连接,静默处理
137+
pass
138+
except FileNotFoundError:
139+
self.send_error(404, 'File Not Found')
123140
except Exception as e:
124-
error_logger.exception("Error serving static file: %s", filepath)
125-
self.send_error(500, str(e))
141+
error_logger.debug("Error serving static file %s: %s", filepath, str(e))
142+
try:
143+
self.send_error(500, 'Internal Server Error')
144+
except:
145+
pass
126146

127147
def get_post_data(self):
128148
"""获取POST请求数据,支持JSON、URL-encoded和XML格式"""
129-
content_length = int(self.headers.get('Content-Length', 0))
130-
if content_length == 0:
131-
return {}
132-
133-
post_data = self.rfile.read(content_length).decode('utf-8')
134-
content_type = self.headers.get('Content-Type', '')
135-
136-
if 'application/json' in content_type:
149+
try:
150+
content_length_str = self.headers.get('Content-Length', '0')
137151
try:
138-
return json.loads(post_data)
139-
except:
152+
content_length = int(content_length_str)
153+
except (ValueError, TypeError):
154+
content_length = 0
155+
156+
if content_length <= 0:
140157
return {}
141-
elif 'application/xml' in content_type or 'text/xml' in content_type:
142-
# XML解析
158+
159+
# 限制最大读取长度,防止内存攻击
160+
max_length = 10 * 1024 * 1024 # 10MB
161+
if content_length > max_length:
162+
content_length = max_length
163+
164+
raw_data = self.rfile.read(content_length)
143165
try:
144-
root = ET.fromstring(post_data)
166+
post_data = raw_data.decode('utf-8')
167+
except UnicodeDecodeError:
168+
# 尝试其他编码
169+
try:
170+
post_data = raw_data.decode('latin-1')
171+
except:
172+
return {}
173+
174+
content_type = self.headers.get('Content-Type', '')
175+
176+
if 'application/json' in content_type:
177+
try:
178+
return json.loads(post_data)
179+
except:
180+
return {}
181+
elif 'application/xml' in content_type or 'text/xml' in content_type:
182+
# XML解析
183+
try:
184+
root = ET.fromstring(post_data)
185+
result = {}
186+
for child in root:
187+
result[child.tag] = child.text or ''
188+
return result
189+
except:
190+
return {}
191+
else:
192+
# application/x-www-form-urlencoded
145193
result = {}
146-
for child in root:
147-
result[child.tag] = child.text or ''
194+
for pair in post_data.split('&'):
195+
if '=' in pair:
196+
key, value = pair.split('=', 1)
197+
try:
198+
result[unquote(key)] = unquote(value)
199+
except:
200+
pass
148201
return result
149-
except:
150-
return {}
151-
else:
152-
# application/x-www-form-urlencoded
153-
result = {}
154-
for pair in post_data.split('&'):
155-
if '=' in pair:
156-
key, value = pair.split('=', 1)
157-
result[unquote(key)] = unquote(value)
158-
return result
202+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
203+
return {}
204+
except Exception as e:
205+
error_logger.debug("Error reading POST data: %s", str(e))
206+
return {}
159207

160208
def get_content_type(self):
161209
"""获取请求的Content-Type类型"""

src/vulnTestServer/server.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424

2525
import os
2626
import sys
27-
import traceback
27+
import socket
2828
from http.server import HTTPServer, BaseHTTPRequestHandler
29+
from socketserver import ThreadingMixIn
2930
from urllib.parse import urlparse, parse_qs
3031

3132
# 添加当前目录到路径
@@ -60,9 +61,42 @@ class VulnShopHandler(
6061
使用Mixin模式组合各个功能模块,实现模块化设计
6162
"""
6263

64+
# 设置请求超时(秒)
65+
timeout = 30
66+
6367
def log_message(self, format, *args):
6468
"""重写日志方法,使用自定义日志器"""
65-
access_logger.info("%s - %s", self.client_address[0], format % args)
69+
try:
70+
access_logger.info("%s - %s", self.client_address[0], format % args)
71+
except Exception:
72+
pass
73+
74+
def handle_one_request(self):
75+
"""重写请求处理,增加异常保护"""
76+
try:
77+
super().handle_one_request()
78+
except socket.timeout:
79+
# 超时不是错误
80+
self.close_connection = True
81+
except ConnectionResetError:
82+
# 客户端重置连接
83+
self.close_connection = True
84+
except BrokenPipeError:
85+
# 管道破裂,客户端断开
86+
self.close_connection = True
87+
except ConnectionAbortedError:
88+
# 连接被中止
89+
self.close_connection = True
90+
except Exception as e:
91+
error_logger.debug("Request handling error: %s", str(e))
92+
self.close_connection = True
93+
94+
def finish(self):
95+
"""重写finish方法,处理连接关闭时的异常"""
96+
try:
97+
super().finish()
98+
except Exception:
99+
pass
66100

67101
def do_OPTIONS(self):
68102
"""处理CORS预检请求"""
@@ -109,10 +143,22 @@ def do_GET(self):
109143
self.send_error(404, 'Not Found')
110144
except WAFBlockedException as e:
111145
access_logger.warning("%s - WAF Blocked: %s", self.client_address[0], e.reason)
112-
self.send_error_response(f'WAF Blocked: {e.reason}', 403)
146+
self._safe_send_error('WAF Blocked: ' + e.reason, 403)
147+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
148+
# 客户端断开连接,无需处理
149+
pass
113150
except Exception as e:
114151
error_logger.exception("Error processing GET %s: %s", path, str(e))
115-
self.send_error_response(str(e), 500, sql_error=e)
152+
self._safe_send_error(str(e), 500, sql_error=e)
153+
154+
def _safe_send_error(self, message, status=400, sql_error=None):
155+
"""安全发送错误响应,处理连接断开的情况"""
156+
try:
157+
self.send_error_response(message, status, sql_error)
158+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
159+
pass
160+
except Exception:
161+
pass
116162

117163
def do_POST(self):
118164
"""处理POST请求"""
@@ -145,10 +191,19 @@ def do_POST(self):
145191
self.send_error(404, 'Not Found')
146192
except WAFBlockedException as e:
147193
access_logger.warning("%s - WAF Blocked: %s", self.client_address[0], e.reason)
148-
self.send_error_response(f'WAF Blocked: {e.reason}', 403)
194+
self._safe_send_error('WAF Blocked: ' + e.reason, 403)
195+
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
196+
# 客户端断开连接,无需处理
197+
pass
149198
except Exception as e:
150199
error_logger.exception("Error processing POST %s: %s", path, str(e))
151-
self.send_error_response(str(e), 500, sql_error=e)
200+
self._safe_send_error(str(e), 500, sql_error=e)
201+
202+
203+
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
204+
"""支持多线程的HTTP服务器"""
205+
daemon_threads = True # 守护线程,主线程退出时自动结束
206+
allow_reuse_address = True # 允许端口复用
152207

153208

154209
def run_server():
@@ -157,9 +212,10 @@ def run_server():
157212
init_database()
158213
logger.info("Database initialized")
159214

160-
# 创建服务器
161-
server = HTTPServer((HOST, PORT), VulnShopHandler)
162-
logger.info("Server created on %s:%d", HOST, PORT)
215+
# 创建多线程服务器
216+
server = ThreadedHTTPServer((HOST, PORT), VulnShopHandler)
217+
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
218+
logger.info("Threaded server created on %s:%d", HOST, PORT)
163219

164220
print(f"""
165221
╔═══════════════════════════════════════════════════════════════════════════════════════╗

0 commit comments

Comments
 (0)