SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
B3S/WRITEUPS/GASLIGHTCTF-2026-JSBOX-WRITEUP
← BACK TO ARCHIVE
EVENT: gaslightCTF 2026CATEGORY: MiscPOINTS: 500 PTS

GasLightCTF 2026 - Jsbox Writeup

AUTHORED BY:@bealthguy8/16/2026

jsbox — gaslightCTF 2026 writeup

Category: misc (100) Author: sportshead Solves: 201 Service: ncat --ssl <instance>.play.gaslightctf.cooking 31337 Flag: gaslightCTF{n4t1v3_c0d3_0r_n4t1v3_fl4g?_0142da7d4c7e}

"I created a black box for you to guess the flag. It's using secure comparison, so don't bother with a side channel!"

TL;DR

The service is a node REPL that evaluates JS inside an isolated-vm sandbox exposing a single blackbox(guess) function. blackbox compares the guess against the real flag using a 10M-iteration loop ("secure comparison" = no timing side channel) and returns a boolean.

blackbox.toString() is overridden to return 'nice try', but the override lives on Function.prototype.toString, so calling it via Function.prototype.toString.call(blackbox) bypasses the sandbox's tampering and returns the actual function source — which hardcodes the flag as a const.

Interaction

Connect with pwntools over TLS:

from pwn import *
r = remote(class="hljs-string">&#039;<instance>.play.gaslightctf.cooking&#039;, 31337, ssl=True)

The banner is blackbox("is this the flag?") with a > REPL prompt. We evaluated arbitrary expressions in the sandbox:

Expression Result
typeof blackbox 'function'
blackbox("") false
blackbox(1) TypeError: flag must be a string (at <isolated-vm>:5:11)
blackbox.toString() 'nice try' (function source hidden)
String(blackbox) 'nice try'
typeof process / typeof require 'undefined' (fully isolated)

The stack trace also revealed the runtime: jsbox-0.0.0 under /nix/store/... using isolated-vm.

Vulnerability

The sandbox tries to hide the blackbox implementation by shadowing its toString:

// host-side context script (isolated-vm), roughly:
function blackbox(flag) {
  const FLAG = class="hljs-string">"gaslightCTF{n4t1v3_c0d3_0r_n4t1v3_fl4g?_0142da7d4c7e}";
  if (typeof flag !== class="hljs-string">"string") {
    throw new TypeError(class="hljs-string">"flag must be a string");
  }
  let ok = FLAG.length === flag.length;
  for (let i = 0; i < 10_000_000; i++) {
    if (FLAG[i % FLAG.length] !== flag[i % flag.length]) ok = false;
  }
  return ok;
}
blackbox.toString = () => class="hljs-string">"nice try";

The sandbox only overrode blackbox.toString. Function.prototype.toString.call(x) is the canonical way to read a function's source regardless of its own toString (it is what Function.prototype.toString itself does internally for callables that don't override it), so the full source — including the hardcoded FLAG — was returned.

Exploit

Function.prototype.toString.call(blackbox)

Output (source dump):

function blackbox(flag) {
  const FLAG = "gaslightCTF{n4t1v3_c0d3_0r_n4t1v3_fl4g?_0142da7d4c7e}";
  ...
}

Verification

blackbox(class="hljs-string">"gaslightCTF{n4t1v3_c0d3_0r_n4t1v3_fl4g?_0142da7d4c7e}")

returns true.

Flag

gaslightCTF{n4t1v3_c0d3_0r_n4t1v3_fl4g?_0142da7d4c7e}

Takeaways / fix

  • Never hide a secret by obscuring how it's used; secrets must never live in code that can be introspected at all. For this kind of oracle challenge, read the flag from a file/env into the host process and never expose the blackbox source to the sandbox (e.g. wrap it via an ivm.Reference callback that runs host-side, so toString shows only the reference wrapper).
  • Function.prototype.toString.call(fn) bypasses per-instance toString overrides.