Scan Surprise picoCTF Writeup

I was about 30 minutes into a weekend picoCTF practice session when I saw it: a ZIP file, and inside, a single PNG that opened as a QR code. My instinct was to write a Python script. I’d been programming long enough to know that pyzbar existed, that opencv could load images, and that reaching for a command-line tool I’d never heard of felt risky compared to code I could control. That instinct cost me 25 minutes on a challenge that a single terminal command would have solved in seconds.

This is my writeup for picoCTF Scan Surprise, a General Skills challenge. The problem is straightforward — extract a flag from a QR code image — but the lesson it left behind has changed how I approach every forensics problem since: before you write code, ask whether the code is already written for you.


The Challenge Setup

picoCTF Scan Surprise drops you into a General Skills category, which usually means the path to the flag is more about awareness than deep technical wizardry. The challenge provides a ZIP archive. Inside, after extracting, is a single file: flag.png.

$ unzip challenge.zip
Archive:  challenge.zip
  inflating: flag.png

$ file flag.png
flag.png: PNG image data, 304 x 304, 8-bit/color RGB, non-interlaced

Opening flag.png shows a clean black-and-white QR code. No steganography, no corrupted header, no hidden layers. Just a QR code. At this point I had two options: scan it with a phone, or find a terminal-based solution. CTF best practice rules out the phone — you need a reproducible command that another person can run and verify. So I reached for Python.


The Python Rabbit Hole (25 Wasted Minutes)

My first move was pyzbar, a Python QR/barcode decoding library I’d read about before. Installation seemed fine:

$ pip install pyzbar pillow
Collecting pyzbar
  Downloading pyzbar-0.1.10-py3-none-any.whl (13 kB)
Collecting pillow
  ...
Successfully installed pillow-10.2.0 pyzbar-0.1.10

Then I wrote the script:

from pyzbar.pyzbar import decode
from PIL import Image

img = Image.open("flag.png")
result = decode(img)
print(result)

Running it:

ImportError: Unable to find zbar shared library

pyzbar is a Python wrapper — it doesn’t include the underlying C library. I needed to install the native dependency separately, which is easy enough on Debian-based systems but not something I knew off the top of my head mid-challenge:

$ sudo apt install libzbar0
Reading package lists... Done
...
Setting up libzbar0:amd64 (0.23.1-1) ...

The script ran this time. Output:

[Decoded(data=b'picoCTF{p33k_@_b00_3f7cf1ae}', type='QRCODE', rect=Rect(left=29, top=29, width=246, height=246), polygon=[...])]

Flag extracted. But I’d just spent nearly 20 minutes on dependency errors, documentation tabs, and trial-and-error — all to solve something that zbarimg, a tool I didn’t know existed yet, could have done in one line from the terminal.

I only found out about zbarimg because someone on a CTF Discord mentioned it while I was explaining my Python ordeal. “Why didn’t you just use zbarimg?” they asked. I had no answer.


The Actual Solution: zbarimg

zbarimg is part of the zbar-tools package — the same underlying library that pyzbar wraps. But unlike the Python wrapper, it’s a self-contained command-line program. Install once, run directly:

$ sudo apt install zbar-tools
$ zbarimg flag.png
QR-Code:picoCTF{p33k_@_b00_3f7cf1ae}
scanned 1 barcode symbols from 1 images in 0 seconds

One command. Less than one second. No script, no dependency errors, no library version conflicts. The flag: picoCTF{p33k_@_b00_3f7cf1ae}.


Breaking Down the Flag

The flag content p33k_@_b00 is leet-speak for “peek-a-boo” — a direct reference to the challenge name “Scan Surprise.” Something is hiding in plain sight, and you need the right tool to peek at it. It’s a small detail, but picoCTF challenge designers are deliberate about naming. When the challenge name and the flag content mirror each other like this, it usually signals that the problem is testing awareness rather than technique.


All Attempts at a Glance

StepActionResultWhy It Failed / Succeeded
1pip install pyzbar pillowInstalled OKWrapper only — no native lib
2Run Python decode scriptImportError: Unable to find zbar shared librarylibzbar0 not installed
3sudo apt install libzbar0Installed OK
4Re-run Python scriptFlag decoded successfullyWorks, but ~20 min spent
5sudo apt install zbar-tools && zbarimg flag.pngFlag decoded in <1 secondPurpose-built CLI — fastest path

Why QR Codes Appear in CTF Forensics

QR codes encode data in a 2D matrix pattern using ISO 18004 standards. The format supports multiple encoding modes (numeric, alphanumeric, binary, kanji) and includes error correction at four levels (L/M/Q/H) — allowing up to 30% of the code to be corrupted and still decoded correctly.

In CTF challenges, QR codes are interesting because they look like noise to the untrained eye but carry machine-readable data. Challenge designers use them to hide flags in images, video frames, or even audio spectrograms. In real-world security contexts, QR codes have been weaponized for:

  • Phishing: QR codes in emails or posters pointing to malicious URLs that bypass link scanners
  • Payload delivery: Encoding shellcode or malware download links
  • Data exfiltration: Encoding sensitive strings in innocuous-looking images

The skill of decoding QR codes from the command line — and recognizing when a QR code has been tampered with (inverted, resized, embedded in a larger image) — is genuinely transferable beyond CTF.


Harder Variants You’ll Encounter

Scan Surprise uses a clean, standard QR code. But picoCTF and other CTFs introduce complications in harder variants. Here’s what to watch for:

Inverted Colors

ISO 18004 requires dark modules on a light background. An inverted QR code — white on black — causes zbarimg to return “0 barcodes detected” with no explanation. Fix with ImageMagick:

$ zbarimg inverted_flag.png
scanned 0 barcode symbols from 1 images in 0 seconds

$ convert inverted_flag.png -negate fixed.png
$ zbarimg fixed.png
QR-Code:picoCTF{example_flag}

Low Resolution

QR codes smaller than ~100×100 pixels fail reliably. The modules are too small for the scanner to distinguish. Use nearest-neighbor upscaling (not bicubic, which blurs edges):

$ identify tiny_flag.png
tiny_flag.png PNG 60x60 ...

$ convert tiny_flag.png -resize 400% -filter point upscaled.png
$ zbarimg upscaled.png
QR-Code:picoCTF{example_flag}

QR Code Inside a Video Frame

Some challenges embed QR codes in video files. Extract frames with FFmpeg first:

$ ffmpeg -i challenge.mp4 -vf fps=1 frame_%04d.png
$ zbarimg frame_*.png

Combined Fixes

When you’re not sure what’s wrong, apply negation and upscaling together:

$ convert flag.png -negate -resize 400% -filter point fixed.png && zbarimg fixed.png

What This Challenge Is Actually Testing

Scan Surprise sits in the General Skills category for a reason. It’s not testing your ability to write QR decoders. It’s testing whether you know that QR decoders already exist as command-line tools, and whether you reach for them before reaching for Python.

This sounds obvious in hindsight, but in a timed competition environment, the reflex to “just script it” is strong — especially if you’re a programmer. The challenge is designed to reward the mindset of: what tool handles this format natively?

picoCTF’s General Skills category is full of this kind of problem. The format changes — sometimes it’s a QR code, sometimes it’s a binary you need strings on, sometimes it’s an archive with an unusual extension — but the underlying skill is always the same: know your toolbox before competition, so during competition you reach for the right tool in seconds instead of minutes.

I’ve since made a habit of asking “does a CLI tool exist for this format?” before opening any editor. For QR codes: zbarimg. For audio: sox, audacity. For PDFs: pdfdumper. For binaries: binwalk, strings. For disk images: fdisk, dd. Ten seconds of searching saves twenty minutes of debugging.


Quick Decision Guide: When You See a QR Code in CTF

If you’re mid-competition and you get a QR code image, this is the order of operations:

SituationFirst Command
Any QR/barcode PNGzbarimg flag.png
Got “0 barcodes detected”convert flag.png -negate neg.png && zbarimg neg.png
Image looks tiny (under 100px)convert flag.png -resize 400% -filter point up.png && zbarimg up.png
QR code is inside a bigger imageconvert flag.png -crop 200×200+X+Y crop.png && zbarimg crop.png
QR code is in a video fileffmpeg -i challenge.mp4 -vf fps=1 frame_%04d.png && zbarimg frame_*.png
All of the above failFall back to Python pyzbar for programmatic control

Next Time I’d Do This Differently

If I saw this challenge again, my first terminal command would be:

$ zbarimg flag.png

No Python, no scripts. If zbarimg returned 0 results, I’d then look at the image visually — is it inverted? Low resolution? Cropped oddly? — and apply fixes before reaching for a scripting solution. The Python route is always available as a fallback, but it should be the fallback, not the first attempt.


Further Reading

If Scan Surprise is your introduction to QR decoding in CTF, the natural next step is to go deeper on the tool itself. My full breakdown of zbarimg in CTF — including all common failure modes and how to diagnose silent failures covers everything I’ve learned from using it across multiple challenges.

For a broader picture of forensics tooling, CTF Forensics Tools: The Ultimate Guide for Beginners maps out the full toolkit — from file analysis with binwalk to image inspection with pngcheck — with guidance on which tool fits which scenario.

Scan Surprise is part of picoCTF’s General Skills category. Other challenges in that series worth working through include problems that build on tool awareness in different formats — stego, binary, and disk image challenges all test a similar mindset.

投稿をさらに読み込む