JSON Crypto Helper a Ruby-based Burp Extension for JSON Encryption/Decryption - Part I
Burp Suite is one of my favorite tools when performing security assessments of web applications. Although it can handle almost all the basic situations by itself, corner case applications need some extra Burp customizations to maximize your assessment capabilities. For this purpose, Burp provides an extension API that enables you to extend its functionality. Extensions can be written in Java, Python or Ruby.
This is the first post in a three-part series about how to write a simple Ruby extension that helps deal with encrypted JSON messages (read part two here). You can download the complete extension code here. It was inspired by the JSON Decoder extension by Michal Melewski, which is available here.
Context
The last mobile application I tested used encrypted JSON messages to communicate with the server and any tampering or fuzzing was almost impossible. Requesting URLs in a browser looked like this:
As you can see, only the value string was encrypted and encoded with base64:
{
"hello":"9DpW68SsC5nHi5PeyXEHEA==",
"test":{
"input1":"irTKsV484FzLRuH2A1r42Q==",
"input2":"fFAUsaRu8W47lfusAtzV88ewPj9etXuoPtbCUUMzQHs="
}
}
From a security perspective, the additional encryption step is not really efficient because the encryption/decryption is performed locally on the device; it is not difficult to retrieve all the information needed to decrypt the message directly from the application binary. And that's what I did. So, after reversing the binary (an iOS application), I managed to extract the following information (NOTE: the encryption key and the IV have been changed for demonstration purposes):
Encryption Key (hex digits): 0cc91185341d6a27c380e97fed30b4a1dcafd7f5044f09cc7fa6b3fa0df290b
IV (hex digits): abdad4a94d544b52f4782e2856f82874
Encryption Algorithm: AES 128bit CBC
You can verify this using OpenSSL from the terminal:
$ echo "9DpW68SsC5nHi5PeyXEHEA==" | openssl enc -aes-128-cbc -base64 -d -K a0cc91185341d6a27c380e97fed30b4a1dcafd7f5044f09cc7fa6b3fa0df290b -iv abdad4a94d544b52f4782e2856f82874
=> world!
So, now you have all you need to get rid of the encryption and use Burp the same way you do with unencrypted content. The end goal is to automatically decrypt the value anywhere in the Burp interface, automatically encrypt the value you want to send to the server (using the Repeater tool for example) and fuzz the JSON parameters with on-the-fly encryption (Intruder tool).
Setup the environment
Because we will use Ruby to implement the extension, you need to setup the environment with JRuby. The easiest way to do it is using RVM. Just install it using the RVM install guide and run the following commands in the directory you will create your Ruby extension:
$ rvm install jruby
$ rvm --ruby-version use jruby@burp --create
This will install JRuby (if you don't have it yet) and create the gemset burp. It also adds .ruby-version and .ruby-gemset files to tell RVM which ruby version and which gemset to use when entering the directory.
Then just run Burp like this:
JRUBY_HOME=$MY_RUBY_HOME java -XX:MaxPermSize=1G -Xmx1g -Xms1g -jar [burp_install_dir]/burpsuite_pro_v1.6.09.jar
It is important to setup the JRUBY_HOME environment variable to tell Burp where the JRuby environment is located.
IBurpExtender interface
The first thing to do is to implement the IBurpExtender interface with the method #registerExtenderCallbacks.
require 'java'
java_import 'burp.IBurpExtender'
class BurpExtender
include IBurpExtender
def registerExtenderCallbacks(callbacks)
callbacks.setExtensionName("JSON Crypto Helper")
end
end
This method is called when the extension is loaded; passing a callbacks instance that implements the IBurpExtenderCallbacks interface. This instance will be used to perform many actions, for example here, #setExtensionName is used to inform the extension name that will be displayed in Burp.
To load the extension you need first to configure Burp Suite Extender tool (Options tab) and specify the path where the JRuby jar is installed. If you're using RVM, it should be in your RVM project path, commonly installed here: $HOME/.rvm/rubies/jruby-x.x.x/lib/jruby.jar.
Then select Add in the Extensions tab, select the extension type Ruby and your .rb file. You should see your new extension showing up in the list:
Creating a Custom Tab
In order to interact with any Burp tools, you will need to create a specific tab that will display the decrypted JSON content. To do so, you need to implement the IMessageEditorTabFactory interface and register your extension with #registerMessageEditorTabFactory. Then you create a separate class that will take care of it (I called it JSONDecryptorTab) and instantiate it in the #createNewInstance method. Because you will use the callbacks a lot, it is a good idea to create an instance variable to easily access it inside JSONDecryptorTab instances.
require 'java'
java_import 'burp.IBurpExtender'
java_import 'burp.IMessageEditorTabFactory'
class BurpExtender
include IBurpExtender
include IMessageEditorTabFactory
attr_reader :callbacks
def registerExtenderCallbacks(callbacks)
@callbacks = callbacks
callbacks.setExtensionName("JSON Crypto Helper")
callbacks.registerMessageEditorTabFactory(self)
end
def createNewInstance(controller, editable)
JSONDecryptorTab.new(@callbacks, editable)
end
end
Let's see how the JSONDecryptorTab class looks like. First it needs to implement the IMessageEditorTab interface:
class JSONDecryptorTab
include IMessageEditorTab
...[SNIP]...
and the following methods:
- #initialize: the constructor, which setups some instance variable:
def initialize(callbacks, editable)
# Burp Suite useful helpers:
@helper = callbacks.get_helpers()
# Create a Burp's plain text editor to use with this extension:
@txt_input = callbacks.create_text_editor()
# Indicates if the text editor is read-only or not:
@editable = editable
end
- #getTabCaption: the tab name that will be displayed by Burp
def getTabCaption
return "JSON Crypto Helper"
end
- #isEnabled: this method is invoked each time Burp displays a new message to check if the new custom tab should be displayed. It should return a Boolean (see below).
- #setMessage: this method is invoked each time a new message is displayed in your custom tab. This method will take care of processing the message, decrypting the JSON and displaying it (see below).
- #getMessage: this method is invoked each time you leave the custom tab. It returns an array of bytes that will be used by Burp (see below).
- #isModified: this method is invoked after calling #getMessage and if the editor tab is editable (in the Repeater tool for example). It should return true if the message has been edited. You simply use the value returned by #text_modified? of the text editor object:
def isModified
return @txt_input.text_modified?
end
- #getSelectedData: not in use, but still need to be defined.
Decrypting JSON
Burp invokes the #isEnabled method to know if the custom tab has to be displayed for a given message (request or response). In this method, you will simply analyze the content and check if it is a proper JSON:
def isEnabled(content, is_request)
return false if content.nil? or content.empty?
if is_request
info = @helper.analyze_request(content)
else
info = @helper.analyze_response(content)
end
return json?(info, is_request)
end
The method #json? uses the information already gathered by Burp:
def json?(info, is_request)
if is_request
return info.content_type == IRequestInfo::CONTENT_TYPE_JSON
end
return (info.stated_mime_type == "JSON" or info.inferred_mime_type == "JSON")
end
Don't forget to import the java interface for the constant CONTENT_TYPE_JSON:
java_import 'burp.IRequestInfo'
Now the tab should be displayed on every JSON request or response. Next step, you need to fill it with the decrypted JSON. The #setMessage method will take care of this:
def setMessage(content, is_request)
return if content.nil? or content.empty? or @txt_input.text_modified?
if is_request
info = @helper.analyze_request(content)
else
info = @helper.analyze_response(content)
end
headers = content[ 0..(info.get_body_offset - 1) ].to_s
body = content[ info.get_body_offset..-1 ].to_s
body = process_json(body, :decrypt) if json?(info, is_request)
@txt_input.text = (headers + body).to_java_bytes
@txt_input.editable = @editable
end
Basically, the method analyses the message content, extracts the body, processes it and fills the editor.
#process_json will take care of decrypting/encrypting the JSON message. First, it will parse the content using the JSON Ruby library, decrypt/encrypt it and finally generate a well formatted JSON using #pretty_generate from the same library:
def process_json(json, mode = :no_encryption)
message = ""
begin
json_tmp = JSON.parse(json)
if mode == :decrypt
json_tmp = decrypt_json(json_tmp)
elsif mode == :encrypt
json_tmp = encrypt_json(json_tmp)
end
message << JSON.pretty_generate(json_tmp)
rescue OpenSSL::Cipher::CipherError => e
# not encrypted, ignore and return the original message
puts "process_json: Cryptography error: #{e.message}"
message << json
rescue JSON::ParserError => e
puts "process_json: Parsing error: #{e.message}"
message << json
end
message
end
Note that in case of decryption/encryption or parsing errors, the method rolls-back to the unmodified JSON message.
Decryption and Encryption Routines
I created a separate module for all the cryptographic methods. You will need to include it in the JSONDecryptorTab class.
module EncryptionHelper
# Key and IV retrieved from the application binary
KEY = "\xa0\xcc\x91\x18\x53\x41\xd6\xa2\x7c\x38\x0e\x97\xfe\xd3\x0b\x4a\x1d\xca\xfd\x7f\x50\x44\xf0\x9c\xc7\xfa\x6b\x3f\xa0\xdf\x29\x0b"
IV = "\xab\xda\xd4\xa9\x4d\x54\x4b\x52\xf4\x78\x2e\x28\x56\xf8\x28\x74"
ALGO = "aes-128-cbc"
def encrypt text
cipher = OpenSSL::Cipher::Cipher.new(ALGO)
cipher.encrypt
cipher.key = KEY
cipher.iv = IV
ciphertext = cipher.update(text)
ciphertext << cipher.final
end
def decrypt ciphertext
cipher = OpenSSL::Cipher::Cipher.new(ALGO)
cipher.decrypt
cipher.key = KEY
cipher.iv = IV
text = cipher.update(ciphertext)
text << cipher.final
end
def encode_b64 text
[text].pack('m0')
end
def decode_b64 encoded_text
encoded_text.unpack('m')[0]
end
# This method will base64-decode and decrypt each value recursively
def decrypt_json(json)
json.each do |key, value|
if value.is_a?(Hash)
json[key] = decrypt_json(value)
else
value_tmp = decode_b64(value)
if value_tmp.empty?
json[key] = value
else
json[key] = decrypt(value_tmp)
end
end
end
json
end
# This method will encrypt and base64-encode each value recursively
def encrypt_json(json)
json.each do |key, value|
if value.is_a?(Hash)
json[key] = encrypt_json(value)
else
if value.empty?
json[key] = value
else
json[key] = encode_b64(encrypt(value))
end
end
end
json
end
end
Also, don't forget to require openssl and json in the file:
require "openssl"
require "json"
You are now able to decrypt JSON messages in the new custom tab, which is available in any tool. For example, in the Proxy tool:
Parting Thoughts
With this first post we covered the basics of Burp Extender tool and learned how to write a simple ruby-based extension. We took an encrypted JSON as an example but this also works with any content you want to process before displaying. For example, it won't be too complicated to implement a parser for an in-house communication protocol used by the application.
In the next post I will explain how to automatically encrypt the JSON values using the Repeater tool. This will enable you to write or modify a plaintext JSON and send it encrypted to the remote server.
Stay tuned!
ABOUT TRUSTWAVE
Trustwave is a globally recognized cybersecurity leader that reduces cyber risk and fortifies organizations against disruptive and damaging cyber threats. Our comprehensive offensive and defensive cybersecurity portfolio detects what others cannot, responds with greater speed and effectiveness, optimizes client investment, and improves security resilience. Learn more about us.