The setup
One file: centralized_siem_logs.jsonl, 50,000 lines. The scenario is that an adversary cleared the host logs and only the centralized SIEM copy survived, so this is all I get. The challenge is called “Proxy not proxy Proxy not Proxy”, which I ignored at first and came back to later.
Each line is a JSON event. The fields that ended up mattering:
{
"timestamp": "2025-10-14T07:21:17Z",
"log_type": "http_access",
"host": "web01",
"service": "nginx-web-prod",
"container": "blackgate-proxy:sma9lo",
"image": "blackgate-proxy:sma9lo",
"method": "GET",
"path": "/search?q=test",
"status": 301,
"bytes": 8297
} 50,000 events is too many to read and too few to need anything clever. Everything here is grep, sort, and a bit of Python.
Finding the compromise
I started by grouping events by service and container to see the shape of the environment. Almost every container followed the same convention: trusted-db, trusted-redis, trusted-authsvc, trusted-minio, trusted-monitoring. A trusted- prefix on a human-readable name.
One didn’t. blackgate-proxy:sma9lo is in image:tag format, not a container name. Nobody names a container after its image and tag by accident, and nothing else in 50,000 logs looks like it. That’s the IOC, and it sits on nginx-web-prod, host web01 (10.10.1.5).
The name is also the challenge telling on itself. sma9lo is Darija: 9la / qla is to fry, so sma9lo is roughly “they fried it”. Fried, as in owned. And the title finally parses: a proxy service (nginx-web-prod) running a container that calls itself a proxy (blackgate-proxy) but isn’t one. Proxy, not proxy.
That container accounts for 10,668 of the 50,000 logs, 9,300 of them http_access. Way too much traffic for one service, which is the point: it’s cover.
The 9,300-request haystack
So the box is compromised and this container is loud. The next question is what it exfiltrated. There’s nothing in the request paths (/, /upload, /api/v1/token, /metrics, /admin, all boring), nothing in the user agents, nothing in the bodies because there are no bodies. Just access logs.
I spent a while here staring at fields that go nowhere. The traffic is deliberately uniform: a handful of paths, plausible status codes, spread across days. If there’s a signal it isn’t in the content, so it has to be in the metadata. The only numeric field that varies per response and that the attacker fully controls is bytes, the response size.
Response sizes for these paths are in the thousands. But a few weren’t. A few were small, two and three digit numbers, and every one of them landed in the printable ASCII range, 32 to 126. That’s not what a real response body weighs. That’s a channel.
Reading the sizes
Pull every http_access event from the malicious container whose bytes fall in 32 to 126, sort by timestamp, and read the byte value as a character.
import json
rows = [json.loads(l) for l in open('centralized_siem_logs.jsonl')]
mal = [r for r in rows
if r.get('container') == 'blackgate-proxy:sma9lo'
and r.get('log_type') == 'http_access']
enc = [r for r in mal if 32 <= r.get('bytes', 0) <= 126]
enc.sort(key=lambda r: r['timestamp'])
print(''.join(chr(r['bytes']) for r in enc)) Sixteen events. Here they are in order:
2025-10-10T10:00:42Z 107 'k' /upload
2025-10-10T19:18:56Z 118 'v' /api/v1/token
2025-10-10T19:21:47Z 100 'd' /api/v1/login
2025-10-10T21:25:01Z 110 'n' /metrics
2025-10-11T06:20:03Z 105 'i' /api/v1/token
2025-10-11T16:24:23Z 103 'g' /api/v1/login
2025-10-12T11:18:25Z 123 '{' /search?q=test
2025-10-15T06:46:24Z 112 'p' /upload
2025-10-15T10:43:42Z 112 'p' /api/v1/token
2025-10-15T21:37:13Z 121 'y' /
2025-10-16T10:47:23Z 123 '{' /
2025-10-16T20:08:46Z 84 'T' /
2025-10-16T22:21:52Z 116 't' /admin
2025-10-16T22:38:32Z 125 '}' /admin
2025-10-17T20:09:21Z 99 'c' /health
2025-10-18T16:39:44Z 93 ']' /api/v1/token Read straight through, that’s kvdnig{ppy{Tt}c]. Not quite a flag. There are stray brackets in there, {, }, ], that don’t sit where flag delimiters should. The exfiltration ran two characters a day over nine days, so a couple of noise bytes slipping into the stream is exactly what you’d expect from a low-and-slow channel, and the challenge wraps its flags in AKASEC{...} anyway. Drop the punctuation, keep the alphanumerics and the case:
kvdnigppyTtc Wrap it:
AKASEC{kvdnigppyTtc} The flag
AKASEC{kvdnigppyTtc} What the attack actually was
Stepping back, the whole thing is one covert channel with a lot of noise built around it. The attacker deployed a container that violated the naming convention, then had it generate roughly 9,300 requests across nine ordinary-looking paths so the real traffic would drown. Sixteen of those responses carried one character each, encoded as the response size, spaced hours apart so no daily volume spike would show up. Sixteen characters over nine days is painfully slow on purpose. Nothing here trips a content filter because nothing is in the content.
The two things that gave it away were both metadata. The container named itself out of the crowd, and the exfiltration lived in a numeric field nobody baselines. If you wanted to catch this, you’d alert on container names that look like image:tag, and you’d flag HTTP responses whose size sits in the printable ASCII range, because a real response is basically never 84 bytes.