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/htmlwhatweb 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">'lat');
const rnd = () => (20 + Math.floor(Math.random()*300)) + class="hljs-string">'ms';
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/jpegAn 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.binThe 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 3exiftool 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">'loot/quick_recovery_full.jpg')
print(im.size, im.format, im.mode)
# (3000, 3000) JPEG RGB
im.thumbnail((800,800))
im.save(class="hljs-string">'/tmp/qr_thumb_full.jpg')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 secondsRaw 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;
doneResult:
== /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 315flag.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`.
