The other day I came across a service that verified the signature of requests on the server side. It was a small online casino, which for every request checked some value sent by the user from the browser. Regardless of what you were doing in the casino: placing a bet or making a deposit, an additional parameter in each request was the value “sign”, consisting of a set of seemingly random characters. It was impossible to send a request without it - the site returned an error, and it prevented me from sending my own custom requests.
Had it not been for this value, I would have left the site at that moment and never thought of it again. But, against all odds, it wasn't the sense of quick profit that made me excited, but rather the research interest and challenge that the casino was giving me with its fool-proofing.
Regardless of the purpose the developers had in mind when they added this parameter, it seems to me that it was a waste of time. After all, the signature itself is generated on the client side, and any client-side action can be subject to reverse-engineering.
In this article, I will discuss how I managed to:
This article will teach you how to save your precious time and reject useless solutions if you are a developer who is interested in doing secure projects. And if you are a pentester, after reading this article, you can learn some useful lessons on debugging as well as programming your own extensions for the Swiss Knife of security. In short, everyone is on the plus side.
Let's finally get to the point.
So, the service is an online casino with a set of classic games:
Interaction with the server works entirely on the basis of HTTP requests. Regardless of the game you choose, every POST request to the server must be signed - otherwise the server will generate an error. Signing requests in each of these games works on the same principle - I'll take only one game to investigate so that I don't have to do the same job twice.
And I'm going to take a game called Dragon Dungeon.
The essence of this game is to choose the doors in the castle sequentially in the role of a knight. Behind each door hides either a treasure or a dragon. If the player comes across a dragon behind a door, the game stops and he loses money. If the treasure is found - the amount of the initial bet increases and the game continues until the player takes the winnings, loses or passes all levels.
Before starting the game, the player must specify the amount of the bet and the number of dragons.
I enter the number 10 as the sum, leave one dragon and look at the request that will be sent. This can be done from the developer tools in any browser, in Chromium the Network tab is responsible for this.
Here you can also see that the request was sent to the /srv/api/v1/dungeon
endpoint.
The Payload tab displays the request body itself in JSON format
The first two parameters are obvious - I chose them from the UI; the last one, as you might guess, is timestamp
or the time that has passed since January 1, 1970, with the typical Javascript precision of milliseconds.
That leaves one unsolved parameter and, - and that's the signature itself. In order to understand how it is formed, I go to the Sources tab - this place contains all the resources of the service that the browser has loaded. Including Javascript, which is responsible for all the logic of the client part of the site.
It is not so easy to understand this code - it is minified. You can try to deobfuscate it all - but it is a long and tedious process that will take a lot of time (considering the amount of source code), I am not ready to do it.
The second and simpler option is to simply find the necessary part of the code by a keyword and use the debugger. That's what I will do, because I don't need to know how the whole site works, I just need to know how the signature is generated.
So, to find the part of the code that is responsible for generating the code, you can open a search through all the sources using the CTRL+SHIFT+F
key combination and look for the assignment of a value to the sign
key that is sent in the request.
Fortunately, there is only one match, which means I'm on the right track.
If you click on a match, you can get to the code section where the signature itself is generated. The code is obfuscated as before, so it is still difficult to read.
Opposite the line of code I put a breakpoint, refresh the page and make a new bid in “dragons” - now the script has stopped its work exactly at the moment of signature formation, and you can see the state of some variables.
The called function consists of one letter, the variables too - but no problem. You can go to the console and display the values of each of them. The situation starts to become clearer.
The first value I output is the value of the variable H
, which is a function. You can click on it from the console and move to the place where it is declared in the code, below is the listing.
This is a pretty big snippet of code where I saw a clue - SHA256. This is a hashing algorithm. You can also see that two parameters are passed to the function, which hints that this may not just be SHA256, but HMAC SHA256 with a secret.
Probably the variables that are passed here (also output to the console):
10;1;6693a87bbd94061678473bfb;1732817300080;gRdVWfmU-YR_RCuSkWFLCUTly_GZfDx3KEM8
- directly the value to which the HMAC SHA256 operation is applied.31754cff-be0f-446f-9067-4cd827ba8707
is a static constant that acts as a secretTo make sure of this, I call the function and get the assumed signature
Now I go to the site that counts HMAC SHA256 and pass values into it.
And comparing it to the one that was sent in the request when I placed the bid.
The result is identical, which means that my guesses were correct - it really uses HMAC SHA256 with a static secret, which is passed a specially formed string with the rate, number of dragons and some other parameters, which I will tell you about further on in the course of the article.
The algorithm is quite simple and straightforward. But it's still not enough - if it was a goal within a work project for pentest to find vulnerabilities, I would need to learn how to send my own queries using Burp Suite.
And this definitely needs automation, which is what I'm going to talk about now.
I have figured out the algorithm of signature generation. Now it's time to learn how to generate it automatically in order to abstract away all the unnecessary stuff when sending requests.
You can send requests using ZAP, Caido, Burp Suite, and other pentest tools. This article will focus on Burp Suite, as I find it to be the most user-friendly and almost perfect. Community Edition can be downloaded for free from the official site, it is enough for all experiments.
Out of the box Burp Suite does not know how to generate HMAC SHA256. Therefore, in order to do this, you can use extensions that complement Burp Suite's functionality.
Extensions are created both by community members and by the developers themselves. They are distributed either through the built-in free BApp Store, Github, or other source code repositories.
There are two paths you can take:
Each of these paths has its pros and cons, I will show you both.
The method with a ready-made extension is the easiest. It is to download it from the BApp Store and use its features to generate a value for the sign
parameter.
The extension I used is called Hackvertor. It allows you to use XML like syntax so that you can dynamically encode/decode, encrypt/decrypt, hash various data.
In order to install it, Burp requires:
Go to the Extensions tab
Type Hackvertor in the search
Select the found extension in the list
Click Install
Once it is installed, a tab with the same name will appear in Burp. You can go to it and evaluate the capabilities of the extension and the number of available tags, each of which can be combined with each other.
To give an example, you can encrypt something with symmetric AES using the tag <@aes_encrypt('supersecret12356','AES/ECB/PKCS5PADDING')>MySuperSecretText<@/aes_encrypt>
.
The secret and algorithm are in brackets, and between the tags is the text itself to be encrypted. Any tags can be used in Repeater, Intruder and other built-in Burp Suite tools.
With the help of Hackvertor extension you can describe how a signature should be generated at the tag level. I'm going to do it on the example of a real request.
So, I make a bet in Dragon Dungeon, intercept the same request I intercepted at the beginning of this article with Intercept Proxy, and stress it into Repeater to be able to edit it and resubmit it.
Now in place of the ae04afe621864f569022347f1d1adcaa3f11bebec2116d49c4539ae1d2c825fc
value, we need to substitute the algorithm to generate HMAC SHA256 using the tags provided by Hackvertor.
Формула генерации у меня получилась следующая <@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>10;1;6693a87bbd94061678473bfb;<@timestamp/>000;MDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4<@/hmac_sha256>
.
Consider all the parameters:
10
- bet amount1
- number of dragons6693a87bbd94061678473bfb
- unique user ID from MongoDB database, I saw it while analyzing the signature from the browser, but I didn't write about it then. I was able to find it by searching through the contents of the queries in Burp Suite, it comes back from the /srv/api/v1/profile/me
endpoint query.<@timestamp/>000
- timestamp generation, the last three zeros refines the time to millisecondsMDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4
- CSRF token, which is returned from /srv/api/v1/csrf
endpoint, and substituted in each request, in X-Xsrf-Token
header.
<@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>
and <@/hmac_sha256>
- opening and closing tags for generating HMAC SHA256 from the substituted value with the secret as a constant 31754cff-be0f-446f-9067-4cd827ba8707
.Important to note: the parameters must be connected to each other via ;
in strict order, - otherwise the signature will be generated incorrectly - as in this screenshot where I have swapped the rate and the number of dragons
That's where all the magic lies.
Now I make a correct query, where I specify the parameters in the correct order, and get information that everything was successful and the game started - this means that Hackvertor generated a signature instead of a formula, substituted it into the query, and everything works.
However, this method has a significant disadvantage - you can't get rid of manual work completely. Every time you change the rate or number of dragons in JSON, you have to change it in the signature itself to make them match.
Also, if you send a new request from the Proxy tab to Intruder or Repeater, you have to re-write the formula, which is very, very inconvenient when you need many tabs for different test cases.
This formula will also fail in other queries where other parameters are used.
So I decided to write my own extension to overcome these disadvantages.
You can write extensions for Burp Suite in Java and Python. I will use the second programming language as it is simpler and more visual. But you need to prepare yourself beforehand: first you need to download Jython Standalone from the official website, and then the path to the downloaded file in Burp Suite settings.
After that, you need to create a file with the source code itself and the extension *.py
.
I already have a billet that defines the basic logic, here are its contents:
Everything is intuitively simple and straightforward:
getActionName
- this method returns the name of the action to be performed by the extension. The extension itself adds a Session Handling Rule that can be flexibly applied to any of the requests, but more on that later. It is important to know that this name may be different from the extension's name, and that it will be selectable from the interface.performAction
- the logic of the rule itself, which will be applied to the selected requests, will be spelled out hereBoth methods are declared according to the ISessionHandlingAction interface.
Now to the IBurpExtender interface. It declares the only necessary method registerExtenderCallbacks
, which is executed immediately after loading the extension, and is needed for it to work at all.
This is where the basic configuration is done:
callbacks.setExtensionName(EXTENSION_NAME)
- registers the current extension as an action to handle sessionssys.stdout = callbacks.getStdout()
- redirects the standard output (stdout) to the Burp Suite output window (the “Extensions” panel)self.stderr = PrintWriter(callbacks.getStdout(), True)
- creates a stream for outputting errorsself.stdout.println(EXTENSION_NAME)
- prints the name of the extension in Burp Suiteself.callbacks = callbacks
- saves the callbacks object as a self attribute. This is needed for later use of the Burp Suite API in other parts of the extension code.self.helpers = callbacks.getHelpers()
- also gets useful methods that will be needed as the extension runsWith the preliminary preparations done, that's it. Now you can load the extension and make sure that it works at all. To do this, go to the Extensions tab and click Add.
In the window that appears, specify
And click Next.
If the source code file has been properly formatted, no errors should occur, and the Output tab will display the name of the extension. This means that everything is working fine.
The extension loads and works - but all that was loaded was a wrapper without any logic, now I need the code directly to sign the request. I have already written it and it is shown in the screenshot below.
The way the whole extension works is that before the request is sent to the server, it will be modified by my extension.
I first take the request that the extension intercepted, and get the rate, and the number of dragons, from its body
json_body = json.loads(message_body)
amount_currency = json_body["amountCurrency"]
dragons = json_body["dragons"]
Next, I read the current Timestamp and get the CSRF token from the corresponding header
currentTime = str(time.time()).split('.')[0]+'100'
xcsrf_token = None
for header in headers:
if header.startswith("X-Xsrf-Token"):
xcsrf_token = header.split(":")[1].strip()
Next, the request itself is signed using HMAC SHA256
hmac_sign = hmac_sha256(key, message=";".join([str(amount_currency), str(dragons), user_id, currentTime, xcsrf_token]))
The function itself and the constants denoting the secret and the user ID were pre-declared at the top
def hmac_sha256(key, message):
return hmac.new(
key.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256
).hexdigest()
key = "434528cb-662f-484d-bda9-1f080b861392"
user_id = "zex2q6cyc4ba3gvkyex5f80m"
Then the values are written to the request body and converted to JSON
json_body["sign"] = hmac_sign
json_body["t"] = currentTime
message_body = json.dumps(json_body)
The final step is to generate a signed and modified request and send it to
httpRequest = self.helpers.buildHttpMessage(get_final_headers, message_body)
baseRequestResponse.setRequest(httpRequest)
That's all, the source code is written. Now you can reload the extension in Burp Suite (it should be done after each script modification), and make sure that everything works.
But first you need to add a new rule for processing requests. To do this, go to Settings, to the Sessions section. Here you will find all the different rules that are triggered when sending requests.
Click Add to add an extension that triggers on certain types of requests.
In the window that appears, I leave everything as it is and select Add in Rule actions
A drop-down list will appear. In it, select Invoke a Burp extension.
And specify for it the extension that will be called when sending requests. I have one, and it is Burp Extension.
After selecting the extension, I click OK. And I go to the Scope tab, where I specify:
Tools scope - Repeater (the extension should trigger when I send requests manually through Repeater)
URL Scope - Include all URLs (so that it works on all requests I send).
It should work like in the screenshot below.
After clicking OK, the extension rule appeared in the general list.
Finally, you can test everything in action! Now you can change some query and see how the signature will dynamically update. And even though the query will fail, it will be because I chose a negative rate, not because there is something wrong with the signature (I just don't want to waste money 😀). The extension itself works and the signature is generated correctly.
Everything is great, but there are three problems:
To solve this, we need to add two additional requests, which can be done by the built-in Burp Suite library, instead of any third-party ones, instead of requests
.
To do this, I've wrapped some standard logic to make the queries more convenient. Through Burp's standard methods, the interaction with queries is done in pleintext.
def makeRequest(self, method="GET", path="/", headers=None, body=None):
first_line = method + " " + path + " HTTP/1.1"
headers[0] = first_line
if body is None:
body = "{}"
http_message = self.helpers.buildHttpMessage(headers, body)
return self.callbacks.makeHttpRequest(self.request_host, self.request_port, True, http_message)
And added two functions extracting the data I need, CSRF token, and UserID.
def get_csrf_token(self, headers):
response = self.makeRequest("GET", "/srv/api/v1/csrf", headers)
message = self.helpers.analyzeRequest(response)
raw_headers = str(message.getHeaders())
match = re.search(r'XSRF-TOKEN=([a-zA-Z0-9_-]+)', raw_headers)
return match.group(1)
def get_user_id(self, headers):
raw_response = self.makeRequest("POST", "/srv/api/v1/profile/me", headers)
response = self.helpers.bytesToString(raw_response)
match = re.search(r'"_id":"([a-f0-9]{24})"', response)
return match.group(1)
And by updating the token itself in the sent headers
def update_csrf(self, headers, token):
for i, header in enumerate(headers):
if header.startswith("X-Xsrf-Token:"):
headers[i] = "X-Xsrf-Token: " + token
return headers
The signature function looks like this. Here it is important to note that I take all the custom parameters that are sent in the request, add the standard user_id
, currentTime
, csrf_token
to the end of them, and sign them all together using ;
as a separator.
def sign_body(self, json_body, user_id, currentTime, csrf_token):
values = []
for key, value in json_body.items():
if key == "sign":
break
values.append(str(value))
values.extend([str(user_id), str(currentTime), str(csrf_token)])
return hmac_sha256(hmac_secret, message=";".join(values))
The main floo got reduced to a few lines:
OrderedDict
which generates the dictionary in a rigid sequence as it is important to preserve it while signing.csrf_token = self.get_csrf_token(headers)
final_headers = self.update_csrf(final_headers, csrf_token)
user_id = self.get_user_id(headers)
currentTime = str(time.time()).split('.')[0]+'100'
json_body = json.loads(message_body, object_pairs_hook=OrderedDict)
sign = self.sign_body(json_body, user_id, currentTime, csrf_token)
json_body["sign"] = sign
json_body["t"] = currentTime
message_body = json.dumps(json_body)
httpRequest = self.helpers.buildHttpMessage(final_headers, message_body)
baseRequestResponse.setRequest(httpRequest)
A screenshot, just to be sure
Now, if you go to some other game where custom parameters are already 3 instead of 2, and send a request, you can see that it will be sent successfully. This means that my extension is now universal and works for all requests.
Example of sending a request for account replenishment
Extensions are an integral part of Burp Suite. Often services implement custom functionality that no one else but you will write in advance. That's why it's important not only to download ready-made extensions, but also to write your own, which is what I tried to teach you in this article.
That's all for now, improve yourself and don't get sick.
Link to source code of the extension: *click*.