neven@dimitri-dev:~$
cd ../hackultet-2026

$cat avatar-app.mdWebHackultet 2026

Avatar App

A PHP avatar uploader wraps a deliberately hard deserialization chain around a much simpler bug, a file the Dockerfile has to serve just to start the container.

  • #dockerfile
  • #misconfiguration
  • #enumeration

$man avatar-app

What this one teaches:

  • Reading a Dockerfile as part of the attack surface, not just the application code.
  • Why a shell redirect like < file needs that file to exist before the command runs.
  • Checking the obvious, reachable files before chasing an elaborate exploit chain.

Task description

An avatar upload application, with its PHP source made available through .phps symlinks for review.

An elaborate-looking app

The app has everything in place for a hard web challenge: a JPEG-only upload form, an avatar viewer, an integrity checker that calls file_exists() on a path we control, and a chain of PHP classes (Profile, EventDispatcher, LogHandler, AvatarProcessor) that looks built for PHP object deserialization, ending in a call to ImageMagick’s convert. All the PHP source is readable via .phps files the Dockerfile exposes.

Put together, that reads like a PHAR deserialization chain into ImageMagick command execution, the kind of multi-step exploit these components are usually there to teach. We started tracing it, then checked the Dockerfile that builds the container, since it’s part of what ships with the challenge and worth reading before committing to a plan.

The file the server needs to start

The key lines:

COPY flag.txt 3
RUN chown root:root 3 && chmod 400 3

WORKDIR /var/www/html
COPY src/ /var/www/html/
RUN chown -R root:root /var/www/html && chmod -R 755 /var/www/html

ENTRYPOINT docker-php-entrypoint apache2-foreground <3

Reading it line by line:

  • COPY flag.txt 3 places the flag at /3 (the working directory is still / at that point in the build), and it’s locked to chmod 400, root-only.
  • The entrypoint ends in <3, a shell redirect that feeds the file named 3 to Apache’s stdin. By the time this line runs, WORKDIR is /var/www/html, so the shell looks for 3 there, not at /3.
  • For that redirect to succeed at all, and for the container to start, a file named 3 has to exist inside /var/www/html. That means src/ in the build context contains one.
  • The very next line, chmod -R 755 /var/www/html, makes everything under the web root world-readable, including that 3.

So a second copy of the flag, needed only so the container’s startup command has something to read, ends up sitting in the public web root with permissions that let Apache serve it as a plain static file.

The solve

curl http://chal.live.hackultet.hr:14007/3

That alone returns the flag. No upload, no deserialization gadget, no ImageMagick at all.

What the rest of the app was for

The PHAR gadget chain, ImageMagick’s relaxed @* policy, the JPEG structure validator, and the scheme filtering that blocks most PHP stream wrappers except http/https would all combine into a genuinely hard exploit, the one the challenge’s components are clearly built to teach. None of it was needed here: a build artifact meant only to keep the entrypoint’s stdin redirect from failing was left readable from the outside.

$cat flag.txt revealCTF2026[964734005206]

Written by Neven Marić. This writeup was written with the help of Claude Code (Anthropic).