A Guide For Advanced Message Protected API Hacking Using Hackvertor and Burp (Part #2)

More up-to-date Hackvertor game-changer techniques, code examples, and tips for advanced penetration testing and bug bounty.

Intro

Hackvertor is a Burp extension that programmatically extends Burp capabilities, by allowing you to embed neat code logic directly into HTTP requests sent/proxies by Burp and its extensions. Similar to Postman pre-request scripts.

Here, I will try to provide more structured information and recipes, so in time of need, you won’t spend time on setup and basic things. Go beyond that with your attacks!

This article is the continuation of the part #1. I will address more use cases, edge cases, and will provide code examples to quickly start using Hackvertor.

HV Is A Game-Changer Tool

  • Dissect custom encryption, message signing, or any other message security layers — inside Burp.
  • Leverage Burp’s features and automate testing of mobile applications, APIs, or WEB.
  • Challenge security by obscurity approach; Hack easily seemingly bulletproof APIs when their message protection layers peeled off.
  • Link any external tool through Burp’s proxy with HV scripts processing the requests (like sqlmap).
  • Much easier than writing Burp extensions (when we don’t need to process the server’s response).

Testing Approach

Most of the things we will do here can be done manually or using any pure programming language or in combination with additional tools, such as Postman. But you will give up on all the Burp capabilities and extensions. With HV you can leverage Intruder, Extensions, Logger, Proxy, and more to work like a PRO.

For such a thing, we need a dynamic layer in between Burp and the server, luckily, we have a well-integrated and easy-to-use solution that works across all the Burp tools — Hackvertor extension (see part 1 for more info).

Of course, you can set up your custom middleware proxy that could do the same with the full code customizations as we want. Or you can write a custom Burp extension. But at what cost? Using HV custom scripts is a much faster, and more reusable solution.

When you stumble upon a custom message security layer or any variant of message level obstacle, in most cases that area will be a white spot across many testers. Always challenge the message protocol. There would probably hide some juicy issues.

With HV you can leverage Burp’s attack capabilities and Intruder automation. Also, you can use the HV tag with external tools that support proxies. Burp’s proxy also can process tags.

Passing Parameters To Our Python Code

  • Passing up to 2 params directly into the function (string/int) We can pass more;

In delimited string “a:z:c” and them split it

_input1 = str(input).split(“:”)\[0\]_

We can pass JS_ON and parse it

jsonObj = json.loads(inputJSON)  
for i in jsonObj:  
str(jsonObj\[i\])_
  • Every set HV variable is automatically injected/marshaled into python You can see all of them by running_dir()_ inside python code. For test purposes, you can just reflect the_output = str(dir())_ and view all the initialized parameters

Param Sequence Notes

When we want to update our request header upon body defined param that itself contains a HV input, like

GET /
Host: host.com
AuthHeader: <@_Custom(‘Signs inJSON variable’,’signing Key’,’’)><@/_ Custom>

<@set_inJSON('false')>
{“sqlQuery”:”SELECT …”,
“timestamp”: <@timestamp/> }
<@/set_inJSON>

Param processing is nested and done from within the deepest layer up. For example, in the example, below inJSON parameter that would hit the Python code would contain the execution result of <@timestamp/> tag.

Execution steps:

  • Timestamp
  • Param definition
  • Custom signing tag

Another example of passing parameters:

GET <@set_b_full_url('false')>/v1/account/prefs<@/set_b_full_url> HTTP/1.1
Host: host.com
User-Agent: python-requests/2.26.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
X-API-Key: <@set_b_key('false')>XXXXXXX<@/set_b_key>
Authorization: Bearer <@_Custom('0acb0974295f467f01ff05b01b9d2e64')><@/_Custom>
Content-Length: 39

<@set_b_body('false')>{...JSON Body...}<@/set_b_body>

Param processing when using the Intruder. First, the Intruder’s payload is placed into the request, and the HV processes the tag. Everything works seamlessly. However, the Intruder Attack UI won’t show the processing result, only a tag. The actual processed request you can view in the Logger tab.

Tips

  • Do not forget to set param type as string/int, the name won’t be saved.
  • Work with files. Upon each file save Hackvertor automatically reloads the scrip, at least for Repeater/HV tab (used to tune up the logic). Save the file, alter the HV Tab view (press space) and HV will automatically update the view based using the latest version of the file
  • You can debug the code without burp (set mock parameters inside the script):java -jar .\jython-standalone-2.7.0.jar script.py
  • When reopening Burp, the code execution tag id should be updated. When Burp starts, click Allow Code Execution inside HV top menu, open HV tab, add a custom tag and copy the new id value to the tags already used.

<@_Custom(“id code”)><@/_ Custom>

  • Debugging your Python code without Burp and Hackvertor

java -jar .\jython-standalone-2.7.0.jar .\Fireblocks.py

Note, that standalone Jython loads the classes from ./ local Lib folder, while Hackvertor’s loads from the temp folder created during the start of Burp/HV.

  • As for today, when using the latest version of Burp, Hackvertor, and JavaScript on Windows you have a bad time. When trying to reuse the old Hackvertor project a while ago I had a lot of compatibility problems with Windows support, Java 15 deprecation of certain in-use features, and more. To run the old code you need to downgrade the Java version and Hackvertor.
  • We aren’t really interested in fully replicating the original SDK/client behavior and can alter classes are we need it, for example with can use just a simple random number generator instead of a secure one.
  • If you can’t make a Python library work with HV, don’t hesitate to embed only relevant code parts from it into your HV script. It has more benefits than drawbacks (also much faster) and will give you more ideas on how to attack the application. All of the needed code is already there, you just need to assemble the right parts in the right order into your code.
  • Remember, that you can substitute the usage of well-known Python libs by using their java counterparts through Jython.
  • For advanced cases when the usage of external libs is needed, like crypto stick to java classes like java.security, javax.crypto with Jython.
  • When working with crypto, stick to the same classes and formats. Note, that hashing JSON string and different JSON representations will give different hash result values. Be aware of the exact classes in use. Let’s see some examples.

We have a couple of popular Python options to work with a JSON object. But be careful when using them in crypto-related flows.

The next 4 code examples show that each hashing technique could result in a different output, that will result in an invalid message signature. Know exactly how the client library works to be able to replicate its behaviour to create a valid message.

>>> sha256(json.dumps('{"a":"a"}').encode("utf-8")).hexdigest()
'c4602ab462c46e37f6677e339371bd86337051838ce1c49d9ff4b91ffffa67c4'

sha256(json.dumps('{"a":  "a"}').encode("utf-8")).hexdigest()
'e37d578267a7f6d8936b8a749f1eaedcde9434a01ddb6333c2d60646d87c8132'

sha256('{"a":"a"}'.encode("utf-8")).hexdigest()
'681523631e0f5d3904d881dd163683081e0e45afdad34376ff5bf5fbadada6c7'

>>> sha256(str(json.loads('{"a":"a"}')).encode("utf-8")).hexdigest()
'8e9ee3e5afddd3561dc6e1d693ad3c90a711c579621d755447e07c357a311450'

Code Examples

HV_AES_Encrypt.py

This Hackvertor script shows the basic usage of symmetric encryption. We would use it when certain parameters should be sent encrypted. In this particular example using AES/CBC/PKCS5PADDING suit with a padded timestamp as an initialization vector.

#!/usr/bin/env python
# -*- coding: utf-8 -*-  
#
# This Hackvertor script shows basic usage of symmetric encryption.
# We would use it when certain parameters should be sent encrypted. 
# In this particular example using AES/CBC/PKCS5PADDING suit with a 
# padded timestamp as an initialization vector.

import base64
import hashlib
from java.util import Base64
from javax.crypto import Cipher
from javax.crypto.spec import IvParameterSpec
from javax.crypto.spec import SecretKeySpec

timestamp = "000000"+ str(ts)
# timestamp = "0000001631634319"

# either static or external value
encryptionKey = "theKey"
inCVV = str(input)

_cipherTransformation = 'AES/CBC/PKCS5PADDING'
_aesEncryptionAlgorithem = 'AES'

encryptedText = ''
cipher = Cipher.getInstance(_cipherTransformation)
key = encryptionKey.encode('utf-8')
secretKey = SecretKeySpec(key,'AES')
ivParameterSpec = IvParameterSpec(timestamp)
cipher.init(Cipher.ENCRYPT_MODE,secretKey,ivParameterSpec)
cipherText = cipher.doFinal(inCVV.encode('UTF-8'))
encoder = Base64.getEncoder()
encryptedText = encoder.encodeToString(cipherText)
output = encryptedText

# For local debugging
# print(encryptedText)
# return encryptedText

HV_API_Hash_Signing.py

This Hackvertor script shows a basic example of signing an HTTP body JSON message. Each message has a timestamp and signature calculated by concatenating API-specific JSON keys altogether with a secret value/token and hashing them with SHA384. The server upon message validation checks the timestamp and validates the signature by re-calculating the input JSON with the server-side stored authentication key of the client. Also, the server decrypts the CVV with a symmetric key linked to the specific client.

#!/usr/bin/env python
# -*- coding: utf-8 -*-  
#
# This Hackvertor script shows a basic example of signing an HTTP body JSON message. 
# Each message has a timestamp and signature calculated by concatenating API-specific
# JSON keys altogether with a secret value/token and hashing them with SHA384. 
# The server upon message validation checks the timestamp and validates the signature
# by re-calculating the input JSON with the server-side stored authentication key of 
# the client. Also, the server decrypts the CVV with a symmetric key linked to the specific client.

import json
import hashlib

# Mock Params used for local testing
# apiName = "api1"
# clientSecret = "XXXXXXXXXXXXXXXXXXX"
# inputJSON = '{"field1":"John","field2":"Smith","field3":"authorization","field4":
# "USD","field5":1000,"wallet_data":{"email":"test@gmail.com","phone":"1111111111",
# "login":"333"},"field6":"378","locale":"en-GB","field7":"certainID","field8":
# "http:\/\/some.callback.url.com/callback","field9":"http:\/\/some.callback.url.com/callback2",
# "field10":"someAdditionalID","request_id":null,"variable1":"val1","variable2":"val2",
# "variable3":"val3","version":"1.3","field11":1631281107}'

#  Burp params
inputJSON = inJSON
clientSecret = str(mSecret)
apiName = str(api)

output = str(dir())
apiSignDic = {
    "api1": 
    ["field1","field2","field11","field3","field6","field10","field4","field5","field7","field8","field9"],
    "api2":
    ["field1","field2","field11","intent","field6","field10","agent_name"],
    "api3":
    ["field1","field2","field11","field3","field6","field10","field4","field5","field7","field8","field9"],
    "api4":
    ["field1","field2","field11","field3","field6","field10","field4","field5","field7","field8","field9","field13"],
    "api5":
    ["field1","field2","field11","field3","field6","field10","field4","field5","field7","field8","field9","field13"]}

jsonObj = json.loads(inputJSON)

# null values ignored
concat = ""
for k in apiSignDic[apiName]:
    for i in jsonObj:
        if i == k and jsonObj[i]:
            concat += str(jsonObj[i])  + "\n"

concat = concat.replace("\n","")
m = hashlib.sha384()
m.update(concat.encode('utf-8'))
hResult = m.hexdigest()
output = hResult

# output = concat
# print(concat)
# print("\n")
# print(hResult)
# print(apiSignDic[apiName][0])

# Burp raw example
# POST /controller/api HTTP/1.1
# Host: host.com
# Accept: */*
# Content-Type: application/json
# timestamp: <@set_ts('true')><@timestamp/><@/set_ts>
# Custom-Authentication: <@_APISign('api/name' ,'client-provided secret',
# 'Hackvertor code execution code')><@/_APISign>
# Content-Length: 0

# <@set_inJSON('false')>
# {
#     "field": "xxxx",
#     "field2": "xxxx",
#     "field3": "xxx",
#     "field4": "xxx",
#     "field5": 100,
#     "field6": "xxxxx",
#     "cvv_encrypted": "<@_EncryptCVV('client-provided secret','Hackvertor code execution code')>223<@/_EncryptCVV>",
#     "timestamp": <@get_ts/>
# }
# <@/set_inJSON>

# Burp result example
# POST /controller/api HTTP/1.1
# Host: host.com
# Accept: */*
# Content-Type: application/json
# timestamp: <@set_ts('true')><@timestamp/><@/set_ts>
# Custom-Authentication:0beedf0971f784a6635a14ae8edc4c000e697f714a273409838b2fc701678d526f5848a416e7da8f4e90525f5c56f7bd
# Content-Length: 0

# {
#     "field": "xxxx",
#     "field2": "xxxx",
#     "field3": "xxx",
#     "field4": "xxx",
#     "field5": 100,
#     "field6": "xxxxx",
#     "cvv_encrypted": "AESEncryptionResult",
#     "timestamp": 1631281107
# }

HV_JWT_Based_Request_Signing.py

This Hackvertor script shows an example for creating JWT based message signature for an HTTP request. The code takes as parameters: path, query, and HTTP body. Hashes them, and creates a one-time JWT token that is signed with the client’s RSA key. The JWT token is used for both authentication and message signing.

#!/usr/bin/env python
# -*- coding: utf-8 -*-  
#
# This Hackvertor script shows an example for creating JWT based message signature for HTTP requests.
# The code takes as parameters: path, query, and HTTP body. Hashes them, and creates a one-time JWT 
# token that is signed with the client's RSA key. The JWT token is used for both authentication and message signing. 

import json
import time
import math
import random
import base64
from hashlib import sha256
from collections import OrderedDict
import re
import sys
import os
from java.util import Base64
from javax.crypto import Cipher
from javax.crypto.spec import IvParameterSpec
from javax.crypto.spec import SecretKeySpec
from java.security import Signature
from java.security import PrivateKey
from java.security.spec import PKCS8EncodedKeySpec
from java.security import KeyFactory

# Example of JWT token produced by the code 
# {"typ":"JWT","alg":"RS256"}.{"uri":"/v1/controller/method","nonce":2080922210380511619,"iat":1644951606,
# "exp":1644951661,"sub":"41604da2-da3f-462a-bac1-3c4c0419f40e","bodyHash":
# "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126"}.validsignature

private_key = """
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
"""
api_key = "41604da2-da3f-462a-bac1-3c4c0419f40e"

# Default param values for testing

if not 'b_full_url' in globals():
        path = "/v1/controller/method"
# else from <@set_b_body('false')>XX<@/set_b_body> set in Burp
else:
        path = str(b_full_url)

if not 'b_body' in globals():
        body_json = ""
else:
        # note that using the below line will produce a string object different 
        # from the the input, thus just take body as string
        # body_json = json.loads(str(b_body))
        body_json = str(b_body)

timestamp = time.time()
nonce = random.getrandbits(63)
timestamp_secs = int(math.floor(timestamp))
# In case there is a need for special escaping of an input data (HTTP request body in our case) 
# before it would be signed (replicating client's behaviour)
# path = path.replace("[", "%5B")
# path = path.replace("]", "%5D")

# Here we need of OrderedDict to maintain the correct order of JSON keys
# else, the signature validation in certain cases may break, since the server may rebuild the
# payload not as it was sent
# token = OrderedDict([
#         ("uri", path),
#         ("nonce", nonce),
#         ("iat", timestamp_secs),
#         ("exp", timestamp_secs + 55), 
#         ("sub", api_key),
#         ("bodyHash", str(sha256(json.dumps(body_json).encode("utf-8"))))
#         ]
# )
# json_payload = json.dumps(token,sort_keys=False)
# json_payload = re.sub("\s+","", json_payload)

# original code from
token = {
            "uri": path,
            "nonce": nonce,
            "iat": timestamp_secs,
            "exp": timestamp_secs + 55, 
            "sub": api_key,
            # Hashing request body as a string
            "bodyHash": sha256(body_json.encode("utf-8")).hexdigest()
            # Hashing request body as serialized object
            # Serialize obj to a JSON formatted str using this conversion table. 
            # https://docs.python.org/3/library/json.html
            # "bodyHash": sha256(json.dumps(body_json).encode("utf-8")).hexdigest()
        }

# As with HTTP message body, making sure our JWT's token format will match the one expected by the server;
# here should be present any specific data transformations as they performed by the client, else server 
# may fail to verify our signature
json_payload = json.dumps(
            token, separators=(",", ":"), cls=None).encode("utf-8")
# clearing whitespaces   
json_payload = re.sub("\s+","", json_payload)

# Construction header and payload parts
algorithm = "RS256"
segments = []
header = {"typ": "JWT", "alg": "RS256"}
json_header = json.dumps(header, separators=(",", ":")).encode()
segments.append(base64.urlsafe_b64encode(bytes(json_header)).replace(b"=", b""))
segments.append(base64.urlsafe_b64encode(bytes(json_payload)).replace(b"=", b""))
jwtToSign = b".".join(segments)

# Clearing out the private key; removing header,footer and whitespace
private_key = private_key.replace("-----BEGIN PRIVATE KEY-----","")
private_key = private_key.replace("-----END PRIVATE KEY-----","")
private_key = re.sub("\s+","",private_key)

# Creating the singature
keyBytes = base64.b64decode(private_key)
keySpec = PKCS8EncodedKeySpec(keyBytes);
keyFactory = KeyFactory.getInstance("RSA");
privKey = keyFactory.generatePrivate(keySpec)
Signer = Signature.getInstance("SHA256withRSA");
Signer.initSign(privKey)
Signer.update(jwtToSign)
signatureBytes = Signer.sign()

# Appending the signature to the header and the payload
segments.append(base64.urlsafe_b64encode(signatureBytes).replace(b"=", b""))
encoded_string = b".".join(segments)

# Sending back the result to Burp
output = encoded_string.decode("utf-8")

# debug output while testing the code without Burp
# output = json.dumps(body_json).encode("utf-8")
# print("\n" + str(output))


# Burp Request will look like
# POST <@set_b_full_url('false')>/v1/controller/api/object<@/set_b_full_url> HTTP/1.1
# Host: host.com
# User-Agent: python-requests/2.26.0
# Accept-Encoding: gzip, deflate
# content-type: application/json
# Accept: */*
# Connection: close
# X-API-Key: 41604da2-da3f-462a-bac1-3c4c0419f40e
# Authorization: Bearer <@_OurScriptTag('e03a3b0a2e3adb9507b2e0e758bb0a6a')><@/_OurScriptTag>
# Content-Length: 138

# <@set_b_body('false')>{"id":"someid","status":"APPROVED","address":"address","tag":"s"}<@/set_b_body>

# result
# GET /v1/controller/api/object HTTP/1.1
# Host: host.com
# User-Agent: python-requests/2.26.0
# Accept-Encoding: gzip, deflate
# content-type: application/json
# Accept: */*
# Connection: close
# X-API-Key: 41604da2-da3f-462a-bac1-3c4c0419f40e
# Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIzZWVjODY1OS03MWJmLTg1YTQ
# tMmQxNi0yY2ZlMGE4Nzc4YmQiLCJib2R5SGFzaCI6IjQyYjcxZTBhYmY0NTliNzQ4MTNmMzAwNWRjMGIwNmUzMWNjYmI1M
# 2RmMWI1YThkZTEyZGRlYzE2Mzk0ODMxODUiLCJ1cmkiOiIvdjEvY29udHJvbGxlci9hcGkvb2JqZWN0Iiwibm9uY2UiOjQ
# xNzAyOTQ0Njg4MzM4OTY2MiwiZXhwIjoxNjQ2NDgzMjk4LCJpYXQiOjE2NDY0ODMyNDN9.oDNYpgp2XdCc5uqlcthHM5lI
# Xm59Q5razTOm90AjdSzrhZv5Xab-8IMUO6GOPdioswZO4L14byRGo-mAQzivne-EROUSm9YOQMwBh6c4MHXkQCKA-U0cre
# G7hViyoSygFHsgBaPioNCBCTLSY8Xv-f3Ftz5EIZ9kd6NwThCMt0UC-GepD2QDdSutNJrNGlF-K3juOTYj3zcLxHJgbQk2
# LuwkWz0qp7kADmWmqTJNyEZaTCm16U3sS1GoT-gE_41f8W__KLdoLH3PPtZR2f6YhDBNix4P1Fb5E8xenQXTwI_eJzRi_w
# jpLmYnrtzxNxFsy4fXnRFUyRezwHVXlqdv1PbH_FaqBuKlM2u6_MqZ4h2-Ys1NTxbHU7kGqGbGw2L_xIu5A6J__9z3Q447
# uHjaIioBNUutBh_tYxFkBlL8RtKtwC3YyHim4vbyP_-C5ZmJiETx69vHW3KqdGXetRBYYjgSRW5lMJnqjuC2xRy3Dc0kRk
# yhe241kCCh3MBXjcJDFtFmbWInrvb_h5kzrKjZEc__pbyGDAlW-NmhmdF11obi8-j_Np2y93VF9xcA3F13m38z1wpEqd1k
# GG2W9-n_zgetLV4qsTK5lgzuVhlKzDiGpiydrphz2ZdiZj3YzC2w1bwL4v_fHmqnXBNyjCnNkHzi7q2vo7W67e6SZB9zTfs
# Content-Length: 138

# {"id":"someid","status":"APPROVED","address":"address","tag":"s"}

# {"typ":"JWT","alg":"RS256"}.{"sub":"3eec8659-71bf-85a4-2d16-2cfe0a8778bd","bodyHash":
# "42b71e0abf459b74813f3005dc0b06e31ccbb53df1b5a8de12ddec1639483185","uri":
# "/v1/controller/api/object","nonce":417029446883389662,"exp":1646483298,"iat":1646483243}.
# oDNYpgp2XdCc5uqlcthHM5lIXm59Q5razTOm90AjdSzrhZv5Xab-8IMUO6GOPdioswZO4L14byRGo-mAQzivne-
# EROUSm9YOQMwBh6c4MHXkQCKA-U0creG7hViyoSygFHsgBaPioNCBCTLSY8Xv-f3Ftz5EIZ9kd6NwThCMt0UC-GepD
# 2QDdSutNJrNGlF-K3juOTYj3zcLxHJgbQk2LuwkWz0qp7kADmWmqTJNyEZaTCm16U3sS1GoT-gE_41f8W__KLdoLH3
# PPtZR2f6YhDBNix4P1Fb5E8xenQXTwI_eJzRi_wjpLmYnrtzxNxFsy4fXnRFUyRezwHVXlqdv1PbH_FaqBuKlM2u6_
# MqZ4h2-Ys1NTxbHU7kGqGbGw2L_xIu5A6J__9z3Q447uHjaIioBNUutBh_tYxFkBlL8RtKtwC3YyHim4vbyP_-C5Zm
# JiETx69vHW3KqdGXetRBYYjgSRW5lMJnqjuC2xRy3Dc0kRkyhe241kCCh3MBXjcJDFtFmbWInrvb_h5kzrKjZEc__p
# byGDAlW-NmhmdF11obi8-j_Np2y93VF9xcA3F13m38z1wpEqd1kGG2W9-n_zgetLV4qsTK5lgzuVhlKzDiGpiydrph
# z2ZdiZj3YzC2w1bwL4v_fHmqnXBNyjCnNkHzi7q2vo7W67e6SZB9zTfs

Using External Python Libraries

I wouldn’t recommend using external python libraries. Instead, either use Java classes or copy relevant Python libs code snippets into your code.

  • In part one I showed that we can easily embed JS libs into our code by just copying the minified version of them. That scenario won’t work with Python.
  • Using external Python libs, even designed for 2.7, would not be sufficient since they mostly cross-depend on valid python repo ecosystem/installation.

Install the latest python 2.7 with pip, install the needed package using PIP, and then copy the library folder (Python27\Lib\site-packages\jwt) to the Lib folder loaded by Jython.

  • Local run: in the same folder as HV script file
  • Burp: temporary Lib folder that is set at each start; you can get the path by calling the next code from Hackvertor script import sys output = str(sys.path)

Wrap up

Have fun!