Overview
mitmproxy integration is what enables bet365-re-js to intercept HTTP responses, identify obfuscated JavaScript, deobfuscate it on-the-fly, and inject the transformed code back into the browser. This creates a seamless debugging experience where the browser executes readable JavaScript instead of obfuscated code.
What is mitmproxy?
mitmproxy is an interactive HTTPS proxy that allows you to intercept, inspect, and modify HTTP traffic. bet365-re-js uses mitmproxy’s Python scripting capabilities to:
- Intercept responses containing obfuscated JavaScript
- Save the obfuscated code to disk
- Run the Node.js deobfuscation pipeline
- Replace the obfuscated code with deobfuscated code
- Inject pre/post-transform instrumentation code
- Return the modified response to the browser
Architecture
Starting mitmproxy
The project includes a simple shell script to start mitmproxy with the custom addon:
#!/usr/bin/env bash
mitmproxy -s mitmproxy/src/python/main/download-payload.py
Run it with:
This starts mitmproxy on port 8080 by default, loading the JavascriptExtractor addon.
Browser Configuration
Configure your browser to use mitmproxy as a proxy server:
open -a "Google Chrome" --args \
--proxy-server=http://localhost:8080 \
--enable-logging \
--v=1 \
--user-data-dir=$(pwd)/chrome-profile
google-chrome \
--proxy-server=http://localhost:8080 \
--enable-logging \
--v=1 \
--user-data-dir=$(pwd)/chrome-profile
chrome.exe ^
--proxy-server=http://localhost:8080 ^
--enable-logging ^
--v=1 ^
--user-data-dir=%cd%\chrome-profile
Using a separate Chrome profile (--user-data-dir) ensures your main browsing profile isn’t affected by the proxy configuration.
The core of the integration is the JavascriptExtractor class in download-payload.py:
class JavascriptExtractor:
current_directory = Path(__file__).parent.absolute()
project_root_directory = current_directory.joinpath("../../../..").absolute()
output_base_path = str((project_root_directory / "output").absolute()) + "/"
javascript_base_path = str((project_root_directory / "mitmproxy/src/javascript").absolute()) + "/"
file_delimiter = b'\x03'b'\x06'b'\x05'
obfuscated_start_string = "(function(){ var _0x123a="
pre_transform_code_file_name = "pre-transform-code.js"
post_transform_code_file_name = "post-transform-code.js"
refactor_script_on_fly = True
Configuration
The addon configures itself on startup:
def configure(self, updated):
# Filter for specific endpoint containing JavaScript
self.filter = flowfilter.parse("~u '/Api/1/Blob'")
# Find node executable
self.node_executable_file = subprocess.run(
["which", "node"],
capture_output=True,
text=True
).stdout.strip()
# Load any pre-transformed code mappings
transformed_files = Path(self.javascript_base_path).glob('**/transformed-*.js')
for transformed_file in transformed_files:
identifier = transformed_file.name.removeprefix("transformed-").removesuffix(".js")
post_transform_file = Path(self.javascript_base_path + f'/post-transform-{identifier}.js')
if post_transform_file.is_file():
transformed_file_contents = transformed_file.read_text()
post_transform_file_contents = post_transform_file.read_text()
self.replace_contents[jsmin(transformed_file_contents)] = jsmin(post_transform_file_contents)
Request Interception
The addon modifies outgoing requests to hide automation:
def request(self, flow: http.HTTPFlow):
# Remove "Headless" from User-Agent to avoid detection
flow.request.headers["User-Agent"] = self.__strip_headless(
flow.request.headers["User-Agent"]
)
The response method is where the magic happens:
def response(self, flow: http.HTTPFlow) -> None:
if flowfilter.match(self.filter, flow):
file_identifier = str(time.time())
split_content_bytes = flow.response.content.split(self.file_delimiter)
sent_bytes_array = []
for index, received_file_content_bytes in enumerate(split_content_bytes):
sent_bytes = received_file_content_bytes
start_byte = received_file_content_bytes[0:1]
content_bytes = received_file_content_bytes[1:]
content_start_string = content_bytes[:len(self.obfuscated_start_string)].decode()
is_obfuscated_content = content_start_string.startswith(self.obfuscated_start_string)
if is_obfuscated_content:
logging.info("Intercepting response: " + flow.request.url)
# Save obfuscated file
received_file_name = self.__get_file_name(
self.output_base_path,
received_file_content_bytes,
file_identifier,
"received",
index
)
received_file = self.__create_file(received_file_name, "received")
if received_file is not None:
received_file.write(content_bytes)
received_file.close()
# Run deobfuscation on the fly
if self.refactor_script_on_fly:
refactor_file = Path(self.javascript_base_path + "/refactor-obfuscated-code-jscodeshift.js")
deobfuscated_file_name = self.__get_file_name(
self.output_base_path,
received_file_content_bytes,
file_identifier,
"deobfuscated",
index
)
# Execute Node.js deobfuscation
subprocess.run([
self.node_executable_file,
refactor_file,
received_file_name,
deobfuscated_file_name
], stdout=subprocess.PIPE)
# Build complete file with instrumentation
pre_transform_code_content_string = Path(
self.javascript_base_path + self.pre_transform_code_file_name
).read_text()
deobfuscated_file_content_minified_string = jsmin(
Path(deobfuscated_file_name).read_text()
)
post_transform_code_content_string = Path(
self.javascript_base_path + self.post_transform_code_file_name
).read_text()
# Apply any additional replacements
for replace_content in self.replace_contents:
deobfuscated_file_content_minified_string = \
deobfuscated_file_content_minified_string.replace(
replace_content,
self.replace_contents[replace_content]
)
complete_file_content_string = (
pre_transform_code_content_string +
deobfuscated_file_content_minified_string +
post_transform_code_content_string
)
else:
# Use pre-deobfuscated file
deobfuscated_file = Path(self.javascript_base_path + "/deobfuscated.js")
complete_file_content_string = deobfuscated_file.read_text()
# Save sent file
sent_file_name = self.__get_file_name(
self.output_base_path,
received_file_content_bytes,
file_identifier,
"sent",
index
)
sent_file = self.__create_file(sent_file_name, "sent")
if sent_file is not None:
pretty_js_string = jsbeautifier.beautify(complete_file_content_string)
sent_file.write(pretty_js_string.encode())
sent_file.close()
sent_bytes = start_byte + complete_file_content_string.encode()
sent_bytes_array.append(sent_bytes)
# Replace response content
flow.response.content = self.file_delimiter.join(sent_bytes_array)
File Structure
The addon creates several files in the output/ directory:
output/
├── 1741234567.123-received-14.js # Original obfuscated code
├── 1741234567.123-deobfuscated-14.js # Deobfuscated code
└── 1741234567.123-sent-14.js # Final code sent to browser
Filenames contain:
- Timestamp: Unix timestamp with microseconds
- Type:
received, deobfuscated, or sent
- Index: File index in the response
Instrumentation Code
The pre-transform-code.js file is injected before the deobfuscated code. It provides:
- Logging utilities
- State tracking functions
- Debug helpers
var consoleLogger = function(string) {
if (console) {
console.log(string);
}
};
var print = function(label, input) {
if (Array.isArray(input)) {
var object = Object.assign({}, input);
Object.keys(object).forEach((key, value) => {
try {
object[key] = object[key];
} catch (exception) {
consoleLogger(exception)
}
});
consoleLogger(label + ": " + toJsonString(object));
} else if(typeof input === "object") {
consoleLogger(label + ": " + toJsonString(input));
} else {
consoleLogger(label + ": " + input);
}
};
var toJsonString = function(input) {
return JSON.stringify(input, circularObjectReferenceToString);
};
var beforeFunctionState = function(functionName, globalState) {
var stateContext = new Map([
["functionName", functionName],
["before", globalState.slice()]
]);
return stateContext;
};
var afterFunctionState = function(stateContext, globalState) {
var beforeFunctionGlobalState = stateContext.get("before");
var changes = getGlobalStateChanges(beforeFunctionGlobalState, globalState);
if (changes) {
stateContext.delete("before");
stateContext.set("change", changes);
states.push(stateContext);
}
};
var tapeKeywords = {};
Post-transform Code
The post-transform-code.js file is injected after the deobfuscated code for final setup:
var consoleLogger = function(string) {
if (console) {
console.log(string);
}
};
Detecting Obfuscated Code
The addon identifies obfuscated JavaScript by checking if the content starts with a specific pattern:
obfuscated_start_string = "(function(){ var _0x123a="
# Check if content is obfuscated
content_start_string = content_bytes[:len(self.obfuscated_start_string)].decode()
is_obfuscated_content = content_start_string.startswith(self.obfuscated_start_string)
This detection pattern may need updating if bet365 changes their obfuscation wrapper. Monitor the received-*.js files to identify new patterns.
Handling Multiple Files
bet365 sends multiple JavaScript and CSS files in a single response, delimited by a special byte sequence:
file_delimiter = b'\x03'b'\x06'b'\x05'
# Split response into individual files
split_content_bytes = flow.response.content.split(self.file_delimiter)
Each file has a type byte prefix:
4: JavaScript file
5: CSS file
@staticmethod
def __is_js(file_content):
return bool(file_content) and 4 == file_content[0]
@staticmethod
def __is_css(file_content):
return bool(file_content) and 5 == file_content[0]
On-the-fly vs Pre-deobfuscated
The addon supports two modes:
Pros:
- Always uses latest deobfuscation logic
- Handles new obfuscation patterns immediately
Cons:
- Slower response time (Node.js process overhead)
- Can block browser if deobfuscation is slow
Pros:
- Fast response time
- Predictable performance
Cons:
- Must manually update
deobfuscated.js
- Doesn’t adapt to code rotation
Debugging
View Console Output
When Chrome is started with --enable-logging --v=1, console output is written to:
<user-data-dir>/chrome_debug.log
View just the JSON output:
tail -f <user-data-dir>/chrome_debug.log | \
sed -En "s/.*inside.*\]: (.*)\", source\: \(3\)/\1/p"
mitmproxy Web Interface
mitmproxy also provides a web interface at http://localhost:8081 (default) where you can:
- Inspect all intercepted requests and responses
- View request/response headers and bodies
- Replay requests
- Filter traffic
Recommendations
Use Dedicated Chrome Profile
Always use a separate Chrome profile for mitmproxy to avoid affecting your main browsing session.
Install Clear Cache Extension
Install a clear cache extension to quickly clear the cache without opening DevTools.
Monitor Output Directory
Watch the output/ directory to see what files are being intercepted and transformed.
Check Logs
Monitor mitmproxy logs for errors or issues with the deobfuscation process.
Using obfuscated-code-logger.js (if enabled) might add significant noise to console debugging output.
Troubleshooting
No Files in Output Directory
- Verify mitmproxy is running
- Check browser proxy settings
- Ensure you’re visiting bet365.com pages that load the obfuscated JavaScript
- Check the URL filter in
configure() method
Deobfuscation Fails
- Check Node.js is installed and in PATH
- Verify the deobfuscation script path is correct
- Look for errors in mitmproxy logs
- Check if the obfuscation pattern has changed
Browser Shows Certificate Error
Next Steps