跳到内容

注入防御速查表

简介

本文旨在为您的应用程序提供清晰、简单、可操作的指导,以防止所有类别的注入漏洞。注入攻击,尤其是SQL注入,不幸的是非常常见。

应用程序可访问性是保护和预防注入漏洞的一个非常重要因素。公司/企业内所有应用程序中只有少数是内部开发的,而大多数应用程序来自外部。开源应用程序至少提供了修复问题的机会,但闭源应用程序需要不同的方法来处理注入漏洞。

当应用程序将不受信任的数据发送给解释器时,就会发生注入漏洞。注入漏洞非常普遍,尤其是在旧代码中,常见于SQL查询、LDAP查询、XPath查询、OS命令、程序参数等。注入漏洞在代码审查时很容易发现,但通过测试发现则更困难。扫描器和模糊测试工具可以帮助攻击者找到它们。

根据可访问性,必须采取不同的措施来修复它们。最好的方法始终是在源代码本身中修复问题,甚至重新设计应用程序的某些部分。但如果源代码不可用,或者修复旧版软件不经济,那么只有虚拟补丁才有意义。

应用程序类型

公司内部通常可以看到三类应用程序。这三类应用程序需要被识别,以便采取行动防止/修复注入漏洞。

A1: 新应用程序

处于设计阶段或早期开发阶段的新Web应用程序。

A2: 生产性开源应用程序

一个已经投入生产的应用程序,可以轻松进行调整。模型-视图-控制器 (MVC) 类型的应用程序只是具有易于访问的应用程序架构的一个例子。

A3: 生产性闭源应用程序

一个生产性应用程序,无法或只能在困难的情况下进行修改。

注入形式

存在多种形式的注入,针对不同的技术,包括SQL查询、LDAP查询、XPath查询和操作系统命令。

查询语言

最著名的注入形式是SQL注入,攻击者可以修改现有的数据库查询。欲了解更多信息,请参阅SQL注入防御速查表

但LDAP、SOAP、XPath和基于REST的查询也可能受到注入攻击,从而允许数据检索或绕过控制。

SQL 注入

SQL注入攻击是指通过数据输入或从客户端(浏览器)传输到Web应用程序,插入或“注入”部分或完整的SQL查询。

一次成功的SQL注入攻击可以从数据库读取敏感数据,修改数据库数据(插入/更新/删除),执行数据库管理操作(例如关闭DBMS),恢复DBMS文件系统上给定文件的内容或将文件写入文件系统,在某些情况下,甚至可以向操作系统发出命令。SQL注入攻击是一种注入攻击类型,其中SQL命令被注入到数据平面输入中,以影响预定义SQL命令的执行。

SQL注入攻击可分为以下三类

  • 带内: 数据使用与注入SQL代码相同的通道提取。这是最直接的攻击方式,其中检索到的数据直接呈现在应用程序网页中。
  • 带外: 数据通过不同通道检索(例如,生成包含查询结果的电子邮件并发送给测试人员)。
  • 推断性或盲注: 没有实际数据传输,但测试人员能够通过发送特定请求并观察数据库服务器的相应行为来重建信息。
如何测试此问题
代码审查期间

请检查任何数据库查询是否未通过预处理语句完成。

如果正在创建动态语句,请检查数据在使用前是否已进行净化处理。

审计人员应始终查找SQL Server存储过程中sp_execute、execute或exec的使用情况。对于其他供应商的类似功能,需要类似的审计指南。

自动化利用

以下大多数情况和技术都可以使用某些工具以自动化方式执行。在本文中,测试人员可以找到如何使用SQLMap执行自动化审计的信息。

同样,静态代码分析数据流规则可以检测未经净化的用户控制输入是否会改变SQL查询。

存储过程注入

在存储过程中使用动态SQL时,应用程序必须正确净化用户输入,以消除代码注入的风险。如果未净化,用户可能会输入恶意SQL,这些SQL将在存储过程中执行。

时间延迟利用技术

当测试人员遇到盲SQL注入情况时,时间延迟利用技术非常有用,在这种情况下,操作的结果未知。该技术包括发送一个注入的查询,如果条件为真,测试人员可以监测服务器响应所需的时间。如果存在延迟,测试人员可以假定条件查询的结果为真。这种利用技术可能因DBMS而异(请检查DBMS特定部分)。

http://www.example.com/product.php?id=10 AND IF(version() like '5%', sleep(10), 'false'))--

在这个例子中,测试人员正在检查MySql版本是否为5.x,使服务器延迟10秒响应。测试人员可以增加延迟时间并监测响应。测试人员也无需等待响应。有时他们可以设置一个非常高的值(例如100),并在几秒钟后取消请求。

带外利用技术

当测试人员遇到盲SQL注入情况时,该技术非常有用,在这种情况下,操作的结果未知。该技术包括使用DBMS函数执行带外连接,并将注入查询的结果作为请求的一部分发送到测试人员的服务器。与基于错误的技术一样,每个DBMS都有自己的函数。请检查特定DBMS部分。

补救措施
防御选项1: 预处理语句(带参数化查询)

预处理语句确保攻击者无法改变查询的意图,即使攻击者插入了SQL命令。在下面的安全示例中,如果攻击者输入用户ID为tom' or '1'='1,参数化查询将不会受到攻击,而是会查找字面上与整个字符串tom' or '1'='1匹配的用户名。

防御选项2: 存储过程

预处理语句和存储过程的区别在于,存储过程的SQL代码在数据库本身中定义和存储,然后从应用程序中调用。

这两种技术在防止SQL注入方面具有相同的效果,因此您的组织应选择最适合您的方法。存储过程并非总是免受SQL注入攻击。然而,某些标准的存储过程编程结构在安全实现时*具有与使用参数化查询相同的效果,这对于大多数存储过程语言来说是常态。

注意: “安全实现”意味着存储过程不包含任何不安全的动态SQL生成。

防御选项3: 白名单输入验证

SQL查询的各个部分并非绑定变量的合法位置,例如表或列的名称,以及排序顺序指示符(ASC或DESC)。在这种情况下,输入验证或查询重新设计是最合适的防御措施。对于表或列的名称,理想情况下这些值应来自代码,而不是用户参数。

但是,如果用户参数值用于区分表名和列名,那么这些参数值应该映射到合法/预期的表名或列名,以确保未经验证的用户输入不会最终进入查询。请注意,这是设计不佳的症状,如果时间允许,应考虑完全重写。

防御选项4: 转义所有用户提供输入

这种技术只能作为最后手段使用,当上述任何方法都不可行时。输入验证可能是更好的选择,因为这种方法与其他防御措施相比脆弱,我们无法保证它能在所有情况下防止所有SQL注入。

这种技术是在将用户输入放入查询之前对其进行转义。通常只建议在实现输入验证不具成本效益时,对遗留代码进行改造。

示例代码 - Java
安全的Java预处理语句示例

以下代码示例使用PreparedStatement(Java中参数化查询的实现)来执行相同的数据库查询。

// This should REALLY be validated too
String custname = request.getParameter("customerName");
// Perform input validation to detect attacks
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, custname);
ResultSet results = pstmt.executeQuery();

我们展示了Java中的示例,但实际上所有其他语言,包括Cold Fusion和Classic ASP,都支持参数化查询接口。

安全的Java存储过程示例

以下代码示例使用CallableStatement(Java中存储过程接口的实现)来执行相同的数据库查询。sp_getAccountBalance存储过程必须在数据库中预定义,并实现与上述查询相同的功能。

// This should REALLY be validated
String custname = request.getParameter("customerName");
try {
 CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
 cs.setString(1, custname);
 ResultSet results = cs.executeQuery();
 // Result set handling...
} catch (SQLException se) {
 // Logging and error handling...
}

LDAP注入

LDAP注入是一种攻击,用于利用基于用户输入构建LDAP语句的Web应用程序。当应用程序未能正确净化用户输入时,可以通过类似于 SQL注入的技术修改LDAP语句。LDAP注入攻击可能导致授予未经授权的查询权限,以及LDAP树内部的内容修改。有关LDAP注入攻击的更多信息,请访问 LDAP注入

LDAP注入 攻击常见的原因有两个:

  1. 缺乏更安全、参数化的LDAP查询接口
  2. LDAP在用户身份验证中的广泛使用。
如何测试此问题
代码审查期间

请检查任何LDAP查询是否转义了特殊字符,请参阅此处

自动化利用

像OWASP ZAP这样的工具的扫描模块具有检测LDAP注入问题的模块。

补救措施
使用正确的LDAP编码函数转义所有变量

LDAP存储名称的主要方式基于DN( distinguished name, 识别名)。您可以将其视为一个唯一标识符。这些有时用于访问资源,例如用户名。

一个DN可能看起来像这样

cn=Richard Feynman, ou=Physics Department, dc=Caltech, dc=edu

uid=inewton, ou=Mathematics Department, dc=Cambridge, dc=com

在DN中,某些字符被视为特殊字符。完整列表如下:\ # + < > , ; " = 以及前导或尾随空格

每个DN指向且只指向1个条目,这可以被视为关系型数据库(RDBMS)中的一行。对于每个条目,将有1个或多个属性,这些属性类似于RDBMS列。如果您有兴趣在LDAP中搜索具有特定属性的用户,可以使用搜索过滤器。在搜索过滤器中,您可以使用标准布尔逻辑来获取匹配任意约束的用户列表。搜索过滤器采用波兰表示法,也称为前缀表示法。

示例

(&(ou=Physics)(| (manager=cn=Freeman Dyson,ou=Physics,dc=Caltech,dc=edu)
(manager=cn=Albert Einstein,ou=Physics,dc=Princeton,dc=edu) ))

在应用程序代码中构建LDAP查询时,您必须转义添加到任何LDAP查询中的所有不受信任的数据。LDAP转义有两种形式:LDAP搜索编码和LDAP DN(识别名)编码。正确的转义取决于您是正在净化搜索过滤器的输入,还是使用DN作为类似用户名的凭据来访问某些资源。

示例代码 - Java
安全的Java LDAP转义示例
public String escapeDN (String name) {
 //From RFC 2253 and the / character for JNDI
 final char[] META_CHARS = {'+', '"', '<', '>', ';', '/'};
 String escapedStr = new String(name);
 //Backslash is both a Java and an LDAP escape character,
 //so escape it first
 escapedStr = escapedStr.replaceAll("\\\\\\\\","\\\\\\\\");
 //Positional characters - see RFC 2253
 escapedStr = escapedStr.replaceAll("\^#","\\\\\\\\#");
 escapedStr = escapedStr.replaceAll("\^ | $","\\\\\\\\ ");
 for (int i=0 ; i < META_CHARS.length ; i++) {
        escapedStr = escapedStr.replaceAll("\\\\" +
                     META_CHARS[i],"\\\\\\\\" + META_CHARS[i]);
 }
 return escapedStr;
}

请注意,反斜杠字符是Java字符串字面量和正则表达式转义字符。

public String escapeSearchFilter (String filter) {
 //From RFC 2254
 String escapedStr = new String(filter);
 escapedStr = escapedStr.replaceAll("\\\\\\\\","\\\\\\\\5c");
 escapedStr = escapedStr.replaceAll("\\\\\*","\\\\\\\\2a");
 escapedStr = escapedStr.replaceAll("\\\\(","\\\\\\\\28");
 escapedStr = escapedStr.replaceAll("\\\\)","\\\\\\\\29");
 escapedStr = escapedStr.replaceAll("\\\\" +
               Character.toString('\\u0000'), "\\\\\\\\00");
 return escapedStr;
}

XPath注入

待办

脚本语言

Web应用程序中使用的所有脚本语言都有一种eval调用形式,它在运行时接收代码并执行。如果代码是使用未经验证和未转义的用户输入精心制作的,就可能发生代码注入,从而允许攻击者颠覆应用程序逻辑并最终获得本地访问权限。

每次使用脚本语言时,‘高级’脚本语言的实际实现都是使用C等‘低级’语言完成的。如果脚本语言在数据处理代码中存在缺陷,则可以部署‘空字节注入’攻击向量,从而获取内存中其他区域的访问权限,导致攻击成功。

操作系统命令

操作系统命令注入是一种通过Web界面执行Web服务器上操作系统命令的技术。用户通过Web界面提供操作系统命令以执行操作系统命令。

任何未正确净化的Web界面都容易受到此漏洞的影响。通过执行操作系统命令的能力,用户可以上传恶意程序甚至获取密码。在应用程序设计和开发过程中强调安全性,可以防止操作系统命令注入。

如何测试此问题

代码审查期间

检查是否有任何命令执行方法被调用,以及未经验证的用户输入是否被用作该命令的数据。

此外,在URL查询参数的末尾附加一个分号,后跟一个操作系统命令,将执行该命令。%3B是URL编码,解码后为分号。这是因为;被解释为命令分隔符。

示例:http://sensitive/something.php?dir=%3Bcat%20/etc/passwd

如果应用程序响应中包含/etc/passwd文件的输出,那么您就知道攻击成功了。许多Web应用程序扫描器可用于测试此攻击,因为它们会注入各种命令注入变体并测试响应。

同样,静态代码分析工具会检查不受信任的用户输入进入Web应用程序的数据流,并检查数据是否随后进入执行用户输入作为命令的危险方法中。

补救措施

如果认为将用户提供的输入整合到系统命令调用中是不可避免的,那么在软件内部应使用以下两层防御措施以防止攻击:

  1. 参数化 - 如果可用,请使用结构化机制,自动强制数据和命令之间的分离。这些机制有助于提供相关的引用和编码。
  2. 输入验证 - 命令的值和相关参数都应进行验证。对于实际命令及其参数有不同程度的验证。
    • 对于使用的 命令 ,必须根据允许的命令列表进行验证。
    • 对于这些命令使用的 参数 ,应使用以下选项进行验证:
      • 正向或白名单输入验证 - 明确定义允许的参数
      • 白名单正则表达式 - 明确定义允许的良好字符列表和字符串的最大长度。确保元字符(如& | ; $ > < \ \ !`)和空格不属于正则表达式。例如,以下正则表达式只允许小写字母和数字,不包含元字符。长度也限制在3-10个字符。

^[a-z0-9]{3,10}$

示例代码 - Java

不正确用法
ProcessBuilder b = new ProcessBuilder("C:\DoStuff.exe -arg1 -arg2");

在这个例子中,命令和参数作为单个字符串传递,这使得操纵该表达式并注入恶意字符串变得容易。

正确用法

这是一个以修改后的工作目录启动进程的示例。命令和每个参数都是分开传递的。这使得验证每个术语变得容易,并降低了插入恶意字符串的风险。

ProcessBuilder pb = new ProcessBuilder("TrustedCmd", "TrustedArg1", "TrustedArg2");
Map<String, String> env = pb.environment();
pb.directory(new File("TrustedDir"));
Process p = pb.start();

网络协议

Web应用程序通常与网络守护进程(如SMTP、IMAP、FTP)通信,其中用户输入成为通信流的一部分。在这种情况下,可以注入命令序列以滥用已建立的会话。

注入防御规则

规则 #1 (执行适当的输入验证)

执行适当的输入验证。还建议进行正向或白名单输入验证以及适当的规范化,但这并非完全防御,因为许多应用程序的输入中需要特殊字符。

规则 #2 (使用安全的API)

首选选项是使用安全的API,它完全避免使用解释器或提供参数化接口。请注意那些参数化的API,例如存储过程,它们在底层仍然可能引入注入。

规则 #3 (上下文转义用户数据)

如果参数化API不可用,您应该使用该解释器特定的转义语法仔细转义特殊字符。

其他注入速查表

SQL 注入预防备忘单

操作系统命令注入防御备忘单

LDAP 注入防御备忘录

Java 注入预防备忘单