← Back to Writeups
CUSTOMEasyWeb

Presence of Absence

XESXOR9/10/20263 min read
#web#custom#easy#error_page_differential_analys#exposed_asset_inspection#printable_strings_extraction#linux

Presence of Absence

Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-03 | Status: Solved Techniques: error_page_differential_analysis, exposed_asset_inspection, printable_strings_extraction

Summary

Task: an nginx site returned only custom 403 and 404 pages, but the less-damaged 404 page exposed an extra favicon asset. Solution: compare both error pages, fetch the missing favicon, extract printable strings, and pick the candy-themed flag that matches the icon hint.

Presence of Absence — HackerLab

Description

Original task name: «Присутствие отсутствия»

Target: http://62.173.140.174:16006

The service looked broken at first glance: / returned a custom 403 page, while missing paths returned a custom 404 page. The key hint was that one page was "less damaged", so the correct approach was to compare the error pages and inspect any extra assets still referenced by the better-preserved page.

Analysis

Recon

The server identified itself as nginx/1.25.0. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Useful observations:

  • GET / returned 403 Forbidden with a custom HTML page.
  • GET /nonexistent returned 404 Not Found with a different custom HTML page.
  • GET /robots.txt also returned the same 404 page.

This already suggested that the challenge was not about bypassing access control, but about reading the pages carefully and comparing what survived in each template.

Why the 404 page mattered

The 403 and 404 pages were similar, but the 404 page still referenced more static files: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • ./css/bootstrap.css
  • ./js/bootstrap.bundle.js
  • ./assets/favicon.ico

The 403 page included the CSS and JavaScript files, but did not include the favicon reference. That difference matched the challenge hint: one page was less broken, so the missing clue was likely hidden in an asset only referenced there.

Asset inspection

Fetching /assets/favicon.ico returned a normal ICO file. There was no visible flag in the page source itself, so the next step was to inspect the icon as raw data. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Running strings on the favicon revealed three candidate strings:

text
CODEBY{IT IS NOT A FLAG BUT YOU CAN TRY}
CODEBY{MAYBE LOWER ?}
CODEBY{REDACTED}

The favicon image was a candy icon, which made the third candidate the only one that matched the embedded visual hint. The user confirmation matched that interpretation.

Solution

Step 1: Compare the error pages

bash
curl -i http://62.173.140.174:16006/
curl -i http://62.173.140.174:16006/nonexistent
curl -i http://62.173.140.174:16006/robots.txt

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Expected result:

  • / -> HTTP/1.1 403 Forbidden
  • missing pages -> HTTP/1.1 404 Not Found

Step 2: Notice the extra asset on the 404 page

The 404 page source contained:

html
<link rel="stylesheet" href="./css/bootstrap.css">
<link rel="shortcut icon" href="./assets/favicon.ico" type="image/x-icon">
<script type="text/javascript" src="./js/bootstrap.bundle.js"></script>

The favicon reference was the important difference.

Step 3: Download and inspect the favicon

bash
curl -O http://62.173.140.174:16006/assets/favicon.ico
file favicon.ico
exiftool favicon.ico
strings favicon.ico

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The file and exiftool checks were optional sanity checks. The crucial output came from strings:

text
CODEBY{IT IS NOT A FLAG BUT YOU CAN TRY}
CODEBY{MAYBE LOWER ?}
CODEBY{REDACTED}

Step 4: Select the semantically correct candidate

Because the favicon was a candy icon, the right flag was:

text
CODEBY{REDACTED}

Minimal Python reproduction

python
#!/usr/bin/env python3
import re
import requests

BASE = "http://62.173.140.174:16006"

def printable_candidates(data: bytes):
    text = data.decode("latin-1", errors="ignore")
    return re.findall(r"CODEBY\{[^\r\n\}]{1,200}\}", text)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

index = requests.get(f"{BASE}/")
missing = requests.get(f"{BASE}/nonexistent")

print("/ status:", index.status_code)
print("/nonexistent status:", missing.status_code)

if "./assets/favicon.ico" in missing.text and "./assets/favicon.ico" not in index.text:
    print("404 page exposes an extra favicon reference")

favicon = requests.get(f"{BASE}/assets/favicon.ico")
candidates = printable_candidates(favicon.content)

print("Candidates:")
for candidate in candidates:
    print(candidate)

print("\nFinal flag: CODEBY{REDACTED}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Lessons Learned

  • Compare all response variants, not just the main page.
  • Error pages can leak clues through leftover asset references.
  • Run strings on downloaded binaries and images even when they look harmless.
  • When multiple candidate flags appear, use the visual or thematic hint to choose the correct one. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍