CTF特训营:技术详解、解题方法与竞赛技巧
上QQ阅读APP看书,第一时间看更新

7.2 条件竞争问题分析及测试

接下来将分别从因一般代码逻辑问题引发的条件竞争和因数据库无锁引发的条件竞争来分析条件竞争问题及相关测试方法。

1.一般代码逻辑引发的条件竞争

对于不涉及数据库的这一类问题,其主要原因在于服务端的代码对于共享资源的处理存在问题,首先来看一个较为简单的例子,示例代码如下:


<?php
    $cnt=file_get_contents("count.txt");
    //count.txt的初始内容为0
    $cnt+=1;
    echo "This site was visited $cnt times.";
    file_put_contents("count.txt",$cnt);
?>

在这个例子中,我们想要实现的功能就是记录这个网站页面被访问了多少次。假设每一次用户请求,服务端都应将count.txt中的数值加1,用户得到的输出内容每次都应该与实际请求次数一致,但是这段代码因为条件竞争的原因很可能没办法对访问次数进行准确记录,从而导致用户得到的输出内容与实际请求次数不一致。

如果存在两个用户同时访问,就会造成count.txt里面的数值本应该增加2,却只增加了1的情况,如图7-1所示。

到这里,大家已经不难窥见条件竞争漏洞的原理了,接下来以样例代码为例,介绍一下常见的条件竞争漏洞的测试方法。

图7-1 多用户同时访问形成的条件竞争

首先,我们利用前面介绍的Burp的Intruder模块进行测试,截取数据包之后进行配置,在Payloads一栏配置访问总次数为1000,如图7-2所示。

图7-2 设置访问数量为1000次

然后,在Options一栏配置并发线程数为80(如图7-3所示),当然这个数目越大就越容易触发条件竞争,但是数目过大可能会对服务器造成一定负担。

图7-3 设置线程数量为80

我们期望最后结果的值应该是1000次,但是从实验结果可以发现,1000个访问请求最后只记录了786次,如图7-4所示。

图7-4 Burp测试条件竞争的结果

接下来,我们用一个简短的Python脚本来进行测试,测试代码如下:


import requests
import threading
import Queue
url = "http://example.com/count.php"

requests_time = 0
message_queue = Queue.Queue()
stop = 0

def output():
    global message_queue,stop
    while stop!=1 or message_queue.empty()!=True:
        try:
            msg = message_queue.get()
        except:
            continue
        print msg

def request():
    global requests_time,message_queue
    while requests_time < 1000:
        message_queue.put(requests.get(url).content)
        requests_time += 1

Thread_count = 80
threading.Thread(target=output).start()
for i in xrange(Thread_count):
    t = threading.Thread(target=request, args=())
    t.start()
stop = 1

需要注意的是,这里设置了requests_time<1000,但是总共发送的请求数量肯定会超过1000,原因也是因为条件竞争。

由图7-5可以看出,我们发起了超过1000次的访问,但是输出结果的最大值远远不够1000。

图7-5 Python测试条件竞争的结果

以上就是两种较为常用的条件竞争测试方法(Brup测试和Python脚本测试),当然还有很多其他可以快速实现并发的编程语言(如Golang等)也可用来进行测试,有兴趣的读者可以自行研究。

2.数据库无锁机制引发的条件竞争

在说明之前,这里需要简单介绍下数据库的锁机制。在MySQL数据库中,不同的存储引擎支持不同的锁机制,以InnoDB存储引擎为例,InnoDB中常用的有两种行级锁(对单行数据加锁):共享锁(读锁)和排他锁(写锁)。

两种锁实现的语法不同,其功能自然也不同。共享锁,顾名思义,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改;排他锁,即一个事务获取了一个数据行的锁,其他事务就不能再获取该行的其他锁(排他锁或者共享锁)了,也就是说,一个事务在读取一个数据行时,其他事务不能对该数据行进行增、删、改查操作。

对于update、insert、delete语句,InnoDB会自动给设计的数据集添加排他锁。对于普通的select语句,InnoDB不会加任何锁。以下是select语句设置共享锁和排他锁的方式:


SELECT ... LOCK IN SHARE MODE;        //设置共享锁
SELECT ... FOR UPDATE;                //设置排他锁

但是,我们在编写服务端程序与数据库进行交互的时候,出于对一些原因的考虑,并不会对select语句加锁,而这通常会成为我们成功利用条件竞争漏洞的关键点。接下来,我们来看看概述里面所列举的例子,如图7-6所示。

图7-6 数据库查询语句未加锁导致的条件竞争

讲到这里,相信大家已经基本能够理解其中的原因了,由于查询数据库的select语句无锁,即当多个线程同时访问时均能获取到结果,从而导致我们能够利用条件竞争漏洞。0CTF 2017中有一道题目便与我们所列举的这个例子类似,具体将在8.3节的实例中进行详细介绍。