SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
B3S/WRITEUPS/QUICK-RECOVERY-WEB
← BACK TO ARCHIVE
EVENT: Safaricom PwnZone 2.0 Qualifiers 2026CATEGORY: WebPOINTS: 150 PTS

Quick Recovery

AUTHORED BY:@bealthguy10/5/2026

Quick Recovery

150 pts. Web. The brief is short:

Something went wrong. Then, somehow, everything looked normal again. The system recovered quickly, but...

Target: http://54.72.82.22:8010

The wording points at recovery mechanisms: backups, journals, or undelete tooling that returns state to a prior location. The actual solution is simpler than that, with one unresolved loose end.

Working directory for everything below: ~/.projects/ctfs/events/saf-pwnzone-2/web/quick-recovery/ with recon/, evidence/ and loot/ underneath.

First look

The page is titled AFTERHOURS, with a music festival theme: a large hero, "Tonight at Afterhours", and two buttons, Member sign-in (/login.html) and About the venue (/about). The page reports System: ONLINE, a latency widget and live feeds. The presentation resembles a stock CTF skin over a different underlying application.

ffcurl confirms Apache serving static HTML:

ffcurl http://54.72.82.22:8010 -i
HTTP/1.1 200 OK
Date: Fri, 02 Oct 2026 03:15:23 GMT
Server: Apache/2.4.68 (Debian)
Last-Modified: Wed, 30 Sep 2026 12:49:27 GMT
ETag: "1738-65cb2babac3c0-gzip"
Content-Type: text/html

whatweb reports the same picture without further detail:

http://54.72.82.22:8010/ [200 OK] Apache[2.4.68], Country[UNITED STATES][US], HTML5, HTTPServer[Debian Linux][Apache/2.4.68 (Debian)], IP[54.72.82.22], Script, Title[AFTERHOURS]

Saved to recon/whatweb.txt.

The login and the dead about page

/login.html is a non-functional form: username, password and inline JavaScript that suppresses submission and alerts a fixed failure message.

function fakeLogin(e){
  e.preventDefault();
  alert(class="hljs-string">"Sign-in was unsuccessful. Please try again.");
}

/about, the button target on the homepage, returns 404:

ffcurl http://54.72.82.22:8010/about -i
HTTP/1.1 404 Not Found
Server: Apache/2.4.68 (Debian)
...
<title>404 Not Found</title>

/js/ui.js exists solely to animate the latency figure and create visual noise:

// fake noisy widget values (for confusion)
(function(){
  const lat = document.getElementById(class="hljs-string">&#039;lat&#039;);
  const rnd = () => (20 + Math.floor(Math.random()*300)) + class="hljs-string">&#039;ms&#039;;
  if(lat) lat.textContent = rnd();
  setInterval(()=>{ if(lat) lat.textContent = rnd(); }, 1500);
})();

No authentication bypass exists on this surface.

The hidden image

The page source provides the lead. At the bottom of landing.html:

class="hljs-keyword"><img src="/assets/quick_recovery.jpg" alt="qr" style="display:none">

A hidden image named after the challenge with alt="qr". Response headers indicate 1.5 MB:

ffcurl http://54.72.82.22:8010/assets/quick_recovery.jpg -i
HTTP/1.1 200 OK
Server: Apache/2.4.68 (Debian)
Last-Modified: Wed, 01 Oct 2025 20:26:46 GMT
ETag: "171c15-6401eaffc2d80"
Accept-Ranges: bytes
Content-Length: 1514517
Content-Type: image/jpeg

An initial full download stalled, yielding 344K in 120 seconds before the shell terminated it, which suggested throttling. A small range request returned immediately:

ffcurl -r 0-1023 http://54.72.82.22:8010/assets/quick_recovery.jpg -o /tmp/testrange.bin

The 1K response carried a valid JFIF header, so the full transfer was merely slow during that minute. Running it under nohup in the background completed shortly after: 1,515 kB, 3000x3000, baseline JPEG.

file loot/quick_recovery_full.jpg
loot/quick_recovery_full.jpg: JPEG image data, JFIF standard 1.01, baseline, precision 8, 3000x3000, components 3

exiftool shows nothing embedded: no comment, no GPS, only image geometry. Saved to evidence/exiftool.txt.

Thumbnailing is required to view the image at that resolution:

from PIL import Image
im = Image.open(class="hljs-string">&#039;loot/quick_recovery_full.jpg&#039;)
print(im.size, im.format, im.mode)
# (3000, 3000) JPEG RGB
im.thumbnail((800,800))
im.save(class="hljs-string">&#039;/tmp/qr_thumb_full.jpg&#039;)

The content is a QR code: black modules on a purple background, at large scale.

zbarimg decodes it on the first attempt:

zbarimg --raw loot/quick_recovery_full.jpg
Hey Jerry, I couldnt let the people know our secret if I was compromised. This path should lead you to the right way, Remember, from the root. Stay Cyber Aware and piece everything together.
 /VXarjLbhJbhyqSvaqGuvf
scanned 1 barcode symbols from 1 images in 0.66 seconds

Raw bytes are saved to evidence/qr_raw.txt and decoded text to evidence/qr_decoded.txt. A full od -c shows the payload ends ...together. \r \n /VXarjLbhJbhyqSvaqGuvf \n. The trailing token is clean, 21 characters.

Used as a web path from root, as the note instructs, the token returns 404:

ffcurl http://54.72.82.22:8010/VXarjLbhJbhyqSvaqGuvf -i
HTTP/1.1 404 Not Found
...
<title>404 Not Found</title>

It also returns 404 under the robots path described below, at /IKnewYouWouldFindThis/VXarjLbhJbhyqSvaqGuvf, and with a trailing slash. Two readings are available and neither was confirmed: a second stage not yet accessible, or an indirect hint toward the real directory. The token does not gate the flag.

robots.txt and the forbidden directory

A lightweight check, prior to any brute force:

ffcurl http://54.72.82.22:8010/robots.txt
User-agent: *
Disallow: /IKnewYouWouldFindThis/
# Legacy recovery records use their original file paths.

The comment is the significant line. Legacy recovery records using their original file paths describes backup or recovery tooling that restores content to its original location. Either way, the disallowed directory is confirmed to exist:

ffcurl http://54.72.82.22:8010/IKnewYouWouldFindThis/ -i
HTTP/1.1 403 Forbidden
...
<title>403 Forbidden</title>

403 rather than 404 indicates an existing directory with listing disabled. Filenames must then be guessed, and the host is slow: ffuf with the full common.txt timed out twice. A small hand-rolled loop with ffcurl and --max-time 10 is sufficient:

for p in "" "index.html" "flag.txt" "flag" "secret.txt" "VXarjLbhJbhyqSvaqGuvf" ".htaccess" "robots.txt"; do
  echo "== /IKnewYouWouldFindThis/$p ==";
  ffcurl -o /dev/null -w "%{http_code} %{size_download}\n" --max-time 10 http://54.72.82.22:8010/IKnewYouWouldFindThis/$p;
done

Result:

== /IKnewYouWouldFindThis/ ==
403 318
== /IKnewYouWouldFindThis/index.html ==
404 315
== /IKnewYouWouldFindThis/flag.txt ==
200 40
== /IKnewYouWouldFindThis/flag ==
404 315
== /IKnewYouWouldFindThis/secret.txt ==
404 315
== /IKnewYouWouldFindThis/VXarjLbhJbhyqSvaqGuvf ==
404 315

flag.txt returns 200 with 40 bytes, which matches safctf{ plus 32 hex characters plus } exactly.

Flag

ffcurl http://54.72.82.22:8010/IKnewYouWouldFindThis/flag.txt -i
HTTP/1.1 200 OK
Date: Fri, 02 Oct 2026 03:22:30 GMT
Server: Apache/2.4.68 (Debian)
X-Powered-By: PHP/8.2.34
Content-Length: 40
Content-Type: text/plain; charset=utf-8

safctf{69f779b5b18bad69606f1926395e7c2a}

A clean fetch is saved to loot/flag.txt (40 bytes, no headers):

safctf{69f779b5b18bad69606f1926395e7c2a}

Browser render, for the record:

The captured landing.html for that render is:

class="hljs-keyword"><html>class="hljs-keyword"><head>class="hljs-keyword"><link rel="stylesheet" href="resource://content-accessible/plaintext.css">class="hljs-keyword"></head>class="hljs-keyword"><body>class="hljs-keyword"><pre>safctf{69f779b5b18bad69606f1926395e7c2a}class="hljs-keyword"></pre>class="hljs-keyword"></body>class="hljs-keyword"></html>

Flag:

safctf{69f779b5b18bad69606f1926395e7c2a}

Alternate approaches

ffuf and nuclei defaults were expensive against this host. robots.txt and the page source were both fast and both productive, and would have been the better opening moves. The QR detour consumed further time: /VXarjLbhJbhyqSvaqGuvf was pursued as a URL, then under the disallowed directory, then with slash variants, then through base64 decoding, all without result. In hindsight the QR code served only as confirmation that something hidden existed, and the flag did not depend on it. Guessing flag.txt inside a 403 directory is a blunt approach even though it succeeded here. A small wordlist remains preferable to typed filenames, provided concurrency is limited with -t 5 and a short timeout so the slow server does not stall the run.

txt`.