Web 安全:条件竞争漏洞(Race Conditions)
本文参考 PortSwigger Web Security Academy ,笔者对原文进行翻译并调整行文顺序,添加了注释代码及说明见解等内容。
1. 漏洞说明
1.1 概念
条件竞争漏洞属于业务逻辑漏洞,由于多个不同线程同时与相同的数据进行交互,从而导致碰撞冲突(例如,后端多个线程同时修改数据库中的某一个字段值)。攻击者通过精心定制请求到达后端的时间故意制造冲突,并由此实现恶意目的。
可能发生碰撞冲突的时间段称为竞争窗口 (Race Window)。
1.2 原理示例
正常的业务逻辑如下图所示:商城系统中,用户提交折扣码,后端会检测用户是否已经使用过折扣码。若未使用过,则使用该折扣码,并记录折扣码使用状态为True。若已使用折扣码,用户再次使用折扣码时,后端程序读取折扣码使用状态为True,拒绝用户再次使用折扣码。
后端逻辑代码简化如下,处理请求的handle_req()函数是并发(多线程/多进程)执行的:
def handle_req():
if code_already_used = False: # 判断如果没有使用过折扣码(实际应该是对数据库的查询,此处简化写法)
use_code() # 使用折扣码,对商品进行打折
code_already_used = True # 使用过折扣码后,将折扣码使用状态为True(实际应该是对数据库的数据更新,此处简化写法)
else: # 若已经使用过折扣码
reject_use_code() # 拒绝再次使用折扣码
但上述过程中可能存在条件竞争漏洞,试想如下情况:第一个请求发送至后端,上述代码执行到第4行(更新数据库数据)但尚未执行完毕时,第二个同样的请求也发送至后端,后端启动了一个新的进程执行handle_req()函数。新进程执行到第2行代码时,判断code_already_used的状态依然为False,然后也执行了use_code()函数。这样use_code()函数就被执行了两次,也就是使用了两次折扣码。
如下图所示:后端将记录折扣码使用状态为True之前,可能存在条件竞争,上述第2、3行代码执行的时间就是竞争窗口。
竞争窗口一般是一段很短的时间段(几毫秒甚至更短),用户通过并行发送多个数据包,使得服务端竞争窗口内执行多次特定的功能。
2. 如何查找漏洞
按照 选择测试功能点 – 判断是否可能存在碰撞 的步骤,查找潜在的条件竞争漏洞。
2.1 预测潜在碰撞
- 选择测试功能点
可以对订单提交、提现、密码修改、暴力破解限制(重用单个验证码、反暴力破解速率限制)等重要功能点重点进行测试,不涉及重要功能的功能点无需详细测试。
- 判断是否可能存在碰撞
存在条件竞争漏洞的前提条件是:两个或多个请求对同一记录的操作。
如上图所示(设想业务场景中, token 为用户登录依据,后端将 token 存入数据库后,通过 Set-Cookie 响应头发送至客户端)
第一种情况:两个并行请求为两个不同的用户设置 token ,其不可能存在碰撞。
第二种情况:两个并行请求为同一个 sessionid 的用户设置 Token ,这就有可能存在碰撞。
# 第一种情况后端可能执行的SQL,两条语句执行的先后顺序不会对数据库结果产生影响,因为其是对不同记录的操作。
update users set token = "56XXXX" where userid = "hacker";
update users set token = "67XXXX" where userid = "vicitim";
# 第二种情况后端可能执行的SQL,两条语句执行的先后顺序会对数据库结果产生影响,因为其是对同一记录的操作。
update users set userid = "hacker", token = "12XXXX" where sessioinid = "b94";
update users set userid = "victim", token = "12XXXX" where sessioinid = "b94";
2.2 实际测试
确定测试功能点,并猜测功能点可能存在碰撞后,按照 顺序测试 – 并发测试 – 对比测试结果 三个步骤查找漏洞。
- 顺序测试
首先在正常条件下测试功能点:在BurpSuite Repeater中的使用 Send group in sequence (separate connections) 选项发送数据,即将数据包组中的每个数据包通过单个连接按顺序发送。
- 并发测试
发送并发数据包测试功能点:在BurpSuite Repeter中使用 Send group in parallel 选项发送数据,即将数据包组中的每个数据包通过单个连接并行发送,以最大程度上减少网络延迟、抖动带来的影响。
- 对比结果
对比上述两次测试的结果是否存在偏差,并发测试结果中是否有一个或多个响应结果与正常顺序测试不同(HTTP状态码、重定向URL、页面回显结果、页面元素等)。
2.3 测试案例
测试过程分为5个步骤,依次为:选择测试功能点 – 判断是否可能存在碰撞 – 顺序测试 – 并发测试 – 对比测试结果。
- 选择功能点
以 PortSwigger Lab: Limit overrun race conditions 用户折扣码功能点为例进行测试。正常来说用户只能使用一次折扣码,后续再使用折扣码会被系统阻止。登陆系统后添加商品至购物车后,提交折扣码的 HTTP POST 请求体大致如下:
POST /cart/coupon HTTP/2
Host: 0a80002c035ab08d80d49e89005400f7.web-security-academy.net
Cookie: session=vk1ZWBWH6WvPyq3pv7gg9U0AiD9l74dJ
csrf=VxVOsohhcMZ1uLOgQTyDGNjtzUEp3a36&coupon=PROMO20
- 判断是否可能存在碰撞
观察发现请求体中携带折扣码,删除Cookie后,系统要求用户重新登录,多次请求只有首次响应结果为折扣码可用。
由此确定:购物车状态存储在服务端;系统将用户Cookie中的session字段值与用户购物车内容、是否使用折扣码进行了关联,猜测后端数据表格式如下:
sessionid | cart_items | code_already_used |
---|---|---|
vk1ZWBWH6WvPyq3pv7gg9U0AiD9l74dJ | [1,2] | False |
猜测后端示例代码如上述1.2章节所示,其中第四行代码(使用折扣码)对应执行的SQL如下:
# 使用折扣码对应的SQL
update cart_status set code_already_used = True where sessioinid = "vk1ZWBWH6WvPyq3pv7gg9U0AiD9l74dJ";
上述SQL是对同一个对象[sessioinid = “vk1ZWBWH6WvPyq3pv7gg9U0AiD9l74dJ”]进行的操作,因而可能存在碰撞。
- 顺序测试
将提交折扣码的数据包发送至BurpSuite Repeater中,按下Ctrl+R复制多次(建议至少20个数据包),并将这些数据包添加至同一个数据包组中,使用 Send group in sequence (separate connections) 选项,使用单个连接按序发送数据。
发现仅有第一个数据包响应结果为Coupon applied(折扣码应用成功),其他数据包皆为Coupon already applied(折扣码已经使用)
- 并发测试
取消使用折扣码后,在BurpSuite Repeter中使用 Send group in parallel 选项发送数据,即使用单个连接并发发送数据包组中所有的数据包。
发现多个数据包响应结果为Coupon applied(折扣码应用成功)
- 对比结果
上述顺序发送数据包时,只有最初的一个数据包响应结果为Coupon applied(折扣码应用成功),后续数据包皆为Coupon already applied(折扣码已经使用)。但并发发送数据包时,多个数据包响应结果为Coupon applied(折扣码应用成功)。刷新前端页面也发现确实多次使用了折扣码,证明该功能点存在条件竞争漏洞。
3. 漏洞分类
从涉及的API端点或URL数量来区分,条件竞争漏洞可分为单端点条件竞争漏洞和多端点条件竞争漏洞。如果所有竞争请求都发送到同一个端点/URL,那么它是单端点条件竞争。如果涉及多个不同的端点/URL,那么它是多端点条件竞争。
3.1 单端点条件竞争
单端点条件竞争(Single-endpoint race conditions):这种竞争条件涉及单个API端点或web应用的单个URL。当两个或多个请求几乎同时到达同一个端点,并试图修改或访问同一资源时,可能会出现这种竞争条件。
单端点条件竞争可分为超限条件竞争、替换条件竞争等类型。
3.1.1 超限条件竞争
超限条件竞争(Limit overrun race conditions):当多个并发请求试图利用某个特定的限额时,由于没有适当的同步或检查机制,在到达限制之前,可能会有多个请求成功,从而绕过限制。
可能出现漏洞的业务场景如下:
-
在在线银行场景中,假设一个用户的账户只有100$。如果该用户同时发出两个各为100$的提款请求,由于条件竞争,可能会导致两个请求都成功,从而导致账户的超支。
-
在在线商店中,假设一个特定的促销代码只允许使用一次,但由于条件竞争,一个用户可能会在短时间内多次使用它。参考上文2.3示例中的PortSwigger靶场:单端点条件竞争:超限条件竞争绕过使用限制。
-
在API使用场景中,如果一个用户的API调用配额为每分钟100次,但由于条件竞争,他可能在一分钟内发出200个请求,并且所有请求都得到了响应。参考PortSwigger靶场:单端点条件竞争:超限条件竞争绕过速率限制。
3.1.2 替换条件竞争
替换条件竞争:当两个请求几乎同时试图删除或替换同一资源时,由于没有适当的同步或检查机制,可能会扰乱程序执行逻辑顺序,导致出现非预期结果。
可能出现漏洞的业务场景如下:
- 考虑一种密码重置机制,该机制将用户 ID 和重置令牌存储在用户的服务端会话中。在这种情况下,从同一会话发送两个并行密码重置请求,但使用两个不同的用户名,可能会导致以下冲突:服务端会话现在包含受害者的用户 ID,但有效的重置令牌将发送给攻击者。
# 按照上述流程图,实际执行结果如下:
session['reset-user'] = victim # 重置受害者用户的密码
session['reset-token'] = 1234 # 服务端随机生成密码重置token
Send session['reset-token']to hacker # 将密码重置token发送至攻击者邮箱
电子邮件地址确认或任何基于电子邮件的操作通常是单端点条件竞争的良好目标。在服务器向客户端发出 HTTP 响应后,电子邮件通常在后台线程中发送,这使得竞争情况更有可能发生。参考PortSwigger靶场:单端点条件竞争:用户邮箱/密码重置
3.2 多端点条件竞争
多端点条件竞争(Multi-endpoint race conditions):这种竞争条件涉及多个API端点或web应用的多个URL。攻击者可以同时或几乎同时访问多个端点,以便在不应该允许的情况下执行某些操作。
例如,考虑一个在线商店应用,用户可以在一个端点URL上添加物品到购物车,在另一个端点URL上进行结账。如果攻击者可以在结账前快速地多次添加物品,而系统没有适当的同步机制来处理这种情况,那么可能会出现结账金额不正确或其他不一致的行为。
正常业务流程为:向购物车内添加商品后,提交订单,系统会确定订单并完成扣款。
# 购物车添加商品
POST /cart HTTP/2
Host: 0a1200bb037ed54480fa2b2c00bd0057.web-security-academy.net
Cookie: session=n63foVg8cjWNaNbly9or9bx2hutZNKPX
productId=2&redir=PRODUCT&quantity=1
# 提交订单,完成支付
POST /cart/checkout HTTP/2
Host: 0a1200bb037ed54480fa2b2c00bd0057.web-security-academy.net
Cookie: session=n63foVg8cjWNaNbly9or9bx2hutZNKPX
csrf=jdVWF4PFyWVLBKRUJGjrghkV3VXlVbMH
上述两个端点可能存在条件竞争漏洞。若在提交第二个数据包后,系统执行步骤为:完成订单金额计算 – 清空购物车并确认订单。在系统完成订单金额计算后到清空购物车并确认订单之前,可能存在竞争窗口。可以通过并发发送上述两数据包进行测试(可能需要进行多次并发测试才可触发漏洞,可以将向购物车添加商品的数据包复制多份后,再与提交订单的数据包并行发送以提高成功概率)。
测试案例:PortSwigger靶场:多端点条件竞争