KubSTU 2026 was a Russian university CTF with 53 challenges and a strong capybara theme. This is the first of two grouped writeups, covering Web, Crypto, and Stego.

Web

CAPY-CAPY Bank 1

A Flask banking app issued two cookies:

  • access_token_cookie — Flask-JWT-Extended HS256, signed with facetoface.
  • session — itsdangerous-signed JSON that stored pending_signatures client-side.

The JWT secret cracked from rockyou in seconds. Forging sub:"4" let us authenticate as mgalankov@4274 (ACC004), a high-balance user.

The real bug was uglier: the “one-time cryptographic signature” required for transfers was just a lookup key in the pending_signatures dictionary inside the session cookie. We resigned a session that already contained a matching signature entry, submitted /transfer with it, and the server skipped the PIN check entirely.

Flag: KubSTU{1d0r_b4nk4_d4l_d0stup_k_chuzh1m_sch3t4m}

CAPY-CAPY Bank 2

The devs patched Bank 1: the session cookie no longer carries pending_signatures; signatures are stored server-side. The JWT secret was rotated to ifeveryonecared3 — still in rockyou.

What they did not fix: the issued signature is not bound to the user or transaction details. We authenticated as ourselves, got a signature with our own PIN, forged a JWT for mgalankov@4274, and submitted /transfer with arbitrary to_account/amount plus the same valid signature. The server accepted it because the signature was technically issued by the server — just for a different account and a smoke-test transfer.

Flag: KubSTU{p0dm3n4_p4r4m3tr0v_1zm3n1l4_summu_tr4nz4kts11}

Deadlock

The front page on 159.194.199.67:5000 returned a hardcoded “Portal” response with a persistent off-by-one Content-Length, which turned into a long rabbit hole. The actual gate was simpler: the author’s Burp screenshot (partially hidden by a capybara plushie) showed Host: admin.challenge.local:8081 and X-Admin-Access: true. On the live target, the admin vhost was bound on port 5000, so the :8081 suffix was a red herring from local testing. The winning request was:

GET /admin HTTP/1.1
Host: admin.challenge.local
X-Admin-Access: true

Flag: KubSTU{Pipelined_Smuggling_Success_5521}

Capybara Library

An XXE in the library search endpoint read internal files and returned the flag directly.

Flag: KubSTU{xxe_1s_v3ry_c0mmon_1n_capy_l1brary}

[Partner] repoforge

A Ruby/Sinatra code-hosting lab on HackAdvisor. The app used AES-GCM encrypted rack.session cookies and had show_exceptions enabled. A malformed POST /profile (array params) crashed SQLite and rendered the full Sinatra debug page, leaking the cookie secret, session ID, and CSRF token.

With the secret we forged an admin session. From there we mapped /admin/jobs, SSRF via git://127.1:6379/%0D%0A... to Redis, HTTP SSRF through loopback aliases (127.1, 0x7f.1, 2130706433), and multi-frame error-page leaks of server.rb. Marshal RCE through Gem::Source::Git was blocked by a missing git cache directory. Redis contained only Sidekiq metadata, no flag. The challenge was ultimately unsolved, but the recon chain is worth documenting.

[Partner] teamforge

A related partner lab. The Master API Key field itself was the flag.

Flag: KubSTU{21509994fd5a1383bfb6b4c4d85b4cf0}

Crypto

Not enough part 1

Partial RSA key recovery from top bits of two primes. After fitting the prefix model, both keys factor normally, and the shared master secret is derived via:

key = sha256(long_to_bytes(d1) + long_to_bytes(d2)).digest()[:16]

That key decrypts the AES-GCM blob.

Flag: KubSTU{1_h0p3_y0u_solv3d_7hi5_wi7h0ut_4ny_pr0bl3m5}

Not enough part 2

Same structure as part 1, but dump_a had a corrupted decimal substring. The correct recovery required replacing 36 with 03 at decimal index 234. After fixing the printed modulus, the prefix model fit and the two primes recovered with 72 and 80 lost low bits respectively.

Flag: KubSTU{1_h0p3_y0u_solv3d_7hi5_p4rt2_7hi5_1s_much_h4rd3r}

Nintendo 3DS

A challenge themed around Nintendo 3DS encryption. The vulnerable point was AES-CBC mode with a predictable IV/padding oracle path. Decrypting the final layer gave the flag.

Flag: KubSTU{3d3s_n1nt3nd0_cbc_m0d3_n07_h4rd_3n0ugh}

Unlucky 13

Thirteen layers of weak encryption stacked on top of each other. Peeling them one by one:

Flag: KubSTU{unLucky_13_l4y3r5_0f_encrypt10n_n0_luck_h3r3}

Steganography

Hidden Glyphs

A PDF whose custom Type 3 font had an unusual /Widths array. Each glyph width divided by 10 gave an ASCII code. Reading the width table left to right:

750  -> 75  -> K
1170 -> 117 -> u
980  -> 98  -> b
...

Flag: KubSTU{typ3_3_f0nt_w1dth5_4r3_tr1cky}

Meow Message

White-space steganography in a text file. The hidden data lived in the whitespace characters themselves.

Flag: KubSTU{wh1t3_sp4ce}

Capybara Secret

A straightforward stego challenge with the flag buried in the carrier.

Flag: KubSTU{W0W_1ncred1ble_capyba6a}

Capybara in Nightmare Land

Another capybara-themed stego solve.

Flag: KubSTU{H0ly_M0ly_CapyHaCk1r}

The Ancient Note

A hidden message between the layers of an ancient-looking document.

Flag: KubSTU{h1dd3n_truth_b3tw33n}

Bembembem

Stego built around the Mellstroy meme audio/video. The flag survived the chaos.

Flag: KubSTU{3nj0y_1h_0f_M3ll57r0y_m3m3s}

Takeaways

  • Web: Rotated secrets that are still in wordlists are not rotated. Client-side state that carries security-critical lookups (like pending_signatures) is an IDOR waiting to happen.
  • Web: Virtual-host and custom-header gating looks like hardening but is often the whole access-control mechanism. Burp screenshots are challenge hints, not decoration.
  • Crypto: Partial-key recovery with prefix models is fast when the bit loss is bounded; always check whether the printed modulus itself is corrupted before blaming the math.
  • Stego: PDF font metrics are a classic underused channel — /Widths can encode ASCII directly and still render normally.