← Back to Writeups
CUSTOMEasyForensics

Modem Metamorphosis

XESXOR9/10/20264 min read
#forensics#custom#easy#pcap_triage#http_object_extraction#basic_auth_decoding#firmware_upload_analysis#linux

Modem Metamorphosis

Platform: DawgCTF SP26 | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-07 | Status: Solved Techniques: pcap_triage, http_object_extraction, basic_auth_decoding, firmware_upload_analysis

Summary

Task: analyze a PCAP containing a router web administration session and firmware upgrade. Solution: extract HTTP objects, recover the router UI pages and multipart upload, then combine the old Linksys WRT610N v1 firmware version with the uploaded OpenWrt image name.

Modem Metamorphosis — DawgCTF SP26

Description

Provided artifact: morph.pcap

Flag format: DawgCTF{Manufacturer_Model_OldFirmwareVersion_NewFirmwareName_NewFirmwareVersion}

English summary: the challenge gives a packet capture of a router administration session. The goal is to identify the device, determine the old firmware version, recover the uploaded replacement firmware, and assemble the flag fields in the required order.

Analysis

Step 1: Initial PCAP triage

The first step was to confirm what protocols were present:

bash
tshark -r morph.pcap -q -z io,phs

Relevant result:

text
http             frames:250 bytes:67977
mime_multipart   frames:1 bytes:375

That immediately suggests a web-based workflow, and the single mime_multipart entry is a strong hint that a file upload happened inside HTTP.

Step 2: Identify router admin traffic

Listing HTTP requests showed a browser talking directly to 192.168.1.1 and visiting typical embedded router pages such as /Wireless_Basic.asp, /Management.asp, and /Upgrade.asp.

bash
tshark -r morph.pcap -Y "http.request" -T fields \
  -e frame.number -e http.request.method -e http.host -e http.request.uri

Important requests included:

text
2748  GET   192.168.1.1  /Management.asp
3291  GET   192.168.1.1  /Upgrade.asp
13300 POST  192.168.1.1  /upgrade.cgi

The capture also contains repeated Basic Authentication headers:

bash
tshark -r morph.pcap -Y "http.authorization" -T fields \
  -e frame.number -e ip.src -e ip.dst -e http.authorization

Example output:

text
69  192.168.1.101  192.168.1.1  Basic OmFkbWlu

Decoding the Base64 portion confirms the credentials:

bash
python3 - <<'PY'
import base64
print(base64.b64decode('OmFkbWlu').decode())
PY

Output:

text
:admin

The observed header decodes to :admin, showing that the capture uses the default admin password during HTTP Basic Auth. That was enough to explain why the browser could access the router interface in the capture.

Step 3: Extract HTTP objects from the PCAP

To inspect the router web interface offline, I exported all HTTP objects:

bash
mkdir http_objects
tshark -r morph.pcap --export-objects http,http_objects

This recovered the router pages and the upload transaction artifacts, including:

  • http_objects/Upgrade.asp
  • http_objects/Management.asp
  • http_objects/upgrade.cgi
  • http_objects/upgrade(1).cgi

At this point the challenge becomes a straightforward reconstruction of what the administrator saw and uploaded.

Step 4: Recover the old firmware version from the web UI

Searching the recovered ASP pages for model and version strings:

bash
rg -n 'WRT610N|1\.00\.00|B18|WRT610NV1' http_objects/*.asp

The key evidence is in Upgrade.asp:

text
style="FONT-SIZE: 7pt" ... : 1.00.00 B18
style="FONT-SIZE: 8pt"><B>WRT610N</B>

That gives the visible router model WRT610N and old firmware version 1.00.00 B18.

Management.asp provides an additional detail that matters for the flag:

text
onclick=window.location.href="WRT610NV1_v1.00.00.cfg"

This backup filename is the strongest evidence for the hardware revision being v1. The main UI only displays WRT610N, but the configuration backup naming convention includes WRT610NV1, so the correct model field for the flag is WRT610N_v1.

Step 5: Extract and parse the firmware upload

The upload is the POST to /upgrade.cgi seen at frame 13300. After exporting objects, the multipart body is preserved in http_objects/upgrade.cgi.

The easiest way to recover the uploaded filename is:

bash
strings http_objects/upgrade.cgi | rg 'filename=|openwrt|Content-Disposition'

Output:

text
Content-Disposition: form-data; name="file"; filename="openwrt-24.10.0-bcm47xx-generic-REDACTED-v1-squashfs.bin"

This single filename gives almost everything needed for the new firmware portion:

  • manufacturer: Linksys
  • model/revision: WRT610N v1
  • new firmware name: OpenWrt
  • new firmware version: 24.10.0

I also verified that the extracted firmware image itself matches the same naming pattern:

bash
strings openwrt-24.10.0-bcm47xx-generic-REDACTED-v1-squashfs.bin | \
  rg 'OpenWrt|24\.10\.0|wrt610n|linksys'

The important point is that the router upload is not ambiguous: the multipart POST explicitly contains the OpenWrt image filename.

Solution

Assemble the flag fields in the required order:

  1. Manufacturer: Linksys
  2. Model: WRT610N_v1
    • WRT610N is shown in the UI
    • v1 is justified by WRT610NV1_v1.00.00.cfg in Management.asp
  3. Old firmware version: 1.00.00_B18
    • from Upgrade.asp
  4. New firmware name: OpenWrt
    • from uploaded firmware filename
  5. New firmware version: 24.10.0
    • from uploaded firmware filename

Final flag:

text
DawgCTF{REDACTED}

Minimal reconstruction commands:

python
#!/usr/bin/env python3
import base64
from pathlib import Path
import re

upgrade = Path("http_objects/Upgrade.asp").read_text(errors="ignore")
management = Path("http_objects/Management.asp").read_text(errors="ignore")
multipart = Path("http_objects/upgrade.cgi").read_text(errors="ignore")

old_fw = re.search(r'1\.00\.00 B18', upgrade).group(0)
model = re.search(r'WRT610N', upgrade).group(0)
rev = 'v1' if 'WRT610NV1_v1.00.00.cfg' in management else 'unknown'
filename = re.search(r'filename="([^"]+)"', multipart).group(1)

assert base64.b64decode('OmFkbWlu').decode() == ':admin'
assert filename == 'openwrt-24.10.0-bcm47xx-generic-REDACTED-v1-squashfs.bin'

flag = f'DawgCTF{{Linksys_{model}_{rev}_{old_fw.replace(" ", "_")}REDACTED}}'
print(flag)