tryhackme Web 框架:代码审计

https://tryhackme.com/room/webframeworkscodereview

熟悉代码库

  • 阅读顺序
    1 自述文件和文档 该应用程序的功能、运行方式以及使用的框架
    2 依赖关系清单 库和版本、第三方攻击面
    3 配置文件 调试标志、密钥、数据库字符串
    4 路线/入口点 所有入侵途径列表,我们的攻击面地图
    5 身份验证中间件和装饰器 哪些路由受到保护,哪些不受保护。
    6 数据库/模型层 数据存储在哪里以及如何构建查询
    7 单个路由处理程序 每个入口点背后的逻辑

  • 依赖关系表现

Python: requirements.txt (或 pyproject.toml )
Java: pom.xml
.NET:.csproj
pip-audit 可以作为快速的补充检查工具,它会将清单与漏洞数据库进行比较,但请将其视为辅助检查,而非代码审查的重点。

  • 配置文件

未关闭的调试标志( DEBUG = True )
字符串字面量形式编写的密钥、嵌入密码的数据库连接字符串以及被禁用的安全检查
在 Flask 项目中,这些文件通常位于 config.py 文件中

  • 路由即攻击面

在 Flask 中,路由使用 @app.route 装饰器进行标记
Django为 urlpatterns 列表中
Spring 使用所谓类似 @GetMapping 的注解

  • 身份验证边界

在 Flask 中,这通常意味着在路由上方添加 @login_required 装饰器(或自定义检查)

  • 陌生的框架

回到上述的阅读顺序

源到汇分析

  • 源或输入点

在 Flask 中,常见的数据源都位于 request 对象中:

request.args        # query string parameters
request.form        # POST form fields
request.json        # JSON request body
request.cookies     # cookie values
request.headers     # request headers
request.files       # uploaded files
  • 汇或接收器

主要的接收器包括:
SQL 查询构造( cursor.execute )
Shell 命令执行( os.system , subprocess , shell=True )
模板渲染( render_template_string )
反序列化( pickle.loads , yaml.load )
文件路径构造( open , send_file )
任意执行( eval , exec )

  • 数据流

源与汇之间的数据流可能存在化解输入风险的数据清理(Sanitisation)、校验(Validation)或类型强转(Type Coercion),也可能什么都没有。例如,在 ID 进入查询语句前进行 int() 类型强转即可堵住 SQL 注入漏洞;在打开路径前通过正则匹配拒绝 ../ 则能消除路径穿越风险。请认真研读路径本身,切勿凭空假设其存在。

  • 双向追踪

  • 为什么源头消毒还不够

比如存储型 XSS 攻击,比如二阶注入:进行追踪时,我们会追踪数据进入数据库并返回的整个过程,而不仅仅是从请求到第一个访问该数据的函数。

  • 示例
@app.route("/greet")
def greet():
    name = request.args.get("name")
    return render_template_string(f"Hello {name}")

源: request.args.get("name")
汇: render_template_string
它会将参数编译成 Jinja2 模板。它们之间的数据流是一个 f-string,它会将 name 直接插入到模板文本中,没有任何验证。用户控制模板语法,这属于服务器端模板注入 (SSTI)。
对比一下安全版本,其中值作为数据传递给一个固定的模板,永远不会变成模板代码:

return render_template("greet.html", name=name)
  • 使用工具进行追踪

Semgrep:污点模式会追踪从已声明的源到已声明的汇的值,并报告路径,这使得手动追踪技术能够扩展到整个代码库
CodeQL与上述类似,并进行了更深入的跨过程分析

使用 Grep 检索代码风险

  • 搜索危险调用
grep -rn --include="*.py" -E "os\.system|subprocess|eval\(|exec\(|pickle\.loads|render_template_string|cursor\.execute|send_file|open\(" .

在大型代码库中,ripgrep(rg) 是更快的直接替代方案,默认会跳过 .gitignore 中的内容,从而避免引入的第三方包出现在搜索结果中。

  • 使用 Grep 命令查找 Secrets 和 Config
寻找硬编码
$ grep -rnE "(SECRET|KEY|TOKEN|PASSWORD|API_KEY)\s*=\s*['\"]" --include="*.py" .

寻找配置反模式
$ grep -rnE "DEBUG\s*=\s*True|TESTING\s*=\s*True|verify\s*=\s*False" .

寻找 AWS 密钥
grep -rE "AKIA[0-9A-Z]{16}" .
  • Semgrep 用于基于规则的分类

grep 匹配文本。Semgrep 匹配代码结构

$ pip install semgrep        # already installed on the room's machine
$ semgrep --config p/owasp-top-ten .   # registry ruleset, needs internet: run on a connected box, not the offline VM

--config 标志用于选择规则集。p/owasp-top-ten 将发现映射到 OWASP 十大分类,p/python 则是一个更广泛的 Python 规则集。

每条发现都会列出一个规则、一个文件、一行代码以及一个严重级别。将每条发现视为一个候选结果,就像我们解读 grep 的匹配项一样:Semgrep 标记了一个模式,但我们仍需确认该输入是否为用户可控的。

  • 最简自定义规则
rules:
  - id: render-template-string-usage
    pattern: render_template_string(...)
    message: render_template_string on possible user input, check for SSTI
    severity: WARNING
    languages: [python]

pattern 是要匹配的代码形态,message 是命中时输出的提示信息,severity 则用于对输出结果分级。这些已足够让我们将现有规则适配到目标项目;而编写复杂规则本身则是一门独立的技能。
如果我们需要完全开源的引擎,Opengrep 是 Semgrep 的 LGPL 分支,兼容相同的规则语法。

  • 适时止步

分类排查的风险在于盲目自信。grep 能定位到 cursor.execute,却无法判断传入其中的字符串是来自用户输入,还是来自上方两行处的硬编码常量。按影响程度依次处理候选列表,通过阅读上下文代码逐一验证每个命中项,只有在确认后才将其定性为发现(finding)。开始之前先排序:命中 render_template_string 或 cursor.execute 的条目,应当优先于命中 open( 的条目得到关注——后者绝大多数情况下是无害的。一长串 grep 命中结果只是一份待办清单,而非报告。

代码中的注入漏洞

注入类漏洞全都遵循同一个模式:用户输入流入了某种解释器——数据库、Shell、模板引擎或反序列化器——而解释器会按输入的指令执行。一旦我们掌握了每一类漏洞中危险的接收点(sink)及其安全的对应写法,就能一眼识别出该缺陷。

  • SQL注入

该漏洞在于通过 f-string 或字符串拼接将用户输入直接嵌入 SQL 字符串来构建查询,而非使用占位符:

# Vulnerable: q is formatted straight into the SQL text
q = request.args.get("q")
cursor.execute(f"SELECT * FROM items WHERE name = '{q}'")

安全写法将值作为参数传入,使数据库驱动程序将数据与代码分离开来:

# Safe: the ? is a placeholder, q is bound as data
cursor.execute("SELECT * FROM items WHERE name = ?", (q,))

也要注意二阶注入的情况:输入被安全地存储,但后续查询又将其通过 f-string 拼回 SQL。接收点(sink)相同,只是源头变成了数据库。

SQL 注入很少仅仅是读取数据库。根据查询语句和数据库引擎的不同,它可以修改行数据、绕过认证检查,或读取本地文件。它还隐藏在 ORM 之中——ORM 通常会为我们构建参数化查询,这也是使用 ORM 让人感到安全的原因。但一旦开发者使用了原始的"逃生通道"——如 SQLAlchemy 的 text()、Django 的 .raw() 或 .extra()——他们就会将手工拼接的字符串交给数据库,从而失去这层保护。因此,使用了 ORM 并不能保证查询就是参数化的。当我们在这些逃生通道内部发现 f-string 时,应将其视为原始的 cursor.execute 一样对待。

  • 命令注入

该漏洞在于将用户输入放入会被 Shell 解析的命令字符串中:

# Vulnerable: shell=True means the shell parses the whole string
host = request.args.get("host")
subprocess.run(f"ping -c 1 {host}", shell=True)

像 127.0.0.1; cat /etc/passwd 这样的值会执行第二条命令。os.system 和 os.popen 也存在同样的风险。安全模式将参数作为列表传入,并去掉 shell,这样输入永远只能是一个单独的参数,绝不会被解析为新的语法:

# Safe: no shell, host is one argument and cannot add commands
subprocess.run(["ping", "-c", "1", host])

关键开关在于 shell。 当 shell=True 时,整个字符串被交给 /bin/sh 解析,而 sh 会将 ;、|、&& 和反引号视为可执行的语法。当使用列表且不带 shell 时,操作系统直接运行指定的程序,每个元素都是字面量参数,因此攻击者没有任何语法可供走私注入。

  • 服务端模板注入
posted @ 2026-08-04 03:12  sec875  阅读(3)  评论(0)    收藏  举报