Python之条件竞争

0x00 条件竞争

一、漏洞概念

竞争条件指多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间的情形。例如:考虑下面的例子:

假设两个进程P1和P2共享了变量a。在某一执行时刻,P1更新a为1,在另一时刻,P2更新a为2。因此两个任务竞争地写变量a。在这个例子中,竞争的“失败者”(最后更新的进程)决定了变量a的最终值。多个进程并发访问和操作同一数据且执行结果与访问的特定顺序有关,称为竞争条件。

如果程序未做或者为做好线程同步,会造成最终共享变量异常,引起其他安全问题,例如无限制购买、绕过某些次数限制等等。

二、漏洞案例

案例1:辣条购买(2018护网杯题目)

Python之条件竞争_第1张图片

题目如上所示,用户只有20元,只能买4包大辣条,此处我们不要求获取flag,但如何换取一个辣条之王呢?肯定是需要想办法获取另外的一包。(该题目解析详情请见:https://blog.csdn.net/iamsongyu/article/details/83346029)

从开发者角度梳理下购买流程:

  1. 点击购买大辣条
  2. 启动购买线程,从数据库中获取用户余额
  3. 查询余额是否大于商品价格,若大于购买成功
  4. 余额减5
  5. 将剩余余额写入数据库。下次购买再从1开始循环。

若在某时刻购买多个辣条,如果按照上述顺序依次循环执行,是没有问题的。然而如果开发者没有做好线程同步就会产生条件竞争。

问题发生在2-3-5这一过程,余额减5后如果没有及时更新到数据库,存在其它线程此刻获取数据库中的余额数据,使得下次获取的余额不正确。

从条件竞争角度看,其中数据库中的余额为共享变量,多次购买发生在同一时刻可触发多个线程访问共享变量余额。所以可以通过Berpsuit工具同一时刻多线程发送大量购买报文,期望变更后的余额在没有更新入数据库前,而被后续的购买操作获取了非法余额,造成下一次实际余额已经不足但是购买成功。而实际操作是成功的。

所以开发时必须保持2到5这一过程的原子性。

案例2:TOCTOU条件竞争

Linux环境中有如下运行的代码:

int i;
for(i=1;i

一般地进程ruid=euid或者在windows 程序中,即使通过上述间隙,字符串路径被软链接到其它特权文件,open打开是也会报无法访问的错误码。

出现问题的是在具有suid的进程中,此时ruid为真实用户的uid,ruid的用户运行具备suid的程序后,euid被设置为该程序拥有者的权限,如果该程序拥有者为root,则当前ruid用户对应的euid将会变成0,也就是root权限。所以问题就来了,如果程序中对于访问文件的权限希望是通过真实用户的ruid进行判断,这样也是正常业务符合逻辑的,如果符合权限就可以open文件。但是open使用的是euid来判断是否有权限打开文件,所以open的范围将比access的范围要大,如果存在euid能够访问的敏感文件但是不能被ruid访问,导致了前后防御的不一致,也就是违反了韧性中的层层防御原则,没有做到一致的协同保护。(ruid、euid和suid详情请见:https://blog.csdn.net/charles_neil/article/details/79762334),所以会有如下的攻击场景。

如果运行的程序是有suid的root权限,且打开文件时希望使用真实用户的id判断权限。攻击者可以通过在access()open()之间的间隙替换掉原来的文件,如下所示:

程序运行流程

攻击者

access(“/tmp/attack”)

 

 

unlink("/tmp/attack")

 

symlink("/etc/shadow","/tmp/attack")

open(“/tmp/attack”)

 

       上述漏洞就是著名的TOCTOU条件竞争漏洞,英文为:Time of Check,Time of Use。总结该漏洞的原因本质就是:检查和使用之间存在间隙,在此间隙内改变检查所指的对象,是的使用时造成未授权访问。从英文全称也是可以看出的。但是一般需要检查和使用时的权限有差别可能才会造成越权事故。

修正方法思想就是要做到协同保护,确保不同保护机制之间的协调一致, 最大程度减少相互之间干扰、连锁故障与防守空档。

  1. 所以直接使用open去访问,利用open的错误码进行判断是否有权限。这就打破了TOCTOU的流程。
  2. 又因为open使用euid,权限过大,所以通过setresuid(运行条件是当前进程的euid是root)临时将程序降权到ruid。如果此时suid是root

代码如下所示

int i;
int caller_uid = getuid();
int owner_uid = geteuid();

for(i=1;i

 

0x01 Python中的条件竞争

一、模拟条件竞争场景

模拟以下场景:假设有个在线计数器,用于统计用户投票或者点击情况。如下代码所示,通过访问http:// 10.164.XXX.XXX:8000/add/模拟计数功能。代码如下所示(基于Django框架),每次访问开启一个线程对用户的请求做处理。

count = 10

def increment():
    time.sleep(0.1)
    global count
    new_count = count + 1
    count = new_count
    print str(count) + '  end\n'
    return

def incorrectThread(request):
    t1 = threading.Thread(target=increment)
    t1.start()
    return HttpResponse(str(count))

urlpatterns = [
    url(r'^add/$', incorrectThread),
    url(r'^printCount/$', printCount),

]

上述代码increment函数每次给全局计数变量count加1,从代码可以看出没有为count的访问进行线程同步保护,在大量同步访问设置值时会产生条件竞争问题。最终可能造成最后技术小于实际访问的人数。

为了模拟多人同时点击,通过Berpsuit工具首先拦截了请求报文,然后设置999线程同时访问上述URL,若没有条件竞争问题,理论上在999次访问后并加上拦截报文的1次后,count最后的值应为1010。然而实际结果如下所示:

Python之条件竞争_第2张图片

最终结果为1009,与理论值少1。通过上图日志发现在649时发生了条件竞争,count少加了一次。通过查询count的结果发现,count值为非预期值:

Python之条件竞争_第3张图片

 

二、问题修复

上节中的案例可通过python内置threading模块的互斥锁进行防御,代码如下所示:

count = 10
#互斥锁实例
lock = threading.Lock()
def increment():
    global lock
    #acquire请求锁
    if lock.acquire():
        time.sleep(0.1)
        global count
        new_count = count + 1
        count = new_count
        print str(count) + '  end\n'
        # release释放锁,使得其他线程可获取锁
        lock.release()
    return

threading模块中有如下两个方法:

acquire([timeout]): 使线程进入同步阻塞状态,尝试获得锁定。

release(): 释放锁。使用前线程必须已获得锁定,否则将抛出异常。

修改上述代码后,保证了线程同步。进行多次测试,最终的计数结果均是正确的。

Python之条件竞争_第4张图片

 

三、Python线程同步模块

解决条件竞争的方法就是要让存取公共变量的线程进行同步,也就是线程安全,目的是让过程实现原子性。例如读和写某一变量的过程是原子性的,不可分割的,在读写的过程中,其他指令无法插入,等待设置完值后才可以读其中的数据,这就使得每次读取的值就是最新值。一般线程安全可通过锁(互斥所、读写锁等)、条件变量、信号量、事件等方式实现。

Python中进行线程同步的方法见:

https://www.cnblogs.com/huxi/archive/2010/06/26/1765808.html

       该文给出了通过python内置库实现线程同步的方法:thread在python3中改名为_thread和threading。

thread和threading模块都可以用来创建和管理线程,而thread模块提供了基本的线程和锁支持。threading提供的是更高级的完全的线程管理。低级别的thread模块是推荐给高手用,一般应用程序推荐使用更高级的threading模块:

1.它更先进,有完善的线程管理支持,此外,在thread模块的一些属性会和threading模块的这些属性冲突。

2.thread模块有很少的(实际上是一个)同步原语,而threading却有很多。

3.thread模块没有很好的控制,特别当你的进程退出时,

比如:当主线程执行完退出时,其他的线程都会无警告,无保存的死亡,

而threading会允许默认,重要的子线程完成后再退出,它可以特别指定daemon类型的线程。

官方文档见:

Threading:

https://docs.python.org/3/library/threading.html

https://docs.python.org/2.7/library/threading.html

Thread(python2):

https://docs.python.org/2.7/library/thread.html#module-thread

_Thread(python3)

https://docs.python.org/3/library/_thread.html#module-_thread

四、文件访问TOCTOU

Python中也有类似的方法编写和1.2.3小节相同的代码。

Python之条件竞争_第5张图片

1.2.3小节已经讲述了问题的原因,所以根据官方文档分析下这两个函数是否有防守空挡。

官方文档对os.access函数解释如下:

Python之条件竞争_第6张图片

已经说明了存在TOCTOU漏洞,所以Python中的Access和open使用的权限分别也是ruid和euid。

案例可以做成如下:

例如代码文件test.py,linux中设置改程序suidroot,然后转到普通用户运行改程序,最后会输出密码文件信息。此处在代码插入symlink为模拟有其他程序一直尝试在修改字符串链接到敏感文件,如果运气好就可能执行该流程读者可自行尝试下

import os

def fileCompetion():
    path = "/root/ZZYDIR/1.txt"
    if os.access(path, os.R_OK):
        #在access判断权限后变更路径为敏感文件的软链接
        os.unlink(path)
        os.symlink('/etc/passwd', path)
        f = open(path, 'r')
        for i in f:
            print i
        print "success"
    print "error"
fileCompetion()

同时也存在和1.2.3中相同的对于suid为root下临时变更权限的函数 os.setresuid(ruid, euid, suid) 。所以suid为root下也可以使用该函数进行防御。

0x02 安全测试建议

一、发现条件竞争场景

条件竞争从黑盒测试方法较容易发现,代码白盒审计较难发现。所以需要敏感嗅觉发现可能开发者编码不当造成条件竞争的场景。条件竞争问题常见于如下场景,渗透测试这需要关注:

  1. 购买场景:付款/购买/积分/订单操纵(例如1.2.1案例1)
  2. 兑换:积分/优惠券/注册邀请码
  3. 绕过次数、同名限制
  4. 多过程处理,如文件上传处理
  5. 以及其他识别出共享同一资源进行操作的场景

特点总结来说就是——共享同一资源,生成其他结果。该漏洞具有偶现性,很受环境因素的影响,比如网络延迟、服务器的处理能力等,所以只执行一次可能并不会成功,尽量多尝试几次。

二、挖掘方法

如前文大量案例所示,可使用Berpsuit软件的Inturder模块,设置多个线程进行异步发包。通过查看多个异步请求返回的不同结果,比如11个测试中有10个相同,那一个包可能就是攻击成功的请求。

Python之条件竞争_第7张图片

0x03 开发建议

一、对于一般的共享变量保护

  1. 开发者需要识别线程中访问的共享变量
  2. 判断该共享变量是否需要保持同步性
  3. 若需要保持同步,使用Python内置的threading或者thread(python3为_thread)模块的线程同步的相关方法保护共享变量的读写。这些模块包含了大量的同步方法,例如锁(互斥所、读写锁等)、条件变量、信号量、事件等。
  4. 可能由于同步设计和实现的不合理,仍然造成条件竞争,建议开发者通过上述安全测试方法多次进行验证。

二、文件访问TOCTOU

本质思想是检查和使用时采取同样的策略(一般是相同的权限校验机制)。常见的对于suid为root的程序采用上述的os.setresuid(ruid, euid, suid)临时降权。

 

你可能感兴趣的:(web安全)