paint-brush
A Pentester Took Apart a Website’s Code to Prove It Was Totally Pointlessby@vognik
1,615 reads
1,615 reads

A Pentester Took Apart a Website’s Code to Prove It Was Totally Pointless

by Maksim RogovDecember 6th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

This article explains how to reverse engineer unfamiliar code and how to write your own extension for Burp Suite.
featured image - A Pentester Took Apart a Website’s Code to Prove It Was Totally Pointless
Maksim Rogov HackerNoon profile picture

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:

  1. Resolve the request signature generation algorithm
  2. Write my own extension for Burp Suite that automates all the dirty work


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.

Ariadne's thread: unraveling the signature algorithm


So, the service is an online casino with a set of classic games:

  • Plinko — a game where players drop a ball from the top of a peg-filled board, watching it bounce down to land in a slot with a win or lose value;
  • Tickets — players purchase lottery tickets with a set of numbers and win if their numbers match the randomly drawn numbers;
  • LiveDealers — online casino games conducted by real dealers in real-time, allowing players to watch and interact via video stream.
  • Double — a simple game where players bet on whether the next card will be higher or lower than the current card.
  • Crash — players bet and watch a multiplier increase, aiming to cash out before the multiplier crashes;
  • Nvuti — players bet on whether a number will fall below or above a certain interval;
  • Slots — casino games where players spin reels with symbols and win if certain combinations appear on the screen.


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):

  • string 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 secret


To 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.

Is it even necessary to write your own extension?

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:

  1. Use an off-the-shelf extension from the BApp Store
  2. Write your own extension


Each of these paths has its pros and cons, I will show you both.

Getting to know Hackvertor

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:

  1. Go to the Extensions tab

  2. Type Hackvertor in the search

  3. Select the found extension in the list

  4. 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.

Using Hackvertor in combat

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 amount
  • 1 - number of dragons
  • 6693a87bbd94061678473bfb - 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 milliseconds
  • MDWpmNV9-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.

Discover all the magic of Burp with your extension

Initial settings

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 here


Both 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 sessions
  • sys.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 errors
  • self.stdout.println(EXTENSION_NAME) - prints the name of the extension in Burp Suite
  • self.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 runs


With 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

  • Extension type - Python, or the programming language in which the extension is written
  • Extension file - the path to the extension file itself.


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.

A test of the pen

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.

Testing the new rule in action

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.


Bringing it to perfection

Everything is great, but there are three problems:

  1. The CSRF token is taken from the header. Generally it should be disposable, but probably here it has a lifetime (or not, which is wrong). In any case, it would be more correct to make a separate request itself to get a new one and update it. 2- A predefined user ID is used. If I want to check IDOR on this service, my previous script will become invalid for another user, since the ID is hardcoded.
  2. Different queries can have different parameters. And the scheme that was described for the script initially will be valid only for Dungeon Dragons, and no other. And I would like to be able to edit and send any request.


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:

  1. CSRF token and UserID acquisition is performed
  2. Timestamp is calculated and a signature is generated based on all parameters. It is important to note here that I am using OrderedDict which generates the dictionary in a rigid sequence as it is important to preserve it while signing.
  3. The final body of the request is generated and it is sent onward
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

Conclusion

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*.