zwesty
Write-ups / Day 7 / Android
Day 7 · Wed, Oct 7 · Android

WebView Bridge

Prey slaughtered

Static APK inspection (an APK is a zip), reading packaged assets, simple decoding (caesar)

STTMClaude Homebase Both

Flags are shortened to their first 8 and last 4 characters because they're unique per team.

Challenge ID
mobile-android-day-07-webview-bridge (+ /time_trial, /mastery)
Solved
2026-10-10, all three modes, 5 min 7 s from first start to last CORRECT
Player
Zwesty (solo), with Claude Code (Opus 5.5)

The challenge

Cross a challenge-owned WebView bridge through an untrusted origin.

One question, answer: “For case MOB-07-9F5C796C19 in package com.huntress.mobile.d07.v9f5c796c198b: Cross a challenge-owned WebView bridge through an untrusted origin. Submit only the exact backend answer code returned for this case.” Artifact: webview_bridge.apk. Format: ANS- followed by exactly 16 uppercase hexadecimal characters; no spaces or explanatory text, normalization exact.

How this lane was solved is explained once, in the Network Config Split write-up (Day 10, the first Android solve). Short version: every assigned APK carries assets/huntress_puzzle.json, and decoding it gives the answer. The app never has to run.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
challenge-owned WebView bridge JavaScript-to-app bridges in a WebView Static route
untrusted origin content from a site the app shouldn't trust The puzzle was a character-code shift with a key

Honest note: the prompts describe real Android attack surface, which would mean installing and driving the app. In this event the answer sat in a packaged JSON file that the app's own code never reads, so none of that was needed.

Safety

Static only. The APKs were opened as zip files on our Kali VM and one JSON asset was read; nothing was installed or run.

What was in the files

webview_bridge.apk, about 12.7 KB: one classes.dex, the manifest, a few resources, and assets/huntress_puzzle.json:

{"schema":"huntress.mobile.puzzle.v1","day":7,"codec":"caesar","data":"KX]7BKPOK?>:CCP=;:@O","key":10}

Codec caesar: each character of data shifted up by key character codes (not alphabet letters): subtract key from each code. ] (93) − 10 = S.

The Time Trial and Mastery APKs use the same template with different values and keys.

Single Solve

Started untimed to download the APK, decoded by our shared Android solver, and sent once: ANS-8AFEA54099F3106E → CORRECT in 1,199 ms (start to submit), flag a5f53a51…0be0.

Solver: our Android solver from Day 10 (shared by Days 1–10). It reads the asset from the zip, checks the schema, decodes the codec, and returns the answer only if it is exactly ANS- + 16 uppercase hex; anything unknown stops the run before sending. Its tests: 10/10.

Time Trial

No clock (deadline: null). Same puzzle schema and codec as the Single Solve, with different keys (18).

This route is opt-in timed. A plain start, as here, is unranked: CORRECT, but no ranked time. A ranked time needs a retry that asks for the clock (done; see Ranked retries).

started_at 2026-10-10T15:56:49.618Z  deadline None  answer_ids ['answer']
  mode time_trial  unranked  unique_attempts_remaining 16  ranked_attempts_remaining 16  variant_fresh True
  artifact webview_bridge_time_trial.apk 12704 bytes sha256 e0cc2bbb4bb6564f… (checked)
  answer: ANS-EF4F4D6A583828D4
  start         719 ms
  artifact      289 ms
  solve           0 ms
  submit        762 ms
  TOTAL        1771 ms
result CORRECT  flag 8cf42538…da03

Mastery

No clock (deadline: null). 3 APKs, one per set; the questions name them delivery N: …_mastery_0N.apk. Same puzzle schema and codec as the Single Solve, with different keys (8, 5, 7).

This route is opt-in timed. A plain start, as here, is unranked: CORRECT, but no ranked time. A ranked time needs a retry that asks for the clock (done; see Ranked retries).

started_at 2026-10-10T15:56:52.368Z  deadline None  answer_ids ['set1.answer', 'set2.answer', 'set3.answer']
  mode mastery  unranked  unique_attempts_remaining 8  ranked_attempts_remaining 8  variant_fresh True
  artifact 01-webview_bridge_mastery_01.apk 12704 bytes sha256 97ddeecbcc1d0a68… (checked)
  artifact 02-webview_bridge_mastery_02.apk 12704 bytes sha256 a6c23965ffc1ae3d… (checked)
  artifact 03-webview_bridge_mastery_03.apk 12704 bytes sha256 c271aa7e2e35151a… (checked)
  set1.answer: ANS-16253A1B7808CAEB
  set2.answer: ANS-6BBBC3AE4F0C841E
  set3.answer: ANS-8A0B92D866C90B33
  start         644 ms
  artifact      326 ms
  solve           1 ms
  submit        586 ms
  TOTAL        1557 ms
result CORRECT  flag a2c2dd7b…73ea

Ranked retries (opt-in clock)

Both timed modes were retried with the clock switched on so they count as ranked, with real deadlines. Same solver, same pipeline:

Mode started_at Clock On clock (est.) Result
Time Trial 2026-10-10T16:14:12.807Z 9 s 1,246 ms (artifact 373 + solve 1 + submit 872) CORRECT, 8cf42538…da03
Mastery (3 sets) 2026-10-10T16:14:16.486Z 10 s 1,150 ms (artifact 408 + solve 1 + submit 741) CORRECT, a2c2dd7b…73ea

Time to solve

All three modes were solved on 2026-10-10 in 5 min 7 s, from the first Single Solve start at 15:51:46 UTC to the Mastery CORRECT at 15:56:53 UTC, each on its first submit. Those timed runs had no clock and were unranked; the ranked retries of the Time Trial and Mastery both went CORRECT at 16:14 UTC.

Takeaways

  • Read every packaged file before assuming the app must run. The answer sat in assets/, unread by the app's code.
  • The answer format is a strong check. ANS- + 16 uppercase hex rules out wrong decodings (Day 10's other split reading, a wrong Caesar direction) before anything is sent.
  • Older routes become opt-in timed. This day's timed runs had no clock, so they count as CORRECT without a ranked time; a retry with the clock switched on is needed for one.