├── .gitignore ├── docs ├── diagram.png └── p3P2oJBJ5b.png ├── settings.gradle ├── .vscode └── tasks.json ├── README.MD └── src └── burp ├── ProjectSettings.java ├── DigitalOceanProxyTab.java └── BurpExtender.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .DS_Store 3 | build/ 4 | !build/libs/digitalocean-droplet-openvpn-all.jar -------------------------------------------------------------------------------- /docs/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honoki/burp-digitalocean-openvpn-socks/HEAD/docs/diagram.png -------------------------------------------------------------------------------- /docs/p3P2oJBJ5b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honoki/burp-digitalocean-openvpn-socks/HEAD/docs/p3P2oJBJ5b.png -------------------------------------------------------------------------------- /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/7.2/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'digitalocean-droplet-openvpn' -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "gradle", 8 | "type": "shell", 9 | "command": "gradle bigjar", 10 | // "command": "gradlew.bat bigjar", // Wrapper on Windows 11 | // "command": "gradlew bigjar", // Wrapper on *nix 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## DigitalOcean OpenVPN/SOCKS for Burp Suite 2 | 3 | This Burp extension allows you to spin up a DigitalOcean droplet based on an OpenVPN configuration file. The droplet also functions as a SOCKS5 proxy to allow routing all Burp traffic through the VPN tunnel. The Burp proxy settings are automatically configured to route traffic through the SOCKS5 and OpenVPN droplet. 4 | 5 | ![](docs/diagram.png) 6 | 7 | ### How to use 8 | 9 | 1. Download the JAR from `build/libs/digitalocean-droplet-openvpn-all.jar` or build from source yourself; 10 | 2. Load the extension in Burp via the Extensions tab; 11 | 3. Create a DigitalOcean API token and enter your token on the extension tab "OpenVPN/SOCKS"; 12 | 4. Select an OpenVPN configurataion file (.ovpn) 13 | 4. Click "Deploy" to start deploying the SOCKS and OpenVPN containers on a fresh droplet, and the extension will take care of the rest; 14 | 5. Allow up to a few minutes for the Docker image to complete installation before the proxy starts responding 15 | 16 | ![](docs/p3P2oJBJ5b.png) 17 | 18 | ### Features 19 | 20 | * Remember your DigitalOcean API token; 21 | * Remember your OpenVPN configuration file and credentials (optional) per project file; 22 | * Automatically shut down the droplet when Burp closes or the extension is unloaded; 23 | * A context menu so you can right-click > enable or disable tunnelling through the VPN 24 | * Opens a Repeater tab to `ifconfig.co` to easily verify if the VPN is working correctly 25 | 26 | ### Potential improvements 27 | 28 | * add an option to allow persisting the droplet when shutting down Burp to avoid the waiting time on startup - although this could result in unexpected DigitalOcean cost; 29 | * collect feedback from the running Docker containers to help troubleshoot when things go wrong 30 | 31 | Feel free to use the link below to set up your DigitalOcean account. If you use this referral link, you get $200 in credit, and I get $25 for every $25 you spend. 32 | 33 | [![DigitalOcean Referral Badge](https://web-platforms.sfo2.digitaloceanspaces.com/WWW/Badge%202.svg)](https://www.digitalocean.com/?refcode=08cd936d0d32&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) -------------------------------------------------------------------------------- /src/burp/ProjectSettings.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | import java.util.HashMap; 6 | 7 | public class ProjectSettings implements IHttpRequestResponse { 8 | 9 | private BurpExtender burp; 10 | private static final String SITEMAP_PREFIX = "BURP_PROJECT_SETTINGS_DO_NOT_DELETE"; 11 | private IHttpService httpService; 12 | private byte[] requestBytes; 13 | private HashMap settings; 14 | private byte[] serialized_settings; 15 | 16 | public ProjectSettings(BurpExtender burp, String extensionName) { 17 | this.burp = burp; 18 | this.httpService = burp.callbacks.getHelpers().buildHttpService(SITEMAP_PREFIX, 65535, true); 19 | IHttpRequestResponse[] settingsSiteMap = burp.callbacks.getSiteMap(this.httpService.toString()+"/"+extensionName); 20 | 21 | try { 22 | // need to overwrite the request bytes to force Burp to add it to the sitemap as a new item 23 | this.requestBytes = burp.callbacks.getHelpers().buildHttpRequest(new URL(httpService.getProtocol(), httpService.getHost(), httpService.getPort(), "/"+extensionName)); 24 | if(settingsSiteMap.length > 0) { 25 | this.serialized_settings = settingsSiteMap[0].getResponse(); 26 | } 27 | } catch (MalformedURLException e) { 28 | burp.stdout.println("ProjectSettings: Malformed URL"); 29 | } 30 | } 31 | 32 | @Override 33 | public byte[] getRequest() { 34 | return requestBytes; 35 | } 36 | 37 | @Override 38 | public void setRequest(byte[] message) { 39 | // TODO Auto-generated method stub 40 | 41 | } 42 | 43 | @Override 44 | public byte[] getResponse() { 45 | if(this.serialized_settings != null) { 46 | return this.serialized_settings; 47 | } 48 | 49 | return "".getBytes(); 50 | } 51 | 52 | @Override 53 | public void setResponse(byte[] message) { 54 | this.serialized_settings = message; 55 | } 56 | 57 | @Override 58 | public String getComment() { 59 | return null; 60 | } 61 | 62 | @Override 63 | public void setComment(String comment) {} 64 | 65 | @Override 66 | public String getHighlight() { 67 | return null; 68 | } 69 | 70 | @Override 71 | public void setHighlight(String color) {} 72 | 73 | @Override 74 | public IHttpService getHttpService() { 75 | return this.httpService; 76 | } 77 | 78 | @Override 79 | public void setHttpService(IHttpService httpService) { 80 | this.httpService = httpService; 81 | } 82 | 83 | /** 84 | * Store settings as a key-value map in a special project settings request in the site map 85 | * @param string 86 | * @param program_name 87 | */ 88 | public void saveProjectSetting(String key, String value) { 89 | if(this.settings == null) { 90 | this.settings = new HashMap(); 91 | } 92 | this.settings.put(key, value); 93 | 94 | // store values as KEY=VALUE;KEY2=VALUE2 95 | String serialized = ""; 96 | for(String s : this.settings.keySet()) { 97 | serialized += s+"="+this.settings.get(s)+";"; 98 | } 99 | this.setResponse(this.burp.callbacks.getHelpers().stringToBytes(serialized)); 100 | burp.callbacks.addToSiteMap(this); 101 | } 102 | 103 | public String loadProjectSetting(String key) { 104 | 105 | // init hashmap from serialized data in request 106 | if(this.settings == null) { 107 | 108 | this.settings = new HashMap(); 109 | 110 | String serialized = this.burp.callbacks.getHelpers().bytesToString(this.getResponse()); 111 | String[] parts = serialized.split(";"); 112 | for(String p: parts) { 113 | String[] pp = p.split("="); 114 | if(pp.length == 2) { 115 | this.settings.put(pp[0], pp[1]); 116 | } 117 | } 118 | } 119 | 120 | return this.settings.get(key); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/burp/DigitalOceanProxyTab.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.JPanel; 4 | import javax.swing.JLabel; 5 | import javax.swing.JPasswordField; 6 | import javax.swing.JTextField; 7 | import javax.swing.JFileChooser; 8 | import javax.swing.GroupLayout; 9 | import javax.swing.GroupLayout.Alignment; 10 | import javax.swing.LayoutStyle.ComponentPlacement; 11 | import java.io.File; 12 | 13 | import com.myjeeva.digitalocean.exception.DigitalOceanException; 14 | import com.myjeeva.digitalocean.exception.RequestUnsuccessfulException; 15 | 16 | import javax.swing.JButton; 17 | import java.awt.event.ActionListener; 18 | import java.awt.event.ActionEvent; 19 | import javax.swing.JTextPane; 20 | import javax.swing.filechooser.FileNameExtensionFilter; 21 | 22 | public class DigitalOceanProxyTab extends JPanel { 23 | private BurpExtender burp; 24 | private JPasswordField txtDigitalOceanApiKey; // api key 25 | private JTextField txtOvpnFileLocation; // ovpn file 26 | // ovpn username 27 | private JLabel lblOvpnUsername; 28 | private JTextField txtOvpnUsername; 29 | // ovpn password 30 | private JLabel lblOvpnPassword; 31 | private JPasswordField txtOvpnPassword; 32 | protected JTextPane textPane; 33 | // status of the proxy: 0 = not deployed, 1 = deployed and waiting for network, 2 = deployed and ready 34 | private int STATUS = 0; 35 | 36 | public DigitalOceanProxyTab(BurpExtender burp) { 37 | 38 | this.burp = burp; 39 | 40 | JLabel lblApiKey = new JLabel("DigitalOcean API key"); 41 | JButton btnDestroy = new JButton("Destroy"); 42 | this.textPane = new JTextPane(); 43 | 44 | JLabel lblOvpnFile = new JLabel("OpenVPN config file"); 45 | JLabel lblOvpnUsername = new JLabel("username"); 46 | JLabel lblOvpnPassword = new JLabel("password"); 47 | JFileChooser ovpnFileChooser = new JFileChooser(); 48 | JButton btnOvpnFileChooser = new JButton("Browse"); 49 | ovpnFileChooser.setFileFilter(new FileNameExtensionFilter("OpenVPN files", "ovpn")); 50 | ovpnFileChooser.setAcceptAllFileFilterUsed(false); 51 | ovpnFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 52 | ovpnFileChooser.setMultiSelectionEnabled(false); 53 | ovpnFileChooser.setControlButtonsAreShown(false); 54 | ovpnFileChooser.setApproveButtonText("Select"); 55 | ovpnFileChooser.setDialogTitle("Select OpenVPN config file"); 56 | // ... 57 | 58 | ovpnFileChooser.addActionListener(new ActionListener() { 59 | public void actionPerformed(ActionEvent e) { 60 | if(e.getActionCommand().equals(JFileChooser.APPROVE_SELECTION)) { 61 | File file = ovpnFileChooser.getSelectedFile(); 62 | txtOvpnFileLocation.setText(file.getAbsolutePath()); 63 | burp.setOvpnFileLocation(file.getAbsolutePath()); 64 | } 65 | } 66 | }); 67 | 68 | btnOvpnFileChooser.addActionListener(new ActionListener() { 69 | public void actionPerformed(ActionEvent e) { 70 | ovpnFileChooser.showOpenDialog(null); 71 | } 72 | }); 73 | 74 | 75 | JButton btnDeploy = new JButton("Deploy"); 76 | btnDeploy.addActionListener(new ActionListener() { 77 | public void actionPerformed(ActionEvent e) { 78 | burp.setApiKey(txtDigitalOceanApiKey.getText()); 79 | burp.setOvpnCredentials(txtOvpnUsername.getText(), txtOvpnPassword.getText()); 80 | 81 | try { 82 | // First get list of existing droplet in account that weren't deleted yet 83 | int nbExisting = burp.loadExistingProxyDroplets(); 84 | if(nbExisting > 0) { 85 | textPane.setText("WARNING: there are still "+nbExisting+" proxies deployed - they will be removed when hitting the Destroy button."); 86 | } 87 | 88 | 89 | btnDeploy.setEnabled(false); 90 | textPane.setText(textPane.getText() + "\nDeploying openvpn proxy to DigitalOcean..."); 91 | burp.deployNewDODroplet("burp-openvpn","nyc1","s-1vcpu-1gb"); 92 | textPane.setText(textPane.getText() + "\nProxy droplet is being deployed, waiting to come online..."); 93 | STATUS = 1; 94 | Thread thread = new Thread(() -> { 95 | // as long as status is "new", wait 60 seconds and check again 96 | try { 97 | while(burp.getDropletStatus().equals("new")) { 98 | textPane.setText(textPane.getText() + "\nProxy droplet is not ready yet, waiting 60 seconds..."); 99 | try { 100 | Thread.sleep(60000); 101 | } catch (InterruptedException e2) { 102 | e2.printStackTrace(); 103 | } 104 | } 105 | } catch (DigitalOceanException | RequestUnsuccessfulException e1) { 106 | e1.printStackTrace(); 107 | } 108 | finishedWaiting(); 109 | btnDestroy.setEnabled(true); 110 | }); 111 | thread.start(); 112 | } catch (DigitalOceanException | RequestUnsuccessfulException e1) { 113 | burp.stdout.println("Error deploying droplet: " + e1.getMessage()); 114 | e1.printStackTrace(); 115 | } 116 | } 117 | }); 118 | 119 | txtDigitalOceanApiKey = new JPasswordField(burp.api_key); 120 | txtDigitalOceanApiKey.setColumns(10); 121 | txtOvpnFileLocation = new JTextField(burp.ovpn_file); 122 | txtOvpnFileLocation.setColumns(10); 123 | txtOvpnFileLocation.setEnabled(false); 124 | txtOvpnUsername = new JTextField(burp.ovpn_username); 125 | txtOvpnUsername.setColumns(10); 126 | txtOvpnPassword = new JPasswordField(burp.ovpn_password); 127 | txtOvpnPassword.setColumns(10); 128 | 129 | btnDestroy.addActionListener(new ActionListener() { 130 | public void actionPerformed(ActionEvent e) { 131 | // you can't destroy what is never built 132 | if(STATUS == 0) return; 133 | try { 134 | btnDestroy.setEnabled(false); 135 | textPane.setText(textPane.getText() +"\nDestroying OpenVPN proxy..."); 136 | burp.destroyAllDroplets(); 137 | textPane.setText(textPane.getText() +"\nResetting Burp socks proxy config..."); 138 | burp.clearProxyConfiguration(); 139 | textPane.setText(textPane.getText() +"\nProxy destroyed."); 140 | STATUS = 0; 141 | btnDeploy.setEnabled(true); 142 | } catch (Exception e1) { 143 | burp.stdout.println("Error destroying proxy: " + e1.getMessage()); 144 | e1.printStackTrace(); 145 | } 146 | 147 | } 148 | }); 149 | 150 | GroupLayout groupLayout = new GroupLayout(this); 151 | groupLayout.setHorizontalGroup( 152 | groupLayout.createParallelGroup(Alignment.LEADING) 153 | .addGroup(groupLayout.createSequentialGroup() 154 | .addGap(45) 155 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 156 | .addComponent(textPane, GroupLayout.PREFERRED_SIZE, 615, GroupLayout.PREFERRED_SIZE) 157 | .addGroup(groupLayout.createSequentialGroup() 158 | .addComponent(lblApiKey) 159 | .addGap(43) 160 | .addComponent(txtDigitalOceanApiKey, GroupLayout.PREFERRED_SIZE, 318, GroupLayout.PREFERRED_SIZE) 161 | .addGap(3) 162 | .addComponent(btnDeploy) 163 | .addGap(3) 164 | .addComponent(btnDestroy)) 165 | .addGap(45) 166 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 167 | .addGroup(groupLayout.createSequentialGroup() 168 | .addComponent(lblOvpnFile) 169 | .addGap(43) 170 | .addComponent(txtOvpnFileLocation, GroupLayout.PREFERRED_SIZE, 318, GroupLayout.PREFERRED_SIZE) 171 | .addComponent(btnOvpnFileChooser) 172 | )) 173 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 174 | .addGroup(groupLayout.createSequentialGroup() 175 | .addComponent(lblOvpnUsername) 176 | .addGap(43) 177 | .addComponent(txtOvpnUsername, GroupLayout.PREFERRED_SIZE, 150, GroupLayout.PREFERRED_SIZE) 178 | )) 179 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 180 | .addGroup(groupLayout.createSequentialGroup() 181 | .addComponent(lblOvpnPassword) 182 | .addGap(43) 183 | .addComponent(txtOvpnPassword, GroupLayout.PREFERRED_SIZE, 150, GroupLayout.PREFERRED_SIZE) 184 | )) 185 | ) 186 | .addContainerGap(20, Short.MAX_VALUE)) 187 | ); 188 | groupLayout.setVerticalGroup( 189 | groupLayout.createParallelGroup(Alignment.LEADING) 190 | .addGroup(groupLayout.createSequentialGroup() 191 | .addGap(40) 192 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 193 | .addGroup(groupLayout.createSequentialGroup() 194 | .addGap(4) 195 | .addComponent(lblApiKey)) 196 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 197 | .addComponent(txtDigitalOceanApiKey, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) 198 | .addComponent(btnDeploy) 199 | .addComponent(btnDestroy))) 200 | .addGap(18) 201 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 202 | .addComponent(lblOvpnFile) 203 | .addComponent(txtOvpnFileLocation, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) 204 | .addComponent(btnOvpnFileChooser)) 205 | // ovpn username 206 | .addGap(18) 207 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 208 | .addComponent(lblOvpnUsername) 209 | .addComponent(txtOvpnUsername, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) 210 | // ovpn password 211 | .addGap(18) 212 | .addGroup(groupLayout.createParallelGroup(Alignment.LEADING) 213 | .addComponent(lblOvpnPassword) 214 | .addComponent(txtOvpnPassword, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) 215 | .addGap(18) 216 | .addPreferredGap(ComponentPlacement.UNRELATED) 217 | .addComponent(textPane, GroupLayout.DEFAULT_SIZE, 49, Short.MAX_VALUE) 218 | .addGap(38)) 219 | ); 220 | setLayout(groupLayout); 221 | 222 | } 223 | 224 | protected void finishedWaiting() { 225 | // don't execute this if the proxy is destroyed in the meantime 226 | if(STATUS == 0) 227 | return; 228 | 229 | textPane.setText(textPane.getText() + "\nProxy droplet is ready, configuring proxy..."); 230 | try { 231 | String ip = burp.getCurrentDropletIP(); 232 | textPane.setText(textPane.getText() + "\nProxy IP: " + ip); 233 | } catch (DigitalOceanException | RequestUnsuccessfulException e) { 234 | e.printStackTrace(); 235 | } 236 | burp.configureSocksProxy(); 237 | textPane.setText(textPane.getText() +"\nBurp SOCKS proxy settings configured."); 238 | textPane.setText(textPane.getText() +"\nProxy is ready to use. Allow some time for the Docker images to start."); 239 | textPane.setText(textPane.getText() +"\nOpening repeater tab..."); 240 | burp.openIfconfigRepeaterTab(); 241 | STATUS = 2; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/burp/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import javax.swing.JDialog; 9 | import javax.swing.JMenu; 10 | import javax.swing.JMenuItem; 11 | 12 | import java.awt.Component; 13 | import java.awt.event.ActionEvent; 14 | import java.awt.event.ActionListener; 15 | 16 | import com.myjeeva.digitalocean.*; 17 | import com.myjeeva.digitalocean.impl.DigitalOceanClient; 18 | import com.myjeeva.digitalocean.pojo.*; 19 | import com.myjeeva.digitalocean.exception.*; 20 | import java.nio.file.Files; 21 | import java.nio.file.Paths; 22 | import java.io.IOException; 23 | 24 | public class BurpExtender extends JDialog implements IBurpExtender, IExtensionStateListener, IContextMenuFactory, ITab { 25 | 26 | protected IBurpExtenderCallbacks callbacks; 27 | protected PrintWriter stdout; 28 | protected String api_key; 29 | protected String ovpn_file; // the file location of the last loaded OVPN file 30 | protected String ovpn_username; // the username to use for the OVPN file 31 | protected String ovpn_password; // the password to use for the OVPN file 32 | private String ip; 33 | private int proxyCount = 0; 34 | private DigitalOcean apiClient; 35 | protected ProjectSettings settings; 36 | 37 | // gui elements 38 | public DigitalOceanProxyTab myPanel; 39 | 40 | // keep a copy of our openvpn droplet 41 | Droplet droplet = new Droplet(); 42 | // the script to run on the droplet when it is created 43 | protected String droplet_init_script = "#!/bin/bash\n" + 44 | "mkdir -p /tmp/openvpn\n" + 45 | "cat < /tmp/openvpn/config.ovpn\n" + 46 | "PLACEHOLDER_OVPN\n" + 47 | "EOF\n" + 48 | "docker run -d --name openvpn -p 1080:1080 --cap-add=NET_ADMIN --device /dev/net/tun -v /tmp/openvpn:/vpn avpnusr/docker-openvpn PLACEHOLDER_AUTH\n" + 49 | "docker run -d --name socks5 --network container:openvpn -e PROXY_USER=burp -e PROXY_PASSWORD=changeme serjs/go-socks5-proxy\n"; 50 | // the socks password 51 | private CharSequence password; 52 | 53 | @Override 54 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { 55 | callbacks.setExtensionName("OpenVPN/SOCKS Proxy"); 56 | this.stdout = new PrintWriter(callbacks.getStdout(), true); 57 | this.callbacks = callbacks; 58 | 59 | // unload resources when this extension is removed; 60 | stdout.println("Registering extension state listener."); 61 | callbacks.registerExtensionStateListener(this); 62 | 63 | // load extension-specific settings 64 | stdout.println("Loading existing settings."); 65 | this.api_key = callbacks.loadExtensionSetting("digitalocean-api-key"); 66 | // this.ovpn_file = callbacks.loadExtensionSetting("ovpn-file-location"); 67 | 68 | // load project-specific settings 69 | this.settings = new ProjectSettings(this, "digitalocean-openvpn-socks"); 70 | this.ovpn_file = settings.loadProjectSetting("ovpn-file-location"); 71 | this.ovpn_username = settings.loadProjectSetting("ovpn-username"); 72 | this.ovpn_password = settings.loadProjectSetting("ovpn-password"); 73 | 74 | // create the tab 75 | stdout.println("Creating DigitalOcean OVPN Proxy tab."); 76 | myPanel = new DigitalOceanProxyTab(this); 77 | callbacks.addSuiteTab(this); 78 | 79 | // register the right-click menu: 80 | callbacks.registerContextMenuFactory(this); 81 | 82 | stdout.println("OpenVPN/SOCKS extension initialized."); 83 | 84 | } 85 | 86 | // use the DigitalOcean API to create a new droplet 87 | protected void deployNewDODroplet(String droplet_name, String region, String size) throws DigitalOceanException, RequestUnsuccessfulException { 88 | proxyCount++; 89 | apiClient = new DigitalOceanClient(this.api_key); 90 | Droplet newDroplet = new Droplet(); 91 | newDroplet.setName(droplet_name); 92 | newDroplet.setSize(size); 93 | newDroplet.setRegion(new Region(region)); 94 | newDroplet.setImage(new Image("docker-20-04")); // use docker so we can run the necessary containers 95 | newDroplet.setTags(Arrays.asList("burp-openvpn")); // set a tag so they get removed when hitting "destroy" 96 | 97 | // add your public ssh key to the droplet 98 | //List keys = new ArrayList(); 99 | //keys.add(new Key(123)); 100 | //newDroplet.setKeys(keys); 101 | 102 | // generate a new password if we don't have one yet (first droplet) 103 | 104 | if(this.password == null) { 105 | this.password = randomPassword(16); 106 | stdout.println("Generated random password for socks proxy: " + this.password); 107 | } 108 | 109 | try { 110 | // set the init script to run on the droplet 111 | // copy the ovpn configuration to the droplet and start the openvpn and socks5 containers 112 | String ovpn = new String(Files.readAllBytes(Paths.get(this.ovpn_file))); 113 | String auth = ""; 114 | if(this.ovpn_username != null && !this.ovpn_username.isEmpty()) { 115 | // escape all necessary characters for bash 116 | // i.e. encapsulated single quotes and escape backslashes and dollar signs 117 | auth = "-a '"+ this.ovpn_username + ";"+this.ovpn_password.replaceAll("'","'\"'\"'").replaceAll("([\\$])", "\\\\$1")+"'"; 118 | } 119 | newDroplet.setUserData(droplet_init_script.replace("changeme", this.password) 120 | .replace("PLACEHOLDER_OVPN", ovpn) 121 | .replace("PLACEHOLDER_AUTH", auth)); 122 | // create a new droplet 123 | stdout.println("Creating new droplet: " + newDroplet.getName()); 124 | } catch (IOException e) { 125 | e.printStackTrace(); 126 | stdout.println("Error reading ovpn file: " + e.getMessage()); 127 | } 128 | this.droplet = apiClient.createDroplet(newDroplet); 129 | } 130 | 131 | // get a list of droplets named burp-openvpn* that already exist on the account 132 | // note that these cannot be used because the proxy password is randomized; 133 | // return the number of existing proxy droplets found. 134 | protected int loadExistingProxyDroplets() { 135 | apiClient = new DigitalOceanClient(this.api_key); 136 | Droplets existing_droplets; 137 | try { 138 | existing_droplets = apiClient.getAvailableDropletsByTagName("burp-openvpn", 1, 100); 139 | return existing_droplets.getDroplets().size(); 140 | } catch (DigitalOceanException | RequestUnsuccessfulException e) { 141 | // TODO Auto-generated catch block 142 | e.printStackTrace(); 143 | } 144 | return -1; 145 | } 146 | 147 | // generate a random password for the socks proxy 148 | private CharSequence randomPassword(int i) { 149 | String AlphaNumericString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "abcdefghijklmnopqrstuvxyz"; 150 | StringBuilder sb = new StringBuilder(i); 151 | for (int j = 0; j < i; j++) { 152 | int index = (int) (AlphaNumericString.length() * Math.random()); 153 | sb.append(AlphaNumericString.charAt(index)); 154 | } 155 | return sb; 156 | } 157 | 158 | 159 | // destroy one droplet by its id 160 | protected void destroyDODroplet(int droplet_id) throws DigitalOceanException, RequestUnsuccessfulException { 161 | DigitalOcean apiClient = new DigitalOceanClient(this.api_key); 162 | stdout.println("Destroying droplets"); 163 | apiClient.deleteDroplet(droplet_id); 164 | // reset the IP so it gets refreshed for next droplet 165 | this.ip = null; 166 | } 167 | 168 | // destroy all droplets 169 | protected void destroyAllDroplets() throws DigitalOceanException, RequestUnsuccessfulException { 170 | DigitalOcean apiClient = new DigitalOceanClient(this.api_key); 171 | apiClient.deleteDropletByTagName("burp-openvpn"); 172 | stdout.println("Destroying droplet: " + this.droplet.getName()); 173 | apiClient.deleteDroplet(this.droplet.getId()); 174 | // reset the IP so it gets refreshed for next droplet 175 | this.ip = null; 176 | } 177 | 178 | @Override 179 | public void extensionUnloaded() { 180 | stdout.println("Destroying all droplets..."); 181 | try { 182 | this.destroyAllDroplets(); 183 | } catch(Exception e) { 184 | stdout.println("ERROR - Failed to destroy droplets"); 185 | stdout.println(e.getMessage()); 186 | } 187 | 188 | } 189 | 190 | @Override 191 | public List createMenuItems(IContextMenuInvocation invocation) { 192 | ArrayList menu = new ArrayList(); 193 | 194 | JMenuItem enableProxy = new JMenuItem("Tunnel through OpenVPN proxy"); 195 | JMenuItem disableProxy = new JMenuItem("Stop tunnelling through OpenVPN proxy"); 196 | 197 | IHttpRequestResponse[] selected = invocation.getSelectedMessages(); 198 | 199 | enableProxy.addActionListener(new ActionListener() { 200 | @Override 201 | public void actionPerformed(ActionEvent e) { 202 | configureSocksProxy(); 203 | } 204 | }); 205 | 206 | disableProxy.addActionListener(new ActionListener() { 207 | @Override 208 | public void actionPerformed(ActionEvent e) { 209 | clearProxyConfiguration(); 210 | } 211 | }); 212 | 213 | menu.add(enableProxy); 214 | menu.add(disableProxy); 215 | 216 | return menu; 217 | } 218 | 219 | @Override 220 | public String getTabCaption() { 221 | return "OpenVPN/SOCKS"; 222 | } 223 | 224 | @Override 225 | public Component getUiComponent() { 226 | return myPanel; 227 | } 228 | 229 | protected void configureSocksProxy() { 230 | String ip = ""; 231 | try { 232 | ip = this.getCurrentDropletIP(); 233 | } catch (DigitalOceanException | RequestUnsuccessfulException e) { 234 | stdout.println("ERROR - Failed to get droplet IP address."); 235 | e.printStackTrace(); 236 | } 237 | myPanel.textPane.setText(myPanel.textPane.getText() + "\nChanging Burp SOCKS proxy settings..."); 238 | callbacks.loadConfigFromJson("{\"project_options\":{\"connections\":{\"socks_proxy\":{\"dns_over_socks\":false,\"host\":\"ip_address\",\"password\":\"changeme\",\"port\":1080,\"use_proxy\":true,\"use_user_options\":false,\"username\":\"burp\"}}}}" 239 | .replace("ip_address",ip) 240 | .replace("changeme",this.password)); 241 | } 242 | 243 | public void clearProxyConfiguration() { 244 | callbacks.loadConfigFromJson("{\"project_options\":{\"connections\":{\"socks_proxy\":{\"dns_over_socks\":false,\"host\":\"0.0.0.0\",\"password\":\"changeme\",\"port\":1080,\"use_proxy\":false,\"use_user_options\":false,\"username\":\"burp\"}}}}"); 245 | } 246 | 247 | public void setApiKey(String api_key) { 248 | this.api_key = api_key; 249 | callbacks.saveExtensionSetting("digitalocean-api-key", api_key); 250 | } 251 | 252 | public void setOvpnFileLocation(String ovpn_file) { 253 | this.ovpn_file = ovpn_file; 254 | settings.saveProjectSetting("ovpn-file-location", ovpn_file); 255 | } 256 | 257 | public void setOvpnCredentials(String ovpn_username, String ovpn_password) { 258 | if(ovpn_username == null) { 259 | ovpn_username = ""; 260 | } 261 | if(ovpn_password == null) { 262 | ovpn_password = ""; 263 | } 264 | this.ovpn_username = ovpn_username; 265 | this.ovpn_password = ovpn_password; 266 | settings.saveProjectSetting("ovpn-username", ovpn_username); 267 | settings.saveProjectSetting("ovpn-password", ovpn_password); 268 | } 269 | 270 | public void refreshDroplet() throws DigitalOceanException, RequestUnsuccessfulException { 271 | stdout.println("Refreshing droplet information..."); 272 | this.droplet = apiClient.getDropletInfo(this.droplet.getId()); 273 | } 274 | 275 | public String getCurrentDropletIP() throws DigitalOceanException, RequestUnsuccessfulException { 276 | //if(this.ip != null && !this.ip.isEmpty()) { 277 | // return this.ip; 278 | //} 279 | this.refreshDroplet(); 280 | stdout.println("Getting droplet IP address: " + this.droplet.getName()); 281 | this.ip = this.droplet.getNetworks().getVersion4Networks().get(0).getIpAddress(); 282 | return this.ip; 283 | } 284 | 285 | public String getDropletStatus() throws DigitalOceanException, RequestUnsuccessfulException { 286 | this.refreshDroplet(); 287 | return this.droplet.getStatus().toString(); 288 | } 289 | 290 | protected void openIfconfigRepeaterTab() { 291 | stdout.println("Opening ifconfig.co repeater tab..."); 292 | String request = "GET / HTTP/1.1\r\nHost: ifconfig.co\r\nUser-Agent: curl\r\n\r\n"; 293 | callbacks.sendToRepeater("ifconfig.co", 443, true, callbacks.getHelpers().stringToBytes(request), "IP check"); 294 | } 295 | 296 | } --------------------------------------------------------------------------------