攻击应用程序逻辑

所有Web应用程序都通过逻辑实现各种功能。从根本上讲,用编程语言编写代码就是把一个复杂的进程分解成一些非常简单而又相互独立的逻辑步骤。将一项对人类有用的功能转换成一系列计算机能够执行的细微操作,需要掌握大量的技巧并进行周密的安排。顺利、安全地完成以上任务就更显困难。如果由背景各不相同的开发者与程序员并行开发同一个应用程序,那么在这个过程中可能会发生很多错误。

在所有即使是非常简单的Web应用程序中,每个阶段都会执行数目庞大的逻辑操作。这些逻辑代表着一个复杂的受攻击面,它虽然从未消失,但往往被人们忽略。许多代码审查与渗透测试主要针对常见的“头条”式漏洞,如SQL注入和跨站点脚本,因为它们具有容易辨别的签名,人们对它们的利用方法也进行了广泛的研究。相反,应用程序的逻辑缺陷更难以辨别:每一种缺陷似乎都是唯一的,通常自动漏洞扫描器也无法发现它们。因此,它们并未受到应有的重视与关注,攻击者对之非常感兴趣。

本章将描述各种常见的Web应用程序逻辑缺陷,以及渗透测试员在探查与攻击应用程序逻辑时需要采取的实用步骤。我们将举出一系列实际示例,每个示例说明一种不同的逻辑缺陷,它们共同说明设计者与开发者做出的假设可能会直接导致逻辑缺陷,在应用程序中造成安全漏洞。

逻辑缺陷的本质

Web应用程序中的逻辑缺陷各不相同。它们包括代码中的简单错误,以及几种应用程序核心组件互操作方面的极其复杂的漏洞。有时候,这些缺陷非常明显,很容易发现;但是,有些缺陷可能极其微妙,能够避开最为严格的代码审查或渗透测试。

与SQL注入或跨站点脚本漏洞不同,逻辑缺陷没有共有的“签名”。当然,定义特性是指应用程序执行的逻辑存在某种缺陷。许多时候,逻辑缺陷表现为设计者或开发者在思考过程中做出的特殊假设存在明显或隐含的错误。简单来讲,程序员可能这样认为:“如果发生A,就一定会出现B,因此我执行C。”他们并不会提出截然不同的问题:“如果发生X会怎样?”因而没有考虑到假设以外的情形。在许多情况下,这种错误的假设会造成大量的安全漏洞。

近些年来,人们防范常见Web应用程序漏洞的意识已经增强,一些漏洞的出现几率与严重程度也显著降低。然而,鉴于逻辑缺陷的本质,即使是实施安全开发标准、使用代码审查工具或常规渗透测试,我们仍然无法避免这种缺陷。逻辑缺陷的多样性本质,以及探查与防止它们往往需要从各个不同的角度思考问题,预示着在很长一段时期内,逻辑缺陷仍将大量存在。因此,精明的攻击者会特别注意目标应用程序采用的逻辑方式,设法了解设计者与开发者做出的可能假设,然后考虑如何攻破这些假设。

现实中的逻辑缺陷

掌握理论知识并不是了解逻辑缺陷的最佳办法,通过实例进行学习才是最佳途径。虽然各种逻辑缺陷之间存在巨大的差异,但它们仍包含一些共同特征,并证实开发者总会犯各种各样的错误。因此,从研究逻辑缺陷实例获得的启示有助于攻击者在各种不同的情况下发现新的缺陷。

例1:征求提示

笔者曾在许多不同类型的应用程序中发现“加密提示”漏洞。攻击者可以利用这种漏洞实施各种攻击,如解密打印软件中的域证书或破坏云计算。下面是这种漏洞的一个典型示例,是在一个软件销售站点上发现的。

功能

该应用程序实施“记住我”功能,允许应用程序在浏览器中设置一个永久cookie,用户从而无须登录即可访问应用程序。这个cookie受到一个加密算法的保护,以防止篡改或披露。该算法基于一个由姓名、用户ID和不定数据组成的字符串,以确保合成值是唯一的,并且无法预测。为确保能够访问该cookie的攻击者无法实施重放攻击,应用程序还收集机器专用的数据,包括IP地址。

于是,这个cookie被视为一个可靠的解决方案,用于保护业务功能中易受攻击的部分。

除“记住我”功能外,该应用程序还具有另一项功能,将用户的昵称存储在一个名为ScreenName的cookie中。这样,在用户下次访问该站点时,就可以在站点的角落位置收到个性化的问候。鉴于这个名称也属于安全信息,因此也应对它进行加密。

假设

开发者认为,与RememberMe cookie相比,ScreenName cookie对攻击者而言价值不大,于是他们决定使用相同的加密算法来保护这两个cookie。他们没有考虑到的是,用户可以指定自己的昵称,并在屏幕上查看该名称。这在无意间使用户能够访问用于保护永久身份验证令牌RememberMe的加密功能(及加密密钥)。

攻击方法

在一个简单的攻击中,用户提交其RememberMe cookie的加密值来替代加密的ScreenName cookie。在向用户显示昵称时,应用程序将解密该值,如果解密成功,将在屏幕上显示结果。这个过程生成了如下消息:

虽然这是个有趣的问题,但不一定是个高风险的问题。它只是说明,攻击者可以列出加密的RememberMe cookie的内容,包括用户名、用户ID和IP地址。由于cookie中没有保存密码,攻击者并没有办法对获得的信息立即加以利用。

真正的问题在于,用户能够指定他们的昵称。因此,用户可以选择自己的昵称,例如:

如果用户退出系统然后重新登录,应用程序就会加密这个值,将它作为加密的ScreenName cookie存储在用户的浏览器中。如果攻击者提交这个加密的令牌,将它作为RememberMe cookie的值,应用程序就会解密该cookie,读取用户ID,并让攻击者以管理员身份登录。即使应用程序采用三重DES加密,使用强大的密钥并阻止重放攻击,攻击者仍然可以将应用程序作为“加密提示”,以解密并加密任意值。

渗透测试步骤

这种类型的漏洞表现在许多不同的情况下,包括账户恢复令牌;基于令牌访问经过验证的资源;以及向客户端发送的、需要防篡改或对用户不可读的任何其他值。

(1)在应用程序中查找任何使用加密(而非散列)的位置。确定任何应用程序加密或解密用户提交的值的位置,并尝试替代在应用程序中发现的任何其他加密值。尝试在应用程序中导致可以揭示加密值,或可以在屏幕上“有意”显示加密值的错误。

(2)确定应用程序中可以通过提交加密值导致在响应中显示对应的解密值的位置,以查找“提示提示”漏洞。确定这种漏洞是否会导致敏感信息(如密码或信用卡)被披露。

(3)确定可以通过提交明文值导致应用程序返回对应的加密值的位置,以查找“提示加密”漏洞。确定是否可以通过指定任意值,或应用程序将会处理的恶意有效载荷,对这种漏洞加以利用。

例2:欺骗密码修改功能

我们曾在一家金融服务公司使用的Web应用程序以及AOL AIM企业网关应用程序中发现过这种逻辑缺陷。

功能

应用程序为终端用户提供密码修改功能。它要求用户填写用户名、现有密码、新密码与确认新密码字段。

应用程序还为管理员提供密码修改功能。这项功能允许他们修改任何用户的密码,而不必提交现有密码。这两项功能在同一个服务器端脚本中执行。

假设

应用程序为用户和管理员提供的客户端界面仅有一点不同:在管理员界面中没有用于填写现有密码的字段。当服务器端应用程序处理密码修改请求时,它通过其中是否包含现有密码参数确定请求是来自管理员,还是来自普通用户。换句话说,它认为普通用户总会提交现有密码参数。

负责执行这项功能的代码如下:

攻击方法

一旦确定开发者做出的假设后,逻辑缺陷就变得非常明显。当然,普通用户也可以提交并不包含现有密码参数的请求,因为用户控制着他们提出的请求的每一个方面。

这种逻辑缺陷可能给应用程序造成巨大破坏。攻击者可利用这种缺陷重新设置任何用户的密码,完全控制他们的账户。

渗透测试步骤

(1)在关键功能中探查逻辑缺陷时,尝试轮流删除在请求中提交的每一个参数,包括cookie、查询字符串字段与POST数据项。

(2)既要删除参数名称,也要删除参数值。不要只提交一个空字符串,因为服务器会对这种字符串另做处理。

(3)一次仅攻击一个参数,确保到达应用程序中所有与参数有关的代码路径。

(4)如果控制的请求属于多阶段过程,一定要完成整个过程,因为后面的一些逻辑可能会处理在前面的步骤中提交并在会话中保存的数据。

例3:直接结算

我们曾在一家网上零售商使用的Web应用程序中发现过这种逻辑缺陷。

功能

下订单的过程包括以下步骤。

(1)浏览产品目录并往购物车中添加商品。

(2)返回购物车并最终确认订单。

(3)输入支付信息。

(4)输入交货信息。

假设

开发者认为用户总会按预定的顺序执行每一个步骤,因为这是应用程序通过显示在浏览器中的导航链接和表单向用户提供的处理顺序。因此,开发者认为任何完成订购过程的用户一定已经提交了令人满意的支付信息。

攻击方法

很明显,开发者的假设存在缺陷。用户控制着他们向应用程序提出的每一个请求,因此能够按任何顺序访问订购过程的每一个阶段。如果直接从第(2)步进入第(4)步,攻击者就可生成一个最终确定交货、但实际上并未支付的订单。

渗透测试步骤

发现并利用这种缺陷的技巧叫作强制浏览,包括避开浏览器导航对应用程序功能访问顺序实施的任何控制。

(1)如果一个多阶段过程需要按预定的顺序提交一系列请求,尝试按其他顺序提交这些请求。尝试完全省略某些阶段、几次访问同一个阶段或者推后访问前一个阶段。

(2)这些阶段的结果可通过一系列指向特殊URL的GET或POST请求进行访问,或者需要向同一个URL提交不同的参数。被访问的阶段可通过在被请求的参数中提交功能名称或索引来指定。确保完全了解应用程序访问特殊阶段所使用的机制。

(3)根据执行功能的情形,试图了解开发者做出的假设及主要受攻击面位于何处。设法找到违反这些假设从而在应用程序中造成反常行为的方法。

(4)如果不按顺序访问多阶段功能,应用程序常常表现出一系列异常现象,如变量值为空字符或未被初始化、状态仅部分定义或相互矛盾以及其他无法预料的行为。这时,应用程序可能会返回有用的错误消息与调试结果,可用于充分了解其内部机制并对当前或其他攻击进行优化(请参阅第15章了解相关内容)。有时,应用程序可能会进入一种完全出乎开发者意料的状态,导致严重的安全缺陷。

 注解 许多类型的访问控制漏洞与这种逻辑缺陷类似。如果一项特权功能需要完成几个按预定顺序访问的阶段才能实现处理,应用程序可能认为用户总会按这个顺序处理该项功能。应用程序可能会对这个过程的初始阶段实施严格的访问控制,并认为任何到达后面阶段的用户一定已经获得相关授权。如果一个低权限的用户直接进入了后面的一个阶段,他就能够无限制地访问这个功能。请参阅第8章了解查找并利用这种漏洞的详情。

例4:修改保险单

我们曾在一家金融服务公司使用的Web应用程序中遇到过这种逻辑缺陷。

功能

应用程序为用户提供保险报价,如果需要,用户可在线完成并提交一份保险申请。这个过程包括如下几个阶段。

第一阶段,申请人提交一些基本信息,并指定首选月保费或希望投保的金额。应用程序提供一个报价,同时计算申请人并未指定的其他值。

后几个阶段,申请人提交其他各种个人信息,包括健康状况、职业与爱好。

最后,应用程序连接一名为保险公司工作的保险员。保险员使用该Web应用程序审核申请人提交的信息,并决定是否接受申请,或者修改最初的报价以反映任何额外的风险。

在上述的每一个阶段中,应用程序使用一个共享组件处理用户提交的每一个参数。这个组件将每个POST请求中的所有数据解析成名/值对,并使用收到的数据更新其状态信息。

假设

处理用户提交的数据的组件认为每个请求仅包含用户在相关HTML表单中提交的参数。而开发者并未考虑到这种情形:如果一名用户提交了应用程序并不希望他提交的参数,将会出现什么情况。

攻击方法

上述假设当然存在缺陷,因为用户可在每个请求中提交任意参数与参数值。因此,应用程序的核心功能有许多不完善的地方。

攻击者可以利用共享组件避开所有服务器端的输入确认。在报价过程的每一个阶段,应用程序对这些阶段提交的数据执行严格的确认,并拒绝任何未通过这种确认的数据。但是,共享组件使用用户提交的每一个参数更新应用程序的状态。因此,如果攻击者提供应用程序在较早阶段需要的一个名/值对,不按顺序提交数据,那么应用程序将不对其进行任何确认,直接接受并处理该数据。如果出现这种情况,恶意用户就可以据此实施针对保险员的保存型跨站点脚本攻击,访问属于其他应用程序的个人信息(请参阅第12章了解相关内容)。

攻击者能够以任意价格购买保险。在报价过程的第一阶段,申请人指定他们首选的月保费或希望投保的金额,应用程序据此计算其他值。然而,如果用户在后续某个阶段为上面的一个或几个数据项提交新的值,那么应用程序将根据这些值更新自己的状态。不按顺序提交这些参数,攻击者就可以获得任意价格的保险报价及任意月保费。

应用程序并不对某一类用户能够提交哪些参数实施访问控制。当保险员审核完成的申请时,他们会更新各种数据,包括做出承保决定。这些数据由处理普通用户提交的数据的同一个共享组件处理。如果攻击者知道或猜测出保险员在审查申请时使用的参数名称,就可以提交这些参数,不用签署保单即可接受自己的申请。

渗透测试步骤

这些缺陷可严重危及应用程序的安全,但是,如果攻击者仅拦截浏览器请求并修改被提交的参数值,还是无法确定其中任何一个缺陷。

(1)只要应用程序通过几个阶段执行一项关键操作,就应该提取在某个阶段提交的参数,然后尝试在另一个阶段提交这些参数。如果相关数据随应用程序的状态一起更新,应该探索这种行为的衍生效果,确定是否可以利用它实施任何恶意操作,如前面的3个示例所述。

(2)如果应用程序执行一项功能,不同类型的用户可根据一组共同的数据更新或执行其他操作,应该利用每种类型的用户执行该功能并观察他们提交的参数。如果不同的用户提交不同的参数,就提取由一名用户提交的每个参数,并尝试以其他用户的身份提交这些参数。如果应用程序接受并处理这些参数,如前面所述,探索这种行为的衍生效果。

例5:入侵银行

我们曾在一家大型金融服务公司使用的Web应用程序中遇到过这种逻辑缺陷。

功能

应用程序允许尚未使用在线应用程序的顾客进行注册。然后,应用程序要求新用户提供一些基本的个人信息,在一定程度上确认他们的身份。这些信息包含姓名、地址和出生日期,但并不包括任何机密信息,如现有密码或 PIN 号码。

顾客正确输入这些信息后,应用程序再将注册请求转交给后端系统处理。然后,再向用户注册的家庭地址邮寄一个信息包裹。包裹内含有如何通过给公司呼叫中心拨打电话激活在线访问的指导,以及用户在第一次登录应用程序时使用的一次性密码。

假设

应用程序的设计者认为这种机制可为防止未授权访问提供强大的保护。该机制实施以下3层保护。

应用程序要求用户提前输入一部分个人信息,阻止恶意攻击者或恶作剧用户以其他用户的身份进行注册。

注册过程包括以非常规邮寄的形式向顾客注册的家庭地址传送一些机密信息。要想实施攻击,任何攻击者都必须盗取受害人的个人邮件。

注册功能要求顾客给呼叫中心拨打电话,并根据个人信息与在PIN号码中选择的数字,以常规方式核实他们的身份。

这种设计确实非常安全。但是,该机制的实际执行过程存在逻辑缺陷。

执行注册机制的开发者需要以某种方式保存用户提交的个人信息,并将它们与公司数据库中储存的客户身份关联起来。由于希望重复利用现有代码,他们使用以下这个似乎能够满足要求的类:

获得用户信息后,这个对象被实例化,与提交的信息一起保存在用户会话中。然后,应用程序核对用户信息,如果信息有效,就给该用户分配一个唯一的顾客号码,并将其用在公司的所有系统中。随后,应用程序将这个号码连同用户的其他一些有用信息,一起添加到这个对象中。最后,这个对象被传送至处理注册请求的后端系统进行处理。

开发者认为使用这个代码组件并无妨碍,不会造成任何安全问题。然而,这种错误的假设可能会造成严重的后果。

攻击方法

应用程序的其他功能(包括核心功能)也使用合并到注册功能中的相同代码组件,核心功能允许通过验证的用户访问账户、账目、转账和其他信息。一名注册用户成功通过应用程序的验证后,这个对象也被实例化,并保存在他的会话中,用于存储与其身份有关的关键信息。应用程序的绝大多数功能在执行操作时引用这个对象中保存的信息。例如,应用程序根据保存在这个对象中的唯一顾客号码生成在用户主页面显示的账户信息。

应用程序的其他功能已经使用这个代码组件,意味着开发者的假设存在缺陷,应用程序重复使用它们的方式确实会造成一个巨大的漏洞。

虽然这个漏洞非常严重,但实际上我们很难发现并利用这个漏洞。应用程序的主要功能受到几层访问控制的保护,用户需要拥有一个完全合法的会话才能通过这些控制。因此,为利用这个逻辑缺陷,攻击者需要执行以下步骤。

使用他自己的有效账户证书登录应用程序。

使用登录后得到的通过验证的会话,访问注册功能并提交另一名顾客的个人信息。这样,应用程序就会用一个与目标顾客有关的对象,重写攻击者会话中最初的CCustomer对象。

返回应用程序主要功能并访问其他顾客的账户。

从“黑盒”角度探查应用程序时,这种漏洞并不明显。同时,当审查或编写源代码时,我们也很难发现它。如果未能明确、全面地了解应用程序及其在不同区域使用的各种组件,我们可能无法知道开发者做出的错误假设。当然,添加明确注释的源代码与设计文档也有助于降低引入或探测不到这种缺陷的可能性。

渗透测试步骤

(1)在一个需要隔离水平权限或垂直权限的复杂应用程序中,设法确定个体用户能够在会话中“聚积”大量与其身份有关的状态信息的所有情况。

(2)尝试浏览一个功能区域,然后转换到另一个完全无关的区域,确定任何聚积的状态信息是否会对应用程序的行为造成影响。

例6:规避交易限制

我们在一家制造公司使用的基于Web企业资源规划的应用程序中发现过这种逻辑缺陷。

功能

财务人员有资格在公司拥有的银行账户与公司关键客户和供应商的账户之间进行转账。为防止金融欺诈,应用程序将大多数用户的转账金额限制在10 000美元之内。如果转账金额超出这个限制,就需要得到高级经理的批准。

假设

应用程序中负责金额检查的代码极其简单:

开发者认为这种透明的检查非常安全。如果转账金额超出预先设定的限制,只有得到高级经理的许可交易才能进行。

攻击方法

开发者的假设存在缺陷,因为他完全忽略了用户用负金额进行转账的可能性。由于任何负值都小于转账金额限制,因此不需要得到进一步的批准。但是,应用程序的银行模块接受负值转账,并以反向正值转账的形式对其进行处理。因此,如果用户希望从A账户转账20 000美元到B账户,他只需从B账户转账-20 000美元到A账户,即可得到相同的效果,并且不需要经过批准。应用程序实施的反欺诈防御措施也被轻易避开!

 注解 许多Web应用程序在它们的交易逻辑中采用数字限额,例如:

零售应用程序禁止用户订购超出其库存量的商品;

银行应用程序禁止用户支付超出其当前账户余额的账单;

保险应用程序根据年龄限制调整报价。

找到规避这个限额的方法通常并不表示应用程序存在安全漏洞。但是,这样做会造成严重的商业后果,并表示所有者依赖应用程序实施的控制存在缺陷。

在发布应用程序前执行的用户验收测试过程中,我们通常可以检测出最明显的漏洞。然而,隐藏较深的漏洞依然存在,特别是操纵隐藏参数造成的漏洞。

渗透测试步骤

尝试规避交易限制的第一步是了解受控制的相关输入接受哪些字符。

(1)尝试输入负值,看应用程序是否接受这些值并按预想的方式对它们进行处理。

(2)可能需要执行几步操作,改变应用程序的状态,使其对攻击有用。例如,可能需要在账户之间进行几次转账,直到得到可提取的适当余额。

例7:获得大幅折扣

我们在一家软件供应商的零售应用程序中遇到过这种逻辑缺陷。

功能

应用程序允许用户订购软件产品,如果购买的商品达到一定数量,就有资格获得大幅折扣。例如,如果用户分别购买了一款防病毒解决方案、个人防火墙与防垃圾邮件软件,他就可以获得25%的折扣。

假设

当用户在购物车中增加一件商品时,应用程序就使用各种规则决定他选择购买的产品是否让他有资格获得任何折扣。如果用户可以获得折扣,应用程序就根据折扣率调整购物车中的商品价格。开发者认为用户只有购买捆绑销售的商品,才能获得折扣。

攻击方法

开发者的假设存在相当明显的缺陷,因为该假设忽略了一个事实,即用户向购物车中添加商品后可能会再将其从中移走。狡猾的用户可能会往购物车中添加供应商出售的大量产品,以获得最大可能的折扣。当购物车中的商品可以采用折扣时,他就会把不需要的商品从中取走,而购物车中剩下的商品仍然可以享受原来的折扣。

渗透测试步骤

(1)如果有任何价格或其他敏感价值需要根据用户控制的数据或操作确定的标准进行调整,首先应了解应用程序使用的算法以及需要调整的逻辑。确定这些调整是一次性行为,还是需要根据用户执行的其他操作进行修改。

(2)发挥想象,努力想出操纵应用程序行为的办法,使应用程序进行的调整与开发者最初设定的标准相互矛盾。如前所述,在应用折扣后再从购物车中取出商品就是最典型的示例。

例8:避免转义

我们曾在各种Web应用程序中遇到过这种逻辑缺陷,包括一款网络入侵检测产品使用的Web管理界面。

功能

应用程序的设计者决定执行某种功能,该功能需要以自变量的形式向操作系统命令提交用户控制的输入。应用程序的开发者知道这种操作包含着内在的风险(请参见第9章了解相关内容),并决定净化用户输入中出现的任何潜在的恶意字符,从而防御这种风险。下面的字符都需要使用反斜线(\)进行转义:

以这种方式进行转义后,shell命令解释器就把它们当做提交给被调用命令的自变量的一部分,而非shell元字符。后者可用于注入其他命令或自变量、重定向输出等。

假设

开发者确信,他们设计的方法可有效防御命令注入攻击。他们考虑到了每一个可能被攻击者利用的字符,并确保对它们进行了适当的转义处理,因而它们不会造成风险。

攻击方法

开发者忘记了对转义字符本身进行转义。

通常,攻击者在利用简单的命令注入漏洞时并不直接使用反斜线,因此开发者认为它并非恶意字符。然而,正是由于没有对它进行转义,攻击者就可以完全破坏应用程序的净化机制。

假设攻击者向易受攻击的功能提交以下输入:

如前所述,应用程序对其进行适当的转义处理,因此攻击者的输入变成:

当这个数据作为自变量提交给操作系统命令时,shell 解释器把第一个反斜线作为转义字符,而把第二个反斜线当做字面量反斜线处理;反斜线不是一个转义字符,而是自变量的一部分。然后它遇到分号字符,该字符明显没有进行转义。解释器把分号作为一个命令分隔符,因此继续执行攻击者注入的命令。

渗透测试步骤

当在应用程序中探查命令注入及其他缺陷时,尝试在受控制的数据中插入相关元字符后,接着在每个元字符前插入一个反斜线,测试前面描述的逻辑缺陷。

 注解 一些防御跨站点脚本攻击(请参阅第12章了解相关内容)的措施中也存在这种的逻辑缺陷。将用户提交的输入直接复制到一段JavaScript脚本的字符串变量值中时,这个值包含在引号内。为防御跨站点脚本攻击,许多应用程序使用反斜线对出现在用户输入中的引号进行转义。然而,如果反斜线本身并没有转义,那么攻击者就可以提交\’破坏字符串,从而控制脚本。早先版本的Ruby On Rails 框架的escape_javascript函数中就存在这种漏洞。

例9:避开输入确认

笔者曾在一个电子商务站点的Web应用程序中发现这种逻辑缺陷。许多其他应用程序中也存在类似的缺陷。

功能

该应用程序包含一组输入确认程序,以防范各种类型的攻击。其中的两种防御机制为SQL注入过滤和长度限制。

通常,应用程序对在基于字符串的用户输入中出现的任何单引号进行转义(并拒绝在数字输入中出现的任何单引号),以防范SQL注入。如第9章所述,两个单引号在一起将构成一个转义序列,表示一个原义单引号,数据库会将其解释为引用字符串中的数据,而不是结束字符串的终止符。因此,许多开发者认为,通过将用户提交的输入中的任何单引号配对,就可以防止SQL注入攻击。

长度限制适用于所有输入,确保用户提交的变量不会超过128个字符。如果任何变量超过128个字符,它会将其截短。

假设

从安全的角度来说,SQL注入过滤和长度限制都属于适当的防御机制,因此两种防御机制都应该采用。

攻击方法

SQL注入防御通过将用户输入中的任何引号配对而生效,因此,在每对引号中,第一个引号将作为第二个引号的转义字符。但是,开发者并没有考虑到,如果将经过转义的输入提交给“截短”功能,将会发生什么情况。

回到第9章中的登录功能SQL注入示例。假设应用程序将用户输入中的任何单引号配对,然后对该数据实施长度限制,将其截短为128个字符。如果提交以下用户名:

将导致以下无法避开登录的查询:

但是,如果提交以下用户名(包含127个a后接一个单引号):

应用程序会首先将单引号配对,然后将字符串截短为128个字符,导致输入又恢复其原始值。这时会生成数据库错误,因为在查询中注入了另外一个单引号,而没有纠正周围的语法。此时如果提交密码:

应用程序将执行以下查询,从而成功避开登录:

由a组成的字符串末尾的已配对引号将被解释为转义引号,因而被作为查询数据的一部分。这个字符串将继续有效,直到下一个单引号位置结束,而在原始的查询中,这个位置为用户提交的密码值的开始部分。这样,数据库理解的用户名为如下所示的字符串数据:

因此,之后的任何内容均被解释为查询的一部分,因而可以进行专门设计以破坏查询逻辑。

 提示 不必清楚了解应用程序实施的长度限制,只需轮流提交下面的两个长字符串,并确定是否会生成错误,即可测试这种类型的漏洞:

截短转义输入将在偶数或奇数个字符之后发生。无论是哪一种情况,以上其中一个字符串将导致在查询中插入奇数数量的单引号,从而生成无效的语法。

渗透测试步骤

记下应用程序修改用户输入(特别是截短、删除数据、编码或解码)的任何位置。对于观察到的每一个位置,确定是否可以人为构造恶意字符串。

(1)如果数据已被过滤一次(非递归),确定是否可以提交一个“补偿”过滤操作的字符串。例如,如果应用程序过滤SELECT这个SQL关键字,则可以提交SELSELECTECT,看过滤机制是否会删除其中的SELECT子字符串,而留下SELECT。

(2)如果数据确认按设定的顺序发生,并且有一个或多个确认步骤修改了数据,则确定是否可以将这些步骤用于破坏之前的确认步骤。例如,如果应用程序执行URL编码,然后过滤掉恶意数据(如<script>标签),则可以通过提交以下字符串来避开确认机制:

 注解 跨站点脚本过滤经常会错误地删除HTML标签对之间的所有数据,如<tag1>aaaaa</tag1>。这种行为通常易于受到上述攻击。

例10:滥用搜索功能

我们曾在一个提供基于预订的金融新闻和信息访问的应用程序中发现过这种逻辑缺陷。随后,我们又在两个完全无关的应用程序中遇到相同的漏洞,这表明许多逻辑缺陷既难以捉摸,又广泛存在。

功能

应用程序允许用户访问大量的历史档案与当前信息,包括公司报表与账目、新闻稿、市场分析等。大部分信息只有付费用户才可查阅。

应用程序提供一个功能强大、分类详细的搜索功能,所有用户都可使用这项功能。如果匿名用户执行一项查询,搜索功能将返回所有与查询相匹配的文档链接。然而,如果用户想要查看查询返回的受保护文档的实际内容,就需要付费订阅。应用程序的所有者认为这种行为是一种有用的营销策略。

假设

应用程序的设计者认为,如果用户不付费订阅,就无法使用搜索功能提取任何有用的信息。搜索结果返回的文档标题往往含义模糊,例如,“2010年度报告”、“新闻稿08-03-2011”等。

攻击方法

因为搜索功能指出与某一查询匹配的文档数量,狡猾的用户就可以提交大量查询,并通过推断利用搜索功能提取正常情况下需要付费才能查阅的信息。例如,下面的查询可从一个受保护的文档中提取内容。

虽然用户不能查看文档的具体内容,但通过发挥充分的想象并使用有针对性的请求,他就能够相对清楚地了解文档的内容。

 提示 在某些情况下,能够以这种方式通过搜索功能过滤信息,对应用程序的安全非常重要:它会披露大量与管理功能、密码和釆用的技术有关的信息。

 提示 事实证明,使用这种技巧可对内部文档管理软件实施有效攻击。笔者曾釆用此技巧对存储在维基百科中的配置文件内的关键密码实施过蛮力攻击。由于维基百科会返回提示,说明搜索字符串是否出现在页面的任何位置(而不是匹配整个单词),因此,可以通过搜索以下内容,逐个字母地对密码实施蛮力攻击:

例11:利用调试消息

我们曾在一家金融服务公司使用的Web应用程序中发现过这种逻辑缺陷。

功能

该应用程序最近才开发出来,像许多新软件一样,其中包含大量与功能有关的缺陷。每隔一段时间,应用程序的各种操作就会意外中断,并向用户返回一条错误消息。

为方便错误调查,开发者决定在这些消息中提供详尽的信息,包括:

用户的身份;

当前会话的令牌;

被访问的URL;

在造成错误的请求中提交的所有参数。

提供这些消息对服务台工作人员调查并恢复系统故障非常有用,而且有助于消除剩下的功能缺陷。

假设

尽管安全顾问经常提出警告,称这种详尽的调试消息可能会被攻击者滥用,但开发者仍然认为它们不会造成任何安全漏洞。通过检查浏览器处理的请求与响应,用户就有可能获得调试消息中包含的所有信息。但是,这些消息中并未包含与实际故障有关的任何细节(如栈跟踪),因此无法帮助攻击者向应用程序发动有效攻击。

攻击方法

尽管开发者对调试消息的内容进行了合理保护,但由于他们在创建调试消息时犯下的错误,假设仍然存在缺陷。

当错误发生时,应用程序的一个组件将收集所有必要的信息,并将其保存起来。用户收到一个HTTP重定向,它指向一个显示这些被保存信息的URL。问题在于,在应用程序保存调试信息、用户访问错误消息时,并没有使用会话。相反,调试信息被保存在一个静态容器内,并且错误消息URL总显示最后放入这个容器的信息。因此,开发者认为,使用重定向的用户只会看到与错误有关的调试信息。

实际上,在这种情况下,如果两个错误几乎同时发生,普通用户偶尔会看到与另一名用户造成的错误有关的调试信息。除线程安全问题外(见下一个示例),这并非一个简单的竞态条件。如果攻击者知道错误机制的工作原理,他就可以重复访问消息URL,并记录下所有不同的错误消息。只需短短几个小时,他就可以获得大量应用程序用户的敏感数据:

一组可用在密码猜测攻击中的用户名;

一组可用于劫持会话的会话令牌;

一组用户提交的输入,其中包含密码和其他敏感数据。

因此,错误机制可能会造成严重的安全威胁。由于管理用户有时会收到这类内容详细的错误消息,监控错误消息的攻击者就可以迅速获得足够的信息,从而攻破整个应用程序。

渗透测试步骤

(1)为探查这种缺陷,首先列出应用程序中可能出现的反常事件和条件,以及以非常规方式向浏览器返回有用的用户信息的情况,如返回调试错误消息。

(2)同时以两名用户的名义使用应用程序,使用一名或两名用户系统性地创造每一个条件,并确定另一名用户是否受到影响。

例12:与登录机制竞赛

最近,这种逻辑缺陷给几个大型应用程序造成了严重威胁。

功能

应用程序执行采用一种安全、多阶段的登录机制,要求用户提交几个不同的证书才能获得访问权限。

假设

验证机制接受了大量设计审查与渗透测试。应用程序的所有者确信,攻击者无法向验证机制发动有效攻击,从而获得未授权访问。

攻击方法

实际上,验证机制存在一个细小的缺陷。有时,顾客登录后,他可以访问另外一名用户的账户,查看该用户的所有金融信息,甚至使用其他用户的账户进行支付。最初,应用程序的行为完全是随机性的:在获得未授权访问之前,用户并没有执行任何非法操作;再次登录时,反常现象也不会重复出现。

经过一些调查,银行发现,如果两个不同的用户在同一时间登录,就会出现错误。而且,并不是每次出现这种情况都会发生错误(仅在少数情况下错误才会发生)。发生这种错误的根本原因在于,应用程序将与新近通过验证的用户有关的标识符临时保存在一个静态(非会话)变量中。改写这个值不久后,应用程序再读取这个变量的值。如果在这个过程中有另外一个线程(处理另一个登录)写入到变量中,早先登录的用户就会分配到属于随后登录的用户的会话。

这种漏洞源于与前面错误消息示例中相同的错误:应用程序使用静态存储保存应根据独立线程或会话保存的信息。然而,由于这类错误不会反复出现,当前示例中的缺陷更难发现,也更难对其加以利用。

这种缺陷叫做“竞态条件”,因为其中的漏洞仅在某些特殊情况下才会出现,而且存在时间很短。由于漏洞仅在短时间内存在,攻击者面临着一次“竞赛”,必须赶在应用程序关闭它之前对其加以利用。如果攻击者是应用程序的本地用户,他就有可能知道竞态条件出现的具体情景,并在有效的时间内利用漏洞。如果攻击者属于远程用户,要想实施攻击就比较困难。

如果远程攻击者了解到这种漏洞的本质,那么他就可以通过使用一段脚本连续进行登录,并查看被访问账户的详细资料,从而设计出有效的攻击方法。但是,由于这种漏洞可被利用的时间极短,攻击者可能需要提交数目庞大的请求。

鉴于以上原因,我们在正常渗透测试过程中没有发现竞态条件也就不足为怪了。只有当应用程序的用户数量足够庞大、可导致反常现象(由顾客上报)发生时,这种条件才会出现。然而,如果对验证与会话管理逻辑进行严格的代码审查,还是有可能发现此类问题。

渗透测试步骤

进行远程“黑盒测试”查找这类细微的线程安全问题非常麻烦,应被视为一项专门任务,只有极其注重安全的应用程序才需要进行这种测试。

(1)针对选择的关键功能进行测试,如登录机制、密码修改功能与转账过程。

(2)对每一项测试的功能,确定某位用户在执行一项操作时需要提交一个或少数几个请求。同时,找到确定操作结果的最简单方法,例如,核实用户登录后是否能够查看他们自己的账户信息。

(3)使用几台高规格的机器,从不同的网络位置访问应用程序,写出一段攻击脚本,代表几名不同的用户反复执行相同的操作。确定每项操作是否达到预期的结果。

(4)为接收大量错误警报做好准备。根据为应用程序提供支持的基础架构的规模,可能需要对安装的软件进行负载测试。有时,反常现象可能与安全无关。

避免逻辑缺陷

就像无法通过明确的特征确定Web应用程序中存在的逻辑缺陷一样,同样也没有能够保护应用程序的万能防御措施。例如,虽然无法找到安全的方法替代危险的API,但是,下面的一系列最佳实践可显著降低在应用程序中出现逻辑缺陷造成的风险。

确保将应用程序各方面的设计信息清楚、详细地记录在文档中,以方便其他人了解设计者做出的每个假设。同时将所有这些假设明确记录在设计文档中。

要求所有源代码提供清楚的注释,包括以下信息:

每个代码组件的用途和预计用法;

每个组件对它无法直接控制的内容做出的假设;

利用组件的所有客户端代码引用,清楚记录它的效果有助于阻止在线注册功能中的逻辑缺陷。(注意:这里的“客户端”不是指客户端一服务器关系中的用户,而是指组件主要依赖的代码。)

在以安全为中心的应用程序设计审核中,考虑在设计过程中做出的每一个假设,并想象假设被违背的每种情况。尤其应注意任何应用程序用户可完全控制的假定条件。

在以安全为中心的代码审查中,从各个角度考虑以下两个因素:应用程序如何处理用户的反常行为和输入;不同代码组件与应用程序功能之间的相互依赖和互操作可能造成的不利影响。

我们可从本章描述的特殊逻辑缺陷实例中汲取以下教训。

始终记住,用户可以控制请求每一个方面的内容(请参阅第1章了解相关内容)。他们可以按任何顺序访问多阶段功能;他们可以提交应用程序并未要求的参数;他们可以完全省略某些参数,而不仅仅是篡改参数值。

根据会话确定用户的身份与权限(请参阅第8章了解相关内容)。不要根据请求的任何其他特性对用户的权限做出任何假设。

当根据用户提交的数据或者用户执行的操作更新会话数据时,仔细考虑更新后的数据可能会给应用程序的其他功能造成什么影响。注意,这些数据可能会给由其他程序员或其他开发团队编写的完全无关的功能造成意想不到的不利影响。

如果一项搜索功能可用于查询禁止某些用户访问的敏感数据,确保那些用户无法利用该项功能、根据搜索结果推断出有用的信息。如果可以,根据不同的用户权限保留几个搜索索引(search index),或者使用当前用户的权限进行动态信息搜索。

如果一项功能允许用户从审计追踪中删除任何记录,在使用该项功能时应特别小心。另外,在大量使用审计的应用程序与双重授权模型中,考虑一名高级权限用户创建另一个相同权限的用户可能造成的影响。

如果应用程序根据数字交易限额执行检查,在处理用户输入前,必须对所有数据实施严格的规范化与数据确认。如果没有考虑到使用负数的情况,应立即拒绝包含负数的请求。

如果应用程序根据订购商品的数量决定折扣,必须保证在实际应用折扣前确定订单。

如果在将用户提交的数据提交给可能易于受到攻击的应用程序组件前,对其进行转义处理,一定要记得对转义字符本身进行转义,否则整个确认机制可能会遭到破坏。

始终使用适当的存储方法保存与某位用户有关的数据(或者保存在会话中,或者保存在用户资料中)。

小结

当攻击应用程序的逻辑缺陷时,渗透测试员既要进行系统性地探查,也要从不同的角度思考问题。如前所述,渗透测试员应该始终执行各种关键检查以确定应用程序在收到反常输入后的行为。这类反常输入包括从请求中删除参数、使用强制浏览不按预定顺序访问功能,以及向应用程序的不同位置提交参数。通常,应用程序响应这些操作的方式会反应出一些存在缺陷的假设,不做这些假设就可以避免造成不良后果。

除了这些基本的测试外,在探查逻辑缺陷时面临的最大挑战,是如何深入了解开发者的思维方式。需要了解他们想要达到什么目的、可能会做出什么假设、可能采用哪些捷径、将会犯下什么错误。想象一下,假设完工的最后期限临近,但还主要担心功能而非安全,试图在现有代码中增加一项新功能,或者需要使用其他人编写的质量不佳的API。在那样的情况下,开发者可能会犯什么错误?如何利用这些错误?

问题

欲知问题答案,请访问http://mdsec.net/wahh。

(1)何为强制浏览?可以通过它确定哪些漏洞?

(2)为防止不同类型的攻击,应用程序对用户输入实施各种全局过滤。为防止SQL注入,它将出现在用户输入中的单引号配对。为防止针对一些本地代码组件的缓冲区溢出攻击,它将超长的数据截短到适当的长度。这些过滤有什么问题?

(3)可以采取哪些步骤来探查某登录功能中是否存在故障开放条件?(列出想到的各种不同的测试。)

(4)某银行应用程序采用一种非常安全可靠的多阶段登录机制。在第一个阶段,用户输入用户名和密码。在第二个阶段,用户输入在物理令牌上显示的一个不断变化的值,并通过一个隐藏表单字段重新提交前面输入的用户名。

可以立即发现的逻辑缺陷有哪些?

(5)在通过提交专门设计的输入探查一个应用程序中是否存在常见的漏洞时,应用程序频繁返回包含调试信息的详细错误消息。有时,这些消息与其他用户造成的错误有关。这种情况后,就无法令其再次发生。这表示应用程序存在什么逻辑缺陷,接下来该如何处理?

浙ICP备11005866号-12