app.get("/", (req, res) => { req.query.proxy.includes("nginx") ? res.status(400).send("Access here directly, not via nginx :(") : res.send(`Congratz! You got a flag: ${FLAG}`); });
Option parameterLimit specifies the maximum number of query parameters and the default value is 1000.
The parameter is used in:
1 2 3
// from: https://github.com/ljharb/qs/blob/v6.11.0/lib/parse.js#L54-L55 var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit; var parts = cleanStr.split(options.delimiter, limit);
As you can see, Express ignores parameters after a parameterLimit-th parameter. Thus, if you send a request with more than 1000 query parameters, proxy=nginx is ignored.
from flask import Flask, request, Response import subprocess import os
app = Flask(__name__)
defvalidate(key: str) -> bool: # E.g. key == "{name}" -> True # key == "name" -> False iflen(key) == 0: returnFalse is_valid = True for i, c inenumerate(key): if i == 0: is_valid &= c == "{" elif i == len(key) - 1: is_valid &= c == "}" else: is_valid &= c != "{"and c != "}" return is_valid
deftemplate(text: str, params: dict[str, str]) -> str: # A very simple template engine for key, value in params.items(): ifnot validate(key): returnf"Invalid key: {key}" text = text.replace(key, value) return text
Unfortunately, path traversal to /flag.txt is prevented:
1 2
if".."in filename or"%"in filename: return"Do not try path traversal :("
By the way, curl has a feature of URL globbing, and you can access multiple resources at the same time. You can bypass the above defense using this feature:
1 2 3 4 5 6 7 8 9
$ http "http://localhost:3000/.{.}/.{.}/flag.txt" HTTP/1.1 200 OK Connection: close Content-Length: 10 Content-Type: text/html; charset=utf-8 Date: Sat, 05 Nov 2022 12:09:18 GMT Server: Werkzeug/2.2.2 Python/3.10.8
Try harder
However, the following WAF hides the flag response:
defvalidate(key: str) -> bool: # E.g. key == "{name}" -> True # key == "name" -> False iflen(key) == 0: returnFalse is_valid = True for i, c inenumerate(key): if i == 0: is_valid &= c == "{" elif i == len(key) - 1: is_valid &= c == "}" else: is_valid &= c != "{"and c != "}" return is_valid
deftemplate(text: str, params: dict[str, str]) -> str: # A very simple template engine for key, value in params.items(): ifnot validate(key): returnf"Invalid key: {key}" text = text.replace(key, value) return text
Is it possible to show the flag string without SECCON by abusing this template engine?
The first important point is that validate("{") is True. You can bypass it with this bug and URL globbing.
payload = "" method = req.method path = req.path_info if req.query_string: path += "?" + req.query_string payload += f"{method}{path} HTTP/1.1\r\n" for k, v in req.headers.items(): payload += f"{k}: {v}\r\n" payload += "\r\n"
sock.send(payload.encode()) time.sleep(.3) try: data = sock.recv(4096) body = data.split(b"\r\n\r\n", 1)[1].decode() except (IndexError, TimeoutError) as e: print(e) body = str(e) return body
bff constructs HTTP requests and sends them using socket.
Herein, the process for headers in cherrypy is as follows:
If the first line of a HTTP request is invalid, waitress returns the invalid HTTP method in the body. By abusing this behavior, you can steal the flag from the response body.
There are three points to steal the flag:
HTTP request splitting with CRLF injection
Adjusting a Content-Length value of the first splitted request so that the cookie value is at the position of the HTTP method of the second splitted request
Adding another cookie so that the second request matches with the following regex:
"".match(/^$/) is a process to delete values of the above static properties. If this line does not exist, you can steal the flag from RegExp.input with:
There is a trivial Prototype Pollution vulnerability. Also, unlike usual Prototype Pollution, you can pollute something by methods of some built-in Objects (E.g., Object, String, and Array).
By Prototype Pollution, can you do something in the following parts:
1 2 3 4 5 6 7
if ("{{FLAG}}"in output) { delete output["{{FLAG}}"]; }
In this challenge settings, if there is deno.json in the current directory, the deno command reads it as a config file. This is possible using the Prototype Pollution described in Step 1.
You will notice a interesting property importMap if you check the schema of the configuration:
1 2 3 4 5 6 7
// From: https://deno.land/x/[email protected]/cli/schemas/config-file.v1.json /* snip */ "importMap": { "description": "The location of an import map to be used when resolving modules. If an import map is explicitly specified, it will override this value.", "type": "string" }, /* snip */
Using this property, you can assign https://deno.land/[email protected]/crypto/mod.ts into an arbitrary file. Of course, it includes your JavaScript file served on your server!
Thus, you can do RCE! However, note that there is a permission --allow-write=. and you cannot read the source code.
Step 3: JavaScript Proxy
The goal is to hook any process in the following:
1 2 3
if ("{{FLAG}}"in output) { delete output["{{FLAG}}"]; }
Single Page Application makes our note app simple.
http://spanote.seccon.games:3000
Overview
There is a simple note application.
Create a note:
Delete a note:
The bot accesses a reported URL after creating a note with a flag string.
There is no CSP, but it is seemingly impossible to do XSS
Solution
Step 1: Understanding cache behavior in Google Chrome
Let me get straight to the point, in my solution, you can XSS by abusing cache behavior in Google Chrome. To solve this challenge, you need to have some knowledge of cache behavior (or experiment it).
It stores a resource fetched from the web. The cache doesn't include the JavaScript heap.
The cache is also used for back/forward navigations to skip communication costs.
As a interesting point of disk cache, the cache includes not only the HTTP response rendered to a web page, but also those fetched with fetch. In other words, if you access the URL for a fetched resource, the browser will render the resource on the page.
There is another important point. If both disk cache and bfcache are valid for an accessed page at back/forward navigations, the bfcache has priority over the disk cache. So, it is necessary to have a situation where bfcache is disabled to trigger the above behavior of disk cache.
Step 2: Rendering a fetch response with disk cache
Let's try the interesting behavior in this challenge.
Firstly, you have to disable bfcache[2]. There are many conditions where bfcache is disabled, the list is:
fn.body.clear() ifnot re.fullmatch(r"def \w+\((\w+(, \w+)*)?\):", ast.unparse(fn)): # You must define a function without decorators, type annotations, and so on. returnNone
returnstr(fn.name)
print(""" Latexify as a Service! E.g. `` ` def solve(a, b, c): return (-b + math.sqrt(b**2 - 4*a*c)) / (2*a) `` ` ref. https://github.com/google/latexify_py/blob/v0.1.1/examples/equation.ipynb Input your function (the last line must start with __EOF__): """.strip(), flush=True)
source = "" whileTrue: line = sys.stdin.readline() if line.startswith("__EOF__"): break source += line
name = get_fn_name(source) if name isNone: print("Invalid source") exit(1)
fn.body.clear() ifnot re.fullmatch(r"def \w+\((\w+(, \w+)*)?\):", ast.unparse(fn)): # You must define a function without decorators, type annotations, and so on. returnNone
returnstr(fn.name)
The limitation using AST prevents trivial RCEs.
As a important point, ast.parse ignores comments in the source code. By the way, Python has a feature called magic comment:
# TODO: print the result the above command. # $? == 0 -> It's a text file. # $? != 0 -> It's not a text file. exit 0
There are only three lines! The server executes the script when a player logins with SSH.
Flag location: /flag.txt
Solution
Step 1: Magic file injection
You can specify the arguments of the file command.
man file:
1 2 3 4
-m, --magic-file magicfiles Specify an alternate list of files and directories containing magic. This can be a single item, or a colon-separated list. If a compiled magic file is found alongside a file or directory, it will be used instead.
You can specify a magic file with -m option and some special files (e.g. /dev/tty and /proc/self/fd/0).
However, you cannot get the result of the file command somce the server does not output anything.
Step 2: A time-based attack with ReDoS
man magic:
1 2 3 4
regex A regular expression match in extended POSIX regular expression syntax (like egrep). Regular expressions can take exponential time to process, and their performance is hard to predict, so their use is discouraged. When used in production environments, their performance should be carefully checked. The size of ... snip ...
You can use regex, so you can also do ReDoS! Try a time-based attack with ReDoS.
noise.seed(crypto.randomInt(65536)); const colors = []; for (let y = 0; y < HEIGHT; y++) { for (let x = 0; x < WIDTH; x++) { let v = noise.perlin2(offsetX + x * 0.05, offsetY + y * 0.05); v = (v + 1.0) * 0.5; // [-1, 1] -> [0, 1] colors.push((v * 256) | 0); } }
noise.seed(crypto.randomInt(65536)); const colors = []; for (let y = 0; y < HEIGHT; y++) { for (let x = 0; x < WIDTH; x++) { let v = noise.perlin2(offsetX + x * 0.05, offsetY + y * 0.05); v = (v + 1.0) * 0.5; // [-1, 1] -> [0, 1] colors.push((v * 256) | 0); } }
flagInt/scakeX/scaleY affect only the offsets of the noise. In other words, you may extract flag information from the "position" of a noise.
// 2D Perlin Noise module.perlin2 = function(x, y) { // Find unit grid cell containing point var X = Math.floor(x), Y = Math.floor(y); // Get relative xy coordinates of point within that cell x = x - X; y = y - Y; // Wrap the integer cells at 255 (smaller integer period can be introduced here) X = X & 255; Y = Y & 255;
// Calculate noise contributions from each of the four corners var n00 = gradP[X+perm[Y]].dot2(x, y); var n01 = gradP[X+perm[Y+1]].dot2(x, y-1); var n10 = gradP[X+1+perm[Y]].dot2(x-1, y); var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1);
// Compute the fade curve value for x var u = fade(x);
// Interpolate the four results returnlerp( lerp(n00, n10, u), lerp(n01, n11, u), fade(y)); };
Each gradient gradP is defined on a seed value, and parlin2(x, y) is computed using the gradients of four neighbour lattice points of (x, y). The value is in [−1,1].
Also, each gradient is selected randomly from:
1 2 3
var grad3 = [newGrad(1,1,0),newGrad(-1,1,0),newGrad(1,-1,0),newGrad(-1,-1,0), newGrad(1,0,1),newGrad(-1,0,1),newGrad(1,0,-1),newGrad(-1,0,-1), newGrad(0,1,1),newGrad(0,-1,1),newGrad(0,1,-1),newGrad(0,-1,-1)];
We are considering two dimensions in this challenge, so the candidates of gradients are as follows:
Conversely, it is not true in general at other cases.
Thus, if the size of the interval of x such that each perlin2(x,y0) is 0 with a fixed integer y0 is 1, let x0 be an endpoint of the interval. Then, (x0,y0) is one of the lattice points with high probability.
noise.seed(crypto.randomInt(65536)); const colors = []; for (let y = 0; y < HEIGHT; y++) { for (let x = 0; x < WIDTH; x++) { let v = noise.perlin2(offsetX + x * 0.05, offsetY + y * 0.05); v = (v + 1.0) * 0.5; // [-1, 1] -> [0, 1] colors.push((v * 256) | 0); } }
For noise.perlin2(offsetX + x * 0.05, offsetY + y * 0.05), The offsetX and offsetY contribute only their fractional parts to the position of lattice.
Based on these factors, you can construct an oracle to identify 0/1 for each bit of a flag. Please see the following solver for details.
from concurrent.futures import ThreadPoolExecutor from Crypto.Util.number import long_to_bytes, bytes_to_long from PIL import Image import pwn from io import BytesIO import base64 import os
for _ inrange(10): img = get_image(scale_x, scale_y) # img.save("output.webp") data = list(img.getdata()) assertlen(data) == width*height
for y inrange(0, height, LATTICE_SIZE): cnt = 0 for x inrange(width): color = data[y*width + x][0] ifabs(color - 128) == 0: cnt += 1 else: if0 <= cnt - LATTICE_SIZE < 2: i = (x - cnt - 2) % LATTICE_SIZE return i < LATTICE_SIZE/2 cnt = 0 print("Failed") exit(1)
padded_bit_len = 8*8
flag = 0 with ThreadPoolExecutor(max_workers=8) as executor: bits = executor.map(oracle, range(padded_bit_len, padded_bit_len + flag_bit_len)) for index, bit inenumerate(bits): flag |= bit << index
print(long_to_bytes(flag))
Flag
1
SECCON{p3RLin_W0r1d!}
Because of my lack of consideration, many players solved this challenge by unintended solutions ↩︎
In fact, you can skip this step because bfcache is disabled by default options of puppeteer. ↩︎