第六届XCTF决赛部分Writeup
Prologue
这应该是我个人最近最后一场比赛了,整体而言挺开心的,还和诸葛老师合了影(
dngs2010
由于源码里摆明了让我们去选svg,那我们就去选svg。
在返回的页面中,我们能看到我们输入的内容被这样拼接进了html:
1 | <image x="10" y="10" width="100" height="100" |
继续浏览题目,发现选择二进制格式进行生成时除了像素低一点别的都一样,联想到题目中的selenium,不难猜到后段是用chrome渲染svg然后截图。多试几次就能发现,我们的输入位于 /img/
后的url中,程序取最后一个 .
前的内容作为输入,之后的内容作为生成方式。
自然而言,我们就是要打这个selenium了,不管怎么样先得xss,自然而言就得闭合标签。然而这里基本什么过滤都没有,非常舒适。
proof of concept:
1 | print(ses.get('http://172.35.6.36:3000/img/745679136" style="height:0">'+quote(f'''</image> |
result:
1 |
|
这里有两个小细节:
- xml规定一份文档只能有一个根结点,也就是说我们不能闭合svg标签。如果闭合了svg标签,html会报错导致截图不全,且svg闭合后的内容不会被渲染。
- 由于页面没有指定
DOCTYPE HTML
,我们的script标签中不能出现小于号大于号,不然会被识别成xml标签。
第一个问题注意即可,第二个问题我们可以通过 eval(btoa(代码))
的方式进行规避。
之后的流程参考https://paper.seebug.org/1559/,扫描端口,并向webdriver发起请求,创建新的进程,反弹shell即可。文中涉及到跨域访问仅允许localhost客户端访问webdriver的问题在本题中也不存在,因为发起请求的正是localhost。
这道题整体而言对于这样的比赛来说没有难度,但是坑比较多,比如端口量较大,往往扫不到webdriver控制端口,再比如每次请求都启动了新的chromedriver进程,导致端口不一样,所以扫描把人扫得非常沮丧。
1 | window.state = ""; |
WarmupCMS
审计代码,上手搜eval的时候发现有一个很可疑的 function.math.php
,经查阅文档,发现cms并没有自带这个函数,故猜测这个模版函数是出题人自行实现,暂定为sink点。经过刚才的一番查文档,我们也了解到这个cms有模版功能。
我们可以通过数据库文件中的用户md5值在线反查出密码,进入后台 /admincp.php
。
题目中的文章需要进入后台刷新缓存后才能显示
1 | INSERT INTO `icms_user` (`uid`, `gid`, `pid`, `username`, `nickname`, `password`, `gender`, `fans`, `follow`, `comments`, `article`, `favorite`, `credit`, `regip`, `regdate`, `lastloginip`, `lastlogintime`, `hits`, `hits_today`, `hits_yday`, `hits_week`, `hits_month`, `setting`, `type`, `status`) VALUES |
进入后台后不难发现cms作者的本意是不想让我们在网页上直接修改模版,对可以上传的文件后缀的设置也做了限制,作者还是进行了一些河里的思考的。
可惜cms的上传目录可以相对于 $webroot
任意指定,而cms放置模版的目录正位于 $webroot/template
下。我们可以指定任意文件作为主页、文章等页面的模版(如 htm
文件),而 htm
处于上传后缀白名单中。也就是说我们可以将 上传目录
设置为 template
,然后上传一份htm文件,在文件管理中获取到上传的文件名,并将其设置为主页模版,即可利用模版进行RCE。
回到math。虽然函数实现中对危险函数进行了限制,但我随手构造的 <!--{math equation=(system("/readflag"))}-->
恰巧突破了这一限制(带括号)(又貌似是缓存有助攻)。由于是比赛,时间紧迫,便没有深究。
easy_cms
由于题目并没有正确配置php服务器,我们需要通过手动指定controller来访问所有页面。
thinkphp,那我们先来看看有什么controller呗。
admin下的controller由于需要登录:
1 | if(!captcha_check($data['verify'])){ |
而服务端并没有安装图片相关拓展:
所以登陆admin这条路基本是堵死了,也没必要继续看admin controller(当然不排除有些未认证的controller,只是这题确实没有)
在api 的 Base
controller中我们发现有很明显的上传文件的方法,也有读取文件的方法,非常显然是让我们用phar反序列化来加载tp6的链。所以问题就在于如何登陆。我们回头看一眼路由,发现 Base
controller被套了一个 JwtAuth
中间件。这一中间件取 Authorization
请求头的值作为jwt进行验证,认证通过则取token中的uid写入当前session。JwtAuth
调用了 Jwt
类,而生成 Jwt
的 api controller Common
中配置了jwt的参数:
1 | // route.php |
从配置文件中拿到jwt secret,仔细过一遍认证函数,把必要的属性都给加上,再把过期时间 (exp
) 调的久一些,一个jwt就伪造好了
带着这个token,我们就能上传文件了:
1 | host = 'http://172.35.6.101:31337' |
同时,上传文件的 upload
方法下面就有 checkFileExists
方法,可以用于触发 phar 反序列化:
1 | req = ses.get(host, params={ |
tp6的链略,https://lmgtfy.app