I literally spent about 6 hours on this challenge sob and eventually solved it with my teammates.

chall❯ file ./chal
./chal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b0ba6747a1f840228cb23022a02690d4b8d1f470, not stripped
first run:
(base) sisubeny@ubuntu:/Users/sisubeny/ctf/puctf/bun$ ./chal
warn: CPU lacks AVX support, strange crashes may occur. Reinstall Bun or use *-baseline build:
<https://github.com/oven-sh/bun/releases/download/bun-v1.3.8/bun-linux-x64-baseline.zip>
error: Cannot find module '@std/crypto/crypto' from '/$bunfs/root/index'
Bun v1.3.8 (Linux x64)
as far as we know, this is a bun executable binary, so we can use bun-decompile .
bun add -g @shepherdjerred/bun-decompile
bun-decompile ./chal -o ./extracted
to extract its source code.
├── bun.lock
├── chal
├── decompiled
│ ├── node_modules
│ ├── bundled
│ │ └── index.js
│ └── metadata.json
└── package.json
index.js:
Notice that the admin PIN is derived from the service startup time rounded down to a 5-second bucket and the PIN is inserted into SQLite as BigInt(time) ** 2n, which gets truncated to a signed 64-bit integer.
var db = new Database(":memory:");
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, username STRING, pin INTEGER, admin BOOLEAN)");
var time = Date.now();
time -= time % 5000;
var insertUser = db.prepare("INSERT INTO users (username, pin, admin) VALUES (?, ?, ?)");
insertUser.run("admin", BigInt(time) ** 2n, true);
Login compares against the stored SQLite integer:
async function checkLogin(username, pin) {
const getUser = db.prepare("SELECT * FROM users WHERE username = ?");
const user = getUser.get(username);
if (!user) return false;
return user.pin === pin;
}
Once we get admin primitive we can install a controllable Bun package:
const install = Bun.spawnSync({
cmd: ["bun", "add", "--no-save", "--no-cache", message.package],
stdout: "pipe",
stderr: "pipe"
});