注入类漏洞的危害性无需赘述,究其本质可以追溯到计算机设计之初数据与代码没有分离,没有分离原本没有问题,能用即可,但是当攻击者能够控制输入代码中的数据并且能够作为代码执行时,危害就显现了。从公司的角度来看,注入类的漏洞非常普遍,尤其是在一些老代码中, SQL 查询、LDAP 查询、XPath 查询、操作系统命令、程序参数等中都可能存在,因为通过白盒、安全编码规范都很容易发现注入类的问题,但是大多数老代码是通过安全测试(黑盒)发现同时发现一个整改一个,没有安全的代码review。导致现在还有很多未知的注入类漏洞在一些老代码中。
而要修复这些漏洞,可以根据可访问性的不同,必须采取不同的方式来修复。最好的方式一定是源码级配合架构进行修复,重新进行安全设计,但是由于各种历史遗留问题,可能最终都变成了各种虚拟补丁。
注入形式
注入的本质是一样的,但是由于应用场景的不同(数据库、系统命令等),注入的形式也不同,包括 SQL 、LDAP 、XPath 、XSS、命令行注入等。
注入类漏洞防御的底层逻辑
由注入类漏洞本质与注入类漏洞原理可以看出:
攻击路径是:能输入的数据能够代入到代码中执行(即存在风险)、攻击者能够控制输入的数据(即存在漏洞)、攻击者控制的数据能够代入到代码中执行、攻击者执行后获取了很高的权限(权限分配问题or提权漏洞)、
那么相对应的防守方案即为:不让数据直接代入到代码中执行,针对直接代入的数据进行输入验证、过滤、转义,针对数据的数据做白名单校验,权限最小化。
从而衍生出各类防御的形式:包括参数化查询、输入过滤、输入转义、存储权限控制等等,接下来我会以常见的注入类漏洞做分析,详细拆解注入类漏洞的防御方案,以此验证整体防御逻辑。
SQL 注入
漏洞原理:数据库查询语句被直接代入到数据库中执行并且能够被攻击者控制输入数据
漏洞分类
SQL 注入攻击通过数据承载来看,我个人总结可以分为以下三类:
- 带内:即使用与注入 SQL 代码相同的通道提取数据,检索到的数据直接拉出来
- 带外:指的是攻击者通过非预期的或非传统的通道来检索或传输注入攻击中获取的数据。这里这个带外可能不太常见,我举个例子:
假设一个Web应用程序有一个导出功能,可以将用户数据导出为CSV格式,并通过电子邮件发送给用户:
正常使用:用户请求导出自己的数据,系统将CSV文件发送到用户指定的电子邮件地址。
带外攻击:攻击者通过注入恶意SQL代码,可能使应用程序导出所有用户的数据,并将导出的CSV文件发送到攻击者控制的电子邮件地址。
- 盲注:没有实际的数据传输,但测试人员可以通过发送特定请求并观察 DB 服务器的结果行为来猜。
如何进行安全测试
白盒:
1.检查对数据库的任何查询是否是通过预编译语句完成的。
2.如果是动态语句,需检查在使用作为语句的一部分之前数据是否已进行“数据过滤”。
3.在 SQL Server 存储过程中查找 sp_execute、execute 或 exec 的使用。
自动化利用:
SQLmap+静态代码扫描
手工渗透:
所有能够查数据的点都可能和数据库进行交互,先测有没有数据交互,再测能不能控制数据,再测有哪些防御措施等,这个已经有很多checklist了,这里不再过多说明。
修复
前面提到过注入类漏洞防御的底层逻辑,接下来再以具体的形式进行说明,SQL注入的主要防御方式有以下几种:
1:使用预编译语句(带参数化查询)
2:使用存储过程
3:允许列表输入验证
4:转义所有用户提供的输入
附加防御措施:
- 强制执行最小权限
- 输入验证
不安全示例:
以JAVA举例说明一下SQL注入的形式:它允许攻击者将代码注入到查询中,该查询将由数据库执行。未验证的“customerName”参数简单地附加到查询中,允许攻击者注入他们想要的任何SQL代码:
java
Stringquery ="SELECT account_balance FROM user_data WHERE user_name = "
+request.getParameter("customerName");
try{
Statementstatement =connection.createStatement(...);
ResultSetresults =statement.executeQuery(query);
}
主要防御措施
防御选项1:预编译语句(参数化查询)
使用带变量绑定的预编译语句(也称为参数化查询)是防御SQL注入的最优解。对于开发人员来说,它们易于编写,比动态查询更容易理解。参数化查询强制开发人员首先定义所有SQL代码,然后稍后将每个参数传递给查询。这种编码风格允许数据库区分代码和数据,无论用户提供什么输入。
预编译语句确保即使攻击者插入SQL命令。在下面的安全示例中,如果攻击者输入userID为tom' or '1'='1,参数化查询也不会受到影响,而是会查找字面意义上匹配整个字符串tom'或'1'='1的用户名。
不同语言建议:
- Java EE - 使用PreparedStatement()与绑定变量
- .NET - 使用SqlCommand()或OleDbCommand()与绑定变量的参数化查询
- PHP - 使用PDO与强类型参数化查询(使用bindParam())
- Hibernate - 使用createQuery()与绑定变量(在Hibernate中称为命名参数)
- SQLite - 使用sqlite3_prepare()创建语句对象
安全的Java预编译语句示例:
以下代码示例使用PreparedStatement,Java的参数化查询实现,来执行相同的数据库查询。
java
// 这也真的应该验证
Stringcustname =request.getParameter("customerName");
Stringquery ="SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatementpstmt =connection.prepareStatement(query);
pstmt.setString(1, custname);
ResultSetresults =pstmt.executeQuery();
安全的C# .NET预编译语句示例:
在.NET中,创建和执行查询的过程更简单。你只需使用Parameters.Add()调用将参数传递给查询即可。
csharp
Stringquery ="SELECT account_balance FROM user_data WHERE user_name = ?";
try{
OleDbCommandcommand =newOleDbCommand(query, connection);
command.Parameters.Add(newOleDbParameter("customerName", CustomerName Name.Text));
OleDbDataReaderreader =command.ExecuteReader();
// …
} catch(OleDbExceptionse) {
// 错误处理
}
几乎所有其他语言,包括Cold Fusion和Classic ASP,都支持参数化查询接口。即使是SQL抽象层,如Hibernate查询语言(HQL),也存在相同类型的注入问题(我们称之为HQL注入)。HQL也支持参数化查询:
Hibernate查询语言(HQL)预编译语句(命名参数)示例:
java
// 首先是不安全的HQL语句
QueryunsafeHQLQuery =session.createQuery("from Inventory where productID='"+userSuppliedParameter +"'");
// 这里是使用命名参数的相同查询的安全版本
QuerysafeHQLQuery =session.createQuery("from Inventory where productID=:productid");
safeHQLQuery.setParameter("productid", userSuppliedParameter);
使用 Ruby 内置功能
ruby
insert_new_user =db.prepare "INSERT INTO users (name, age, gender) VALUES (?, ? ,?)"
insert_new_user.execute 'aizatto', '20', 'male'
使用 PHP 与 PHP Data Objects
php
$stmt=$dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);
使用 Cold Fusion 内置功能
coldfusion
<cfquery name = "getFirst" dataSource = "cfsnippets">
SELECT * FROM #strDatabasePrefix#_courses WHERE intCourseID =
<cfqueryparam value = #intCourseID# CFSQLType = "CF_SQL_INTEGER">
</cfquery>
使用 PERL 与 Database Independent Interface
perl
my$sql="INSERT INTO foo (bar, baz) VALUES (?, ?)";
my$sth=$dbh->prepare( $sql);
$sth->execute( $bar, $baz);
预编译不适用场景:
预编译极少会影响性能,但是也会存在不适用的场景,具体包括:
- 静态查询::如果SQL查询完全静态,不包含任何变量或参数,预编译可能不再适用。虽然预编译可以提高性能,但这种情况下直接执行静态查询也是安全的。
- 简单的数据检索:对于非常简单的SELECT查询,如检索公共数据,可能不需要预编译,尤其是当查询不需要复杂逻辑或用户输入时。
- 数据库兼容性问题:一些旧的数据库系统或特定的数据库实现可能不支持预编译语句。
- 性能考虑:在某些高性能场景下,如果查询非常频繁且查询模式固定,预编译语句的准备和执行开销可能