许多类型的Web应用程序漏洞在代码中都有相对一致的签名;通常,这表示通过迅速扫描和搜索代码就可以确定一个应用程序中存在的大部分漏洞。下面列举的示例出现在各种语言中,但在大多数情况下,签名是不区分语言的。最重要的是程序员采用的编程技巧,而不是实际使用的API和语法。
在非常明显的XSS漏洞中,用户收到的HTML代码的一部分明显是由用户可控制的数据构成的。在下面的代码中,HREF链接的目标即由从请求查询字符串中直接提取的字符串构成:
这时,对潜在恶意的内容进行HTML编码这种阻止跨站点脚本的常用方法,并不适用于生成的串联字符串,因为它已经包含有效的HTML标记;任何净化数据的尝试都会按应用程序指定的方式对HTML编码,从而中断应用程序。因此,这个示例肯定易于受到攻击,除非在其他地方实施了过滤,阻止了在查询字符串中包含XSS的请求。这种使用过滤来阻止XSS攻击的方法往往存在缺陷,如果采用,应对它进行仔细审查,以确定解决办法(请参阅第12章了解相关内容)。
在更加微妙的情况下,用户可控制的数据用来指定随后用于创建发送给用户响应的一个变量值。在下面的示例中,类成员变量m_pageTitle被设定为从请求查询字符串中提取的一个值,随后将用于创建被返回的HTML页面的<title>元素:
当遇到这种代码时,渗透测试员应该仔细审查应用程序对m_pageTitle变量的处理过程,以及它是如何被合并到被返回的页面中的,以确定数据是否为防止XSS攻击而进行了适当的编码。
前面的示例明确证明代码审查在查找一些漏洞时的重要作用。XSS漏洞只有在一个不同的参数(type)指定一个特殊的值(3)时才会触发。标准的模糊测试与对相关请求进行漏洞扫描都无法发现这种漏洞。
如果各种硬编码的字符串与用户可控制的数据串联成一个SQL查询,然后在数据库内执行这个查询,那么最可能出现SQL注入漏洞。在下面的代码中,查询由直接从请求查询字符串中提取的数据构成:
在源代码中搜索常用于从用户提交的输入构建查询的硬编码子字符串,是在代码中迅速确定这种明显漏洞的简单方法。这些子字符串通常由SQL代码片断组成,并被源代码引用;因此,寻找由引号、SQL关键字和空格组成的适当模式可能会有用。例如:
在每一种情况中,应该核实将这些字符串与用户可控制的数据串联是否会引入SQL注入漏洞。因为SQL关键字区分大小写,所以在代码中搜索这些关键字时也应区分大小写。请注意,为减少错误警报,这些搜索项后可能附加了空格。
用户可控制的输入未经任何输入确认,或者核实已经选择一个适当的文件,就被传送给一个文件系统API,这是路径遍历漏洞的常见签名。在最常见的情况下,用户数据附加在一个硬编码或系统指定的目录路径之后,让攻击者能够使用点-点-斜线建立目录树,访问其他目录中的文件。例如:
应对任何允许用户上传或下载文件的应用程序功能进行仔细审查,了解它是如何根据用户提交的数据调用文件系统API的,并确定是否可以使用专门设计的输入访问其他位置的文件。通常,通过在代码中搜索任何与文件名有关的查询字符串参数(在本例中为AttachName),以及在相关语言中搜寻所有文件API并检查提交它们的参数,就可以迅速确定相关的功能。(常用语言中的相关API列表见后文。)
通过源代码中的签名常可轻易确定各种钓鱼攻击向量,如任意重定向。在下面这个示例中,查询字符串中用户提交的数据被用于构建一个URL,用户即被重定向到这个URL:
通常,检查客户端代码即可发现任意重定向漏洞;当然,这并不需要了解应用程序的内部机制。在下面的示例中,使用JavaScript从URL查询字符串中提取一个参数,并最终重定向到这个URL:
可见,这段脚本的作者已意识到,该脚本可能会成为指向外部域的一个绝对URL的重定向攻击的目标。该脚本检查重定向URL是否包含一个双斜线(像http://中一样),如果包含,就省略第一个单斜线,将它转换成一个相对URL。但是,当它最后一次调用unescape()函数时,该函数将对任何URL编码的字符进行解码。确认后再执行规范化常常会导致漏洞产生(请参阅第2章了解相关内容)。在这个示例中,攻击者可以使用以下查询字符串造成一个指向任意绝对URL的重定向:
连接到外部系统的代码中常常包含指示代码注入漏洞的签名。在下面的示例中,message与address参数从用户可控制的表单数据中提取出来,然后直接传送给UNIX system API:
除非被恶意程序员有意隐藏,否则,当审查证书确认逻辑时,用于测试或管理目的的后门密码非常容易确定。例如:
同样,使用这种方法还可以轻易确定未引用的函数与隐藏调试参数。
应对应用程序使用的任何本地代码进行仔细审查,确定可被攻击者用于执行任意代码的常见漏洞。
1.缓冲区溢出漏洞
这些漏洞常常使用一个未经检查的API实现对缓冲区的操控。这些API数量众多,包括strcpy、strcat、memcpy与sprintf以及它们的wide-char和其他变体。确定代码中明显的缓 冲区溢出漏洞的一种简单方法是,搜索所有这些API的用法,并检实来源缓冲区是否可由用户控制,以及代码是否已经明确确定目标缓冲区足够大,能够容纳被复制到它里面的数据(因为API不会这样做)。
我们可以轻易确定以易受攻击的方式调用危险API的做法。在下面的示例中,用户可控制的字符串pszName被复制到一个固定大小的栈缓冲区中,但之前并没有检查该缓冲区是否足以容纳这些字符串:
请注意,以一个安全的API替代未经检查的API,这种方法并不能保证缓冲区溢出不会发生。有时,由于错误或误解,一个经过检查的API以危险的方式被使用,以前面漏洞的“修复代码”为例:
因此,要在代码中彻底搜索缓冲区溢出漏洞,必须对整个代码进行仔细地逐行审查,追踪对用户可控制的数据执行的每一项操作。
2.整数漏洞
这些漏洞的表现形式各异,而且非常难以检测;但有时通过源代码中的签名却可立即确定这类漏洞。
比较有符号与无符号的整数时经常会出现问题。以下代码中,上一个漏洞的“修复代码”对有符号的整数(len)与无符号的整数(sizeof(strFileName))进行比较。如果用户能够使len为负值,这个比较就会成功进行,而且未经检查的strcpy仍会运行:
3.格式化字符串漏洞
通常,通过检查printf与FormatMessage系列函数的用法,如果发现格式化字符串参数并未硬编码,而是由用户控制,就可以立即确定这类漏洞。以下面这段调用fprintf函数的代码为例:
许多软件漏洞可以从源代码注释中发现。如果开发者意识到某项操作存在危险,并在代码中标注提示,准备以后修复这个问题,但却从未着手修复,这时就会出现以上情况。另外,如果测试时确定应用程序存在某种反常行为,并将其记录在注释中,但同样从未对这种行为进行全面调查,这时也会出现上面的情况。例如,我们曾在一个应用程序的生产代码中遇到以下代码:
在代码中搜索说明常见问题的注释,往往可以迅速发现许多明显的漏洞。下面是一些已证明有用的搜索项:
bug
problem
bad
hope
todo
fix
overflow
crash
inject
xss
trust