.
================================================
FILE: README.md
================================================
Burp VPS Proxy: Easy Cloud Proxies for Burp Suite

Getting Started »
Features
·
Providers
·
Disclaimers
# 📖 About
Burp VPS Proxy is a Burp Suite extension that allows for the automatic creation and deletion of upstream SOCKS5 proxies on popular cloud providers from within Burp Suite. It automatically configures Burp to use the created proxy so that all outbound traffic comes from a cloud IP address. This is useful to prevent our main IP address from being blacklisted by popular WAFs while performing penetration testing and bug bounty hunting.
Burp VPS Proxy was inspired by @honoki's awesome [DigitalOcean Droplet Proxy for Burp Suite](https://github.com/honoki/burp-digitalocean-droplet-proxy) idea.
Think this is useful? ⭐ Star us on GitHub — it helps!
# 🛠 Features
* Automatic creation, configuration and deletion of upstream SOCKS5 proxy on popular cloud services from within Burp Suite.
* Support for multiple providers: AWS, Digital Ocean and Linode.
* Each provider has its unique settings, including region selection.
* Automatic destruction of proxy when closing Burp or unloading the extension, with an option to preserve the proxy across sessions instead.
* Restores SOCKS5 proxy settings in Burp to their original values when the proxy is destroyed.
* Compatibility across multiple devices, ensuring seamless use without interference from proxies generated on separate computers.
# 🔎 How to use
Visit the [release page](https://github.com/d3mondev/burp-vps-proxy/releases) and download the latest `burp-vps-proxy.jar` file.
In Burp Suite, visit the Extensions tab and click Add. Set the extension type to Java, and select the `burp-vps-proxy.jar` file.
Once loaded, access the extension via the new VPS Proxy tab in Burp. Select your provider, set your API keys and click Deploy.
# 🌐 Providers
## Amazon Web Services (AWS)

The extension will use the `t4g.nano` instance type to minimize costs. Note that not all regions support this instance type. The extension will also create a security group named `burp-vps-proxy` in the region selected to allow connections to port 1080.
You will need an AWS Access Key and AWS Private Key in order to configure the extension. You'll also need to ensure the key pair gives access to at least the following permissions:
```
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EC2Permissions",
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:TerminateInstances",
"ec2:DescribeInstances",
"ec2:DescribeImages",
"ec2:DescribeRegions",
"ec2:CreateTags",
"ec2:CreateSecurityGroup",
"ec2:DescribeSecurityGroups",
"ec2:AuthorizeSecurityGroupIngress"
],
"Resource": "*"
}
]
}
```
## Digital Ocean

Digital Ocean is a popular VPS provider among security researchers, pentesters and bug bounty hunters. If you don't already have an account, you can get a $200 in free credits by using my referral link to signup:
[](https://www.digitalocean.com/?refcode=e4681a7c61c6&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge)
You will need to create an API key and enter it in the Burp VPS Proxy extension.
Provisioning can take some time after the droplet is created. Wait a few minutes after the instance is up.
## Linode

You will need to create an API key and enter it in the Burp VPS Proxy extension. This is done in the "My Settings -> API Tokens" section of your profile in your Linode dashboard. They call it a Personal Access Token.
Ensure the API key has the Read/Write permission for "Linodes".
Provisioning is done via SSH and the proxy is usually available as soon as the extension tells you.
# ⚖ Disclaimers & License
The author and contributors of this extension expressly disclaim any liability for any costs, damages, or consequences resulting from the use of cloud providers in connection with this software.
Using this program for unauthorized or illegal activities, including attacking targets without consent, is strictly prohibited. Users must comply with all applicable laws and regulations. The developer and contributors assume no liability or responsibility for any misuse, damage, or harm caused by this software. It is the user's responsibility to utilize this program in an ethical and lawful manner.
This repository's content is licensed under the GNU General Public License v3.0 (GPLv3).
================================================
FILE: build.gradle
================================================
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
group 'com.github.d3mondev.burpvpsproxy'
version '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.json:json:20230227'
implementation 'com.jcraft:jsch:0.1.55'
implementation 'net.portswigger.burp.extender:burp-extender-api:2.1'
implementation 'com.myjeeva.digitalocean:digitalocean-api-client:2.17'
implementation 'software.amazon.awssdk:ec2:2.20.37'
implementation 'software.amazon.awssdk:core:2.20.37'
}
sourceSets {
main {
java {
srcDir 'src'
}
resources {
srcDir 'resources'
}
}
}
shadowJar {
archiveBaseName = project.name
archiveVersion = ''
archiveFileName = archiveBaseName.get() + ".jar"
}
build.dependsOn shadowJar
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: resources/provisioning.sh
================================================
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
apt-get -yq update && apt-get -yq install dante-server
cat > /etc/danted.conf << EOF
logoutput: syslog
user.privileged: root
user.unprivileged: nobody
internal: 0.0.0.0 port=1080
external: eth0
socksmethod: username
clientmethod: none
client pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
}
socks pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
}
EOF
EXTERNAL_INTERFACE=$(ip route get 1 | awk '{print $5; exit}')
sed -i "s/external: eth0/external: $EXTERNAL_INTERFACE/" /etc/danted.conf
useradd -r -s /bin/false burp-vps-proxy
echo 'burp-vps-proxy:CHANGEME' | chpasswd
systemctl restart danted.service
================================================
FILE: settings.gradle
================================================
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user manual at https://docs.gradle.org/8.0.2/userguide/multi_project_builds.html
*/
rootProject.name = 'burp-vps-proxy'
================================================
FILE: src/burp/BurpExtender.java
================================================
package burp;
import vpsproxy.Logger;
import vpsproxy.VPSProxy;
public class BurpExtender implements IBurpExtender, IExtensionStateListener {
private VPSProxy extension;
@Override
public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
Logger.init(callbacks.getStdout(), null);
// Set our extension name
callbacks.setExtensionName("VPS Proxy");
// Register callback to destroy VPS when our extension is unloaded
callbacks.registerExtensionStateListener(this);
// Create our main extension object
extension = new VPSProxy(callbacks);
callbacks.addSuiteTab(extension.getUI());
}
@Override
public void extensionUnloaded() {
extension.close();
}
}
================================================
FILE: src/vpsproxy/Logger.java
================================================
package vpsproxy;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Logger {
private static PrintWriter printWriter;
private static VPSProxyTab optionsTab;
public static void init(OutputStream stdout, VPSProxyTab tab) {
printWriter = new PrintWriter(stdout, true);
optionsTab = tab;
}
public static void log(String message) {
log(message, true);
}
public static void log(String message, boolean timestamp) {
if (timestamp) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String ts = now.format(formatter);
message = String.format("[%s] %s\n", ts, message);
}
if (printWriter != null) {
printWriter.printf("%s", message);
}
if (optionsTab != null) {
optionsTab.log(message);
}
}
}
================================================
FILE: src/vpsproxy/ProxySettings.java
================================================
package vpsproxy;
public class ProxySettings {
private String ip;
private String port;
private String username;
private String password;
public ProxySettings(String ip, String port, String username, String password) {
this.ip = ip;
this.port = port;
this.username = username;
this.password = password;
}
public String getIp() {
return ip;
}
public String getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
================================================
FILE: src/vpsproxy/RandomString.java
================================================
package vpsproxy;
import java.security.SecureRandom;
import java.util.Random;
public class RandomString {
private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
private static final Random RANDOM = new SecureRandom();
public static String generate(int n) {
StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < n; i++) {
int randomIndex = RANDOM.nextInt(ALPHABET.length());
char randomChar = ALPHABET.charAt(randomIndex);
sb.append(randomChar);
}
return sb.toString();
}
}
================================================
FILE: src/vpsproxy/SettingsKeys.java
================================================
package vpsproxy;
public final class SettingsKeys {
// Exposed settings
public static final String DESTROY_PROXY_ON_EXIT = "DestroyProxyOnExit";
// Internal states
public static final String BURP_INSTANCE_ID = "BurpInstanceId";
public static final String LAST_STATE = "LastState";
public static final String CURRENT_PROVIDER = "CurrentProvider";
public static final String PROXY_SETTINGS_BACKUP = "ProxySettingsBackup";
public static final String PROXY_SETTINGS = "ProxySettings";
}
================================================
FILE: src/vpsproxy/VPSProxy.java
================================================
package vpsproxy;
import java.util.LinkedHashMap;
import java.util.Map;
import burp.IBurpExtenderCallbacks;
import vpsproxy.providers.DigitalOceanProvider;
import vpsproxy.providers.Provider.ProviderException;
import vpsproxy.providers.*;
public class VPSProxy {
private static final String PROXY_CONFIG_TEMPLATE = "{\"project_options\":{\"connections\":{\"socks_proxy\":{\"dns_over_socks\":false,\"host\":\"%s\",\"password\":\"%s\",\"port\":%s,\"use_proxy\":true,\"use_user_options\":false,\"username\":\"%s\"}}}}";
private IBurpExtenderCallbacks callbacks;
private VPSProxyTab optionsTab;
private Map providerMap;
public VPSProxy(IBurpExtenderCallbacks callbacks) {
this.callbacks = callbacks;
createBurpInstanceId();
providerMap = new LinkedHashMap<>();
addProvider(new AWSProvider(callbacks));
addProvider(new DigitalOceanProvider(callbacks));
addProvider(new LinodeProvider(callbacks));
optionsTab = new VPSProxyTab(this, providerMap);
Logger.init(callbacks.getStdout(), optionsTab);
restorePreviousProxy();
}
public VPSProxyTab getUI() {
return optionsTab;
}
public void close() {
String destroyProxySetting = callbacks.loadExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT);
boolean destroyProxy = destroyProxySetting == null || Boolean.parseBoolean(destroyProxySetting);
if (!destroyProxy)
return;
Provider currentProvider = optionsTab.getSelectedProvider();
if (currentProvider != null) {
try {
destroyInstance(currentProvider);
} catch (ProviderException e) {
} catch (Exception e) {
Logger.log(String.format("Unhandled exception: %s", e.getMessage()));
}
}
}
protected IBurpExtenderCallbacks getCallbacks() {
return callbacks;
}
protected void startInstance(Provider provider) throws ProviderException {
try {
ProxySettings proxy = provider.startInstance();
configureProxy(proxy);
} catch (ProviderException e) {
Logger.log(e.getMessage());
throw e;
} catch (Exception e) {
Logger.log(String.format("Unhandled exception: %s", e.getMessage()));
throw e;
}
}
protected void destroyInstance(Provider provider) throws ProviderException {
try {
provider.destroyInstance();
resetProxySettings();
optionsTab.setStoppedState();
} catch (ProviderException e) {
Logger.log(e.getMessage());
throw e;
} catch (Exception e) {
Logger.log(String.format("Unhandled exception: %s", e.getMessage()));
throw e;
}
}
protected void configureProxy(ProxySettings proxy) {
Logger.log(String.format("Configuring proxy %s:%s:%s:%s", proxy.getIp(), proxy.getPort(), proxy.getUsername(),
proxy.getPassword()));
// Save current config
String configBackup = callbacks.saveConfigAsJson("project_options.connections.socks_proxy");
callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS_BACKUP, configBackup);
// Set new proxy settings
String config = String.format(PROXY_CONFIG_TEMPLATE, proxy.getIp(), proxy.getPassword(), proxy.getPort(),
proxy.getUsername());
callbacks.loadConfigFromJson(config);
callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS, config);
Logger.log("Proxy configured. The VPS could still be provisioning, please give it a few minutes.");
}
protected void resetProxySettings() {
String config = callbacks.loadExtensionSetting(SettingsKeys.PROXY_SETTINGS_BACKUP);
if (config != null) {
Logger.log("Restoring proxy settings");
callbacks.loadConfigFromJson(config);
callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS_BACKUP, null);
callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS, null);
}
}
protected void restorePreviousProxy() {
String config = callbacks.loadExtensionSetting(SettingsKeys.PROXY_SETTINGS);
if (config != null) {
Logger.log("Setting proxy from previous session");
callbacks.loadConfigFromJson(config);
}
}
private void createBurpInstanceId() {
String instanceId = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
if (instanceId == null) {
String id = RandomString.generate(6);
callbacks.saveExtensionSetting(SettingsKeys.BURP_INSTANCE_ID, id);
}
}
private void addProvider(Provider provider) {
providerMap.put(provider.getName(), provider);
}
}
================================================
FILE: src/vpsproxy/VPSProxyTab.java
================================================
package vpsproxy;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import burp.ITab;
import java.awt.*;
import java.awt.event.*;
import java.awt.event.ActionListener;
import java.util.Map;
import vpsproxy.providers.Provider;
public class VPSProxyTab implements ITab {
private Map providerMap;
VPSProxy extension;
private JPanel panel;
private JPanel providerPanel;
private JCheckBox destroyProxyCheckBox;
private JLabel destroyProxyLabel;
private JComboBox providerComboBox;
private JButton deployButton;
private JButton stopButton;
private JTextArea logTextArea;
private JScrollPane logScrollPane;
private Font defaultFont;
private Font headerFont;
private Color headerColor;
private int gapSize = 25;
Thread workerThread;
public VPSProxyTab(VPSProxy extension, Map providers) {
providerMap = providers;
this.extension = extension;
// Initialize fonts and colors
JLabel defaultLabel = new JLabel();
extension.getCallbacks().customizeUiComponent(defaultLabel);
defaultFont = defaultLabel.getFont();
headerFont = defaultFont.deriveFont(Font.BOLD, defaultFont.getSize() + 2);
Color defaultColor = defaultLabel.getForeground();
if (defaultColor.getRed() > 128 && defaultColor.getGreen() > 128 && defaultColor.getBlue() > 128)
headerColor = Color.WHITE;
else
headerColor = Color.BLACK;
// Initialize main panel
this.panel = new JPanel();
// Intro UI elements
JLabel introHeaderLabel = new JLabel("Burp VPS Proxy");
introHeaderLabel.setFont(headerFont);
introHeaderLabel.setForeground(headerColor);
JLabel introHelp1Label = new JLabel(
"Select the VPS provider you want to use and enter the proper API key(s). Then, you can click Deploy to launch a new proxy.");
JLabel introHelp2Label = new JLabel(
"Once created, the extension will automatically configure your SOCKS5 proxy in Burp -> Settings -> Network -> Connections.");
JLabel introHelp3Label = new JLabel(
"The proxy server will automatically be terminated when Burp exits or the extension is unloaded.");
// Options UI elements
JLabel optionsHeaderLabel = new JLabel("Options");
optionsHeaderLabel.setFont(headerFont);
optionsHeaderLabel.setForeground(headerColor);
destroyProxyCheckBox = new JCheckBox();
String destroyProxy = extension.getCallbacks().loadExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT);
if (destroyProxy == null || destroyProxy.equals("true")) {
destroyProxyCheckBox.setSelected(true);
}
destroyProxyLabel = new JLabel("Destroy proxy when Burp exits");
// Provider UI elements
JLabel providerHeaderLabel = new JLabel("Provider");
providerHeaderLabel.setFont(headerFont);
providerHeaderLabel.setForeground(headerColor);
providerComboBox = new JComboBox<>();
providerComboBox.setMaximumSize(new Dimension(150, providerComboBox.getPreferredSize().height));
for (String providerName : providerMap.keySet()) {
providerComboBox.addItem(providerName);
}
String selectedProviderName = extension.getCallbacks().loadExtensionSetting(SettingsKeys.CURRENT_PROVIDER);
providerComboBox.setSelectedItem(selectedProviderName);
deployButton = new JButton("Deploy");
stopButton = new JButton("Stop");
stopButton.setEnabled(false);
// Provider settings UI elements
FlowLayout providerLayout = new FlowLayout(FlowLayout.LEFT);
providerLayout.setHgap(0);
providerLayout.setVgap(0);
providerPanel = new JPanel(providerLayout);
providerPanel.setMaximumSize(new Dimension(Short.MAX_VALUE, 200));
Provider currentProvider = getSelectedProvider();
if (currentProvider != null) {
providerPanel.add(currentProvider.getUI());
}
// Log UI elements
JLabel logHeaderLabel = new JLabel("Log");
logHeaderLabel.setFont(headerFont);
logHeaderLabel.setForeground(headerColor);
logTextArea = new JTextArea();
logTextArea.setEditable(false);
int logScrollPaneHeight = 400;
logScrollPane = new JScrollPane(logTextArea);
logScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
logScrollPane.setMaximumSize(new Dimension(Short.MAX_VALUE, logScrollPaneHeight));
JScrollBar verticalBar = logScrollPane.getVerticalScrollBar();
verticalBar.setValue(verticalBar.getMaximum());
// Layout
GroupLayout layout = new GroupLayout(this.panel);
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
layout.setHorizontalGroup(layout.createParallelGroup()
.addComponent(introHeaderLabel)
.addComponent(introHelp1Label)
.addComponent(introHelp2Label)
.addComponent(introHelp3Label)
.addComponent(optionsHeaderLabel)
.addGroup(layout.createSequentialGroup()
.addComponent(destroyProxyCheckBox)
.addComponent(destroyProxyLabel))
.addComponent(providerHeaderLabel)
.addGroup(layout.createSequentialGroup()
.addComponent(providerComboBox)
.addComponent(deployButton)
.addComponent(stopButton))
.addComponent(providerPanel)
.addComponent(logHeaderLabel)
.addComponent(logScrollPane));
layout.setVerticalGroup(layout.createSequentialGroup()
.addComponent(introHeaderLabel)
.addComponent(introHelp1Label)
.addComponent(introHelp2Label)
.addComponent(introHelp3Label)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, gapSize)
.addComponent(optionsHeaderLabel)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(destroyProxyCheckBox)
.addComponent(destroyProxyLabel))
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, gapSize)
.addComponent(providerHeaderLabel)
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(providerComboBox)
.addComponent(deployButton)
.addComponent(stopButton))
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, 5)
.addComponent(providerPanel)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, 2 * gapSize)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, 0, Short.MAX_VALUE)
.addComponent(logHeaderLabel)
.addComponent(logScrollPane, GroupLayout.PREFERRED_SIZE, logScrollPaneHeight,
GroupLayout.PREFERRED_SIZE));
layout.linkSize(deployButton, stopButton);
this.panel.setLayout(layout);
String lastState = extension.getCallbacks().loadExtensionSetting(SettingsKeys.LAST_STATE);
if (lastState != null && lastState.equals("running")) {
setRunningState();
}
installHandlers();
}
@Override
public String getTabCaption() {
return "VPS Proxy";
}
@Override
public Component getUiComponent() {
return this.panel;
}
public void log(String message) {
logTextArea.append(message);
}
private void installHandlers() {
destroyProxyCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (destroyProxyCheckBox.isSelected()) {
extension.getCallbacks().saveExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT, "true");
} else {
extension.getCallbacks().saveExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT, "false");
}
}
});
destroyProxyLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
destroyProxyCheckBox.doClick();
}
});
providerComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Provider selectedProvider = getSelectedProvider();
if (selectedProvider == null) {
return;
}
// Replace the provider UI panel with the new provider UI
providerPanel.removeAll();
providerPanel.add(selectedProvider.getUI());
providerPanel.revalidate();
providerPanel.repaint();
extension.getCallbacks().saveExtensionSetting(SettingsKeys.CURRENT_PROVIDER,
selectedProvider.getName());
}
});
deployButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Provider selectedProvider = getSelectedProvider();
if (selectedProvider == null) {
Logger.log("No provider selected");
return;
}
if (workerThread != null && workerThread.isAlive()) {
Logger.log("Worker thread is already started!");
return;
}
setRunningState();
workerThread = new Thread(() -> {
try {
extension.startInstance(selectedProvider);
} catch (Exception ex) {
setStoppedState();
}
});
workerThread.start();
}
});
stopButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Provider selectedProvider = getSelectedProvider();
if (selectedProvider == null) {
Logger.log("No provider selected");
return;
}
if (workerThread != null && workerThread.isAlive()) {
Logger.log("Wait for instance to finish deploying...");
return;
}
setStoppedState();
try {
extension.destroyInstance(selectedProvider);
} catch (Exception ex) {
}
}
});
logTextArea.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
JScrollBar verticalBar = logScrollPane.getVerticalScrollBar();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
verticalBar.setValue(verticalBar.getMaximum());
}
});
}
@Override
public void removeUpdate(DocumentEvent e) {
// Do nothing
}
@Override
public void changedUpdate(DocumentEvent e) {
// Do nothing
}
});
}
protected Provider getSelectedProvider() {
Object selectedItem = providerComboBox.getSelectedItem();
if (selectedItem == null) {
return null;
}
String providerName = selectedItem.toString();
Provider provider = providerMap.get(providerName);
if (provider == null) {
return null;
}
return provider;
}
public void setRunningState() {
extension.getCallbacks().saveExtensionSetting(SettingsKeys.LAST_STATE, "running");
stopButton.setEnabled(true);
stopButton.requestFocusInWindow();
providerComboBox.setEnabled(false);
deployButton.setEnabled(false);
Component[] providerPanelComponents = providerPanel.getComponents();
if (providerPanelComponents.length != 0) {
JPanel panel = (JPanel) providerPanelComponents[0];
Component[] components = panel.getComponents();
for (Component component : components) {
component.setEnabled(false);
}
}
}
public void setStoppedState() {
extension.getCallbacks().saveExtensionSetting(SettingsKeys.LAST_STATE, "stopped");
deployButton.setEnabled(true);
deployButton.requestFocusInWindow();
providerComboBox.setEnabled(true);
stopButton.setEnabled(false);
Component[] providerPanelComponents = providerPanel.getComponents();
if (providerPanelComponents.length != 0) {
JPanel panel = (JPanel) providerPanelComponents[0];
Component[] components = panel.getComponents();
for (Component component : components) {
component.setEnabled(true);
}
}
}
}
================================================
FILE: src/vpsproxy/providers/AWSProvider.java
================================================
package vpsproxy.providers;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
import java.util.Comparator;
import java.util.Optional;
import javax.swing.*;
import javax.swing.event.*;
import burp.IBurpExtenderCallbacks;
import vpsproxy.*;
import software.amazon.awssdk.auth.credentials.*;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.ec2.Ec2Client;
import software.amazon.awssdk.services.ec2.model.*;
import software.amazon.awssdk.services.ec2.model.Image;
public class AWSProvider extends Provider {
final private String INSTANCE_TAG = "burp-vps-proxy";
final private String AWS_OS_TYPE = "debian-11";
final private String AWS_INSTANCE_ARCH = "arm64";
final private InstanceType AWS_INSTANCE_TYPE = InstanceType.T4_G_NANO;
final private String AWS_ACCESS_KEY_SETTING = "ProviderAWSAccessKey";
final private String AWS_SECRET_KEY_SETTING = "ProviderAWSSecretKey";
final private String AWS_REGION_SETTING = "ProviderAWSRegion";
final private String[] AWS_REGIONS = {
"us-east-2",
"us-east-1",
"us-west-1",
"us-west-2",
"af-south-1",
"ap-east-1",
"ap-south-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-south-1",
"ap-northeast-3",
"ap-northeast-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-1",
"ca-central-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"eu-south-1",
"eu-west-3",
"eu-south-2",
"eu-north-1",
"eu-central-2",
"me-south-1",
"me-central-1",
"sa-east-1",
};
private IBurpExtenderCallbacks callbacks;
private String awsRegion = "us-east-1";
private Ec2Client ec2Client;
public AWSProvider(IBurpExtenderCallbacks callbacks) {
this.callbacks = callbacks;
}
@Override
public String getName() {
return "AWS EC2";
}
@Override
public ProxySettings startInstance() throws ProviderException {
log("creating a new instance");
Ec2Client ec2Client;
try {
ec2Client = createClient();
} catch (ProviderException e) {
throw e;
}
String password = RandomString.generate(12);
String script;
try {
script = getProvisioningScript(password);
} catch (IOException e) {
throw new ProviderException(String.format("error loading provisioning script: %s", e.getMessage()), e);
}
String amiId;
try {
amiId = getAmiId(AWS_OS_TYPE, awsRegion);
} catch (ProviderException e) {
throw e;
}
String securityGroupId;
try {
securityGroupId = createSecurityGroup("burp-vps-proxy", "Allow traffic to port 1080 for the Burp SOCKS Proxy");
} catch (ProviderException e) {
throw e;
}
String instanceName = String.format("burp-vps-proxy-%s", RandomString.generate(4));
Tag nameTag = Tag.builder()
.key("Name")
.value(instanceName)
.build();
String tagValue = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
Tag proxyTag = Tag.builder()
.key(INSTANCE_TAG)
.value(tagValue)
.build();
TagSpecification tagSpecification = TagSpecification.builder()
.resourceType("instance")
.tags(nameTag, proxyTag)
.build();
RunInstancesRequest runRequest = RunInstancesRequest.builder()
.instanceType(AWS_INSTANCE_TYPE)
.maxCount(1)
.minCount(1)
.imageId(amiId)
.userData(script)
.tagSpecifications(tagSpecification)
.securityGroupIds(securityGroupId)
.build();
RunInstancesResponse runResponse;
String instanceId;
try {
runResponse = ec2Client.runInstances(runRequest);
instanceId = runResponse.instances().get(0).instanceId();
ec2Client.waiter().waitUntilInstanceRunning(r -> r.instanceIds(instanceId));
} catch (Exception e) {
throw new ProviderException(String.format("error creating instance: %s", e.getMessage()), e);
}
DescribeInstancesRequest describeRequest = DescribeInstancesRequest.builder()
.instanceIds(instanceId)
.build();
DescribeInstancesResponse describeResponse;
try {
describeResponse = ec2Client.describeInstances(describeRequest);
} catch (Exception e) {
throw new ProviderException(String.format("error reading newly created instance: %s", e.getMessage()), e);
}
String publicIpAddress = describeResponse.reservations().get(0).instances().get(0).publicIpAddress();
return createProxySettings(publicIpAddress, password);
}
@Override
public void destroyInstance() throws ProviderException {
Ec2Client ec2Client;
try {
ec2Client = createClient();
} catch (ProviderException e) {
throw e;
}
String tagValue = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
DescribeInstancesRequest describeRequest = DescribeInstancesRequest.builder()
.filters(
Filter.builder()
.name("tag:" + INSTANCE_TAG)
.values(tagValue)
.build(),
Filter.builder()
.name("instance-state-name")
.values("pending", "running", "rebooting", "stopping", "stopped")
.build())
.build();
DescribeInstancesResponse describeResponse;
try {
describeResponse = ec2Client.describeInstances(describeRequest);
} catch (Exception e) {
throw new ProviderException(String.format("error listing instances: %s", e.getMessage()), e);
}
describeResponse.reservations().stream()
.flatMap(reservation -> reservation.instances().stream())
.forEach(instance -> {
String instanceId = instance.instanceId();
String instanceName = "";
for (Tag tag : instance.tags()) {
if (tag.key().equals("Name")) {
instanceName = tag.value();
break;
}
}
TerminateInstancesRequest terminateRequest = TerminateInstancesRequest.builder()
.instanceIds(instanceId)
.build();
try {
ec2Client.terminateInstances(terminateRequest);
logf("instance %s deleted", instanceName);
} catch (Exception e) {
logf("error deleting instance '%s': %s", instanceName, e.getMessage());
}
});
}
@Override
public JComponent getUI() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JLabel awsAccessKeyLabel = new JLabel("AWS Access Key:");
awsAccessKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JTextField awsAccessKeyTextField = new JTextField();
awsAccessKeyTextField.setAlignmentX(Component.LEFT_ALIGNMENT);
awsAccessKeyTextField.setPreferredSize(new Dimension(200, awsAccessKeyTextField.getPreferredSize().height));
awsAccessKeyTextField.setText(callbacks.loadExtensionSetting(AWS_ACCESS_KEY_SETTING));
JLabel awsSecretKeyLabel = new JLabel("AWS Secret Key:");
awsSecretKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JPasswordField awsSecretKeyPasswordField = new JPasswordField();
awsSecretKeyPasswordField.setAlignmentX(Component.LEFT_ALIGNMENT);
awsSecretKeyPasswordField.setPreferredSize(new Dimension(200, awsSecretKeyPasswordField.getPreferredSize().height));
awsSecretKeyPasswordField.setText(callbacks.loadExtensionSetting(AWS_SECRET_KEY_SETTING));
JLabel awsRegionLabel = new JLabel("Region:");
awsRegionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JComboBox awsRegionComboBox = new JComboBox<>();
awsRegionComboBox.setAlignmentX(Component.LEFT_ALIGNMENT);
awsRegionComboBox.setMaximumSize(new Dimension(125, awsRegionComboBox.getPreferredSize().height));
for (int i = 0; i < AWS_REGIONS.length; i++) {
awsRegionComboBox.addItem(AWS_REGIONS[i]);
}
String selectedRegion = callbacks.loadExtensionSetting(AWS_REGION_SETTING);
if (selectedRegion != null && !selectedRegion.isEmpty()) {
awsRegionComboBox.setSelectedItem(selectedRegion);
}
panel.add(awsAccessKeyLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(awsAccessKeyTextField);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(awsSecretKeyLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(awsSecretKeyPasswordField);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(awsRegionLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(awsRegionComboBox);
awsAccessKeyTextField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void removeUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void changedUpdate(DocumentEvent e) {
saveSetting();
}
private void saveSetting() {
String value = awsAccessKeyTextField.getText();
callbacks.saveExtensionSetting(AWS_ACCESS_KEY_SETTING, value);
}
});
awsSecretKeyPasswordField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void removeUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void changedUpdate(DocumentEvent e) {
saveSetting();
}
private void saveSetting() {
String value = new String(awsSecretKeyPasswordField.getPassword());
callbacks.saveExtensionSetting(AWS_SECRET_KEY_SETTING, value);
}
});
awsRegionComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Object selectedItem = awsRegionComboBox.getSelectedItem();
if (selectedItem == null) {
return;
}
awsRegion = selectedItem.toString();
callbacks.saveExtensionSetting(AWS_REGION_SETTING, awsRegion);
}
});
return panel;
}
@Override
protected String getProvisioningScript(String password) throws IOException {
String script = super.getProvisioningScript(password);
return Base64.getEncoder().encodeToString(script.getBytes());
}
private Ec2Client createClient() throws ProviderException {
// Load the AWS keys from settings
String awsAccessKey = callbacks.loadExtensionSetting(AWS_ACCESS_KEY_SETTING);
String awsSecretKey = callbacks.loadExtensionSetting(AWS_SECRET_KEY_SETTING);
if (awsAccessKey == null || awsSecretKey == null || awsAccessKey.isEmpty() || awsSecretKey.isEmpty()) {
throw new ProviderException("missing API key(s)", null);
}
try {
// Configure the region
Region region = Region.of(awsRegion);
// Create the client
AwsCredentials credentials = AwsBasicCredentials.create(awsAccessKey, awsSecretKey);
ec2Client = Ec2Client.builder()
.region(region)
.credentialsProvider(() -> credentials)
.build();
} catch (Exception e) {
throw new ProviderException(String.format("error creating AWS client: %s", e.getMessage()), e);
}
return ec2Client;
}
private String getAmiId(String osType, String region) throws ProviderException {
// Filter by name
Filter osFilter = Filter.builder()
.name("name")
.values(osType + "-*")
.build();
// Filter by architecture
Filter architectureFilter = Filter.builder()
.name("architecture")
.values(AWS_INSTANCE_ARCH)
.build();
// Find the requested image
DescribeImagesRequest describeImagesRequest = DescribeImagesRequest.builder()
.owners("136693071363") // Debian AMI owner ID
.filters(osFilter, architectureFilter)
.build();
DescribeImagesResponse describeImagesResponse;
try {
describeImagesResponse = ec2Client.describeImages(describeImagesRequest);
} catch (Exception e) {
throw new ProviderException(String.format("failed to find image '%s': %s", osType, e.getMessage()), e);
}
Optional latestImage = describeImagesResponse.images().stream()
.max(Comparator.comparing(Image::creationDate));
// Return the most recent image found
return latestImage.map(Image::imageId).orElse(null);
}
private String createSecurityGroup(String groupName, String groupDescription) throws ProviderException {
// Check if the security group already exists
DescribeSecurityGroupsResponse describeResponse;
try {
describeResponse = ec2Client.describeSecurityGroups();
} catch (Exception e) {
throw new ProviderException(String.format("error listing security groups: %s", e.getMessage()), e);
}
Optional securityGroup = describeResponse.securityGroups().stream()
.filter(sg -> sg.groupName().equals(groupName))
.findFirst();
if (securityGroup.isPresent()) {
// Security group already exists, return its ID
return securityGroup.get().groupId();
}
// Create the security group
CreateSecurityGroupRequest createRequest = CreateSecurityGroupRequest.builder()
.groupName(groupName)
.description(groupDescription)
.build();
CreateSecurityGroupResponse createResponse;
try {
createResponse = ec2Client.createSecurityGroup(createRequest);
} catch (Exception e) {
throw new ProviderException(String.format("error creating security groups: %s", e.getMessage()), e);
}
String groupId = createResponse.groupId();
// Add a rule to the security group that allows traffic to port 1080
IpRange ipRange = IpRange.builder()
.cidrIp("0.0.0.0/0")
.build();
IpPermission ipPermission = IpPermission.builder()
.ipProtocol("tcp")
.fromPort(1080)
.toPort(1080)
.ipRanges(ipRange)
.build();
AuthorizeSecurityGroupIngressRequest authorizeRequest = AuthorizeSecurityGroupIngressRequest.builder()
.groupId(groupId)
.ipPermissions(Collections.singletonList(ipPermission))
.build();
try {
ec2Client.authorizeSecurityGroupIngress(authorizeRequest);
} catch (Exception e) {
throw new ProviderException(String.format("error authorizing security group ingress: %s", e.getMessage()), e);
}
// Return the ID of the security group
return groupId;
}
public class CreateSecurityGroupException extends Exception {
public CreateSecurityGroupException(String message, Throwable cause) {
super(message, cause);
}
}
}
================================================
FILE: src/vpsproxy/providers/DigitalOceanProvider.java
================================================
package vpsproxy.providers;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;
import com.myjeeva.digitalocean.DigitalOcean;
import com.myjeeva.digitalocean.common.DropletStatus;
import com.myjeeva.digitalocean.impl.DigitalOceanClient;
import com.myjeeva.digitalocean.pojo.Droplet;
import burp.IBurpExtenderCallbacks;
import vpsproxy.*;
public class DigitalOceanProvider extends Provider {
final private String DO_API_KEY_SETTING = "Provider_DigitalOcean_APIKey";
final private String DO_REGION_SETTING = "Provider_DigitalOcean_Region";
final private String[] DO_REGIONS = {
"nyc1",
"nyc3",
"ams3",
"sfo3",
"sgp1",
"lon1",
"fra1",
"tor1",
"blr1",
"syd1",
};
private IBurpExtenderCallbacks callbacks;
private String doDropletTag = "burp-vps-proxy";
private String doRegion = "nyc1";
public DigitalOceanProvider(IBurpExtenderCallbacks callbacks) {
this.callbacks = callbacks;
String region = callbacks.loadExtensionSetting(DO_REGION_SETTING);
if (region != null) {
doRegion = region;
}
String burpInstanceId = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
doDropletTag = doDropletTag + "-" + burpInstanceId;
}
@Override
public String getName() {
return "DigitalOcean";
}
@Override
public ProxySettings startInstance() throws ProviderException {
log("creating a new droplet");
DigitalOcean client;
try {
client = getClient();
String dropletName = String.format("burp-vps-proxy-%s", RandomString.generate(4));
List tags = new ArrayList<>();
tags.add(doDropletTag);
Droplet droplet = new Droplet();
droplet.setName(dropletName);
droplet.setRegion(new com.myjeeva.digitalocean.pojo.Region(doRegion));
droplet.setImage(new com.myjeeva.digitalocean.pojo.Image("debian-11-x64"));
droplet.setSize("s-1vcpu-512mb-10gb");
droplet.setTags(tags);
String password = RandomString.generate(12);
droplet.setUserData(getProvisioningScript(password));
droplet = client.createDroplet(droplet);
int attempts = 0;
while (droplet.getStatus() != DropletStatus.ACTIVE) {
Thread.sleep(2000);
attempts++;
if (attempts > 60) {
log("droplet creation timed out");
client.deleteDroplet(droplet.getId());
log("droplet deleted");
return null;
}
droplet = client.getDropletInfo(droplet.getId());
}
logf("droplet %s created", droplet.getName());
return new ProxySettings(droplet.getNetworks().getVersion4Networks().get(0).getIpAddress(), "1080",
"burp-vps-proxy", password);
} catch (ProviderException e) {
throw e;
} catch (Exception e) {
throw new ProviderException(String.format("error creating droplet: %s", e.getMessage()), e);
}
}
@Override
public void destroyInstance() throws ProviderException {
try {
DigitalOcean client = getClient();
List droplets = client.getAvailableDroplets(0, Integer.MAX_VALUE).getDroplets();
for (Droplet droplet : droplets) {
List tags = droplet.getTags();
if (tags != null && tags.contains(doDropletTag)) {
client.deleteDroplet(droplet.getId());
logf("droplet %s deleted", droplet.getName());
}
}
} catch (ProviderException e) {
throw e;
} catch (Exception e) {
throw new ProviderException(String.format("error deleting droplet: %s", e.getMessage()), e);
}
}
@Override
public JComponent getUI() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JLabel apiKeyLabel = new JLabel("API key:");
apiKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JPasswordField apiKeyPasswordField = new JPasswordField();
apiKeyPasswordField.setAlignmentX(Component.LEFT_ALIGNMENT);
apiKeyPasswordField.setPreferredSize(new Dimension(200, apiKeyPasswordField.getPreferredSize().height));
apiKeyPasswordField.setText(callbacks.loadExtensionSetting(DO_API_KEY_SETTING));
JLabel doRegionLabel = new JLabel("Region:");
doRegionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JComboBox doRegionComboBox = new JComboBox<>();
doRegionComboBox.setAlignmentX(Component.LEFT_ALIGNMENT);
doRegionComboBox.setMaximumSize(new Dimension(75, doRegionComboBox.getPreferredSize().height));
for (int i = 0; i < DO_REGIONS.length; i++) {
doRegionComboBox.addItem(DO_REGIONS[i]);
}
String selectedRegion = callbacks.loadExtensionSetting(DO_REGION_SETTING);
if (selectedRegion != null && !selectedRegion.isEmpty()) {
doRegionComboBox.setSelectedItem(selectedRegion);
}
panel.add(apiKeyLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(apiKeyPasswordField);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(doRegionLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(doRegionComboBox);
apiKeyPasswordField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void removeUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void changedUpdate(DocumentEvent e) {
saveSetting();
}
private void saveSetting() {
String value = new String(apiKeyPasswordField.getPassword());
callbacks.saveExtensionSetting(DO_API_KEY_SETTING, value);
}
});
doRegionComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Object selectedItem = doRegionComboBox.getSelectedItem();
if (selectedItem == null) {
return;
}
doRegion = selectedItem.toString();
callbacks.saveExtensionSetting(DO_REGION_SETTING, doRegion);
}
});
return panel;
}
private DigitalOcean getClient() throws ProviderException {
String apiKey = callbacks.loadExtensionSetting(DO_API_KEY_SETTING);
if (apiKey == null) {
throw new ProviderException("no api key defined", null);
}
DigitalOcean client = new DigitalOceanClient(apiKey);
try {
client.getAccountInfo();
} catch (Exception e) {
throw new ProviderException(String.format("error getting account info: %s", e.getMessage()), e);
}
return client;
}
}
================================================
FILE: src/vpsproxy/providers/LinodeProvider.java
================================================
package vpsproxy.providers;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.event.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import org.json.JSONArray;
import org.json.JSONObject;
import burp.IBurpExtenderCallbacks;
import vpsproxy.*;
public class LinodeProvider extends Provider {
public class InstanceInfo {
private int id;
private String label;
public InstanceInfo(int id, String label) {
this.id = id;
this.label = label;
}
public int getId() {
return id;
}
public String getLabel() {
return label;
}
}
final private String LINODE_API_BASE_URL = "https://api.linode.com/v4";
final private String LINODE_API_CREATION_JSON = "{\"region\": \"%s\", \"type\": \"%s\", \"image\": \"%s\", \"root_pass\": \"%s\", \"label\": \"%s\", \"tags\": [\"%s\"]}";
final private String LINODE_API_KEY_SETTING = "Provider_Linode_APIKey";
final private String LINODE_REGION_SETTING = "Provider_Linode_Region";
final private String LINODE_SIZE = "g6-nanode-1";
final private String LINODE_IMAGE = "linode/debian11";
final private int LINODE_TIMEOUT = 120;
final private String[] LINODE_REGIONS = {
"us-east",
"us-central",
"us-west",
"us-southeast",
"ca-central",
"eu-west",
"eu-central",
"ap-south",
"ap-northeast",
"ap-west",
"ap-southeast",
};
private IBurpExtenderCallbacks callbacks;
private String linodeApiKey = "";
private String linodeTag = "burp-vps-proxy";
private String linodeRegion = "us-east";
public LinodeProvider(IBurpExtenderCallbacks callbacks) {
this.callbacks = callbacks;
String burpInstanceId = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
linodeTag = linodeTag + "-" + burpInstanceId;
linodeApiKey = callbacks.loadExtensionSetting(LINODE_API_KEY_SETTING);
String region = callbacks.loadExtensionSetting(LINODE_REGION_SETTING);
if (region != null) {
linodeRegion = region;
}
}
@Override
public String getName() {
return "Linode";
}
@Override
public ProxySettings startInstance() throws ProviderException {
log("creating a new linode");
try {
String password = RandomString.generate(12);
String rootPassword = RandomString.generate(24);
String instanceName = String.format("burp-vps-proxy-%s", RandomString.generate(4));
URL url = new URL(LINODE_API_BASE_URL + "/linode/instances");
HttpURLConnection connection;
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
connection.setDoOutput(true);
String payload = String.format(LINODE_API_CREATION_JSON, linodeRegion, LINODE_SIZE, LINODE_IMAGE,
rootPassword, instanceName, linodeTag);
OutputStream os = connection.getOutputStream();
os.write(payload.getBytes("UTF-8"));
if (connection.getResponseCode() != 200) {
throw new ProviderException("failed to created linode instance: "
+ connection.getResponseCode() + " " + connection.getResponseMessage(), null);
}
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String output;
StringBuilder sb = new StringBuilder();
while ((output = br.readLine()) != null) {
sb.append(output);
}
connection.disconnect();
String responseJson = sb.toString();
int instanceId = extractLinodeId(responseJson);
String ipAddress = getInstanceIpAddress(instanceId);
waitForStatus(instanceId, "running", LINODE_TIMEOUT);
runProvisioningScript(ipAddress, "root", rootPassword, getProvisioningScript(password));
logf("instance %s created", instanceName);
return new ProxySettings(ipAddress, "1080", "burp-vps-proxy", password);
} catch (ProviderException e) {
throw e;
} catch (Exception e) {
throw new ProviderException(String.format("error establishing connection: %s", e.getMessage()),
e);
}
}
@Override
public void destroyInstance() throws ProviderException {
try {
List instanceInfos = getInstanceIdsWithTag(linodeTag);
for (InstanceInfo instanceInfo : instanceInfos) {
deleteLinodeInstance(instanceInfo.id);
logf("instance %s deleted", instanceInfo.label);
}
} catch (ProviderException e) {
throw e;
} catch (Exception e) {
throw new ProviderException(String.format("error deleting linode: %s", e.getMessage()),
e);
}
}
@Override
public JComponent getUI() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JLabel apiKeyLabel = new JLabel("API key:");
apiKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JPasswordField apiKeyPasswordField = new JPasswordField();
apiKeyPasswordField.setAlignmentX(Component.LEFT_ALIGNMENT);
apiKeyPasswordField.setPreferredSize(new Dimension(200, apiKeyPasswordField.getPreferredSize().height));
apiKeyPasswordField.setText(linodeApiKey);
JLabel linodeRegionLabel = new JLabel("Region:");
linodeRegionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
JComboBox linodeRegionComboBox = new JComboBox<>();
linodeRegionComboBox.setAlignmentX(Component.LEFT_ALIGNMENT);
linodeRegionComboBox.setMaximumSize(new Dimension(125, linodeRegionComboBox.getPreferredSize().height));
for (int i = 0; i < LINODE_REGIONS.length; i++) {
linodeRegionComboBox.addItem(LINODE_REGIONS[i]);
}
String selectedRegion = callbacks.loadExtensionSetting(LINODE_REGION_SETTING);
if (selectedRegion != null && !selectedRegion.isEmpty()) {
linodeRegionComboBox.setSelectedItem(selectedRegion);
}
panel.add(apiKeyLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(apiKeyPasswordField);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(linodeRegionLabel);
panel.add(Box.createRigidArea(new Dimension(0, 5)));
panel.add(linodeRegionComboBox);
apiKeyPasswordField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void removeUpdate(DocumentEvent e) {
saveSetting();
}
@Override
public void changedUpdate(DocumentEvent e) {
saveSetting();
}
private void saveSetting() {
String value = new String(apiKeyPasswordField.getPassword());
callbacks.saveExtensionSetting(LINODE_API_KEY_SETTING, value);
linodeApiKey = value;
}
});
linodeRegionComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Object selectedItem = linodeRegionComboBox.getSelectedItem();
if (selectedItem == null) {
return;
}
linodeRegion = selectedItem.toString();
callbacks.saveExtensionSetting(LINODE_REGION_SETTING, linodeRegion);
}
});
return panel;
}
private void deleteLinodeInstance(int linodeId) throws Exception {
URL url = new URL(LINODE_API_BASE_URL + "/linode/instances/" + linodeId);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("DELETE");
connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
if (connection.getResponseCode() != 200) {
throw new ProviderException("failed to delete instance: " + connection.getResponseCode() + " "
+ connection.getResponseMessage(), null);
}
connection.disconnect();
}
private static int extractLinodeId(String json) {
JSONObject responseJson = new JSONObject(json);
return responseJson.getInt("id");
}
private String getInstanceDetails(int linodeId) throws Exception {
URL url = new URL(LINODE_API_BASE_URL + "/linode/instances/" + linodeId);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
if (connection.getResponseCode() != 200) {
throw new ProviderException("failed to get instance details: " + connection.getResponseCode() + " "
+ connection.getResponseMessage(), null);
}
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String output;
StringBuilder sb = new StringBuilder();
while ((output = br.readLine()) != null) {
sb.append(output);
}
connection.disconnect();
return sb.toString();
}
private String getInstanceIpAddress(int linodeId) throws Exception {
String instanceDetails = getInstanceDetails(linodeId);
JSONObject instanceJson = new JSONObject(instanceDetails);
JSONArray ipv4Addresses = instanceJson.getJSONArray("ipv4");
return ipv4Addresses.getString(0);
}
private String getInstanceStatus(int linodeId) throws Exception {
String instanceDetails = getInstanceDetails(linodeId);
JSONObject instanceJson = new JSONObject(instanceDetails);
return instanceJson.getString("status");
}
private List getInstanceIdsWithTag(String tag) throws Exception {
URL url = new URL(LINODE_API_BASE_URL + "/linode/instances");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
if (connection.getResponseCode() != 200) {
throw new ProviderException("failed to list instances: " + connection.getResponseCode() + " "
+ connection.getResponseMessage(), null);
}
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String output;
StringBuilder sb = new StringBuilder();
while ((output = br.readLine()) != null) {
sb.append(output);
}
String responseJson = sb.toString();
JSONObject jsonResponse = new JSONObject(responseJson);
JSONArray instances = jsonResponse.getJSONArray("data");
List instanceInfos = new ArrayList<>();
for (int i = 0; i < instances.length(); i++) {
JSONObject instance = instances.getJSONObject(i);
JSONArray instanceTags = instance.getJSONArray("tags");
for (int j = 0; j < instanceTags.length(); j++) {
if (tag.equals(instanceTags.getString(j))) {
instanceInfos.add(new InstanceInfo(instance.getInt("id"), instance.getString("label")));
break;
}
}
}
return instanceInfos;
}
private void waitForStatus(int linodeId, String status, int timeout) throws Exception {
int elapsed = 0;
while (true) {
if (elapsed >= timeout) {
throw new ProviderException(String.format("timed out waiting for status \"%s\"", status), null);
}
if (getInstanceStatus(linodeId).equalsIgnoreCase(status)) {
break;
}
Thread.sleep(5000);
elapsed += 5;
}
// Wait an extra 5 seconds otherwise the server may not be ready to accept
// connections
Thread.sleep(5000);
}
}
================================================
FILE: src/vpsproxy/providers/Provider.java
================================================
package vpsproxy.providers;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.Scanner;
import javax.swing.JComponent;
import java.nio.charset.StandardCharsets;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import vpsproxy.Logger;
import vpsproxy.ProxySettings;
public abstract class Provider {
private static final String proxyUsername = "burp-vps-proxy";
private static final String proxyPort = "1080";
private static final String SCRIPT_RESOURCE_PATH = "provisioning.sh";
private static String SCRIPT;
private static boolean debug = false;
public abstract String getName();
public abstract ProxySettings startInstance() throws ProviderException;
public abstract void destroyInstance() throws ProviderException;
public abstract JComponent getUI();
protected void log(String message) {
Logger.log(String.format("%s: %s", getName(), message));
}
protected void logf(String format, Object... args) {
format = getName() + ": " + format;
Logger.log(String.format(format, args));
}
protected ProxySettings createProxySettings(String publicIpAddress, String password) {
return new ProxySettings(publicIpAddress, proxyPort, proxyUsername, password);
}
public class ProviderException extends Exception {
public ProviderException(String message, Throwable cause) {
super(getName() + ": " + message, cause);
}
}
protected String getProvisioningScript(String password) throws IOException {
if (SCRIPT == null) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(SCRIPT_RESOURCE_PATH);
if (inputStream != null) {
try (Scanner scanner = new Scanner(inputStream, "UTF-8")) {
SCRIPT = scanner.useDelimiter("\\A").next();
}
} else {
throw new IOException(String.format("Resource '%s' not found", SCRIPT_RESOURCE_PATH));
}
}
return SCRIPT.replaceAll("CHANGEME", password);
}
protected void runProvisioningScript(String ipAddress, String username, String password, String provisioningScript)
throws Exception {
log("provisioning via ssh");
JSch jsch = new JSch();
Session session = jsch.getSession(username, ipAddress, 22);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
session.setConfig("ConnectTimeout", "60000");
session.connect();
ChannelExec channel = (ChannelExec) session.openChannel("exec");
channel.setCommand("bash -s");
channel.setInputStream(new ByteArrayInputStream(provisioningScript.getBytes(StandardCharsets.UTF_8)));
channel.setErrStream(System.err);
InputStream inputStream = channel.getInputStream();
InputStream errorStream = channel.getErrStream();
channel.connect();
BufferedReader stdOutputReader = new BufferedReader(new InputStreamReader(inputStream));
BufferedReader errOutputReader = new BufferedReader(new InputStreamReader(errorStream));
String line;
// log("Standard Output:");
while ((line = stdOutputReader.readLine()) != null) {
if (debug) {
log(line);
}
}
// log("Error Output:");
while ((line = errOutputReader.readLine()) != null) {
if (debug) {
log(line);
}
}
channel.disconnect();
session.disconnect();
}
}