介绍

条件竞争漏洞发生在多个线程同时访问同一个共享代码、变量、文件等,但没有进行锁操作或者同步操作的场景中。这个漏洞存在于操作系统、数据库、web等多个层面,像有名的脏牛(dirty cow)。
条件竞争漏洞属于服务器端漏洞,由于大多服务端框架在处理不同用户的请求时是并发进行的,而开发者在进行代码开发时常常倾向于认为代码会以线性的方式执行,而忽视了并行服务器会并发执行多个线程,这就会导致意想不到的结果;

[!tip]

简单来说,就是多线程同时操作一个对象,而没有对对象进行加锁等保证一致性的操作

举例

以python多线程代码举一个不恰当的例子

  1. #!/usr/bin/env python
  2. from concurrent.futures import ThreadPoolExecutor
  3. def test():
  4. global globalNum
  5. if globalNum >= 5:
  6. print("globalNum >= 5 now")
  7. globalNum -= 1
  8. if __name__ == '__main__':
  9. globalNum = 5
  10. pool = ThreadPoolExecutor(max_workers=10)
  11. for _ in range(5):
  12. pool.submit(test)

咋一看,globalNum初始值为5,一个5次的for循环,每次globalNum-1,那么按理说只有第一次调用test函数的时候print会有输出才对,但是我们运行起来看看结果,却有2次print输出
image-20220301152555830
而产生这种结果的原因,就是多线程同时操作变量globalNumglobalNum还没来得及完成修改就被带入到另一个test()函数中,也就是对并发操作的敏感变量没有加锁保护等。

应用场景

[!note]

结合参数在后端的处理过程,所有后端应该对数据进行加锁或者同步的功能点,都可能存在此漏洞,如购买、签到、转账、兑换等

总的来说有如下几类:

  1. 购买/兑换操作
  2. 绕过次数限制
  3. 绕过多过程处理

下面举几个例子抛砖引玉

购买

假设:用户A有100元,要买一件100元的商品
且后端处理流程:判断A的余额是否>=100 ==> A的余额-100,商品数量+1 (没有对A的余额进行加锁操作)
正常情况下,A购买完一件商品余额就清零了,但攻击者通过并发发起20个请求,后端接收到后,也会并发发起20次上述的处理流程,而在同一时间(A的余额-100之前)去判断A的余额是否>=100,肯定都是满足的,那么商品数量就会多次+1,也就达到了100元购买多件100元商品的目的

[!tip]

这个漏洞具有偶现性,很受环境因素的影响,比如网络延迟、服务器的处理能力等,所以只执行一次可能并不会成功,尽量多尝试几次

绕过次数操作

最容易想到的就是绕过签到次数限制,大多数平台都是一天只允许签到一次,如果后端对是否签到的判断不严,那么我们就可以通过并发达到一天签到多次的目的。这里举一个之前类似的挖到的某网盘绕过大小限制的操作。
image-20220301160816851
前提:该云盘普通用户只允许上传10G空间,想要获得更大的空间就需要开会员
猜测后端处理过程:识别上传文件大小 ==> 当前已用空间+上传文件大小是否>=10G ==> <=10G,上传成功 / >10G,上传失败
我的测试过程:怀疑上传过程中,后端可能没有对已用空间做加锁机制来防止并发操作带来的条件竞争问题,因此我先上传了1G的文件,然后50个线程并发复制这1个文件,最终成功上传了超过10G的文件到服务器中。

绕过多过程处理

这个也是大家听说过最多的,常用的场景:上传webshell时,服务端会先存储该文件,然后判断文件内容是否包含恶意内容,如果包含就删除;
我们可以通过并发不停的向服务器上传webshell,虽然服务器会不断的检查并删除我们上传的文件,但由于我们在一直不间断的上传,服务器可能会还没来得及删除webshell,我们就已经执行了相关的命令达到目的了。
天下武功,唯快不破

修复建议

  1. 对于业务端条件竞争的防范,一般的方法是给对象加锁;
  2. 对于文件上传,一定要经过充分完整的检查之后再上传;
  3. 在操作系统的角度,共享数据要进行上锁保护。

    扩展

    前面一直在说加锁加锁,到底什么是加锁?
    线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区代码的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性
    临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。

    参考