.
675 |
676 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Burp VPS Proxy: Easy Cloud Proxies for Burp Suite
2 |
3 |

4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Getting Started »
13 |
14 |
15 | Features
16 | ·
17 | Providers
18 | ·
19 | Disclaimers
20 |
21 |
22 | # 📖 About
23 |
24 | 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.
25 |
26 | Burp VPS Proxy was inspired by @honoki's awesome [DigitalOcean Droplet Proxy for Burp Suite](https://github.com/honoki/burp-digitalocean-droplet-proxy) idea.
27 |
28 | Think this is useful? ⭐ Star us on GitHub — it helps!
29 |
30 | # 🛠 Features
31 |
32 | * Automatic creation, configuration and deletion of upstream SOCKS5 proxy on popular cloud services from within Burp Suite.
33 | * Support for multiple providers: AWS, Digital Ocean and Linode.
34 | * Each provider has its unique settings, including region selection.
35 | * Automatic destruction of proxy when closing Burp or unloading the extension, with an option to preserve the proxy across sessions instead.
36 | * Restores SOCKS5 proxy settings in Burp to their original values when the proxy is destroyed.
37 | * Compatibility across multiple devices, ensuring seamless use without interference from proxies generated on separate computers.
38 |
39 | # 🔎 How to use
40 |
41 | Visit the [release page](https://github.com/d3mondev/burp-vps-proxy/releases) and download the latest `burp-vps-proxy.jar` file.
42 |
43 | In Burp Suite, visit the Extensions tab and click Add. Set the extension type to Java, and select the `burp-vps-proxy.jar` file.
44 |
45 | Once loaded, access the extension via the new VPS Proxy tab in Burp. Select your provider, set your API keys and click Deploy.
46 |
47 | # 🌐 Providers
48 |
49 | ## Amazon Web Services (AWS)
50 |
51 | 
52 |
53 | 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.
54 |
55 | 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:
56 |
57 | ```
58 | {
59 | "Version": "2012-10-17",
60 | "Statement": [
61 | {
62 | "Sid": "EC2Permissions",
63 | "Effect": "Allow",
64 | "Action": [
65 | "ec2:RunInstances",
66 | "ec2:TerminateInstances",
67 | "ec2:DescribeInstances",
68 | "ec2:DescribeImages",
69 | "ec2:DescribeRegions",
70 | "ec2:CreateTags",
71 | "ec2:CreateSecurityGroup",
72 | "ec2:DescribeSecurityGroups",
73 | "ec2:AuthorizeSecurityGroupIngress"
74 | ],
75 | "Resource": "*"
76 | }
77 | ]
78 | }
79 | ```
80 |
81 | ## Digital Ocean
82 |
83 | 
84 |
85 | 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:
86 |
87 | [](https://www.digitalocean.com/?refcode=e4681a7c61c6&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge)
88 |
89 | You will need to create an API key and enter it in the Burp VPS Proxy extension.
90 |
91 | Provisioning can take some time after the droplet is created. Wait a few minutes after the instance is up.
92 |
93 | ## Linode
94 |
95 | 
96 |
97 | 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.
98 |
99 | Ensure the API key has the Read/Write permission for "Linodes".
100 |
101 | Provisioning is done via SSH and the proxy is usually available as soon as the extension tells you.
102 |
103 | # ⚖ Disclaimers & License
104 |
105 | 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.
106 |
107 | 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.
108 |
109 | This repository's content is licensed under the GNU General Public License v3.0 (GPLv3).
110 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d3mondev/burp-vps-proxy/e23fcb6da5b94554dc9057fa1966997d74c7947c/assets/logo.png
--------------------------------------------------------------------------------
/assets/providers-aws.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d3mondev/burp-vps-proxy/e23fcb6da5b94554dc9057fa1966997d74c7947c/assets/providers-aws.png
--------------------------------------------------------------------------------
/assets/providers-do.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d3mondev/burp-vps-proxy/e23fcb6da5b94554dc9057fa1966997d74c7947c/assets/providers-do.png
--------------------------------------------------------------------------------
/assets/providers-linode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d3mondev/burp-vps-proxy/e23fcb6da5b94554dc9057fa1966997d74c7947c/assets/providers-linode.png
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'com.github.johnrengelman.shadow' version '8.1.1'
4 | }
5 |
6 | group 'com.github.d3mondev.burpvpsproxy'
7 | version '1.0.0'
8 |
9 | repositories {
10 | mavenCentral()
11 | }
12 |
13 | dependencies {
14 | implementation 'org.json:json:20230227'
15 | implementation 'com.jcraft:jsch:0.1.55'
16 | implementation 'net.portswigger.burp.extender:burp-extender-api:2.1'
17 | implementation 'com.myjeeva.digitalocean:digitalocean-api-client:2.17'
18 | implementation 'software.amazon.awssdk:ec2:2.20.37'
19 | implementation 'software.amazon.awssdk:core:2.20.37'
20 | }
21 |
22 | sourceSets {
23 | main {
24 | java {
25 | srcDir 'src'
26 | }
27 | resources {
28 | srcDir 'resources'
29 | }
30 | }
31 | }
32 |
33 | shadowJar {
34 | archiveBaseName = project.name
35 | archiveVersion = ''
36 | archiveFileName = archiveBaseName.get() + ".jar"
37 | }
38 |
39 | build.dependsOn shadowJar
40 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/d3mondev/burp-vps-proxy/e23fcb6da5b94554dc9057fa1966997d74c7947c/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/resources/provisioning.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export DEBIAN_FRONTEND=noninteractive
3 | apt-get -yq update && apt-get -yq install dante-server
4 |
5 | cat > /etc/danted.conf << EOF
6 | logoutput: syslog
7 | user.privileged: root
8 | user.unprivileged: nobody
9 | internal: 0.0.0.0 port=1080
10 | external: eth0
11 | socksmethod: username
12 | clientmethod: none
13 | client pass {
14 | from: 0.0.0.0/0 to: 0.0.0.0/0
15 | }
16 | socks pass {
17 | from: 0.0.0.0/0 to: 0.0.0.0/0
18 | }
19 | EOF
20 |
21 | EXTERNAL_INTERFACE=$(ip route get 1 | awk '{print $5; exit}')
22 | sed -i "s/external: eth0/external: $EXTERNAL_INTERFACE/" /etc/danted.conf
23 |
24 | useradd -r -s /bin/false burp-vps-proxy
25 | echo 'burp-vps-proxy:CHANGEME' | chpasswd
26 |
27 | systemctl restart danted.service
28 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * The settings file is used to specify which projects to include in your build.
5 | *
6 | * Detailed information about configuring a multi-project build in Gradle can be found
7 | * in the user manual at https://docs.gradle.org/8.0.2/userguide/multi_project_builds.html
8 | */
9 |
10 | rootProject.name = 'burp-vps-proxy'
11 |
--------------------------------------------------------------------------------
/src/burp/BurpExtender.java:
--------------------------------------------------------------------------------
1 | package burp;
2 |
3 | import vpsproxy.Logger;
4 | import vpsproxy.VPSProxy;
5 |
6 | public class BurpExtender implements IBurpExtender, IExtensionStateListener {
7 | private VPSProxy extension;
8 |
9 | @Override
10 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
11 | Logger.init(callbacks.getStdout(), null);
12 |
13 | // Set our extension name
14 | callbacks.setExtensionName("VPS Proxy");
15 |
16 | // Register callback to destroy VPS when our extension is unloaded
17 | callbacks.registerExtensionStateListener(this);
18 |
19 | // Create our main extension object
20 | extension = new VPSProxy(callbacks);
21 | callbacks.addSuiteTab(extension.getUI());
22 | }
23 |
24 | @Override
25 | public void extensionUnloaded() {
26 | extension.close();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/vpsproxy/Logger.java:
--------------------------------------------------------------------------------
1 | package vpsproxy;
2 |
3 | import java.io.OutputStream;
4 | import java.io.PrintWriter;
5 | import java.time.LocalDateTime;
6 | import java.time.format.DateTimeFormatter;
7 |
8 | public class Logger {
9 | private static PrintWriter printWriter;
10 | private static VPSProxyTab optionsTab;
11 |
12 | public static void init(OutputStream stdout, VPSProxyTab tab) {
13 | printWriter = new PrintWriter(stdout, true);
14 | optionsTab = tab;
15 | }
16 |
17 | public static void log(String message) {
18 | log(message, true);
19 | }
20 |
21 | public static void log(String message, boolean timestamp) {
22 | if (timestamp) {
23 | LocalDateTime now = LocalDateTime.now();
24 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
25 | String ts = now.format(formatter);
26 | message = String.format("[%s] %s\n", ts, message);
27 | }
28 |
29 | if (printWriter != null) {
30 | printWriter.printf("%s", message);
31 | }
32 |
33 | if (optionsTab != null) {
34 | optionsTab.log(message);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/vpsproxy/ProxySettings.java:
--------------------------------------------------------------------------------
1 | package vpsproxy;
2 |
3 | public class ProxySettings {
4 | private String ip;
5 | private String port;
6 | private String username;
7 | private String password;
8 |
9 | public ProxySettings(String ip, String port, String username, String password) {
10 | this.ip = ip;
11 | this.port = port;
12 | this.username = username;
13 | this.password = password;
14 | }
15 |
16 | public String getIp() {
17 | return ip;
18 | }
19 |
20 | public String getPort() {
21 | return port;
22 | }
23 |
24 | public String getUsername() {
25 | return username;
26 | }
27 |
28 | public String getPassword() {
29 | return password;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/vpsproxy/RandomString.java:
--------------------------------------------------------------------------------
1 | package vpsproxy;
2 |
3 | import java.security.SecureRandom;
4 | import java.util.Random;
5 |
6 | public class RandomString {
7 | private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
8 | private static final Random RANDOM = new SecureRandom();
9 |
10 | public static String generate(int n) {
11 | StringBuilder sb = new StringBuilder(6);
12 | for (int i = 0; i < n; i++) {
13 | int randomIndex = RANDOM.nextInt(ALPHABET.length());
14 | char randomChar = ALPHABET.charAt(randomIndex);
15 | sb.append(randomChar);
16 | }
17 |
18 | return sb.toString();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/vpsproxy/SettingsKeys.java:
--------------------------------------------------------------------------------
1 | package vpsproxy;
2 |
3 | public final class SettingsKeys {
4 | // Exposed settings
5 | public static final String DESTROY_PROXY_ON_EXIT = "DestroyProxyOnExit";
6 |
7 | // Internal states
8 | public static final String BURP_INSTANCE_ID = "BurpInstanceId";
9 | public static final String LAST_STATE = "LastState";
10 | public static final String CURRENT_PROVIDER = "CurrentProvider";
11 | public static final String PROXY_SETTINGS_BACKUP = "ProxySettingsBackup";
12 | public static final String PROXY_SETTINGS = "ProxySettings";
13 | }
14 |
--------------------------------------------------------------------------------
/src/vpsproxy/VPSProxy.java:
--------------------------------------------------------------------------------
1 | package vpsproxy;
2 |
3 | import java.util.LinkedHashMap;
4 | import java.util.Map;
5 | import burp.IBurpExtenderCallbacks;
6 | import vpsproxy.providers.DigitalOceanProvider;
7 | import vpsproxy.providers.Provider.ProviderException;
8 | import vpsproxy.providers.*;
9 |
10 | public class VPSProxy {
11 | 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\"}}}}";
12 |
13 | private IBurpExtenderCallbacks callbacks;
14 | private VPSProxyTab optionsTab;
15 |
16 | private Map providerMap;
17 |
18 | public VPSProxy(IBurpExtenderCallbacks callbacks) {
19 | this.callbacks = callbacks;
20 | createBurpInstanceId();
21 |
22 | providerMap = new LinkedHashMap<>();
23 | addProvider(new AWSProvider(callbacks));
24 | addProvider(new DigitalOceanProvider(callbacks));
25 | addProvider(new LinodeProvider(callbacks));
26 |
27 | optionsTab = new VPSProxyTab(this, providerMap);
28 | Logger.init(callbacks.getStdout(), optionsTab);
29 |
30 | restorePreviousProxy();
31 | }
32 |
33 | public VPSProxyTab getUI() {
34 | return optionsTab;
35 | }
36 |
37 | public void close() {
38 | String destroyProxySetting = callbacks.loadExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT);
39 | boolean destroyProxy = destroyProxySetting == null || Boolean.parseBoolean(destroyProxySetting);
40 |
41 | if (!destroyProxy)
42 | return;
43 |
44 | Provider currentProvider = optionsTab.getSelectedProvider();
45 | if (currentProvider != null) {
46 | try {
47 | destroyInstance(currentProvider);
48 | } catch (ProviderException e) {
49 | } catch (Exception e) {
50 | Logger.log(String.format("Unhandled exception: %s", e.getMessage()));
51 | }
52 | }
53 | }
54 |
55 | protected IBurpExtenderCallbacks getCallbacks() {
56 | return callbacks;
57 | }
58 |
59 | protected void startInstance(Provider provider) throws ProviderException {
60 | try {
61 | ProxySettings proxy = provider.startInstance();
62 | configureProxy(proxy);
63 | } catch (ProviderException e) {
64 | Logger.log(e.getMessage());
65 | throw e;
66 | } catch (Exception e) {
67 | Logger.log(String.format("Unhandled exception: %s", e.getMessage()));
68 | throw e;
69 | }
70 | }
71 |
72 | protected void destroyInstance(Provider provider) throws ProviderException {
73 | try {
74 | provider.destroyInstance();
75 | resetProxySettings();
76 | optionsTab.setStoppedState();
77 | } catch (ProviderException e) {
78 | Logger.log(e.getMessage());
79 | throw e;
80 | } catch (Exception e) {
81 | Logger.log(String.format("Unhandled exception: %s", e.getMessage()));
82 | throw e;
83 | }
84 | }
85 |
86 | protected void configureProxy(ProxySettings proxy) {
87 | Logger.log(String.format("Configuring proxy %s:%s:%s:%s", proxy.getIp(), proxy.getPort(), proxy.getUsername(),
88 | proxy.getPassword()));
89 |
90 | // Save current config
91 | String configBackup = callbacks.saveConfigAsJson("project_options.connections.socks_proxy");
92 | callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS_BACKUP, configBackup);
93 |
94 | // Set new proxy settings
95 | String config = String.format(PROXY_CONFIG_TEMPLATE, proxy.getIp(), proxy.getPassword(), proxy.getPort(),
96 | proxy.getUsername());
97 | callbacks.loadConfigFromJson(config);
98 | callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS, config);
99 |
100 | Logger.log("Proxy configured. The VPS could still be provisioning, please give it a few minutes.");
101 | }
102 |
103 | protected void resetProxySettings() {
104 | String config = callbacks.loadExtensionSetting(SettingsKeys.PROXY_SETTINGS_BACKUP);
105 | if (config != null) {
106 | Logger.log("Restoring proxy settings");
107 | callbacks.loadConfigFromJson(config);
108 | callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS_BACKUP, null);
109 | callbacks.saveExtensionSetting(SettingsKeys.PROXY_SETTINGS, null);
110 | }
111 | }
112 |
113 | protected void restorePreviousProxy() {
114 | String config = callbacks.loadExtensionSetting(SettingsKeys.PROXY_SETTINGS);
115 | if (config != null) {
116 | Logger.log("Setting proxy from previous session");
117 | callbacks.loadConfigFromJson(config);
118 | }
119 | }
120 |
121 | private void createBurpInstanceId() {
122 | String instanceId = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
123 |
124 | if (instanceId == null) {
125 | String id = RandomString.generate(6);
126 | callbacks.saveExtensionSetting(SettingsKeys.BURP_INSTANCE_ID, id);
127 | }
128 | }
129 |
130 | private void addProvider(Provider provider) {
131 | providerMap.put(provider.getName(), provider);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/vpsproxy/VPSProxyTab.java:
--------------------------------------------------------------------------------
1 | package vpsproxy;
2 |
3 | import javax.swing.*;
4 | import javax.swing.event.DocumentEvent;
5 | import javax.swing.event.DocumentListener;
6 |
7 | import burp.ITab;
8 | import java.awt.*;
9 | import java.awt.event.*;
10 | import java.awt.event.ActionListener;
11 | import java.util.Map;
12 | import vpsproxy.providers.Provider;
13 |
14 | public class VPSProxyTab implements ITab {
15 | private Map providerMap;
16 | VPSProxy extension;
17 |
18 | private JPanel panel;
19 | private JPanel providerPanel;
20 | private JCheckBox destroyProxyCheckBox;
21 | private JLabel destroyProxyLabel;
22 | private JComboBox providerComboBox;
23 | private JButton deployButton;
24 | private JButton stopButton;
25 | private JTextArea logTextArea;
26 | private JScrollPane logScrollPane;
27 |
28 | private Font defaultFont;
29 | private Font headerFont;
30 | private Color headerColor;
31 | private int gapSize = 25;
32 |
33 | Thread workerThread;
34 |
35 | public VPSProxyTab(VPSProxy extension, Map providers) {
36 | providerMap = providers;
37 | this.extension = extension;
38 |
39 | // Initialize fonts and colors
40 | JLabel defaultLabel = new JLabel();
41 | extension.getCallbacks().customizeUiComponent(defaultLabel);
42 |
43 | defaultFont = defaultLabel.getFont();
44 | headerFont = defaultFont.deriveFont(Font.BOLD, defaultFont.getSize() + 2);
45 |
46 | Color defaultColor = defaultLabel.getForeground();
47 | if (defaultColor.getRed() > 128 && defaultColor.getGreen() > 128 && defaultColor.getBlue() > 128)
48 | headerColor = Color.WHITE;
49 | else
50 | headerColor = Color.BLACK;
51 |
52 | // Initialize main panel
53 | this.panel = new JPanel();
54 |
55 | // Intro UI elements
56 | JLabel introHeaderLabel = new JLabel("Burp VPS Proxy");
57 | introHeaderLabel.setFont(headerFont);
58 | introHeaderLabel.setForeground(headerColor);
59 |
60 | JLabel introHelp1Label = new JLabel(
61 | "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.");
62 | JLabel introHelp2Label = new JLabel(
63 | "Once created, the extension will automatically configure your SOCKS5 proxy in Burp -> Settings -> Network -> Connections.");
64 | JLabel introHelp3Label = new JLabel(
65 | "The proxy server will automatically be terminated when Burp exits or the extension is unloaded.");
66 |
67 | // Options UI elements
68 | JLabel optionsHeaderLabel = new JLabel("Options");
69 | optionsHeaderLabel.setFont(headerFont);
70 | optionsHeaderLabel.setForeground(headerColor);
71 |
72 | destroyProxyCheckBox = new JCheckBox();
73 | String destroyProxy = extension.getCallbacks().loadExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT);
74 | if (destroyProxy == null || destroyProxy.equals("true")) {
75 | destroyProxyCheckBox.setSelected(true);
76 | }
77 | destroyProxyLabel = new JLabel("Destroy proxy when Burp exits");
78 |
79 | // Provider UI elements
80 | JLabel providerHeaderLabel = new JLabel("Provider");
81 | providerHeaderLabel.setFont(headerFont);
82 | providerHeaderLabel.setForeground(headerColor);
83 |
84 | providerComboBox = new JComboBox<>();
85 | providerComboBox.setMaximumSize(new Dimension(150, providerComboBox.getPreferredSize().height));
86 | for (String providerName : providerMap.keySet()) {
87 | providerComboBox.addItem(providerName);
88 | }
89 |
90 | String selectedProviderName = extension.getCallbacks().loadExtensionSetting(SettingsKeys.CURRENT_PROVIDER);
91 | providerComboBox.setSelectedItem(selectedProviderName);
92 |
93 | deployButton = new JButton("Deploy");
94 | stopButton = new JButton("Stop");
95 | stopButton.setEnabled(false);
96 |
97 | // Provider settings UI elements
98 | FlowLayout providerLayout = new FlowLayout(FlowLayout.LEFT);
99 | providerLayout.setHgap(0);
100 | providerLayout.setVgap(0);
101 |
102 | providerPanel = new JPanel(providerLayout);
103 | providerPanel.setMaximumSize(new Dimension(Short.MAX_VALUE, 200));
104 |
105 | Provider currentProvider = getSelectedProvider();
106 | if (currentProvider != null) {
107 | providerPanel.add(currentProvider.getUI());
108 | }
109 |
110 | // Log UI elements
111 | JLabel logHeaderLabel = new JLabel("Log");
112 | logHeaderLabel.setFont(headerFont);
113 | logHeaderLabel.setForeground(headerColor);
114 |
115 | logTextArea = new JTextArea();
116 | logTextArea.setEditable(false);
117 |
118 | int logScrollPaneHeight = 400;
119 | logScrollPane = new JScrollPane(logTextArea);
120 | logScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
121 | logScrollPane.setMaximumSize(new Dimension(Short.MAX_VALUE, logScrollPaneHeight));
122 |
123 | JScrollBar verticalBar = logScrollPane.getVerticalScrollBar();
124 | verticalBar.setValue(verticalBar.getMaximum());
125 |
126 | // Layout
127 | GroupLayout layout = new GroupLayout(this.panel);
128 | layout.setAutoCreateGaps(true);
129 | layout.setAutoCreateContainerGaps(true);
130 |
131 | layout.setHorizontalGroup(layout.createParallelGroup()
132 | .addComponent(introHeaderLabel)
133 | .addComponent(introHelp1Label)
134 | .addComponent(introHelp2Label)
135 | .addComponent(introHelp3Label)
136 | .addComponent(optionsHeaderLabel)
137 | .addGroup(layout.createSequentialGroup()
138 | .addComponent(destroyProxyCheckBox)
139 | .addComponent(destroyProxyLabel))
140 | .addComponent(providerHeaderLabel)
141 | .addGroup(layout.createSequentialGroup()
142 | .addComponent(providerComboBox)
143 | .addComponent(deployButton)
144 | .addComponent(stopButton))
145 | .addComponent(providerPanel)
146 | .addComponent(logHeaderLabel)
147 | .addComponent(logScrollPane));
148 |
149 | layout.setVerticalGroup(layout.createSequentialGroup()
150 | .addComponent(introHeaderLabel)
151 | .addComponent(introHelp1Label)
152 | .addComponent(introHelp2Label)
153 | .addComponent(introHelp3Label)
154 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, gapSize)
155 | .addComponent(optionsHeaderLabel)
156 | .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
157 | .addComponent(destroyProxyCheckBox)
158 | .addComponent(destroyProxyLabel))
159 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, gapSize)
160 | .addComponent(providerHeaderLabel)
161 | .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
162 | .addComponent(providerComboBox)
163 | .addComponent(deployButton)
164 | .addComponent(stopButton))
165 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, 5)
166 | .addComponent(providerPanel)
167 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, 2 * gapSize)
168 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, 0, Short.MAX_VALUE)
169 | .addComponent(logHeaderLabel)
170 | .addComponent(logScrollPane, GroupLayout.PREFERRED_SIZE, logScrollPaneHeight,
171 | GroupLayout.PREFERRED_SIZE));
172 |
173 | layout.linkSize(deployButton, stopButton);
174 |
175 | this.panel.setLayout(layout);
176 |
177 | String lastState = extension.getCallbacks().loadExtensionSetting(SettingsKeys.LAST_STATE);
178 | if (lastState != null && lastState.equals("running")) {
179 | setRunningState();
180 | }
181 |
182 | installHandlers();
183 | }
184 |
185 | @Override
186 | public String getTabCaption() {
187 | return "VPS Proxy";
188 | }
189 |
190 | @Override
191 | public Component getUiComponent() {
192 | return this.panel;
193 | }
194 |
195 | public void log(String message) {
196 | logTextArea.append(message);
197 | }
198 |
199 | private void installHandlers() {
200 | destroyProxyCheckBox.addActionListener(new ActionListener() {
201 | @Override
202 | public void actionPerformed(ActionEvent e) {
203 | if (destroyProxyCheckBox.isSelected()) {
204 | extension.getCallbacks().saveExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT, "true");
205 | } else {
206 | extension.getCallbacks().saveExtensionSetting(SettingsKeys.DESTROY_PROXY_ON_EXIT, "false");
207 | }
208 | }
209 | });
210 |
211 | destroyProxyLabel.addMouseListener(new MouseAdapter() {
212 | @Override
213 | public void mouseClicked(MouseEvent e) {
214 | destroyProxyCheckBox.doClick();
215 | }
216 | });
217 |
218 | providerComboBox.addActionListener(new ActionListener() {
219 | @Override
220 | public void actionPerformed(ActionEvent e) {
221 | Provider selectedProvider = getSelectedProvider();
222 | if (selectedProvider == null) {
223 | return;
224 | }
225 |
226 | // Replace the provider UI panel with the new provider UI
227 | providerPanel.removeAll();
228 | providerPanel.add(selectedProvider.getUI());
229 | providerPanel.revalidate();
230 | providerPanel.repaint();
231 |
232 | extension.getCallbacks().saveExtensionSetting(SettingsKeys.CURRENT_PROVIDER,
233 | selectedProvider.getName());
234 | }
235 | });
236 |
237 | deployButton.addActionListener(new ActionListener() {
238 | @Override
239 | public void actionPerformed(ActionEvent e) {
240 | Provider selectedProvider = getSelectedProvider();
241 | if (selectedProvider == null) {
242 | Logger.log("No provider selected");
243 | return;
244 | }
245 |
246 | if (workerThread != null && workerThread.isAlive()) {
247 | Logger.log("Worker thread is already started!");
248 | return;
249 | }
250 |
251 | setRunningState();
252 |
253 | workerThread = new Thread(() -> {
254 | try {
255 | extension.startInstance(selectedProvider);
256 | } catch (Exception ex) {
257 | setStoppedState();
258 | }
259 | });
260 | workerThread.start();
261 | }
262 | });
263 |
264 | stopButton.addActionListener(new ActionListener() {
265 | @Override
266 | public void actionPerformed(ActionEvent e) {
267 | Provider selectedProvider = getSelectedProvider();
268 | if (selectedProvider == null) {
269 | Logger.log("No provider selected");
270 | return;
271 | }
272 |
273 | if (workerThread != null && workerThread.isAlive()) {
274 | Logger.log("Wait for instance to finish deploying...");
275 | return;
276 | }
277 |
278 | setStoppedState();
279 |
280 | try {
281 | extension.destroyInstance(selectedProvider);
282 | } catch (Exception ex) {
283 | }
284 | }
285 | });
286 |
287 | logTextArea.getDocument().addDocumentListener(new DocumentListener() {
288 | @Override
289 | public void insertUpdate(DocumentEvent e) {
290 | JScrollBar verticalBar = logScrollPane.getVerticalScrollBar();
291 | SwingUtilities.invokeLater(new Runnable() {
292 | @Override
293 | public void run() {
294 | verticalBar.setValue(verticalBar.getMaximum());
295 | }
296 | });
297 | }
298 |
299 | @Override
300 | public void removeUpdate(DocumentEvent e) {
301 | // Do nothing
302 | }
303 |
304 | @Override
305 | public void changedUpdate(DocumentEvent e) {
306 | // Do nothing
307 | }
308 | });
309 | }
310 |
311 | protected Provider getSelectedProvider() {
312 | Object selectedItem = providerComboBox.getSelectedItem();
313 | if (selectedItem == null) {
314 | return null;
315 | }
316 |
317 | String providerName = selectedItem.toString();
318 | Provider provider = providerMap.get(providerName);
319 | if (provider == null) {
320 | return null;
321 | }
322 |
323 | return provider;
324 | }
325 |
326 | public void setRunningState() {
327 | extension.getCallbacks().saveExtensionSetting(SettingsKeys.LAST_STATE, "running");
328 |
329 | stopButton.setEnabled(true);
330 | stopButton.requestFocusInWindow();
331 | providerComboBox.setEnabled(false);
332 | deployButton.setEnabled(false);
333 |
334 | Component[] providerPanelComponents = providerPanel.getComponents();
335 | if (providerPanelComponents.length != 0) {
336 | JPanel panel = (JPanel) providerPanelComponents[0];
337 | Component[] components = panel.getComponents();
338 | for (Component component : components) {
339 | component.setEnabled(false);
340 | }
341 | }
342 | }
343 |
344 | public void setStoppedState() {
345 | extension.getCallbacks().saveExtensionSetting(SettingsKeys.LAST_STATE, "stopped");
346 |
347 | deployButton.setEnabled(true);
348 | deployButton.requestFocusInWindow();
349 | providerComboBox.setEnabled(true);
350 | stopButton.setEnabled(false);
351 |
352 | Component[] providerPanelComponents = providerPanel.getComponents();
353 | if (providerPanelComponents.length != 0) {
354 | JPanel panel = (JPanel) providerPanelComponents[0];
355 | Component[] components = panel.getComponents();
356 | for (Component component : components) {
357 | component.setEnabled(true);
358 | }
359 | }
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/src/vpsproxy/providers/AWSProvider.java:
--------------------------------------------------------------------------------
1 | package vpsproxy.providers;
2 |
3 | import java.awt.*;
4 | import java.awt.event.*;
5 | import java.io.IOException;
6 | import java.util.Base64;
7 | import java.util.Collections;
8 | import java.util.Comparator;
9 | import java.util.Optional;
10 | import javax.swing.*;
11 | import javax.swing.event.*;
12 | import burp.IBurpExtenderCallbacks;
13 | import vpsproxy.*;
14 |
15 | import software.amazon.awssdk.auth.credentials.*;
16 | import software.amazon.awssdk.regions.Region;
17 | import software.amazon.awssdk.services.ec2.Ec2Client;
18 | import software.amazon.awssdk.services.ec2.model.*;
19 | import software.amazon.awssdk.services.ec2.model.Image;
20 |
21 | public class AWSProvider extends Provider {
22 | final private String INSTANCE_TAG = "burp-vps-proxy";
23 | final private String AWS_OS_TYPE = "debian-11";
24 | final private String AWS_INSTANCE_ARCH = "arm64";
25 | final private InstanceType AWS_INSTANCE_TYPE = InstanceType.T4_G_NANO;
26 |
27 | final private String AWS_ACCESS_KEY_SETTING = "ProviderAWSAccessKey";
28 | final private String AWS_SECRET_KEY_SETTING = "ProviderAWSSecretKey";
29 | final private String AWS_REGION_SETTING = "ProviderAWSRegion";
30 | final private String[] AWS_REGIONS = {
31 | "us-east-2",
32 | "us-east-1",
33 | "us-west-1",
34 | "us-west-2",
35 | "af-south-1",
36 | "ap-east-1",
37 | "ap-south-2",
38 | "ap-southeast-3",
39 | "ap-southeast-4",
40 | "ap-south-1",
41 | "ap-northeast-3",
42 | "ap-northeast-2",
43 | "ap-southeast-1",
44 | "ap-southeast-2",
45 | "ap-northeast-1",
46 | "ca-central-1",
47 | "eu-central-1",
48 | "eu-west-1",
49 | "eu-west-2",
50 | "eu-south-1",
51 | "eu-west-3",
52 | "eu-south-2",
53 | "eu-north-1",
54 | "eu-central-2",
55 | "me-south-1",
56 | "me-central-1",
57 | "sa-east-1",
58 | };
59 |
60 | private IBurpExtenderCallbacks callbacks;
61 |
62 | private String awsRegion = "us-east-1";
63 | private Ec2Client ec2Client;
64 |
65 | public AWSProvider(IBurpExtenderCallbacks callbacks) {
66 | this.callbacks = callbacks;
67 | }
68 |
69 | @Override
70 | public String getName() {
71 | return "AWS EC2";
72 | }
73 |
74 | @Override
75 | public ProxySettings startInstance() throws ProviderException {
76 | log("creating a new instance");
77 |
78 | Ec2Client ec2Client;
79 | try {
80 | ec2Client = createClient();
81 | } catch (ProviderException e) {
82 | throw e;
83 | }
84 |
85 | String password = RandomString.generate(12);
86 | String script;
87 | try {
88 | script = getProvisioningScript(password);
89 | } catch (IOException e) {
90 | throw new ProviderException(String.format("error loading provisioning script: %s", e.getMessage()), e);
91 | }
92 |
93 | String amiId;
94 | try {
95 | amiId = getAmiId(AWS_OS_TYPE, awsRegion);
96 | } catch (ProviderException e) {
97 | throw e;
98 | }
99 |
100 | String securityGroupId;
101 | try {
102 | securityGroupId = createSecurityGroup("burp-vps-proxy", "Allow traffic to port 1080 for the Burp SOCKS Proxy");
103 | } catch (ProviderException e) {
104 | throw e;
105 | }
106 |
107 | String instanceName = String.format("burp-vps-proxy-%s", RandomString.generate(4));
108 | Tag nameTag = Tag.builder()
109 | .key("Name")
110 | .value(instanceName)
111 | .build();
112 |
113 | String tagValue = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
114 | Tag proxyTag = Tag.builder()
115 | .key(INSTANCE_TAG)
116 | .value(tagValue)
117 | .build();
118 |
119 | TagSpecification tagSpecification = TagSpecification.builder()
120 | .resourceType("instance")
121 | .tags(nameTag, proxyTag)
122 | .build();
123 |
124 | RunInstancesRequest runRequest = RunInstancesRequest.builder()
125 | .instanceType(AWS_INSTANCE_TYPE)
126 | .maxCount(1)
127 | .minCount(1)
128 | .imageId(amiId)
129 | .userData(script)
130 | .tagSpecifications(tagSpecification)
131 | .securityGroupIds(securityGroupId)
132 | .build();
133 |
134 | RunInstancesResponse runResponse;
135 | String instanceId;
136 | try {
137 | runResponse = ec2Client.runInstances(runRequest);
138 | instanceId = runResponse.instances().get(0).instanceId();
139 | ec2Client.waiter().waitUntilInstanceRunning(r -> r.instanceIds(instanceId));
140 | } catch (Exception e) {
141 | throw new ProviderException(String.format("error creating instance: %s", e.getMessage()), e);
142 | }
143 |
144 | DescribeInstancesRequest describeRequest = DescribeInstancesRequest.builder()
145 | .instanceIds(instanceId)
146 | .build();
147 |
148 | DescribeInstancesResponse describeResponse;
149 | try {
150 | describeResponse = ec2Client.describeInstances(describeRequest);
151 | } catch (Exception e) {
152 | throw new ProviderException(String.format("error reading newly created instance: %s", e.getMessage()), e);
153 | }
154 |
155 | String publicIpAddress = describeResponse.reservations().get(0).instances().get(0).publicIpAddress();
156 | return createProxySettings(publicIpAddress, password);
157 | }
158 |
159 | @Override
160 | public void destroyInstance() throws ProviderException {
161 | Ec2Client ec2Client;
162 | try {
163 | ec2Client = createClient();
164 | } catch (ProviderException e) {
165 | throw e;
166 | }
167 |
168 | String tagValue = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
169 |
170 | DescribeInstancesRequest describeRequest = DescribeInstancesRequest.builder()
171 | .filters(
172 | Filter.builder()
173 | .name("tag:" + INSTANCE_TAG)
174 | .values(tagValue)
175 | .build(),
176 | Filter.builder()
177 | .name("instance-state-name")
178 | .values("pending", "running", "rebooting", "stopping", "stopped")
179 | .build())
180 | .build();
181 |
182 | DescribeInstancesResponse describeResponse;
183 | try {
184 | describeResponse = ec2Client.describeInstances(describeRequest);
185 | } catch (Exception e) {
186 | throw new ProviderException(String.format("error listing instances: %s", e.getMessage()), e);
187 | }
188 |
189 | describeResponse.reservations().stream()
190 | .flatMap(reservation -> reservation.instances().stream())
191 | .forEach(instance -> {
192 | String instanceId = instance.instanceId();
193 | String instanceName = "";
194 | for (Tag tag : instance.tags()) {
195 | if (tag.key().equals("Name")) {
196 | instanceName = tag.value();
197 | break;
198 | }
199 | }
200 |
201 | TerminateInstancesRequest terminateRequest = TerminateInstancesRequest.builder()
202 | .instanceIds(instanceId)
203 | .build();
204 |
205 | try {
206 | ec2Client.terminateInstances(terminateRequest);
207 | logf("instance %s deleted", instanceName);
208 | } catch (Exception e) {
209 | logf("error deleting instance '%s': %s", instanceName, e.getMessage());
210 | }
211 | });
212 | }
213 |
214 | @Override
215 | public JComponent getUI() {
216 | JPanel panel = new JPanel();
217 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
218 |
219 | JLabel awsAccessKeyLabel = new JLabel("AWS Access Key:");
220 | awsAccessKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
221 |
222 | JTextField awsAccessKeyTextField = new JTextField();
223 | awsAccessKeyTextField.setAlignmentX(Component.LEFT_ALIGNMENT);
224 | awsAccessKeyTextField.setPreferredSize(new Dimension(200, awsAccessKeyTextField.getPreferredSize().height));
225 | awsAccessKeyTextField.setText(callbacks.loadExtensionSetting(AWS_ACCESS_KEY_SETTING));
226 |
227 | JLabel awsSecretKeyLabel = new JLabel("AWS Secret Key:");
228 | awsSecretKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
229 |
230 | JPasswordField awsSecretKeyPasswordField = new JPasswordField();
231 | awsSecretKeyPasswordField.setAlignmentX(Component.LEFT_ALIGNMENT);
232 | awsSecretKeyPasswordField.setPreferredSize(new Dimension(200, awsSecretKeyPasswordField.getPreferredSize().height));
233 | awsSecretKeyPasswordField.setText(callbacks.loadExtensionSetting(AWS_SECRET_KEY_SETTING));
234 |
235 | JLabel awsRegionLabel = new JLabel("Region:");
236 | awsRegionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
237 |
238 | JComboBox awsRegionComboBox = new JComboBox<>();
239 | awsRegionComboBox.setAlignmentX(Component.LEFT_ALIGNMENT);
240 | awsRegionComboBox.setMaximumSize(new Dimension(125, awsRegionComboBox.getPreferredSize().height));
241 | for (int i = 0; i < AWS_REGIONS.length; i++) {
242 | awsRegionComboBox.addItem(AWS_REGIONS[i]);
243 | }
244 |
245 | String selectedRegion = callbacks.loadExtensionSetting(AWS_REGION_SETTING);
246 | if (selectedRegion != null && !selectedRegion.isEmpty()) {
247 | awsRegionComboBox.setSelectedItem(selectedRegion);
248 | }
249 |
250 | panel.add(awsAccessKeyLabel);
251 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
252 | panel.add(awsAccessKeyTextField);
253 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
254 | panel.add(awsSecretKeyLabel);
255 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
256 | panel.add(awsSecretKeyPasswordField);
257 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
258 | panel.add(awsRegionLabel);
259 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
260 | panel.add(awsRegionComboBox);
261 |
262 | awsAccessKeyTextField.getDocument().addDocumentListener(new DocumentListener() {
263 | @Override
264 | public void insertUpdate(DocumentEvent e) {
265 | saveSetting();
266 | }
267 | @Override
268 | public void removeUpdate(DocumentEvent e) {
269 | saveSetting();
270 | }
271 | @Override
272 | public void changedUpdate(DocumentEvent e) {
273 | saveSetting();
274 | }
275 |
276 | private void saveSetting() {
277 | String value = awsAccessKeyTextField.getText();
278 | callbacks.saveExtensionSetting(AWS_ACCESS_KEY_SETTING, value);
279 | }
280 | });
281 |
282 | awsSecretKeyPasswordField.getDocument().addDocumentListener(new DocumentListener() {
283 | @Override
284 | public void insertUpdate(DocumentEvent e) {
285 | saveSetting();
286 | }
287 | @Override
288 | public void removeUpdate(DocumentEvent e) {
289 | saveSetting();
290 | }
291 | @Override
292 | public void changedUpdate(DocumentEvent e) {
293 | saveSetting();
294 | }
295 |
296 | private void saveSetting() {
297 | String value = new String(awsSecretKeyPasswordField.getPassword());
298 | callbacks.saveExtensionSetting(AWS_SECRET_KEY_SETTING, value);
299 | }
300 | });
301 |
302 | awsRegionComboBox.addActionListener(new ActionListener() {
303 | @Override
304 | public void actionPerformed(ActionEvent e) {
305 | Object selectedItem = awsRegionComboBox.getSelectedItem();
306 | if (selectedItem == null) {
307 | return;
308 | }
309 |
310 | awsRegion = selectedItem.toString();
311 | callbacks.saveExtensionSetting(AWS_REGION_SETTING, awsRegion);
312 | }
313 | });
314 |
315 | return panel;
316 | }
317 |
318 | @Override
319 | protected String getProvisioningScript(String password) throws IOException {
320 | String script = super.getProvisioningScript(password);
321 | return Base64.getEncoder().encodeToString(script.getBytes());
322 | }
323 |
324 | private Ec2Client createClient() throws ProviderException {
325 | // Load the AWS keys from settings
326 | String awsAccessKey = callbacks.loadExtensionSetting(AWS_ACCESS_KEY_SETTING);
327 | String awsSecretKey = callbacks.loadExtensionSetting(AWS_SECRET_KEY_SETTING);
328 |
329 | if (awsAccessKey == null || awsSecretKey == null || awsAccessKey.isEmpty() || awsSecretKey.isEmpty()) {
330 | throw new ProviderException("missing API key(s)", null);
331 | }
332 |
333 | try {
334 | // Configure the region
335 | Region region = Region.of(awsRegion);
336 |
337 | // Create the client
338 | AwsCredentials credentials = AwsBasicCredentials.create(awsAccessKey, awsSecretKey);
339 | ec2Client = Ec2Client.builder()
340 | .region(region)
341 | .credentialsProvider(() -> credentials)
342 | .build();
343 | } catch (Exception e) {
344 | throw new ProviderException(String.format("error creating AWS client: %s", e.getMessage()), e);
345 | }
346 |
347 | return ec2Client;
348 | }
349 |
350 | private String getAmiId(String osType, String region) throws ProviderException {
351 | // Filter by name
352 | Filter osFilter = Filter.builder()
353 | .name("name")
354 | .values(osType + "-*")
355 | .build();
356 |
357 | // Filter by architecture
358 | Filter architectureFilter = Filter.builder()
359 | .name("architecture")
360 | .values(AWS_INSTANCE_ARCH)
361 | .build();
362 |
363 | // Find the requested image
364 | DescribeImagesRequest describeImagesRequest = DescribeImagesRequest.builder()
365 | .owners("136693071363") // Debian AMI owner ID
366 | .filters(osFilter, architectureFilter)
367 | .build();
368 |
369 | DescribeImagesResponse describeImagesResponse;
370 | try {
371 | describeImagesResponse = ec2Client.describeImages(describeImagesRequest);
372 | } catch (Exception e) {
373 | throw new ProviderException(String.format("failed to find image '%s': %s", osType, e.getMessage()), e);
374 | }
375 |
376 | Optional latestImage = describeImagesResponse.images().stream()
377 | .max(Comparator.comparing(Image::creationDate));
378 |
379 | // Return the most recent image found
380 | return latestImage.map(Image::imageId).orElse(null);
381 | }
382 |
383 | private String createSecurityGroup(String groupName, String groupDescription) throws ProviderException {
384 | // Check if the security group already exists
385 | DescribeSecurityGroupsResponse describeResponse;
386 | try {
387 | describeResponse = ec2Client.describeSecurityGroups();
388 | } catch (Exception e) {
389 | throw new ProviderException(String.format("error listing security groups: %s", e.getMessage()), e);
390 | }
391 |
392 | Optional securityGroup = describeResponse.securityGroups().stream()
393 | .filter(sg -> sg.groupName().equals(groupName))
394 | .findFirst();
395 |
396 | if (securityGroup.isPresent()) {
397 | // Security group already exists, return its ID
398 | return securityGroup.get().groupId();
399 | }
400 |
401 | // Create the security group
402 | CreateSecurityGroupRequest createRequest = CreateSecurityGroupRequest.builder()
403 | .groupName(groupName)
404 | .description(groupDescription)
405 | .build();
406 |
407 | CreateSecurityGroupResponse createResponse;
408 | try {
409 | createResponse = ec2Client.createSecurityGroup(createRequest);
410 | } catch (Exception e) {
411 | throw new ProviderException(String.format("error creating security groups: %s", e.getMessage()), e);
412 | }
413 |
414 | String groupId = createResponse.groupId();
415 |
416 | // Add a rule to the security group that allows traffic to port 1080
417 | IpRange ipRange = IpRange.builder()
418 | .cidrIp("0.0.0.0/0")
419 | .build();
420 | IpPermission ipPermission = IpPermission.builder()
421 | .ipProtocol("tcp")
422 | .fromPort(1080)
423 | .toPort(1080)
424 | .ipRanges(ipRange)
425 | .build();
426 | AuthorizeSecurityGroupIngressRequest authorizeRequest = AuthorizeSecurityGroupIngressRequest.builder()
427 | .groupId(groupId)
428 | .ipPermissions(Collections.singletonList(ipPermission))
429 | .build();
430 |
431 | try {
432 | ec2Client.authorizeSecurityGroupIngress(authorizeRequest);
433 | } catch (Exception e) {
434 | throw new ProviderException(String.format("error authorizing security group ingress: %s", e.getMessage()), e);
435 | }
436 |
437 | // Return the ID of the security group
438 | return groupId;
439 | }
440 |
441 | public class CreateSecurityGroupException extends Exception {
442 | public CreateSecurityGroupException(String message, Throwable cause) {
443 | super(message, cause);
444 | }
445 | }
446 | }
447 |
--------------------------------------------------------------------------------
/src/vpsproxy/providers/DigitalOceanProvider.java:
--------------------------------------------------------------------------------
1 | package vpsproxy.providers;
2 |
3 | import java.awt.*;
4 | import java.awt.event.*;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import javax.swing.*;
8 | import javax.swing.event.*;
9 | import com.myjeeva.digitalocean.DigitalOcean;
10 | import com.myjeeva.digitalocean.common.DropletStatus;
11 | import com.myjeeva.digitalocean.impl.DigitalOceanClient;
12 | import com.myjeeva.digitalocean.pojo.Droplet;
13 | import burp.IBurpExtenderCallbacks;
14 | import vpsproxy.*;
15 |
16 | public class DigitalOceanProvider extends Provider {
17 | final private String DO_API_KEY_SETTING = "Provider_DigitalOcean_APIKey";
18 | final private String DO_REGION_SETTING = "Provider_DigitalOcean_Region";
19 | final private String[] DO_REGIONS = {
20 | "nyc1",
21 | "nyc3",
22 | "ams3",
23 | "sfo3",
24 | "sgp1",
25 | "lon1",
26 | "fra1",
27 | "tor1",
28 | "blr1",
29 | "syd1",
30 | };
31 |
32 | private IBurpExtenderCallbacks callbacks;
33 | private String doDropletTag = "burp-vps-proxy";
34 | private String doRegion = "nyc1";
35 |
36 | public DigitalOceanProvider(IBurpExtenderCallbacks callbacks) {
37 | this.callbacks = callbacks;
38 |
39 | String region = callbacks.loadExtensionSetting(DO_REGION_SETTING);
40 | if (region != null) {
41 | doRegion = region;
42 | }
43 |
44 | String burpInstanceId = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
45 | doDropletTag = doDropletTag + "-" + burpInstanceId;
46 | }
47 |
48 | @Override
49 | public String getName() {
50 | return "DigitalOcean";
51 | }
52 |
53 | @Override
54 | public ProxySettings startInstance() throws ProviderException {
55 | log("creating a new droplet");
56 |
57 | DigitalOcean client;
58 | try {
59 | client = getClient();
60 |
61 | String dropletName = String.format("burp-vps-proxy-%s", RandomString.generate(4));
62 | List tags = new ArrayList<>();
63 | tags.add(doDropletTag);
64 |
65 | Droplet droplet = new Droplet();
66 | droplet.setName(dropletName);
67 | droplet.setRegion(new com.myjeeva.digitalocean.pojo.Region(doRegion));
68 | droplet.setImage(new com.myjeeva.digitalocean.pojo.Image("debian-11-x64"));
69 | droplet.setSize("s-1vcpu-512mb-10gb");
70 | droplet.setTags(tags);
71 |
72 | String password = RandomString.generate(12);
73 | droplet.setUserData(getProvisioningScript(password));
74 |
75 | droplet = client.createDroplet(droplet);
76 |
77 | int attempts = 0;
78 | while (droplet.getStatus() != DropletStatus.ACTIVE) {
79 | Thread.sleep(2000);
80 | attempts++;
81 |
82 | if (attempts > 60) {
83 | log("droplet creation timed out");
84 | client.deleteDroplet(droplet.getId());
85 | log("droplet deleted");
86 | return null;
87 | }
88 |
89 | droplet = client.getDropletInfo(droplet.getId());
90 | }
91 |
92 | logf("droplet %s created", droplet.getName());
93 | return new ProxySettings(droplet.getNetworks().getVersion4Networks().get(0).getIpAddress(), "1080",
94 | "burp-vps-proxy", password);
95 | } catch (ProviderException e) {
96 | throw e;
97 | } catch (Exception e) {
98 | throw new ProviderException(String.format("error creating droplet: %s", e.getMessage()), e);
99 | }
100 | }
101 |
102 | @Override
103 | public void destroyInstance() throws ProviderException {
104 | try {
105 | DigitalOcean client = getClient();
106 |
107 | List droplets = client.getAvailableDroplets(0, Integer.MAX_VALUE).getDroplets();
108 | for (Droplet droplet : droplets) {
109 | List tags = droplet.getTags();
110 | if (tags != null && tags.contains(doDropletTag)) {
111 | client.deleteDroplet(droplet.getId());
112 | logf("droplet %s deleted", droplet.getName());
113 | }
114 | }
115 | } catch (ProviderException e) {
116 | throw e;
117 | } catch (Exception e) {
118 | throw new ProviderException(String.format("error deleting droplet: %s", e.getMessage()), e);
119 | }
120 | }
121 |
122 | @Override
123 | public JComponent getUI() {
124 | JPanel panel = new JPanel();
125 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
126 |
127 | JLabel apiKeyLabel = new JLabel("API key:");
128 | apiKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
129 |
130 | JPasswordField apiKeyPasswordField = new JPasswordField();
131 | apiKeyPasswordField.setAlignmentX(Component.LEFT_ALIGNMENT);
132 | apiKeyPasswordField.setPreferredSize(new Dimension(200, apiKeyPasswordField.getPreferredSize().height));
133 | apiKeyPasswordField.setText(callbacks.loadExtensionSetting(DO_API_KEY_SETTING));
134 |
135 | JLabel doRegionLabel = new JLabel("Region:");
136 | doRegionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
137 |
138 | JComboBox doRegionComboBox = new JComboBox<>();
139 | doRegionComboBox.setAlignmentX(Component.LEFT_ALIGNMENT);
140 | doRegionComboBox.setMaximumSize(new Dimension(75, doRegionComboBox.getPreferredSize().height));
141 | for (int i = 0; i < DO_REGIONS.length; i++) {
142 | doRegionComboBox.addItem(DO_REGIONS[i]);
143 | }
144 |
145 | String selectedRegion = callbacks.loadExtensionSetting(DO_REGION_SETTING);
146 | if (selectedRegion != null && !selectedRegion.isEmpty()) {
147 | doRegionComboBox.setSelectedItem(selectedRegion);
148 | }
149 |
150 | panel.add(apiKeyLabel);
151 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
152 | panel.add(apiKeyPasswordField);
153 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
154 | panel.add(doRegionLabel);
155 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
156 | panel.add(doRegionComboBox);
157 |
158 | apiKeyPasswordField.getDocument().addDocumentListener(new DocumentListener() {
159 | @Override
160 | public void insertUpdate(DocumentEvent e) {
161 | saveSetting();
162 | }
163 |
164 | @Override
165 | public void removeUpdate(DocumentEvent e) {
166 | saveSetting();
167 | }
168 |
169 | @Override
170 | public void changedUpdate(DocumentEvent e) {
171 | saveSetting();
172 | }
173 |
174 | private void saveSetting() {
175 | String value = new String(apiKeyPasswordField.getPassword());
176 | callbacks.saveExtensionSetting(DO_API_KEY_SETTING, value);
177 | }
178 | });
179 |
180 | doRegionComboBox.addActionListener(new ActionListener() {
181 | @Override
182 | public void actionPerformed(ActionEvent e) {
183 | Object selectedItem = doRegionComboBox.getSelectedItem();
184 | if (selectedItem == null) {
185 | return;
186 | }
187 |
188 | doRegion = selectedItem.toString();
189 | callbacks.saveExtensionSetting(DO_REGION_SETTING, doRegion);
190 | }
191 | });
192 |
193 | return panel;
194 | }
195 |
196 | private DigitalOcean getClient() throws ProviderException {
197 | String apiKey = callbacks.loadExtensionSetting(DO_API_KEY_SETTING);
198 | if (apiKey == null) {
199 | throw new ProviderException("no api key defined", null);
200 | }
201 |
202 | DigitalOcean client = new DigitalOceanClient(apiKey);
203 | try {
204 | client.getAccountInfo();
205 | } catch (Exception e) {
206 | throw new ProviderException(String.format("error getting account info: %s", e.getMessage()), e);
207 | }
208 |
209 | return client;
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/vpsproxy/providers/LinodeProvider.java:
--------------------------------------------------------------------------------
1 | package vpsproxy.providers;
2 |
3 | import java.awt.*;
4 | import java.awt.event.*;
5 | import java.util.List;
6 | import java.util.ArrayList;
7 | import javax.swing.*;
8 | import javax.swing.event.*;
9 | import java.net.HttpURLConnection;
10 | import java.net.URL;
11 | import java.io.BufferedReader;
12 | import java.io.InputStreamReader;
13 | import java.io.OutputStream;
14 | import org.json.JSONArray;
15 | import org.json.JSONObject;
16 |
17 | import burp.IBurpExtenderCallbacks;
18 | import vpsproxy.*;
19 |
20 | public class LinodeProvider extends Provider {
21 | public class InstanceInfo {
22 | private int id;
23 | private String label;
24 |
25 | public InstanceInfo(int id, String label) {
26 | this.id = id;
27 | this.label = label;
28 | }
29 |
30 | public int getId() {
31 | return id;
32 | }
33 |
34 | public String getLabel() {
35 | return label;
36 | }
37 | }
38 |
39 | final private String LINODE_API_BASE_URL = "https://api.linode.com/v4";
40 | final private String LINODE_API_CREATION_JSON = "{\"region\": \"%s\", \"type\": \"%s\", \"image\": \"%s\", \"root_pass\": \"%s\", \"label\": \"%s\", \"tags\": [\"%s\"]}";
41 | final private String LINODE_API_KEY_SETTING = "Provider_Linode_APIKey";
42 | final private String LINODE_REGION_SETTING = "Provider_Linode_Region";
43 | final private String LINODE_SIZE = "g6-nanode-1";
44 | final private String LINODE_IMAGE = "linode/debian11";
45 | final private int LINODE_TIMEOUT = 120;
46 | final private String[] LINODE_REGIONS = {
47 | "us-east",
48 | "us-central",
49 | "us-west",
50 | "us-southeast",
51 | "ca-central",
52 | "eu-west",
53 | "eu-central",
54 | "ap-south",
55 | "ap-northeast",
56 | "ap-west",
57 | "ap-southeast",
58 | };
59 |
60 | private IBurpExtenderCallbacks callbacks;
61 | private String linodeApiKey = "";
62 | private String linodeTag = "burp-vps-proxy";
63 | private String linodeRegion = "us-east";
64 |
65 | public LinodeProvider(IBurpExtenderCallbacks callbacks) {
66 | this.callbacks = callbacks;
67 |
68 | String burpInstanceId = callbacks.loadExtensionSetting(SettingsKeys.BURP_INSTANCE_ID);
69 | linodeTag = linodeTag + "-" + burpInstanceId;
70 |
71 | linodeApiKey = callbacks.loadExtensionSetting(LINODE_API_KEY_SETTING);
72 | String region = callbacks.loadExtensionSetting(LINODE_REGION_SETTING);
73 | if (region != null) {
74 | linodeRegion = region;
75 | }
76 | }
77 |
78 | @Override
79 | public String getName() {
80 | return "Linode";
81 | }
82 |
83 | @Override
84 | public ProxySettings startInstance() throws ProviderException {
85 | log("creating a new linode");
86 |
87 | try {
88 | String password = RandomString.generate(12);
89 | String rootPassword = RandomString.generate(24);
90 | String instanceName = String.format("burp-vps-proxy-%s", RandomString.generate(4));
91 |
92 | URL url = new URL(LINODE_API_BASE_URL + "/linode/instances");
93 |
94 | HttpURLConnection connection;
95 | connection = (HttpURLConnection) url.openConnection();
96 | connection.setRequestMethod("POST");
97 | connection.setRequestProperty("Content-Type", "application/json");
98 | connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
99 | connection.setDoOutput(true);
100 |
101 | String payload = String.format(LINODE_API_CREATION_JSON, linodeRegion, LINODE_SIZE, LINODE_IMAGE,
102 | rootPassword, instanceName, linodeTag);
103 |
104 | OutputStream os = connection.getOutputStream();
105 | os.write(payload.getBytes("UTF-8"));
106 |
107 | if (connection.getResponseCode() != 200) {
108 | throw new ProviderException("failed to created linode instance: "
109 | + connection.getResponseCode() + " " + connection.getResponseMessage(), null);
110 | }
111 |
112 | BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
113 |
114 | String output;
115 | StringBuilder sb = new StringBuilder();
116 | while ((output = br.readLine()) != null) {
117 | sb.append(output);
118 | }
119 |
120 | connection.disconnect();
121 |
122 | String responseJson = sb.toString();
123 | int instanceId = extractLinodeId(responseJson);
124 | String ipAddress = getInstanceIpAddress(instanceId);
125 |
126 | waitForStatus(instanceId, "running", LINODE_TIMEOUT);
127 | runProvisioningScript(ipAddress, "root", rootPassword, getProvisioningScript(password));
128 |
129 | logf("instance %s created", instanceName);
130 | return new ProxySettings(ipAddress, "1080", "burp-vps-proxy", password);
131 | } catch (ProviderException e) {
132 | throw e;
133 | } catch (Exception e) {
134 | throw new ProviderException(String.format("error establishing connection: %s", e.getMessage()),
135 | e);
136 | }
137 | }
138 |
139 | @Override
140 | public void destroyInstance() throws ProviderException {
141 | try {
142 | List instanceInfos = getInstanceIdsWithTag(linodeTag);
143 | for (InstanceInfo instanceInfo : instanceInfos) {
144 | deleteLinodeInstance(instanceInfo.id);
145 | logf("instance %s deleted", instanceInfo.label);
146 | }
147 | } catch (ProviderException e) {
148 | throw e;
149 | } catch (Exception e) {
150 | throw new ProviderException(String.format("error deleting linode: %s", e.getMessage()),
151 | e);
152 | }
153 | }
154 |
155 | @Override
156 | public JComponent getUI() {
157 | JPanel panel = new JPanel();
158 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
159 |
160 | JLabel apiKeyLabel = new JLabel("API key:");
161 | apiKeyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
162 |
163 | JPasswordField apiKeyPasswordField = new JPasswordField();
164 | apiKeyPasswordField.setAlignmentX(Component.LEFT_ALIGNMENT);
165 | apiKeyPasswordField.setPreferredSize(new Dimension(200, apiKeyPasswordField.getPreferredSize().height));
166 | apiKeyPasswordField.setText(linodeApiKey);
167 |
168 | JLabel linodeRegionLabel = new JLabel("Region:");
169 | linodeRegionLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
170 |
171 | JComboBox linodeRegionComboBox = new JComboBox<>();
172 | linodeRegionComboBox.setAlignmentX(Component.LEFT_ALIGNMENT);
173 | linodeRegionComboBox.setMaximumSize(new Dimension(125, linodeRegionComboBox.getPreferredSize().height));
174 | for (int i = 0; i < LINODE_REGIONS.length; i++) {
175 | linodeRegionComboBox.addItem(LINODE_REGIONS[i]);
176 | }
177 |
178 | String selectedRegion = callbacks.loadExtensionSetting(LINODE_REGION_SETTING);
179 | if (selectedRegion != null && !selectedRegion.isEmpty()) {
180 | linodeRegionComboBox.setSelectedItem(selectedRegion);
181 | }
182 |
183 | panel.add(apiKeyLabel);
184 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
185 | panel.add(apiKeyPasswordField);
186 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
187 | panel.add(linodeRegionLabel);
188 | panel.add(Box.createRigidArea(new Dimension(0, 5)));
189 | panel.add(linodeRegionComboBox);
190 |
191 | apiKeyPasswordField.getDocument().addDocumentListener(new DocumentListener() {
192 | @Override
193 | public void insertUpdate(DocumentEvent e) {
194 | saveSetting();
195 | }
196 |
197 | @Override
198 | public void removeUpdate(DocumentEvent e) {
199 | saveSetting();
200 | }
201 |
202 | @Override
203 | public void changedUpdate(DocumentEvent e) {
204 | saveSetting();
205 | }
206 |
207 | private void saveSetting() {
208 | String value = new String(apiKeyPasswordField.getPassword());
209 | callbacks.saveExtensionSetting(LINODE_API_KEY_SETTING, value);
210 | linodeApiKey = value;
211 | }
212 | });
213 |
214 | linodeRegionComboBox.addActionListener(new ActionListener() {
215 | @Override
216 | public void actionPerformed(ActionEvent e) {
217 | Object selectedItem = linodeRegionComboBox.getSelectedItem();
218 | if (selectedItem == null) {
219 | return;
220 | }
221 |
222 | linodeRegion = selectedItem.toString();
223 | callbacks.saveExtensionSetting(LINODE_REGION_SETTING, linodeRegion);
224 | }
225 | });
226 |
227 | return panel;
228 | }
229 |
230 | private void deleteLinodeInstance(int linodeId) throws Exception {
231 | URL url = new URL(LINODE_API_BASE_URL + "/linode/instances/" + linodeId);
232 | HttpURLConnection connection = (HttpURLConnection) url.openConnection();
233 | connection.setRequestMethod("DELETE");
234 | connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
235 |
236 | if (connection.getResponseCode() != 200) {
237 | throw new ProviderException("failed to delete instance: " + connection.getResponseCode() + " "
238 | + connection.getResponseMessage(), null);
239 |
240 | }
241 |
242 | connection.disconnect();
243 | }
244 |
245 | private static int extractLinodeId(String json) {
246 | JSONObject responseJson = new JSONObject(json);
247 | return responseJson.getInt("id");
248 | }
249 |
250 | private String getInstanceDetails(int linodeId) throws Exception {
251 | URL url = new URL(LINODE_API_BASE_URL + "/linode/instances/" + linodeId);
252 | HttpURLConnection connection = (HttpURLConnection) url.openConnection();
253 | connection.setRequestMethod("GET");
254 | connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
255 |
256 | if (connection.getResponseCode() != 200) {
257 | throw new ProviderException("failed to get instance details: " + connection.getResponseCode() + " "
258 | + connection.getResponseMessage(), null);
259 | }
260 |
261 | BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
262 | String output;
263 | StringBuilder sb = new StringBuilder();
264 | while ((output = br.readLine()) != null) {
265 | sb.append(output);
266 | }
267 |
268 | connection.disconnect();
269 |
270 | return sb.toString();
271 | }
272 |
273 | private String getInstanceIpAddress(int linodeId) throws Exception {
274 | String instanceDetails = getInstanceDetails(linodeId);
275 | JSONObject instanceJson = new JSONObject(instanceDetails);
276 | JSONArray ipv4Addresses = instanceJson.getJSONArray("ipv4");
277 | return ipv4Addresses.getString(0);
278 | }
279 |
280 | private String getInstanceStatus(int linodeId) throws Exception {
281 | String instanceDetails = getInstanceDetails(linodeId);
282 | JSONObject instanceJson = new JSONObject(instanceDetails);
283 | return instanceJson.getString("status");
284 | }
285 |
286 | private List getInstanceIdsWithTag(String tag) throws Exception {
287 | URL url = new URL(LINODE_API_BASE_URL + "/linode/instances");
288 | HttpURLConnection connection = (HttpURLConnection) url.openConnection();
289 | connection.setRequestMethod("GET");
290 | connection.setRequestProperty("Authorization", "Bearer " + linodeApiKey);
291 |
292 | if (connection.getResponseCode() != 200) {
293 | throw new ProviderException("failed to list instances: " + connection.getResponseCode() + " "
294 | + connection.getResponseMessage(), null);
295 | }
296 |
297 | BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
298 | String output;
299 | StringBuilder sb = new StringBuilder();
300 | while ((output = br.readLine()) != null) {
301 | sb.append(output);
302 | }
303 |
304 | String responseJson = sb.toString();
305 | JSONObject jsonResponse = new JSONObject(responseJson);
306 | JSONArray instances = jsonResponse.getJSONArray("data");
307 |
308 | List instanceInfos = new ArrayList<>();
309 | for (int i = 0; i < instances.length(); i++) {
310 | JSONObject instance = instances.getJSONObject(i);
311 | JSONArray instanceTags = instance.getJSONArray("tags");
312 | for (int j = 0; j < instanceTags.length(); j++) {
313 | if (tag.equals(instanceTags.getString(j))) {
314 | instanceInfos.add(new InstanceInfo(instance.getInt("id"), instance.getString("label")));
315 | break;
316 | }
317 | }
318 | }
319 |
320 | return instanceInfos;
321 | }
322 |
323 | private void waitForStatus(int linodeId, String status, int timeout) throws Exception {
324 | int elapsed = 0;
325 |
326 | while (true) {
327 | if (elapsed >= timeout) {
328 | throw new ProviderException(String.format("timed out waiting for status \"%s\"", status), null);
329 | }
330 |
331 | if (getInstanceStatus(linodeId).equalsIgnoreCase(status)) {
332 | break;
333 | }
334 |
335 | Thread.sleep(5000);
336 | elapsed += 5;
337 | }
338 |
339 | // Wait an extra 5 seconds otherwise the server may not be ready to accept
340 | // connections
341 | Thread.sleep(5000);
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/src/vpsproxy/providers/Provider.java:
--------------------------------------------------------------------------------
1 | package vpsproxy.providers;
2 |
3 | import java.io.InputStream;
4 | import java.io.InputStreamReader;
5 | import java.io.IOException;
6 | import java.util.Scanner;
7 | import javax.swing.JComponent;
8 | import java.nio.charset.StandardCharsets;
9 | import java.io.BufferedReader;
10 | import java.io.ByteArrayInputStream;
11 | import com.jcraft.jsch.ChannelExec;
12 | import com.jcraft.jsch.JSch;
13 | import com.jcraft.jsch.Session;
14 |
15 | import vpsproxy.Logger;
16 | import vpsproxy.ProxySettings;
17 |
18 | public abstract class Provider {
19 | private static final String proxyUsername = "burp-vps-proxy";
20 | private static final String proxyPort = "1080";
21 |
22 | private static final String SCRIPT_RESOURCE_PATH = "provisioning.sh";
23 | private static String SCRIPT;
24 | private static boolean debug = false;
25 |
26 | public abstract String getName();
27 |
28 | public abstract ProxySettings startInstance() throws ProviderException;
29 |
30 | public abstract void destroyInstance() throws ProviderException;
31 |
32 | public abstract JComponent getUI();
33 |
34 | protected void log(String message) {
35 | Logger.log(String.format("%s: %s", getName(), message));
36 | }
37 |
38 | protected void logf(String format, Object... args) {
39 | format = getName() + ": " + format;
40 | Logger.log(String.format(format, args));
41 | }
42 |
43 | protected ProxySettings createProxySettings(String publicIpAddress, String password) {
44 | return new ProxySettings(publicIpAddress, proxyPort, proxyUsername, password);
45 | }
46 |
47 | public class ProviderException extends Exception {
48 | public ProviderException(String message, Throwable cause) {
49 | super(getName() + ": " + message, cause);
50 | }
51 | }
52 |
53 | protected String getProvisioningScript(String password) throws IOException {
54 | if (SCRIPT == null) {
55 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream(SCRIPT_RESOURCE_PATH);
56 | if (inputStream != null) {
57 | try (Scanner scanner = new Scanner(inputStream, "UTF-8")) {
58 | SCRIPT = scanner.useDelimiter("\\A").next();
59 | }
60 | } else {
61 | throw new IOException(String.format("Resource '%s' not found", SCRIPT_RESOURCE_PATH));
62 | }
63 | }
64 |
65 | return SCRIPT.replaceAll("CHANGEME", password);
66 | }
67 |
68 | protected void runProvisioningScript(String ipAddress, String username, String password, String provisioningScript)
69 | throws Exception {
70 | log("provisioning via ssh");
71 |
72 | JSch jsch = new JSch();
73 |
74 | Session session = jsch.getSession(username, ipAddress, 22);
75 | session.setPassword(password);
76 | session.setConfig("StrictHostKeyChecking", "no");
77 | session.setConfig("ConnectTimeout", "60000");
78 | session.connect();
79 |
80 | ChannelExec channel = (ChannelExec) session.openChannel("exec");
81 | channel.setCommand("bash -s");
82 | channel.setInputStream(new ByteArrayInputStream(provisioningScript.getBytes(StandardCharsets.UTF_8)));
83 | channel.setErrStream(System.err);
84 |
85 | InputStream inputStream = channel.getInputStream();
86 | InputStream errorStream = channel.getErrStream();
87 |
88 | channel.connect();
89 |
90 | BufferedReader stdOutputReader = new BufferedReader(new InputStreamReader(inputStream));
91 | BufferedReader errOutputReader = new BufferedReader(new InputStreamReader(errorStream));
92 |
93 | String line;
94 | // log("Standard Output:");
95 | while ((line = stdOutputReader.readLine()) != null) {
96 | if (debug) {
97 | log(line);
98 | }
99 | }
100 |
101 | // log("Error Output:");
102 | while ((line = errOutputReader.readLine()) != null) {
103 | if (debug) {
104 | log(line);
105 | }
106 | }
107 |
108 | channel.disconnect();
109 | session.disconnect();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------