How to: Burp ♥ OpenVPN

When performing security tests, you will often be required to send all of your traffic through a VPN. If you don’t want to send all of your local traffic over the same VPN, configuring an easy-to-use setup can sometimes be a pain. This post outlines one possible way of configuring Burp Suite to send all its traffic through a remote VPN, without having to run the VPN on your own machine.

In this guide, I will describe a setup that makes use of the following tools. Note, however, that most of these can be replaced by similar tools to accomplish the same goals.

  • Burp Suite
  • PuTTY
  • OpenVPN running on a Virtual Private Server (VPS)
  • A second VPS as a jumphost (not required if you have a static IP)
  • Browser extension Switchy Omega in Chrome or Firefox


There are a few reasons why configuring a VPN to execute your security tests may be a good idea:

  1. Testing assets that are not publicly available, e.g. located on an internal network; this is often the case when performing internal infrastructure tests or tests against UAT environments;
  2. Testing public assets from a whitelisted environment, e.g. web applications that are usually hidden behind a WAF, applications that are internet-accessible, but only available to a number of whitelisted networks, etc;
  3. Bug bounty programs that require the use of their VPN as a condition to participate in the program.

To accommodate this need, you may be inclined to install an OpenVPN client on your local testing machine and get going. While this definitely works, I found that separating my testing activities from other network activity is not only a privacy-conscious decision, but also helps towards freeing up as much of the VPN bandwidth as is possible, because now it is no longer occupied with superfluous traffic.

The setup

Architecturally, the solution that I will describe looks like this:

High-level diagram of proxying traffic through a VPN using Burp Suite.

VPN tunnel

The VPN tunnel is of course the core of this setup, and will allow you to tunnel your (selected) traffic either towards assets inside a target’s environment, or towards internet-accessible assets, but originating from the target’s network. In other words, the web applications you are testing will see you coming from Target X’s IP address range, rather than from your own.

Jump host

If you have a static IP on your home or office network, or this is intended as a temporary setup (i.e. your current IP will do), you can skip this.

Otherwise, this jump host will serve as a bridge towards your VPN-connected EC2 instance. Most importantly, the VPS’s static IP will allow us to configure the traffic to and from this jump host to avoid being sent over the VPN.

On this jump host, make sure you have access to the EC2 instance’s private key (if applicable), and set up a SOCKS proxy using the following command:

ssh -i ~/ssh-private-key.pem -D 2222

This will set up an SSH tunnel that will redirect all traffic proxied through port 2222 on the jump host, towards the original destination via the AWS EC2 instance (i.e. through the VPN when the VPN is activated).

AWS EC2 instance

Because it’s cheap, I opted for a t2.micro instance in AWS EC2 to set up the connection with the VPN. I am a fan of Debian, so I spun up an Ubuntu 18.04 image. Once up and running, you will need the following configured:

  • Install OpenVPN;
  • Upload the ovpn file containing the config of the VPN you want to connect to;
  • Whitelist your jump host (or home/office IP) from the VPN by directing traffic through the usual gateway (source);
  • Whitelist any local DNS servers if needed;
# install OpenVPN client
sudo apt install openvpn

# find out and write down your local gateway's IP address
netstat -anr

# find out and write down your local DNS servers' IP addresses
# (I needed this to allow DNS resolution in AWS EC2 when the VPN is running)
systemd-resolve --status

# Make sure both IPs you wrote down are not redirected through the VPN:
sudo route add -host <your-jumphost-ip> gw <your-local-gateway>
sudo route add -host <your-local-dns-server> gw <your-local-gateway>

# Start the VPN!
sudo openvpn --config ./openvpn-config.ovpn --daemon

Local configuration

The final steps to get this to work are:

  1. Set up local port forwarding to the SOCKS proxy on your jump host;
  2. Configure Burp Suite to use the forwarded local port as a SOCKS proxy;
  3. Use the ProxySwitch browser extension to send only selected sites towards Burp Suite and through the VPN

On Windows, using PuTTY, you can use the following configuration to forward local port 31337 to your jump host on port 2222:

Note that “localhost” in this screenshot is relative to the remote server.

In Burp Suite, go to either User Options or Project Options, and configure SOCKS proxy to point to your localhost on port 31337:

Finally, point your Switchy Omega to your Burp proxy for selected sites:

Before kicking off your tests, I recommend you verify the value of your public IP that is displayed when browsing to a site like or with your proxy enabled.

Pros / cons

The described setup has a few (dis)advantages worth mentioning:


  • Reserve the VPN bandwidth to testing activities only, which can considerably improve your connection speed over a sometimes shaky VPN;
  • Separate your “background” network traffic from the VPN traffic, ensuring your privacy isn’t at risk when testing from your personal device;
  • The AWS EC2 instance can be shut down in-between tests, ensuring your bill doesn’t keep growing overnight;
  • You can configure multiple devices to connect through a single VPN connection by pointing them to the same SOCKS proxy on the jump host.


  • The setup is slightly more convoluted than just running your OpenVPN client on your local machine;
  • In case of a failing VPN connection on the AWS EC2 instance, you may be executing tests outside of the VPN-ed environment without you noticing;
  • When configuring per-domain proxy settings, web application traffic that hits other domains will not be proxied, possibly leading to unexpected results.

I’d love to hear your thoughts on this! Did you use a similar approach? Do you have suggestions to improve or simplify this setup? Let me know in the comments below.

RCE in Slanger, a Ruby implementation of Pusher

While researching a web application last February, I learned about Slanger, an open source server implementation of Pusher. In this post I describe the discovery of a critical RCE vulnerability in Slanger 0.6.0, and the efforts that followed to responsibly disclose the vulnerability.

SECURITY NOTICE – If you are making use of Slanger in your products, stop reading and get your security fix first! A patch is available on GitHub or as part of the Ruby gem in version 0.6.1.

Pusher vs. Slanger

Pusher is a product that provides a number of libraries to enable the use of WebSockets in a variety of programming languages. WebSockets, according to their website, “represent a standard for bi-directional realtime communication between servers and clients.”

Some of the functionalities offered by Pusher are subscribing and unsubscribing to public and private channels. For example, the following WebSocket request would result in a user subscribing to the channel “presence-example-channel”:

  "event": "pusher:subscribe",
  "data": {
    "channel": "presence-example-channel",
    "auth": "<APP_KEY>:<server_generated_signature>",
    "channel_data": "{
      \"user_id\": \"<unique_user_id>\",
      \"user_info\": {
        \"name\": \"Phil Leggetter\",
        \"twitter\": \"@leggetter\",

While researching a web application, I learned about Slanger, an open-source Ruby implementation that “speaks” the Pusher protocol. In other words, it is a free alternative to Pusher that can be spun up as a stand-alone server in order to accept and process messages like the example above. At the time of writing, the vulnerable library has over 45,000 downloads on

I was able to determine what library was being used when the following error message was returned in response to my invalid input:


socket = new WebSocket("wss://<app-id>?");


{"event":"pusher:error","data":"{\"code\":500,\"message\":\"expected true at line 1, column 2 [parse.c:148]\\n /usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/lib/slanger/handler.rb:28:in `load'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/lib/slanger/handler.rb:28:in `onmessage'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/lib/slanger/web_socket_server.rb:30:in `block (3 levels) in run'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/connection.rb:18:in `trigger_on_message'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/message_processor_06.rb:52:in `message'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/framing07.rb:118:in `process_data'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/handler.rb:47:in `receive_data'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/connection.rb:76:in `receive_data'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/eventmachine- `run_machine'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/eventmachine- `run'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/bin/slanger:106:in `<main>'\"}"}

Note the occurrence of gem slanger-0.6.0, which appears to point to this open source Ruby project on GitHub. Having all the source code at my disposition, my attention was drawn to the file handler.rb that contained the following function responsible for handling incoming WebSocket messages:

def onmessage(msg)
   msg = Oj.load(msg)
   msg['data'] = Oj.load(msg['data']) if msg['data'].is_a? String
   event = msg['event'].gsub(/\Apusher:/, 'pusher_')
   if event =~ /\Aclient-/
      msg['socket_id'] = connection.socket_id
      Channel.send_client_message msg
   elsif respond_to? event, true
      send event, msg

Since msg is the string that comes from the user’s end of the two-way communication, that gives us considerable control over this function. In particular, when trying to understand what Oj.load is supposed to do, this code started to look promising — from an attacker’s perspective.

Ruby unmarshalling

As it turns out, Oj (short for “Optimized JSON”) is a “fast JSON parser and Object marshaller as a Ruby gem.” Now that is interesting. From earlier experience, I knew that Ruby marshalling can lead to remote code execution vulnerabilities.

From online documentation, I learned that Oj allows serialization and deserialization of Ruby objects by default. For example, the following string is the result of an object of the class Sample::Doc being serialized with Oj.dump(sample).

{"^o":"Sample::Doc","title":"Sample","user":"ohler","create_time":{"^t":1371361533.272099000},"layers":{},"change_history":[{"^o":"Sample::Change","user":"ohler","time":{"^t":1371361533.272106000},"comment":"Changed at t+0."}],"props":{":purpose":"an example"}}

So presumably, passing a similarly serialized object via socket.send(...) should result in our input being “unmarshalled” by the underlying code in Slanger. All that now stands between an attacker’s input and remote code execution, is the availability of classes and objects that can be manipulated into executing system commands.

Since the Ruby gem appears to have a dependency on the Rails environment, I could build on earlier work by Charlie Somerville to construct a working payload that would lead to remote command execution in two different ways, one of which did not require knowing the app key. See the exploit in action in the video below.

Continue reading on the next page for an account of how I tried to responsibly disclose this bug.

A lot of coffee went into the writing of this article. If it helped you stay secure, please consider buying me a coffee, or invite me to your bug bounty program. 🙂

Buy me a coffeeBuy me a coffee

From blind XXE to root-level file read access

Polyphemus, by Johann Heinrich Wilhelm Tischbein, 1802 (Landesmuseum Oldenburg)

On a recent bug bounty adventure, I came across an XML endpoint that responded interestingly to attempted XXE exploitation. The endpoint was largely undocumented, and the only reference to it that I could find was an early 2016 post from a distraught developer in difficulties.

Below, I will outline the thought process that helped me make sense of what I encountered, and that in the end allowed me to elevate what seemed to be a medium-criticality vulnerability into a critical finding.

I will put deliberate emphasis on the various error messages that I encountered in the hope that it can point others in the right direction in the future.

Note that I have anonymised all endpoints and other identifiable information, as the vulnerability was reported as part of a private disclosure program, and the affected company does not want any information regarding their environment or this finding to be published.

Continue reading

Punicoder – discover domains that are phishing you

So we’re seeing homograph attacks again. Examples show how ‘’ and ‘’ can be mimicked by the use of Internationalized Domain Names (IDN) consisting entirely of unicode characters, i.e. and respectively.

As I found myself looking for ways to discover domain names that could be used for phishing attempts, I created a Python script called Punicoder to do the hard work for me. See the screenshot below for example output, and try it out for yourself here.

Punicoder output

Pro tip: use the following series of commands to find out if any of these domains resolve:

pieter@ubuntu:~$ python | cut -d' ' -f2 | nslookup | grep -Pzo '(?s)Name:\s(.*?)Address: (.*?).Server'
Server 2015: Creative Cheating

Write-up of 2015’s Creative Cheating challenge.

The first challenge I solved on 2015, hosted by FluxFingers, was Creative Cheating.

The challenge

Mr. Miller suspects that some of his students are cheating in an automated computer test. He captured some traffic between crypto nerds Alice and Bob. It looks mostly like garbage but maybe you can figure something out. He knows that Alice’s RSA key is (n, e) = (0x53a121a11e36d7a84dde3f5d73cf, 0x10001) ( and Bob’s is (n, e) = (0x99122e61dc7bede74711185598c7, 0x10001) (

The solution

Upon inspection of the packet capture, we notice every packet from Alice ( to Bob ( contains a base64-encoded payload. E.g.


Continue reading

V – For Victor


In celebration of the birth of my godson Victor in July 2014, I composed a small piece for piano. Click the image above to download the sheet music, and listen to a somewhat messy recording below.

How to set up a Wifi captive portal


The objective of this Wifi captive portal is to mimic the behaviour of a legitimate access point protected by a portal login page for demonstrational purposes. That includes the following:

  • Broadcast a rogue access point
  • Mimic captive portal behaviour:
    • User gets to see a login page when trying to connect;
    • After logging in, the user can continue to access the network and surf freely.

Continue reading

CSRF Discoverer – A Chrome extension

Continue reading

Grab password with XSS

Automatic completion of passwords in web forms allows attackers to grab your password if an XSS vulnerability exists.

We don’t usually associate XSS vulnerabilities with compromised passwords, but it is sometimes possible to steal login credentials through XSS vulnerabilities on a website. Take a look at the example attack below.

Continue reading