代码审计:IRify 实战之 SQL 注入检测
之前的文章为大家介绍过
如何使用IRify功能进行代码审计
今天则是使用一个案例来为大家说明
如何使用IRify实战SQL注入
前段时间我们正式拆分了新的代码审计工具:IRify,这是一个支持多种语言的代码审计辅助工具。发布之初其内置了一些扫描规则,包括常见的漏洞检查,今天我们来尝试分析和使用其中的java部分的sql注入规则。
污点分析
在软件安全领域,污点分析(Taint Analysis) 是一种通过跟踪不可信数据(污点源)在程序中的传播路径,识别潜在漏洞的静态或动态分析技术。针对SQL注入漏洞的分析,污点分析主要通过以下三个核心步骤实现:
1.污点源标记将用户可控的输入点(如HTTP请求参数、表单输入、API接口等)标记为初始污点源。
2. 传播路径追踪通过数据流分析跟踪污点数据在程序内的传播过程,包括变量赋值、字符串拼接、函数参数传递等操作。例如用户输入被直接拼接至SQL语句:
"SELECT * FROM users WHERE id = " + userInput
此时污点会从userInput变量传播到完整的SQL语句字符串。
3.敏感点检测(Sanitization Check)在污点数据到达执行敏感操作的关键节点(如数据库查询接口executeQuery())时,检测是否经过安全净化(如参数化查询、特殊字符转义)。若污点数据未经净化直接到达敏感点,则判定存在SQL注入风险。
那么一个比较明显的事情是:需要先找到很多污点源和敏感点。实际项目中无论是语言内置库,还是框架代码的使用,很多污点源和敏感点都是可预见的。所以我们提供 库规则 提供了一系列的污点源和 敏感点的筛选规则,以减少重复规则的编写。
污点源获取
Spring 是一个被广泛应用的web框架,其中可以通过注解来定义解析一些web参数,这些被Mapping注解修饰的函数的参数则可以视为是一个污点源。
我们提供的规则中,关于Spring的web参数的规则是这样的:
desc(
title: 'checking [spring controller source parameter]',
type: audit,
lib: 'java-spring-param',
level: medium,
desc: <<<TEXT
此规则旨在审计Spring框架中控制器方法的参数来源安全性。确保控制器方法中的参数来源清晰且安全至关重要,因为不当的参数处理可能导致安全漏洞,如SQL注入、跨站脚本攻击(XSS)等。通过检查控制器方法的参数是否明确指定了来源(如通过@RequestParam、@PathVariable等注解),可以防止潜在的参数篡改和注入攻击。此外,这也有助于维护代码的清晰性和可维护性。
TEXT
)
*Mapping.__ref__?{opcode: function} as $start;
// 筛选所有*Mapping注解,再定位到被注解修饰的实例,再筛选出所有的function
// annotation method' formal params.
$start(*?{opcode: param && !have: this} as $formalParams);
// 筛选出的function的所有参数做为静态污点源
.getParameter()?{<getFunc>.annotation.*Mapping} as $dynamicParams;
// 筛选所有的参值,再过滤其所属方法是否有*Mapping注解修饰,如果有则认为其是一个污点源
// merge
$formalParams + $dynamicParams as $output;
// output lib params
alert $output;
我们编译JavaSecLab项目,尝试获取一下污点,可以看到筛选到了一些web参数的值。
敏感点获取
JDBC 是java比较基础和简单的一个sql库。但是如果使用不当就会造成sql注入。
1.Statement 类的方法(危险)
Statement 可以直接通过字符串执行 SQL,是 SQL 注入的高发区。危险方法有
executeQuery: 执行查询语句(如SELECT),返回ResultSet
String userInput = "admin' OR 1=1 --";
String sql = "SELECT * FROM users WHERE username = '" + userInput + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql); // 直接拼接,触发注入!
executeUpdate: 执行更新语句(如 INSERT, UPDATE, DELETE),返回受影响行数
String input = "'; DROP TABLE users; --";
String sql = "UPDATE logs SET content = '" + input + "' WHERE id=1";
stmt.executeUpdate(sql); // 执行后会删除 users 表!
execute: 执行任意 SQL 语句(如存储过程),返回布尔值表示是否返回结果集
String input = "'; DROP TABLE users; --";
String sql = "UPDATE logs SET content = '" + input + "' WHERE id=1";
stmt.executeUpdate(sql); // 执行后会删除 users 表!
2.PreparedStatement类的方法(安全,但需正确使用)
通过预编译和参数化查询防止 SQL 注入,但需避免错误使用导致失效。
- 安全用法:
String userInput = "admin' OR 1=1 --";
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, userInput); // 参数化赋值
ResultSet rs = pstmt.executeQuery(); // 安全!输入会被转义
- 错误用法(仍有风险):
// 错误:在预编译语句外部拼接字符串!
String sql = "SELECT * FROM users WHERE username = '" + userInput + "'";
PreparedStatement pstmt = connection.prepareStatement(sql); // 等同于 Statement,仍有注入风险
那么可以写出两条相关的敏感点规则:
直接执行sql语句的敏感点
desc(
title: "JDBC getConnection.createStatement.executeQuery SQL",
title_zh: "JDBC getConnection.createStatement.executeQuery SQL 执行语句",
type: audit,
level: 'low',
lib: 'java-jdbc-raw-execute-sink',
desc: <<<TEXT
使用 `DriverManager.getConnection().createStatement().executeQuery\executeUpdate\execut` 直接执行 SQL 查询可能会导致 SQL 注入漏洞。SQL 注入是一种攻击技术,攻击者可以通过在输入字段中插入恶意的 SQL 代码,从而操纵后端数据库执行未授权的查询或操作。这可能导致数据泄露、数据篡改或数据库损坏等严重后果。建议使用预处理语句(PreparedStatement)来替代直接执行 SQL 查询,以有效防止 SQL 注入攻击。
TEXT
)
DriverManager.getConnection().createStatement() as $stmt;
$stmt?{!.set*()} as $checkedStmt;
$checkedStmt.execute*(*<slice(start=1)> as $sink);
// 筛选所有调用 Statement.executeQuery\executeUpdate\execute 时的参数作为敏感点
check $sink;
$sink as $output;
alert $output;
即便使用了预处理语句,但是没有做严格的参数化,在构建预处理语句的时候,就进行了语句的拼接,这也会导致sql注入的出现
desc(
title: "JDBC getConnection.prepareStatement.executeQuery SQL",
title_zh: "JDBC getConnection.prepareStatement.executeQuery SQL 执行语句",
type: audit,
level: low,
lib: 'java-jdbc-prepared-execute-sink'
)
DriverManager.getConnection() as $conn;
$conn.prepareStatement(*<slice(start=1)> as $output) as $stmt;
// 构建预处理语句传入的sql语句是一个敏感点
$stmt.executeQuery() as $call;
// 检查预处理语句是否被执行
check $call;
check $output;
alert $output;
在代码审计中尝试一下
路径追踪
现在已经获取到了 对应的 spring web参数 污点源 和 jdbc 数据库操作敏感点。那么现在就需要编写规则检查其数据流的关系。
在IRifty中的内置规则里其实并没有 sql注入规则的直接报告,取而代之的是对敏感sql拼接的检查。
<include('java-jdbc-prepared-execute-sink')> as $params
<include('java-jdbc-raw-execute-sink')> as $params
$params<getCallee>?{<name>?{have:toString}}<getObject>.append(, * as $appendParams)
// 这里是关键的行为,在拿到敏感点的基础上 尝试往数据的来源追溯,检查是否有字符串拼接的行为
// 将参与拼接的值也视为敏感点,完成数据流的缝合
$params<getFunc>(* as $limited) // 处理stringbuilder 形式的 字符串构建
$params + $appendParams as $params
$params ?{opcode: param} as $directly
$params ?{!opcode:param} #{include:<<<INCLUDE
*?{opcode:param && <self> & $limited}
INCLUDE
}-> as $indirectly
$directly + $indirectly as $vuln
alert $vuln;
我们对这条规则稍作改造,尝试和spring的 污点源做路径追踪:
<include('java-spring-param')> as $entry;
<include('java-jdbc-prepared-execute-sink')> as $sink
<include('java-jdbc-raw-execute-sink')> as $sink
$sink<getCallee>?{<name>?{have:toString}}<getObject>.append(, * as $appendParams)
$sink<getFunc>(* as $limited) // 处理stringbuilder 形式的 字符串构建
$sink+ $appendParams as $sink
$sink?{opcode: param} as $directly
$sink?{!opcode:param} #{include:<<<INCLUDE
*?{opcode:param && <self> & $limited}
INCLUDE
}-> as $indirectly
$directly + $indirectly as $sink
$sink #{
include:`* & $entry`,
}->as $high;
alert $high for {
message: "发现Java代码中存在直接可控的sql注入拼接。",
level: high,
risk: "sqli",
};
用来审计一下试试:
可以看到成功审计出来一批漏洞,正确地识别到了危险的路由。而正确使用参数化的路由就没有筛选出来。
但是会发现还是出现一批安全的路由的信息,详细得去看代码会发现,这些路由是采用得过滤函数得方式保证代码得安全性得,这也是为什么我们没有直接提供sql注入的直接报告规则,实际场景中过滤函数很难通过通用规则表述,但是可以根据不同的项目进行过滤筛选。
本文首发于 Yak Project 公众号,阅读原文。
