├── .github
└── workflows
│ └── releases.yml
├── .gitignore
├── .gitlab-ci.yml
├── LICENSE
├── README.md
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── images
└── ContextList.png
├── settings.gradle
└── src
├── main
├── java
│ ├── burp
│ │ └── BurpExtender.java
│ └── com
│ │ └── nccgroup
│ │ └── collaboratorplusplus
│ │ ├── extension
│ │ ├── BurpTabController.java
│ │ ├── CollaboratorEventAdapter.java
│ │ ├── CollaboratorEventListener.java
│ │ ├── CollaboratorPlusPlus.java
│ │ ├── CollaboratorPreferenceFactory.java
│ │ ├── CollaboratorServerResponse.java
│ │ ├── DNSQueryType.java
│ │ ├── Globals.java
│ │ ├── IProxyServiceListener.java
│ │ ├── JTextAreaAppender.java
│ │ ├── ProxyService.java
│ │ ├── ProxyServiceAdapter.java
│ │ ├── Utilities.java
│ │ ├── context
│ │ │ ├── CollaboratorContext.java
│ │ │ ├── CollaboratorContextSerializer.java
│ │ │ ├── CollaboratorServer.java
│ │ │ ├── CollaboratorServerSerializer.java
│ │ │ ├── ContextManager.java
│ │ │ ├── DNSInteraction.java
│ │ │ ├── HTTPInteraction.java
│ │ │ ├── Interaction.java
│ │ │ ├── InteractionSerializer.java
│ │ │ └── SMTPInteraction.java
│ │ ├── exception
│ │ │ ├── AuthenticationException.java
│ │ │ ├── CollaboratorPollingException.java
│ │ │ ├── CollaboratorSSLException.java
│ │ │ ├── InvalidResponseException.java
│ │ │ └── InvalidSecretException.java
│ │ ├── interactionhistory
│ │ │ ├── ContextInformationPanel.java
│ │ │ ├── ContextTable.java
│ │ │ ├── HistoryUI.java
│ │ │ ├── InteractionInfoPanel.java
│ │ │ └── InteractionsTable.java
│ │ └── ui
│ │ │ ├── AboutUI.java
│ │ │ ├── ConfigUI.java
│ │ │ └── ExtensionUI.java
│ │ ├── server
│ │ ├── CollaboratorServer.java
│ │ ├── HttpHandler.java
│ │ └── Utilities.java
│ │ └── utilities
│ │ ├── Encryption.java
│ │ ├── LevelSerializer.java
│ │ ├── NoTextSelectionCaret.java
│ │ ├── SelectableLabel.java
│ │ ├── StaticHTTPMessageController.java
│ │ └── WrappedTextPane.java
└── resources
│ ├── Explain.png
│ ├── GitHubLogoBlack.png
│ ├── GitHubLogoWhite.png
│ ├── NCCGroup.png
│ ├── NCCLarge.png
│ └── TwitterLogo.png
└── test
└── java
└── TestPlugin.java
/.github/workflows/releases.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: releases
3 |
4 | on:
5 | push:
6 | tags:
7 | - "v*"
8 |
9 | jobs:
10 | release:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout source code
14 | uses: actions/checkout@v2.3.4
15 | with:
16 | lfs: true
17 | fetch-depth: 0
18 |
19 | - name: Set up Java
20 | uses: actions/setup-java@v2
21 | with:
22 | distribution: adopt
23 | java-version: '8'
24 |
25 | # burpsuite_pro.jar is not available, disable tests
26 | - name: Build
27 | run: ./gradlew build -x test
28 |
29 | - uses: "marvinpinto/action-automatic-releases@latest"
30 | with:
31 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
32 | prerelease: false
33 | files: |
34 | releases/*.jar
35 | id: "automatic_releases"
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | .idea/
3 | build/
4 | out/
5 | keys/
6 | *.burp
7 | CollaboratorServer.properties
8 | releases/
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | variables:
4 | JDK_VERSION: '8u322-b06'
5 | JDK_HASH: '3d62362a78c9412766471b05253507a4cfc212daea5cdf122860173ce902400e'
6 |
7 | stages:
8 | - build
9 |
10 | build:
11 | only:
12 | - tags
13 | stage: build
14 | image: debian:bullseye
15 | script: |
16 | set -eux
17 | apt-get update
18 | apt-get -y upgrade
19 | apt-get -y install git wget
20 | v=$(echo "${JDK_VERSION}" | tr -d '-')
21 | wget "https://github.com/adoptium/temurin8-binaries/releases/download/jdk${JDK_VERSION}/OpenJDK8U-jdk_x64_linux_hotspot_${v}.tar.gz"
22 | [ "X${JDK_HASH}" = X$(sha256sum OpenJDK* | awk '{print $1;}') ]
23 | gzip -cd Open* | tar -f- -x
24 | PATH="$(pwd)/jdk${JDK_VERSION}/bin:${PATH}"
25 | export PATH
26 |
27 | # https://github.com/gradle/gradle/issues/3117
28 | env -i PATH="${PATH}" ./gradlew build -x test
29 | mv ./releases/* ..
30 | cd -
31 | rm -rf ./jdk* ./Open* ./CollaboratorPlusPlus
32 | artifacts:
33 | untracked: true
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## CollaboratorPlusPlus
2 | Released as open source by NCC Group Plc - http://www.nccgroup.com/
3 |
4 | Developed by Corey Arthur, corey.arthur@nccgroup.com
5 |
6 | http://www.github.com/nccgroup/CollaboratorPlusPlus
7 |
8 | This project is released under AGPL see LICENSE for more information
9 |
10 |
11 | Download from releases [here](https://github.com/nccgroup/CollaboratorPlusPlus/releases).
12 |
13 | ##### Background
14 | This tool aims to extend upon the existing Collaborator functionality provided by Burp Suite, providing a
15 | number of quality of life features, and the implementation of an authentication mechanism to secure private collaborator
16 | deployments, while still being compatible with all existing extensions which generate and poll Collaborator contexts.
17 |
18 | CollaboratorPlusPlus acts as a proxy between Burp and the configured Collaborator server, allowing the capture
19 | of Collaborator contexts being used by the client. CollaboratorPlusPlus can then store and display the observed contexts
20 | and their retrieved interactions in a central interface. In addition, old contexts can be polled manually, allowing
21 | interactions to be retrieved even after the Collaborator client window has been closed.
22 |
23 | 
24 |
25 | ##### Authentication Mechanism
26 |
27 | In addition to the Burp Extension, the Collaborator++ project also includes an optional server-side authentication component
28 | to authenticating incoming polling requests before passing them to the Collaborator server. This can be deployed by owners of private Burp Collaborator servers to restrict polling to only those with knowledge of the shared secret.
29 |
30 | When Burp requests the list of interactions received by the Collaborator server, the extension
31 | encrypts the polling requests with the AES256-CBC encryption scheme, using the shared secret
32 | to generate the encryption key. Provided the shared secret is correct, the authentication
33 | server is able to decrypt the request and forward it to the Collaborator server
34 | to retrieve the interactions for the given Collaborator instance. The response is then
35 | encrypted with the shared-secret before being sent back to the Burp client.
36 |
37 | By using the shared-secret to encrypt the transmission between the Burp client and the authentication server,
38 | the shared-secret does not need to be transmitted along with the request, allowing confidentiality to be
39 | maintained even in cases where HTTP communication must be used between the client and server.
40 |
41 |
42 | ## Collaborator++ - Client
43 |
44 | ##### Running the Client
45 | 1. Add the extension to Burp
46 | - Note: This is the same JAR as the server.
47 | 2. Specify the address and the port the Collaborator Server is listening on within the extension config.
48 | 3. Optional: If you are using to a Collaborator Auth server, specify the shared secret and enable authentication in the extension config.
49 | 4. Start the local server, this will also configure the collaborator settings within Burp for you.
50 | 5. Optional: Run Burp's Collaborator health check to make sure everything is working.
51 |
52 |
53 | ##### Additional Settings
54 | A few additional settings have been added to Collaborator Auth for convenience.
55 |
56 | **Use SSL:** Toggle the use of SSL between the client and server.
57 | *Be sure your server is configured to use SSL on the target port too.*
58 |
59 | **Ignore Certificate Errors:** Disables certificate validity checks.
60 | *Allows usage of self-signed / expired certificates.*
61 |
62 | **Enable SSL Hostname Verification:** Do not check the certificate hostname matches the target domain.
63 |
64 | **Block Public Collaborator Server:** Prevents accidental usage of the public Burp collaborator server.
65 | Adds a DNS entry for *"burpcollaborator.net"* to *127.0.0.1* in Burp's hostname resolution config.
66 |
67 |
68 |
69 |
70 | ## Collaborator Auth - Server
71 |
72 | ##### Running the Server
73 | 1. Execute `java -jar CollaboratorPlusPlus.jar` to generate the default configuration.
74 | - Note: This is the same JAR as the client.
75 | 2. Edit the generated file to point to your private collaborator instance and choose a suitable secret.
76 | 3. Run the server again and specify the configuration to be used `java -jar CollaboratorPlusPlus.jar YOURCONFIGFILE.properties`
77 |
78 | *Note: To allow HTTP and HTTPS requests to the Collaborator++ Auth server, create two copies of the configuration file,
79 | configuring one for HTTP and one for HTTPS and run two instances of the Collaborator++ Auth server.*
80 |
81 | ##### SSL Configuration
82 |
83 | To enable the usage of SSL, generate a certificate for the server and use one of the below methods to configure the server.
84 |
85 | For both methods, ensure `enable_ssl` is `true` in the config file.
86 |
87 | ##### Recommended: Simple Configuration
88 |
89 | 1. Generate a new private key and certificate to use for the server using the following command, or use an existing one.
90 | - `openssl req -newkey rsa:2048 -nodes -keyout privatekey.pem -x509 -days 365 -out certificate.pem`
91 | 1. Optional: Get the certificate signed by a trusted CA.
92 | 1. Edit the config file:
93 | - Set `ssl_private_key_path` to the path of your private key.
94 | - Set `ssl_certificate_path` to the path of your certificate.
95 | - If your certificate chain requires an intermediate certificate:
96 | - Set `ssl_intermediate_certificate_path` to the path of your intermediate certificate.
97 |
98 | ##### Alternative: Java Keystore
99 |
100 | This method was added purely for compatability reasons. I highly recommend using the simple configuration unless
101 | there is a reason otherwise.
102 |
103 | 1. Edit the config file and set the serializedValue for `ssl_private_key_path` to an empty string `""`.
104 | - This will enable the use of the keystore for configuration.
105 | 1. Generate a new private key and certificate to use for the server using the following command.
106 | - `openssl req -newkey rsa:2048 -nodes -keyout privatekey.pem -x509 -days 365 -out certificate.pem`
107 | 1. Convert the key and certificate to PKCS12.
108 | - `openssl pkcs12 -export -in certificate.pem -inkey privatekey.pem -out polling.p12 -name polling`
109 | 1. Enter a password to be used for encrypting the PKCS12 file. **This will be used in the next step!**
110 | 1. Import the PKCS12 file into a new Java Keystore.
111 | - `keytool -importkeystore -deststorepass NEW_PASSWORD_FOR_KEYSTORE -destkeypass NEW_PASSWORD_FOR_PRIVATE_KEY \ `
112 |
`-destkeystore polling.jks -srckeystore polling.p12 -srcstoretype PKCS12 \ `
113 |
`-srcstorepass PASS_FROM_PREVIOUS_STEP -alias polling`
114 | 1. Edit the configuration file to enable ssl, point the server to the keystore and specify the passwords used.
115 | 1. Run the server again and specify the configuration to be used `java -jar CollaboratorAuth-SERVER.jar CollaboratorServer.properties`
116 |
117 |
118 |
119 |
120 | #### Recommended: Secure the *actual* Collaborator Server
121 |
122 | To prevent polling of the Collaborator Server without the usage of Collaborator Auth,
123 | the Burp Collaborators polling location must be restricted.
124 |
125 | This may be done using your firewall, or by modifying the listening interface for polling events.
126 |
127 | **Option 1 - Always require usage of Collaborator Auth.**
128 |
129 | Should you wish to force users of your Burp Collaborator instance to authenticate regardless of their network,
130 | Burp Collaborator can be configured to listen to polling events only on the local machine (i.e. from Collaborator Auth).
131 |
132 | This can be done by changing Burp Collaborator's listening address for polling events to the loopback interface (127.0.0.1) or
133 | using something like iptables to drop incoming requests.
134 |
135 | **Option 2 - Only require usage of Collaborator Auth on external networks.**
136 |
137 | To allow Burp Collaborator to be used as normal when on the same network as the server, but require Collaborator Auth
138 | to be used when on an external network, Burp Collaborator can be configured to listen to polling events from
139 | internal addresses.
140 |
141 | This can be done by changing Burp Collaborator's listening address to polling events to the server's internal address
142 | (192.168.x.x, 10.x.x.x, etc.).
143 |
144 | To ensure external polling events are not processed by Burp Collaborator, the polling port should be blocked on the
145 | internet facing firewall. Alternatively, use iptables to drop incoming traffic from external networks.
146 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | }
4 |
5 | group 'nccgroup'
6 | version '0.3.0-Beta'
7 | sourceCompatibility = 1.8
8 |
9 |
10 | repositories {
11 | mavenCentral()
12 | maven {
13 | url "https://repo.spring.io/plugins-release/"
14 | }
15 | maven {
16 | url "https://jitpack.io"
17 | }
18 | }
19 |
20 | configurations {
21 | includeUnexplodedLib
22 | compile.extendsFrom includeUnexplodedLib
23 | }
24 |
25 | dependencies {
26 | compile 'com.github.CoreyD97:BurpExtenderUtilities:8b5b654ac4d07fb6fcc8f0ae3a9c34ce553d7ea6'
27 | compile 'net.portswigger.burp.extender:burp-extender-api:1.7.22'
28 | compile 'com.google.code.gson:gson:2.8.6'
29 | compile 'org.apache.logging.log4j:log4j-core:2.17.1'
30 | compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.10'
31 | compile group: 'commons-io', name: 'commons-io', version: '2.4'
32 | compile group: 'nu.studer', name: 'java-ordered-properties', version: '1.0.4'
33 | compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.62'
34 | compile group: 'org.swinglabs', name: 'swingx', version: '1.6.1'
35 | testCompile files('/opt/BurpSuitePro/burpsuite_pro.jar')
36 | }
37 |
38 | tasks.withType(Jar){
39 | destinationDir = file("$rootDir/releases")
40 | }
41 |
42 | jar{
43 | manifest {
44 | attributes(
45 | "Main-Class": "com.nccgroup.collaboratorplusplus.server.CollaboratorServer"
46 | )
47 | }
48 | from {
49 | (configurations.compile).collect { it.isDirectory() ? it : zipTree(it) }
50 | }{
51 | exclude "META-INF/*.SF"
52 | exclude "META-INF/*.DSA"
53 | exclude "META-INF/*.RSA"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Feb 18 12:31:08 GMT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS='"-Xmx64m"'
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS="-Xmx64m"
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/images/ContextList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/images/ContextList.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'CollaboratorPlusPlus'
2 |
3 | //if(file('../BurpExtenderUtilities').exists()) {
4 | // includeBuild('../BurpExtenderUtilities') {
5 | // dependencySubstitution {
6 | // substitute module('com.github.CoreyD97:BurpExtenderUtilities') with project(':')
7 | // }
8 | // }
9 | //}
10 |
--------------------------------------------------------------------------------
/src/main/java/burp/BurpExtender.java:
--------------------------------------------------------------------------------
1 | package burp;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
4 |
5 | public class BurpExtender extends CollaboratorPlusPlus {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/BurpTabController.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import javax.swing.*;
4 | import java.awt.*;
5 | import java.awt.event.ActionEvent;
6 | import java.awt.event.ActionListener;
7 | import java.awt.event.ContainerEvent;
8 | import java.awt.event.ContainerListener;
9 | import java.beans.PropertyChangeEvent;
10 | import java.beans.PropertyChangeListener;
11 | import java.util.function.Function;
12 |
13 | public class BurpTabController implements ContainerListener {
14 |
15 | JTabbedPane watchedComponent;
16 | Component targetComponent;
17 | Function onAdd, onRemove;
18 |
19 | int tabIndex;
20 | Color currentColor;
21 |
22 |
23 | public BurpTabController(JTabbedPane watched, Component targetComponent, Function onAdd, Function onRemove){
24 | this.watchedComponent = watched;
25 | this.targetComponent = targetComponent;
26 | this.onAdd = onAdd;
27 | this.onRemove = onRemove;
28 | this.watchedComponent.addContainerListener(this);
29 | tabIndex = this.watchedComponent.indexOfComponent(targetComponent);
30 |
31 | watchedComponent.addPropertyChangeListener(new PropertyChangeListener() {
32 | @Override
33 | public void propertyChange(PropertyChangeEvent evt) {
34 | if(evt.getPropertyName().equalsIgnoreCase("indexForTabComponent")){
35 | if(evt.getNewValue().equals(tabIndex)){
36 | setTabColor(currentColor);
37 | }
38 | }
39 | }
40 | });
41 | }
42 |
43 | @Override
44 | public void componentAdded(ContainerEvent containerEvent) {
45 | if(containerEvent.getChild() == targetComponent){
46 | tabIndex = watchedComponent.indexOfComponent(targetComponent);
47 | setTabColor(this.currentColor);
48 | }
49 | }
50 |
51 | @Override
52 | public void componentRemoved(ContainerEvent containerEvent) {
53 | if(containerEvent.getChild() == targetComponent){
54 | tabIndex = -1;
55 | }
56 | }
57 |
58 | public void setTabColor(Color color){
59 | this.currentColor = color;
60 | JTextField titleComponent = getTabTextComponent();
61 | if(titleComponent != null){
62 | SwingUtilities.invokeLater(() -> {
63 | titleComponent.setDisabledTextColor(color);
64 | titleComponent.repaint();
65 | });
66 | }
67 | }
68 |
69 | public void flashTabColor(Color color){
70 |
71 | }
72 |
73 | private JTextField getTabTextComponent(){
74 | if(tabIndex != -1){
75 | JComponent tabComponent = (JComponent) this.watchedComponent.getTabComponentAt(tabIndex);
76 | if(tabComponent != null) {
77 | return (JTextField) tabComponent.getComponent(0);
78 | }
79 | }
80 | return null;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/CollaboratorEventAdapter.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
4 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorServer;
5 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
6 |
7 | import java.util.ArrayList;
8 |
9 | public abstract class CollaboratorEventAdapter implements CollaboratorEventListener {
10 |
11 | @Override
12 | public void onCollaboratorServerRegistered(CollaboratorServer collaboratorServer, int index) {}
13 |
14 | @Override
15 | public void onCollaboratorServerRemoved(CollaboratorServer collaboratorServer, int index) {}
16 |
17 | @Override
18 | public void onCollaboratorContextRegistered(CollaboratorContext collaboratorContext, int index) {}
19 |
20 | @Override
21 | public void onCollaboratorContextRemoved(CollaboratorContext collaboratorContext, int index) {}
22 |
23 | @Override
24 | public void onPollingRequestSent(CollaboratorContext collaboratorContext) {}
25 |
26 | @Override
27 | public void onPollingResponseReceived(CollaboratorContext collaboratorContext, ArrayList interactions) {}
28 |
29 | @Override
30 | public void onPollingFailure(CollaboratorContext collaboratorContext, String error) {}
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/CollaboratorEventListener.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
4 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorServer;
5 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
6 |
7 | import java.util.ArrayList;
8 |
9 | public interface CollaboratorEventListener {
10 | void onCollaboratorServerRegistered(CollaboratorServer collaboratorServer, int index);
11 | void onCollaboratorServerRemoved(CollaboratorServer collaboratorServer, int index);
12 | void onCollaboratorContextRegistered(CollaboratorContext collaboratorContext, int index);
13 | void onCollaboratorContextRemoved(CollaboratorContext collaboratorContext, int index);
14 | void onPollingRequestSent(CollaboratorContext collaboratorContext);
15 | void onPollingResponseReceived(CollaboratorContext collaboratorContext, ArrayList interactions);
16 | void onPollingFailure(CollaboratorContext collaboratorContext, String error);
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/CollaboratorPlusPlus.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import burp.IBurpExtender;
4 | import burp.IBurpExtenderCallbacks;
5 | import burp.IExtensionStateListener;
6 | import com.coreyd97.BurpExtenderUtilities.DefaultGsonProvider;
7 | import com.coreyd97.BurpExtenderUtilities.Preferences;
8 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
9 | import com.nccgroup.collaboratorplusplus.extension.context.ContextManager;
10 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
11 | import com.nccgroup.collaboratorplusplus.extension.ui.ExtensionUI;
12 | import org.apache.http.HttpHost;
13 | import org.apache.http.impl.bootstrap.HttpServer;
14 | import org.apache.logging.log4j.Level;
15 | import org.apache.logging.log4j.LogManager;
16 | import org.apache.logging.log4j.Logger;
17 | import org.apache.logging.log4j.core.LoggerContext;
18 | import org.apache.logging.log4j.core.config.AppenderRef;
19 | import org.apache.logging.log4j.core.config.Configuration;
20 | import org.apache.logging.log4j.core.config.Configurator;
21 | import org.apache.logging.log4j.core.config.LoggerConfig;
22 | import org.apache.logging.log4j.core.layout.PatternLayout;
23 |
24 | import javax.swing.*;
25 | import java.awt.*;
26 | import java.awt.event.WindowEvent;
27 | import java.net.URI;
28 | import java.net.URISyntaxException;
29 | import java.util.ArrayList;
30 |
31 | import static com.nccgroup.collaboratorplusplus.extension.Globals.*;
32 |
33 | public class CollaboratorPlusPlus implements IBurpExtender, IExtensionStateListener {
34 |
35 | //Vars
36 | public static IBurpExtenderCallbacks callbacks;
37 | public static Logger logger;
38 | private ProxyService proxyService;
39 | private ContextManager contextManager;
40 | private Preferences preferences;
41 | private ArrayList proxyServiceListeners;
42 |
43 | private ExtensionUI ui;
44 | private BurpTabController burpTabController;
45 |
46 | public CollaboratorPlusPlus(){
47 | //Fix Darcula's issue with JSpinner UI.
48 | try {
49 | Class spinnerUI = Class.forName("com.bulenkov.darcula.ui.DarculaSpinnerUI");
50 | UIManager.put("com.bulenkov.darcula.ui.DarculaSpinnerUI", spinnerUI);
51 | Class sliderUI = Class.forName("com.bulenkov.darcula.ui.DarculaSliderUI");
52 | UIManager.put("com.bulenkov.darcula.ui.DarculaSliderUI", sliderUI);
53 | } catch (ClassNotFoundException ignored) {
54 | //Darcula is not installed.
55 | }
56 | }
57 |
58 | @Override
59 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
60 | CollaboratorPlusPlus.callbacks = callbacks;
61 | proxyServiceListeners = new ArrayList<>();
62 |
63 | //Setup preferences
64 | DefaultGsonProvider gsonProvider = new DefaultGsonProvider();
65 | this.preferences = new CollaboratorPreferenceFactory(gsonProvider, callbacks).buildPreferences();
66 | this.contextManager = new ContextManager(this);
67 |
68 | //Load log level from preferences
69 | Level logLevel = preferences.getSetting(PREF_LOG_LEVEL);
70 |
71 | //Setup logger
72 | LoggerContext context = (LoggerContext) LogManager.getContext(false);
73 | Configuration config = context.getConfiguration();
74 | PatternLayout logLayout = PatternLayout.newBuilder()
75 | .withConfiguration(config)
76 | .withPattern("[%-5level] %d{yyyy-MM-dd HH:mm:ss} %msg%n")
77 | .build();
78 | JTextAreaAppender textAreaAppender = JTextAreaAppender.createAppender("JTextAreaAppender", 500,
79 | false, logLayout, null);
80 | textAreaAppender.start();
81 | config.addAppender(textAreaAppender);
82 |
83 | AppenderRef[] appenderRefs = new AppenderRef[]{
84 | AppenderRef.createAppenderRef("JTextAreaAppender", null, null)
85 | };
86 | LoggerConfig loggerConfig = LoggerConfig.createLogger(false, logLevel, "CollaboratorPlusPlus", "true",
87 | appenderRefs, null, config, null);
88 | loggerConfig.addAppender(textAreaAppender, null, null);
89 | config.addLogger(EXTENSION_NAME, loggerConfig);
90 | context.updateLoggers();
91 |
92 | logger = LogManager.getLogger(EXTENSION_NAME);
93 |
94 | //Clean up proxy service on startup failure and color tab when running/stopped
95 | this.addProxyServiceListener(new ProxyServiceAdapter() {
96 |
97 | @Override
98 | public void onStartupSuccess(String message) {
99 | logger.info("Local authentication proxy started!");
100 | SwingUtilities.invokeLater(() -> burpTabController.setTabColor(new Color(60, 146, 38)));
101 | }
102 |
103 | @Override
104 | public void onStartupFail(String message) {
105 | logger.info("Failed to start the local authentication proxy, Reason: " + message);
106 | shutdownProxyService();
107 | }
108 |
109 | @Override
110 | public void onShutdown() {
111 | SwingUtilities.invokeLater(() -> {
112 | if(UIManager.getLookAndFeel().getName().equalsIgnoreCase("darcula")) {
113 | burpTabController.setTabColor(new Color(212, 60, 55));
114 | }else{
115 | burpTabController.setTabColor(new Color(220, 10, 19));
116 | }
117 | });
118 | }
119 | });
120 |
121 | //Color tab orange if errors, green if working correctly.
122 | this.contextManager.addEventListener(new CollaboratorEventAdapter() {
123 | @Override
124 | public void onPollingResponseReceived(CollaboratorContext collaboratorContext, ArrayList interactions) {
125 | SwingUtilities.invokeLater(() -> burpTabController.setTabColor(new Color(60, 146, 38)));
126 | }
127 |
128 | @Override
129 | public void onPollingFailure(CollaboratorContext collaboratorContext, String error) {
130 | SwingUtilities.invokeLater(() -> burpTabController.setTabColor(Color.ORANGE));
131 | }
132 | });
133 |
134 |
135 | SwingUtilities.invokeLater(() -> {
136 | CollaboratorPlusPlus.callbacks.registerExtensionStateListener(this);
137 |
138 | //Create UI and ask burp to add its tab.
139 | this.ui = new ExtensionUI(this);
140 | CollaboratorPlusPlus.callbacks.addSuiteTab(this.ui);
141 | //Use the ui component to get the parent tabbed pane.
142 | JTabbedPane burpTabbedPane = this.ui.getBurpTabbedPane();
143 | //Setup tab controller using the main tabbed panel we found.
144 | this.burpTabController = new BurpTabController(burpTabbedPane, this.ui.getUiComponent(), null, null);
145 |
146 | //Start off the tab as red, until we've started up.
147 | if(UIManager.getLookAndFeel().getName().equalsIgnoreCase("darcula")) {
148 | burpTabController.setTabColor(new Color(212, 60, 55));
149 | }else{
150 | burpTabController.setTabColor(new Color(220, 10, 19));
151 | }
152 |
153 | this.ui.addMenuItemsToBurp();
154 | if(this.preferences.getSetting(PREF_AUTO_START)){
155 | new Thread(() -> {
156 | try {
157 | Thread.sleep(500);
158 | startCollaboratorProxy();
159 | } catch (Exception ignored) {}
160 | }).start();
161 | }
162 | });
163 |
164 | if(this.preferences.getSetting(PREF_BLOCK_PUBLIC_COLLABORATOR)){
165 | Utilities.blockPublicCollaborator();
166 | }
167 | }
168 |
169 | public void startCollaboratorProxy() throws URISyntaxException {
170 | if(isProxyServiceRunning()) throw new IllegalStateException("The proxy service is already running.");
171 |
172 | for (IProxyServiceListener listener : proxyServiceListeners) {
173 | try {
174 | listener.beforeStartup();
175 | }catch (Exception ignored){
176 | ignored.printStackTrace();
177 | }
178 | }
179 |
180 | String collaboratorAddress = preferences.getSetting(PREF_COLLABORATOR_ADDRESS);
181 | if(preferences.getSetting(PREF_POLLING_ADDRESS).equals("")){
182 | logger.info("Polling location was not configured. Defaulting to the Collaborator address.");
183 | preferences.setSetting(PREF_POLLING_ADDRESS, collaboratorAddress);
184 | }
185 |
186 | boolean ssl = this.preferences.getSetting(PREF_REMOTE_SSL_ENABLED);
187 |
188 | URI pollingAddress = new URI(ssl ? "https" : "http", null,
189 | this.preferences.getSetting(PREF_POLLING_ADDRESS),
190 | this.preferences.getSetting(PREF_POLLING_PORT), "/", null, null);
191 | boolean useAuthentication = this.preferences.getSetting(PREF_USE_AUTHENTICATION);
192 | int listenPort = this.preferences.getSetting(PREF_LOCAL_PORT);
193 | String secret = ((String) this.preferences.getSetting(PREF_SECRET)).trim();
194 |
195 | boolean ignoreCertificateErrors = this.preferences.getSetting(PREF_IGNORE_CERTIFICATE_ERRORS);
196 | boolean verifyHostname = this.preferences.getSetting(PREF_SSL_HOSTNAME_VERIFICATION);
197 | HttpHost proxy = null;
198 | if(this.preferences.getSetting(PREF_PROXY_REQUESTS_WITH_BURP)){
199 | // TEMPORARILY DISABLED UNTIL WORKING
200 | // proxy = Utilities.getBurpProxyHost(pollingAddress.getScheme());
201 | }
202 |
203 | Utilities.backupCollaboratorConfig(preferences);
204 | callbacks.loadConfigFromJson(Utilities.buildPollingRedirectionConfig(preferences, listenPort));
205 |
206 | //Build the proxy service with the required values.
207 | proxyService = new ProxyService(contextManager, proxyServiceListeners,
208 | collaboratorAddress, listenPort, pollingAddress,
209 | useAuthentication, secret, ignoreCertificateErrors, verifyHostname, proxy);
210 |
211 | if(tryCloseCollaboratorWindows() && getCollaboratorFrames().size() > 0){
212 | //User accepted request to close collaborator windows.
213 | JOptionPane.showMessageDialog(this.ui.getUiComponent(),
214 | "A Collaborator client window was not closed. Its interactions will not be captured.",
215 | "Collaborator client warning", JOptionPane.WARNING_MESSAGE);
216 | }
217 |
218 | proxyService.start();
219 | }
220 |
221 | private ArrayList getCollaboratorFrames(){
222 | Frame[] frames = Frame.getFrames();
223 | ArrayList collaboratorFrames = new ArrayList<>();
224 | for (Frame frame : frames) {
225 | if(frame.getTitle().equalsIgnoreCase("Burp Collaborator client") && frame.isShowing()){
226 | collaboratorFrames.add(frame);
227 | }
228 | }
229 | return collaboratorFrames;
230 | }
231 |
232 | /**
233 | * Check for any open collaborator windows and ask user if it's okay to close them and proceed.
234 | * @return boolean True if its okay to proceed.
235 | */
236 | public boolean tryCloseCollaboratorWindows(){
237 | ArrayList collaboratorFrames = getCollaboratorFrames();
238 | if(collaboratorFrames.size() == 0) return true;
239 |
240 | if(JOptionPane.showConfirmDialog(ui.getUiComponent(), "Collaborator++ has detected open collaborator client windows.\n" +
241 | "It is recommended that existing Collaborator clients are closed and reopened once Collaborator++ is running, or it will not be able to detect these interactions.\n\n" +
242 | "Close them now?",
243 | "Existing Collaborator Windows", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.NO_OPTION){
244 | return false;
245 | }
246 |
247 | for (Frame collaboratorFrame : collaboratorFrames) {
248 | //Cannot simply close the windows. We must trigger the close event to make sure
249 | //Burp shuts down the scheduled polling event.
250 | collaboratorFrame.dispatchEvent(new WindowEvent(collaboratorFrame, WindowEvent.WINDOW_CLOSING));
251 | }
252 |
253 | return true;
254 | }
255 |
256 | public void shutdownProxyService(){
257 | if(!isProxyServiceRunning()) throw new IllegalStateException("The proxy service is not running.");
258 | proxyService.stop();
259 | logger.info("Proxy Service Stopped...");
260 | for (IProxyServiceListener proxyServiceListener : proxyServiceListeners) {
261 | proxyServiceListener.onShutdown();
262 | }
263 | Utilities.restoreCollaboratorConfig(preferences);
264 | }
265 |
266 | public void addProxyServiceListener(IProxyServiceListener listener){
267 | this.proxyServiceListeners.add(listener);
268 | }
269 |
270 | public void removeProxyServiceListener(IProxyServiceListener listener){
271 | this.proxyServiceListeners.remove(listener);
272 | }
273 |
274 | public boolean isProxyServiceRunning(){
275 | return proxyService != null && proxyService.getServer() != null;
276 | }
277 |
278 | public ProxyService getProxyService() {
279 | return proxyService;
280 | }
281 |
282 | @Override
283 | public void extensionUnloaded() {
284 | try {
285 | shutdownProxyService();
286 | }catch (IllegalStateException ignored){}
287 |
288 | if(this.preferences.getSetting(PREF_BLOCK_PUBLIC_COLLABORATOR)){
289 | Utilities.unblockPublicCollaborator();
290 | }
291 | }
292 |
293 | public Preferences getPreferences() {
294 | return this.preferences;
295 | }
296 |
297 | public ContextManager getContextManager() {
298 | return this.contextManager;
299 | }
300 | }
301 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/CollaboratorPreferenceFactory.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import burp.IBurpExtenderCallbacks;
4 | import com.coreyd97.BurpExtenderUtilities.IGsonProvider;
5 | import com.coreyd97.BurpExtenderUtilities.PreferenceFactory;
6 | import com.coreyd97.BurpExtenderUtilities.Preferences;
7 | import com.google.gson.reflect.TypeToken;
8 | import com.nccgroup.collaboratorplusplus.extension.context.*;
9 | import com.nccgroup.collaboratorplusplus.utilities.LevelSerializer;
10 | import org.apache.logging.log4j.Level;
11 |
12 | import java.util.ArrayList;
13 |
14 | import static com.nccgroup.collaboratorplusplus.extension.Globals.*;
15 |
16 | public class CollaboratorPreferenceFactory extends PreferenceFactory {
17 |
18 | public CollaboratorPreferenceFactory(IGsonProvider gsonProvider, IBurpExtenderCallbacks callbacks) {
19 | super(EXTENSION_NAME, gsonProvider, callbacks);
20 | }
21 |
22 | @Override
23 | protected void createDefaults() {
24 |
25 | }
26 |
27 | @Override
28 | protected void registerTypeAdapters() {
29 | gsonProvider.registerTypeAdapter(Interaction.class, new InteractionSerializer());
30 | // gsonProvider.registerTypeAdapter(CollaboratorContext.class, new CollaboratorContextSerializer());
31 | gsonProvider.registerTypeAdapter(CollaboratorServer.class, new CollaboratorServerSerializer());
32 | gsonProvider.registerTypeAdapter(Level.class, new LevelSerializer());
33 | }
34 |
35 | @Override
36 | protected void registerSettings() {
37 | prefs.registerSetting(PREF_LOG_LEVEL, Level.class, Level.INFO, Preferences.Visibility.GLOBAL);
38 | prefs.registerSetting(PREF_COLLABORATOR_ADDRESS, String.class, "burpcollaborator.net", Preferences.Visibility.GLOBAL);
39 | prefs.registerSetting(PREF_POLLING_ADDRESS, String.class, "polling.burpcollaborator.net", Preferences.Visibility.GLOBAL);
40 | prefs.registerSetting(PREF_POLLING_PORT, Integer.class, 443, Preferences.Visibility.GLOBAL);
41 | prefs.registerSetting(PREF_REMOTE_SSL_ENABLED, Boolean.class, true, Preferences.Visibility.GLOBAL);
42 | prefs.registerSetting(PREF_IGNORE_CERTIFICATE_ERRORS, Boolean.class, false, Preferences.Visibility.GLOBAL);
43 | prefs.registerSetting(PREF_SSL_HOSTNAME_VERIFICATION, Boolean.class, true, Preferences.Visibility.GLOBAL);
44 | prefs.registerSetting(PREF_LOCAL_PORT, Integer.class, 32541, Preferences.Visibility.GLOBAL);
45 | prefs.registerSetting(PREF_SECRET, String.class, "Your Secret String", Preferences.Visibility.GLOBAL);
46 | prefs.registerSetting(PREF_BLOCK_PUBLIC_COLLABORATOR, Boolean.class, false, Preferences.Visibility.PROJECT);
47 | prefs.registerSetting(PREF_PROXY_REQUESTS_WITH_BURP, Boolean.class, false, Preferences.Visibility.GLOBAL);
48 | prefs.registerSetting(PREF_USE_AUTHENTICATION, Boolean.class, false, Preferences.Visibility.GLOBAL);
49 | prefs.registerSetting(PREF_AUTO_START, Boolean.class, false, Preferences.Visibility.GLOBAL);
50 | prefs.registerSetting(PREF_ORIGINAL_COLLABORATOR_SETTINGS, String.class, "", Preferences.Visibility.PROJECT);
51 | prefs.registerSetting(PREF_COLLABORATOR_HISTORY, new TypeToken>(){}.getType(), new ArrayList<>(), Preferences.Visibility.PROJECT);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/CollaboratorServerResponse.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
4 | import org.apache.http.Header;
5 | import org.apache.http.HttpResponse;
6 |
7 | import java.util.ArrayList;
8 |
9 | public class CollaboratorServerResponse {
10 | private HttpResponse httpResponse;
11 | private ArrayList interactions;
12 |
13 | public CollaboratorServerResponse(HttpResponse response, ArrayList interactions){
14 | this.httpResponse = response;
15 | this.interactions = interactions;
16 | }
17 |
18 | public ArrayList getInteractions() {
19 | return interactions;
20 | }
21 |
22 | public HttpResponse getHttpResponse() {
23 | return httpResponse;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/DNSQueryType.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import java.util.ArrayList;
4 | import java.util.HashMap;
5 |
6 | public enum DNSQueryType {
7 | A(1),
8 | AAAA(28),
9 | CNAME(5),
10 | MX(15),
11 | NS(2),
12 | SRV(33),
13 | SOA(6),
14 | TXT(16);
15 |
16 | private int code;
17 |
18 | DNSQueryType(int code){
19 | this.code = code;
20 | }
21 |
22 | static HashMap types;
23 | static {
24 | types = new HashMap<>();
25 | for (DNSQueryType type : DNSQueryType.values()) {
26 | types.put(type.code, type);
27 | }
28 | }
29 |
30 | public static String getTypeByCode(int code){
31 | if(types.containsKey(code)) return types.get(code).toString();
32 | else return "UNKNOWN";
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/Globals.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | public class Globals {
4 |
5 | public static final String EXTENSION_NAME = "Collaborator++";
6 | public static final String PREF_COLLABORATOR_ADDRESS = "collaboratorAddress";
7 | public static final String PREF_POLLING_ADDRESS = "pollingAddress";
8 | public static final String PREF_POLLING_PORT = "remotePort";
9 | public static final String PREF_REMOTE_SSL_ENABLED = "remoteSSLEnabled";
10 | public static final String PREF_PROXY_REQUESTS_WITH_BURP = "useBurpAsProxy";
11 | public static final String PREF_IGNORE_CERTIFICATE_ERRORS = "ignoreCertificateErrors";
12 | public static final String PREF_SSL_HOSTNAME_VERIFICATION = "sslHostnameVerification";
13 | public static final String PREF_LOCAL_PORT = "localPort";
14 | public static final String PREF_SECRET = "sharedSecret";
15 | public static final String PREF_LOG_LEVEL = "logLevel";
16 | public static final String PREF_ORIGINAL_COLLABORATOR_SETTINGS = "origPollSettings";
17 | public static final String PREF_BLOCK_PUBLIC_COLLABORATOR = "blockPublicCollaborator";
18 | public static final String PREF_COLLABORATOR_HISTORY = "collaboratorContexts";
19 | public static final String PREF_USE_AUTHENTICATION = "useAuthentication";
20 | public static final String PREF_AUTO_START = "startAutomatically";
21 | public static final String COLLABORATOR_SERVER_CONFIG_PATH = "project_options.misc.collaborator_server";
22 | public static final String HOSTNAME_RESOLUTION_CONFIG_PATH = "project_options.connections.hostname_resolution";
23 | public static final String PUBLIC_COLLABORATOR_HOSTNAME = "burpcollaborator.net";
24 | public static final String GITHUB_URL = "https://github.com/coreyd97/CollaboratorAuth";
25 | public static final String TWITTER_URL = "https://twitter.com/coreyd97";
26 | public static final String IRSDL_TWITTER_URL = "https://twitter.com/irsdl";
27 | public static final String NCC_TWITTER_URL = "https://twitter.com/NCCGroupInfosec";
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/IProxyServiceListener.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | public interface IProxyServiceListener {
4 | void beforeStartup();
5 | void onStartupSuccess(String message);
6 | void onStartupFail(String message);
7 | void onShutdown();
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/JTextAreaAppender.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import org.apache.logging.log4j.core.*;
4 | import org.apache.logging.log4j.core.appender.AbstractAppender;
5 | import org.apache.logging.log4j.core.config.plugins.Plugin;
6 | import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
7 | import org.apache.logging.log4j.core.config.plugins.PluginElement;
8 | import org.apache.logging.log4j.core.config.plugins.PluginFactory;
9 |
10 | import javax.swing.*;
11 | import java.util.ArrayList;
12 |
13 | import static javax.swing.SwingUtilities.invokeLater;
14 | import static org.apache.logging.log4j.core.config.Property.EMPTY_ARRAY;
15 | import static org.apache.logging.log4j.core.layout.PatternLayout.createDefaultLayout;
16 |
17 | /**
18 | * JTextAreaAppender - https://stackoverflow.com/a/29736246
19 | * Credit to https://stackoverflow.com/users/4544015/vshiro
20 | */
21 |
22 | @Plugin(name = "JTextAreaAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
23 | public class JTextAreaAppender extends AbstractAppender
24 | {
25 | private static volatile ArrayList textAreas = new ArrayList<>();
26 |
27 | private int maxLines;
28 |
29 | private JTextAreaAppender(String name, Layout> layout, Filter filter, int maxLines, boolean ignoreExceptions)
30 | {
31 | super(name, filter, layout, ignoreExceptions, EMPTY_ARRAY);
32 | this.maxLines = maxLines;
33 | }
34 |
35 | @SuppressWarnings("unused")
36 | @PluginFactory
37 | public static JTextAreaAppender createAppender(@PluginAttribute("name") String name,
38 | @PluginAttribute("maxLines") int maxLines,
39 | @PluginAttribute("ignoreExceptions") boolean ignoreExceptions,
40 | @PluginElement("Layout") Layout> layout,
41 | @PluginElement("Filters") Filter filter)
42 | {
43 | if (name == null)
44 | {
45 | LOGGER.error("No name provided for JTextAreaAppender");
46 | return null;
47 | }
48 |
49 | if (layout == null)
50 | {
51 | layout = createDefaultLayout();
52 | }
53 | return new JTextAreaAppender(name, layout, filter, maxLines, ignoreExceptions);
54 | }
55 |
56 | // Add the target JTextArea to be populated and updated by the logging information.
57 | public static void addLog4j2TextAreaAppender(final JTextArea textArea)
58 | {
59 | JTextAreaAppender.textAreas.add(textArea);
60 | }
61 |
62 | @Override
63 | public void append(LogEvent event)
64 | {
65 | String message = new String(this.getLayout().toByteArray(event));
66 |
67 | // Append formatted message to text area using the Thread.
68 | try
69 | {
70 | invokeLater(() ->
71 | {
72 | for (JTextArea textArea : textAreas)
73 | {
74 | try
75 | {
76 | if (textArea != null)
77 | {
78 | if (textArea.getText().length() == 0)
79 | {
80 | textArea.setText(message);
81 | } else
82 | {
83 | textArea.append("\n" + message);
84 | if (maxLines > 0 & textArea.getLineCount() > maxLines + 1)
85 | {
86 | int endIdx = textArea.getDocument().getText(0, textArea.getDocument().getLength()).indexOf("\n");
87 | textArea.getDocument().remove(0, endIdx + 1);
88 | }
89 | }
90 | String content = textArea.getText();
91 | textArea.setText(content.substring(0, content.length() - 1));
92 | }
93 | } catch (Throwable throwable)
94 | {
95 | throwable.printStackTrace();
96 | }
97 | }
98 | });
99 | } catch (IllegalStateException exception)
100 | {
101 | exception.printStackTrace();
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/ProxyService.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.JsonObject;
5 | import com.google.gson.JsonParser;
6 | import com.google.gson.JsonSyntaxException;
7 | import com.google.gson.stream.MalformedJsonException;
8 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
9 | import com.nccgroup.collaboratorplusplus.extension.context.ContextManager;
10 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
11 | import com.nccgroup.collaboratorplusplus.extension.exception.*;
12 | import com.nccgroup.collaboratorplusplus.utilities.Encryption;
13 | import org.apache.http.*;
14 | import org.apache.http.client.ClientProtocolException;
15 | import org.apache.http.client.config.RequestConfig;
16 | import org.apache.http.client.methods.HttpGet;
17 | import org.apache.http.client.methods.HttpPost;
18 | import org.apache.http.config.SocketConfig;
19 | import org.apache.http.conn.ssl.DefaultHostnameVerifier;
20 | import org.apache.http.conn.ssl.NoopHostnameVerifier;
21 | import org.apache.http.conn.ssl.TrustAllStrategy;
22 | import org.apache.http.entity.ByteArrayEntity;
23 | import org.apache.http.entity.StringEntity;
24 | import org.apache.http.impl.NoConnectionReuseStrategy;
25 | import org.apache.http.impl.bootstrap.HttpServer;
26 | import org.apache.http.impl.bootstrap.ServerBootstrap;
27 | import org.apache.http.impl.client.CloseableHttpClient;
28 | import org.apache.http.impl.client.HttpClientBuilder;
29 | import org.apache.http.impl.client.HttpClients;
30 | import org.apache.http.protocol.HttpContext;
31 | import org.apache.http.protocol.HttpRequestHandler;
32 | import org.apache.http.ssl.SSLContextBuilder;
33 | import org.apache.http.ssl.TrustStrategy;
34 | import org.apache.http.util.EntityUtils;
35 | import org.apache.logging.log4j.LogManager;
36 | import org.apache.logging.log4j.Logger;
37 |
38 | import javax.net.ssl.SSLContext;
39 | import javax.net.ssl.SSLHandshakeException;
40 | import java.io.IOException;
41 | import java.net.*;
42 | import java.security.GeneralSecurityException;
43 | import java.security.KeyManagementException;
44 | import java.security.KeyStoreException;
45 | import java.security.NoSuchAlgorithmException;
46 | import java.util.ArrayList;
47 | import java.util.Base64;
48 | import java.util.concurrent.TimeUnit;
49 |
50 | public class ProxyService implements HttpRequestHandler {
51 |
52 | private final ContextManager contextManager;
53 | private final String collaboratorAddress;
54 | private final int listenPort;
55 | private final boolean useAuthentication;
56 | private final boolean ignoreCertificateErrors;
57 | private final boolean hostnameVerification;
58 | private final HttpHost proxyAddress;
59 | private final ArrayList serviceListeners;
60 |
61 | private String sessionKey;
62 | private HttpServer server;
63 | private URI forwardingURI;
64 |
65 | private Logger logger = LogManager.getLogger(Globals.EXTENSION_NAME);
66 |
67 | ProxyService(ContextManager contextManager, ArrayList listeners,
68 | String collaboratorAddress, Integer listenPort, URI forwardingURI, boolean useAuthentication,
69 | String secret, boolean ignoreCertificateErrors, boolean hostnameVerification,
70 | HttpHost proxyAddress){
71 | this.serviceListeners = listeners;
72 | this.contextManager = contextManager;
73 | this.collaboratorAddress = collaboratorAddress;
74 | this.listenPort = listenPort;
75 | this.ignoreCertificateErrors = ignoreCertificateErrors;
76 | this.hostnameVerification = hostnameVerification;
77 | this.useAuthentication = useAuthentication;
78 | this.forwardingURI = forwardingURI;
79 | this.proxyAddress = proxyAddress;
80 | this.sessionKey = secret;
81 | }
82 |
83 | public void start() throws IllegalStateException {
84 | if(server != null){
85 | throw new IllegalStateException();
86 | }
87 |
88 | ServerBootstrap serverBootstrap = ServerBootstrap.bootstrap()
89 | .setConnectionReuseStrategy(new NoConnectionReuseStrategy())
90 | .setLocalAddress(Inet4Address.getLoopbackAddress())
91 | .setListenerPort(listenPort)
92 | .setSocketConfig(SocketConfig.custom().setSoReuseAddress(true).build())
93 | .registerHandler("*", this);
94 |
95 | serverBootstrap.setExceptionLogger(ex -> {
96 | if(!(ex instanceof SocketException) && !(ex instanceof ConnectionClosedException)){
97 | logger.error("Uncaught Exception...");
98 | logger.error(ex.getMessage());
99 | logger.debug(ex);
100 | }
101 | });
102 |
103 | server = serverBootstrap.create();
104 | assert server != null;
105 | try {
106 | logger.info("Server Started...");
107 | server.start();
108 | logger.info("Testing connection to collaborator instance.");
109 | requestInteractionsForContext("test");
110 | handleStartupSuccess("Server Started.");
111 | }catch (Exception e){
112 | handleStartupFailure(e.getMessage());
113 | }
114 |
115 | //Server is started...
116 | }
117 |
118 | public void stop(){
119 | if(server != null){
120 | server.shutdown(500, TimeUnit.MILLISECONDS);
121 | server = null;
122 | }
123 | }
124 |
125 | private CloseableHttpClient buildHttpClient(boolean ignoreCertificateErrors, boolean verifyHostname, HttpHost proxyAddress)
126 | throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
127 | logger.debug("Creating HTTP Client...");
128 | HttpClientBuilder httpClientBuilder =
129 | HttpClients.custom().setSSLContext(createSSLContext(ignoreCertificateErrors))
130 | .setSSLHostnameVerifier(verifyHostname ? new DefaultHostnameVerifier() : NoopHostnameVerifier.INSTANCE)
131 | .setConnectionReuseStrategy(new NoConnectionReuseStrategy())
132 | .setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(5000).build()); //Don't hang around!
133 |
134 | if(proxyAddress != null){
135 | logger.debug("Setting Client Proxy to: " + proxyAddress);
136 | httpClientBuilder.setProxy(proxyAddress);
137 | }
138 |
139 | return httpClientBuilder.build();
140 | }
141 |
142 | private SSLContext createSSLContext(boolean ignoreCertificateErrors) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
143 | TrustStrategy trustStrategy;
144 | SSLContext context;
145 | if(ignoreCertificateErrors){
146 | trustStrategy = new TrustAllStrategy();
147 | context = SSLContextBuilder.create().loadTrustMaterial(trustStrategy).build();
148 | }else{
149 | context = SSLContextBuilder.create().build();
150 | }
151 | return context;
152 | }
153 |
154 | private CloseableHttpClient buildHttpClient() throws CollaboratorPollingException {
155 | CloseableHttpClient client;
156 | try {
157 | client = buildHttpClient(this.ignoreCertificateErrors, this.hostnameVerification, this.proxyAddress);
158 | } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
159 | logger.error("Could not create HTTP client, " + e.getMessage());
160 | logger.debug(e);
161 | throw new CollaboratorPollingException(e.getMessage());
162 | }
163 | logger.debug("HTTP Client Created: " + client);
164 | return client;
165 | }
166 |
167 | public ArrayList requestInteractionsForContext(CollaboratorContext context) throws CollaboratorPollingException {
168 | if(context.getCollaboratorServer().getCollaboratorAddress().equalsIgnoreCase(this.collaboratorAddress)){
169 | return requestInteractionsForContext(context.getIdentifier());
170 | }else{
171 | throw new CollaboratorPollingException("Collaborator++ is not currently targeting this collaborator server.\nPoint Collaborator++ to the correct server and try again.");
172 | }
173 | }
174 |
175 | public ArrayList requestInteractionsForContext(String contextIdentifier) throws CollaboratorPollingException {
176 | CloseableHttpClient client = buildHttpClient();
177 | try {
178 | return requestInteractionsForContext(client, contextIdentifier).getInteractions();
179 | }catch (CollaboratorPollingException e){
180 | if(!contextIdentifier.equalsIgnoreCase("test"))
181 | this.contextManager.pollingFailure(collaboratorAddress, contextIdentifier, e.getMessage());
182 | throw e;
183 | } finally {
184 | if (client != null) {
185 | try {
186 | client.close();
187 | } catch (IOException ignored) {}
188 | }
189 | }
190 | }
191 |
192 | private CollaboratorServerResponse requestInteractionsForContext(CloseableHttpClient httpClient, String contextIdentifier) throws CollaboratorPollingException {
193 |
194 | //Build request to collaborator server
195 | HttpHost host = new HttpHost(forwardingURI.getHost(), forwardingURI.getPort(), forwardingURI.getScheme());
196 | HttpRequest clientRequest;
197 | String pollingRequestUri = "/burpresults?biid=" + URLEncoder.encode(contextIdentifier);
198 | logger.info("Requesting interactions from server for identifier: " + contextIdentifier);
199 |
200 |
201 | String responseString = null;
202 | HttpResponse collaboratorServerResponse = null;
203 | try {
204 | if (useAuthentication) { //If we're using authentication, send an encrypted POST
205 | clientRequest = new HttpPost("/");
206 | buildAuthenticatedRequest((HttpPost) clientRequest, pollingRequestUri);
207 | } else { //Otherwise do a basic GET.
208 | clientRequest = new HttpGet(pollingRequestUri);
209 | }
210 |
211 | clientRequest.addHeader("Connection", "close");
212 |
213 | //Don't process the context if we're testing connection
214 | if (!contextIdentifier.equalsIgnoreCase("test"))
215 | this.contextManager.pollingRequestSent(this.collaboratorAddress, contextIdentifier);
216 |
217 | //Make the request
218 | logger.info("Sending Request.. " + clientRequest);
219 | collaboratorServerResponse = httpClient.execute(host, clientRequest);
220 | final int statusCode = collaboratorServerResponse.getStatusLine().getStatusCode();
221 | boolean isAuthServer = collaboratorServerResponse.getFirstHeader("X-Auth-Compatible") != null;
222 | logger.debug("Received response: Status " + statusCode + ", " + collaboratorServerResponse.getEntity());
223 |
224 | // if (collaboratorServerResponse.getFirstHeader("X-Collaborator-Version") == null) {
225 | // logger.debug("Server Response: " + EntityUtils.toString(collaboratorServerResponse.getEntity()));
226 | // if (isAuthServer) {
227 | // throw new InvalidResponseException("The collaborator server requires authentication.");
228 | // }else{
229 | // throw new InvalidResponseException("Not a valid collaborator response! Are we definitely targeting a Collaborator server?");
230 | // }
231 | // }
232 |
233 |
234 | if (isAuthServer ^ useAuthentication) {
235 | //If auth server and not using auth
236 | // or trying to use auth on a standard collaborator instance
237 | responseString = (useAuthentication
238 | ? "Authentication was enabled but was not supported by the Collaborator instance." :
239 | "The targeted Collaborator instance requires authentication.");
240 | throw new AuthenticationException(responseString);
241 | } else {
242 | if (!useAuthentication || statusCode == HttpStatus.SC_UNAUTHORIZED) {
243 | //If we're not using authentication,
244 | // or we were using auth and the server responded with a 401, the response will be plaintext.
245 | responseString = EntityUtils.toString(collaboratorServerResponse.getEntity());
246 | logger.debug("Plaintext Response Received: " + responseString);
247 | if(statusCode == HttpStatus.SC_UNAUTHORIZED) {
248 | throw new InvalidSecretException(responseString);
249 | }
250 | } else {
251 | //The response should have been encrypted.
252 | byte[] responseBytes = EntityUtils.toByteArray(collaboratorServerResponse.getEntity());
253 |
254 | if (new String(responseBytes).startsWith("") && this.proxyAddress != null) {
255 | //No response from the server, burp responded with its own message!
256 | //Likely due to protocol mismatch.
257 | throw new InvalidResponseException("Communication with the authentication server failed. " +
258 | "It was not possible to determine why due to Burp providing its own response. " +
259 | "Try again without proxying the requests through Burp.");
260 | }
261 |
262 | responseString = Encryption.aesDecryptRequest(this.sessionKey, responseBytes);
263 | logger.debug("Decrypted Response: " + responseString);
264 | }
265 |
266 | if (statusCode == HttpStatus.SC_OK) {
267 | JsonObject interactionsJson;
268 | try {
269 | interactionsJson = JsonParser.parseString(responseString).getAsJsonObject();
270 | }catch (JsonSyntaxException e){
271 | throw new InvalidResponseException("The server did not respond with valid JSON.");
272 | }
273 | ArrayList interactions = Utilities.parseInteractions(interactionsJson);
274 | logger.info(String.format("%d interaction%s retrieved.", interactions.size(),
275 | interactions.size() > 0 ? "s" : ""));
276 |
277 | if (!contextIdentifier.equalsIgnoreCase("test")) {
278 | this.contextManager.addInteractions(this.collaboratorAddress, contextIdentifier, interactions);
279 | }
280 |
281 | return new CollaboratorServerResponse(collaboratorServerResponse, interactions);
282 | }else{
283 | throw new InvalidResponseException("The Collaborator server responded with an error code which was not 200 OK.");
284 | }
285 | }
286 | } catch (ClientProtocolException | SSLHandshakeException e) {
287 | //Make the invalid certificate error a bit more friendly!
288 | if (e.getMessage() != null && e.getMessage().contains("unable to find valid certification path to requested target")) {
289 | responseString = "The SSL certificate provided by the server could not be verified. " +
290 | "To override this, check the \"Ignore Certificate Errors\" option but proceed with caution!";
291 | } else {
292 | responseString = e.getMessage() != null
293 | ? e.getMessage()
294 | : "SSL exception. Check you're targetting the correct protocol and the server is configured correctly.";
295 | }
296 | logger.debug(e);
297 | throw new CollaboratorSSLException(responseString);
298 | }catch (GeneralSecurityException | IOException e) {
299 | logger.debug(e);
300 | throw new CollaboratorPollingException(e.getMessage());
301 | } catch (CollaboratorPollingException e){
302 | logger.debug(e);
303 | throw e;
304 | }
305 | }
306 |
307 | @Override
308 | public void handle(HttpRequest request, HttpResponse forwardedResponse, HttpContext context) {
309 |
310 | String responseString = "";
311 | String contextIdentifier = "";
312 | try {
313 | CloseableHttpClient httpClient = buildHttpClient();
314 | contextIdentifier = URLDecoder.decode(request.getRequestLine().getUri().substring("/burpresults?biid=".length()), "UTF-8");
315 |
316 | CollaboratorServerResponse collaboratorResponse;
317 | try {
318 | collaboratorResponse = requestInteractionsForContext(httpClient, contextIdentifier);
319 | }finally {
320 | if (httpClient != null) {
321 | try {
322 | httpClient.close();
323 | } catch (IOException ignored) {}
324 | }
325 | }
326 |
327 | //Must forward collaborator headers to the client too!
328 | for (Header header : collaboratorResponse.getHttpResponse().getAllHeaders()) {
329 | if (header.getName().startsWith("X-Collaborator")) {
330 | forwardedResponse.addHeader(header);
331 | }
332 | }
333 |
334 | JsonObject interactions =
335 | Utilities.convertInteractionsToCollaboratorResponse(collaboratorResponse.getInteractions());
336 | StringEntity forwardedResponseEntity = new StringEntity(new Gson().toJson(interactions));
337 | forwardedResponseEntity.setContentType("application/json");
338 | forwardedResponse.setStatusCode(HttpStatus.SC_OK);
339 | forwardedResponse.setEntity(forwardedResponseEntity);
340 |
341 | return;
342 | }catch (Exception e){
343 | logger.error(e.getMessage());
344 | logger.debug(e);
345 | responseString = e.getMessage();
346 | }
347 |
348 | this.contextManager.pollingFailure(collaboratorAddress, contextIdentifier, responseString);
349 |
350 | forwardedResponse.setStatusCode(500);
351 | logger.info("Could not retrieve interactions: " + responseString);
352 | }
353 |
354 | private void buildAuthenticatedRequest(HttpPost clientRequest, String pollingRequestUri) throws GeneralSecurityException {
355 | String encodedURI = Base64.getEncoder().encodeToString(pollingRequestUri.getBytes());
356 | byte[] postBody = Encryption.aesEncryptRequest(this.sessionKey, encodedURI);
357 | clientRequest.setEntity(new ByteArrayEntity(postBody));
358 | }
359 |
360 | private void handleStartupSuccess(String message) {
361 | for (IProxyServiceListener serviceListener : this.serviceListeners) {
362 | try{
363 | serviceListener.onStartupSuccess(message);
364 | }catch (Exception ignored){
365 | ignored.printStackTrace();
366 | }
367 | }
368 | }
369 |
370 | private void handleStartupFailure(String message){
371 | for (IProxyServiceListener serviceListener : this.serviceListeners) {
372 | try{
373 | serviceListener.onStartupFail(message);
374 | }catch (Exception ignored){
375 | ignored.printStackTrace();
376 | }
377 | }
378 | }
379 |
380 | public void addProxyServiceListener(IProxyServiceListener listener){
381 | this.serviceListeners.add(listener);
382 | }
383 |
384 | public void removeProxyServiceListener(IProxyServiceListener listener){
385 | this.serviceListeners.remove(listener);
386 | }
387 |
388 | public HttpServer getServer() {
389 | return server;
390 | }
391 | }
392 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/ProxyServiceAdapter.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | public abstract class ProxyServiceAdapter implements IProxyServiceListener {
4 | @Override
5 | public void beforeStartup() {}
6 |
7 | @Override
8 | public void onStartupSuccess(String message) {}
9 |
10 | @Override
11 | public void onStartupFail(String message) {}
12 |
13 | @Override
14 | public void onShutdown() {}
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/Utilities.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Preferences;
4 | import com.google.gson.JsonArray;
5 | import com.google.gson.JsonElement;
6 | import com.google.gson.JsonObject;
7 | import com.google.gson.JsonParser;
8 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
9 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
10 | import org.apache.http.HttpHost;
11 | import org.apache.logging.log4j.LogManager;
12 | import org.apache.logging.log4j.Logger;
13 |
14 | import java.net.Inet4Address;
15 | import java.util.ArrayList;
16 |
17 | import static com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus.callbacks;
18 | import static com.nccgroup.collaboratorplusplus.extension.Globals.*;
19 |
20 | public class Utilities {
21 |
22 | private static final Logger logger = LogManager.getLogger(Globals.EXTENSION_NAME);
23 |
24 | public static ArrayList parseInteractions(JsonObject collaboratorResponse){
25 | ArrayList interactions = new ArrayList<>();
26 | if(collaboratorResponse.has("responses")) {
27 | JsonArray jsonArray = collaboratorResponse.get("responses").getAsJsonArray();
28 | for (JsonElement jsonElement : jsonArray) {
29 | Interaction interaction = Interaction.parseFromJson(jsonElement.getAsJsonObject());
30 | if (interaction != null) interactions.add(interaction);
31 | }
32 | }
33 | return interactions;
34 | }
35 |
36 | public static JsonObject convertInteractionsToCollaboratorResponse(ArrayList interactions){
37 | JsonObject json = new JsonObject();
38 | JsonArray interactionArray = new JsonArray(interactions.size());
39 | for (Interaction interaction : interactions) {
40 | interactionArray.add(interaction.getOriginalObject());
41 | }
42 | json.add("responses", interactionArray);
43 | return json;
44 | }
45 |
46 | public static ArrayList parseInteractions(CollaboratorContext collaboratorContext, JsonArray jsonArray){
47 | ArrayList interactions = new ArrayList<>();
48 | for (JsonElement jsonElement : jsonArray) {
49 | Interaction interaction = Interaction.parseFromJson(collaboratorContext, jsonElement.getAsJsonObject());
50 | if(interaction != null) interactions.add(interaction);
51 | }
52 | return interactions;
53 | }
54 |
55 | public static HttpHost getBurpProxyHost(String scheme) {
56 | String configString = callbacks.saveConfigAsJson("proxy.request_listeners");
57 | JsonObject config = new JsonParser().parse(configString).getAsJsonObject();
58 | JsonArray listeners = config.getAsJsonObject("proxy").getAsJsonArray("request_listeners");
59 | for (JsonElement listener : listeners) {
60 | JsonObject listnerObject = (JsonObject) listener;
61 | if(listnerObject.get("running").getAsBoolean()){
62 | int port = listnerObject.get("listener_port").getAsInt();
63 | String listenMode = listnerObject.get("listen_mode").getAsString();
64 | if(listenMode.equals("loopback_only")){
65 | return new HttpHost("127.0.0.1", port, scheme);
66 | }
67 | if(listenMode.equals("all_interfaces")){
68 | return new HttpHost("0.0.0.0", port, scheme);
69 | }
70 | if(listenMode.equals("specific_address")){
71 | String address = listnerObject.get("listen_specific_address").getAsString();
72 | return new HttpHost(address, port, scheme);
73 | }
74 | }
75 | }
76 | return null;
77 | }
78 |
79 | public static void blockPublicCollaborator(){
80 | String stringConfig = callbacks.saveConfigAsJson(HOSTNAME_RESOLUTION_CONFIG_PATH);
81 | JsonObject config = new JsonParser().parse(stringConfig).getAsJsonObject();
82 | JsonArray resolutionElements = config.getAsJsonObject("project_options")
83 | .getAsJsonObject("connections")
84 | .getAsJsonArray("hostname_resolution");
85 |
86 | boolean resolutionElementsModified = false;
87 | boolean shouldAddEntry = true;
88 | if(resolutionElements.size() > 0){
89 | for (JsonElement resolutionElement : resolutionElements) {
90 | String hostname = resolutionElement.getAsJsonObject().get("hostname").getAsString();
91 | String ip = resolutionElement.getAsJsonObject().get("ip_address").getAsString();
92 | Boolean enabled = resolutionElement.getAsJsonObject().get("enabled").getAsBoolean();
93 |
94 | if(hostname.equalsIgnoreCase(PUBLIC_COLLABORATOR_HOSTNAME)){
95 | if(ip.equalsIgnoreCase("127.0.0.1")){
96 | //Existing entry, just make sure its enabled.
97 | if(enabled){
98 | logger.info("Sink for public collaborator server already exists, continuing...");
99 | }else {
100 | logger.info("Enabling sink for public collaborator server.");
101 | resolutionElement.getAsJsonObject().addProperty("enabled", true);
102 | resolutionElementsModified = true;
103 | }
104 | shouldAddEntry = false;
105 | }else{
106 | //Not our entry,
107 | logger.info("Hostname resolution entry exists for public collaborator server. Disabling and adding sink entry.");
108 | resolutionElement.getAsJsonObject().addProperty("enabled", false);
109 | }
110 | break;
111 | }
112 | }
113 | }else{
114 | logger.info("Adding DNS sink for the public collaborator server: \"burpcollaborator.net\" .");
115 | }
116 | if(shouldAddEntry){
117 | resolutionElements.add(buildPublicCollaboratorSink());
118 | resolutionElementsModified = true;
119 | }
120 | if(resolutionElementsModified){
121 | callbacks.loadConfigFromJson(config.toString());
122 | }
123 | }
124 |
125 | public static void unblockPublicCollaborator(){
126 | String stringConfig = callbacks.saveConfigAsJson(HOSTNAME_RESOLUTION_CONFIG_PATH);
127 | JsonObject config = new JsonParser().parse(stringConfig).getAsJsonObject();
128 | JsonArray resolutionElements = config.getAsJsonObject("project_options")
129 | .getAsJsonObject("connections")
130 | .getAsJsonArray("hostname_resolution");
131 |
132 | for (JsonElement resolutionElement : resolutionElements) {
133 | String hostname = resolutionElement.getAsJsonObject().get("hostname").getAsString();
134 | String ip = resolutionElement.getAsJsonObject().get("ip_address").getAsString();
135 | Boolean enabled = resolutionElement.getAsJsonObject().get("enabled").getAsBoolean();
136 | if(hostname.equalsIgnoreCase(PUBLIC_COLLABORATOR_HOSTNAME) && ip.equalsIgnoreCase("127.0.0.1")){
137 | resolutionElement.getAsJsonObject().addProperty("enabled", false);
138 | logger.info("Disabled sink for public collaborator server.");
139 | break;
140 | }
141 | }
142 |
143 | callbacks.loadConfigFromJson(config.toString());
144 | }
145 |
146 | private static JsonObject buildPublicCollaboratorSink(){
147 | JsonObject entry = new JsonObject();
148 | entry.addProperty("enabled", true);
149 | entry.addProperty("hostname", PUBLIC_COLLABORATOR_HOSTNAME);
150 | entry.addProperty("ip_address", "127.0.0.1");
151 | return entry;
152 | }
153 |
154 | public static void backupCollaboratorConfig(Preferences preferences){
155 | String config = callbacks.saveConfigAsJson(COLLABORATOR_SERVER_CONFIG_PATH);
156 | preferences.setSetting(PREF_ORIGINAL_COLLABORATOR_SETTINGS, config);
157 | }
158 |
159 | public static void restoreCollaboratorConfig(Preferences preferences){
160 | String config = preferences.getSetting(PREF_ORIGINAL_COLLABORATOR_SETTINGS);
161 | callbacks.loadConfigFromJson(config);
162 | }
163 |
164 | public static String buildPollingRedirectionConfig(Preferences preferences, int listenPort){
165 | return "{\"project_options\": {\"misc\": {\"collaborator_server\": " +
166 | "{\"location\": \"" + preferences.getSetting(PREF_COLLABORATOR_ADDRESS) + "\"," +
167 | "\"polling_location\": \"" + Inet4Address.getLoopbackAddress().getHostName() + ":" + listenPort + "\"," +
168 | "\"poll_over_unencrypted_http\": \"true\"," +
169 | "\"type\": \"private\"" +
170 | "}}}}";
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/CollaboratorContext.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import java.awt.*;
4 | import java.util.ArrayList;
5 | import java.util.Date;
6 | import java.util.HashMap;
7 | import java.util.UUID;
8 |
9 | public class CollaboratorContext {
10 |
11 | transient CollaboratorServer collaboratorServer;
12 | String identifier;
13 | Date lastPolled;
14 | private ArrayList interactionIds;
15 | private HashMap interactionEvents;
16 | int dnsEventCount, httpEventCount, smtpEventCount, httpsEventCount, smtpsEventCount;
17 |
18 | Color highlight;
19 | transient ArrayList recentInteractions;
20 |
21 | CollaboratorContext(String identifier){
22 | this.identifier = identifier;
23 | this.lastPolled = new Date();
24 | this.interactionIds = new ArrayList<>();
25 | this.interactionEvents = new HashMap<>();
26 | this.recentInteractions = new ArrayList<>();
27 | }
28 |
29 | CollaboratorContext(CollaboratorServer collaboratorServer, String identifier){
30 | this(identifier);
31 | this.collaboratorServer = collaboratorServer;
32 | }
33 |
34 | public String getIdentifier() {
35 | return identifier;
36 | }
37 |
38 | public Date getLastPolled() {
39 | return lastPolled;
40 | }
41 |
42 | void addInteractions(ArrayList interactions){
43 | if(recentInteractions == null) recentInteractions = new ArrayList<>();
44 | for (Interaction interaction : interactions) {
45 | interactionIds.add(interaction.getUUID());
46 | interactionEvents.put(interaction.getUUID(), interaction);
47 | recentInteractions.add(interaction.getUUID());
48 |
49 | if(this.collaboratorServer != null)
50 | this.collaboratorServer.totalInteractions++;
51 |
52 | //Update context interaction counts
53 | switch (interaction.getInteractionType()){
54 | case DNS: {
55 | this.dnsEventCount++;
56 | if(this.collaboratorServer != null)
57 | this.collaboratorServer.dnsEventCount++;
58 | break;
59 | }
60 | case HTTP: {
61 | this.httpEventCount++;
62 | if(this.collaboratorServer != null)
63 | this.collaboratorServer.httpEventCount++;
64 | break;
65 | }
66 | case HTTPS: {
67 | this.httpsEventCount++;
68 | if(this.collaboratorServer != null)
69 | this.collaboratorServer.httpsEventCount++;
70 | break;
71 | }
72 | case SMTP: {
73 | this.smtpEventCount++;
74 | if(this.collaboratorServer != null)
75 | this.collaboratorServer.smtpEventCount++;
76 | break;
77 | }
78 | case SMTPS: {
79 | this.smtpsEventCount++;
80 | if(this.collaboratorServer != null)
81 | this.collaboratorServer.smtpsEventCount++;
82 | break;
83 | }
84 | }
85 | }
86 | }
87 |
88 | public ArrayList getRecentInteractions() {
89 | return recentInteractions;
90 | }
91 |
92 | public ArrayList getInteractionIds() {
93 | return interactionIds;
94 | }
95 |
96 | public HashMap getInteractionEvents() {
97 | return interactionEvents;
98 | }
99 |
100 | public Interaction getEventAtIndex(int index){
101 | return this.interactionEvents.get(interactionIds.get(index));
102 | }
103 |
104 | public int getDNSInteractionCount() {
105 | return dnsEventCount;
106 | }
107 |
108 | public int getHttpInteractionCount() {
109 | return httpEventCount;
110 | }
111 |
112 | public int getSMTPInteractionCount() {
113 | return smtpEventCount;
114 | }
115 |
116 | public int getHttpsInteractionCount() {
117 | return httpsEventCount;
118 | }
119 |
120 | public int getSMTPSInteractionCount(){
121 | return smtpsEventCount;
122 | }
123 |
124 | public Color getHighlight() {
125 | return highlight;
126 | }
127 |
128 | public CollaboratorServer getCollaboratorServer(){
129 | return this.collaboratorServer;
130 | }
131 |
132 | public void setCollaboratorServer(CollaboratorServer collaboratorServer) {
133 | this.collaboratorServer = collaboratorServer;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/CollaboratorContextSerializer.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import com.google.gson.*;
4 |
5 | import java.lang.reflect.Type;
6 |
7 | public class CollaboratorContextSerializer implements JsonDeserializer, JsonSerializer {
8 | @Override
9 | public CollaboratorContext deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
10 | return null;
11 | }
12 |
13 | @Override
14 | public JsonElement serialize(CollaboratorContext src, Type typeOfSrc, JsonSerializationContext context) {
15 | return null;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/CollaboratorServer.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import java.util.ArrayList;
4 |
5 | public class CollaboratorServer {
6 |
7 | private final String collaboratorAddress;
8 | private final ArrayList contexts;
9 | int totalInteractions, dnsEventCount, httpEventCount, smtpEventCount, httpsEventCount, smtpsEventCount;
10 |
11 | public CollaboratorServer(String collaboratorAddress, ArrayList contexts){
12 | this.collaboratorAddress = collaboratorAddress;
13 | this.contexts = contexts;
14 | }
15 |
16 | public CollaboratorServer(String collaboratorAddress){
17 | this(collaboratorAddress, new ArrayList<>());
18 | }
19 |
20 | public String getCollaboratorAddress() {
21 | return collaboratorAddress;
22 | }
23 |
24 | public boolean contextExists(String identifier){
25 | return this.contexts.stream()
26 | .anyMatch(collaboratorContext -> collaboratorContext.getIdentifier().equalsIgnoreCase(identifier));
27 | }
28 |
29 | public void addContext(CollaboratorContext collaboratorContext){
30 | this.contexts.add(collaboratorContext);
31 | }
32 |
33 | public CollaboratorContext getContext(String identifier){
34 | return this.contexts.stream()
35 | .filter(collaboratorContext -> collaboratorContext.getIdentifier().equalsIgnoreCase(identifier))
36 | .findFirst().orElse(null);
37 | }
38 |
39 | public ArrayList getContexts() {
40 | return contexts;
41 | }
42 |
43 | public void removeContext(String identifier){
44 | this.contexts.removeIf(collaboratorContext -> collaboratorContext.getIdentifier().equalsIgnoreCase(identifier));
45 | }
46 |
47 | public void removeContext(CollaboratorContext collaboratorContext){
48 | this.contexts.remove(collaboratorContext);
49 | this.dnsEventCount -= collaboratorContext.dnsEventCount;
50 | this.httpEventCount -= collaboratorContext.httpEventCount;
51 | this.httpsEventCount -= collaboratorContext.httpsEventCount;
52 | this.smtpEventCount -= collaboratorContext.smtpEventCount;
53 | this.smtpsEventCount -= collaboratorContext.smtpsEventCount;
54 | this.totalInteractions -= collaboratorContext.getInteractionEvents().size();
55 | }
56 |
57 | public int getTotalInteractions() {
58 | return totalInteractions;
59 | }
60 |
61 | public int getDnsEventCount() {
62 | return dnsEventCount;
63 | }
64 |
65 | public int getHttpEventCount() {
66 | return httpEventCount;
67 | }
68 |
69 | public int getSmtpEventCount() {
70 | return smtpEventCount;
71 | }
72 |
73 | public int getHttpsEventCount() {
74 | return httpsEventCount;
75 | }
76 |
77 | public int getSmtpsEventCount() {
78 | return smtpsEventCount;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/CollaboratorServerSerializer.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import com.google.gson.*;
4 |
5 | import java.lang.reflect.Type;
6 | import java.util.ArrayList;
7 |
8 | public class CollaboratorServerSerializer implements JsonDeserializer, JsonSerializer {
9 | @Override
10 | public CollaboratorServer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext deserializationContext) throws JsonParseException {
11 | JsonObject jsonObject = json.getAsJsonObject();
12 | String collaboratorAddress = jsonObject.get("serverAddress").getAsString();
13 | CollaboratorServer collaboratorServer = new CollaboratorServer(collaboratorAddress);
14 | collaboratorServer.totalInteractions = jsonObject.get("totalInteractions").getAsInt();
15 | collaboratorServer.dnsEventCount = jsonObject.get("dnsInteractions").getAsInt();
16 | collaboratorServer.httpEventCount = jsonObject.get("httpInteractions").getAsInt();
17 | collaboratorServer.httpsEventCount = jsonObject.get("httpsInteractions").getAsInt();
18 | collaboratorServer.smtpEventCount = jsonObject.get("smtpInteractions").getAsInt();
19 | collaboratorServer.smtpsEventCount = jsonObject.get("smtpsInteractions").getAsInt();
20 |
21 | for (JsonElement context : jsonObject.getAsJsonArray("contexts")) {
22 | CollaboratorContext collaboratorContext = deserializationContext.deserialize(context, CollaboratorContext.class);
23 | collaboratorContext.setCollaboratorServer(collaboratorServer);
24 | collaboratorServer.addContext(collaboratorContext);
25 | }
26 |
27 | return collaboratorServer;
28 | }
29 |
30 | @Override
31 | public JsonElement serialize(CollaboratorServer src, Type typeOfSrc, JsonSerializationContext context) {
32 | JsonObject obj = new JsonObject();
33 | obj.addProperty("serverAddress", src.getCollaboratorAddress());
34 | obj.addProperty("totalInteractions", src.getTotalInteractions());
35 | obj.addProperty("dnsInteractions", src.getDnsEventCount());
36 | obj.addProperty("httpInteractions", src.getHttpEventCount());
37 | obj.addProperty("httpsInteractions", src.getHttpsEventCount());
38 | obj.addProperty("smtpInteractions", src.getSmtpEventCount());
39 | obj.addProperty("smtpsInteractions", src.getSmtpsEventCount());
40 | JsonArray contexts = new JsonArray();
41 | for (CollaboratorContext collaboratorContext : src.getContexts()) {
42 | contexts.add(context.serialize(collaboratorContext, CollaboratorContext.class));
43 | }
44 | obj.add("contexts", contexts);
45 | return obj;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/ContextManager.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorEventListener;
4 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
5 | import com.nccgroup.collaboratorplusplus.extension.Globals;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Date;
9 |
10 | public class ContextManager {
11 |
12 | private final CollaboratorPlusPlus extension;
13 | private ArrayList collaboratorServers;
14 | private final ArrayList eventListeners;
15 |
16 | public ContextManager(CollaboratorPlusPlus extension){
17 | this.extension = extension;
18 | this.collaboratorServers = new ArrayList<>();
19 | this.eventListeners = new ArrayList<>();
20 | loadState();
21 | }
22 |
23 | public void pollingRequestSent(String collaboratorAddress, String contextIdentifier){
24 | CollaboratorServer collaboratorServer = getCollaboratorServer(collaboratorAddress);
25 | if(collaboratorServer == null){
26 | collaboratorServer = new CollaboratorServer(collaboratorAddress);
27 | int index = collaboratorServers.size();
28 | collaboratorServers.add(collaboratorServer);
29 |
30 | for (CollaboratorEventListener listener : this.eventListeners) {
31 | try{
32 | listener.onCollaboratorServerRegistered(collaboratorServer, index);
33 | }catch (Exception e){}
34 | }
35 | }
36 |
37 | CollaboratorContext collaboratorContext = collaboratorServer.getContext(contextIdentifier);
38 |
39 | if(collaboratorContext == null){
40 | collaboratorContext = new CollaboratorContext(collaboratorServer, contextIdentifier);
41 | int index = collaboratorServer.getContexts().size();
42 | collaboratorServer.addContext(collaboratorContext);
43 |
44 | for (CollaboratorEventListener eventListener : eventListeners) {
45 | try {
46 | eventListener.onCollaboratorContextRegistered(collaboratorContext, index);
47 | }catch (Exception ignored){ }
48 | }
49 | }
50 |
51 | collaboratorContext.lastPolled = new Date();
52 |
53 | saveState();
54 |
55 | for (CollaboratorEventListener eventListener : eventListeners) {
56 | try {
57 | eventListener.onPollingRequestSent(collaboratorContext);
58 | }catch (Exception ignored){
59 | ignored.printStackTrace();
60 | }
61 | }
62 | }
63 |
64 | public void addInteractions(String collaboratorAddress, String contextIdentifier, ArrayList interactions){
65 |
66 | CollaboratorServer collaboratorServer = getCollaboratorServer(collaboratorAddress);
67 | CollaboratorContext collaboratorContext = collaboratorServer.getContext(contextIdentifier);
68 |
69 | collaboratorContext.addInteractions(interactions);
70 |
71 | saveState();
72 |
73 | for (CollaboratorEventListener eventListener : eventListeners) {
74 | try {
75 | eventListener.onPollingResponseReceived(collaboratorContext, interactions);
76 | }catch (Exception ignored){
77 | ignored.printStackTrace();
78 | }
79 | }
80 | }
81 |
82 | public void pollingFailure(String collaboratorAddress, String contextIdentifier, String message){
83 |
84 | CollaboratorServer collaboratorServer = getCollaboratorServer(collaboratorAddress);
85 | CollaboratorContext context = collaboratorServer.getContext(contextIdentifier);
86 |
87 | for (CollaboratorEventListener eventListener : eventListeners) {
88 | try {
89 | eventListener.onPollingFailure(context, message);
90 | }catch (Exception ignored){
91 | ignored.printStackTrace();
92 | }
93 | }
94 | }
95 |
96 | public ArrayList requestInteractions(CollaboratorContext context) throws Exception {
97 | if(extension.getProxyService() == null) throw new Exception("The collaborator proxy is not running.");
98 | return extension.getProxyService().requestInteractionsForContext(context);
99 | }
100 |
101 | public void removeCollaboratorContext(CollaboratorContext context){
102 | int index = context.getCollaboratorServer().getContexts().indexOf(context);
103 | if(index == -1) return;
104 | context.getCollaboratorServer().removeContext(context);
105 | saveState();
106 | for (CollaboratorEventListener eventListener : eventListeners) {
107 | try{
108 | eventListener.onCollaboratorContextRemoved(context, index);
109 | }catch (Exception e){
110 | e.printStackTrace();
111 | }
112 | }
113 | }
114 |
115 | public void removeCollaboratorServer(CollaboratorServer server){
116 | int index = this.collaboratorServers.indexOf(server);
117 | if(index == -1) return;
118 | this.collaboratorServers.remove(server);
119 | saveState();
120 | for (CollaboratorEventListener eventListener : eventListeners) {
121 | try{
122 | eventListener.onCollaboratorServerRemoved(server, index);
123 | }catch (Exception e){
124 | e.printStackTrace();
125 | }
126 | }
127 | }
128 |
129 | public void saveState(){
130 | this.extension.getPreferences().setSetting(Globals.PREF_COLLABORATOR_HISTORY, collaboratorServers);
131 | }
132 |
133 | public boolean hasCollaboratorServer(String collaboratorAddress){
134 | return collaboratorServers.stream()
135 | .anyMatch(collaboratorServer ->
136 | collaboratorServer.getCollaboratorAddress().equalsIgnoreCase(collaboratorAddress)
137 | );
138 | }
139 |
140 | public CollaboratorServer getCollaboratorServer(String collaboratorAddress){
141 | return collaboratorServers.stream()
142 | .filter(collaboratorServer -> collaboratorServer.getCollaboratorAddress().equalsIgnoreCase(collaboratorAddress))
143 | .findFirst().orElse(null);
144 | }
145 |
146 | public ArrayList getCollaboratorServers(){
147 | return this.collaboratorServers;
148 | }
149 |
150 | public void addEventListener(CollaboratorEventListener listener){
151 | this.eventListeners.add(listener);
152 | }
153 |
154 | public void removeEventListener(CollaboratorEventListener listener){
155 | this.eventListeners.remove(listener);
156 | }
157 |
158 | private void loadState(){
159 | this.collaboratorServers = this.extension.getPreferences().getSetting(Globals.PREF_COLLABORATOR_HISTORY);
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/DNSInteraction.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import burp.IMessageEditor;
4 | import com.coreyd97.BurpExtenderUtilities.Alignment;
5 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
6 | import com.google.gson.JsonObject;
7 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
8 | import com.nccgroup.collaboratorplusplus.extension.DNSQueryType;
9 | import com.nccgroup.collaboratorplusplus.utilities.SelectableLabel;
10 | import org.bouncycastle.util.encoders.Base64;
11 |
12 | import javax.swing.*;
13 | import java.awt.*;
14 |
15 | public class DNSInteraction extends Interaction {
16 |
17 | String subDomain;
18 | byte[] rawRequest;
19 | int type;
20 |
21 | DNSInteraction(JsonObject interaction){
22 | this(null, interaction);
23 | }
24 |
25 | DNSInteraction(CollaboratorContext context, JsonObject interaction){
26 | super(context, InteractionType.DNS, interaction);
27 |
28 | //Parse DNS specific properties here
29 | this.subDomain = interaction.getAsJsonObject("data").get("subDomain").getAsString();
30 | this.rawRequest = Base64.decode(interaction.getAsJsonObject("data").get("rawRequest").getAsString());
31 | this.type = interaction.getAsJsonObject("data").get("type").getAsInt();
32 | }
33 |
34 | @Override
35 | public JComponent buildInteractionInfoPanel() {
36 | JComponent basePanel = super.buildInteractionInfoPanel();
37 | IMessageEditor editor = CollaboratorPlusPlus.callbacks.createMessageEditor(null, false);
38 | editor.setMessage(this.rawRequest, true);
39 |
40 | JScrollPane subdomainPane = new JScrollPane(new SelectableLabel(this.subDomain));
41 | subdomainPane.setBorder(null);
42 | subdomainPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
43 | subdomainPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
44 | subdomainPane.setMinimumSize(new Dimension(subdomainPane.getWidth(), 40));
45 | SelectableLabel dnsTypeLabel
46 | = new SelectableLabel(String.format("%s(%d)", DNSQueryType.getTypeByCode(this.type), this.type));
47 |
48 | return new PanelBuilder(null).build(new Component[][]{
49 | new Component[]{basePanel, basePanel},
50 | new Component[]{new JSeparator(JSeparator.HORIZONTAL), new JSeparator(JSeparator.HORIZONTAL)},
51 | new Component[]{new JLabel("SubDomain: "), subdomainPane},
52 | new Component[]{new JLabel("Type: "), dnsTypeLabel},
53 | new Component[]{editor.getComponent(), editor.getComponent()},
54 | }, new int[][]{
55 | new int[]{0, 0},
56 | new int[]{0, 0},
57 | new int[]{0, 1},
58 | new int[]{0, 1},
59 | new int[]{0, 999},
60 | }, Alignment.FILL, 1.0, 1.0);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/HTTPInteraction.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Alignment;
4 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
5 | import com.google.gson.JsonObject;
6 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
7 | import com.nccgroup.collaboratorplusplus.utilities.StaticHTTPMessageController;
8 | import org.bouncycastle.util.encoders.Base64;
9 |
10 | import javax.swing.*;
11 | import java.awt.*;
12 | import java.net.MalformedURLException;
13 | import java.net.URL;
14 |
15 | public class HTTPInteraction extends Interaction{
16 |
17 | byte[] request;
18 | byte[] response;
19 |
20 | protected HTTPInteraction(JsonObject interaction, boolean isHttps) {
21 | this(null, interaction, isHttps);
22 | }
23 |
24 | protected HTTPInteraction(CollaboratorContext context, JsonObject interaction, boolean isHttps) {
25 | super(context, isHttps ? InteractionType.HTTPS : InteractionType.HTTP, interaction);
26 |
27 | //Parse HTTP specific properties here
28 | this.request = Base64.decode(interaction.getAsJsonObject("data").get("request").getAsString());
29 | this.response = Base64.decode(interaction.getAsJsonObject("data").get("response").getAsString());
30 | }
31 |
32 | @Override
33 | public JComponent buildInteractionInfoPanel() {
34 | JComponent basePanel = super.buildInteractionInfoPanel();
35 | StaticHTTPMessageController messageController =
36 | new StaticHTTPMessageController(CollaboratorPlusPlus.callbacks, buildInteractionURL(), this.request, this.response);
37 |
38 | JSplitPane requestResponseViewer = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
39 | messageController.buildRequestViewer().getComponent(),
40 | messageController.buildResponseViewer().getComponent());
41 | requestResponseViewer.setResizeWeight(0.5);
42 |
43 | return new PanelBuilder(null).build(new Component[][]{
44 | new Component[]{basePanel},
45 | new Component[]{requestResponseViewer}
46 | }, new int[][]{
47 | new int[]{0},
48 | new int[]{1}
49 | }, Alignment.FILL, 1.0, 1.0);
50 | }
51 |
52 | private URL buildInteractionURL() {
53 | try {
54 | return new URL(this.interactionType.toString(), this.interactionString +
55 | (this.context != null ? this.context.getCollaboratorServer().getCollaboratorAddress() : ""),
56 | this.interactionType == InteractionType.HTTPS ? 443 : 80, "");
57 | } catch (MalformedURLException e) {
58 | return null;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/Interaction.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Alignment;
4 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
5 | import com.google.gson.JsonObject;
6 | import com.nccgroup.collaboratorplusplus.utilities.SelectableLabel;
7 |
8 | import javax.swing.*;
9 | import java.util.Date;
10 | import java.util.UUID;
11 |
12 | public abstract class Interaction {
13 | public enum InteractionType {SMTP, SMTPS, HTTP, HTTPS, DNS}
14 |
15 | private final UUID identifier;
16 | protected InteractionType interactionType;
17 | protected transient CollaboratorContext context;
18 | protected String interactionString;
19 | protected long time;
20 | protected String client;
21 | protected String clientPart;
22 | protected String opCode;
23 | protected JsonObject originalObject;
24 |
25 | private Interaction(){
26 | this.identifier = UUID.randomUUID();
27 | }
28 |
29 | protected Interaction(CollaboratorContext context, InteractionType type, JsonObject interaction){
30 | this();
31 | this.interactionType = type;
32 | this.context = context;
33 |
34 | //Parse our info from the jsonObject
35 | this.interactionString = interaction.get("interactionString").getAsString();
36 | this.time = interaction.get("time").getAsLong();
37 | this.client = interaction.get("client").getAsString();
38 | this.clientPart = interaction.get("clientPart").getAsString();
39 | this.opCode = interaction.get("opCode").getAsString();
40 | this.originalObject = interaction;
41 | }
42 |
43 | public UUID getUUID() {
44 | return identifier;
45 | }
46 |
47 | public InteractionType getInteractionType(){
48 | return this.interactionType;
49 | }
50 |
51 | public String getInteractionString() {
52 | return interactionString;
53 | }
54 |
55 | public long getTime() {
56 | return time;
57 | }
58 |
59 | public String getClient() {
60 | return client;
61 | }
62 |
63 | public JsonObject getOriginalObject() {
64 | return originalObject;
65 | }
66 |
67 | public String getInteractionStringWithDomain(){
68 | if(this.context != null) return String.format("%s.%s", this.interactionString,
69 | context.getCollaboratorServer().getCollaboratorAddress());
70 | else return this.interactionString;
71 | }
72 |
73 | @Override
74 | public String toString() {
75 | return originalObject.toString();
76 | }
77 |
78 | public JComponent buildInteractionInfoPanel(){
79 | return new PanelBuilder(null).build(new JComponent[][]{
80 | new JComponent[]{new JLabel("Type: "), new JLabel(this.interactionType.toString())},
81 | new JComponent[]{new JLabel("Interaction String: "), new SelectableLabel(getInteractionStringWithDomain())},
82 | new JComponent[]{new JLabel("Client: "), new JLabel(this.client)},
83 | new JComponent[]{new JLabel("Time: "), new JLabel(new Date(this.time).toString())},
84 | }, new int[][]{
85 | new int[]{0, 100},
86 | new int[]{0, 100},
87 | new int[]{0, 100},
88 | new int[]{0, 100}
89 | }, Alignment.TOPLEFT, 1.0, 0.0);
90 | }
91 |
92 | public static Interaction parseFromJson(CollaboratorContext collaboratorContext, JsonObject json){
93 | switch(json.getAsJsonObject().get("protocol").getAsString().toUpperCase()){
94 | case "DNS": return new DNSInteraction(collaboratorContext, json);
95 | case "HTTP": return new HTTPInteraction(collaboratorContext, json, false);
96 | case "HTTPS": return new HTTPInteraction(collaboratorContext, json, true);
97 | case "SMTP": return new SMTPInteraction(collaboratorContext, json, false);
98 | case "SMTPS": return new SMTPInteraction(collaboratorContext, json, true);
99 | }
100 | return null;
101 | }
102 |
103 | public static Interaction parseFromJson(JsonObject json){
104 | switch(json.getAsJsonObject().get("protocol").getAsString().toUpperCase()){
105 | case "DNS": return new DNSInteraction(json);
106 | case "HTTP": return new HTTPInteraction(json, false);
107 | case "HTTPS": return new HTTPInteraction(json, true);
108 | case "SMTP": return new SMTPInteraction(json, false);
109 | case "SMTPS": return new SMTPInteraction(json, true);
110 | }
111 | return null;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/InteractionSerializer.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import com.google.gson.*;
4 | import org.apache.http.protocol.HTTP;
5 |
6 | import java.lang.reflect.Type;
7 |
8 | public class InteractionSerializer implements JsonDeserializer, JsonSerializer {
9 | @Override
10 | public Interaction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
11 | switch(((JsonObject) json).get("protocol").getAsString().toUpperCase()){
12 | case "DNS": return new DNSInteraction(json.getAsJsonObject());
13 | case "HTTP": return new HTTPInteraction(json.getAsJsonObject(), false);
14 | case "HTTPS": return new HTTPInteraction(json.getAsJsonObject(), true);
15 | case "SMTP": return new SMTPInteraction(json.getAsJsonObject(), false);
16 | case "SMTPS": return new SMTPInteraction(json.getAsJsonObject(), true);
17 | }
18 | return null;
19 | }
20 |
21 | @Override
22 | public JsonElement serialize(Interaction src, Type typeOfSrc, JsonSerializationContext context) {
23 | return src.getOriginalObject();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/context/SMTPInteraction.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.context;
2 |
3 | import burp.IMessageEditor;
4 | import com.coreyd97.BurpExtenderUtilities.Alignment;
5 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
6 | import com.google.gson.JsonElement;
7 | import com.google.gson.JsonObject;
8 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
9 | import com.nccgroup.collaboratorplusplus.utilities.SelectableLabel;
10 | import org.bouncycastle.util.encoders.Base64;
11 |
12 | import javax.swing.*;
13 | import java.awt.*;
14 | import java.util.ArrayList;
15 | import java.util.stream.Collectors;
16 |
17 | public class SMTPInteraction extends Interaction{
18 |
19 | String sender;
20 | ArrayList recipients;
21 | String message;
22 | String conversation;
23 |
24 | protected SMTPInteraction(JsonObject interaction, boolean isSSL) {
25 | this(null, interaction, isSSL);
26 | }
27 |
28 | protected SMTPInteraction(CollaboratorContext context, JsonObject interaction, boolean isSSL) {
29 | super(context, isSSL ? InteractionType.SMTPS : InteractionType.SMTP, interaction);
30 |
31 | //Parse SMTP specific properties here
32 | sender = new String(Base64.decode(interaction.getAsJsonObject("data").get("sender").getAsString()));
33 | message = new String(Base64.decode(interaction.getAsJsonObject("data").get("message").getAsString()));
34 | conversation = new String(Base64.decode(interaction.getAsJsonObject("data").get("conversation").getAsString()));
35 | recipients = new ArrayList<>();
36 | for (JsonElement jsonElement : interaction.getAsJsonObject("data").get("recipients").getAsJsonArray()) {
37 | String recipient = new String(Base64.decode(jsonElement.getAsString()));
38 | recipients.add(recipient);
39 | }
40 | }
41 |
42 | public String getSender() {
43 | return sender;
44 | }
45 |
46 | public ArrayList getRecipients() {
47 | return recipients;
48 | }
49 |
50 | public String getMessage() {
51 | return message;
52 | }
53 |
54 | public String getConversation() {
55 | return conversation;
56 | }
57 |
58 | @Override
59 | public JComponent buildInteractionInfoPanel() {
60 | JComponent basePanel = super.buildInteractionInfoPanel();
61 | IMessageEditor editor = CollaboratorPlusPlus.callbacks.createMessageEditor(null, false);
62 | editor.setMessage(this.conversation.getBytes(), true);
63 |
64 | return new PanelBuilder(null).build(new Component[][]{
65 | new Component[]{basePanel, basePanel},
66 | new Component[]{new JSeparator(JSeparator.HORIZONTAL), new JSeparator(JSeparator.HORIZONTAL)},
67 | new Component[]{new JLabel("Sender: "), new SelectableLabel(sender)},
68 | new Component[]{new JLabel("Recipients: "), new SelectableLabel(this.recipients.stream().collect(Collectors.joining("; ")))},
69 | new Component[]{editor.getComponent(), editor.getComponent()},
70 | }, new int[][]{
71 | new int[]{0, 0},
72 | new int[]{0, 0},
73 | new int[]{0, 0},
74 | new int[]{0, 1},
75 | new int[]{0, 999},
76 | }, Alignment.FILL, 1.0, 1.0);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/exception/AuthenticationException.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.exception;
2 |
3 | public class AuthenticationException extends CollaboratorPollingException {
4 |
5 | public AuthenticationException(String message){
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/exception/CollaboratorPollingException.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.exception;
2 |
3 | public class CollaboratorPollingException extends Exception {
4 | public CollaboratorPollingException(String message){
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/exception/CollaboratorSSLException.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.exception;
2 |
3 | public class CollaboratorSSLException extends CollaboratorPollingException {
4 |
5 | public CollaboratorSSLException(String message){
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/exception/InvalidResponseException.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.exception;
2 |
3 | public class InvalidResponseException extends CollaboratorPollingException {
4 |
5 | public InvalidResponseException(String message){
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/exception/InvalidSecretException.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.exception;
2 |
3 | public class InvalidSecretException extends CollaboratorPollingException {
4 |
5 | public InvalidSecretException(String message){
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/interactionhistory/ContextInformationPanel.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.interactionhistory;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Alignment;
4 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
5 | import com.coreyd97.BurpExtenderUtilities.Preferences;
6 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorEventAdapter;
7 | import com.nccgroup.collaboratorplusplus.extension.context.ContextManager;
8 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
9 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
10 | import com.nccgroup.collaboratorplusplus.utilities.SelectableLabel;
11 |
12 | import javax.swing.*;
13 | import java.awt.*;
14 |
15 | class ContextInformationPanel extends JPanel {
16 |
17 | private final ContextManager contextManager;
18 | private final Preferences preferences;
19 |
20 | CollaboratorContext selectedContext;
21 | JTextField identifierLabel;
22 | JLabel lastPolledLabel;
23 | InteractionInfoPanel interactionInformationPanel;
24 | JButton pollNowButton;
25 | Color originalLastPolledColor;
26 |
27 | InteractionsTable interactionsTable;
28 |
29 | ContextInformationPanel(ContextManager contextManager, Preferences preferences){
30 | super(new BorderLayout());
31 | this.contextManager = contextManager;
32 | this.preferences = preferences;
33 | this.interactionsTable = new InteractionsTable(contextManager);
34 | this.interactionInformationPanel = new InteractionInfoPanel(preferences);
35 |
36 | JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
37 | splitPane.setResizeWeight(0.5);
38 | splitPane.setLeftComponent(new JScrollPane(interactionsTable));
39 | splitPane.setRightComponent(interactionInformationPanel);
40 |
41 | this.add(buildIDInfoPane(), BorderLayout.NORTH);
42 | this.add(splitPane, BorderLayout.CENTER);
43 |
44 | registerListeners();
45 | }
46 |
47 | private JComponent buildIDInfoPane(){
48 | PanelBuilder panelBuilder = new PanelBuilder(preferences);
49 | identifierLabel = new SelectableLabel("N/A");
50 | lastPolledLabel = new JLabel("N/A");
51 | pollNowButton = new JButton("Poll Now");
52 | originalLastPolledColor = lastPolledLabel.getForeground();
53 |
54 | pollNowButton.setEnabled(false);
55 | pollNowButton.addActionListener(e -> {
56 | try {
57 | contextManager.requestInteractions(selectedContext);
58 | } catch (Exception e1) {
59 | JOptionPane.showMessageDialog(this, "Could not retrieve interactions:\n"
60 | + e1.getMessage(), "Polling Error", JOptionPane.ERROR_MESSAGE);
61 | }
62 | });
63 |
64 | JLabel idTitle = new JLabel("ID: ");
65 | JLabel lpTitle = new JLabel("Last Polled: ");
66 | idTitle.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 30));
67 | lpTitle.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 30));
68 | return panelBuilder.build(new JComponent[][]{
69 | new JComponent[] {idTitle, identifierLabel, pollNowButton},
70 | new JComponent[] {lpTitle, lastPolledLabel, pollNowButton},
71 | }, new int[][]{
72 | new int[]{0, 1, 0},
73 | new int[]{0, 1, 0},
74 | new int[]{0, 100, 0}
75 | }, Alignment.CENTER, 1.0, 1.0);
76 | }
77 |
78 | void displayContext(CollaboratorContext collaboratorContext){
79 | if(collaboratorContext != null) {
80 | this.selectedContext = collaboratorContext;
81 | this.interactionsTable.setContext(collaboratorContext);
82 | this.identifierLabel.setText(collaboratorContext.getIdentifier());
83 | this.lastPolledLabel.setText(collaboratorContext.getLastPolled().toString());
84 | }else{
85 | this.selectedContext = null;
86 | this.interactionsTable.setContext(null);
87 | this.identifierLabel.setText("N/A");
88 | this.lastPolledLabel.setText("N/A");
89 | }
90 | this.interactionInformationPanel.setActiveInteraction(null);
91 | pollNowButton.setEnabled(collaboratorContext != null);
92 | }
93 |
94 | private void registerListeners(){
95 | this.interactionsTable.getSelectionModel().addListSelectionListener(e -> {
96 | int selectedRow = interactionsTable.getSelectedRow();
97 | if(selectedRow == -1) return;
98 | Interaction selectedInteraction = selectedContext.getEventAtIndex(interactionsTable.convertRowIndexToModel(selectedRow));
99 | interactionInformationPanel.setActiveInteraction(selectedInteraction);
100 | });
101 |
102 | //Update last polled text when poll request sent
103 | this.contextManager.addEventListener(new CollaboratorEventAdapter() {
104 | @Override
105 | public void onPollingRequestSent(CollaboratorContext collaboratorContext) {
106 | if(selectedContext != null && selectedContext.equals(collaboratorContext)){
107 | lastPolledLabel.setText(selectedContext.getLastPolled().toString());
108 | lastPolledLabel.setForeground(Color.ORANGE);
109 |
110 | Timer colorResetTimer = new Timer(1000, e -> lastPolledLabel.setForeground(originalLastPolledColor));
111 | colorResetTimer.setRepeats(false);
112 | colorResetTimer.start();
113 | }
114 | }
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/interactionhistory/ContextTable.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.interactionhistory;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorEventAdapter;
4 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorServer;
5 | import com.nccgroup.collaboratorplusplus.extension.context.ContextManager;
6 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
7 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
8 | import org.jdesktop.swingx.JXTreeTable;
9 | import org.jdesktop.swingx.treetable.AbstractTreeTableModel;
10 |
11 | import javax.swing.*;
12 | import javax.swing.table.DefaultTableCellRenderer;
13 | import javax.swing.table.TableColumn;
14 | import javax.swing.tree.TreePath;
15 | import java.awt.event.MouseAdapter;
16 | import java.awt.event.MouseEvent;
17 | import java.util.ArrayList;
18 |
19 | class ContextTable extends JXTreeTable {
20 |
21 | ContextManager contextManager;
22 |
23 | ContextTable(ContextManager contextManager) {
24 | this.contextManager = contextManager;
25 |
26 | ContextTreeTableModel model = new ContextTreeTableModel(this.contextManager.getCollaboratorServers());
27 | model.registerListeners();
28 | this.setTreeTableModel(model);
29 |
30 | this.setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS);
31 |
32 | DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
33 | centerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
34 | this.getColumnModel().getColumn(1).setCellRenderer(centerRenderer);
35 | this.getColumnModel().getColumn(2).setCellRenderer(centerRenderer);
36 | for (int i = 3; i < 8; i++) {
37 | TableColumn col = this.getColumnModel().getColumn(i);
38 | col.setCellRenderer(centerRenderer);
39 | col.setMinWidth(100);
40 | col.setMaxWidth(100);
41 | }
42 |
43 | this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
44 | this.setAutoCreateRowSorter(true);
45 | this.addContextMenuListener();
46 | }
47 |
48 | private void addContextMenuListener() {
49 | this.addMouseListener(new MouseAdapter() {
50 | @Override
51 | public void mouseReleased(MouseEvent e) {
52 | if (SwingUtilities.isRightMouseButton(e)) {
53 | ContextTable table = ContextTable.this;
54 | ContextManager contextManager = table.contextManager;
55 |
56 | int row = table.rowAtPoint(e.getPoint());
57 | table.getSelectionModel().setSelectionInterval(row, row);
58 | TreePath selectedPath = table.getPathForRow(row);
59 |
60 | JPopupMenu popupMenu = new JPopupMenu();
61 | JMenuItem headerItem;
62 | JMenuItem exportMenuItem = new JMenuItem("Export - Not Implemented");
63 | JMenuItem deleteMenuItem = new JMenuItem("Delete");
64 |
65 | if (selectedPath.getLastPathComponent() instanceof CollaboratorServer) {
66 | CollaboratorServer collaboratorServer = (CollaboratorServer) selectedPath.getLastPathComponent();
67 | headerItem = new JMenuItem((collaboratorServer).getCollaboratorAddress());
68 | //Now add event listeners to delete and export buttons
69 | deleteMenuItem.addActionListener(e1 -> {
70 | String warningMessage = String.format("Are you sure you wish to delete all contexts" +
71 | " associated with this server?\n\nServer: %s\nContexts: %d\nTotal Interactions: %d",
72 | collaboratorServer.getCollaboratorAddress(),
73 | collaboratorServer.getContexts().size(),
74 | collaboratorServer.getTotalInteractions());
75 | int result = JOptionPane.showConfirmDialog(table, warningMessage, "Are you sure?", JOptionPane.YES_NO_OPTION);
76 | if (result == JOptionPane.YES_OPTION) {
77 | contextManager.removeCollaboratorServer(collaboratorServer);
78 | }
79 | });
80 | } else if (selectedPath.getLastPathComponent() instanceof CollaboratorContext) {
81 | CollaboratorContext context = (CollaboratorContext) selectedPath.getLastPathComponent();
82 | headerItem = new JMenuItem(context.getIdentifier());
83 | //Now add event listeners to delete and export buttons
84 | deleteMenuItem.addActionListener(e1 -> {
85 | int result = JOptionPane.showConfirmDialog(table, "Are you sure you wish to delete this context?" +
86 | "\n\nIdentifier: " + context.getIdentifier() +
87 | "\nInteractions: " + context.getInteractionEvents().size(), "Are you sure?", JOptionPane.YES_NO_OPTION);
88 | if (result == JOptionPane.YES_OPTION) {
89 | contextManager.removeCollaboratorContext(context);
90 | }
91 | });
92 |
93 | } else {
94 | return;
95 | }
96 |
97 | headerItem.setEnabled(false);
98 | popupMenu.add(headerItem);
99 | popupMenu.add(new JPopupMenu.Separator());
100 | // popupMenu.add(exportMenuItem);
101 | popupMenu.add(deleteMenuItem);
102 | popupMenu.show(ContextTable.this, e.getX(), e.getY());
103 | }
104 | }
105 | });
106 | }
107 |
108 | private class ContextTreeTableModel extends AbstractTreeTableModel {
109 |
110 | ArrayList servers;
111 |
112 | ContextTreeTableModel(ArrayList servers) {
113 | super(new Object());
114 | this.servers = servers;
115 |
116 | }
117 |
118 |
119 | @Override
120 | public int getColumnCount() {
121 | return 8;
122 | }
123 |
124 | @Override
125 | public Class> getColumnClass(int columnIndex) {
126 | return columnIndex > 2 ? Integer.class : String.class;
127 | }
128 |
129 | @Override
130 | public String getColumnName(int column) {
131 | switch (column) {
132 | case 0:
133 | return "Context";
134 | case 1:
135 | return "Last Polled";
136 | case 2:
137 | return "Interactions";
138 | case 3:
139 | return "DNS";
140 | case 4:
141 | return "HTTP";
142 | case 5:
143 | return "HTTPS";
144 | case 6:
145 | return "SMTP";
146 | case 7:
147 | return "SMTPS";
148 | }
149 | return null;
150 | }
151 |
152 |
153 | @Override
154 | public Object getValueAt(Object node, int columnIndex) {
155 | if (node instanceof CollaboratorServer) {
156 | switch (columnIndex) {
157 | case 0:
158 | return ((CollaboratorServer) node).getCollaboratorAddress();
159 | case 2:
160 | return ((CollaboratorServer) node).getTotalInteractions();
161 | case 3:
162 | return ((CollaboratorServer) node).getDnsEventCount();
163 | case 4:
164 | return ((CollaboratorServer) node).getHttpEventCount();
165 | case 5:
166 | return ((CollaboratorServer) node).getHttpsEventCount();
167 | case 6:
168 | return ((CollaboratorServer) node).getSmtpEventCount();
169 | case 7:
170 | return ((CollaboratorServer) node).getSmtpsEventCount();
171 | default:
172 | return null;
173 | }
174 | }
175 | if (node instanceof CollaboratorContext) {
176 | switch (columnIndex) {
177 | case 0:
178 | return ((CollaboratorContext) node).getIdentifier();
179 | case 1:
180 | return ((CollaboratorContext) node).getLastPolled();
181 | case 2:
182 | return ((CollaboratorContext) node).getInteractionEvents().size();
183 | case 3:
184 | return ((CollaboratorContext) node).getDNSInteractionCount();
185 | case 4:
186 | return ((CollaboratorContext) node).getHttpInteractionCount();
187 | case 5:
188 | return ((CollaboratorContext) node).getHttpsInteractionCount();
189 | case 6:
190 | return ((CollaboratorContext) node).getSMTPInteractionCount();
191 | case 7:
192 | return ((CollaboratorContext) node).getSMTPSInteractionCount();
193 | }
194 | }
195 | return null;
196 | }
197 |
198 | @Override
199 | public Object getChild(Object o, int i) {
200 | if (o == this.getRoot()) {
201 | if (i == -1) return null;
202 | return contextManager.getCollaboratorServers().get(i);
203 | }
204 | if (o instanceof CollaboratorServer) {
205 | if (i == -1) return null;
206 | return ((CollaboratorServer) o).getContexts().get(i);
207 | }
208 | return null;
209 | }
210 |
211 | @Override
212 | public int getChildCount(Object o) {
213 | if (o == this.getRoot())
214 | return contextManager.getCollaboratorServers().size();
215 | if (o instanceof CollaboratorServer)
216 | return ((CollaboratorServer) o).getContexts().size();
217 | return 0;
218 | }
219 |
220 | @Override
221 | public int getIndexOfChild(Object o, Object o1) {
222 | if (o == null) return contextManager.getCollaboratorServers().indexOf(o1);
223 | if (o instanceof CollaboratorServer) return ((CollaboratorServer) o).getContexts().indexOf(o1);
224 | return -1;
225 | }
226 |
227 | @Override
228 | public boolean isLeaf(Object node) {
229 | return node instanceof CollaboratorContext;
230 | }
231 |
232 | private void registerListeners() {
233 | contextManager.addEventListener(new CollaboratorEventAdapter() {
234 | @Override
235 | public void onCollaboratorServerRegistered(CollaboratorServer collaboratorServer, int index) {
236 | modelSupport.fireChildAdded(new TreePath(getRoot()), index, collaboratorServer);
237 | }
238 |
239 | @Override
240 | public void onCollaboratorServerRemoved(CollaboratorServer collaboratorServer, int index) {
241 | modelSupport.fireChildRemoved(new TreePath(getRoot()), index, collaboratorServer);
242 | }
243 |
244 | @Override
245 | public void onCollaboratorContextRegistered(CollaboratorContext collaboratorContext, int index) {
246 | modelSupport.fireChildAdded(new TreePath(new Object[]{getRoot(), collaboratorContext.getCollaboratorServer()}),
247 | index, collaboratorContext);
248 | }
249 |
250 | @Override
251 | public void onCollaboratorContextRemoved(CollaboratorContext collaboratorContext, int index) {
252 | modelSupport.fireChildRemoved(new TreePath(new Object[]{getRoot(), collaboratorContext.getCollaboratorServer()}), index, collaboratorContext);
253 | }
254 |
255 | @Override
256 | public void onPollingRequestSent(CollaboratorContext collaboratorContext) {
257 | modelSupport.fireChildChanged(new TreePath(new Object[]{getRoot(), collaboratorContext.getCollaboratorServer()}),
258 | collaboratorContext.getCollaboratorServer().getContexts().indexOf(collaboratorContext),
259 | collaboratorContext);
260 | }
261 |
262 | @Override
263 | public void onPollingResponseReceived(CollaboratorContext collaboratorContext, ArrayList interactions) {
264 | modelSupport.firePathChanged(new TreePath(new Object[]{collaboratorContext.getCollaboratorServer()}));
265 | }
266 | });
267 | }
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/interactionhistory/HistoryUI.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.interactionhistory;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Preferences;
4 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
5 | import com.nccgroup.collaboratorplusplus.extension.context.ContextManager;
6 |
7 | import javax.swing.*;
8 | import java.awt.*;
9 |
10 | public class HistoryUI extends JSplitPane {
11 |
12 | private final ContextManager contextManager;
13 | private final Preferences preferences;
14 | private ContextTable contextTable;
15 | private ContextInformationPanel contextInformationPanel;
16 |
17 | public HistoryUI(ContextManager contextManager, Preferences preferences){
18 | super(VERTICAL_SPLIT);
19 | this.contextManager = contextManager;
20 | this.preferences = preferences;
21 | buildMainPanel();
22 | }
23 |
24 | private void buildMainPanel(){
25 | contextTable = new ContextTable(contextManager);
26 | JScrollPane contextScrollPane = new JScrollPane(contextTable);
27 | contextScrollPane.setMinimumSize(new Dimension(0, 75));
28 | contextInformationPanel = new ContextInformationPanel(contextManager, preferences);
29 |
30 | contextTable.getSelectionModel().addListSelectionListener(e -> {
31 | int selectedRow = contextTable.getSelectedRow();
32 | if(selectedRow == -1) {
33 | SwingUtilities.invokeLater(() -> contextInformationPanel.displayContext(null));
34 | }else{
35 | Object selectedComponent = contextTable.getPathForRow(selectedRow).getLastPathComponent();
36 | if(selectedComponent instanceof CollaboratorContext) {
37 | SwingUtilities.invokeLater(() ->
38 | contextInformationPanel.displayContext((CollaboratorContext) selectedComponent));
39 | }else{
40 | SwingUtilities.invokeLater(() -> contextInformationPanel.displayContext(null));
41 | }
42 | }
43 | });
44 |
45 | this.setTopComponent(contextScrollPane);
46 | this.setBottomComponent(contextInformationPanel);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/interactionhistory/InteractionInfoPanel.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.interactionhistory;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Alignment;
4 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
5 | import com.coreyd97.BurpExtenderUtilities.Preferences;
6 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
7 | import com.nccgroup.collaboratorplusplus.extension.context.DNSInteraction;
8 | import com.nccgroup.collaboratorplusplus.extension.context.HTTPInteraction;
9 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
10 | import com.nccgroup.collaboratorplusplus.extension.context.SMTPInteraction;
11 |
12 | import javax.swing.*;
13 | import java.awt.*;
14 | import java.util.Date;
15 |
16 | public class InteractionInfoPanel extends JTabbedPane {
17 |
18 | private final Preferences preferences;
19 | private Interaction selectedInteraction;
20 | private JPanel infoPanel;
21 | private JTextArea rawArea;
22 | private JLabel noInteractionLabel = new JLabel("No interaction selected.");
23 |
24 | public InteractionInfoPanel(Preferences preferences){
25 | this.preferences = preferences;
26 | this.infoPanel = createInfoInnerPanel();
27 | this.infoPanel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
28 |
29 | this.addTab("Info", createInfoWrapperPanel(this.infoPanel));
30 | this.addTab("Raw", createRawPanel());
31 | }
32 |
33 | private JPanel createInfoInnerPanel(){
34 | JPanel panel = new JPanel(new BorderLayout());
35 | panel.add(noInteractionLabel, BorderLayout.CENTER);
36 |
37 | return panel;
38 | }
39 |
40 | private JPanel createInfoWrapperPanel(JPanel innerPanel){
41 | return new PanelBuilder(null).build(innerPanel, Alignment.FILL, 1.0, 1.0);
42 | }
43 |
44 | public JComponent createRawPanel(){
45 | rawArea = new JTextArea();
46 | rawArea.setEditable(false);
47 | rawArea.setWrapStyleWord(true);
48 | rawArea.setLineWrap(true);
49 | return new JScrollPane(rawArea);
50 | }
51 |
52 | public void setActiveInteraction(Interaction interaction){
53 | this.selectedInteraction = interaction;
54 | if(selectedInteraction == null){
55 | rawArea.setText(null);
56 | infoPanel.removeAll();
57 | infoPanel.add(noInteractionLabel, BorderLayout.CENTER);
58 | infoPanel.revalidate();
59 | infoPanel.repaint();
60 | }else {
61 | rawArea.setText(interaction.getOriginalObject().toString());
62 | infoPanel.removeAll();
63 | infoPanel.add(this.selectedInteraction.buildInteractionInfoPanel(), BorderLayout.CENTER);
64 | infoPanel.revalidate();
65 | infoPanel.repaint();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/interactionhistory/InteractionsTable.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.interactionhistory;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorEventAdapter;
4 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
5 | import com.nccgroup.collaboratorplusplus.extension.context.CollaboratorContext;
6 | import com.nccgroup.collaboratorplusplus.extension.context.ContextManager;
7 | import com.nccgroup.collaboratorplusplus.extension.context.Interaction;
8 | import org.bouncycastle.util.Arrays;
9 |
10 | import javax.swing.*;
11 | import javax.swing.table.AbstractTableModel;
12 | import javax.swing.table.TableCellRenderer;
13 | import java.awt.*;
14 | import java.awt.event.MouseAdapter;
15 | import java.awt.event.MouseEvent;
16 | import java.text.SimpleDateFormat;
17 | import java.util.ArrayList;
18 | import java.util.UUID;
19 |
20 | public class InteractionsTable extends JTable {
21 | static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat();
22 |
23 | private final ContextManager contextManager;
24 | CollaboratorContext collaboratorContext;
25 |
26 | InteractionsTable(ContextManager contextManager) {
27 | this.setModel(new InteractionsTableModel());
28 | this.setAutoCreateRowSorter(true);
29 | this.contextManager = contextManager;
30 |
31 | this.registerCollaboratorEventListeners();
32 |
33 | this.getSelectionModel().addListSelectionListener(e -> {
34 | if (collaboratorContext != null && collaboratorContext.getRecentInteractions() != null && this.getSelectedRows().length > 0) {
35 | for (int selectedRow : this.getSelectedRows()) {
36 | UUID selectedUUID = collaboratorContext.getInteractionIds().get(convertRowIndexToModel(selectedRow));
37 | collaboratorContext.getRecentInteractions().remove(selectedUUID);
38 | }
39 | }
40 | });
41 |
42 | this.addMouseListener(new MouseAdapter() {
43 | @Override
44 | public void mouseClicked(MouseEvent e) {
45 | if(SwingUtilities.isRightMouseButton(e)){
46 | int[] selectedRows = InteractionsTable.this.getSelectedRows();
47 | if(selectedRows.length == 0) return;
48 | JPopupMenu popupMenu = new JPopupMenu();
49 | JMenuItem header = new JMenuItem(selectedRows.length + " interactions");
50 | header.setEnabled(false);
51 | popupMenu.add(header);
52 | popupMenu.add(new JPopupMenu.Separator());
53 |
54 | //TODO Add event export and delete
55 |
56 | // popupMenu.show(InteractionsTable.this, e.getX(), e.getY());
57 | }
58 | }
59 | });
60 | }
61 |
62 | private void registerCollaboratorEventListeners() {
63 | this.contextManager.addEventListener(new CollaboratorEventAdapter() {
64 | @Override
65 | public void onPollingResponseReceived(CollaboratorContext collaboratorContext, ArrayList interactions) {
66 | SwingUtilities.invokeLater(() -> {
67 | if (interactions.size() > 0 && InteractionsTable.this.collaboratorContext != null
68 | && InteractionsTable.this.collaboratorContext.equals(collaboratorContext)) {
69 | int initialSize = collaboratorContext.getInteractionEvents().size() - interactions.size();
70 |
71 | try {
72 | ((AbstractTableModel) InteractionsTable.this.getModel()).fireTableRowsInserted(
73 | initialSize, InteractionsTable.this.getModel().getRowCount() - 1);
74 | } catch (Exception e) {
75 | e.printStackTrace();
76 | CollaboratorPlusPlus.logger.error(e);
77 | }
78 | }
79 | });
80 | }
81 | });
82 | }
83 |
84 | //Sneak in row coloring just before rendering the cell.
85 | //Highlight recent interactions which have not yet been viewed.
86 | @Override
87 | public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
88 | Component c = super.prepareRenderer(renderer, row, column);
89 |
90 | if (Arrays.contains(this.getSelectedRows(), row)) {
91 | c.setForeground(this.getSelectionForeground());
92 | c.setBackground(this.getSelectionBackground());
93 | return c;
94 | }
95 |
96 | UUID targetUUID = collaboratorContext.getInteractionIds().get(convertRowIndexToModel(row));
97 | if (collaboratorContext.getRecentInteractions() != null && collaboratorContext.getRecentInteractions().contains(targetUUID)) {
98 | c.setBackground(Color.ORANGE);
99 | c.setForeground(Color.WHITE);
100 | } else {
101 | c.setForeground(this.getForeground());
102 | c.setBackground(this.getBackground());
103 | }
104 |
105 | return c;
106 | }
107 |
108 |
109 | void setContext(CollaboratorContext collaboratorContext) {
110 | if (this.collaboratorContext != collaboratorContext) {
111 | this.collaboratorContext = collaboratorContext;
112 | ((AbstractTableModel) this.getModel()).fireTableDataChanged();
113 | }
114 | }
115 |
116 | private class InteractionsTableModel extends AbstractTableModel {
117 |
118 | @Override
119 | public int getRowCount() {
120 | if (collaboratorContext == null) return 0;
121 | return collaboratorContext.getInteractionEvents().size();
122 | }
123 |
124 | @Override
125 | public int getColumnCount() {
126 | return 4;
127 | }
128 |
129 | @Override
130 | public String getColumnName(int column) {
131 | switch (column) {
132 | case 0:
133 | return "Interaction String";
134 | case 1:
135 | return "Protocol";
136 | case 2:
137 | return "Time";
138 | case 3:
139 | return "Client";
140 | }
141 | return null;
142 | }
143 |
144 | @Override
145 | public Object getValueAt(int rowIndex, int columnIndex) {
146 | if (collaboratorContext == null || rowIndex >= collaboratorContext.getInteractionEvents().size()) return null;
147 | Interaction interaction = collaboratorContext.getEventAtIndex(rowIndex);
148 |
149 | switch (columnIndex) {
150 | case 0:
151 | return interaction.getInteractionString();
152 | case 1:
153 | return interaction.getInteractionType();
154 | case 2:
155 | return DATE_FORMAT.format(interaction.getTime());
156 | case 3:
157 | return interaction.getClient();
158 | }
159 |
160 | return null;
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/ui/AboutUI.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.ui;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Alignment;
4 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
5 | import com.coreyd97.BurpExtenderUtilities.Preferences;
6 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
7 | import com.nccgroup.collaboratorplusplus.extension.Globals;
8 | import com.nccgroup.collaboratorplusplus.utilities.NoTextSelectionCaret;
9 | import com.nccgroup.collaboratorplusplus.utilities.WrappedTextPane;
10 |
11 | import javax.swing.*;
12 | import javax.swing.text.Style;
13 | import javax.swing.text.StyleConstants;
14 | import javax.swing.text.StyledDocument;
15 | import java.awt.*;
16 | import java.awt.event.ComponentAdapter;
17 | import java.awt.event.ComponentEvent;
18 | import java.awt.event.MouseAdapter;
19 | import java.awt.event.MouseEvent;
20 | import java.awt.image.BufferedImage;
21 | import java.io.IOException;
22 | import java.io.PrintWriter;
23 | import java.io.StringWriter;
24 | import java.net.URI;
25 | import java.net.URISyntaxException;
26 | import java.net.URL;
27 |
28 | public class AboutUI extends JPanel {
29 |
30 | private final CollaboratorPlusPlus collaboratorPlusPlus;
31 | private final Preferences preferences;
32 | private JComponent panel;
33 |
34 | public AboutUI(CollaboratorPlusPlus collaboratorPlusPlus){
35 | this.setLayout(new BorderLayout());
36 | this.collaboratorPlusPlus = collaboratorPlusPlus;
37 | this.preferences = collaboratorPlusPlus.getPreferences();
38 |
39 | this.panel = buildMainPanel();
40 | this.add(panel, BorderLayout.NORTH);
41 | this.setMinimumSize(panel.getSize());
42 | this.addMouseListener(new MouseAdapter() {
43 | @Override
44 | public void mouseClicked(MouseEvent e) {
45 | if(e.getButton() == MouseEvent.BUTTON2){
46 | AboutUI.this.removeAll();
47 | panel = buildMainPanel();
48 | AboutUI.this.add(panel, BorderLayout.NORTH);
49 | AboutUI.this.setMinimumSize(panel.getSize());
50 | AboutUI.this.revalidate();
51 | AboutUI.this.repaint();
52 | }
53 | }
54 | });
55 | }
56 |
57 | private JComponent buildMainPanel(){
58 | PanelBuilder panelBuilder = new PanelBuilder(preferences);
59 |
60 | JLabel headerLabel = new JLabel("Collaborator++");
61 | Font font = this.getFont().deriveFont(32f).deriveFont(this.getFont().getStyle() | Font.BOLD);
62 | headerLabel.setFont(font);
63 | headerLabel.setHorizontalAlignment(SwingConstants.CENTER);
64 |
65 |
66 | JLabel subtitle = new JLabel("Enhanced client for secure collaborator interaction");
67 | Font subtitleFont = subtitle.getFont().deriveFont(16f).deriveFont(subtitle.getFont().getStyle() | Font.ITALIC);
68 | subtitle.setFont(subtitleFont);
69 | subtitle.setHorizontalAlignment(SwingConstants.CENTER);
70 |
71 | JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL);
72 | JPanel separatorPadding = new JPanel();
73 | separatorPadding.setBorder(BorderFactory.createEmptyBorder(0,0,7,0));
74 |
75 | BufferedImage twitterImage = loadImage("TwitterLogo.png");
76 | JButton twitterButton;
77 | if(twitterImage != null){
78 | twitterButton = new JButton("Follow me (@CoreyD97) on Twitter", new ImageIcon(scaleImageToWidth(twitterImage, 20)));
79 | twitterButton.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
80 | twitterButton.setIconTextGap(7);
81 | }else{
82 | twitterButton = new JButton("Follow me (@CoreyD97) on Twitter");
83 | }
84 |
85 | twitterButton.setMaximumSize(new Dimension(0, 10));
86 |
87 | twitterButton.addActionListener(actionEvent -> {
88 | try {
89 | Desktop.getDesktop().browse(new URI(Globals.TWITTER_URL));
90 | } catch (IOException | URISyntaxException e) {}
91 | });
92 |
93 | JButton irsdlTwitterButton;
94 | if(twitterImage != null){
95 | irsdlTwitterButton = new JButton("Follow Soroush (@irsdl) on Twitter", new ImageIcon(scaleImageToWidth(twitterImage, 20)));
96 | irsdlTwitterButton.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
97 | irsdlTwitterButton.setIconTextGap(7);
98 | }else{
99 | irsdlTwitterButton = new JButton("Follow Soroush (@irsdl) on Twitter");
100 | }
101 |
102 | irsdlTwitterButton.setMaximumSize(new Dimension(0, 10));
103 |
104 | irsdlTwitterButton.addActionListener(actionEvent -> {
105 | try {
106 | Desktop.getDesktop().browse(new URI(Globals.IRSDL_TWITTER_URL));
107 | } catch (IOException | URISyntaxException e) {}
108 | });
109 |
110 |
111 | JButton nccTwitterButton;
112 | BufferedImage nccImage = loadImage("NCCGroup.png");
113 | if(nccImage != null){
114 | nccTwitterButton = new JButton("Follow NCC Group on Twitter", new ImageIcon(scaleImageToWidth(nccImage, 20)));
115 | nccTwitterButton.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
116 | nccTwitterButton.setIconTextGap(7);
117 | }else{
118 | nccTwitterButton = new JButton("Follow NCC Group on Twitter");
119 | }
120 |
121 | nccTwitterButton.addActionListener(actionEvent -> {
122 | try {
123 | Desktop.getDesktop().browse(new URI(Globals.NCC_TWITTER_URL));
124 | } catch (IOException | URISyntaxException e) {}
125 | });
126 |
127 | String githubLogoFilename = "GitHubLogo" +
128 | (UIManager.getLookAndFeel().getName().equalsIgnoreCase("darcula") ? "White" : "Black")
129 | + ".png";
130 | BufferedImage githubImage = loadImage(githubLogoFilename);
131 | JButton viewOnGithubButton;
132 | if(githubImage != null){
133 | viewOnGithubButton = new JButton("View Project on GitHub", new ImageIcon(scaleImageToWidth(githubImage, 20)));
134 | viewOnGithubButton.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
135 | viewOnGithubButton.setIconTextGap(7);
136 | }else{
137 | viewOnGithubButton = new JButton("View Project on GitHub");
138 | }
139 | viewOnGithubButton.addActionListener(actionEvent -> {
140 | try {
141 | Desktop.getDesktop().browse(new URI(Globals.GITHUB_URL));
142 | } catch (IOException | URISyntaxException e) {}
143 | });
144 |
145 |
146 | BufferedImage nccLargeImage = loadImage("NCCLarge.png");
147 | ImageIcon nccLargeImageIcon = new ImageIcon(scaleImageToWidth(nccLargeImage, 300));
148 | JLabel nccBranding = new JLabel(nccLargeImageIcon);
149 | nccBranding.addComponentListener(new ComponentAdapter() {
150 | @Override
151 | public void componentResized(ComponentEvent e) {
152 | int width = e.getComponent().getWidth();
153 | nccLargeImageIcon.setImage(scaleImageToWidth(nccLargeImage, width));
154 | }
155 | });
156 |
157 | JLabel createdBy = new JLabel("Created by: Corey Arthur ( @CoreyD97 )");
158 | createdBy.setHorizontalAlignment(SwingConstants.CENTER);
159 | createdBy.setBorder(BorderFactory.createEmptyBorder(0,0,7,0));
160 | JLabel ideaBy = new JLabel("Idea by: Soroush Dalili ( @irsdl )");
161 | ideaBy.setHorizontalAlignment(SwingConstants.CENTER);
162 | ideaBy.setBorder(BorderFactory.createEmptyBorder(0,0,7,0));
163 | JComponent creditsPanel;
164 | try {
165 | creditsPanel = panelBuilder.build(new JComponent[][]{
166 | new JComponent[]{createdBy},
167 | new JComponent[]{ideaBy},
168 | new JComponent[]{nccBranding},
169 | new JComponent[]{nccBranding}
170 | }, Alignment.FILL, 1, 1);
171 | }catch (Exception e){
172 | creditsPanel = new JLabel("Could not build Panel");
173 | }
174 |
175 | WrappedTextPane aboutContent = new WrappedTextPane();
176 | aboutContent.setLayout(new BorderLayout());
177 | aboutContent.setEditable(false);
178 | aboutContent.setOpaque(false);
179 | aboutContent.setCaret(new NoTextSelectionCaret(aboutContent));
180 |
181 | JScrollPane aboutScrollPane = new JScrollPane(aboutContent);
182 | aboutScrollPane.setBorder(null);
183 | aboutScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
184 | Style bold = aboutContent.getStyledDocument().addStyle("bold", null);
185 | StyleConstants.setBold(bold, true);
186 | Style italics = aboutContent.getStyledDocument().addStyle("italics", null);
187 | StyleConstants.setItalic(italics, true);
188 |
189 | BufferedImage explanationImageOriginal = loadImage("Explain.png");
190 | JLabel explanationImage = new JLabel(new ImageIcon(explanationImageOriginal));
191 | explanationImage.addComponentListener(new ComponentAdapter() {
192 | @Override
193 | public void componentShown(ComponentEvent e) {
194 | fitToWidth(e.getComponent().getWidth());
195 | }
196 |
197 | @Override
198 | public void componentResized(ComponentEvent e) {
199 | fitToWidth(e.getComponent().getWidth());
200 | }
201 | private void fitToWidth(int width){
202 | width = Math.min(600, width);
203 | explanationImage.setIcon(new ImageIcon(scaleImageToWidth(explanationImageOriginal, (int) Math.floor(width*0.85))));
204 | }
205 | });
206 |
207 |
208 |
209 | try {
210 | String intro = "When testing for out-of-band vulnerabilities, Collaborator has been an invaluable tool since its initial release in 2015. " +
211 | "However, some issues remain which hinder its effectiveness. While Collaborator contexts created as part of scans " +
212 | "are saved within the Burp project, there is currently no method of polling old contexts such as those created with " +
213 | "extensions such as Collaborator Everywhere or the Collaborator Client. " +
214 | "Additionally, while private Collaborator instances may be deployed, there is currently no " +
215 | "authentication mechanism to restrict usage besides limiting polling to an internal network. \n\n" +
216 | "This extension aims to aleviate those issues.\n\n";
217 |
218 | String instructions = "Instructions\n";
219 | String instructionsContent = "To use Collaborator++, simply configure the extension to target your Collaborator server and start the extension.\n" +
220 | "A local server will be started and Burp will be configured to direct all polling requests to it.\n" +
221 | "The extension will then use the options configured to make a request to the polling server on behalf of Burp. " +
222 | "This allows control of various options such as ignoring SSL issues, and allows the extension to capture " +
223 | "the Collaborator context identifiers and associated interactions for future display and manual polling.\n\n";
224 | String auth = "Authentication\n";
225 | String authInstructions = "In addition to being able to control various configuration aspects of the polling request, " +
226 | "the ability to modify the request also allows the extension to add an authentication mechanism to private collaborator instances.\n\n" +
227 | "This is achieved by using a shared secret known by both the client and server to generate a symmetric encryption key " +
228 | "which is used to encrypt communication between the client and server using AES256. " +
229 | "In addition to authentication, this also enables the usage of HTTP without loss of confidentiality should SSL not be available " +
230 | "for any reason.\n\n" +
231 | "To use authentication, ensure that the server you wish to use is running the Collaborator++ auth server component " +
232 | "and simply configure this extension to target it, making sure to set the port as configured by the server.\n" +
233 | "Details on how to setup the server component can be found on the projects GitHub page.";
234 |
235 |
236 | String[] sections = new String[]{intro, instructions, instructionsContent, auth, authInstructions};
237 | Style[] styles = new Style[]{null, bold, null, bold, null, null, italics};
238 |
239 | StyledDocument document = aboutContent.getStyledDocument();
240 | for (int i = 0; i < sections.length; i++) {
241 | String section = sections[i];
242 | document.insertString(document.getLength(), String.valueOf(section), styles[i]);
243 | }
244 |
245 | } catch (Exception e) {
246 | StringWriter writer = new StringWriter();
247 | e.printStackTrace(new PrintWriter(writer));
248 | CollaboratorPlusPlus.callbacks.printError(writer.toString());
249 | }
250 |
251 | aboutContent.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
252 | JScrollPane aboutContentScrollPane = new JScrollPane(aboutContent);
253 | aboutContentScrollPane.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
254 |
255 | JPanel paddingPanel = new JPanel();
256 | try {
257 | JPanel panel = panelBuilder.build(new JComponent[][]{
258 | new JComponent[]{headerLabel, headerLabel},
259 | new JComponent[]{subtitle, subtitle},
260 | new JComponent[]{separator, separator},
261 | new JComponent[]{separatorPadding, separatorPadding},
262 | new JComponent[]{creditsPanel, twitterButton},
263 | new JComponent[]{creditsPanel, irsdlTwitterButton},
264 | new JComponent[]{creditsPanel, nccTwitterButton},
265 | new JComponent[]{creditsPanel, viewOnGithubButton},
266 | new JComponent[]{aboutContentScrollPane, aboutContentScrollPane},
267 | new JComponent[]{explanationImage, explanationImage},
268 | // new JComponent[]{paddingPanel, paddingPanel}
269 | }, new int[][]{
270 | new int[]{1,1},
271 | new int[]{1,1},
272 | new int[]{1,1},
273 | new int[]{1,1},
274 | new int[]{1,1},
275 | new int[]{1,1},
276 | new int[]{1,1},
277 | new int[]{1,1},
278 | new int[]{200,200},
279 | new int[]{0,0},
280 | // new int[]{0,0},
281 | }, Alignment.TOPMIDDLE, 0.5, 0.9);
282 | return panel;
283 | } catch (Exception e) {
284 | return new JLabel("Failed to build credits panel :(");
285 | }
286 | }
287 |
288 | private BufferedImage loadImage(String filename){
289 | ClassLoader cldr = this.getClass().getClassLoader();
290 | URL imageURLMain = cldr.getResource(filename);
291 |
292 | if(imageURLMain != null) {
293 | Image original = new ImageIcon(imageURLMain).getImage();
294 | ImageIcon originalIcon = new ImageIcon(original);
295 | BufferedImage bufferedImage = new BufferedImage(originalIcon.getIconWidth(), originalIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
296 | Graphics2D g = (Graphics2D) bufferedImage.getGraphics();
297 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
298 | g.drawImage(originalIcon.getImage(), null, null);
299 | return bufferedImage;
300 | }
301 | return null;
302 | }
303 |
304 | private Image scaleImageToWidth(BufferedImage image, int width){
305 | int height = (int) (Math.floor((image.getHeight() * width) / (double) image.getWidth()));
306 | return image.getScaledInstance(width, height, Image.SCALE_SMOOTH);
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/ui/ConfigUI.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.ui;
2 |
3 | import com.coreyd97.BurpExtenderUtilities.Alignment;
4 | import com.coreyd97.BurpExtenderUtilities.ComponentGroup;
5 | import com.coreyd97.BurpExtenderUtilities.PanelBuilder;
6 | import com.nccgroup.collaboratorplusplus.extension.*;
7 | import org.apache.logging.log4j.Level;
8 | import org.apache.logging.log4j.LogManager;
9 | import org.apache.logging.log4j.core.LoggerContext;
10 | import org.apache.logging.log4j.core.Logger;
11 | import org.apache.logging.log4j.core.config.Configurator;
12 |
13 | import javax.swing.*;
14 | import java.awt.*;
15 | import java.awt.event.MouseAdapter;
16 | import java.awt.event.MouseEvent;
17 | import java.net.URISyntaxException;
18 |
19 | import static com.nccgroup.collaboratorplusplus.extension.Globals.*;
20 |
21 | public class ConfigUI extends JPanel implements IProxyServiceListener {
22 |
23 | private final CollaboratorPlusPlus extension;
24 | private JToggleButton startStopButton;
25 | private JSpinner localPortSpinner;
26 | private JSpinner remotePortSpinner;
27 | private JTextField collaboratorPollingField;
28 | private JTextField collaboratorLocationField;
29 | private JCheckBox sslEnabledCheckbox;
30 | private JCheckBox trustSelfSignedCheckbox;
31 | private JCheckBox hostnameVerificationCheckbox;
32 | private JCheckBox blockPublicCollaborator;
33 | private JCheckBox proxyRequestsWithBurp;
34 | private JCheckBox enableAuthentication;
35 | private JTextArea secretArea;
36 | private JLabel statusLabel;
37 | private JTextArea logArea;
38 |
39 | public ConfigUI(CollaboratorPlusPlus extension){
40 | this.setLayout(new BorderLayout());
41 | this.extension = extension;
42 | this.extension.addProxyServiceListener(this);
43 | JPanel panel = buildMainPanel();
44 | this.add(panel, BorderLayout.CENTER);
45 | this.addMouseListener(new MouseAdapter() {
46 |
47 | @Override
48 | public void mouseReleased(MouseEvent e) {
49 | if(e.getButton() != MouseEvent.BUTTON3) return;
50 | ConfigUI.this.removeAll();
51 | ConfigUI.this.add(buildMainPanel());
52 | ConfigUI.this.revalidate();
53 | ConfigUI.this.repaint();
54 | }
55 | });
56 | }
57 |
58 | public JPanel buildMainPanel(){
59 | PanelBuilder panelBuilder = new PanelBuilder(extension.getPreferences());
60 | ComponentGroup configGroup = panelBuilder.createComponentGroup("Configuration");
61 | localPortSpinner = configGroup.addPreferenceComponent(PREF_LOCAL_PORT, "Local Port");
62 | ((SpinnerNumberModel) localPortSpinner.getModel()).setMinimum(0);
63 | ((SpinnerNumberModel) localPortSpinner.getModel()).setMaximum(65535);
64 | localPortSpinner.setEditor(new JSpinner.NumberEditor(localPortSpinner, "#"));
65 | collaboratorLocationField = configGroup.addPreferenceComponent(PREF_COLLABORATOR_ADDRESS, "Collaborator Location");
66 | collaboratorPollingField = configGroup.addPreferenceComponent(PREF_POLLING_ADDRESS, "Collaborator Polling Location");
67 | remotePortSpinner = configGroup.addPreferenceComponent(PREF_POLLING_PORT, "Polling Port");
68 | ((SpinnerNumberModel) remotePortSpinner.getModel()).setMinimum(0);
69 | ((SpinnerNumberModel) remotePortSpinner.getModel()).setMaximum(65535);
70 | remotePortSpinner.setEditor(new JSpinner.NumberEditor(remotePortSpinner, "#"));
71 |
72 |
73 | sslEnabledCheckbox = panelBuilder.createPreferenceCheckBox(PREF_REMOTE_SSL_ENABLED, "Use SSL");
74 | trustSelfSignedCheckbox = panelBuilder.createPreferenceCheckBox(PREF_IGNORE_CERTIFICATE_ERRORS, "Ignore Certificate Errors");
75 | hostnameVerificationCheckbox = panelBuilder.createPreferenceCheckBox(PREF_SSL_HOSTNAME_VERIFICATION, "Enable SSL Hostname Verification");
76 | blockPublicCollaborator = panelBuilder.createPreferenceCheckBox(PREF_BLOCK_PUBLIC_COLLABORATOR, "Block Public Collaborator Server");
77 | proxyRequestsWithBurp = panelBuilder.createPreferenceCheckBox(PREF_PROXY_REQUESTS_WITH_BURP, "Proxy Polling Requests with Burp");
78 |
79 | blockPublicCollaborator.addActionListener(actionEvent -> {
80 | if(blockPublicCollaborator.isSelected()) Utilities.blockPublicCollaborator();
81 | else Utilities.unblockPublicCollaborator();
82 | });
83 | sslEnabledCheckbox.addActionListener(actionEvent -> {
84 | boolean sslEnabled = sslEnabledCheckbox.isSelected();
85 | trustSelfSignedCheckbox.setEnabled(sslEnabled);
86 | hostnameVerificationCheckbox.setEnabled(sslEnabled);
87 | });
88 | JComponent checkboxComponentsPanel;
89 | try {
90 | checkboxComponentsPanel = panelBuilder.build(new JComponent[][]{
91 | new JComponent[]{sslEnabledCheckbox, trustSelfSignedCheckbox},
92 | new JComponent[]{hostnameVerificationCheckbox, blockPublicCollaborator},
93 | }, new int[][]{
94 | new int[]{1, 1},
95 | new int[]{1, 1}
96 | }, Alignment.FILL, 1, 1);
97 | } catch (Exception e) {
98 | checkboxComponentsPanel = new JLabel("Could not build checkbox components panel");
99 | }
100 | configGroup.addComponent(checkboxComponentsPanel);
101 |
102 |
103 | //Control Panel
104 | statusLabel = new JLabel("Status: Not Running");
105 | statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
106 |
107 | startStopButton = panelBuilder.createToggleButton("Start", actionEvent -> {
108 | JToggleButton thisButton = (JToggleButton) actionEvent.getSource();
109 | new Thread(() -> {
110 | if(thisButton.isSelected()){
111 | try {
112 | this.extension.startCollaboratorProxy();
113 | } catch (URISyntaxException e) {
114 | e.printStackTrace();
115 | }
116 | }else{
117 | this.extension.shutdownProxyService();
118 | }
119 | }).start();
120 | });
121 | JComponent autoStart = panelBuilder.createPreferenceCheckBox(PREF_AUTO_START, "Start Automatically on Load");
122 | JComponent controlGroup;
123 | try{
124 | controlGroup = panelBuilder.build(new JComponent[][]{
125 | new JComponent[]{statusLabel},
126 | new JComponent[]{autoStart},
127 | new JComponent[]{startStopButton},
128 | }, new int[][]{
129 | new int[]{1},
130 | new int[]{0},
131 | new int[]{0},
132 | }, Alignment.FILL, 1.0, 1.0);
133 | controlGroup.setBorder(BorderFactory.createTitledBorder("Control"));
134 | }catch (Exception e){
135 | controlGroup = new JLabel("Could not build control panel :(");
136 | }
137 |
138 |
139 | ComponentGroup authenticationPanel = panelBuilder.createComponentGroup("Collaborator Authentication");
140 | enableAuthentication = panelBuilder.createPreferenceCheckBox(PREF_USE_AUTHENTICATION, "Enable Authentication");
141 | enableAuthentication.setBorder(BorderFactory.createEmptyBorder(0,0,10,30));
142 | enableAuthentication.addActionListener(e -> {
143 | secretArea.setEnabled(enableAuthentication.isSelected());
144 | });
145 | authenticationPanel.addComponent(enableAuthentication);
146 |
147 | ComponentGroup secretInputPanel = panelBuilder.createComponentGroup("Shared Authentication Secret");
148 | secretArea = panelBuilder.createPreferenceTextArea(PREF_SECRET);
149 | secretArea.setLineWrap(true);
150 | secretArea.setEnabled(extension.getPreferences().getSetting(PREF_USE_AUTHENTICATION));
151 | JScrollPane secretScrollPane = new JScrollPane(secretArea);
152 | secretScrollPane.setBorder(BorderFactory.createEmptyBorder());
153 | secretInputPanel.addComponent(secretScrollPane);
154 |
155 | JPanel authenticationWrapperPanel = panelBuilder.build(new Component[][]{
156 | new Component[]{authenticationPanel, secretInputPanel}
157 | }, new int[][]{
158 | new int[]{0, 1}
159 | }, Alignment.FILL, 1.0, 1.0);
160 |
161 | ComponentGroup logGroup = panelBuilder.createComponentGroup("Message Log");
162 | logArea = new JTextArea();
163 | logArea.setLineWrap(true);
164 | logArea.setWrapStyleWord(true);
165 | logArea.setBorder(null);
166 | logArea.setEditable(false);
167 | logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, logArea.getFont().getSize()));
168 | JTextAreaAppender.addLog4j2TextAreaAppender(logArea);
169 |
170 | JScrollPane logScrollPane = new JScrollPane(logArea);
171 | logScrollPane.setBorder(BorderFactory.createLineBorder(Color.ORANGE));
172 | logScrollPane.setBorder(null);
173 | logScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
174 | JPanel logLevelPanel = new JPanel(new GridLayout(1,2));
175 | JComboBox logLevelSelector = new JComboBox<>(new Level[]{Level.INFO, Level.DEBUG});
176 | logLevelSelector.setSelectedItem(LogManager.getRootLogger().getLevel());
177 | logLevelSelector.addActionListener(e -> {
178 | Level selectedLevel = (Level) logLevelSelector.getSelectedItem();
179 | extension.getPreferences().setSetting(PREF_LOG_LEVEL, selectedLevel);
180 | ((LoggerContext) LogManager.getContext(false))
181 | .getLogger(EXTENSION_NAME).setLevel(selectedLevel);
182 | });
183 | logLevelPanel.add(new JLabel("Log Level: "));
184 | logLevelPanel.add(logLevelSelector);
185 | JButton clearButton = panelBuilder.createButton("Clear Logs", actionEvent -> {
186 | logArea.setText("");
187 | });
188 |
189 | GridBagConstraints logPanelGbc = logGroup.generateNextConstraints();
190 | logPanelGbc.weighty = 0;
191 | logGroup.addComponent(logLevelPanel, logPanelGbc);
192 | logPanelGbc = logGroup.generateNextConstraints();
193 | logPanelGbc.weighty = 0;
194 | logGroup.addComponent(new JSeparator(JSeparator.HORIZONTAL), logPanelGbc);
195 | logGroup.addComponent(logScrollPane, logGroup.generateNextConstraints());
196 | logPanelGbc = logGroup.generateNextConstraints();
197 | logPanelGbc.weighty = 0;
198 | logGroup.addComponent(clearButton, logPanelGbc);
199 |
200 | try {
201 | return panelBuilder.build(
202 | new JComponent[][]{
203 | new JComponent[]{configGroup, controlGroup},
204 | new JComponent[]{authenticationWrapperPanel, authenticationWrapperPanel},
205 | new JComponent[]{logGroup, logGroup},
206 | }, new int[][]{
207 | new int[]{0, 0},
208 | new int[]{0, 0},
209 | new int[]{1 ,1},
210 | }, Alignment.TOPMIDDLE, 1, 1);
211 | } catch (Exception e) {
212 | e.printStackTrace();
213 | JLabel error = new JLabel("Could not build the panel! :(");
214 | JPanel panel = new JPanel();
215 | panel.add(error);
216 | return panel;
217 | }
218 | }
219 |
220 | @Override
221 | public void beforeStartup() {
222 | SwingUtilities.invokeLater(() -> {
223 | startStopButton.setText("Starting...");
224 | startStopButton.setEnabled(false);
225 | startStopButton.setSelected(true);
226 |
227 | //Disable all other controls
228 | enableControls(false);
229 | });
230 | }
231 |
232 | @Override
233 | public void onStartupFail(String message) {
234 | SwingUtilities.invokeLater(() -> {
235 | startStopButton.setText("Start");
236 | startStopButton.setSelected(false);
237 | startStopButton.setEnabled(true);
238 |
239 | //Enable all other controls
240 | enableControls(true);
241 |
242 | statusLabel.setText("Status: Not Running");
243 | });
244 | }
245 |
246 | @Override
247 | public void onStartupSuccess(String message) {
248 | SwingUtilities.invokeLater(() -> {
249 | startStopButton.setText("Stop");
250 | startStopButton.setSelected(true);
251 | statusLabel.setText("Status: Listening on port " + localPortSpinner.getValue());
252 |
253 | //Disable all other controls
254 | enableControls(false);
255 | startStopButton.setEnabled(true);
256 | this.revalidate();
257 | this.repaint();
258 | });
259 | }
260 |
261 | @Override
262 | public void onShutdown() {
263 | SwingUtilities.invokeLater(() -> {
264 | statusLabel.setText("Status: Not Running");
265 |
266 | //Reenable other controls
267 | //Disable all other controls
268 | enableControls(true);
269 | startStopButton.setEnabled(true);
270 | startStopButton.setSelected(false);
271 | startStopButton.setText("Start");
272 | this.revalidate();
273 | this.repaint();
274 | });
275 | }
276 |
277 | private void enableControls(boolean enabled){
278 | localPortSpinner.setEnabled(enabled);
279 | collaboratorLocationField.setEnabled(enabled);
280 | collaboratorPollingField.setEnabled(enabled);
281 | remotePortSpinner.setEnabled(enabled);
282 | sslEnabledCheckbox.setEnabled(enabled);
283 | blockPublicCollaborator.setEnabled(enabled);
284 | trustSelfSignedCheckbox.setEnabled(enabled);
285 | hostnameVerificationCheckbox.setEnabled(enabled);
286 | proxyRequestsWithBurp.setEnabled(enabled);
287 | enableAuthentication.setEnabled(enabled);
288 | secretArea.setEnabled(enabled && enableAuthentication.isSelected());
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/extension/ui/ExtensionUI.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.extension.ui;
2 |
3 | import burp.IExtensionStateListener;
4 | import burp.ITab;
5 | import com.coreyd97.BurpExtenderUtilities.PopOutPanel;
6 | import com.nccgroup.collaboratorplusplus.extension.CollaboratorPlusPlus;
7 | import com.nccgroup.collaboratorplusplus.extension.interactionhistory.HistoryUI;
8 |
9 | import javax.swing.*;
10 | import java.awt.*;
11 |
12 | import static com.nccgroup.collaboratorplusplus.extension.Globals.EXTENSION_NAME;
13 |
14 | public class ExtensionUI implements ITab, IExtensionStateListener {
15 |
16 | private final CollaboratorPlusPlus extension;
17 | private PopOutPanel popOutPanel;
18 | private JMenuBar menuBar;
19 | private JMenu extensionMenu;
20 |
21 | public ExtensionUI(CollaboratorPlusPlus extension){
22 | this.extension = extension;
23 | CollaboratorPlusPlus.callbacks.registerExtensionStateListener(this);
24 | this.popOutPanel = new PopOutPanel(buildMainPanel(), EXTENSION_NAME);
25 | }
26 |
27 | private JComponent buildMainPanel(){
28 | JTabbedPane tabbedPane = new JTabbedPane();
29 | tabbedPane.addTab("Config", new ConfigUI(extension));
30 | tabbedPane.addTab("Interaction History", new HistoryUI(extension.getContextManager(), extension.getPreferences()));
31 | tabbedPane.addTab("About", new AboutUI(extension));
32 | return tabbedPane;
33 | }
34 |
35 | public void addMenuItemsToBurp(){
36 | JFrame rootFrame = (JFrame) SwingUtilities.getWindowAncestor(this.popOutPanel);
37 | menuBar = rootFrame.getJMenuBar();
38 | if(menuBar != null) {
39 | extensionMenu = new JMenu("Collaborator++");
40 | extensionMenu.add(popOutPanel.getPopoutMenuItem());
41 | menuBar.add(extensionMenu, menuBar.getMenuCount() - 1);
42 | }
43 | }
44 |
45 | public void removeMenuItemsFromBurp(){
46 | if(menuBar != null && extensionMenu != null){
47 | menuBar.remove(extensionMenu);
48 | }
49 | }
50 |
51 | public JComponent getTabForExtension(){
52 | JTabbedPane mainPane = getBurpTabbedPane();
53 | int tabIndex = mainPane.indexOfComponent(this.popOutPanel);
54 | return (JComponent) mainPane.getTabComponentAt(tabIndex);
55 | }
56 |
57 | public JTabbedPane getBurpTabbedPane(){
58 | return (JTabbedPane) this.popOutPanel.getParent();
59 | }
60 |
61 | @Override
62 | public void extensionUnloaded() {
63 | removeMenuItemsFromBurp();
64 | }
65 |
66 | @Override
67 | public String getTabCaption() {
68 | return EXTENSION_NAME;
69 | }
70 |
71 | @Override
72 | public Component getUiComponent() {
73 | return popOutPanel;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/server/CollaboratorServer.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.server;
2 |
3 | import nu.studer.java.util.OrderedProperties;
4 | import org.apache.http.impl.NoConnectionReuseStrategy;
5 | import org.apache.http.impl.bootstrap.HttpServer;
6 | import org.apache.http.impl.bootstrap.ServerBootstrap;
7 | import org.apache.http.ssl.SSLContexts;
8 | import org.apache.logging.log4j.Level;
9 | import org.apache.logging.log4j.LogManager;
10 | import org.apache.logging.log4j.Logger;
11 | import org.apache.logging.log4j.core.LoggerContext;
12 | import org.apache.logging.log4j.core.appender.ConsoleAppender;
13 | import org.apache.logging.log4j.core.config.Configuration;
14 | import org.apache.logging.log4j.core.config.Configurator;
15 | import org.apache.logging.log4j.core.layout.PatternLayout;
16 |
17 | import javax.net.ssl.SSLContext;
18 | import javax.net.ssl.SSLException;
19 | import java.io.File;
20 | import java.io.FileInputStream;
21 | import java.io.FileOutputStream;
22 | import java.io.IOException;
23 | import java.net.InetAddress;
24 | import java.security.KeyStore;
25 | import java.security.PrivateKey;
26 | import java.security.cert.Certificate;
27 | import java.util.ArrayList;
28 | import java.util.Properties;
29 | import java.util.concurrent.TimeUnit;
30 |
31 | public class CollaboratorServer {
32 |
33 | public static Logger logger = LogManager.getRootLogger();
34 | private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
35 | private static final String COLLABORATOR_SERVER_ADDRESS = "collaborator_server_address";
36 | private static final String COLLABORATOR_SERVER_PORT = "collaborator_server_port";
37 | private static final String COLLABORATOR_SERVER_ISHTTPS = "collaborator_server_ishttps";
38 | private static final String SECRET = "secret";
39 | private static final String LISTEN_PORT = "listen_port";
40 | private static final String LISTEN_ADDRESS = "listen_address";
41 | private static final String ENABLE_SSL = "enable_ssl";
42 | private static final String PRIVATE_KEY_PATH = "ssl_private_key_path";
43 | private static final String CERTIFICATE_PATH = "ssl_certificate_path";
44 | private static final String INTERMEDIATE_CERTIFICATE_PATH = "ssl_intermediate_certificate_path";
45 | private static final String INTERMEDIATE_CERTIFICATE_DEFAULT = "/certs/intermediate.crt";
46 | private static final String KEYSTORE_FILE = "keystore_file";
47 | private static final String KEYSTORE_PASSWORD = "keystore_password";
48 | private static final String KEYSTORE_KEY_PASSWORD = "keystore_key_password";
49 | private static final String LOG_LEVEL = "log_level";
50 |
51 | private HttpServer server;
52 | private Integer listenPort;
53 |
54 | private CollaboratorServer(Properties properties) throws Exception {
55 | Level logLevel = Level.valueOf(properties.getProperty(LOG_LEVEL));
56 | Configurator.setRootLevel(logLevel);
57 |
58 | String actualAddress = properties.getProperty(COLLABORATOR_SERVER_ADDRESS);
59 | Integer actualPort = Integer.parseInt(properties.getProperty(COLLABORATOR_SERVER_PORT));
60 | boolean actualIsHttps = Boolean.parseBoolean(properties.getProperty(COLLABORATOR_SERVER_ISHTTPS));
61 |
62 | listenPort = Integer.parseInt(properties.getProperty(LISTEN_PORT));
63 | InetAddress listenAddress = InetAddress.getByName(properties.getProperty(LISTEN_ADDRESS));
64 | boolean enableSSL = Boolean.parseBoolean(properties.getProperty(ENABLE_SSL));
65 |
66 | String secret = properties.getProperty(SECRET).trim();
67 |
68 | ServerBootstrap serverBootstrap = ServerBootstrap.bootstrap()
69 | .setConnectionReuseStrategy(new NoConnectionReuseStrategy())
70 | .setListenerPort(listenPort)
71 | .setLocalAddress(listenAddress)
72 | .setExceptionLogger(ex -> {
73 | if(ex instanceof SSLException){
74 | logger.error("Client Connection Failed: " + ex.getMessage());
75 | }else{
76 | logger.error(ex);
77 | }
78 | })
79 | .registerHandler("*", new HttpHandler(actualAddress, actualPort, actualIsHttps, secret));
80 |
81 | if(enableSSL){
82 | logger.info("Starting server in HTTPS mode. Creating SSL context.");
83 | SSLContext sslContext;
84 | if(!properties.getProperty(PRIVATE_KEY_PATH).equals("")){
85 | //Load private key
86 | logger.info("Loading private key from file: " + properties.getProperty(PRIVATE_KEY_PATH));
87 | PrivateKey privateKey = Utilities.loadPrivateKeyFromFile(properties.getProperty(PRIVATE_KEY_PATH));
88 |
89 | ArrayList certificateList = new ArrayList<>();
90 | //Load certificate
91 | logger.info("Loading certificate from file: " + properties.getProperty(CERTIFICATE_PATH));
92 | certificateList.add(Utilities.loadCertificateFromFile(properties.getProperty(CERTIFICATE_PATH)));
93 | //Load intermediate certificate
94 | String intermediatePath = properties.getProperty(INTERMEDIATE_CERTIFICATE_PATH);
95 | if(!intermediatePath.equals("") && !intermediatePath.equals(INTERMEDIATE_CERTIFICATE_DEFAULT)){
96 | logger.info("Loading intermediate certificate from file: " + properties.getProperty(INTERMEDIATE_CERTIFICATE_PATH));
97 | certificateList.add(Utilities.loadCertificateFromFile(intermediatePath));
98 | }
99 | Certificate[] certificateChain = certificateList.toArray(new Certificate[0]);
100 |
101 | //Create new keystore
102 | KeyStore keyStore = KeyStore.getInstance("JKS");
103 | keyStore.load(null, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
104 | keyStore.setKeyEntry("collaboratorAuth", privateKey,
105 | DEFAULT_KEYSTORE_PASSWORD.toCharArray(), certificateChain);
106 | // keyStore.
107 | sslContext = createSSLContext(keyStore, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
108 | }else {
109 | File keystoreFile = new File(properties.getProperty(KEYSTORE_FILE));
110 | String storePassword = properties.getProperty(KEYSTORE_PASSWORD);
111 | String keyPassword = properties.getProperty(KEYSTORE_KEY_PASSWORD);
112 | sslContext = createSSLContext(keystoreFile, storePassword, keyPassword);
113 | }
114 | serverBootstrap.setSslContext(sslContext);
115 | }else{
116 | logger.info("Starting server in HTTP mode.");
117 | }
118 |
119 |
120 | logger.debug("Shared Secret: " + secret);
121 | serverBootstrap.setExceptionLogger(ex -> {
122 | logger.error(ex);
123 | });
124 |
125 | server = serverBootstrap.create();
126 | }
127 |
128 | public void start() throws IOException {
129 | if(server != null) {
130 | server.start();
131 | logger.info("Server started. Listening for poll requests on port " + listenPort + "...");
132 | Runtime.getRuntime().addShutdownHook(new Thread(() -> {
133 | server.shutdown(500, TimeUnit.MILLISECONDS);
134 | }));
135 | }
136 | }
137 |
138 | private SSLContext createSSLContext(final File keyStoreFile, final String storePassword, final String keyPassword) throws Exception {
139 | return SSLContexts.custom()
140 | .loadKeyMaterial(keyStoreFile, storePassword.toCharArray(), keyPassword.toCharArray())
141 | .build();
142 | }
143 |
144 | private SSLContext createSSLContext(final KeyStore keyStore, final char[] password) throws Exception {
145 | return SSLContexts.custom().loadKeyMaterial(keyStore, password).build();
146 | }
147 |
148 | public static void main(String[] args) throws Exception {
149 |
150 | OrderedProperties properties = getDefaultProperties();
151 | if(args.length == 0){
152 | //Create default properties file
153 | File defaultsFile = new File("CollaboratorServer.properties");
154 | if(defaultsFile.exists()){
155 | logger.error("Could not create the defaults file. File exists.");
156 | logger.error("Start the server with `java -jar CollaboratorAuth.jar " + defaultsFile.getName() + "`" +
157 | " or remove the file to allow it to be populated with the defaults");
158 | return;
159 | }
160 | FileOutputStream outputStream = new FileOutputStream(defaultsFile);
161 | properties.store(outputStream, "MAKE SURE THE SECRET IS CHANGED TO SOMETHING MORE SECURE!\n" +
162 | "By default, the private key and certificates will be used to\n" +
163 | "configure the SSL context. To use a keystore instead, comment out " + PRIVATE_KEY_PATH + ".");
164 | logger.info("Default config written to " + defaultsFile.getName());
165 | logger.info("Edit the config (especially the secret!)");
166 | logger.info("Then start the server with `java -jar CollaboratorAuth.jar " + defaultsFile.getName() + "`");
167 | return;
168 | }else{
169 | File configFile = new File(args[0]);
170 | if(!configFile.exists()){
171 | logger.error("Config file does not exist. Run the jar without arguments to generate the default config.");
172 | return;
173 | }else{
174 | FileInputStream inputStream = new FileInputStream(configFile);
175 | try {
176 | properties.load(inputStream);
177 | }finally {
178 | inputStream.close();
179 | }
180 | }
181 | }
182 |
183 | try {
184 | CollaboratorServer server = new CollaboratorServer(properties.toJdkProperties());
185 | server.start();
186 | }catch (Exception e){
187 | e.printStackTrace();
188 | }
189 | }
190 |
191 | private static OrderedProperties getDefaultProperties(){
192 | OrderedProperties defaultProperties = new OrderedProperties.OrderedPropertiesBuilder()
193 | .withSuppressDateInComment(true).build();
194 | defaultProperties.setProperty(COLLABORATOR_SERVER_ADDRESS, "127.0.0.1");
195 | defaultProperties.setProperty(COLLABORATOR_SERVER_PORT, "80");
196 | defaultProperties.setProperty(LISTEN_PORT, "5443");
197 | defaultProperties.setProperty(LISTEN_ADDRESS, "0.0.0.0");
198 | defaultProperties.setProperty(ENABLE_SSL, "false");
199 | defaultProperties.setProperty(PRIVATE_KEY_PATH, "/certs/key.pem.pkcs8");
200 | defaultProperties.setProperty(CERTIFICATE_PATH, "/certs/cert.crt");
201 | defaultProperties.setProperty(INTERMEDIATE_CERTIFICATE_PATH, INTERMEDIATE_CERTIFICATE_DEFAULT);
202 | defaultProperties.setProperty(KEYSTORE_FILE, "/path/to/java/keystore");
203 | defaultProperties.setProperty(KEYSTORE_PASSWORD, "NEW_PASSWORD_FOR_KEYSTORE");
204 | defaultProperties.setProperty(KEYSTORE_KEY_PASSWORD, "NEW_PASSWORD_FOR_PRIVATE_KEY");
205 |
206 | defaultProperties.setProperty(SECRET, "CHANGE_ME");
207 | defaultProperties.setProperty(LOG_LEVEL, "INFO");
208 |
209 | return defaultProperties;
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/server/HttpHandler.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.server;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.Globals;
4 | import com.nccgroup.collaboratorplusplus.utilities.Encryption;
5 | import org.apache.commons.io.IOUtils;
6 | import org.apache.http.*;
7 | import org.apache.http.client.config.RequestConfig;
8 | import org.apache.http.client.methods.HttpGet;
9 | import org.apache.http.conn.ssl.DefaultHostnameVerifier;
10 | import org.apache.http.conn.ssl.NoopHostnameVerifier;
11 | import org.apache.http.entity.ByteArrayEntity;
12 | import org.apache.http.entity.ContentType;
13 | import org.apache.http.entity.StringEntity;
14 | import org.apache.http.impl.NoConnectionReuseStrategy;
15 | import org.apache.http.impl.client.CloseableHttpClient;
16 | import org.apache.http.impl.client.HttpClientBuilder;
17 | import org.apache.http.impl.client.HttpClients;
18 | import org.apache.http.message.BasicHeader;
19 | import org.apache.http.message.BasicHttpEntityEnclosingRequest;
20 | import org.apache.http.protocol.HttpContext;
21 | import org.apache.http.protocol.HttpRequestHandler;
22 | import org.apache.logging.log4j.LogManager;
23 | import org.apache.logging.log4j.Logger;
24 |
25 | import javax.crypto.BadPaddingException;
26 | import javax.crypto.IllegalBlockSizeException;
27 | import javax.crypto.NoSuchPaddingException;
28 | import java.io.ByteArrayOutputStream;
29 | import java.net.URI;
30 | import java.net.URL;
31 | import java.security.*;
32 | import java.security.spec.InvalidKeySpecException;
33 | import java.security.spec.InvalidParameterSpecException;
34 | import java.util.Base64;
35 |
36 | public class HttpHandler implements HttpRequestHandler {
37 |
38 | private static Logger logger = LogManager.getLogger(Globals.EXTENSION_NAME);
39 | private final String actualAddress;
40 | private final Integer actualPort;
41 | private final boolean actualIsHttps;
42 | private final String secret;
43 |
44 | public HttpHandler(String actualAddress, Integer actualPort, boolean actualIsHttps, String secret){
45 | this.actualAddress = actualAddress;
46 | this.actualPort = actualPort;
47 | this.actualIsHttps = actualIsHttps;
48 | this.secret = secret;
49 | }
50 |
51 | private CloseableHttpClient buildHttpClient() {
52 | HttpClientBuilder httpClientBuilder =
53 | HttpClients.custom()
54 | .setConnectionReuseStrategy(new NoConnectionReuseStrategy())
55 | .setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(5000).build()); //Don't hang around!
56 |
57 | return httpClientBuilder.build();
58 | }
59 |
60 | @Override
61 | public void handle(HttpRequest request, HttpResponse response, HttpContext context) {
62 | response.addHeader(new BasicHeader("X-Auth-Compatible", "true"));
63 |
64 | if (!(request instanceof BasicHttpEntityEnclosingRequest)) {
65 | response.setEntity(new StringEntity(Utilities.getAboutPage(), ContentType.TEXT_HTML));
66 | response.setStatusCode(HttpStatus.SC_OK);
67 | return;
68 | }
69 |
70 | logger.debug("Retrieved request: ");
71 | logger.debug(((BasicHttpEntityEnclosingRequest) request).getEntity().toString());
72 |
73 | try (CloseableHttpClient client = buildHttpClient()) {
74 | try {
75 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
76 | ((BasicHttpEntityEnclosingRequest) request).getEntity().writeTo(byteArrayOutputStream);
77 | String encodedRequest = Encryption.aesDecryptRequest(this.secret, byteArrayOutputStream.toByteArray());
78 | String requestDecoded = new String(Base64.getDecoder().decode(encodedRequest));
79 |
80 | logger.debug("Decoded Request: " + requestDecoded);
81 |
82 | if (!requestDecoded.startsWith("/burpresults?biid=")) { //If request is not a valid collaborator request. (SSRF protection!)
83 | throw new IllegalArgumentException("The request does not look like a valid collaborator polling request!");
84 | }
85 |
86 | //Make request to actual collaborator server
87 | URI getURI = new URL((actualIsHttps ? "https://" : "http://") + actualAddress + ":" + actualPort
88 | + requestDecoded).toURI();
89 | HttpGet getRequest = new HttpGet(getURI);
90 | getRequest.addHeader("Connection", "close");
91 | HttpResponse actualRequestResponse = client.execute(getRequest);
92 | final int actualStatus = actualRequestResponse.getStatusLine().getStatusCode();
93 |
94 |
95 | if (actualRequestResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
96 | String actualResponse = IOUtils.toString(actualRequestResponse.getEntity().getContent());
97 | response.setEntity(createEncryptedResponse(actualResponse));
98 |
99 | for (Header header : actualRequestResponse.getAllHeaders()) {
100 | if (header.getName().startsWith("X-Collaborator")) {
101 | response.addHeader(header);
102 | }
103 | }
104 |
105 | } else {
106 | throw new Exception("The Collaborator server responded with a status code: " + actualStatus);
107 | }
108 | } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) {
109 | //Could not decrypt the request. The client probably used an invalid secret.
110 | logger.error(e);
111 | response.setStatusCode(HttpStatus.SC_UNAUTHORIZED);
112 | response.setEntity(new StringEntity("The server could not decrypt the request. Is the secret correct?"));
113 | } catch (IllegalArgumentException e) {
114 | logger.debug(e.getMessage());
115 | response.setStatusCode(HttpStatus.SC_BAD_REQUEST);
116 | response.setEntity(createEncryptedResponse(e.getMessage()));
117 | }
118 | } catch (Exception e) {
119 | // Log exception?
120 | response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
121 | }
122 |
123 | logger.debug("Response: ");
124 | logger.debug(response.toString());
125 | }
126 |
127 | private ByteArrayEntity createEncryptedResponse(String message) throws GeneralSecurityException {
128 | byte[] encrypted = Encryption.aesEncryptRequest(this.secret, message);
129 | return new ByteArrayEntity(encrypted);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/server/Utilities.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.server;
2 |
3 | import com.nccgroup.collaboratorplusplus.extension.Globals;
4 | import org.apache.logging.log4j.LogManager;
5 | import org.apache.logging.log4j.Logger;
6 | import org.bouncycastle.util.io.pem.PemObject;
7 | import org.bouncycastle.util.io.pem.PemReader;
8 |
9 | import java.io.ByteArrayInputStream;
10 | import java.io.FileReader;
11 | import java.io.IOException;
12 | import java.nio.file.Files;
13 | import java.nio.file.Paths;
14 | import java.security.KeyFactory;
15 | import java.security.NoSuchAlgorithmException;
16 | import java.security.PrivateKey;
17 | import java.security.cert.Certificate;
18 | import java.security.cert.CertificateException;
19 | import java.security.cert.CertificateFactory;
20 | import java.security.spec.InvalidKeySpecException;
21 | import java.security.spec.PKCS8EncodedKeySpec;
22 |
23 | public class Utilities {
24 |
25 | public static final Logger logger = LogManager.getLogger(Globals.EXTENSION_NAME);
26 |
27 | public static PrivateKey loadPrivateKeyFromFile(String path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
28 | if(!Files.exists(Paths.get(path))){
29 | logger.error("Cannot load private key \"" + path + "\". File does not exist!");
30 | }
31 | FileReader fileReader = new FileReader(path);
32 | PemObject pemObject = new PemReader(fileReader).readPemObject();
33 | final byte[] pemContent = pemObject.getContent();
34 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pemContent);
35 | KeyFactory keyFactory = KeyFactory.getInstance("RSA");
36 | return keyFactory.generatePrivate(keySpec);
37 | }
38 |
39 | public static Certificate loadCertificateFromFile(String path) throws IOException, CertificateException {
40 | if(!Files.exists(Paths.get(path))){
41 | logger.error("Cannot load certificate \"" + path + "\". File does not exist!");
42 | }
43 | FileReader fileReader = new FileReader(path);
44 | PemObject pemObject = new PemReader(fileReader).readPemObject();
45 | final byte[] pemContent = pemObject.getContent();
46 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
47 | return certificateFactory.generateCertificate(new ByteArrayInputStream(pemContent));
48 | }
49 |
50 | public static String getAboutPage(){
51 | return "Collaborator++
" +
52 | "Collaborator++ is a project designed to provide an authentication mechanism to the " +
53 | "Burp Collaborator service, a tool provided as part of the Burp Suite application.
" +
54 | "
" +
55 | "Collaborator Authenticator is an Open Source project and is released under the AGPL-v3.0 licence.
" +
56 | "View the project on GitHub" +
57 | "
" +
58 | "Burp Suite
" +
59 | "Burp Suite is a web testing application " +
60 | "developed by PortSwigger.
";
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/utilities/Encryption.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.utilities;
2 |
3 |
4 | import javax.crypto.*;
5 | import javax.crypto.spec.IvParameterSpec;
6 | import javax.crypto.spec.PBEKeySpec;
7 | import javax.crypto.spec.SecretKeySpec;
8 | import java.security.*;
9 | import java.security.spec.InvalidKeySpecException;
10 | import java.security.spec.InvalidParameterSpecException;
11 | import java.security.spec.KeySpec;
12 | import java.util.Arrays;
13 |
14 | public class Encryption {
15 |
16 | private static SecretKey generateKey(String secret) throws GeneralSecurityException {
17 | SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
18 | KeySpec keySpec = new PBEKeySpec(secret.toCharArray(), "CollaboratorAuth".getBytes(), 128, 256);
19 | SecretKey tmpSecret = keyFactory.generateSecret(keySpec);
20 |
21 | return new SecretKeySpec(tmpSecret.getEncoded(), "AES");
22 | }
23 |
24 |
25 | public static byte[] aesEncryptRequest(String secret, String request) throws GeneralSecurityException {
26 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
27 | SecretKey secretKey = generateKey(secret);
28 | cipher.init(Cipher.ENCRYPT_MODE, secretKey);
29 | AlgorithmParameters params = cipher.getParameters();
30 | byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
31 | byte[] cipherText = cipher.doFinal(request.getBytes());
32 |
33 | byte[] cipherTextWithIv = new byte[cipherText.length+iv.length];
34 | System.arraycopy(iv, 0, cipherTextWithIv, 0, iv.length);
35 | System.arraycopy(cipherText, 0, cipherTextWithIv, iv.length, cipherText.length);
36 | return cipherTextWithIv;
37 | }
38 |
39 |
40 | public static String aesDecryptRequest(String secret, byte[] encryptedWithIv) throws GeneralSecurityException {
41 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
42 | SecretKey secretKey = generateKey(secret);
43 | byte[] iv = Arrays.copyOfRange(encryptedWithIv, 0, 16);
44 | byte[] cipherText = Arrays.copyOfRange(encryptedWithIv, 16, encryptedWithIv.length);
45 | cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
46 | String plaintext = new String(cipher.doFinal(cipherText));
47 |
48 | return plaintext;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/utilities/LevelSerializer.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.utilities;
2 |
3 | import com.google.gson.*;
4 | import org.apache.logging.log4j.Level;
5 |
6 | import java.lang.reflect.Type;
7 |
8 | public class LevelSerializer implements JsonDeserializer {
9 |
10 | @Override
11 | public Level deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
12 | return Level.valueOf(json.getAsJsonObject().get("name").getAsString());
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/utilities/NoTextSelectionCaret.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.utilities;
2 |
3 | import javax.swing.text.DefaultCaret;
4 | import javax.swing.text.JTextComponent;
5 |
6 | public class NoTextSelectionCaret extends DefaultCaret {
7 | public NoTextSelectionCaret(JTextComponent component){
8 | setBlinkRate(component.getCaret().getBlinkRate());
9 | component.setHighlighter(null);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/utilities/SelectableLabel.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.utilities;
2 |
3 | import javax.swing.*;
4 |
5 | public class SelectableLabel extends JTextField {
6 |
7 | public SelectableLabel(String text){
8 | super(text);
9 | this.setEditable(false);
10 | this.setBorder(null);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/utilities/StaticHTTPMessageController.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.utilities;
2 |
3 | import burp.IBurpExtenderCallbacks;
4 | import burp.IHttpService;
5 | import burp.IMessageEditor;
6 | import burp.IMessageEditorController;
7 |
8 | import java.net.URL;
9 |
10 | public class StaticHTTPMessageController implements IMessageEditorController {
11 |
12 | private final IBurpExtenderCallbacks callbacks;
13 | private final byte[] request;
14 | private final byte[] response;
15 | private final IHttpService service;
16 |
17 | public StaticHTTPMessageController(IBurpExtenderCallbacks callbacks, URL url, byte[] request, byte[] response){
18 | this.callbacks = callbacks;
19 | this.request = request;
20 | this.response = response;
21 |
22 | this.service = callbacks.getHelpers().buildHttpService(url.getHost(), url.getPort(), url.getProtocol());
23 | }
24 |
25 | @Override
26 | public IHttpService getHttpService() {
27 | return this.service;
28 | }
29 |
30 | @Override
31 | public byte[] getRequest() {
32 | return this.request;
33 | }
34 |
35 | @Override
36 | public byte[] getResponse() {
37 | return this.response;
38 | }
39 |
40 | public IMessageEditor buildRequestViewer(){
41 | IMessageEditor editor = callbacks.createMessageEditor(this, false);
42 | editor.setMessage(request, true);
43 | return editor;
44 | }
45 |
46 | public IMessageEditor buildResponseViewer(){
47 | IMessageEditor editor = callbacks.createMessageEditor(this, false);
48 | editor.setMessage(response, false);
49 | return editor;
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/nccgroup/collaboratorplusplus/utilities/WrappedTextPane.java:
--------------------------------------------------------------------------------
1 | package com.nccgroup.collaboratorplusplus.utilities;
2 |
3 | import javax.swing.*;
4 | import javax.swing.text.*;
5 |
6 | public class WrappedTextPane extends JTextPane {
7 |
8 | public WrappedTextPane(){
9 | this.setEditorKit(new WrapEditorKit());
10 | }
11 |
12 | class WrapEditorKit extends StyledEditorKit {
13 | ViewFactory factory = new WrapColumnFactory();
14 | public ViewFactory getViewFactory() {
15 | return factory;
16 | }
17 | }
18 |
19 | class WrapColumnFactory implements ViewFactory {
20 | public View create(Element elem) {
21 | String kind = elem.getName();
22 | if (kind != null) {
23 | if (kind.equals(AbstractDocument.ContentElementName)) {
24 | return new WrapLabelView(elem);
25 | } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
26 | return new ParagraphView(elem);
27 | } else if (kind.equals(AbstractDocument.SectionElementName)) {
28 | return new BoxView(elem, View.Y_AXIS);
29 | } else if (kind.equals(StyleConstants.ComponentElementName)) {
30 | return new ComponentView(elem);
31 | } else if (kind.equals(StyleConstants.IconElementName)) {
32 | return new IconView(elem);
33 | }
34 | }
35 |
36 | // default to text display
37 | return new LabelView(elem);
38 | }
39 | }
40 |
41 | class WrapLabelView extends LabelView {
42 | public WrapLabelView(Element elem) {
43 | super(elem);
44 | }
45 |
46 | public float getMinimumSpan(int axis) {
47 | switch (axis) {
48 | case View.X_AXIS:
49 | return 0;
50 | case View.Y_AXIS:
51 | return super.getMinimumSpan(axis);
52 | default:
53 | throw new IllegalArgumentException("Invalid axis: " + axis);
54 | }
55 | }
56 |
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/resources/Explain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/src/main/resources/Explain.png
--------------------------------------------------------------------------------
/src/main/resources/GitHubLogoBlack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/src/main/resources/GitHubLogoBlack.png
--------------------------------------------------------------------------------
/src/main/resources/GitHubLogoWhite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/src/main/resources/GitHubLogoWhite.png
--------------------------------------------------------------------------------
/src/main/resources/NCCGroup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/src/main/resources/NCCGroup.png
--------------------------------------------------------------------------------
/src/main/resources/NCCLarge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/src/main/resources/NCCLarge.png
--------------------------------------------------------------------------------
/src/main/resources/TwitterLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccgroup/CollaboratorPlusPlus/66666544261a184a6182899ed959c29f4e1b52a3/src/main/resources/TwitterLogo.png
--------------------------------------------------------------------------------
/src/test/java/TestPlugin.java:
--------------------------------------------------------------------------------
1 | public class TestPlugin {
2 | public static void main(String[] args) {
3 | burp.StartBurp.main(args);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------