目录
1. 什么是业务逻辑漏洞
业务逻辑漏洞是应用程序的设计和实现中的缺陷,攻击者可以利用这些缺陷引发意外行为。这可能使攻击者能够操纵合法功能来实现恶意行为。这些缺陷通常是由于无法预期可能发生的异常应用程序状态,因此无法安全处理它们所导致的。
逻辑漏洞通常很难发现的,因为通常不会在应用程序的正常使用中暴露它们。但是,攻击者可以通过开发人员“意料之外”的方式与应用程序进行交互来攻击利用。
业务逻辑的主要设计意图之一是强制执行设计应用程序或功能时定义的规则和约束。业务规则规定了在特定场景发生时应用程序应如何反应,包括阻止用户会对业务产生负面影响的行为。
逻辑上的缺陷可以使攻击者绕过这些规则。例如,他们可能无需购买即可完成交易。在其他情况下,可能通过将意外的值传递到服务器端逻辑,攻击者有可能诱使应用程序执行不应执行的操作。
基于逻辑的漏洞可能非常多样,并且通常是应用程序及其特定功能所独有的。识别它们通常需要一定数量的专业知识,例如对业务流程的理解或攻击者在给定上下文中可能具有的目标。逻辑漏洞很难使用自动漏洞扫描程序进行检测。
2. 业务逻辑漏洞如何产生
由于开发团队对用户如何与应用程序交互中错误行为的处理,因此经常会出现业务逻辑漏洞。这些错误的行为可能导致用户输入验证不足。例如,如果开发人员规定用户将仅通过Web浏览器传递数据,则应用程序可能完全依赖不安全的客户端来验证输入。攻击者可以使用拦截代理轻松绕过这些漏洞。
最终,这意味着当攻击者输入超出预期的用户行为时,应用程序将无法采取适当的措施来防止这种情况的发生,因此,也就无法安全地处理这种情况。
逻辑漏洞在过于复杂的系统中尤其常见,即使开发团队本身也无法完全理解。为了避免逻辑缺陷,开发人员需要从整体上理解应用程序。这包括了解如何以意想不到的方式组合不同的功能。使用大型代码库的开发人员可能对应用程序的所有区域如何工作没有深入的了解。在一个组件上工作的人可能会对另一组件的工作方式做出错误的假设,结果,无意中引入了严重的逻辑缺陷。如果开发人员未明确记录正在做的任何预期行为,则此类漏洞很容易蔓延到应用程序中。
3. 逻辑漏洞示例
业务逻辑漏洞发生于特定的上下文,因此各个漏洞的特点也不同,主要有以下几个:
3.1 信任不安全的客户端
一个基本的逻辑漏洞是,用户仅仅通过提供的web界面与服务端直接交互,虽然客户端会对用户的恶意输入进行验证,但是攻击者可以通过代理拦截来篡改数据,这些数据是在浏览器发送之后,再发送到服务端,即绕过了客户端的检验,使得客户端检验变得无用。
下面通过100$来购买1337$的商品:
将商品添加到购物车的同时抓包,将商品的price改为1$:
随即可以成功以1$的价格购买:
3.2 未能处理非预测输入
业务逻辑的作用之一就是:将用户输入限制为符合业务规则的值。例如网上购物商品时,用户通常会指定商品数量,理论上任何整数都可以是有效输入,但是业务逻辑规则不允许用户订购商品数量超过库存数量;也不允许接受负数值的商品数量。当然,此类逻辑漏洞不仅仅发生在商品购物上,还可能在银行转账等功能上:
$transferAmount = $_POST['amount'];
$currentBalance = $user->getBalance();
if ($transferAmount <= $currentBalance) {
// Complete the transfer
} else {
// Block the transfer: insufficient funds
}
若没有验证account参数可能存在负值的情况,攻击者可能会利用它来绕过余额检验,并反向转移资金。比如向受害者账户转移了-1000$,这可能会使攻击者从受害者那里获取到1000$
3.2.1 数量可设为负值
将商品添加购物车的同时,抓包将商品数量改为-1:
发现服务端对商品价格进行了验证不小于0:
那么可以加入其它商品的负值数量,使得价格在负数,再将要购买的商品加入购物车,使价格凑够0$以上:
再尝试购买:
3.2.2 整数型溢出
例如c语言中,int型的数值范围是:-2147483647~2147483647
将价格最高的商品加入购物车,商品数量设置为最大数量99,利用Intruder一直重复加入购物车:
这时候可以发现Total价格变成了负数,即发生了整数溢出:
继续增加,负值的价格会逐渐变为正数,慢慢寻找一个临界值,使得价格处于0$附近:
购买即可:
3.2.3 对输入的值处理后未验证
服务端有时候处理的用户异常的输入后,未对处理后的值做有效验证,导致了逻辑漏洞的发生。例如可以通过逻辑漏洞未授权访问页面:
通过 "Target" > "Site map" > "Engagement tools" > "Discover content" 来发现敏感页面/admin:
访问发现页面提示只能由DontWannaCry
类型的用户访问
通过注册功能发现,可以通过DontWannaCry公司的邮箱来
注册一个DontWannaCry
类型的用户,使用以下格式的超长email格式地址注册:
very-long-string@your-email-id.com
very-long-string字符(用户名)尽可能的长:
通过注册邮件激活用户,登录之后发现email长度被截断为256个字符了(原本是262个字符的邮箱域名):
那么就可以利用这个点,将构造一个的邮箱格式如下:
very-long-string@dontwannacry.com.your-email-id.com
利用email过长会被截断,将攻击者自定义的email截断成DontWannaCry类型的邮箱,这要将有效长度控制在256以内:
点击注册后,邮箱 attacker@ac5f1fb21f8ec87c8075313401d5000c.web-security-academy.ne t会收到注册激活邮件:
激活之后,登录就有Admin Panel:
查看其邮箱,发现的确是截断为DontWannaCry类型的用户邮箱了
:
至此就可以利用未校验处理之后的email的权限验证,导致越权的发生。
3.3 篡改或删除参数
web应用程序总是认为用户必须填入相应的字段值或参数。浏览器可能会拦截缺少字段或参数的表单。但攻击者可能会篡改甚至删除传输中的参数,使得可以访问本无法访问的页面。
查找该类逻辑漏洞,可以尝试依次修改/删除每一个参数,并观察对应的响应是否有差异:
- 每次只修改/删除一个字段或参数,观察响应;
- 尝试删除参数名和参数值,服务端通常以不同的方式处理这两种情况;
- 一个多步骤的功能(如重置密码分为几个步骤来验证),在一个步骤中篡改参数会对后面的步骤产生影响
这些存在于GET、POST参数,甚至是Cookie中。
3.3.1 删除参数导致的重置任意用户密码
普通用户登录之后,可以修改自己的密码,在修改密码的POST请求中,发现以下数据包:
username参数值可控,修改为:
username=administrator
发现修改失败,判断可能是会验证当前用户名和密码是否正确后,才进行修改密码的操作。
代码处理过程可能如下:
$username = $_POST['username'];
$current_passwd = $_POSR['current_password'];
$password = $_POSR['new_password'];
if (isset($username) && isset($current_passwd)) {
// 验证username和其密码
if (check($username, $current_passwd)) {
// 验证错误直接return
}
}
// 修改username的密码
modify($username, $password);
那么其中删除current_password参数即可绕过用户名和密码的校验:
成功修改了administrator的密码为666666:
3.3.2 删除参数值导致的重置任意用户密码
在登录界面中,可以通过忘记密码来修改用户的密码,首先输入用户名,会通过一封重置密码的邮件来重置该用户的密码:
点击该链接之后,有如下GET数据包:
之后无需验证旧密码即可修改密码:
修改密码的数据包如下:
发现 temp-forgot-password-token 参数比较特别,和发送修改密码的邮件有一定的关联。试想是否可通过删除该参数值来影响修改任意用户密码的操作呢?
成功绕过验证,重置administrator用户的密码:
3.4 破坏验证流程
许多业务逻辑依赖设定好的工作流程或步骤,web界面会引导用户完成此类流程,每一个步骤之间依赖性可能很强,破坏某一步骤都会导致验证机制失效,攻击者会破坏/跳过某个步骤,利用此类逻辑漏洞绕过验证机制。
此类漏洞一般是没有对每一个步骤作严格的鉴权验证,简单来说,验证第一个登录因子之后就可以跳过验证第二个登录因子了。
3.4.1 跳过验证码步骤
比如,很多网站登录都采用2FA(two-factor authentication)二因子认证,用户在登录用户名和密码之后,还需要输入发送在用户邮箱的验证码,普通用户肯定会遵循输入验证码的步骤,但是攻击者可能直接跳过此步骤,直接访问敏感页面。
登录自己的wiener:peter用户之后,在邮箱中有4位数的验证码code:
验证码登录的数据包:
正常登录之后,访问wiener用户的主页,观察其数据包:
发现id=wiener可控,退出当前用户后,尝试登录受害者账户 carlos:montoya:
在第一步输入账户和密码后,在进行验证码登录验证之前,drop掉验证码的POST /login2 请求包,直接可访问受害者carlos的主页:

3.4.2 步骤之间验证不严格
在功能的每一个步骤流程中,每一步骤之间的验证机制都不完善,换言之,步骤之间没有特定的鉴权机制,只要有正确的数据包就可以通过,并没有验证此数据包是否是该用户发出的。所以对每一步骤先后顺序的调整都可能对验证流程造成影响,
以商品购物为例,正常的购物流程为:
在资金充足的情况下,点击购买商品之后,观察有一个 /cart/checkout 数据包:
验证成功的情况下,响应会重定向到 /cart/order-confirmation?order-confirmed=true ,随机购买成功 :
记录下 /cart/order-confirmation?order-confirmed=true 数据包,
在资金不充足的情况下购买商品,拦截 /cart/checkout 的响应包,将重定向的页面改为成功的URL:
随即购买成功:
事实上,由于步骤的顺序性没有做严格的验证,在将商品加入购物车之后,再Repeater一下购买成功的数据包 /cart/order-confirmation?order-confirmed=true,也可以购买成功购物车中的所有商品:
3.4.3 跳过设置步骤,使用默认配置
例如,一些用户登录之后需要设置自身身份,此时是没有管理员身份的选项的,但是有时候可以通过drop设置的数据包,来使用默认身份,如果Web服务端设置的默认身份为管理员,就可以通过跳过身份设置步骤来使用默认管理员身份。
正常登录普通用户wiener后,出现选择角色:
尝试修改数据中的role=administrator也不行,猜测服务端应该对身份设置做了严格的校验;
那么可以尝试在登录之后直接drop掉GET /role-selector 数据包,从而采用默认身份设置:
重新刷新主页,发现身份变为管理员(默认),可直接访问 /admin:
3.5 特定业务逻辑漏洞
一些特殊的业务逻辑容易发生特殊的逻辑漏洞。
折扣商店的折扣功能是经典的攻击点。举个很简单的例子,对1000$以上的订单,享有10%的折扣。如果业务逻辑在折扣后无法检查订单是否已更改,则可能容易受到滥用。在这种情况下,攻击者可以简单地将商品添加到购物车中,直到达到1000$的门槛,然后在下订单,在下订单之前,将不需要的商品删除。然后,即使他们不再满足折扣的标准,他们也将获得订单的折扣。
3.5.1 重复使用优惠券
现在有两个优惠券:SIGNUP30、NEWCUST5
对一个商品使用两个优惠券之后,再使用会提示优惠券已使用:
但是交替使用优惠券可以绕过检测:
3.5.2 利用优惠折扣差刷金额
如果一个商场有折扣优惠券+商品可兑换为现金的功能,那么可以利用此类功能实现无限刷金额。
首先有一个3$的优惠券,利用它买一个10$礼品卡,购买价格为7$:
礼品卡兑换码 UXByjQJ5Li 可以在用户主页兑换为现金:
兑换之后发现金额多了3$,这个就是从优惠券得来的:
从获取优惠券到兑换现金的数据包依次如下:
POST /cart
POST /cart/coupon
POST /cart/checkout
GET /cart/order-confirmation?order-confirmed=true
POST /gift-card
我们需要自动化地完成这几个步骤,达到无限刷现金的目的。
打开bp,在 "Project options" > "Sessions" > "Session handling rules"中,点击 "Add":
在 "Scope" > "URL Scope" 中选择 "Include All URLs":
在 "Details" > "Rule Actions" > "Add" > "Run a Marco":
在 "Select marco" > "Add" 按顺序依次选择那5个数据包:
这时候我们需要将第4个数据包返回的优惠券码,传递给第5个数据包,以使用优惠券来兑换现金:
然后在第5个数据包中,使用第4个数据包响应包中的gift-card值:
点击 "Test macro",返回码正常:
在开启Project Session > "Session Handling Rules"的情况下,将 /my-account 用 Intruder 不断请求:
刷新下一下账户主页,发现金额在不断增加:
3.6 加解密算法利用
用户可控制加密数据的输入,然后又可以以某种方式使得到密文对应的明文时,这种情况称为“encryption oracle”。攻击者可以使用一个请求来加密任意数据;使用另一个请求来解密加密的数据得到明文。通俗的来说,直接有了加解密的工具(黑盒状态),可以透视任何加密的数据。
例如,在登录中有保持登录状态,并且在响应包中的Cookie中,stay-logged-on值被加密:
这时候是不知道是什么加密算法的,继续看到在发表评论时,写一个错误的email格式:
页面被重定向到 /post?postId=4,且在Cookie中的notification参数值加密方法和stay-logged-on是大致相同的:
继续访问重定向后的页面 /post?postId=4:
明显可以看出,该请求头中Cookie的notification参数值是 /post/comment 响应包中的notification参数值,并且该请求的响应包就是email明文,由此可以推测notification参数值是email的加密后的值。
1. 请求 POST /post/comment 的email参数值格式错误时,email参数值会被加密被作为notification参数返回在响应包中;
2. 请求 GET /post?postId=4 的Cookie中,notification参数值就是 POST /post/comment 响应包的中来的,并且该响应中会包含notification的明文。
总的来说,得出以下结论:
- 请求 POST /post/comment 的email参数,用来加密;
- 请求 GET /post?postId=4 的notification参数,用来解密;
于是,我们可以解密一下stay-logged-on参数值:
发现是 用户名:时间戳 的格式,那么可以改为:
administrator:1602312541466
来尝试越权操作,再利用请求POST /post/comment 来加密一下:
得到notification的值,可以作为Cookie的stay-logged-on来尝试登录为管理员:
失败,验证权限未通过。重新解密一下notification参数值:
观察发现明文的前缀:
Invalid email address:
是固定的,为23个字节长度。每次判断email格式错误之后,都会先加上此前缀再加密,所以要想办法去掉这个前缀才能Cookie登录。
将刚刚的notification用 "URL" > "Base64" 解密,由于前23个字符是固定为:Invalid email address: ,所以在解密后要删除:
删除后再重新用 "Base64" > "URL" 加密:
得到加密后的值,尝试用请求 GET /post?postId=4 来解密,是否能得到明文 administrator:1602312541466
解密失败,报错提示为,输入长度必须为16的倍数;那么可以在明文前面加上9个字符,使其凑够16的倍数,格式如下:
xxxxxxxxxadministrator:1602312541466
重复之前的步骤,加密:
加密得到的值,用 "URL" > "Base64" 解密,并且去掉前23+9个字节(xxxxxxxxx和Invalid email address: ),一共是32个字节:
得到的新密文再用请求 GET /post?postId=4 来解密:
解密成功,g36a96tXlnY/CcvspWvaaO3Lwnpal%2brHILzVjthGh8M%3d 尝试作为Cookie登录管理员:
如何防止业务逻辑漏洞
简而言之,防止业务逻辑漏洞的关键是:
- 确保开发人员和测试人员了解应用程序服务的使用范围
- 避免对用户行为或应用程序其他部分的行为做不必要的处理
这包括在继续操作之前确保任何输入的值都是合理的。
确保开发人员和测试人员都能够充分理解这些预期行为以及应用程序在不同情况下应如何处理也很重要。这可以帮助团队尽早发现逻辑漏洞。为此,开发团队应尽可能遵循以下最佳实践:
- 维护所有事务和工作流的清晰的设计文档和数据流,并注意在每个阶段所做的任何假设。
- 尽可能清晰地编写代码。如果很难理解应该发生什么,那么将很难发现任何逻辑漏洞。在不可避免的复杂情况下,提供清晰的文档对于确保其他开发人员和测试人员处理的预期行为至关重要。
- 注意对使用每个组件的其他代码的任何引用。如果恶意方以异常方式操纵它们,请考虑一下这些依赖关系的任何副作用。