basic-file-exploit picoCTF 2022 Solution

Published: July 20, 2023

Description

A netcat binary lets you write and read back numbered entries. The read path parses your entry number with strtol, which returns 0 for non-numeric input. There is a hidden branch that prints the flag when the entry number is 0, but the read code only runs once at least one entry exists. So: create one dummy entry, then read with a non-numeric value to land on entry 0 and dump the flag.

Launch a challenge instance from the picoCTF web panel; it shows the host and a port shown here as <PORT_FROM_INSTANCE>.

Connect with netcat and read the provided source. Note the read path uses strtol and a special entry_number == 0 branch.

Explore the menu: option 1 writes an entry, option 2 reads one back.

bash
nc saturn.picoctf.net <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Read the source and find the strtol quirk
    Observation
    I noticed the challenge ships with its source code and the description mentioned strtol, which suggested reading the source carefully to find the exact code path where strtol's return value is used as an array index without validating the endptr.
    The source ships with the challenge. The read handler does something like entry_number = strtol(input, ...) and then has a branch: if entry_number == 0, print the flag. strtol returns 0 when the input has no leading digits, so any non-numeric input selects entry 0. But the read handler is only reachable after at least one entry has been created, so you must add a dummy entry first.
    bash
    # Read the source: confirm strtol on the read input and an if (entry_number == 0) flag branch.
    bash
    # Confirm the read path requires num_entries >= 1.
    What didn't work first

    Tried: Try reading entry 0 immediately without writing any entry first.

    The read handler checks num_entries >= 1 before evaluating the entry number at all, so it returns early with a 'no entries' message and the strtol trick never gets a chance to fire. You must create at least one dummy entry before the read path is reachable.

    Tried: Assume the flag branch is triggered by passing the literal string '0' as the entry number.

    Passing '0' is a valid numeric string, so strtol returns 0 just as it would for 'a' - but the interesting insight is that it also returns 0 for completely non-numeric input. The challenge is designed around the non-numeric path; using '0' does work but misses the actual bug class (unchecked endptr), which is what the source review is meant to surface.

    Learn more

    strtol parses an optional sign and leading digits and returns 0 if there are none (e.g. the input "a"). Many programs treat that 0 as a valid index. Here entry 0 is the sentinel that prints the flag, so the bug is feeding the parser something it silently turns into 0, not any memory corruption.

  2. Step 2
    Create one dummy entry, then read a non-numeric value
    Observation
    I noticed that the read handler guards itself behind a num_entries >= 1 check and that strtol returns 0 for non-numeric input, which suggested the two-step exploit: first write a dummy entry to pass the guard, then supply a non-numeric string to land on the special entry 0 flag branch.
    First use the write option to create any entry (the content does not matter; it just makes num_entries >= 1 so the read path runs). Then use the read option and, instead of a number, type a non-numeric string such as 'a'. strtol turns that into 0, the entry_number == 0 branch fires, and the flag is printed.
    bash
    nc saturn.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # 1) Option 1 (write): add any entry (e.g. data 'x', length 1)
    bash
    # 2) Option 2 (read): when asked for the entry number, type a non-numeric string:
    bash
    a

    Expected output

    picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_...}
    What didn't work first

    Tried: Type '0' as the entry number during the read step instead of a non-numeric string.

    Entering '0' does trigger the flag branch because strtol('0', ...) legitimately returns 0, so the if (entry_number == 0) check fires. However, some contestants try this and get confused when the program validates that entry 0 does not exist in the entries array before reaching the flag branch - the exact control flow depends on the source. Reading the source first clarifies which path is actually guarded and which is the real entry point.

    Tried: Skip the dummy write step and go straight to the read option with a non-numeric string.

    Without any stored entries, num_entries is 0 and the read handler returns early with an error before it ever calls strtol on your input. The strtol trick is unreachable until the entries count is at least 1, so the server just prints 'no entries' and nothing more happens.

    Learn more

    The dummy write is the part that is easy to miss: with zero entries the read handler returns early before it ever evaluates the entry number, so the strtol trick alone does nothing. Once one entry exists, reading with non-numeric input lands on entry 0 and prints the flag.

    The proper fix is to validate that the input is actually numeric (check strtol's endptr) and to bounds-check the index, rather than letting a parse failure silently select a privileged slot.

  3. Step 3
    Collect the flag
    Observation
    I noticed the server output the flag string directly to stdout once the entry_number == 0 branch executed, which confirmed the exploit succeeded and the flag just needed to be copied from the terminal output.
    Once the entry_number == 0 branch runs, the server prints the flag.
    Learn more

    Static analyzers and careful review catch the unchecked strtol return; fuzzers surface it by feeding non-numeric and boundary inputs (empty string, "a", "0", very large numbers).

Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_...}

Not an integer underflow. strtol returns 0 for non-numeric input, and there is a flag branch for entry_number == 0, but the read path only runs once an entry exists. Create one dummy entry, then read with a non-numeric value (e.g. 'a') to trip the entry-0 branch and print the flag.

Key takeaway

Input parsing functions like strtol have well-defined but surprising edge cases: strtol returns 0 for non-numeric input rather than signaling an error, and programs that forget to check the endptr pointer silently accept garbage. This class of bug, trusting a parser's return value without validating its success, appears across file parsers, network protocol handlers, and CLI tools in any language with C-style conversion functions. The fix is always to check the parser's out-parameters and bounds-validate the result before using it as an index or selector.

Related reading

Want more picoCTF 2022 writeups?

Useful tools for Binary Exploitation

What to try next