├── static └── mime.pb ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ ├── com │ │ │ └── redpois0n │ │ │ │ └── terminal │ │ │ │ ├── SizeChangeListener.java │ │ │ │ ├── InputListener.java │ │ │ │ ├── TerminalCaret.java │ │ │ │ └── JTerminal.java │ │ └── org │ │ │ └── zeromq │ │ │ └── codec │ │ │ └── Z85.java │ ├── yaml │ │ └── defaults.yaml │ ├── kotlin │ │ └── burp │ │ │ ├── Editors.kt │ │ │ ├── Enums.kt │ │ │ ├── Extensions.kt │ │ │ ├── Serialization.kt │ │ │ ├── BurpExtender.kt │ │ │ └── ConfigGUI.kt │ └── proto │ │ └── burp │ │ └── piper.proto └── test │ └── kotlin │ └── burp │ └── SerializationKtTest.kt ├── .gitignore ├── BappManifest.bmf ├── BappDescription.html ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE.md /static/mime.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentsignal/burp-piper/HEAD/static/mime.pb -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silentsignal/burp-piper/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/com/redpois0n/terminal/SizeChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.redpois0n.terminal; 2 | 3 | public interface SizeChangeListener { 4 | 5 | void sizeChange(JTerminal terminal, boolean reset, int width, int height); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-bin.zip 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStorePath=wrapper/dists 5 | zipStoreBase=GRADLE_USER_HOME 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Eclipse 2 | .classpath 3 | .project 4 | test-output 5 | .settings 6 | 7 | #IntelliJ 8 | *.iml 9 | *.ipr 10 | *.iws 11 | .idea/ 12 | 13 | #Gradle 14 | .gradle 15 | classes/ 16 | 17 | 18 | #Build directories 19 | bin/ 20 | build/ 21 | target/ -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: e4e0f6c4f0274754917dcb5f4937bb9e 2 | ExtensionType: 1 3 | Name: Piper 4 | RepoName: piper 5 | ScreenVersion: 0.7.2 6 | SerialVersion: 11 7 | MinPlatformVersion: 0 8 | ProOnly: False 9 | Author: Andras Veres-Szentkiralyi 10 | ShortDescription: Easily integrate external tools into Burp 11 | EntryPoint: build/libs/piper.jar 12 | BuildCommand: ./gradlew build -x test 13 | -------------------------------------------------------------------------------- /src/main/java/com/redpois0n/terminal/InputListener.java: -------------------------------------------------------------------------------- 1 | package com.redpois0n.terminal; 2 | 3 | public abstract class InputListener { 4 | 5 | /** 6 | * Called when a command is entered 7 | * @param terminal 8 | * @param c 9 | */ 10 | public abstract void processCommand(JTerminal terminal, char c); 11 | 12 | /** 13 | * Called when Ctrl+C is pressed 14 | * @param terminal 15 | */ 16 | public void onTerminate(JTerminal terminal) { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/kotlin/burp/SerializationKtTest.kt: -------------------------------------------------------------------------------- 1 | package burp 2 | 3 | import org.testng.Assert.* 4 | import org.testng.annotations.Test 5 | import kotlin.text.Charsets.UTF_8 6 | 7 | val testInput = "ABCDEFGHIJKLMNO".toByteArray(UTF_8) 8 | 9 | class SerializationKtTest { 10 | 11 | @Test 12 | fun testPad4() { 13 | for (len in 0.rangeTo(testInput.size)) { 14 | val subset = testInput.copyOfRange(0, len) 15 | val padded = pad4(subset) 16 | assertEquals(padded.size % 4, 0) 17 | assertEquals(subset, unpad4(padded)) 18 | } 19 | } 20 | 21 | @Test 22 | fun testCompress() { 23 | assertEquals(testInput, decompress(compress(testInput))) 24 | } 25 | } -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

Piper makes integrating external tools into Burp easier.

2 |

In true Unix 3 | fashion, small things that do one thing and do it well can be connected 4 | together like building blocks to aid quick experimentation. By lowering 5 | the level of coupling, any programming language can be used as long as 6 | it can accept inputs on the standard input or as command line 7 | parameters.

8 |

Extending message editors for new formats or integrating a 9 | better comparator/diff tool is as simple as filling a short form with 10 | the command line and some details with sensible defaults.

11 |

Useful configs 12 | can be shared using the the textual YAML format, the defaults already 13 | contain some simple examples.

14 | -------------------------------------------------------------------------------- /src/main/java/com/redpois0n/terminal/TerminalCaret.java: -------------------------------------------------------------------------------- 1 | package com.redpois0n.terminal; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics; 5 | import java.awt.Rectangle; 6 | 7 | import javax.swing.text.DefaultCaret; 8 | import javax.swing.text.JTextComponent; 9 | 10 | @SuppressWarnings("serial") 11 | public class TerminalCaret extends DefaultCaret { 12 | 13 | protected synchronized void damage(Rectangle r) { 14 | if (r == null) 15 | return; 16 | 17 | x = r.x; 18 | y = r.y; 19 | height = r.height; 20 | 21 | if (width <= 0) { 22 | width = getComponent().getWidth(); 23 | } 24 | 25 | repaint(); 26 | } 27 | 28 | public void paint(Graphics g) { 29 | JTextComponent comp = getComponent(); 30 | 31 | if (comp == null) { 32 | return; 33 | } 34 | 35 | int dot = getDot(); 36 | Rectangle r = null; 37 | char dotChar; 38 | 39 | try { 40 | r = comp.modelToView(dot); 41 | if (r == null) { 42 | return; 43 | } 44 | dotChar = comp.getText(dot, 1).charAt(0); 45 | } catch (Exception e) { 46 | e.printStackTrace(); 47 | return; 48 | } 49 | 50 | if ((x != r.x) || (y != r.y)) { 51 | repaint(); 52 | x = r.x; 53 | y = r.y; 54 | height = r.height; 55 | } 56 | 57 | g.setColor(Color.white); 58 | g.setXORMode(comp.getBackground()); 59 | 60 | width = g.getFontMetrics().charWidth(dotChar); 61 | 62 | if (isVisible()) { 63 | g.fillRect(r.x, r.y, width, r.height); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /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= 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Piper for Burp Suite 2 | ==================== 3 | 4 | Piper integrates external tools and their pipelines to Burp Suite. The 5 | extension can pass HTTP requests and responses from Burp to external programs, 6 | then feed the execution result back to Burp. With Piper you can create: 7 | 8 | * **Commentators**: Display the output of an external program in Proxy History 9 | as comments. For example, you can display the cryptographic hash of every 10 | request by piping their content to `sha256sum`. 11 | * **Highlighters**: Highlight items in the proxy history based on their 12 | contents. For example, you can highlight items where HTTP response includes 13 | elements of a wordlist. 14 | * **Message Viewers**: Display the contents of HTTP messages with custom 15 | formatting. For example, you can display Protobuf structures by piping message 16 | contents to `protoc`. 17 | * **Context Menu Items**: Invoke external tools from context menu. For example, 18 | you can use an external diff GUI to compare HTTP messages. 19 | * **Intruder Payload Generators**: Generate payloads for Intruder with external 20 | tools. For example, you can make Intruder use password candidates generated by 21 | John the Ripper. 22 | * **Intruder Payload Processors**: Transform Intruder payloads. For example, you 23 | can apply base64 encoding with a custom alphabet using an external script. 24 | * **Macros**: You can use external tools as part of Macros. For example, you 25 | can automatically generate predictable CSRF tokens for every outgoing request. 26 | * **HTTP Listeners**: Transform outgoing and incoming HTTP messages. For 27 | example, you can use an external Python script to handle custom encryption. 28 | 29 | Detailed usage information is provided in the original [GWAPT Gold 30 | Paper](https://www.sans.org/white-papers/39440/), and in [this demonstration 31 | video](https://vimeo.com/401007109). 32 | 33 | Building 34 | -------- 35 | 36 | Execute `./gradlew build` and you'll have the plugin ready in 37 | `build/libs/burp-piper.jar` 38 | 39 | Known issues 40 | ------------ 41 | 42 | - The terminal emulator ignores background color when _Look and feel_ is set 43 | to _Nimbus_, see https://bugs.openjdk.java.net/browse/JDK-8058704 44 | 45 | Security 46 | -------- 47 | 48 | Piper configurations can be exported and imported. As configurations define 49 | commands to be executed on the user's machine, importing malicious 50 | configurations is a security risk. 51 | 52 | Piper disables configurations loaded via the GUI to prevent exploitation, and 53 | unexpected behavior (e.g.: modification of HTTP messages). To support 54 | automation, Piper enables configurations loaded via the `PIPER_CONFIG` 55 | environment variable, so extra care must be taken in this use case. 56 | 57 | Users should always review configurations before importing or enabling them. 58 | 59 | License 60 | ------- 61 | 62 | The whole project is available under the GNU General Public License v3.0, 63 | see `LICENSE.md`. The [swing-terminal component][1] was developed by 64 | @redpois0n, released under this same license. 65 | 66 | [1]: https://github.com/redpois0n/swing-terminal 67 | -------------------------------------------------------------------------------- /src/main/yaml/defaults.yaml: -------------------------------------------------------------------------------- 1 | messageViewers: 2 | - prefix: [openssl, asn1parse, -inform, DER, -i] 3 | inputMethod: stdin 4 | name: OpenSSL ASN.1 decoder 5 | filter: 6 | orElse: 7 | - prefix: !!binary |- 8 | MII= 9 | - prefix: !!binary |- 10 | MIA= 11 | - prefix: [dumpasn1] 12 | inputMethod: filename 13 | name: DumpASN1 14 | filter: 15 | orElse: 16 | - prefix: !!binary |- 17 | MII= 18 | - prefix: !!binary |- 19 | MIA= 20 | - prefix: [python, -m, json.tool] 21 | inputMethod: stdin 22 | name: Python JSON formatter 23 | filter: 24 | orElse: 25 | - {prefix: '{', postfix: '}'} 26 | - {prefix: '[', postfix: ']'} 27 | - prefix: [hd] 28 | inputMethod: stdin 29 | name: hd 30 | - prefix: [protoc, --decode_raw] 31 | inputMethod: stdin 32 | exitCode: [0] 33 | name: ProtoBuf 34 | - prefix: [gron] 35 | inputMethod: stdin 36 | name: gron 37 | filter: {prefix: '{'} 38 | menuItems: 39 | - prefix: [okular] 40 | inputMethod: filename 41 | name: Okular 42 | filter: 43 | regex: 44 | pattern: ^%PDF-1\.[0-9] 45 | flags: [case insensitive, dotall] 46 | hasGUI: true 47 | - prefix: [feh, -FZ] 48 | inputMethod: filename 49 | name: feh 50 | filter: 51 | cmd: 52 | prefix: [file, -i, '-'] 53 | inputMethod: stdin 54 | stdout: 55 | regex: {pattern: image/} 56 | hasGUI: true 57 | maxInputs: 1 58 | - prefix: [urxvt, -e, hexcurse] 59 | inputMethod: filename 60 | name: hexcurse 61 | hasGUI: true 62 | maxInputs: 1 63 | - prefix: [urxvt, -e, vbindiff] 64 | inputMethod: filename 65 | name: vbindiff without headers 66 | hasGUI: true 67 | maxInputs: 2 68 | - prefix: [urxvt, -e, vbindiff] 69 | inputMethod: filename 70 | passHeaders: true 71 | name: vbindiff with headers 72 | hasGUI: true 73 | maxInputs: 2 74 | - prefix: [git, diff, --color=always] 75 | inputMethod: filename 76 | name: git diff 77 | minInputs: 2 78 | maxInputs: 2 79 | - prefix: [git, diff, --color=always, -w] 80 | inputMethod: filename 81 | name: git diff (ignore whitespace) 82 | minInputs: 2 83 | maxInputs: 2 84 | - prefix: [radiff2, -x] 85 | inputMethod: filename 86 | name: radiff2 (two column hexdump diffing) 87 | minInputs: 2 88 | maxInputs: 2 89 | - prefix: [sh, -c, dos2unix | xclip -selection clipboard] 90 | inputMethod: stdin 91 | passHeaders: true 92 | requiredInPath: [dos2unix, xclip] 93 | name: Copy to clipboard with headers without \r 94 | hasGUI: true 95 | - prefix: [meld] 96 | inputMethod: filename 97 | passHeaders: true 98 | name: Meld 99 | hasGUI: true 100 | avoidPipe: true 101 | minInputs: 2 102 | maxInputs: 3 103 | - prefix: [sh, -c, fromdos | xsel -i -b] 104 | inputMethod: stdin 105 | passHeaders: true 106 | requiredInPath: [fromdos, xsel] 107 | name: Copy to clipboard with headers without \r (alt) 108 | hasGUI: true 109 | commentators: 110 | - prefix: [sha256sum] 111 | inputMethod: stdin 112 | name: SHA-256 113 | intruderPayloadProcessors: 114 | - prefix: [sh, -c, dd if=/dev/urandom of=/dev/stdout bs=1 count=`shuf -i1-100 -n1` status=none] 115 | inputMethod: stdin 116 | requiredInPath: [dd, shuf] 117 | name: replace with /dev/urandom 118 | intruderPayloadGenerators: 119 | - prefix: [seq, '5'] 120 | inputMethod: stdin 121 | name: seq 122 | - prefix: [python3, /path/to/exrex.py, 'foobar[a-c]{1,2}[0-9]'] 123 | inputMethod: stdin 124 | name: exrex 125 | -------------------------------------------------------------------------------- /src/main/java/org/zeromq/codec/Z85.java: -------------------------------------------------------------------------------- 1 | package org.zeromq.codec; 2 | 3 | public final class Z85 4 | { 5 | private Z85() 6 | { 7 | } 8 | 9 | public static byte[] ENCODER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 10 | 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '.', '-', 11 | ':', '+', '=', '^', '!', '/', '*', '?', '&', '<', '>', '(', ')', '[', ']', '{', '}', '@', '%', '$', '#', '0' }; 12 | 13 | public static byte[] DECODER = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00, 0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x40, 14 | 0x00, 0x49, 0x42, 0x4A, 0x47, 0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 15 | 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 16 | 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 }; 17 | 18 | /** 19 | * Encode a binary frame as a string using Z85 encoding; 20 | * @param data 21 | * @return 22 | */ 23 | public static String Z85Encoder(byte[] data) 24 | { 25 | if (data == null || data.length % 4 != 0) { 26 | return null; 27 | } 28 | int size = data.length; 29 | int char_nbr = 0; 30 | int byte_nbr = 0; 31 | long value = 0; 32 | byte[] dest = new byte[size * 5 / 4]; 33 | while (byte_nbr < size) { 34 | // Accumulate value in base 256 (binary) 35 | value = value * 256 + (data[byte_nbr++] & 0xFF); // Convert signed 36 | // byte to int 37 | if (byte_nbr % 4 == 0) { 38 | // Output value in base 85 39 | long divisor = 85 * 85 * 85 * 85; 40 | while (divisor > 0) { 41 | int index = (int) (value / divisor % 85); 42 | dest[char_nbr++] = ENCODER[index]; 43 | divisor /= 85; 44 | } 45 | value = 0; 46 | } 47 | } 48 | return new String(dest); 49 | } 50 | 51 | public static byte[] Z85Decoder(String string) 52 | { 53 | // Accepts only strings bounded to 5 bytes 54 | if (string == null || string.length() % 5 != 0) 55 | return null; 56 | 57 | int decoded_size = string.length() * 4 / 5; 58 | byte[] decoded = new byte[decoded_size]; 59 | 60 | int byte_nbr = 0; 61 | int char_nbr = 0; 62 | long value = 0; 63 | while (char_nbr < string.length()) { 64 | // Accumulate value in base 85 65 | value = value * 85 + DECODER[(byte) string.charAt(char_nbr++) - 32]; 66 | if (char_nbr % 5 == 0) { 67 | // Output value in base 256 68 | long divisor = 256 * 256 * 256; 69 | while (divisor > 0) { 70 | decoded[byte_nbr++] = (byte) (value / divisor % 256); 71 | divisor /= 256; 72 | } 73 | value = 0; 74 | } 75 | } 76 | return decoded; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/burp/Editors.kt: -------------------------------------------------------------------------------- 1 | package burp 2 | 3 | import com.redpois0n.terminal.JTerminal 4 | import java.awt.Component 5 | import javax.swing.JScrollPane 6 | import javax.swing.SwingUtilities 7 | import kotlin.concurrent.thread 8 | 9 | abstract class Editor(private val tool: Piper.MessageViewer, 10 | protected val helpers: IExtensionHelpers, 11 | private val callbacks: IBurpExtenderCallbacks) : IMessageEditorTab { 12 | private var msg: ByteArray? = null 13 | 14 | override fun getMessage(): ByteArray? = msg 15 | override fun isModified(): Boolean = false 16 | override fun getTabCaption(): String = tool.common.name 17 | 18 | override fun isEnabled(content: ByteArray?, isRequest: Boolean): Boolean { 19 | if (content == null || !tool.common.isInToolScope(isRequest)) return false 20 | 21 | val rr = RequestResponse.fromBoolean(isRequest) 22 | val payload = getPayload(content, rr) 23 | 24 | if (payload.isEmpty()) return false 25 | 26 | if (!tool.common.hasFilter()) { 27 | val cmd = tool.common.cmd 28 | return !cmd.hasFilter || cmd.matches(payload, helpers, callbacks) // TODO cache output 29 | } 30 | 31 | val mi = MessageInfo(payload, helpers.bytesToString(payload), rr.getHeaders(content, helpers), url = null) 32 | return tool.common.filter.matches(mi, helpers, callbacks) 33 | } 34 | 35 | override fun setMessage(content: ByteArray?, isRequest: Boolean) { 36 | msg = content 37 | if (content == null) return 38 | thread(start = true) { 39 | val input = getPayload(content, RequestResponse.fromBoolean(isRequest)) 40 | tool.common.cmd.execute(input).processOutput(this::outputProcessor) 41 | } 42 | } 43 | 44 | private fun getPayload(content: ByteArray, rr: RequestResponse) = 45 | if (tool.common.cmd.passHeaders) content 46 | else content.copyOfRange(rr.getBodyOffset(content, helpers), content.size) 47 | 48 | abstract fun outputProcessor(process: Process) 49 | 50 | abstract override fun getSelectedData(): ByteArray 51 | abstract override fun getUiComponent(): Component 52 | } 53 | 54 | class TerminalEditor(tool: Piper.MessageViewer, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks) : Editor(tool, helpers, callbacks) { 55 | private val terminal = JTerminal() 56 | private val scrollPane = JScrollPane() 57 | 58 | init { 59 | scrollPane.setViewportView(terminal) 60 | } 61 | 62 | override fun getSelectedData(): ByteArray = helpers.stringToBytes(terminal.selectedText) 63 | override fun getUiComponent(): Component = terminal 64 | 65 | override fun outputProcessor(process: Process) { 66 | terminal.text = "" 67 | for (stream in arrayOf(process.inputStream, process.errorStream)) { 68 | thread { 69 | val reader = stream.bufferedReader() 70 | while (true) { 71 | val line = reader.readLine() ?: break 72 | terminal.append("$line\n") 73 | } 74 | }.start() 75 | } 76 | } 77 | } 78 | 79 | class TextEditor(tool: Piper.MessageViewer, helpers: IExtensionHelpers, 80 | callbacks: IBurpExtenderCallbacks) : Editor(tool, helpers, callbacks) { 81 | private val editor = callbacks.createTextEditor() 82 | 83 | init { 84 | editor.setEditable(false) 85 | } 86 | 87 | override fun getSelectedData(): ByteArray = editor.selectedText 88 | override fun getUiComponent(): Component = editor.component 89 | 90 | override fun outputProcessor(process: Process) { 91 | process.inputStream.use { 92 | val bytes = it.readBytes() 93 | SwingUtilities.invokeLater { editor.text = bytes } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/proto/burp/piper.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Piper for Burp Suite (https://github.com/silentsignal/burp-piper) 3 | * Copyright (c) 2018 Andras Veres-Szentkiralyi 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | syntax = "proto3"; 20 | 21 | package burp; 22 | 23 | message RegularExpression { 24 | string pattern = 1; 25 | int32 flags = 2; 26 | } 27 | 28 | message HeaderMatch { 29 | string header = 1; 30 | RegularExpression regex = 2; 31 | } 32 | 33 | message CommandInvocation { 34 | repeated string prefix = 1; 35 | repeated string postfix = 2; 36 | enum InputMethod { 37 | STDIN = 0; 38 | FILENAME = 1; 39 | } 40 | InputMethod inputMethod = 3; 41 | bool passHeaders = 4; 42 | repeated string requiredInPath = 5; 43 | repeated int32 exitCode = 6; 44 | MessageMatch stdout = 7; 45 | MessageMatch stderr = 8; 46 | } 47 | 48 | message MessageMatch { 49 | bytes prefix = 1; 50 | bytes postfix = 2; 51 | RegularExpression regex = 3; 52 | HeaderMatch header = 4; 53 | CommandInvocation cmd = 5; 54 | bool negation = 6; 55 | repeated MessageMatch andAlso = 7; 56 | repeated MessageMatch orElse = 8; 57 | bool inScope = 9; 58 | } 59 | 60 | message MinimalTool { 61 | string name = 1; 62 | CommandInvocation cmd = 2; 63 | MessageMatch filter = 3; 64 | bool enabled = 4; 65 | enum Scope { 66 | REQUEST_RESPONSE = 0; 67 | REQUEST_ONLY = 1; 68 | RESPONSE_ONLY = 2; 69 | } 70 | Scope scope = 5; 71 | } 72 | 73 | message UserActionTool { 74 | MinimalTool common = 1; 75 | bool hasGUI = 2; 76 | int32 maxInputs = 3; 77 | int32 minInputs = 4; 78 | bool avoidPipe = 5; 79 | } 80 | 81 | message MessageViewer { 82 | MinimalTool common = 1; 83 | bool usesColors = 2; 84 | } 85 | 86 | enum HttpListenerScope { 87 | REQUEST = 0; 88 | RESPONSE = 1; 89 | RESPONSE_WITH_REQUEST = 2; 90 | } 91 | 92 | message HttpListener { 93 | MinimalTool common = 1; 94 | int32 tool = 2; 95 | HttpListenerScope scope = 3; 96 | bool ignoreOutput = 4; 97 | } 98 | 99 | message Commentator { 100 | MinimalTool common = 1; 101 | // 2 was RequestResponse 102 | bool overwrite = 3; 103 | bool applyWithListener = 4; 104 | } 105 | 106 | message Highlighter { 107 | MinimalTool common = 1; 108 | string color = 2; 109 | bool overwrite = 3; 110 | bool applyWithListener = 4; 111 | } 112 | 113 | message Config { 114 | repeated MinimalTool macro = 1; 115 | repeated MessageViewer messageViewer = 2; 116 | repeated UserActionTool menuItem = 3; 117 | repeated HttpListener httpListener = 4; 118 | repeated Commentator commentator = 5; 119 | bool developer = 6; 120 | repeated MinimalTool intruderPayloadProcessor = 7; 121 | repeated Highlighter highlighter = 8; 122 | repeated MinimalTool intruderPayloadGenerator = 9; 123 | } 124 | 125 | message MimeTypes { 126 | repeated Type type = 1; 127 | 128 | message Type { 129 | string name = 1; 130 | repeated Subtype subtype = 2; 131 | } 132 | 133 | message Subtype { 134 | string name = 1; 135 | string extension = 2; 136 | } 137 | } -------------------------------------------------------------------------------- /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="" 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 | -------------------------------------------------------------------------------- /src/main/kotlin/burp/Enums.kt: -------------------------------------------------------------------------------- 1 | package burp 2 | 3 | import org.snakeyaml.engine.v1.api.Dump 4 | import org.snakeyaml.engine.v1.api.DumpSettingsBuilder 5 | import java.awt.Color 6 | import java.util.* 7 | import java.util.regex.Pattern 8 | 9 | @Suppress("UNUSED", "SpellCheckingInspection") 10 | enum class RegExpFlag { 11 | CASE_INSENSITIVE, MULTILINE, DOTALL, UNICODE_CASE, CANON_EQ, 12 | UNIX_LINES, LITERAL, UNICODE_CHARACTER_CLASS, COMMENTS; 13 | 14 | val value = Pattern::class.java.getField(name).getInt(null) 15 | 16 | override fun toString(): String { 17 | return name.toLowerCase().replace('_', ' ') 18 | } 19 | } 20 | 21 | enum class RequestResponse(val isRequest: Boolean, val contexts: Set) { 22 | REQUEST(isRequest = true, contexts = setOf(IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_REQUEST, 23 | IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_REQUEST)) { 24 | override fun getMessage(rr: IHttpRequestResponse): ByteArray? = rr.request 25 | 26 | override fun setMessage(rr: IHttpRequestResponse, value: ByteArray) { 27 | rr.request = value 28 | } 29 | 30 | override fun getBodyOffset(data: ByteArray, helpers: IExtensionHelpers): Int = 31 | helpers.analyzeRequest(data).bodyOffset 32 | 33 | override fun getHeaders(data: ByteArray, helpers: IExtensionHelpers): List = 34 | helpers.analyzeRequest(data).headers 35 | }, 36 | 37 | RESPONSE(isRequest = false, contexts = setOf(IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_RESPONSE, 38 | IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_RESPONSE)) { 39 | override fun getMessage(rr: IHttpRequestResponse): ByteArray? = rr.response 40 | 41 | override fun setMessage(rr: IHttpRequestResponse, value: ByteArray) { 42 | rr.response = value 43 | } 44 | 45 | override fun getBodyOffset(data: ByteArray, helpers: IExtensionHelpers): Int = 46 | helpers.analyzeResponse(data).bodyOffset 47 | 48 | override fun getHeaders(data: ByteArray, helpers: IExtensionHelpers): List = 49 | helpers.analyzeResponse(data).headers 50 | }; 51 | 52 | abstract fun getMessage(rr: IHttpRequestResponse): ByteArray? 53 | abstract fun setMessage(rr: IHttpRequestResponse, value: ByteArray) 54 | abstract fun getBodyOffset(data: ByteArray, helpers: IExtensionHelpers): Int 55 | abstract fun getHeaders(data: ByteArray, helpers: IExtensionHelpers): List 56 | 57 | companion object { 58 | fun fromBoolean(isRequest: Boolean) = if (isRequest) REQUEST else RESPONSE 59 | } 60 | } 61 | 62 | @Suppress("SpellCheckingInspection") 63 | enum class BurpTool { 64 | SUITE, TARGET, PROXY, SPIDER, SCANNER, INTRUDER, REPEATER, SEQUENCER, DECODER, COMPARER, EXTENDER; 65 | 66 | val value = IBurpExtenderCallbacks::class.java.getField("TOOL_$name").getInt(null) 67 | 68 | override fun toString(): String { 69 | return name.toLowerCase().capitalize() 70 | } 71 | } 72 | 73 | enum class MatchNegation(val negation: Boolean, private val description: String) { 74 | NORMAL(false, "Match when all the rules below apply"), 75 | NEGATED(true, "Match when none of the rules below apply"); 76 | 77 | override fun toString(): String = description 78 | } 79 | 80 | enum class Highlight(val color: Color?, val textColor: Color = Color.BLACK) { 81 | CLEAR(null), 82 | RED( Color(0xFF, 0x64, 0x64), Color.WHITE), 83 | ORANGE( Color(0xFF, 0xC8, 0x64) ), 84 | YELLOW( Color(0xFF, 0xFF, 0x64) ), 85 | GREEN( Color(0x64, 0xFF, 0x64) ), 86 | CYAN( Color(0x64, 0xFF, 0xFF) ), 87 | BLUE( Color(0x64, 0x64, 0xFF), Color.WHITE), 88 | PINK( Color(0xFF, 0xC8, 0xC8) ), 89 | MAGENTA(Color(0xFF, 0x64, 0xFF) ), 90 | GRAY( Color(0xB4, 0xB4, 0xB4) ); 91 | 92 | override fun toString(): String = super.toString().toLowerCase() 93 | 94 | val burpValue: String? get() = if (color == null) null else toString() 95 | 96 | companion object { 97 | private val lookupTable = values().associateBy(Highlight::toString) 98 | 99 | fun fromString(value: String): Highlight? = lookupTable[value] 100 | } 101 | } 102 | 103 | enum class ConfigHttpListenerScope(val hls: Piper.HttpListenerScope, val inputList: List) { 104 | REQUEST (Piper.HttpListenerScope.REQUEST, Collections.singletonList(RequestResponse.REQUEST)), 105 | RESPONSE(Piper.HttpListenerScope.RESPONSE, Collections.singletonList(RequestResponse.RESPONSE)), 106 | RESPONSE_WITH_REQUEST(Piper.HttpListenerScope.RESPONSE_WITH_REQUEST, 107 | listOf(RequestResponse.REQUEST, RequestResponse.RESPONSE)) { 108 | override fun toString(): String = "HTTP responses with request prepended" 109 | }; 110 | 111 | override fun toString(): String = "HTTP ${hls.toString().toLowerCase()}s" 112 | 113 | companion object { 114 | fun fromHttpListenerScope(hls: Piper.HttpListenerScope): ConfigHttpListenerScope = values().first { it.hls == hls } 115 | } 116 | } 117 | 118 | enum class ConfigMinimalToolScope(val scope: Piper.MinimalTool.Scope) { 119 | REQUEST_RESPONSE(Piper.MinimalTool.Scope.REQUEST_RESPONSE) { 120 | override fun toString(): String = "HTTP requests and responses" 121 | }, 122 | REQUEST_ONLY(Piper.MinimalTool.Scope.REQUEST_ONLY) { 123 | override fun toString(): String = "HTTP requests only" 124 | }, 125 | RESPONSE_ONLY(Piper.MinimalTool.Scope.RESPONSE_ONLY) { 126 | override fun toString(): String = "HTTP responses only" 127 | }; 128 | 129 | companion object { 130 | fun fromScope(scope: Piper.MinimalTool.Scope): ConfigMinimalToolScope = values().first { it.scope == scope } 131 | } 132 | } 133 | 134 | enum class ConfigFormat { 135 | YAML { 136 | override fun parse(blob: ByteArray): Piper.Config = configFromYaml(String(blob, Charsets.UTF_8)) 137 | override fun serialize(config: Piper.Config): ByteArray = 138 | Dump(DumpSettingsBuilder().build()).dumpToString(config.toSettings()).toByteArray(/* default is UTF-8 */) 139 | 140 | override val fileExtension: String 141 | get() = "yaml" 142 | }, 143 | 144 | PROTOBUF { 145 | override fun parse(blob: ByteArray): Piper.Config = Piper.Config.parseFrom(blob).updateEnabled(false) 146 | override fun serialize(config: Piper.Config): ByteArray = config.updateEnabled(false).toByteArray() 147 | override val fileExtension: String 148 | get() = "pb" 149 | }; 150 | 151 | abstract fun serialize(config: Piper.Config): ByteArray 152 | abstract fun parse(blob: ByteArray): Piper.Config 153 | abstract val fileExtension: String 154 | } 155 | 156 | enum class MessageInfoMatchStrategy { 157 | ANY { override fun predicate(objects: List, check: (MessageInfo) -> Boolean): Boolean = objects.any(check) }, 158 | ALL { override fun predicate(objects: List, check: (MessageInfo) -> Boolean): Boolean = objects.all(check) }; 159 | 160 | abstract fun predicate(objects: List, check: (MessageInfo) -> Boolean): Boolean 161 | } 162 | 163 | enum class CommandInvocationPurpose { 164 | EXECUTE_ONLY, 165 | SELF_FILTER, 166 | MATCH_FILTER; 167 | } -------------------------------------------------------------------------------- /src/main/java/com/redpois0n/terminal/JTerminal.java: -------------------------------------------------------------------------------- 1 | package com.redpois0n.terminal; 2 | 3 | import java.awt.Color; 4 | import java.awt.Font; 5 | import java.awt.event.KeyEvent; 6 | import java.awt.event.KeyListener; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import javax.swing.JTextPane; 13 | import javax.swing.text.SimpleAttributeSet; 14 | import javax.swing.text.StyleConstants; 15 | import javax.swing.text.StyleContext; 16 | import javax.swing.text.StyledDocument; 17 | 18 | @SuppressWarnings("serial") 19 | public class JTerminal extends JTextPane { 20 | 21 | public static final String RESET = "0"; 22 | public static final String BOLD = "1"; 23 | public static final String DIM = "2"; 24 | public static final String UNDERLINED = "4"; 25 | public static final String INVERTED = "7"; 26 | public static final String HIDDEN = "8"; 27 | 28 | public static final Font DEFAULT_FONT; 29 | public static final Color DEFAULT_FOREGROUND = Color.white; 30 | public static final Color DEFAULT_BACKGROUND = Color.black; 31 | public static final char NULL_CHAR = '\u0000'; 32 | 33 | public static final char ESCAPE = 27; 34 | public static final String UNIX_CLEAR = ESCAPE + "[H" + ESCAPE + "[J"; 35 | 36 | public static final Map COLORS = new HashMap(); 37 | 38 | static { 39 | DEFAULT_FONT = new Font("monospaced", Font.PLAIN, 14); 40 | 41 | // Default colors 42 | COLORS.put("30", Color.black); 43 | COLORS.put("31", Color.red.darker()); 44 | COLORS.put("32", Color.green.darker()); 45 | COLORS.put("33", Color.yellow.darker()); 46 | COLORS.put("34", Color.blue); 47 | COLORS.put("35", Color.magenta.darker()); 48 | COLORS.put("36", Color.cyan.darker()); 49 | COLORS.put("37", Color.lightGray); 50 | COLORS.put("39", DEFAULT_FOREGROUND); 51 | 52 | // Bright colors 53 | COLORS.put("90", Color.gray); 54 | COLORS.put("91", Color.red); 55 | COLORS.put("92", Color.green); 56 | COLORS.put("93", Color.yellow); 57 | COLORS.put("94", Color.blue.brighter()); 58 | COLORS.put("95", Color.magenta); 59 | COLORS.put("96", Color.cyan); 60 | COLORS.put("97", Color.white); 61 | 62 | // Background 63 | 64 | // Default colors 65 | COLORS.put("40", Color.black); 66 | COLORS.put("41", Color.red.darker()); 67 | COLORS.put("42", Color.green.darker()); 68 | COLORS.put("43", Color.yellow.darker()); 69 | COLORS.put("44", Color.blue); 70 | COLORS.put("45", Color.magenta.darker()); 71 | COLORS.put("46", Color.cyan.darker()); 72 | COLORS.put("47", Color.lightGray); 73 | COLORS.put("49", DEFAULT_FOREGROUND); 74 | 75 | // Bright colors 76 | COLORS.put("100", Color.gray); 77 | COLORS.put("101", Color.red); 78 | COLORS.put("102", Color.green); 79 | COLORS.put("103", Color.yellow); 80 | COLORS.put("104", Color.blue.brighter()); 81 | COLORS.put("105", Color.magenta); 82 | COLORS.put("106", Color.cyan); 83 | COLORS.put("107", Color.white); 84 | } 85 | 86 | public static boolean isBackground(String s) { 87 | return s.startsWith("4") || s.startsWith("10"); 88 | } 89 | 90 | public static Color getColor(String s) { 91 | Color color = DEFAULT_FOREGROUND; 92 | 93 | boolean bright = s.contains("1;"); 94 | s = s.replace("1;", ""); 95 | 96 | if (s.endsWith("m")) { 97 | s = s.substring(0, s.length() - 1); 98 | } 99 | 100 | if (COLORS.containsKey(s)) { 101 | color = COLORS.get(s); 102 | } 103 | 104 | if (bright) { 105 | color = color.brighter(); 106 | } 107 | 108 | return color; 109 | } 110 | 111 | private List inputListeners = new ArrayList(); 112 | 113 | private StyledDocument doc; 114 | 115 | public JTerminal() { 116 | this.doc = getStyledDocument(); 117 | setFont(DEFAULT_FONT); 118 | setForeground(DEFAULT_FOREGROUND); 119 | setBackground(DEFAULT_BACKGROUND); 120 | setCaret(new TerminalCaret()); 121 | 122 | addKeyListener(new KeyEventListener()); 123 | addInputListener(new InputListener() { 124 | @Override 125 | public void processCommand(JTerminal terminal, char c) { 126 | 127 | } 128 | }); 129 | } 130 | 131 | /** 132 | * Gets main key listener 133 | * @return 134 | */ 135 | public KeyListener getKeyListener() { 136 | return super.getKeyListeners()[0]; 137 | } 138 | 139 | public synchronized void append(String s) { 140 | boolean fg = true; 141 | Color foreground = DEFAULT_FOREGROUND; 142 | Color background = DEFAULT_BACKGROUND; 143 | boolean bold = false; 144 | boolean underline = false; 145 | boolean dim = false; 146 | 147 | StringBuilder s1 = new StringBuilder(); 148 | 149 | for (int cp = 0; cp < s.toCharArray().length; cp++) { 150 | char c = s.charAt(cp); 151 | 152 | if (c == ESCAPE) { 153 | append(s1.toString(), foreground, background, bold, underline); 154 | char next = s.charAt(cp + 1); 155 | 156 | if (next == '[') { 157 | s1 = new StringBuilder(); 158 | cp++; 159 | while ((c = s.charAt(++cp)) != 'm') { 160 | s1.append(c); 161 | } 162 | 163 | String[] attributes = s1.toString().split(";"); 164 | 165 | for (String at : attributes) { 166 | if (at.equals(RESET) || s1.length() == 0) { 167 | foreground = DEFAULT_FOREGROUND; 168 | background = DEFAULT_BACKGROUND; 169 | fg = true; 170 | underline = false; 171 | dim = false; 172 | bold = false; 173 | } else if (at.equals(BOLD)) { 174 | bold = !bold; 175 | } else if (at.equals(DIM)) { 176 | dim = !dim; 177 | } else if (at.equals(INVERTED)) { 178 | fg = !fg; 179 | if (fg) { 180 | Color temp = foreground; 181 | foreground = background; 182 | background = temp; 183 | } else { 184 | Color temp = background; 185 | background = foreground; 186 | foreground = temp; 187 | } 188 | } else if (at.equals(UNDERLINED)) { 189 | underline = !underline; 190 | } else if (s1.length() > 0) { 191 | Color color = getColor(at); 192 | 193 | if (isBackground(at)) { 194 | background = color; 195 | } else { 196 | foreground = color; 197 | } 198 | 199 | if (!fg) { // inverted 200 | Color temp = background; 201 | background = foreground; 202 | foreground = temp; 203 | } 204 | 205 | if (dim) { 206 | foreground = foreground.brighter(); 207 | } 208 | } 209 | } 210 | 211 | s1 = new StringBuilder(); 212 | continue; 213 | } 214 | } 215 | 216 | s1.append(c); 217 | } 218 | 219 | if (s1.length() > 0) { 220 | append(s1.toString(), foreground, background, bold, underline); 221 | } 222 | 223 | setCursorInEnd(); 224 | 225 | } 226 | 227 | public void append(String s, Color fg, Color bg, boolean bold, boolean underline) { 228 | StyleContext sc = StyleContext.getDefaultStyleContext(); 229 | 230 | setCursorInEnd(); 231 | 232 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, fg), false); 233 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Background, bg), false); 234 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Bold, bold), false); 235 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Underline, underline), false); 236 | 237 | replaceSelection(s); 238 | } 239 | 240 | public void setCursorInEnd() { 241 | setCaretPosition(doc.getLength()); 242 | } 243 | 244 | /** 245 | * Called when key pressed, checks if character is valid and checks for combinations such as Ctrl+C 246 | * @param c 247 | */ 248 | public void keyPressed(char c) { 249 | for (InputListener l : inputListeners) { 250 | l.processCommand(this, c); 251 | } 252 | } 253 | 254 | public class KeyEventListener implements KeyListener { 255 | 256 | @Override 257 | public void keyPressed(KeyEvent e) { 258 | if (e.getKeyCode() == KeyEvent.VK_ENTER) { 259 | JTerminal.this.keyPressed('\n'); 260 | } 261 | } 262 | 263 | @Override 264 | public void keyReleased(KeyEvent e) { 265 | if (e.getKeyCode() == KeyEvent.VK_CONTROL) { 266 | //ctrl = false; 267 | } 268 | } 269 | 270 | @Override 271 | public void keyTyped(KeyEvent e) { 272 | JTerminal.this.keyPressed(e.getKeyChar()); 273 | } 274 | } 275 | 276 | public void addInputListener(InputListener listener) { 277 | inputListeners.add(listener); 278 | } 279 | 280 | public void removeInputListener(InputListener listener) { 281 | inputListeners.remove(listener); 282 | } 283 | 284 | } 285 | -------------------------------------------------------------------------------- /src/main/kotlin/burp/Extensions.kt: -------------------------------------------------------------------------------- 1 | package burp 2 | 3 | import com.google.protobuf.ByteString 4 | import java.awt.Window 5 | import java.io.File 6 | import java.io.IOException 7 | import java.io.InputStream 8 | import java.lang.RuntimeException 9 | import java.util.* 10 | import java.util.regex.Pattern 11 | import javax.swing.DefaultListModel 12 | 13 | ////////////////////////////////////// GUI ////////////////////////////////////// 14 | 15 | fun Piper.MessageMatch.toHumanReadable(negation: Boolean, hideParentheses: Boolean = false): String { 16 | val match = this 17 | val negated = negation xor match.negation 18 | val items = sequence { 19 | if (match.prefix != null && !match.prefix.isEmpty) { 20 | val prefix = if (negated) "doesn't start" else "starts" 21 | yield("$prefix with ${match.prefix.toHumanReadable()}") 22 | } 23 | if (match.postfix != null && !match.postfix.isEmpty) { 24 | val prefix = if (negated) "doesn't end" else "ends" 25 | yield("$prefix with ${match.postfix.toHumanReadable()}") 26 | } 27 | if (match.hasRegex()) yield(match.regex.toHumanReadable(negated)) 28 | 29 | if (match.hasHeader()) yield(match.header.toHumanReadable(negated)) 30 | 31 | if (match.hasCmd()) yield(match.cmd.toHumanReadable(negated)) 32 | 33 | if (match.inScope) yield("request is" + (if (negated) "n't" else "") + " in scope") 34 | 35 | if (match.andAlsoCount > 0) { 36 | yield(match.andAlsoList.joinToString(separator = (if (negated) " or " else " and "), 37 | transform = { it.toHumanReadable(negated) } )) 38 | } 39 | 40 | if (match.orElseCount > 0) { 41 | yield(match.orElseList.joinToString(separator = (if (negated) " and " else " or "), 42 | transform = { it.toHumanReadable(negated) } )) 43 | } 44 | }.toList() 45 | val result = items.joinToString(separator = (if (negated) " or " else " and ")).truncateTo(64) 46 | return if (items.size == 1 || hideParentheses) result else "($result)" 47 | } 48 | 49 | fun Piper.HeaderMatch.toHumanReadable(negation: Boolean): String = 50 | "header \"$header\" " + regex.toHumanReadable(negation) 51 | 52 | fun Piper.CommandInvocation.toHumanReadable(negation: Boolean): String = sequence { 53 | if (this@toHumanReadable.exitCodeCount > 0) { 54 | val nt = if (negation) "n't" else "" 55 | val ecl = this@toHumanReadable.exitCodeList 56 | val values = 57 | if (ecl.size == 1) ecl[0].toString() 58 | else ecl.dropLast(1).joinToString(separator = ", ") + " or ${ecl.last()}" 59 | yield("exit code is$nt $values") 60 | } 61 | if (this@toHumanReadable.hasStdout()) { 62 | yield("stdout " + this@toHumanReadable.stdout.toHumanReadable(negation)) 63 | } 64 | if (this@toHumanReadable.hasStderr()) { 65 | yield("stderr " + this@toHumanReadable.stderr.toHumanReadable(negation)) 66 | } 67 | }.joinToString(separator = (if (negation) " or " else " and "), 68 | prefix = "when invoking `${this@toHumanReadable.commandLine}`, ") 69 | 70 | val Piper.CommandInvocation.commandLine: String 71 | get() = sequence { 72 | yieldAll(this@commandLine.prefixList.map(::shellQuote)) 73 | if (this@commandLine.inputMethod == Piper.CommandInvocation.InputMethod.FILENAME) yield(CMDLINE_INPUT_FILENAME_PLACEHOLDER) 74 | yieldAll(this@commandLine.postfixList.map(::shellQuote)) 75 | }.joinToString(separator = " ").truncateTo(64) 76 | 77 | fun shellQuote(s: String): String = if (!s.contains(Regex("[\"\\s\\\\]"))) s 78 | else '"' + s.replace(Regex("[\"\\\\]")) { "\\" + it.groups[0]!!.value } + '"' 79 | 80 | fun String.truncateTo(charLimit: Int): String = if (length < charLimit) this else this.substring(0, charLimit) + "..." 81 | 82 | fun Piper.RegularExpression.toHumanReadable(negation: Boolean): String = 83 | (if (negation) "doesn't match" else "matches") + 84 | " regex \"${this.pattern}\"" + 85 | (if (this.flags == 0) "" else " (${this.flagSet.joinToString(separator = ", ")})") 86 | 87 | fun ByteString.toHumanReadable(): String = if (this.isValidUtf8) '"' + this.toStringUtf8() + '"' 88 | else "bytes " + this.toHexPairs() 89 | 90 | fun ByteString.toHexPairs(): String = this.toByteArray().toHexPairs() 91 | 92 | fun ByteArray.toHexPairs(): String = this.joinToString(separator = ":", 93 | transform = { it.toInt().and(0xFF).toString(16).padStart(2, '0') }) 94 | 95 | ////////////////////////////////////// MATCHING ////////////////////////////////////// 96 | 97 | fun Piper.MinimalTool.isInToolScope(isRequest: Boolean): Boolean = 98 | when (scope) { 99 | Piper.MinimalTool.Scope.REQUEST_ONLY -> isRequest 100 | Piper.MinimalTool.Scope.RESPONSE_ONLY -> !isRequest 101 | else -> true 102 | } 103 | 104 | fun Piper.MinimalTool.canProcess(md: List, mims: MessageInfoMatchStrategy, helpers: IExtensionHelpers, 105 | callbacks: IBurpExtenderCallbacks): Boolean = 106 | !this.hasFilter() || mims.predicate(md) { this.filter.matches(it, helpers, callbacks) } 107 | 108 | fun Piper.MinimalTool.buildEnabled(value: Boolean? = null): Piper.MinimalTool { 109 | val enabled = value ?: try { 110 | this.cmd.checkDependencies() 111 | true 112 | } catch (_: DependencyException) { 113 | false 114 | } 115 | return toBuilder().setEnabled(enabled).build() 116 | } 117 | 118 | fun Piper.UserActionTool.buildEnabled(value: Boolean? = null): Piper.UserActionTool = toBuilder().setCommon(common.buildEnabled(value)).build() 119 | fun Piper.HttpListener .buildEnabled(value: Boolean? = null): Piper.HttpListener = toBuilder().setCommon(common.buildEnabled(value)).build() 120 | fun Piper.MessageViewer .buildEnabled(value: Boolean? = null): Piper.MessageViewer = toBuilder().setCommon(common.buildEnabled(value)).build() 121 | fun Piper.Commentator .buildEnabled(value: Boolean? = null): Piper.Commentator = toBuilder().setCommon(common.buildEnabled(value)).build() 122 | fun Piper.Highlighter .buildEnabled(value: Boolean? = null): Piper.Highlighter = toBuilder().setCommon(common.buildEnabled(value)).build() 123 | 124 | fun Piper.MessageMatch.matches(message: MessageInfo, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean = ( 125 | (this.prefix == null || this.prefix.size() == 0 || message.content.startsWith(this.prefix)) && 126 | (this.postfix == null || this.postfix.size() == 0 || message.content.endsWith(this.postfix)) && 127 | (!this.hasRegex() || this.regex.matches(message.text)) && 128 | (!this.hasCmd() || this.cmd.matches(message.content, helpers, callbacks)) && 129 | 130 | (message.headers == null || !this.hasHeader() || this.header.matches(message.headers)) && 131 | (message.url == null || !this.inScope || callbacks.isInScope(message.url)) && 132 | 133 | (this.andAlsoCount == 0 || this.andAlsoList.all { it.matches(message, helpers, callbacks) }) && 134 | (this.orElseCount == 0 || this.orElseList.any { it.matches(message, helpers, callbacks) }) 135 | ) xor this.negation 136 | 137 | fun ByteArray.startsWith(value: ByteString): Boolean { 138 | val mps = value.size() 139 | return this.size >= mps && this.copyOfRange(0, mps) contentEquals value.toByteArray() 140 | } 141 | 142 | fun ByteArray.endsWith(value: ByteString): Boolean { 143 | val mps = value.size() 144 | val mbs = this.size 145 | return mbs >= mps && this.copyOfRange(mbs - mps, mbs) contentEquals value.toByteArray() 146 | } 147 | 148 | private const val DEFAULT_FILE_EXTENSION = ".bin" 149 | 150 | fun Piper.CommandInvocation.execute(vararg inputs: ByteArray): Pair> = execute(*inputs.map { it to null }.toTypedArray()) 151 | 152 | fun Piper.CommandInvocation.execute(vararg inputs: Pair): Pair> { 153 | val tempFiles = if (this.inputMethod == Piper.CommandInvocation.InputMethod.FILENAME) { 154 | inputs.map { (contents, extension) -> 155 | File.createTempFile("piper-", extension ?: DEFAULT_FILE_EXTENSION).apply { writeBytes(contents) } 156 | } 157 | } else emptyList() 158 | val args = this.prefixList + tempFiles.map(File::getAbsolutePath) + this.postfixList 159 | val p = Runtime.getRuntime().exec(args.toTypedArray()) 160 | if (this.inputMethod == Piper.CommandInvocation.InputMethod.STDIN) { 161 | try { 162 | p.outputStream.use { 163 | inputs.map(Pair::first).forEach(p.outputStream::write) 164 | } 165 | } catch (_: IOException) { 166 | // ignore, see https://github.com/silentsignal/burp-piper/issues/6 167 | } 168 | } 169 | return p to tempFiles 170 | } 171 | 172 | val Piper.CommandInvocationOrBuilder.hasFilter: Boolean 173 | get() = hasStderr() || hasStdout() || exitCodeCount > 0 174 | 175 | fun Piper.CommandInvocation.matches(subject: ByteArray, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean { 176 | val (process, tempFiles) = this.execute(subject) 177 | if ((this.hasStderr() && !this.stderr.matches(process.errorStream, helpers, callbacks)) || 178 | (this.hasStdout() && !this.stdout.matches(process.inputStream, helpers, callbacks))) return false 179 | val exitCode = process.waitFor() 180 | tempFiles.forEach { it.delete() } 181 | return (this.exitCodeCount == 0) || exitCode in this.exitCodeList 182 | } 183 | 184 | fun Piper.MessageMatch.matches(stream: InputStream, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean = 185 | this.matches(stream.readBytes(), helpers, callbacks) 186 | 187 | fun Piper.MessageMatch.matches(data: ByteArray, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean = 188 | this.matches(MessageInfo(data, helpers.bytesToString(data), headers = null, url = null), helpers, callbacks) 189 | 190 | fun Piper.HeaderMatch.matches(headers: List): Boolean = headers.any { 191 | it.startsWith("${this.header}: ", true) && 192 | this.regex.matches(it.substring(this.header.length + 2)) 193 | } 194 | 195 | fun Piper.RegularExpression.matches(subject: String): Boolean = 196 | this.compile().matcher(subject).find() 197 | 198 | fun Piper.RegularExpression.compile(): Pattern = Pattern.compile(this.pattern, this.flags) 199 | 200 | val Piper.RegularExpression.flagSet: Set 201 | get() = calcEnumSet(RegExpFlag::class.java, RegExpFlag::value, flags, EnumSet.noneOf(RegExpFlag::class.java)) 202 | 203 | fun Piper.RegularExpression.Builder.setFlagSet(flags: Set): Piper.RegularExpression.Builder = 204 | this.setFlags(flags.fold(0) { acc: Int, regExpFlag: RegExpFlag -> acc or regExpFlag.value }) 205 | 206 | val Piper.HttpListener.toolSet: Set 207 | get() = calcEnumSet(BurpTool::class.java, BurpTool::value, tool, EnumSet.allOf(BurpTool::class.java)) 208 | 209 | fun Piper.HttpListener.Builder.setToolSet(tools: Set): Piper.HttpListener.Builder = 210 | this.setTool(tools.fold(0) { acc: Int, tool: BurpTool -> acc or tool.value }) 211 | 212 | fun Pair>.processOutput(processor: (Process) -> E): E { 213 | val output = processor(this.first) 214 | this.first.waitFor() 215 | this.second.forEach { it.delete() } 216 | return output 217 | } 218 | 219 | fun > calcEnumSet(enumClass: Class, getter: (E) -> Int, value: Int, zero: Set): Set = 220 | if (value == 0) zero else EnumSet.copyOf(enumClass.enumConstants.filter { (getter(it) and value) != 0 }) 221 | 222 | fun DefaultListModel.map(transform: (S) -> T): Iterable = toIterable().map(transform) 223 | fun DefaultListModel.toIterable(): Iterable = (0 until size).map(this::elementAt) 224 | 225 | class DependencyException(dependency: String) : RuntimeException("Dependent executable `$dependency` cannot be found in \$PATH") 226 | 227 | fun Piper.CommandInvocation.checkDependencies() { 228 | val s = sequence { 229 | if (prefixCount != 0) yield(getPrefix(0)!!) 230 | yieldAll(requiredInPathList) 231 | } 232 | throw DependencyException(s.firstOrNull { !findExecutable(it) } ?: return) 233 | } 234 | 235 | private fun findExecutable(name: String): Boolean { 236 | val endings = if ("Windows" in System.getProperty("os.name")) listOf("", ".cmd", ".exe", ".com", ".bat") else listOf("") 237 | return sequence { 238 | yield(null) // current directory 239 | yieldAll(System.getenv().filterKeys { it.equals("PATH", ignoreCase = true) }.values.map { it.split(File.pathSeparator) }.flatten()) 240 | }.any { parent -> endings.any { ending -> canExecute(File(parent, name + ending)) } } 241 | } 242 | 243 | private fun canExecute(f: File): Boolean = f.exists() && !f.isDirectory && f.canExecute() 244 | 245 | fun Window.repack() { 246 | val oldWidth = width 247 | pack() 248 | val loc = location 249 | setLocation(loc.x + ((oldWidth - width) / 2), loc.y) 250 | } 251 | 252 | fun Piper.Config.updateEnabled(value: Boolean): Piper.Config { 253 | return Piper.Config.newBuilder() 254 | .addAllMacro (macroList .map { it.buildEnabled(value) }) 255 | .addAllMenuItem (menuItemList .map { it.buildEnabled(value) }) 256 | .addAllMessageViewer (messageViewerList .map { it.buildEnabled(value) }) 257 | .addAllHttpListener (httpListenerList .map { it.buildEnabled(value) }) 258 | .addAllCommentator (commentatorList .map { it.buildEnabled(value) }) 259 | .addAllIntruderPayloadProcessor(intruderPayloadProcessorList.map { it.buildEnabled(value) }) 260 | .addAllIntruderPayloadGenerator(intruderPayloadGeneratorList.map { it.buildEnabled(value) }) 261 | .addAllHighlighter (highlighterList .map { it.buildEnabled(value) }) 262 | .build() 263 | } -------------------------------------------------------------------------------- /src/main/kotlin/burp/Serialization.kt: -------------------------------------------------------------------------------- 1 | package burp 2 | 3 | import com.google.protobuf.ByteString 4 | import org.snakeyaml.engine.v1.api.Load 5 | import org.snakeyaml.engine.v1.api.LoadSettingsBuilder 6 | import java.io.ByteArrayOutputStream 7 | import java.io.InputStream 8 | import java.lang.RuntimeException 9 | import java.util.zip.DeflaterOutputStream 10 | import java.util.zip.InflaterInputStream 11 | 12 | fun configFromYaml(value: String): Piper.Config { 13 | val ls = Load(LoadSettingsBuilder().build()) 14 | val b = Piper.Config.newBuilder()!! 15 | with(ls.loadFromString(value) as Map>) { 16 | copyListOfStructured("messageViewers", b::addMessageViewer, ::messageViewerFromMap) 17 | copyListOfStructured("macros", b::addMacro, ::minimalToolFromMap) 18 | copyListOfStructured("menuItems", b::addMenuItem, UserActionToolFromMap) 19 | copyListOfStructured("httpListeners", b::addHttpListener, ::httpListenerFromMap) 20 | copyListOfStructured("commentators", b::addCommentator, ::commentatorFromMap) 21 | copyListOfStructured("intruderPayloadProcessors", b::addIntruderPayloadProcessor, ::minimalToolFromMap) 22 | copyListOfStructured("highlighters", b::addHighlighter, ::highlighterFromMap) 23 | copyListOfStructured("intruderPayloadGenerators", b::addIntruderPayloadGenerator, ::minimalToolFromMap) 24 | } 25 | return b.build() 26 | } 27 | 28 | fun highlighterFromMap(source: Map): Piper.Highlighter { 29 | val b = Piper.Highlighter.newBuilder()!! 30 | source.copyBooleanFlag("overwrite", b::setOverwrite) 31 | source.copyBooleanFlag("applyWithListener", b::setApplyWithListener) 32 | val c = source["color"] 33 | if (c != null) b.color = c.toString() 34 | return b.setCommon(minimalToolFromMap(source)).build() 35 | } 36 | 37 | fun commentatorFromMap(source: Map): Piper.Commentator { 38 | val b = Piper.Commentator.newBuilder()!! 39 | source.copyBooleanFlag("overwrite", b::setOverwrite) 40 | source.copyBooleanFlag("applyWithListener", b::setApplyWithListener) 41 | return b.setCommon(minimalToolFromMap(source)).build() 42 | } 43 | 44 | fun messageViewerFromMap(source: Map): Piper.MessageViewer { 45 | val b = Piper.MessageViewer.newBuilder()!! 46 | source.copyBooleanFlag("usesColors", b::setUsesColors) 47 | return b.setCommon(minimalToolFromMap(source)).build() 48 | } 49 | 50 | fun minimalToolFromMap(source: Map): Piper.MinimalTool { 51 | val b = Piper.MinimalTool.newBuilder()!! 52 | .setName(source.stringOrDie("name")) 53 | .setCmd(commandInvocationFromMap(source)) 54 | val scope = source["scope"] 55 | if (scope != null && scope is String) { 56 | b.scope = enumFromString(scope, Piper.MinimalTool.Scope::class.java) 57 | } 58 | source.copyStructured("filter", b::setFilter, ::messageMatchFromMap) 59 | return b.build() 60 | } 61 | 62 | fun httpListenerFromMap(source: Map): Piper.HttpListener { 63 | val b = Piper.HttpListener.newBuilder()!! 64 | .setScope(enumFromString(source.stringOrDie("scope"), 65 | Piper.HttpListenerScope::class.java)) 66 | val ss = source.stringSequence("tool", required = false) 67 | .map { enumFromString(it, BurpTool::class.java) } 68 | if (ss.isNotEmpty()) b.setToolSet(ss.toSet()) 69 | source.copyBooleanFlag("ignoreOutput", b::setIgnoreOutput) 70 | val minimalToolMap = source.toMutableMap() 71 | minimalToolMap["scope"] = Piper.MinimalTool.Scope.REQUEST_RESPONSE.name 72 | return b.setCommon(minimalToolFromMap(minimalToolMap)).build() 73 | } 74 | 75 | fun commandInvocationFromMap(source: Map): Piper.CommandInvocation { 76 | val b = Piper.CommandInvocation.newBuilder()!! 77 | .addAllPrefix(source.stringSequence("prefix")) 78 | .addAllPostfix(source.stringSequence("postfix", required = false)) 79 | .setInputMethod(enumFromString(source.stringOrDie("inputMethod"), 80 | Piper.CommandInvocation.InputMethod::class.java)) 81 | .addAllRequiredInPath(source.stringSequence("requiredInPath", required = false)) 82 | .addAllExitCode(source.intSequence("exitCode")) 83 | with(source) { 84 | copyBooleanFlag("passHeaders", b::setPassHeaders) 85 | copyStructured("stdout", b::setStdout, ::messageMatchFromMap) 86 | copyStructured("stderr", b::setStderr, ::messageMatchFromMap) 87 | } 88 | return b.build() 89 | } 90 | 91 | fun messageMatchFromMap(source: Map): Piper.MessageMatch { 92 | val b = Piper.MessageMatch.newBuilder()!! 93 | with(source) { 94 | copyBytes("prefix", b::setPrefix) 95 | copyBytes("postfix", b::setPostfix) 96 | copyBooleanFlag("negation", b::setNegation) 97 | copyBooleanFlag("inScope", b::setInScope) 98 | copyStructured("regex", b::setRegex, RegExpFromMap) 99 | copyStructured("header", b::setHeader, HeaderMatchFromMap) 100 | copyStructured("cmd", b::setCmd, ::commandInvocationFromMap) 101 | copyListOfStructured("andAlso", b::addAndAlso, ::messageMatchFromMap) 102 | copyListOfStructured("orElse", b::addOrElse, ::messageMatchFromMap) 103 | } 104 | return b.build() 105 | } 106 | 107 | object HeaderMatchFromMap : (Map) -> Piper.HeaderMatch { 108 | override fun invoke(source: Map): Piper.HeaderMatch { 109 | val b = Piper.HeaderMatch.newBuilder()!! 110 | .setHeader(source.stringOrDie("header")) 111 | source.copyStructured("regex", b::setRegex, RegExpFromMap) 112 | return b.build() 113 | } 114 | } 115 | 116 | object RegExpFromMap : (Map) -> Piper.RegularExpression { 117 | override fun invoke(source: Map): Piper.RegularExpression { 118 | val b = Piper.RegularExpression.newBuilder()!! 119 | .setPattern(source.stringOrDie("pattern")) 120 | val ss = source.stringSequence("flags", required = false) 121 | .map { enumFromString(it, RegExpFlag::class.java) } 122 | if (ss.isNotEmpty()) b.setFlagSet(ss.toSet()) 123 | return b.build() 124 | } 125 | } 126 | 127 | object UserActionToolFromMap : (Map) -> Piper.UserActionTool { 128 | override fun invoke(source: Map): Piper.UserActionTool { 129 | val b = Piper.UserActionTool.newBuilder()!! 130 | .setCommon(minimalToolFromMap(source)) 131 | source.copyBooleanFlag("hasGUI", b::setHasGUI) 132 | source.copyBooleanFlag("avoidPipe", b::setAvoidPipe) 133 | source.copyInt("minInputs", b::setMinInputs) 134 | source.copyInt("maxInputs", b::setMaxInputs) 135 | return b.build() 136 | } 137 | } 138 | 139 | fun Map.copyInt(key: String, setter: (Int) -> Any) { 140 | when (val value = this[key] ?: return) { 141 | is Int -> if (value != 0) setter(value) 142 | else -> throw RuntimeException("Invalid value for $key: $value") 143 | } 144 | } 145 | 146 | fun Map.copyStructured(key: String, setter: (E) -> Any, transform: (Map) -> E) { 147 | when (val value = this[key] ?: return) { 148 | is Map<*, *> -> setter(transform(value as Map)) 149 | else -> throw RuntimeException("Invalid value for $key: $value") 150 | } 151 | } 152 | 153 | fun Map.copyListOfStructured(key: String, setter: (E) -> Any, transform: (Map) -> E) { 154 | when (val value = this[key] ?: return) { 155 | is List<*> -> value.forEach { setter(transform(it as Map)) } 156 | else -> throw RuntimeException("Invalid value for $key: $value") 157 | } 158 | } 159 | 160 | fun Map.copyBytes(key: String, setter: (ByteString) -> Any) { 161 | when (val value = this[key] ?: return) { 162 | is String -> setter(ByteString.copyFromUtf8(value)) 163 | is ByteArray -> setter(ByteString.copyFrom(value)) 164 | else -> throw RuntimeException("Invalid value for $key: $value") 165 | } 166 | } 167 | 168 | fun > enumFromString(value: String, cls: Class): E { 169 | try { 170 | val search = value.replace(' ', '_') 171 | return cls.enumConstants.first { it.name.equals(search, ignoreCase = true) } 172 | } catch (_: NoSuchElementException) { 173 | throw RuntimeException("Invalid value for enumerated type: $value") 174 | } 175 | } 176 | 177 | fun Map.stringOrDie(key: String): String { 178 | when (val value = this[key]) { 179 | null -> throw RuntimeException("Missing value for $key") 180 | is String -> return value 181 | else -> throw RuntimeException("Invalid value for $key: $value") 182 | } 183 | } 184 | 185 | fun Map.stringSequence(key: String, required: Boolean = true): Iterable { 186 | return when (val value = this[key]) { 187 | null -> if (required) throw RuntimeException("Missing list for $key") else return emptyList() 188 | is List<*> -> value.map { 189 | when (it) { 190 | null -> throw RuntimeException("Invalid item for $key") 191 | is String -> return@map it 192 | else -> throw RuntimeException("Invalid value for $key: $it") 193 | } 194 | } 195 | else -> throw RuntimeException("Invalid value for $key: $value") 196 | } 197 | } 198 | 199 | fun Map.intSequence(key: String): Iterable { 200 | return when (val value = this[key]) { 201 | null -> emptyList() 202 | is List<*> -> value.map { 203 | when (it) { 204 | null -> throw RuntimeException("Invalid item for $key") 205 | is Int -> return@map it 206 | else -> throw RuntimeException("Invalid value for $key: $it") 207 | } 208 | } 209 | else -> throw RuntimeException("Invalid value for $key: $value") 210 | } 211 | } 212 | 213 | fun Map.copyBooleanFlag(key: String, setter: (Boolean) -> Any) { 214 | val value = this[key] 215 | if (value != null && value is Boolean && value) setter(true) 216 | } 217 | 218 | fun pad4(value: ByteArray): ByteArray { 219 | val pad = (4 - value.size % 4).toByte() 220 | return value + pad.downTo(1).map { pad }.toByteArray() 221 | } 222 | 223 | @Suppress("SpellCheckingInspection") 224 | fun unpad4(value: ByteArray): ByteArray = 225 | value.dropLast(value.last().toInt()).toByteArray() 226 | 227 | fun compress(value: ByteArray): ByteArray { 228 | val bos = ByteArrayOutputStream() 229 | DeflaterOutputStream(bos).use { it.write(value) } 230 | return bos.toByteArray() 231 | } 232 | 233 | fun decompress(value: ByteArray): ByteArray = 234 | InflaterInputStream(value.inputStream()).use(InputStream::readBytes) 235 | 236 | fun Piper.Config.toSettings(): Map = mutableMapOf().apply { 237 | add("messageViewers", messageViewerList, Piper.MessageViewer::toMap) 238 | add("menuItems", menuItemList, Piper.UserActionTool::toMap) 239 | add("macros", macroList, Piper.MinimalTool::toMap) 240 | add("intruderPayloadProcessors", intruderPayloadProcessorList, Piper.MinimalTool::toMap) 241 | add("httpListeners", httpListenerList, Piper.HttpListener::toMap) 242 | add("commentators", commentatorList, Piper.Commentator::toMap) 243 | add("highlighters", highlighterList, Piper.Highlighter::toMap) 244 | add("intruderPayloadGenerators", intruderPayloadGeneratorList, Piper.MinimalTool::toMap) 245 | } 246 | 247 | fun MutableMap.add(key: String, value: List, transform: (E) -> Any) { 248 | if (value.isNotEmpty()) this[key] = value.map(transform) 249 | } 250 | 251 | fun MutableMap.add(key: String, value: ByteString?) { 252 | if (value == null || value.isEmpty) return 253 | this[key] = if (value.isValidUtf8) value.toStringUtf8() else value.toByteArray() 254 | } 255 | 256 | fun Piper.MessageViewer.toMap(): Map = 257 | if (this.usesColors) this.common.toMap() + ("usesColors" to true) else this.common.toMap() 258 | 259 | fun Piper.UserActionTool.toMap(): Map { 260 | val m = this.common.toMap() 261 | if (this.hasGUI) m["hasGUI"] = true 262 | if (this.avoidPipe) m["avoidPipe"] = true 263 | if (this.minInputs != 0) m["minInputs"] = this.minInputs 264 | if (this.maxInputs != 0) m["maxInputs"] = this.maxInputs 265 | return m 266 | } 267 | 268 | fun Piper.HttpListener.toMap(): Map { 269 | val m = this.common.toMap() 270 | if (this.tool != 0) m["tool"] = this.toolSet.toSortedStringList() 271 | m["scope"] = this.scope.name.toLowerCase() 272 | if (this.ignoreOutput) m["ignoreOutput"] = true 273 | return m 274 | } 275 | 276 | fun Piper.Commentator.toMap(): Map { 277 | val m = this.common.toMap() 278 | if (this.overwrite) m["overwrite"] = true 279 | if (this.applyWithListener) m["applyWithListener"] = true 280 | return m 281 | } 282 | 283 | fun Piper.Highlighter.toMap(): Map { 284 | val m = this.common.toMap() 285 | if (this.overwrite) m["overwrite"] = true 286 | if (this.applyWithListener) m["applyWithListener"] = true 287 | if (!this.color.isNullOrEmpty()) m["color"] = this.color 288 | return m 289 | } 290 | 291 | fun Piper.MinimalTool.toMap(): MutableMap { 292 | val m = this.cmd.toMap() 293 | m["name"] = this.name!! 294 | if (this.hasFilter()) m["filter"] = this.filter.toMap() 295 | if (this.scope != Piper.MinimalTool.Scope.REQUEST_RESPONSE) m["scope"] = this.scope.name.toLowerCase() 296 | return m 297 | } 298 | 299 | fun Piper.MessageMatch.toMap(): Map { 300 | val m = mutableMapOf() 301 | m.add("prefix", this.prefix) 302 | m.add("postfix", this.postfix) 303 | if (this.hasRegex()) m["regex"] = this.regex.toMap() 304 | if (this.hasHeader()) m["header"] = this.header.toMap() 305 | if (this.hasCmd()) m["cmd"] = this.cmd.toMap() 306 | if (this.negation) m["negation"] = true 307 | if (this.inScope) m["inScope"] = true 308 | if (this.orElseCount > 0) m["orElse"] = this.orElseList.map(Piper.MessageMatch::toMap) 309 | if (this.andAlsoCount > 0) m["andAlso"] = this.andAlsoList.map(Piper.MessageMatch::toMap) 310 | return m 311 | } 312 | 313 | fun Piper.CommandInvocation.toMap(): MutableMap { 314 | val m = mutableMapOf() 315 | m["prefix"] = this.prefixList 316 | if (this.postfixCount > 0) m["postfix"] = this.postfixList 317 | m["inputMethod"] = this.inputMethod.name.toLowerCase() 318 | if (this.passHeaders) m["passHeaders"] = true 319 | if (this.requiredInPathCount > 0) m["requiredInPath"] = this.requiredInPathList 320 | if (this.exitCodeCount > 0) m["exitCode"] = this.exitCodeList 321 | if (this.hasStdout()) m["stdout"] = this.stdout.toMap() 322 | if (this.hasStderr()) m["stderr"] = this.stderr.toMap() 323 | return m 324 | } 325 | 326 | fun Piper.RegularExpression.toMap(): Map { 327 | val m = mutableMapOf("pattern" to this.pattern!!) 328 | if (this.flags != 0) m["flags"] = this.flagSet.toSortedStringList() 329 | return m 330 | } 331 | 332 | fun Set.toSortedStringList() = this.asSequence().map { it.toString() }.sorted().toList() 333 | 334 | fun Piper.HeaderMatch.toMap() = mapOf(("header" to this.header), ("regex" to this.regex.toMap())) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom 19 | to share and change all versions of a program--to make sure it remains 20 | free software for all its users. We, the Free Software Foundation, use 21 | the GNU General Public License for most of our software; it applies 22 | also to any other work released this way by its authors. You can apply 23 | it to your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you 34 | have certain responsibilities if you distribute copies of the 35 | software, or if you modify it: responsibilities to respect the freedom 36 | of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the 56 | manufacturer can do so. This is fundamentally incompatible with the 57 | aim of protecting users' freedom to change the software. The 58 | systematic pattern of such abuse occurs in the area of products for 59 | individuals to use, which is precisely where it is most unacceptable. 60 | Therefore, we have designed this version of the GPL to prohibit the 61 | practice for those products. If such problems arise substantially in 62 | other domains, we stand ready to extend this provision to those 63 | domains in future versions of the GPL, as needed to protect the 64 | freedom of users. 65 | 66 | Finally, every program is threatened constantly by software patents. 67 | States should not allow patents to restrict development and use of 68 | software on general-purpose computers, but in those that do, we wish 69 | to avoid the special danger that patents applied to a free program 70 | could make it effectively proprietary. To prevent this, the GPL 71 | assures that patents cannot be used to render the program non-free. 72 | 73 | The precise terms and conditions for copying, distribution and 74 | modification follow. 75 | 76 | ### TERMS AND CONDITIONS 77 | 78 | #### 0. Definitions. 79 | 80 | "This License" refers to version 3 of the GNU General Public License. 81 | 82 | "Copyright" also means copyright-like laws that apply to other kinds 83 | of works, such as semiconductor masks. 84 | 85 | "The Program" refers to any copyrightable work licensed under this 86 | License. Each licensee is addressed as "you". "Licensees" and 87 | "recipients" may be individuals or organizations. 88 | 89 | To "modify" a work means to copy from or adapt all or part of the work 90 | in a fashion requiring copyright permission, other than the making of 91 | an exact copy. The resulting work is called a "modified version" of 92 | the earlier work or a work "based on" the earlier work. 93 | 94 | A "covered work" means either the unmodified Program or a work based 95 | on the Program. 96 | 97 | To "propagate" a work means to do anything with it that, without 98 | permission, would make you directly or secondarily liable for 99 | infringement under applicable copyright law, except executing it on a 100 | computer or modifying a private copy. Propagation includes copying, 101 | distribution (with or without modification), making available to the 102 | public, and in some countries other activities as well. 103 | 104 | To "convey" a work means any kind of propagation that enables other 105 | parties to make or receive copies. Mere interaction with a user 106 | through a computer network, with no transfer of a copy, is not 107 | conveying. 108 | 109 | An interactive user interface displays "Appropriate Legal Notices" to 110 | the extent that it includes a convenient and prominently visible 111 | feature that (1) displays an appropriate copyright notice, and (2) 112 | tells the user that there is no warranty for the work (except to the 113 | extent that warranties are provided), that licensees may convey the 114 | work under this License, and how to view a copy of this License. If 115 | the interface presents a list of user commands or options, such as a 116 | menu, a prominent item in the list meets this criterion. 117 | 118 | #### 1. Source Code. 119 | 120 | The "source code" for a work means the preferred form of the work for 121 | making modifications to it. "Object code" means any non-source form of 122 | a work. 123 | 124 | A "Standard Interface" means an interface that either is an official 125 | standard defined by a recognized standards body, or, in the case of 126 | interfaces specified for a particular programming language, one that 127 | is widely used among developers working in that language. 128 | 129 | The "System Libraries" of an executable work include anything, other 130 | than the work as a whole, that (a) is included in the normal form of 131 | packaging a Major Component, but which is not part of that Major 132 | Component, and (b) serves only to enable use of the work with that 133 | Major Component, or to implement a Standard Interface for which an 134 | implementation is available to the public in source code form. A 135 | "Major Component", in this context, means a major essential component 136 | (kernel, window system, and so on) of the specific operating system 137 | (if any) on which the executable work runs, or a compiler used to 138 | produce the work, or an object code interpreter used to run it. 139 | 140 | The "Corresponding Source" for a work in object code form means all 141 | the source code needed to generate, install, and (for an executable 142 | work) run the object code and to modify the work, including scripts to 143 | control those activities. However, it does not include the work's 144 | System Libraries, or general-purpose tools or generally available free 145 | programs which are used unmodified in performing those activities but 146 | which are not part of the work. For example, Corresponding Source 147 | includes interface definition files associated with source files for 148 | the work, and the source code for shared libraries and dynamically 149 | linked subprograms that the work is specifically designed to require, 150 | such as by intimate data communication or control flow between those 151 | subprograms and other parts of the work. 152 | 153 | The Corresponding Source need not include anything that users can 154 | regenerate automatically from other parts of the Corresponding Source. 155 | 156 | The Corresponding Source for a work in source code form is that same 157 | work. 158 | 159 | #### 2. Basic Permissions. 160 | 161 | All rights granted under this License are granted for the term of 162 | copyright on the Program, and are irrevocable provided the stated 163 | conditions are met. This License explicitly affirms your unlimited 164 | permission to run the unmodified Program. The output from running a 165 | covered work is covered by this License only if the output, given its 166 | content, constitutes a covered work. This License acknowledges your 167 | rights of fair use or other equivalent, as provided by copyright law. 168 | 169 | You may make, run and propagate covered works that you do not convey, 170 | without conditions so long as your license otherwise remains in force. 171 | You may convey covered works to others for the sole purpose of having 172 | them make modifications exclusively for you, or provide you with 173 | facilities for running those works, provided that you comply with the 174 | terms of this License in conveying all material for which you do not 175 | control copyright. Those thus making or running the covered works for 176 | you must do so exclusively on your behalf, under your direction and 177 | control, on terms that prohibit them from making any copies of your 178 | copyrighted material outside their relationship with you. 179 | 180 | Conveying under any other circumstances is permitted solely under the 181 | conditions stated below. Sublicensing is not allowed; section 10 makes 182 | it unnecessary. 183 | 184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 185 | 186 | No covered work shall be deemed part of an effective technological 187 | measure under any applicable law fulfilling obligations under article 188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 189 | similar laws prohibiting or restricting circumvention of such 190 | measures. 191 | 192 | When you convey a covered work, you waive any legal power to forbid 193 | circumvention of technological measures to the extent such 194 | circumvention is effected by exercising rights under this License with 195 | respect to the covered work, and you disclaim any intention to limit 196 | operation or modification of the work as a means of enforcing, against 197 | the work's users, your or third parties' legal rights to forbid 198 | circumvention of technological measures. 199 | 200 | #### 4. Conveying Verbatim Copies. 201 | 202 | You may convey verbatim copies of the Program's source code as you 203 | receive it, in any medium, provided that you conspicuously and 204 | appropriately publish on each copy an appropriate copyright notice; 205 | keep intact all notices stating that this License and any 206 | non-permissive terms added in accord with section 7 apply to the code; 207 | keep intact all notices of the absence of any warranty; and give all 208 | recipients a copy of this License along with the Program. 209 | 210 | You may charge any price or no price for each copy that you convey, 211 | and you may offer support or warranty protection for a fee. 212 | 213 | #### 5. Conveying Modified Source Versions. 214 | 215 | You may convey a work based on the Program, or the modifications to 216 | produce it from the Program, in the form of source code under the 217 | terms of section 4, provided that you also meet all of these 218 | conditions: 219 | 220 | - a) The work must carry prominent notices stating that you modified 221 | it, and giving a relevant date. 222 | - b) The work must carry prominent notices stating that it is 223 | released under this License and any conditions added under 224 | section 7. This requirement modifies the requirement in section 4 225 | to "keep intact all notices". 226 | - c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | - d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | #### 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms of 251 | sections 4 and 5, provided that you also convey the machine-readable 252 | Corresponding Source under the terms of this License, in one of these 253 | ways: 254 | 255 | - a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | - b) Convey the object code in, or embodied in, a physical product 260 | (including a physical distribution medium), accompanied by a 261 | written offer, valid for at least three years and valid for as 262 | long as you offer spare parts or customer support for that product 263 | model, to give anyone who possesses the object code either (1) a 264 | copy of the Corresponding Source for all the software in the 265 | product that is covered by this License, on a durable physical 266 | medium customarily used for software interchange, for a price no 267 | more than your reasonable cost of physically performing this 268 | conveying of source, or (2) access to copy the Corresponding 269 | Source from a network server at no charge. 270 | - c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | - d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | - e) Convey the object code using peer-to-peer transmission, 288 | provided you inform other peers where the object code and 289 | Corresponding Source of the work are being offered to the general 290 | public at no charge under subsection 6d. 291 | 292 | A separable portion of the object code, whose source code is excluded 293 | from the Corresponding Source as a System Library, need not be 294 | included in conveying the object code work. 295 | 296 | A "User Product" is either (1) a "consumer product", which means any 297 | tangible personal property which is normally used for personal, 298 | family, or household purposes, or (2) anything designed or sold for 299 | incorporation into a dwelling. In determining whether a product is a 300 | consumer product, doubtful cases shall be resolved in favor of 301 | coverage. For a particular product received by a particular user, 302 | "normally used" refers to a typical or common use of that class of 303 | product, regardless of the status of the particular user or of the way 304 | in which the particular user actually uses, or expects or is expected 305 | to use, the product. A product is a consumer product regardless of 306 | whether the product has substantial commercial, industrial or 307 | non-consumer uses, unless such uses represent the only significant 308 | mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to 312 | install and execute modified versions of a covered work in that User 313 | Product from a modified version of its Corresponding Source. The 314 | information must suffice to ensure that the continued functioning of 315 | the modified object code is in no case prevented or interfered with 316 | solely because modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or 331 | updates for a work that has been modified or installed by the 332 | recipient, or for the User Product in which it has been modified or 333 | installed. Access to a network may be denied when the modification 334 | itself materially and adversely affects the operation of the network 335 | or violates the rules and protocols for communication across the 336 | network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | #### 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders 364 | of that material) supplement the terms of this License with terms: 365 | 366 | - a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | - b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | - c) Prohibiting misrepresentation of the origin of that material, 372 | or requiring that modified versions of such material be marked in 373 | reasonable ways as different from the original version; or 374 | - d) Limiting the use for publicity purposes of names of licensors 375 | or authors of the material; or 376 | - e) Declining to grant rights under trademark law for use of some 377 | trade names, trademarks, or service marks; or 378 | - f) Requiring indemnification of licensors and authors of that 379 | material by anyone who conveys the material (or modified versions 380 | of it) with contractual assumptions of liability to the recipient, 381 | for any liability that these contractual assumptions directly 382 | impose on those licensors and authors. 383 | 384 | All other non-permissive additional terms are considered "further 385 | restrictions" within the meaning of section 10. If the Program as you 386 | received it, or any part of it, contains a notice stating that it is 387 | governed by this License along with a term that is a further 388 | restriction, you may remove that term. If a license document contains 389 | a further restriction but permits relicensing or conveying under this 390 | License, you may add to a covered work material governed by the terms 391 | of that license document, provided that the further restriction does 392 | not survive such relicensing or conveying. 393 | 394 | If you add terms to a covered work in accord with this section, you 395 | must place, in the relevant source files, a statement of the 396 | additional terms that apply to those files, or a notice indicating 397 | where to find the applicable terms. 398 | 399 | Additional terms, permissive or non-permissive, may be stated in the 400 | form of a separately written license, or stated as exceptions; the 401 | above requirements apply either way. 402 | 403 | #### 8. Termination. 404 | 405 | You may not propagate or modify a covered work except as expressly 406 | provided under this License. Any attempt otherwise to propagate or 407 | modify it is void, and will automatically terminate your rights under 408 | this License (including any patent licenses granted under the third 409 | paragraph of section 11). 410 | 411 | However, if you cease all violation of this License, then your license 412 | from a particular copyright holder is reinstated (a) provisionally, 413 | unless and until the copyright holder explicitly and finally 414 | terminates your license, and (b) permanently, if the copyright holder 415 | fails to notify you of the violation by some reasonable means prior to 416 | 60 days after the cessation. 417 | 418 | Moreover, your license from a particular copyright holder is 419 | reinstated permanently if the copyright holder notifies you of the 420 | violation by some reasonable means, this is the first time you have 421 | received notice of violation of this License (for any work) from that 422 | copyright holder, and you cure the violation prior to 30 days after 423 | your receipt of the notice. 424 | 425 | Termination of your rights under this section does not terminate the 426 | licenses of parties who have received copies or rights from you under 427 | this License. If your rights have been terminated and not permanently 428 | reinstated, you do not qualify to receive new licenses for the same 429 | material under section 10. 430 | 431 | #### 9. Acceptance Not Required for Having Copies. 432 | 433 | You are not required to accept this License in order to receive or run 434 | a copy of the Program. Ancillary propagation of a covered work 435 | occurring solely as a consequence of using peer-to-peer transmission 436 | to receive a copy likewise does not require acceptance. However, 437 | nothing other than this License grants you permission to propagate or 438 | modify any covered work. These actions infringe copyright if you do 439 | not accept this License. Therefore, by modifying or propagating a 440 | covered work, you indicate your acceptance of this License to do so. 441 | 442 | #### 10. Automatic Licensing of Downstream Recipients. 443 | 444 | Each time you convey a covered work, the recipient automatically 445 | receives a license from the original licensors, to run, modify and 446 | propagate that work, subject to this License. You are not responsible 447 | for enforcing compliance by third parties with this License. 448 | 449 | An "entity transaction" is a transaction transferring control of an 450 | organization, or substantially all assets of one, or subdividing an 451 | organization, or merging organizations. If propagation of a covered 452 | work results from an entity transaction, each party to that 453 | transaction who receives a copy of the work also receives whatever 454 | licenses to the work the party's predecessor in interest had or could 455 | give under the previous paragraph, plus a right to possession of the 456 | Corresponding Source of the work from the predecessor in interest, if 457 | the predecessor has it or can get it with reasonable efforts. 458 | 459 | You may not impose any further restrictions on the exercise of the 460 | rights granted or affirmed under this License. For example, you may 461 | not impose a license fee, royalty, or other charge for exercise of 462 | rights granted under this License, and you may not initiate litigation 463 | (including a cross-claim or counterclaim in a lawsuit) alleging that 464 | any patent claim is infringed by making, using, selling, offering for 465 | sale, or importing the Program or any portion of it. 466 | 467 | #### 11. Patents. 468 | 469 | A "contributor" is a copyright holder who authorizes use under this 470 | License of the Program or a work on which the Program is based. The 471 | work thus licensed is called the contributor's "contributor version". 472 | 473 | A contributor's "essential patent claims" are all patent claims owned 474 | or controlled by the contributor, whether already acquired or 475 | hereafter acquired, that would be infringed by some manner, permitted 476 | by this License, of making, using, or selling its contributor version, 477 | but do not include claims that would be infringed only as a 478 | consequence of further modification of the contributor version. For 479 | purposes of this definition, "control" includes the right to grant 480 | patent sublicenses in a manner consistent with the requirements of 481 | this License. 482 | 483 | Each contributor grants you a non-exclusive, worldwide, royalty-free 484 | patent license under the contributor's essential patent claims, to 485 | make, use, sell, offer for sale, import and otherwise run, modify and 486 | propagate the contents of its contributor version. 487 | 488 | In the following three paragraphs, a "patent license" is any express 489 | agreement or commitment, however denominated, not to enforce a patent 490 | (such as an express permission to practice a patent or covenant not to 491 | sue for patent infringement). To "grant" such a patent license to a 492 | party means to make such an agreement or commitment not to enforce a 493 | patent against the party. 494 | 495 | If you convey a covered work, knowingly relying on a patent license, 496 | and the Corresponding Source of the work is not available for anyone 497 | to copy, free of charge and under the terms of this License, through a 498 | publicly available network server or other readily accessible means, 499 | then you must either (1) cause the Corresponding Source to be so 500 | available, or (2) arrange to deprive yourself of the benefit of the 501 | patent license for this particular work, or (3) arrange, in a manner 502 | consistent with the requirements of this License, to extend the patent 503 | license to downstream recipients. "Knowingly relying" means you have 504 | actual knowledge that, but for the patent license, your conveying the 505 | covered work in a country, or your recipient's use of the covered work 506 | in a country, would infringe one or more identifiable patents in that 507 | country that you have reason to believe are valid. 508 | 509 | If, pursuant to or in connection with a single transaction or 510 | arrangement, you convey, or propagate by procuring conveyance of, a 511 | covered work, and grant a patent license to some of the parties 512 | receiving the covered work authorizing them to use, propagate, modify 513 | or convey a specific copy of the covered work, then the patent license 514 | you grant is automatically extended to all recipients of the covered 515 | work and works based on it. 516 | 517 | A patent license is "discriminatory" if it does not include within the 518 | scope of its coverage, prohibits the exercise of, or is conditioned on 519 | the non-exercise of one or more of the rights that are specifically 520 | granted under this License. You may not convey a covered work if you 521 | are a party to an arrangement with a third party that is in the 522 | business of distributing software, under which you make payment to the 523 | third party based on the extent of your activity of conveying the 524 | work, and under which the third party grants, to any of the parties 525 | who would receive the covered work from you, a discriminatory patent 526 | license (a) in connection with copies of the covered work conveyed by 527 | you (or copies made from those copies), or (b) primarily for and in 528 | connection with specific products or compilations that contain the 529 | covered work, unless you entered into that arrangement, or that patent 530 | license was granted, prior to 28 March 2007. 531 | 532 | Nothing in this License shall be construed as excluding or limiting 533 | any implied license or other defenses to infringement that may 534 | otherwise be available to you under applicable patent law. 535 | 536 | #### 12. No Surrender of Others' Freedom. 537 | 538 | If conditions are imposed on you (whether by court order, agreement or 539 | otherwise) that contradict the conditions of this License, they do not 540 | excuse you from the conditions of this License. If you cannot convey a 541 | covered work so as to satisfy simultaneously your obligations under 542 | this License and any other pertinent obligations, then as a 543 | consequence you may not convey it at all. For example, if you agree to 544 | terms that obligate you to collect a royalty for further conveying 545 | from those to whom you convey the Program, the only way you could 546 | satisfy both those terms and this License would be to refrain entirely 547 | from conveying the Program. 548 | 549 | #### 13. Use with the GNU Affero General Public License. 550 | 551 | Notwithstanding any other provision of this License, you have 552 | permission to link or combine any covered work with a work licensed 553 | under version 3 of the GNU Affero General Public License into a single 554 | combined work, and to convey the resulting work. The terms of this 555 | License will continue to apply to the part which is the covered work, 556 | but the special requirements of the GNU Affero General Public License, 557 | section 13, concerning interaction through a network will apply to the 558 | combination as such. 559 | 560 | #### 14. Revised Versions of this License. 561 | 562 | The Free Software Foundation may publish revised and/or new versions 563 | of the GNU General Public License from time to time. Such new versions 564 | will be similar in spirit to the present version, but may differ in 565 | detail to address new problems or concerns. 566 | 567 | Each version is given a distinguishing version number. If the Program 568 | specifies that a certain numbered version of the GNU General Public 569 | License "or any later version" applies to it, you have the option of 570 | following the terms and conditions either of that numbered version or 571 | of any later version published by the Free Software Foundation. If the 572 | Program does not specify a version number of the GNU General Public 573 | License, you may choose any version ever published by the Free 574 | Software Foundation. 575 | 576 | If the Program specifies that a proxy can decide which future versions 577 | of the GNU General Public License can be used, that proxy's public 578 | statement of acceptance of a version permanently authorizes you to 579 | choose that version for the Program. 580 | 581 | Later license versions may give you additional or different 582 | permissions. However, no additional obligations are imposed on any 583 | author or copyright holder as a result of your choosing to follow a 584 | later version. 585 | 586 | #### 15. Disclaimer of Warranty. 587 | 588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 596 | CORRECTION. 597 | 598 | #### 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 609 | 610 | #### 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | ### How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these 626 | terms. 627 | 628 | To do so, attach the following notices to the program. It is safest to 629 | attach them to the start of each source file to most effectively state 630 | the exclusion of warranty; and each file should have at least the 631 | "copyright" line and a pointer to where the full notice is found. 632 | 633 | 634 | Copyright (C) 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper 650 | mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands \`show w' and \`show c' should show the 661 | appropriate parts of the General Public License. Of course, your 662 | program's commands might be different; for a GUI interface, you would 663 | use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or 666 | school, if any, to sign a "copyright disclaimer" for the program, if 667 | necessary. For more information on this, and how to apply and follow 668 | the GNU GPL, see . 669 | 670 | The GNU General Public License does not permit incorporating your 671 | program into proprietary programs. If your program is a subroutine 672 | library, you may consider it more useful to permit linking proprietary 673 | applications with the library. If this is what you want to do, use the 674 | GNU Lesser General Public License instead of this License. But first, 675 | please read . 676 | -------------------------------------------------------------------------------- /src/main/kotlin/burp/BurpExtender.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Piper for Burp Suite (https://github.com/silentsignal/burp-piper) 3 | * Copyright (c) 2018 Andras Veres-Szentkiralyi 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package burp 20 | 21 | import com.redpois0n.terminal.JTerminal 22 | import org.zeromq.codec.Z85 23 | import java.awt.* 24 | import java.awt.event.MouseEvent 25 | import java.awt.event.MouseListener 26 | import java.beans.PropertyChangeListener 27 | import java.beans.PropertyChangeSupport 28 | import java.io.BufferedReader 29 | import java.io.File 30 | import java.io.PrintStream 31 | import java.net.URL 32 | import java.time.LocalDateTime 33 | import java.time.format.DateTimeFormatter 34 | import java.util.* 35 | import javax.swing.* 36 | import javax.swing.event.ListDataEvent 37 | import javax.swing.event.ListDataListener 38 | import javax.swing.event.ListSelectionEvent 39 | import javax.swing.event.ListSelectionListener 40 | import javax.swing.filechooser.FileNameExtensionFilter 41 | import kotlin.concurrent.thread 42 | 43 | 44 | const val NAME = "Piper" 45 | const val EXTENSION_SETTINGS_KEY = "settings" 46 | const val CONFIG_ENV_VAR = "PIPER_CONFIG" 47 | 48 | data class MessageInfo(val content: ByteArray, val text: String, val headers: List?, val url: URL?, val hrr: IHttpRequestResponse? = null) { 49 | val asContentExtensionPair: Pair get() { 50 | return content to fileExtension 51 | } 52 | 53 | private val fileExtension: String? get() { 54 | if (url != null) { 55 | val match = Regex("\\.[a-z0-9]$", RegexOption.IGNORE_CASE).find(url.path) 56 | if (match != null) { 57 | return match.groups[0]!!.value 58 | } 59 | } 60 | headers?.filter { it.startsWith("content-type: ", ignoreCase = true) }?.forEach { 61 | val parts = it.split(' ', ';')[1].split('/') 62 | val ext = mimeTypes[parts[0]]?.get(parts[1]) ?: return@forEach 63 | return ".$ext" 64 | } 65 | return null 66 | } 67 | 68 | // make sure noone tries to compare such objects since they have an array member 69 | override fun equals(other: Any?): Boolean { throw NotImplementedError() } 70 | override fun hashCode(): Int { throw NotImplementedError() } 71 | 72 | companion object { 73 | val mimeTypes: Map> 74 | 75 | init { 76 | val db = Piper.MimeTypes.parseFrom(BurpExtender::class.java.classLoader.getResourceAsStream("mime.pb")) 77 | mimeTypes = db.typeOrBuilderList.map { type -> 78 | type.name to type.subtypeList.map { subtype -> 79 | subtype.name to subtype.extension 80 | }.toMap() 81 | }.toMap() 82 | } 83 | } 84 | } 85 | 86 | class BurpExtender : IBurpExtender, ITab, ListDataListener, IHttpListener { 87 | 88 | private lateinit var callbacks: IBurpExtenderCallbacks 89 | private lateinit var helpers: IExtensionHelpers 90 | private lateinit var configModel: ConfigModel 91 | private val queue = Queue() 92 | private val tabs = JTabbedPane() 93 | 94 | override fun contentsChanged(p0: ListDataEvent?) = saveConfig() 95 | override fun intervalAdded(p0: ListDataEvent?) = saveConfig() 96 | override fun intervalRemoved(p0: ListDataEvent?) = saveConfig() 97 | 98 | private inner class MessageViewerManager : RegisteredToolManager( 99 | configModel.messageViewersModel, callbacks::removeMessageEditorTabFactory, callbacks::registerMessageEditorTabFactory) { 100 | override fun isModelItemEnabled(item: Piper.MessageViewer): Boolean = item.common.enabled 101 | 102 | override fun modelToBurp(modelItem: Piper.MessageViewer): IMessageEditorTabFactory = IMessageEditorTabFactory { _, _ -> 103 | if (modelItem.usesColors) TerminalEditor(modelItem, helpers, callbacks) 104 | else TextEditor(modelItem, helpers, callbacks) 105 | } 106 | } 107 | 108 | private inner class MacroManager : RegisteredToolManager( 109 | configModel.macrosModel, callbacks::removeSessionHandlingAction, callbacks::registerSessionHandlingAction) { 110 | override fun isModelItemEnabled(item: Piper.MinimalTool): Boolean = item.enabled 111 | 112 | override fun modelToBurp(modelItem: Piper.MinimalTool): ISessionHandlingAction = object : ISessionHandlingAction { 113 | override fun performAction(currentRequest: IHttpRequestResponse?, macroItems: Array?) { 114 | modelItem.pipeMessage(Collections.singletonList(RequestResponse.REQUEST), currentRequest ?: return) 115 | } 116 | 117 | override fun getActionName(): String = modelItem.name 118 | } 119 | } 120 | 121 | private inner class HttpListenerManager : RegisteredToolManager( 122 | configModel.httpListenersModel, callbacks::removeHttpListener, callbacks::registerHttpListener) { 123 | override fun isModelItemEnabled(item: Piper.HttpListener): Boolean = item.common.enabled 124 | 125 | override fun modelToBurp(modelItem: Piper.HttpListener): IHttpListener = IHttpListener { toolFlag, messageIsRequest, messageInfo -> 126 | if ((messageIsRequest xor (modelItem.scope == Piper.HttpListenerScope.REQUEST)) 127 | || (modelItem.tool != 0 && (modelItem.tool and toolFlag == 0))) return@IHttpListener 128 | modelItem.common.pipeMessage(ConfigHttpListenerScope.fromHttpListenerScope(modelItem.scope).inputList, messageInfo, modelItem.ignoreOutput) 129 | } 130 | } 131 | 132 | private inner class IntruderPayloadProcessorManager : RegisteredToolManager( 133 | configModel.intruderPayloadProcessorsModel, callbacks::removeIntruderPayloadProcessor, callbacks::registerIntruderPayloadProcessor) { 134 | override fun isModelItemEnabled(item: Piper.MinimalTool): Boolean = item.enabled 135 | 136 | override fun modelToBurp(modelItem: Piper.MinimalTool): IIntruderPayloadProcessor = object : IIntruderPayloadProcessor { 137 | override fun processPayload(currentPayload: ByteArray, originalPayload: ByteArray, baseValue: ByteArray): ByteArray? = 138 | if (modelItem.hasFilter() && !modelItem.filter.matches(MessageInfo(currentPayload, helpers.bytesToString(currentPayload), 139 | headers = null, url = null), helpers, callbacks)) null 140 | else getStdoutWithErrorHandling(modelItem.cmd.execute(currentPayload), modelItem) 141 | 142 | override fun getProcessorName(): String = modelItem.name 143 | } 144 | } 145 | 146 | private inner class IntruderPayloadGeneratorManager : RegisteredToolManager( 147 | configModel.intruderPayloadGeneratorsModel, callbacks::removeIntruderPayloadGeneratorFactory, callbacks::registerIntruderPayloadGeneratorFactory) { 148 | override fun isModelItemEnabled(item: Piper.MinimalTool): Boolean = item.enabled 149 | 150 | override fun modelToBurp(modelItem: Piper.MinimalTool): IIntruderPayloadGeneratorFactory = object : IIntruderPayloadGeneratorFactory { 151 | override fun createNewInstance(attack: IIntruderAttack?): IIntruderPayloadGenerator { 152 | return object : IIntruderPayloadGenerator { 153 | var process: Process? = null 154 | var reader: BufferedReader? = null 155 | 156 | override fun reset() { 157 | reader?.close() 158 | process?.destroy() 159 | process = null 160 | reader = null 161 | } 162 | 163 | override fun getNextPayload(baseValue: ByteArray?): ByteArray = 164 | stdout.readLine().toByteArray(charset = Charsets.ISO_8859_1) 165 | 166 | override fun hasMorePayloads(): Boolean { 167 | val p = process 168 | return p == null || p.isAlive || stdout.ready() 169 | } 170 | 171 | private val stdout: BufferedReader 172 | get() { 173 | val currentReader = reader 174 | return if (currentReader == null) { 175 | val p = modelItem.cmd.execute(ByteArray(0)).first 176 | process = p 177 | val newReader = p.inputStream.bufferedReader(charset = Charsets.ISO_8859_1) 178 | reader = newReader 179 | newReader 180 | } else currentReader 181 | } 182 | } 183 | } 184 | 185 | override fun getGeneratorName(): String = modelItem.name 186 | } 187 | } 188 | 189 | private abstract inner class RegisteredToolManager(private val model: DefaultListModel, 190 | private val remove: (B) -> Unit, 191 | private val add: (B) -> Unit) : ListDataListener { 192 | 193 | private val registeredInBurp: MutableList = model.map(this::modelToRegListItem).toMutableList() 194 | 195 | abstract fun isModelItemEnabled(item: M): Boolean 196 | abstract fun modelToBurp(modelItem: M): B 197 | 198 | private fun modelToRegListItem(modelItem: M): B? = 199 | if (isModelItemEnabled(modelItem)) modelToBurp(modelItem).apply(add) else null 200 | 201 | override fun contentsChanged(e: ListDataEvent) { 202 | for (i in e.index0 .. e.index1) { 203 | val currentRegistered = registeredInBurp[i] 204 | registeredInBurp[i] = modelToRegListItem(model[i]) 205 | remove(currentRegistered ?: continue) 206 | } 207 | saveConfig() 208 | } 209 | 210 | override fun intervalAdded(e: ListDataEvent) { 211 | for (i in e.index0 .. e.index1) registeredInBurp.add(i, modelToRegListItem(model[i])) 212 | saveConfig() 213 | } 214 | 215 | override fun intervalRemoved(e: ListDataEvent) { 216 | for (i in e.index1 downTo e.index0) remove(registeredInBurp.removeAt(i) ?: continue) 217 | saveConfig() 218 | } 219 | } 220 | 221 | override fun registerExtenderCallbacks(callbacks: IBurpExtenderCallbacks) { 222 | this.callbacks = callbacks 223 | helpers = callbacks.helpers 224 | configModel = ConfigModel(loadConfig()) 225 | 226 | configModel.menuItemsModel.addListDataListener(this) // Menu items are loaded on-demand, thus saving the config is enough 227 | configModel.commentatorsModel.addListDataListener(this) // Commentators are menu items as well, see above 228 | configModel.highlightersModel.addListDataListener(this) // Highlighters are menu items as well, see above 229 | configModel.messageViewersModel.addListDataListener(MessageViewerManager()) 230 | configModel.macrosModel.addListDataListener(MacroManager()) 231 | configModel.httpListenersModel.addListDataListener(HttpListenerManager()) 232 | configModel.intruderPayloadProcessorsModel.addListDataListener(IntruderPayloadProcessorManager()) 233 | configModel.intruderPayloadGeneratorsModel.addListDataListener(IntruderPayloadGeneratorManager()) 234 | 235 | configModel.addPropertyChangeListener({ saveConfig() }) 236 | 237 | callbacks.setExtensionName(NAME) 238 | callbacks.registerContextMenuFactory { 239 | val messages = it.selectedMessages 240 | if (messages.isNullOrEmpty()) return@registerContextMenuFactory emptyList() 241 | val topLevel = JMenu(NAME) 242 | val sb = it.selectionBounds 243 | val selectionContext = if (sb == null || sb.toSet().size < 2) null else it.invocationContext to sb 244 | generateContextMenu(messages.asList(), topLevel::add, selectionContext, includeCommentators = true) 245 | if (topLevel.subElements.isEmpty()) return@registerContextMenuFactory emptyList() 246 | return@registerContextMenuFactory Collections.singletonList(topLevel as JMenuItem) 247 | } 248 | 249 | populateTabs(configModel, null) 250 | callbacks.addSuiteTab(this) 251 | callbacks.registerHttpListener(this) // TODO add/remove based on actual demand w.r.t current config 252 | } 253 | 254 | override fun processHttpMessage(toolFlag: Int, messageIsRequest: Boolean, messageInfo: IHttpRequestResponse) { 255 | if (messageIsRequest || toolFlag != IBurpExtenderCallbacks.TOOL_PROXY) return 256 | val messageDetails = messagesToMap(Collections.singleton(messageInfo)) 257 | 258 | configModel.enabledCommentators.filter(Piper.Commentator::getApplyWithListener).forEach { cfgItem -> 259 | messageDetails.filterApplicable(cfgItem.common).forEach { (_, md) -> 260 | performCommentator(cfgItem, md) 261 | } 262 | } 263 | 264 | configModel.enabledHighlighters.filter(Piper.Highlighter::getApplyWithListener).forEach { cfgItem -> 265 | messageDetails.filterApplicable(cfgItem.common).forEach { (_, md) -> 266 | performHighlighter(cfgItem, md) 267 | } 268 | } 269 | } 270 | 271 | private fun Piper.MinimalTool.pipeMessage(rrList: List, messageInfo: IHttpRequestResponse, ignoreOutput: Boolean = false) { 272 | require(rrList.isNotEmpty()) 273 | val body = rrList.map { rr -> 274 | val bytes = rr.getMessage(messageInfo)!! 275 | val headers = rr.getHeaders(bytes, helpers) 276 | val bo = if (this.cmd.passHeaders) 0 else rr.getBodyOffset(bytes, helpers) 277 | val body = if (this.cmd.passHeaders) bytes else { 278 | if (bo < bytes.size - 1) { 279 | bytes.copyOfRange(bo, bytes.size) 280 | } else null // if the request has no body, passHeaders=false tools have no use for it 281 | } 282 | body to headers 283 | } 284 | val (lastBody, headers) = body.last() 285 | if (lastBody == null) return 286 | if (this.hasFilter() && !this.filter.matches(MessageInfo(lastBody, helpers.bytesToString(lastBody), 287 | headers, try { helpers.analyzeRequest(messageInfo).url } catch (_: Exception) { null }), 288 | helpers, callbacks)) return 289 | val input = body.mapNotNull(Pair>::first).toTypedArray() 290 | val replacement = getStdoutWithErrorHandling(this.cmd.execute(*input), this) 291 | if (!ignoreOutput) { 292 | rrList.last().setMessage(messageInfo, 293 | if (this.cmd.passHeaders) replacement else helpers.buildHttpMessage(headers, replacement)) 294 | } 295 | } 296 | 297 | private fun getStdoutWithErrorHandling(executionResult: Pair>, tool: Piper.MinimalTool): ByteArray = 298 | executionResult.processOutput { process -> 299 | if (configModel.developer) { 300 | val stderr = process.errorStream.readBytes() 301 | if (stderr.isNotEmpty()) { 302 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 303 | val ts = LocalDateTime.now().format(formatter) 304 | callbacks.stderr.buffered().use { 305 | it.bufferedWriter().use { w -> 306 | w.newLine() 307 | w.write("${tool.name} called ${tool.cmd.commandLine} at $ts and stderr was not empty:") 308 | w.newLine() 309 | w.newLine() 310 | } 311 | it.write(stderr) 312 | } 313 | } 314 | } 315 | process.inputStream.readBytes() 316 | } 317 | 318 | private fun populateTabs(cfg: ConfigModel, parent: Component?) { 319 | val switchToCommentator = { tabs.selectedIndex = 4 } 320 | 321 | tabs.addTab("Message viewers", MessageViewerListEditor(cfg.messageViewersModel, parent, 322 | cfg.commentatorsModel, switchToCommentator)) 323 | tabs.addTab("Context menu items", MinimalToolListEditor(cfg.menuItemsModel, parent, 324 | ::MenuItemDialog, Piper.UserActionTool::getDefaultInstance, UserActionToolFromMap, Piper.UserActionTool::toMap)) 325 | tabs.addTab("Macros", MinimalToolListEditor(cfg.macrosModel, parent, 326 | ::MacroDialog, Piper.MinimalTool::getDefaultInstance, ::minimalToolFromMap, Piper.MinimalTool::toMap)) 327 | tabs.addTab("HTTP listeners", MinimalToolListEditor(cfg.httpListenersModel, parent, 328 | ::HttpListenerDialog, Piper.HttpListener::getDefaultInstance, ::httpListenerFromMap, Piper.HttpListener::toMap)) 329 | tabs.addTab("Commentators", MinimalToolListEditor(cfg.commentatorsModel, parent, 330 | ::CommentatorDialog, Piper.Commentator::getDefaultInstance, ::commentatorFromMap, Piper.Commentator::toMap)) 331 | tabs.addTab("Intruder payload processors", MinimalToolListEditor(cfg.intruderPayloadProcessorsModel, parent, 332 | ::IntruderPayloadProcessorDialog, Piper.MinimalTool::getDefaultInstance, ::minimalToolFromMap, Piper.MinimalTool::toMap)) 333 | tabs.addTab("Intruder payload generators", MinimalToolListEditor(cfg.intruderPayloadGeneratorsModel, parent, 334 | ::IntruderPayloadGeneratorDialog, Piper.MinimalTool::getDefaultInstance, ::minimalToolFromMap, Piper.MinimalTool::toMap)) 335 | tabs.addTab("Highlighters", MinimalToolListEditor(cfg.highlightersModel, parent, 336 | ::HighlighterDialog, Piper.Highlighter::getDefaultInstance, ::highlighterFromMap, Piper.Highlighter::toMap)) 337 | tabs.addTab("Queue", queue) 338 | tabs.addTab("Load/Save configuration", createLoadSaveUI(cfg, parent)) 339 | tabs.addTab("Developer", createDeveloperUI(cfg)) 340 | } 341 | 342 | private fun createDeveloperUI(cfg: ConfigModel): Component = 343 | JCheckBox("show user interface elements suited for developers").apply { 344 | isSelected = cfg.developer 345 | cfg.addPropertyChangeListener({ isSelected = cfg.developer }) 346 | addChangeListener { cfg.developer = isSelected } 347 | } 348 | 349 | // ITab members 350 | override fun getTabCaption(): String = NAME 351 | override fun getUiComponent(): Component = tabs 352 | 353 | private data class MessageSource(val direction: RequestResponse, val region: Region) : Comparable { 354 | enum class Region(val includeHeaders: Boolean) { 355 | WHOLE_MESSAGE(true), 356 | HTTP_BODY(false), 357 | SELECTION(false); 358 | } 359 | 360 | override fun compareTo(other: MessageSource): Int = 361 | compareValuesBy(this, other, MessageSource::direction, MessageSource::region) 362 | } 363 | 364 | private fun generateContextMenu(messages: Collection, add: (Component) -> Component, 365 | selectionContext: Pair?, includeCommentators: Boolean) { 366 | val msize = messages.size 367 | val plural = if (msize == 1) "" else "s" 368 | val selectionMenu = mutableListOf() 369 | 370 | fun createSubMenu(msrc: MessageSource) : JMenu = JMenu("Process $msize ${msrc.direction.name.toLowerCase()}$plural") 371 | 372 | fun EnumMap.addMenuItemIfApplicable(menuItem: Piper.UserActionTool, mv: Piper.MessageViewer?, msrc: MessageSource, 373 | md: List) { 374 | val (first, second) = if (mv == null) (menuItem.common to null) else (mv.common to menuItem.common) 375 | if (!isToolApplicable(first, msrc, md, MessageInfoMatchStrategy.ALL)) return 376 | val mi = createMenuItem(first, second) { performMenuAction(menuItem, md, mv) } 377 | if (msrc.region == MessageSource.Region.SELECTION) { 378 | selectionMenu.add(mi) 379 | } else { 380 | this.getOrPut(msrc.direction) { createSubMenu(msrc) }.add(mi) 381 | } 382 | } 383 | 384 | val messageDetails = messagesToMap(messages, selectionContext) 385 | val categoryMenus = EnumMap(RequestResponse::class.java) 386 | 387 | for (cfgItem in configModel.enabledMenuItems) { 388 | // TODO check dependencies 389 | if ((cfgItem.maxInputs != 0 && cfgItem.maxInputs < msize) || cfgItem.minInputs > msize) continue 390 | for ((msrc, md) in messageDetails) { 391 | categoryMenus.addMenuItemIfApplicable(cfgItem, null, msrc, md) 392 | if (!cfgItem.common.cmd.passHeaders && !cfgItem.common.hasFilter() && !cfgItem.avoidPipe) { 393 | configModel.enabledMessageViewers.forEach { mv -> 394 | categoryMenus.addMenuItemIfApplicable(cfgItem, mv, msrc, md) 395 | } 396 | } 397 | } 398 | } 399 | 400 | fun addMessageAnnotatorMenuItems(source: List, common: (E) -> Piper.MinimalTool, action: (E, List) -> Unit) { 401 | val childCategoryMenus = EnumMap(RequestResponse::class.java) 402 | 403 | source.forEach { cfgItem -> 404 | messageDetails.filterApplicable(common(cfgItem)).forEach {(msrc, md) -> 405 | val childMenu = childCategoryMenus.getOrPut(msrc.direction) { 406 | categoryMenus[msrc.direction]?.apply { addSeparator() } 407 | ?: createSubMenu(msrc).apply { categoryMenus[msrc.direction] = this } 408 | } 409 | childMenu.add(createMenuItem(common(cfgItem), null) { action(cfgItem, md) }) 410 | } 411 | } 412 | } 413 | 414 | if (includeCommentators) { 415 | addMessageAnnotatorMenuItems(configModel.enabledCommentators, Piper.Commentator::getCommon, ::performCommentator) 416 | addMessageAnnotatorMenuItems(configModel.enabledHighlighters, Piper.Highlighter::getCommon, ::performHighlighter) 417 | } 418 | 419 | categoryMenus.values.map(add) 420 | if (selectionMenu.isNotEmpty()) add(JMenu("Process selection").apply { selectionMenu.map(this::add) }) 421 | add(JMenuItem("Add to queue").apply { addActionListener { queue.add(messages) } }) 422 | } 423 | 424 | private fun messagesToMap(messages: Collection, selectionContext: Pair? = null): Map> { 425 | val messageDetails = TreeMap>() 426 | for (rr in RequestResponse.values()) { 427 | val httpMessages = ArrayList(messages.size) 428 | val httpBodies = ArrayList(messages.size) 429 | val selections = ArrayList(1) 430 | messages.forEach { 431 | val bytes = rr.getMessage(it) ?: return@forEach 432 | val headers = rr.getHeaders(bytes, helpers) 433 | val url = try { helpers.analyzeRequest(it).url } catch (_: Exception) { null } 434 | httpMessages.add(MessageInfo(bytes, helpers.bytesToString(bytes), headers, url, it)) 435 | val bo = rr.getBodyOffset(bytes, helpers) 436 | if (bo < bytes.size - 1) { 437 | // if the request has no body, passHeaders=false actions have no use for it 438 | val body = bytes.copyOfRange(bo, bytes.size) 439 | httpBodies.add(MessageInfo(body, helpers.bytesToString(body), headers, url, it)) 440 | } 441 | if (selectionContext != null) { 442 | val (context, bounds) = selectionContext 443 | if (context in rr.contexts) { 444 | val body = try { 445 | // handle utf-8 content 446 | bytes.decodeToString(throwOnInvalidSequence=true).substring(bounds[0], bounds[1]).encodeToByteArray() 447 | } catch (ex: java.nio.charset.MalformedInputException) { 448 | // Converting to utf-8 string failed. 449 | // Revert to plain byte extraction 450 | bytes.copyOfRange(bounds[0], bounds[1]) 451 | } catch (ex: Exception) { 452 | // What happened here? 453 | ex.printStackTrace(PrintStream(callbacks.stderr)) 454 | bytes.copyOfRange(bounds[0], bounds[1]) 455 | } 456 | selections.add(MessageInfo(body, helpers.bytesToString(body), headers, url, it)) 457 | } 458 | } 459 | } 460 | messageDetails[MessageSource(rr, MessageSource.Region.WHOLE_MESSAGE)] = httpMessages 461 | if (httpBodies.isNotEmpty()) { 462 | messageDetails[MessageSource(rr, MessageSource.Region.HTTP_BODY)] = httpBodies 463 | } 464 | if (selections.isNotEmpty()) { 465 | messageDetails[MessageSource(rr, MessageSource.Region.SELECTION)] = selections 466 | } 467 | } 468 | return messageDetails 469 | } 470 | 471 | private fun createMenuItem(tool: Piper.MinimalTool, pipe: Piper.MinimalTool?, action: () -> Unit) = 472 | JMenuItem(tool.name + (if (pipe == null) "" else " | ${pipe.name}")).apply { addActionListener { action() } } 473 | 474 | private fun Map>.filterApplicable(tool: Piper.MinimalTool): Map> = filter { 475 | (msrc, md) -> isToolApplicable(tool, msrc, md, MessageInfoMatchStrategy.ANY) 476 | } 477 | 478 | private fun isToolApplicable(tool: Piper.MinimalTool, msrc: MessageSource, md: List, mims: MessageInfoMatchStrategy) = 479 | tool.cmd.passHeaders == msrc.region.includeHeaders && tool.isInToolScope(msrc.direction.isRequest) && tool.canProcess(md, mims, helpers, callbacks) 480 | 481 | inner class Queue : JPanel(BorderLayout()), ListDataListener, ListSelectionListener, MouseListener { 482 | 483 | inner class HttpRequestResponse(original: IHttpRequestResponse) : IHttpRequestResponse { 484 | 485 | inner class HttpService(original: IHttpService) : IHttpService { 486 | private val host = original.host 487 | private val port = original.port 488 | private val protocol = original.protocol 489 | 490 | override fun getHost(): String = host 491 | override fun getPort(): Int = port 492 | override fun getProtocol(): String = protocol 493 | } 494 | 495 | private val comment = original.comment 496 | private val highlight = original.highlight 497 | private val httpService = HttpService(original.httpService) 498 | private val request = original.request.clone() 499 | private val response = original.response.clone() 500 | 501 | override fun getComment(): String = comment 502 | override fun getHighlight(): String = highlight 503 | override fun getHttpService(): IHttpService = httpService 504 | override fun getRequest(): ByteArray = request 505 | override fun getResponse(): ByteArray = response 506 | 507 | override fun setComment(comment: String?) {} 508 | override fun setHighlight(color: String?) {} 509 | override fun setHttpService(httpService: IHttpService?) {} 510 | override fun setRequest(message: ByteArray?) {} 511 | override fun setResponse(message: ByteArray?) {} 512 | 513 | override fun toString() = toHumanReadable(this) 514 | } 515 | 516 | private val model = DefaultListModel() 517 | private val pnToolbar = JPanel() 518 | private val listWidget = JList(model) 519 | private val btnProcess = JButton("Process") 520 | 521 | fun add(values: Iterable) = values.map(::HttpRequestResponse).forEach(model::addElement) 522 | 523 | private fun toHumanReadable(value: IHttpRequestResponse): String { 524 | val req = helpers.analyzeRequest(value) 525 | val resp = helpers.analyzeResponse(value.response) 526 | val size = value.response.size - resp.bodyOffset 527 | val plural = if (size == 1) "" else "s" 528 | return "${resp.statusCode} ${req.method} ${req.url} (response size = $size byte$plural)" 529 | } 530 | 531 | private fun addButtons() { 532 | btnProcess.addActionListener { 533 | val b = it.source as Component 534 | val loc = b.locationOnScreen 535 | showMenu(loc.x, loc.y + b.height) 536 | } 537 | 538 | listOf(createRemoveButton(listWidget, model), btnProcess).map(pnToolbar::add) 539 | } 540 | 541 | private fun showMenu(x: Int, y: Int) { 542 | val pm = JPopupMenu() 543 | generateContextMenu(listWidget.selectedValuesList, pm::add, selectionContext = null, includeCommentators = false) 544 | pm.show(this, 0, 0) 545 | pm.setLocation(x, y) 546 | } 547 | 548 | override fun mouseClicked(event: MouseEvent) { 549 | if (event.button == MouseEvent.BUTTON3) { 550 | showMenu(event.xOnScreen, event.yOnScreen) 551 | } 552 | } 553 | 554 | override fun mouseEntered(p0: MouseEvent?) {} 555 | override fun mouseExited(p0: MouseEvent?) {} 556 | override fun mousePressed(p0: MouseEvent?) {} 557 | override fun mouseReleased(p0: MouseEvent?) {} 558 | 559 | override fun valueChanged(p0: ListSelectionEvent?) { updateBtnEnableDisableState() } 560 | override fun contentsChanged(p0: ListDataEvent?) { updateBtnEnableDisableState() } 561 | override fun intervalAdded (p0: ListDataEvent?) { updateBtnEnableDisableState() } 562 | override fun intervalRemoved(p0: ListDataEvent?) { updateBtnEnableDisableState() } 563 | 564 | private fun updateBtnEnableDisableState() { 565 | btnProcess.isEnabled = !listWidget.isSelectionEmpty 566 | } 567 | 568 | init { 569 | listWidget.addListSelectionListener(this) 570 | listWidget.addMouseListener(this) 571 | model.addListDataListener(this) 572 | 573 | addButtons() 574 | updateBtnEnableDisableState() 575 | add(pnToolbar, BorderLayout.NORTH) 576 | add(JScrollPane(listWidget), BorderLayout.CENTER) 577 | } 578 | } 579 | 580 | private fun loadConfig(): Piper.Config { 581 | try { 582 | val env = System.getenv(CONFIG_ENV_VAR) 583 | if (env != null) { 584 | val fmt = if (env.endsWith(".yml") || env.endsWith(".yaml")){ 585 | ConfigFormat.YAML 586 | } else { 587 | ConfigFormat.PROTOBUF 588 | } 589 | val configFile = File(env) 590 | return fmt.parse(configFile.readBytes()).updateEnabled(true) 591 | } 592 | 593 | val serialized = callbacks.loadExtensionSetting(EXTENSION_SETTINGS_KEY) 594 | if (serialized != null) { 595 | return Piper.Config.parseFrom(decompress(unpad4(Z85.Z85Decoder(serialized)))) 596 | } 597 | 598 | throw Exception("Fallback to default config") 599 | } catch (e: Exception) { 600 | val cfgMod = loadDefaultConfig() 601 | saveConfig(cfgMod) 602 | return cfgMod 603 | } 604 | } 605 | 606 | private fun saveConfig(cfg: Piper.Config = configModel.serialize()) { 607 | val serialized = Z85.Z85Encoder(pad4(compress(cfg.toByteArray()))) 608 | callbacks.saveExtensionSetting(EXTENSION_SETTINGS_KEY, serialized) 609 | } 610 | 611 | private fun performMenuAction(cfgItem: Piper.UserActionTool, messages: List, 612 | messageViewer: Piper.MessageViewer?) { 613 | thread { 614 | val (input, tools) = if (messageViewer == null) { 615 | messages.map(MessageInfo::asContentExtensionPair) to Collections.singletonList(cfgItem.common) 616 | } else { 617 | messages.map { msg -> 618 | messageViewer.common.cmd.execute(msg.asContentExtensionPair).processOutput { process -> 619 | process.inputStream.use { it.readBytes() } 620 | } to null 621 | } to listOf(messageViewer.common, cfgItem.common) 622 | } 623 | cfgItem.common.cmd.execute(*input.toTypedArray()).processOutput { process -> 624 | if (!cfgItem.hasGUI) handleGUI(process, tools) 625 | } 626 | }.start() 627 | } 628 | 629 | private fun performCommentator(cfgItem: Piper.Commentator, messages: List) { 630 | messages.forEach { mi -> 631 | val hrr = mi.hrr ?: return@forEach 632 | if ((hrr.comment.isNullOrEmpty() || cfgItem.overwrite) && 633 | (!cfgItem.common.hasFilter() || cfgItem.common.filter.matches(mi, helpers, callbacks))) { 634 | val stdout = cfgItem.common.cmd.execute(mi.asContentExtensionPair).processOutput { process -> 635 | process.inputStream.readBytes() 636 | } 637 | hrr.comment = String(stdout, Charsets.UTF_8) 638 | } 639 | } 640 | } 641 | 642 | private fun performHighlighter(cfgItem: Piper.Highlighter, messages: List) { 643 | messages.forEach { mi -> 644 | val hrr = mi.hrr ?: return@forEach 645 | if ((hrr.highlight.isNullOrEmpty() || cfgItem.overwrite) && 646 | (!cfgItem.common.hasFilter() || cfgItem.common.filter.matches(mi, helpers, callbacks)) && 647 | cfgItem.common.cmd.matches(mi.content, helpers, callbacks)) { 648 | val h = Highlight.fromString(cfgItem.color) ?: return@forEach 649 | hrr.highlight = h.burpValue 650 | } 651 | } 652 | } 653 | 654 | companion object { 655 | @JvmStatic 656 | fun main (args: Array) { 657 | if (args.size > 2 && args[0] == "build-static") { 658 | val map = mutableMapOf>() 659 | File(args[1]).bufferedReader().use { input -> 660 | input.forEachLine { line -> 661 | if (line.startsWith('#')) return@forEachLine 662 | val parts = line.split('\t', ' ').filter(String::isNotEmpty) 663 | val type = parts[0].split('/') 664 | val subtypes = map.getOrPut(type[0]) { mutableListOf() } 665 | val ext = parts[1] 666 | subtypes.add(Piper.MimeTypes.Subtype.newBuilder().setName(type[1]).setExtension(ext).build()) 667 | } 668 | } 669 | val mt = Piper.MimeTypes.newBuilder() 670 | map.forEach { (typeName, subtypes) -> 671 | mt.addType(Piper.MimeTypes.Type.newBuilder().setName(typeName).addAllSubtype(subtypes)) 672 | } 673 | File(args[2]).writeBytes(mt.build().toByteArray()) 674 | return 675 | } 676 | val be = BurpExtender() 677 | val cfg = loadDefaultConfig() 678 | val dialog = JDialog() 679 | be.populateTabs(ConfigModel(cfg), dialog) 680 | showModalDialog(900, 600, be.uiComponent, NAME, dialog, null) 681 | } 682 | } 683 | } 684 | 685 | class ConfigModel(config: Piper.Config = Piper.Config.getDefaultInstance()) { 686 | private val pcs = PropertyChangeSupport(this) 687 | 688 | val enabledMessageViewers get() = messageViewersModel.toIterable().filter { it.common.enabled } 689 | val enabledMenuItems get() = menuItemsModel.toIterable().filter { it.common.enabled } 690 | val enabledCommentators get() = commentatorsModel.toIterable().filter { it.common.enabled } 691 | val enabledHighlighters get() = highlightersModel.toIterable().filter { it.common.enabled } 692 | 693 | val macrosModel = DefaultListModel() 694 | val messageViewersModel = DefaultListModel() 695 | val menuItemsModel = DefaultListModel() 696 | val httpListenersModel = DefaultListModel() 697 | val commentatorsModel = DefaultListModel() 698 | val intruderPayloadProcessorsModel = DefaultListModel() 699 | val highlightersModel = DefaultListModel() 700 | val intruderPayloadGeneratorsModel = DefaultListModel() 701 | 702 | private var _developer = config.developer 703 | var developer: Boolean 704 | get() = _developer 705 | set(value) { 706 | val old = _developer 707 | _developer = value 708 | pcs.firePropertyChange("developer", old, value) 709 | } 710 | 711 | init { fillModels(config) } 712 | 713 | fun addPropertyChangeListener(listener: PropertyChangeListener) { 714 | pcs.addPropertyChangeListener(listener) 715 | } 716 | 717 | fun fillModels(config: Piper.Config) { 718 | fillDefaultModel(config.macroList, macrosModel) 719 | fillDefaultModel(config.messageViewerList, messageViewersModel) 720 | fillDefaultModel(config.menuItemList, menuItemsModel) 721 | fillDefaultModel(config.httpListenerList, httpListenersModel) 722 | fillDefaultModel(config.commentatorList, commentatorsModel) 723 | fillDefaultModel(config.intruderPayloadProcessorList, intruderPayloadProcessorsModel) 724 | fillDefaultModel(config.highlighterList, highlightersModel) 725 | fillDefaultModel(config.intruderPayloadGeneratorList, intruderPayloadGeneratorsModel) 726 | } 727 | 728 | fun serialize(): Piper.Config = Piper.Config.newBuilder() 729 | .addAllMacro(macrosModel.toIterable()) 730 | .addAllMessageViewer(messageViewersModel.toIterable()) 731 | .addAllMenuItem(menuItemsModel.toIterable()) 732 | .addAllHttpListener(httpListenersModel.toIterable()) 733 | .addAllCommentator(commentatorsModel.toIterable()) 734 | .addAllIntruderPayloadProcessor(intruderPayloadProcessorsModel.toIterable()) 735 | .addAllHighlighter(highlightersModel.toIterable()) 736 | .addAllIntruderPayloadGenerator(intruderPayloadGeneratorsModel.toIterable()) 737 | .setDeveloper(developer) 738 | .build() 739 | } 740 | 741 | private fun createLoadSaveUI(cfg: ConfigModel, parent: Component?): Component { 742 | return JPanel().apply { 743 | add(JButton("Load/restore default config").apply { 744 | addActionListener { 745 | if (JOptionPane.showConfirmDialog(parent, 746 | "This will overwrite your currently loaded configuration with the default one. Are you sure?", 747 | "Confirm restoring default configuration", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { 748 | cfg.fillModels(loadDefaultConfig()) 749 | } 750 | } 751 | }) 752 | add(JButton("Export to YAML file" ).apply { addActionListener { exportConfig(ConfigFormat.YAML, cfg, parent) } }) 753 | add(JButton("Export to ProtoBuf file" ).apply { addActionListener { exportConfig(ConfigFormat.PROTOBUF, cfg, parent) } }) 754 | add(JButton("Import from YAML file" ).apply { addActionListener { importConfig(ConfigFormat.YAML, cfg, parent) } }) 755 | add(JButton("Import from ProtoBuf file").apply { addActionListener { importConfig(ConfigFormat.PROTOBUF, cfg, parent) } }) 756 | } 757 | } 758 | 759 | private fun exportConfig(fmt: ConfigFormat, cfg: ConfigModel, parent: Component?) { 760 | val fc = JFileChooser() 761 | fc.fileFilter = FileNameExtensionFilter(fmt.name, fmt.fileExtension) 762 | if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) { 763 | fc.selectedFile.writeBytes(fmt.serialize(cfg.serialize())) 764 | } 765 | } 766 | 767 | private fun importConfig(fmt: ConfigFormat, cfg: ConfigModel, parent: Component?) { 768 | val fc = JFileChooser() 769 | fc.fileFilter = FileNameExtensionFilter(fmt.name, fmt.fileExtension) 770 | if (fc.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { 771 | try { 772 | cfg.fillModels(fmt.parse(fc.selectedFile.readBytes())) 773 | } catch (e: Exception) { 774 | JOptionPane.showMessageDialog(parent, e.message, "Error while importing ${fc.selectedFile}", JOptionPane.ERROR_MESSAGE) 775 | } 776 | } 777 | } 778 | 779 | private fun loadDefaultConfig(): Piper.Config { 780 | // TODO use more efficient Protocol Buffers encoded version 781 | return configFromYaml(BurpExtender::class.java.classLoader 782 | .getResourceAsStream("defaults.yaml").reader().readText()).updateEnabled(true) 783 | } 784 | 785 | private fun handleGUI(process: Process, tools: List) { 786 | val terminal = JTerminal() 787 | val scrollPane = JScrollPane() 788 | scrollPane.setViewportView(terminal) 789 | val frame = JFrame() 790 | with(frame) { 791 | defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE 792 | addKeyListener(terminal.keyListener) 793 | add(scrollPane) 794 | setSize(675, 300) 795 | title = tools.joinToString(separator = " | ", prefix = "$NAME - ", transform = Piper.MinimalTool::getName) 796 | isVisible = true 797 | } 798 | 799 | for (stream in arrayOf(process.inputStream, process.errorStream)) { 800 | thread { 801 | val reader = stream.bufferedReader() 802 | while (true) { 803 | val line = reader.readLine() ?: break 804 | terminal.append("$line\n") 805 | } 806 | }.start() 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /src/main/kotlin/burp/ConfigGUI.kt: -------------------------------------------------------------------------------- 1 | package burp 2 | 3 | import com.google.protobuf.ByteString 4 | import org.snakeyaml.engine.v1.api.Dump 5 | import org.snakeyaml.engine.v1.api.DumpSettingsBuilder 6 | import org.snakeyaml.engine.v1.api.Load 7 | import org.snakeyaml.engine.v1.api.LoadSettingsBuilder 8 | import java.awt.* 9 | import java.awt.datatransfer.* 10 | import java.awt.event.* 11 | import java.util.* 12 | import javax.swing.* 13 | import javax.swing.event.ListDataEvent 14 | import javax.swing.event.ListDataListener 15 | import javax.swing.event.ListSelectionEvent 16 | import javax.swing.event.ListSelectionListener 17 | import kotlin.math.max 18 | 19 | private fun minimalToolHumanReadableName(cfgItem: Piper.MinimalTool) = if (cfgItem.enabled) cfgItem.name else cfgItem.name + " [disabled]" 20 | 21 | const val TOGGLE_DEFAULT = "Toggle enabled" 22 | 23 | abstract class ListEditor(protected val model: DefaultListModel, protected val parent: Component?, 24 | caption: String?) : JPanel(BorderLayout()), ListDataListener, ListCellRenderer, ListSelectionListener { 25 | protected val pnToolbar = JPanel() 26 | protected val listWidget = JList(model) 27 | private val btnClone = JButton("Clone") 28 | private val cr = DefaultListCellRenderer() 29 | 30 | abstract fun editDialog(value: E): E? 31 | abstract fun addDialog(): E? 32 | abstract fun toHumanReadable(value: E): String 33 | 34 | private fun addButtons() { 35 | val btnAdd = JButton("Add") 36 | btnAdd.addActionListener { 37 | model.addElement(addDialog() ?: return@addActionListener) 38 | } 39 | btnClone.addActionListener { 40 | (listWidget.selectedValuesList.reversed().asSequence() zip listWidget.selectedIndices.reversed().asSequence()).forEach {(value, index) -> 41 | model.insertElementAt(value, index) 42 | } 43 | } 44 | 45 | listOf(btnAdd, createRemoveButton(listWidget, model), btnClone).map(pnToolbar::add) 46 | } 47 | 48 | override fun getListCellRendererComponent(list: JList?, value: E, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component { 49 | val c = cr.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) 50 | cr.text = toHumanReadable(value) 51 | return c 52 | } 53 | 54 | override fun valueChanged(p0: ListSelectionEvent?) { updateBtnEnableDisableState() } 55 | override fun contentsChanged(p0: ListDataEvent?) { updateBtnEnableDisableState() } 56 | override fun intervalAdded (p0: ListDataEvent?) { updateBtnEnableDisableState() } 57 | override fun intervalRemoved(p0: ListDataEvent?) { updateBtnEnableDisableState() } 58 | 59 | private fun updateCloneBtnState() { 60 | btnClone.isEnabled = !listWidget.isSelectionEmpty 61 | } 62 | 63 | open fun updateBtnEnableDisableState() { 64 | updateCloneBtnState() 65 | } 66 | 67 | init { 68 | listWidget.addDoubleClickListener { 69 | model[it] = editDialog(model[it]) ?: return@addDoubleClickListener 70 | } 71 | listWidget.cellRenderer = this 72 | 73 | listWidget.addListSelectionListener(this) 74 | model.addListDataListener(this) 75 | 76 | addButtons() 77 | updateCloneBtnState() 78 | if (caption == null) { 79 | add(pnToolbar, BorderLayout.PAGE_START) 80 | } else { 81 | add(pnToolbar, BorderLayout.SOUTH) 82 | add(JLabel(caption), BorderLayout.PAGE_START) 83 | } 84 | add(JScrollPane(listWidget), BorderLayout.CENTER) 85 | } 86 | } 87 | 88 | open class MinimalToolListEditor(model: DefaultListModel, parent: Component?, private val dialog: (E, Component?) -> MinimalToolDialog, 89 | private val default: () -> E, private val fromMap: (Map) -> E, 90 | private val toMap: (E) -> Map) : ListEditor(model, parent, null), ClipboardOwner { 91 | 92 | private val btnEnableDisable = JButton() 93 | private val btnCopy = JButton("Copy") 94 | private val btnPaste = JButton("Paste") 95 | 96 | override fun addDialog(): E? { 97 | val enabledDefault = dialog(default(), parent).buildEnabled(true) 98 | return dialog(enabledDefault, parent).showGUI() 99 | } 100 | 101 | override fun editDialog(value: E): E? = dialog(value, parent).showGUI() 102 | override fun toHumanReadable(value: E): String = dialog(value, parent).toHumanReadable() 103 | 104 | override fun updateBtnEnableDisableState() { 105 | super.updateBtnEnableDisableState() 106 | updateEnableDisableBtnState() 107 | } 108 | 109 | private fun updateEnableDisableBtnState() { 110 | val si = listWidget.selectedIndices 111 | val selectionNotEmpty = si.isNotEmpty() 112 | btnCopy.isEnabled = selectionNotEmpty 113 | btnEnableDisable.isEnabled = selectionNotEmpty 114 | val maxIndex = si.maxOrNull() 115 | btnEnableDisable.text = if (maxIndex == null || maxIndex >= model.size()) TOGGLE_DEFAULT else 116 | { 117 | val states = listWidget.selectedValuesList.map { dialog(it, parent).isToolEnabled() }.toSet() 118 | if (states.size == 1) (if (states.first()) "Disable" else "Enable") else TOGGLE_DEFAULT 119 | } 120 | } 121 | 122 | init { 123 | btnCopy.addActionListener { 124 | Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection( 125 | Dump(DumpSettingsBuilder().build()).dumpToString(toMap(listWidget.selectedValue ?: return@addActionListener))), this) 126 | } 127 | btnPaste.addActionListener { 128 | val s = Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String ?: return@addActionListener 129 | val ls = Load(LoadSettingsBuilder().build()) 130 | try { 131 | model.addElement(fromMap(ls.loadFromString(s) as Map)) 132 | } catch (e: Exception) { 133 | JOptionPane.showMessageDialog(listWidget, e.message) 134 | } 135 | } 136 | btnEnableDisable.addActionListener { 137 | (listWidget.selectedValuesList.asSequence() zip listWidget.selectedIndices.asSequence()).forEach { (value, index) -> 138 | model[index] = dialog(value, parent).buildEnabled(!dialog(value, parent).isToolEnabled()) 139 | } 140 | } 141 | listOf(btnEnableDisable, btnCopy, btnPaste).map(pnToolbar::add) 142 | updateEnableDisableBtnState() 143 | } 144 | 145 | override fun lostOwnership(p0: Clipboard?, p1: Transferable?) {} /* ClipboardOwner */ 146 | } 147 | 148 | class MessageViewerListEditor(model: DefaultListModel, parent: Component?, 149 | private val commentatorModel: DefaultListModel, 150 | private val switchToCommentator: () -> Unit) : 151 | MinimalToolListEditor(model, parent, ::MessageViewerDialog, 152 | Piper.MessageViewer::getDefaultInstance, ::messageViewerFromMap, Piper.MessageViewer::toMap) { 153 | 154 | private val btnConvertToCommentator = JButton("Convert to commentator") 155 | 156 | override fun updateBtnEnableDisableState() { 157 | super.updateBtnEnableDisableState() 158 | updateEnableDisableBtnState() 159 | } 160 | 161 | private fun updateEnableDisableBtnState() { 162 | btnConvertToCommentator.isEnabled = !listWidget.isSelectionEmpty 163 | } 164 | 165 | init { 166 | btnConvertToCommentator.addActionListener { 167 | listWidget.selectedValuesList.forEach { 168 | commentatorModel.addElement(Piper.Commentator.newBuilder().setCommon(it.common).build()) 169 | } 170 | switchToCommentator() 171 | } 172 | pnToolbar.add(btnConvertToCommentator) 173 | updateEnableDisableBtnState() 174 | } 175 | } 176 | 177 | fun JList.addDoubleClickListener(listener: (Int) -> Unit) { 178 | this.addMouseListener(object : MouseAdapter() { 179 | override fun mouseClicked(e: MouseEvent) { 180 | if (e.clickCount == 2) { 181 | listener(this@addDoubleClickListener.locationToIndex(e.point)) 182 | } 183 | } 184 | }) 185 | } 186 | 187 | class CancelClosingWindow : RuntimeException() 188 | 189 | class MinimalToolWidget(tool: Piper.MinimalTool, private val panel: Container, cs: GridBagConstraints, w: Window, 190 | showPassHeaders: Boolean, purpose: CommandInvocationPurpose, showScope: Boolean, showFilter: Boolean) { 191 | private val tfName = createLabeledTextField("Name: ", tool.name, panel, cs) 192 | private val lsScope: JComboBox? = if (showScope) createLabeledWidget("Can handle... ", 193 | JComboBox(ConfigMinimalToolScope.values()).apply { selectedItem = ConfigMinimalToolScope.fromScope(tool.scope) }, panel, cs) else null 194 | private val cbEnabled: JCheckBox 195 | private val cciw: CollapsedCommandInvocationWidget = CollapsedCommandInvocationWidget(w, cmd = tool.cmd, purpose = purpose, showPassHeaders = showPassHeaders) 196 | private val ccmw: CollapsedMessageMatchWidget = CollapsedMessageMatchWidget(w, mm = tool.filter, showHeaderMatch = true, caption = "Filter: ") 197 | 198 | fun toMinimalTool(): Piper.MinimalTool { 199 | if (tfName.text.isEmpty()) throw RuntimeException("Name cannot be empty.") 200 | val command = cciw.value ?: throw RuntimeException("Command must be specified") 201 | try { 202 | if (cbEnabled.isSelected) command.checkDependencies() 203 | } catch (c: DependencyException) { 204 | when (JOptionPane.showConfirmDialog(panel, "${c.message}\n\nAre you sure you want this enabled?")) { 205 | JOptionPane.NO_OPTION -> cbEnabled.isSelected = false 206 | JOptionPane.CANCEL_OPTION -> throw CancelClosingWindow() 207 | } 208 | } 209 | 210 | return Piper.MinimalTool.newBuilder().apply { 211 | name = tfName.text 212 | if (cbEnabled.isSelected) enabled = true 213 | if (ccmw.value != null) filter = ccmw.value 214 | if (lsScope != null) scope = (lsScope.selectedItem as ConfigMinimalToolScope).scope 215 | cmd = command 216 | }.build() 217 | } 218 | 219 | fun addFilterChangeListener(listener: ChangeListener) { 220 | ccmw.addChangeListener(listener) 221 | } 222 | 223 | init { 224 | if (showFilter) ccmw.buildGUI(panel, cs) 225 | cciw.buildGUI(panel, cs) 226 | cbEnabled = createFullWidthCheckBox("Enabled", tool.enabled, panel, cs) 227 | } 228 | } 229 | 230 | abstract class CollapsedWidget(private val w: Window, var value: E?, private val caption: String, val removable: Boolean) : ClipboardOwner { 231 | private val label = JLabel() 232 | private val pnToolbar = JPanel(FlowLayout(FlowLayout.LEFT)) 233 | private val btnRemove = JButton("Remove") 234 | private val btnCopy = JButton("Copy") 235 | private val btnPaste = JButton("Paste") 236 | private val changeListeners = mutableListOf>() 237 | 238 | abstract fun editDialog(value: E, parent: Component): E? 239 | abstract fun toHumanReadable(): String 240 | abstract val asMap: Map? 241 | abstract val default: E 242 | 243 | abstract fun parseMap(map: Map): E 244 | 245 | private fun update() { 246 | label.text = toHumanReadable() + " " 247 | btnRemove.isEnabled = value != null 248 | btnCopy.isEnabled = value != null 249 | w.repack() 250 | changeListeners.forEach { it.valueChanged(value) } 251 | } 252 | 253 | fun addChangeListener(listener: ChangeListener) { 254 | changeListeners.add(listener) 255 | listener.valueChanged(value) 256 | } 257 | 258 | fun buildGUI(panel: Container, cs: GridBagConstraints) { 259 | val btnEditFilter = JButton("Edit...") 260 | btnEditFilter.addActionListener { 261 | value = editDialog(value ?: default, panel) ?: return@addActionListener 262 | update() 263 | } 264 | 265 | update() 266 | cs.gridwidth = 1 267 | cs.gridy++ 268 | 269 | cs.gridx = 0 ; panel.add(JLabel(caption), cs) 270 | cs.gridx = 1 ; panel.add(label, cs) 271 | cs.gridwidth = 2 272 | cs.gridx = 2 ; panel.add(pnToolbar, cs) 273 | 274 | listOf(btnEditFilter, btnCopy, btnPaste).map(pnToolbar::add) 275 | 276 | if (removable) { 277 | pnToolbar.add(btnRemove) 278 | btnRemove.addActionListener { 279 | value = null 280 | update() 281 | } 282 | } 283 | } 284 | 285 | init { 286 | if (value == default) value = null 287 | btnCopy.addActionListener { 288 | Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection( 289 | Dump(DumpSettingsBuilder().build()).dumpToString(asMap ?: return@addActionListener)), this) 290 | } 291 | btnPaste.addActionListener { 292 | val s = Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String ?: return@addActionListener 293 | val ls = Load(LoadSettingsBuilder().build()) 294 | try { 295 | value = parseMap(ls.loadFromString(s) as Map) 296 | update() 297 | } catch (e: Exception) { 298 | JOptionPane.showMessageDialog(w, e.message) 299 | } 300 | } 301 | } 302 | 303 | override fun lostOwnership(p0: Clipboard?, p1: Transferable?) {} /* ClipboardOwner */ 304 | } 305 | 306 | interface ChangeListener { 307 | fun valueChanged(value: E?) 308 | } 309 | 310 | class CollapsedMessageMatchWidget(w: Window, mm: Piper.MessageMatch?, val showHeaderMatch: Boolean, caption: String) : 311 | CollapsedWidget(w, mm, caption, removable = true) { 312 | 313 | override fun editDialog(value: Piper.MessageMatch, parent: Component): Piper.MessageMatch? = 314 | MessageMatchDialog(value, showHeaderMatch = showHeaderMatch, parent = parent).showGUI() 315 | 316 | override fun toHumanReadable(): String = 317 | value?.toHumanReadable(negation = false, hideParentheses = true) ?: "(no filter)" 318 | 319 | override val asMap: Map? 320 | get() = value?.toMap() 321 | 322 | override fun parseMap(map: Map): Piper.MessageMatch = messageMatchFromMap(map) 323 | 324 | override val default: Piper.MessageMatch 325 | get() = Piper.MessageMatch.getDefaultInstance() 326 | } 327 | 328 | class CollapsedCommandInvocationWidget(w: Window, cmd: Piper.CommandInvocation, private val purpose: CommandInvocationPurpose, private val showPassHeaders: Boolean = true) : 329 | CollapsedWidget(w, cmd, "Command: ", removable = (purpose == CommandInvocationPurpose.MATCH_FILTER)) { 330 | 331 | override fun toHumanReadable(): String = (if (purpose == CommandInvocationPurpose.MATCH_FILTER) value?.toHumanReadable(negation = false) else value?.commandLine) ?: "(no command)" 332 | override fun editDialog(value: Piper.CommandInvocation, parent: Component): Piper.CommandInvocation? = 333 | CommandInvocationDialog(value, purpose = purpose, parent = parent, showPassHeaders = showPassHeaders).showGUI() 334 | 335 | override val asMap: Map? 336 | get() = value?.toMap() 337 | 338 | override fun parseMap(map: Map): Piper.CommandInvocation = commandInvocationFromMap(map) 339 | 340 | override val default: Piper.CommandInvocation 341 | get() = Piper.CommandInvocation.getDefaultInstance() 342 | } 343 | 344 | abstract class ConfigDialog(private val parent: Component?, private val caption: String) : JDialog() { 345 | protected val panel = JPanel(GridBagLayout()) 346 | protected val cs = GridBagConstraints().apply { 347 | fill = GridBagConstraints.HORIZONTAL 348 | gridx = 0 349 | gridy = 0 350 | } 351 | private var state: E? = null 352 | 353 | fun showGUI(): E? { 354 | addFullWidthComponent(createOkCancelButtonsPanel(), panel, cs) 355 | title = caption 356 | defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE 357 | add(panel) 358 | rootPane.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) 359 | pack() 360 | setLocationRelativeTo(parent) 361 | isModal = true 362 | isVisible = true 363 | return state 364 | } 365 | 366 | private fun createOkCancelButtonsPanel(): Component { 367 | val btnOK = JButton("OK") 368 | val btnCancel = JButton("Cancel") 369 | rootPane.defaultButton = btnOK 370 | 371 | btnOK.addActionListener { 372 | try { 373 | state = processGUI() 374 | isVisible = false 375 | } catch (e: CancelClosingWindow) { 376 | /* do nothing, just skip closing the window */ 377 | } catch (e: Exception) { 378 | JOptionPane.showMessageDialog(this, e.message) 379 | } 380 | } 381 | 382 | btnCancel.addActionListener { 383 | isVisible = false 384 | } 385 | 386 | return JPanel().apply { 387 | add(btnOK) 388 | add(btnCancel) 389 | } 390 | } 391 | 392 | abstract fun processGUI(): E 393 | } 394 | 395 | abstract class MinimalToolDialog(private val common: Piper.MinimalTool, parent: Component?, noun: String, 396 | showPassHeaders: Boolean = true, showScope: Boolean = false, 397 | showFilter: Boolean = true, 398 | purpose: CommandInvocationPurpose = CommandInvocationPurpose.SELF_FILTER) : 399 | ConfigDialog(parent, if (common.name.isEmpty()) "Add $noun" else "Edit $noun \"${common.name}\"") { 400 | private val mtw = MinimalToolWidget(common, panel, cs, this, showPassHeaders = showPassHeaders, 401 | purpose = purpose, showScope = showScope, showFilter = showFilter) 402 | 403 | override fun processGUI(): E = processGUI(mtw.toMinimalTool()) 404 | 405 | fun isToolEnabled() : Boolean = common.enabled 406 | fun toHumanReadable(): String = minimalToolHumanReadableName(common) 407 | 408 | abstract fun buildEnabled(value: Boolean) : E 409 | abstract fun processGUI(mt: Piper.MinimalTool): E 410 | 411 | protected fun addFilterChangeListener(listener: ChangeListener) { 412 | mtw.addFilterChangeListener(listener) 413 | } 414 | } 415 | 416 | class MessageViewerDialog(private val messageViewer: Piper.MessageViewer, parent: Component?) : 417 | MinimalToolDialog(messageViewer.common, parent, "message viewer", showScope = true) { 418 | 419 | private val cbUsesColors = createFullWidthCheckBox("Uses ANSI (color) escape sequences", messageViewer.usesColors, panel, cs) 420 | 421 | override fun processGUI(mt: Piper.MinimalTool): Piper.MessageViewer = Piper.MessageViewer.newBuilder().apply { 422 | common = mt 423 | if (cbUsesColors.isSelected) usesColors = true 424 | }.build() 425 | 426 | override fun buildEnabled(value: Boolean): Piper.MessageViewer = messageViewer.buildEnabled(value) 427 | } 428 | 429 | const val HTTP_LISTENER_NOTE = "Note: Piper settings are global and thus apply to all your Burp projects.
HTTP listeners without filters might have hard-to-debug side effects, you've been warned." 430 | 431 | class HttpListenerDialog(private val httpListener: Piper.HttpListener, parent: Component?) : 432 | MinimalToolDialog(httpListener.common, parent, "HTTP listener") { 433 | 434 | private val lsScope = createLabeledWidget("Listen to ", 435 | JComboBox(ConfigHttpListenerScope.values()).apply { selectedItem = ConfigHttpListenerScope.fromHttpListenerScope(httpListener.scope) }, panel, cs) 436 | private val btw = EnumSetWidget(httpListener.toolSet, panel, cs, "sent/received by", BurpTool::class.java) 437 | private val cbIgnore = createFullWidthCheckBox("Ignore output (if you only need side effects)", httpListener.ignoreOutput, panel, cs) 438 | private val lbNote = addFullWidthComponent(JLabel(HTTP_LISTENER_NOTE), panel, cs) 439 | 440 | init { 441 | addFilterChangeListener(object : ChangeListener { 442 | override fun valueChanged(value: Piper.MessageMatch?) { 443 | lbNote.isVisible = value == null 444 | repack() 445 | } 446 | }) 447 | } 448 | 449 | override fun processGUI(mt: Piper.MinimalTool): Piper.HttpListener { 450 | val bt = btw.toSet() 451 | return Piper.HttpListener.newBuilder().apply { 452 | common = mt 453 | scope = (lsScope.selectedItem as ConfigHttpListenerScope).hls 454 | if (cbIgnore.isSelected) ignoreOutput = true 455 | if (bt.size < BurpTool.values().size) setToolSet(bt) 456 | }.build() 457 | } 458 | 459 | override fun buildEnabled(value: Boolean): Piper.HttpListener = httpListener.buildEnabled(value) 460 | } 461 | 462 | class CommentatorDialog(private val commentator: Piper.Commentator, parent: Component?) : 463 | MinimalToolDialog(commentator.common, parent, "commentator", showScope = true) { 464 | 465 | private val cbOverwrite: JCheckBox = createFullWidthCheckBox("Overwrite comments on items that already have one", commentator.overwrite, panel, cs) 466 | private val cbListener: JCheckBox = createFullWidthCheckBox("Continuously apply to future requests/responses", commentator.applyWithListener, panel, cs) 467 | 468 | override fun processGUI(mt: Piper.MinimalTool): Piper.Commentator = Piper.Commentator.newBuilder().apply { 469 | common = mt 470 | if (cbOverwrite.isSelected) overwrite = true 471 | if (cbListener.isSelected) applyWithListener = true 472 | }.build() 473 | 474 | override fun buildEnabled(value: Boolean): Piper.Commentator = commentator.buildEnabled(value) 475 | } 476 | 477 | class HighlighterDialog(private val highlighter: Piper.Highlighter, parent: Component?) : 478 | MinimalToolDialog(highlighter.common, parent, "highlighter", showScope = true) { 479 | 480 | private val cbOverwrite: JCheckBox = createFullWidthCheckBox("Overwrite highlight on items that already have one", highlighter.overwrite, panel, cs) 481 | private val cbListener: JCheckBox = createFullWidthCheckBox("Continuously apply to future requests/responses", highlighter.applyWithListener, panel, cs) 482 | private val cbColor = createLabeledWidget("Set highlight to ", JComboBox(Highlight.values()), panel, cs) 483 | 484 | init { 485 | cbColor.renderer = object : DefaultListCellRenderer() { 486 | override fun getListCellRendererComponent(list: JList<*>?, value: Any?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component { 487 | val c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) 488 | val v = value as Highlight 489 | if (v.color != null) { 490 | c.background = v.color 491 | c.foreground = v.textColor 492 | } 493 | return c 494 | } 495 | } 496 | val h = Highlight.fromString(highlighter.color) 497 | if (h != null) cbColor.selectedItem = h 498 | } 499 | 500 | override fun processGUI(mt: Piper.MinimalTool): Piper.Highlighter = Piper.Highlighter.newBuilder().apply { 501 | common = mt 502 | color = cbColor.selectedItem.toString() 503 | if (cbOverwrite.isSelected) overwrite = true 504 | if (cbListener.isSelected) applyWithListener = true 505 | }.build() 506 | 507 | override fun buildEnabled(value: Boolean): Piper.Highlighter = highlighter.buildEnabled(value) 508 | } 509 | 510 | private fun createFullWidthCheckBox(caption: String, initialValue: Boolean, panel: Container, cs: GridBagConstraints): JCheckBox { 511 | cs.gridwidth = 4 512 | cs.gridx = 0 513 | cs.gridy++ 514 | return createCheckBox(caption, initialValue, panel, cs) 515 | } 516 | 517 | private fun createCheckBox(caption: String, initialValue: Boolean, panel: Container, cs: GridBagConstraints): JCheckBox { 518 | val cb = JCheckBox(caption) 519 | cb.isSelected = initialValue 520 | panel.add(cb, cs) 521 | return cb 522 | } 523 | 524 | class MenuItemDialog(private val menuItem: Piper.UserActionTool, parent: Component?) : 525 | MinimalToolDialog(menuItem.common, parent, "menu item", 526 | purpose = CommandInvocationPurpose.EXECUTE_ONLY, showScope = true) { 527 | 528 | private val cbHasGUI: JCheckBox = createFullWidthCheckBox("Has its own GUI (no need for a console window)", menuItem.hasGUI, panel, cs) 529 | private val cbAvoidPipe: JCheckBox = createFullWidthCheckBox("Avoid piping into this tool (reduces clutter in menu if it doesn't make sense)", menuItem.avoidPipe, panel, cs) 530 | private val smMinInputs: SpinnerNumberModel = createSpinner("Minimum required number of selected items: ", 531 | max(menuItem.minInputs, 1), 1, panel, cs) 532 | private val smMaxInputs: SpinnerNumberModel = createSpinner("Maximum allowed number of selected items: (0 = no limit) ", 533 | menuItem.maxInputs, 0, panel, cs) 534 | 535 | override fun processGUI(mt: Piper.MinimalTool): Piper.UserActionTool { 536 | val minInputsValue = smMinInputs.number.toInt() 537 | val maxInputsValue = smMaxInputs.number.toInt() 538 | 539 | if (maxInputsValue in 1 until minInputsValue) throw RuntimeException( 540 | "Maximum allowed number of selected items cannot be lower than minimum required number of selected items.") 541 | 542 | return Piper.UserActionTool.newBuilder().apply { 543 | common = mt 544 | if (cbHasGUI.isSelected) hasGUI = true 545 | if (cbAvoidPipe.isSelected) avoidPipe = true 546 | if (minInputsValue > 1) minInputs = minInputsValue 547 | if (maxInputsValue > 0) maxInputs = maxInputsValue 548 | }.build() 549 | } 550 | 551 | override fun buildEnabled(value: Boolean): Piper.UserActionTool = menuItem.buildEnabled(value) 552 | } 553 | 554 | private fun createSpinner(caption: String, initial: Int, minimum: Int, panel: Container, cs: GridBagConstraints): SpinnerNumberModel { 555 | val model = SpinnerNumberModel(initial, minimum, Integer.MAX_VALUE, 1) 556 | 557 | cs.gridy++ 558 | cs.gridwidth = 2 559 | cs.gridx = 0 ; panel.add(JLabel(caption), cs) 560 | cs.gridx = 2 ; panel.add(JSpinner(model), cs) 561 | 562 | return model 563 | } 564 | 565 | class IntruderPayloadProcessorDialog(private val ipp: Piper.MinimalTool, parent: Component?) : 566 | MinimalToolDialog(ipp, parent, "Intruder payload processor", showPassHeaders = false) { 567 | 568 | override fun processGUI(mt: Piper.MinimalTool): Piper.MinimalTool = mt 569 | override fun buildEnabled(value: Boolean): Piper.MinimalTool = ipp.buildEnabled(value) 570 | } 571 | 572 | class IntruderPayloadGeneratorDialog(private val ipp: Piper.MinimalTool, parent: Component?) : 573 | MinimalToolDialog(ipp, parent, "Intruder payload generator", 574 | showPassHeaders = false, showFilter = false) { 575 | 576 | override fun processGUI(mt: Piper.MinimalTool): Piper.MinimalTool = mt 577 | override fun buildEnabled(value: Boolean): Piper.MinimalTool = ipp.buildEnabled(value) 578 | } 579 | 580 | class MacroDialog(private val macro: Piper.MinimalTool, parent: Component?) : 581 | MinimalToolDialog(macro, parent, "macro") { 582 | 583 | override fun processGUI(mt: Piper.MinimalTool): Piper.MinimalTool = mt 584 | override fun buildEnabled(value: Boolean): Piper.MinimalTool = macro.buildEnabled(value) 585 | } 586 | 587 | fun createLabeledTextField(caption: String, initialValue: String, panel: Container, cs: GridBagConstraints): JTextField { 588 | return createLabeledWidget(caption, JTextField(initialValue), panel, cs) 589 | } 590 | 591 | fun createLabeledComboBox(caption: String, initialValue: String, panel: Container, cs: GridBagConstraints, choices: Array): JComboBox { 592 | val cb = JComboBox(choices) 593 | cb.isEditable = true 594 | cb.selectedItem = initialValue 595 | return createLabeledWidget(caption, cb, panel, cs) 596 | } 597 | 598 | fun createLabeledWidget(caption: String, widget: T, panel: Container, cs: GridBagConstraints): T { 599 | cs.gridy++ 600 | cs.gridwidth = 1 ; cs.gridx = 0 ; panel.add(JLabel(caption), cs) 601 | cs.gridwidth = 3 ; cs.gridx = 1 ; panel.add(widget, cs) 602 | return widget 603 | } 604 | 605 | class HeaderMatchDialog(hm: Piper.HeaderMatch, parent: Component) : ConfigDialog(parent, "Header filter editor") { 606 | private val commonHeaders = arrayOf("Content-Disposition", "Content-Type", "Cookie", 607 | "Host", "Origin", "Referer", "Server", "User-Agent", "X-Requested-With") 608 | private val cbHeader = createLabeledComboBox("Header name: (case insensitive) ", hm.header, panel, cs, commonHeaders) 609 | private val regExpWidget: RegExpWidget = RegExpWidget(hm.regex, panel, cs) 610 | 611 | override fun processGUI(): Piper.HeaderMatch { 612 | val text = cbHeader.selectedItem?.toString() 613 | if (text.isNullOrEmpty()) throw RuntimeException("The header name cannot be empty.") 614 | 615 | return Piper.HeaderMatch.newBuilder().apply { 616 | header = text 617 | regex = regExpWidget.toRegularExpression() 618 | }.build() 619 | } 620 | } 621 | 622 | const val CMDLINE_INPUT_FILENAME_PLACEHOLDER = "" 623 | const val CMDLINE_EMPTY_STRING_PLACEHOLDER = "" 624 | 625 | data class CommandLineParameter(val value: String?) { // null = input file name 626 | val isInputFileName: Boolean 627 | get() = value == null 628 | val isEmptyString: Boolean 629 | get() = value?.isEmpty() == true 630 | override fun toString(): String = when { 631 | isInputFileName -> CMDLINE_INPUT_FILENAME_PLACEHOLDER 632 | value.isNullOrEmpty() -> CMDLINE_EMPTY_STRING_PLACEHOLDER // empty strings would be rendered as a barely visible 1 to 2 px high item 633 | else -> value 634 | } 635 | } 636 | 637 | const val PASS_HTTP_HEADERS_NOTE = "Note: if the above checkbox is unchecked, messages without a body (such as
" + 638 | "GET/HEAD requests or 204 No Content responses) are ignored by this tool." 639 | 640 | class CommandInvocationDialog(ci: Piper.CommandInvocation, private val purpose: CommandInvocationPurpose, parent: Component, 641 | showPassHeaders: Boolean) : ConfigDialog(parent, "Command invocation editor") { 642 | private val ccmwStdout = CollapsedMessageMatchWidget(this, mm = ci.stdout, showHeaderMatch = false, caption = "Match on stdout: ") 643 | private val ccmwStderr = CollapsedMessageMatchWidget(this, mm = ci.stderr, showHeaderMatch = false, caption = "Match on stderr: ") 644 | private val monospaced12 = Font("monospaced", Font.PLAIN, 12) 645 | private var tfExitCode: JTextField? = null 646 | private val cbPassHeaders: JCheckBox? 647 | private val tfDependencies = JTextField() 648 | 649 | private val hasFileName = ci.inputMethod == Piper.CommandInvocation.InputMethod.FILENAME 650 | private val paramsModel = fillDefaultModel(sequence { 651 | yieldAll(ci.prefixList) 652 | if (hasFileName) yield(null) 653 | yieldAll(ci.postfixList) 654 | }.map(::CommandLineParameter)) 655 | 656 | fun parseExitCodeList(): Iterable { 657 | val text = tfExitCode!!.text 658 | return if (text.isEmpty()) emptyList() 659 | else text.filterNot(Char::isWhitespace).split(',').map(String::toInt) 660 | } 661 | 662 | init { 663 | val lsParams = JList(paramsModel) 664 | lsParams.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION 665 | lsParams.font = monospaced12 666 | 667 | lsParams.cellRenderer = object : DefaultListCellRenderer() { 668 | override fun getListCellRendererComponent(list: JList<*>?, value: Any?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component { 669 | val c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) 670 | val v = value as CommandLineParameter 671 | if (v.isInputFileName) { 672 | c.background = Color.RED 673 | c.foreground = if (isSelected) Color.YELLOW else Color.WHITE 674 | } else if (v.isEmptyString) { 675 | c.background = Color.YELLOW 676 | c.foreground = if (isSelected) Color.RED else Color.BLUE 677 | } 678 | return c 679 | } 680 | } 681 | 682 | lsParams.addDoubleClickListener { 683 | if (paramsModel[it].isInputFileName) { 684 | JOptionPane.showMessageDialog(this, CMDLINE_INPUT_FILENAME_PLACEHOLDER + 685 | " is a special placeholder for the names of the input file(s), and thus cannot be edited.") 686 | return@addDoubleClickListener 687 | } 688 | paramsModel[it] = CommandLineParameter( 689 | JOptionPane.showInputDialog(this, "Edit command line parameter no. ${it + 1}:", paramsModel[it].value) 690 | ?: return@addDoubleClickListener) 691 | } 692 | 693 | cs.gridwidth = 4 694 | 695 | panel.add(JLabel("Command line parameters: (one per line)"), cs) 696 | 697 | cs.gridy = 1 698 | cs.gridwidth = 3 699 | cs.gridheight = 3 700 | cs.fill = GridBagConstraints.BOTH 701 | 702 | panel.add(JScrollPane(lsParams), cs) 703 | 704 | val btnMoveUp = JButton("Move up") 705 | btnMoveUp.addActionListener { 706 | val si = lsParams.selectedIndices 707 | if (si.isEmpty() || si[0] == 0) return@addActionListener 708 | si.forEach { 709 | paramsModel.insertElementAt(paramsModel.remove(it - 1), it) 710 | } 711 | } 712 | 713 | val btnMoveDown = JButton("Move down") 714 | btnMoveDown.addActionListener { 715 | val si = lsParams.selectedIndices 716 | if (si.isEmpty() || si.last() == paramsModel.size - 1) return@addActionListener 717 | si.reversed().forEach { 718 | paramsModel.insertElementAt(paramsModel.remove(it + 1), it) 719 | } 720 | lsParams.selectedIndices = si.map { it + 1 }.toIntArray() 721 | } 722 | 723 | cs.gridx = 3 724 | cs.gridwidth = 1 725 | cs.gridheight = 1 726 | cs.fill = GridBagConstraints.HORIZONTAL 727 | 728 | panel.add(createRemoveButton(lsParams, paramsModel), cs) 729 | 730 | cs.gridy = 2; panel.add(btnMoveUp, cs) 731 | cs.gridy = 3; panel.add(btnMoveDown, cs) 732 | 733 | val tfParam = JTextField() 734 | val btnAdd = JButton("Add") 735 | 736 | btnAdd.addActionListener { 737 | paramsModel.addElement(CommandLineParameter(tfParam.text)) 738 | tfParam.text = "" 739 | } 740 | 741 | cs.gridy = 4 742 | 743 | cs.gridx = 0; cs.gridwidth = 1; panel.add(JLabel("Add parameter: "), cs) 744 | cs.gridx = 1; cs.gridwidth = 2; panel.add(tfParam, cs) 745 | cs.gridx = 3; cs.gridwidth = 1; panel.add(btnAdd, cs) 746 | 747 | val cbSpace = createFullWidthCheckBox("Auto-add upon pressing space or closing quotes", true, panel, cs) 748 | 749 | tfParam.font = monospaced12 750 | tfParam.addKeyListener(object : KeyAdapter() { 751 | override fun keyTyped(e: KeyEvent) { 752 | if (cbSpace.isSelected) { 753 | val t = tfParam.text 754 | if (t.startsWith('"')) { 755 | if (e.keyChar == '"') { 756 | tfParam.text = t.substring(1) 757 | btnAdd.doClick() 758 | e.consume() 759 | } 760 | } else if (t.startsWith('\'')) { 761 | if (e.keyChar == '\'') { 762 | tfParam.text = t.substring(1) 763 | btnAdd.doClick() 764 | e.consume() 765 | } 766 | } else if (e.keyChar == ' ' && t.isNotEmpty()) { 767 | btnAdd.doClick() 768 | e.consume() 769 | } 770 | } 771 | } 772 | }) 773 | 774 | cs.gridy = 6 775 | 776 | InputMethodWidget.create(this, panel, cs, hasFileName, paramsModel) 777 | 778 | cbPassHeaders = if (showPassHeaders) { 779 | val cb = createFullWidthCheckBox("Pass HTTP headers to command", ci.passHeaders, panel, cs) 780 | addFullWidthComponent(JLabel(PASS_HTTP_HEADERS_NOTE), panel, cs) 781 | cb 782 | } else null 783 | 784 | addFullWidthComponent(JLabel("Binaries required in PATH: (comma separated)"), panel, cs) 785 | addFullWidthComponent(tfDependencies, panel, cs) 786 | tfDependencies.text = ci.requiredInPathList.joinToString(separator = ", ") 787 | 788 | if (purpose != CommandInvocationPurpose.EXECUTE_ONLY) { 789 | val exitValues = ci.exitCodeList.joinToString(", ") 790 | 791 | if (purpose == CommandInvocationPurpose.SELF_FILTER) { 792 | addFullWidthComponent(JLabel("If any filters are set below, they are treated the same way as a pre-exec filter."), panel, cs) 793 | } 794 | ccmwStdout.buildGUI(panel, cs) 795 | ccmwStderr.buildGUI(panel, cs) 796 | val tfExitCode = createLabeledTextField("Match on exit code: (comma separated) ", exitValues, panel, cs) 797 | 798 | tfExitCode.inputVerifier = object : InputVerifier() { 799 | override fun verify(input: JComponent?): Boolean = 800 | try { 801 | parseExitCodeList(); true 802 | } catch (e: NumberFormatException) { 803 | false 804 | } 805 | } 806 | 807 | this.tfExitCode = tfExitCode 808 | } 809 | } 810 | 811 | override fun processGUI(): Piper.CommandInvocation = Piper.CommandInvocation.newBuilder().apply { 812 | if (purpose != CommandInvocationPurpose.EXECUTE_ONLY) { 813 | if (ccmwStdout.value != null) stdout = ccmwStdout.value 814 | if (ccmwStderr.value != null) stderr = ccmwStderr.value 815 | try { 816 | addAllExitCode(parseExitCodeList()) 817 | } catch (e: NumberFormatException) { 818 | throw RuntimeException("Exit codes should contain numbers separated by commas only. (Whitespace is ignored.)") 819 | } 820 | if (purpose == CommandInvocationPurpose.MATCH_FILTER && !hasFilter) { 821 | throw RuntimeException("No filters are defined for stdio or exit code.") 822 | } 823 | } 824 | val d = tfDependencies.text.replace("\\s".toRegex(), "") 825 | if (d.isNotEmpty()) addAllRequiredInPath(d.split(',')) 826 | if (paramsModel.isEmpty) throw RuntimeException("The command must contain at least one argument.") 827 | if (paramsModel[0].isEmptyString) throw RuntimeException("The first argument (the command) is an empty string") 828 | val params = paramsModel.map(CommandLineParameter::value) 829 | addAllPrefix(params.takeWhile(Objects::nonNull)) 830 | if (prefixCount < paramsModel.size) { 831 | inputMethod = Piper.CommandInvocation.InputMethod.FILENAME 832 | addAllPostfix(params.drop(prefixCount + 1)) 833 | } 834 | if (cbPassHeaders?.isSelected == true) passHeaders = true 835 | }.build() 836 | } 837 | 838 | private class InputMethodWidget(private val w: Window, private val label: JLabel = JLabel(), 839 | private val button: JButton = JButton(), 840 | private var hasFileName: Boolean) { 841 | fun update() { 842 | label.text = "Input method: " + (if (hasFileName) "filename" else "standard input") + " " 843 | button.text = if (hasFileName) "Set to stdin (remove $CMDLINE_INPUT_FILENAME_PLACEHOLDER)" 844 | else "Set to filename (add $CMDLINE_INPUT_FILENAME_PLACEHOLDER)" 845 | w.repack() 846 | } 847 | 848 | companion object { 849 | fun create(w: Window, panel: Container, cs: GridBagConstraints, hasFileName: Boolean, paramsModel: DefaultListModel): InputMethodWidget { 850 | val imw = InputMethodWidget(w = w, hasFileName = hasFileName) 851 | imw.update() 852 | cs.gridwidth = 2 853 | cs.gridx = 0 ; panel.add(imw.label, cs) 854 | cs.gridx = 2 ; panel.add(imw.button, cs) 855 | 856 | paramsModel.addListDataListener(object : ListDataListener { 857 | override fun intervalRemoved(p0: ListDataEvent?) { 858 | if (!imw.hasFileName || paramsModel.toIterable().any(CommandLineParameter::isInputFileName)) return 859 | imw.hasFileName = false 860 | imw.update() 861 | } 862 | 863 | override fun contentsChanged(p0: ListDataEvent?) { /* ignore */ } 864 | override fun intervalAdded(p0: ListDataEvent?) { /* ignore */ } 865 | }) 866 | 867 | imw.button.addActionListener { 868 | if (imw.hasFileName) { 869 | val iof = paramsModel.toIterable().indexOfFirst(CommandLineParameter::isInputFileName) 870 | if (iof >= 0) paramsModel.remove(iof) // this triggers intervalRemoved above, no explicit update() necessary 871 | } else { 872 | paramsModel.addElement(CommandLineParameter(null)) 873 | imw.hasFileName = true 874 | imw.update() 875 | } 876 | } 877 | 878 | return imw 879 | } 880 | } 881 | } 882 | 883 | private fun addFullWidthComponent(c: E, panel: Container, cs: GridBagConstraints): E { 884 | cs.gridx = 0 885 | cs.gridy++ 886 | cs.gridwidth = 4 887 | 888 | panel.add(c, cs) 889 | return c 890 | } 891 | 892 | class HexASCIITextField(private val tf: JTextField = JTextField(), 893 | private val rbHex: JRadioButton = JRadioButton("Hex"), 894 | private val rbASCII: JRadioButton = JRadioButton("ASCII"), 895 | private val field: String, private var isASCII: Boolean) { 896 | 897 | constructor(field: String, source: ByteString, dialog: Component) : this(field=field, isASCII=source.isValidUtf8) { 898 | if (isASCII) { 899 | tf.text = source.toStringUtf8() 900 | rbASCII.isSelected = true 901 | } else { 902 | tf.text = source.toHexPairs() 903 | rbHex.isSelected = true 904 | } 905 | 906 | with(ButtonGroup()) { add(rbHex); add(rbASCII); } 907 | 908 | rbASCII.addActionListener { 909 | if (isASCII) return@addActionListener 910 | val bytes = try { parseHex() } catch(e: NumberFormatException) { 911 | JOptionPane.showMessageDialog(dialog, "Error in $field field: hexadecimal string ${e.message}") 912 | rbHex.isSelected = true 913 | return@addActionListener 914 | } 915 | tf.text = String(bytes, Charsets.UTF_8) 916 | isASCII = true 917 | } 918 | 919 | rbHex.addActionListener { 920 | if (!isASCII) return@addActionListener 921 | tf.text = tf.text.toByteArray(/* default is UTF-8 */).toHexPairs() 922 | isASCII = false 923 | } 924 | } 925 | 926 | private fun parseHex(): ByteArray = tf.text.filter(Char::isLetterOrDigit).run { 927 | if (length % 2 != 0) { 928 | throw NumberFormatException("needs to contain an even number of hex digits") 929 | } 930 | if (any { c -> c in 'g'..'z' || c in 'G'..'Z' }) { 931 | throw NumberFormatException("contains non-hexadecimal letters (maybe typo?)") 932 | } 933 | chunked(2, ::parseHexByte).toByteArray() 934 | } 935 | 936 | fun getByteString(): ByteString = if (isASCII) ByteString.copyFromUtf8(tf.text) else try { 937 | ByteString.copyFrom(parseHex()) 938 | } catch (e: NumberFormatException) { 939 | throw RuntimeException("Error in $field field: hexadecimal string ${e.message}") 940 | } 941 | 942 | fun addWidgets(caption: String, cs: GridBagConstraints, panel: Container) { 943 | cs.gridy++ 944 | cs.gridx = 0 ; panel.add(JLabel(caption), cs) 945 | cs.gridx = 1 ; panel.add(tf, cs) 946 | cs.gridx = 2 ; panel.add(rbASCII, cs) 947 | cs.gridx = 3 ; panel.add(rbHex, cs) 948 | } 949 | } 950 | 951 | private fun parseHexByte(cs: CharSequence): Byte = (parseHexNibble(cs[0]) shl 4 or parseHexNibble(cs[1])).toByte() 952 | 953 | private fun parseHexNibble(c: Char): Int = if (c in '0'..'9') (c - '0') 954 | else ((c.toLowerCase() - 'a') + 0xA) 955 | 956 | class RegExpWidget(regex: Piper.RegularExpression, panel: Container, cs: GridBagConstraints) { 957 | private val tfPattern = createLabeledTextField("Matches regular expression: ", regex.pattern, panel, cs) 958 | private val esw = EnumSetWidget(regex.flagSet, panel, cs, "Regular expression flags: (see JDK documentation)", RegExpFlag::class.java) 959 | 960 | fun hasPattern(): Boolean = tfPattern.text.isNotEmpty() 961 | 962 | fun toRegularExpression(): Piper.RegularExpression { 963 | return Piper.RegularExpression.newBuilder().setPattern(tfPattern.text).setFlagSet(esw.toSet()).build().apply { compile() } 964 | } 965 | } 966 | 967 | class EnumSetWidget>(set: Set, panel: Container, cs: GridBagConstraints, caption: String, enumClass: Class) { 968 | private val cbMap: Map 969 | 970 | fun toSet(): Set = cbMap.filterValues(JCheckBox::isSelected).keys 971 | 972 | init { 973 | addFullWidthComponent(JLabel(caption), panel, cs) 974 | cs.gridy++ 975 | cs.gridwidth = 1 976 | 977 | cbMap = enumClass.enumConstants.asIterable().associateWithTo(EnumMap(enumClass)) { 978 | val cb = createCheckBox(it.toString(), it in set, panel, cs) 979 | if (cs.gridx == 0) { 980 | cs.gridx = 1 981 | } else { 982 | cs.gridy++ 983 | cs.gridx = 0 984 | } 985 | cb 986 | }.toMap() 987 | } 988 | } 989 | 990 | class CollapsedHeaderMatchWidget(w: Window, hm: Piper.HeaderMatch?) : 991 | CollapsedWidget(w, hm, "Header: ", removable = true) { 992 | 993 | override fun editDialog(value: Piper.HeaderMatch, parent: Component): Piper.HeaderMatch? = 994 | HeaderMatchDialog(value, parent = parent).showGUI() 995 | 996 | override fun toHumanReadable(): String = value?.toHumanReadable(negation = false) ?: "(no header match)" 997 | 998 | override val asMap: Map? 999 | get() = value?.toMap() 1000 | 1001 | override fun parseMap(map: Map): Piper.HeaderMatch = HeaderMatchFromMap.invoke(map) 1002 | 1003 | override val default: Piper.HeaderMatch 1004 | get() = Piper.HeaderMatch.getDefaultInstance() 1005 | } 1006 | 1007 | class MessageMatchDialog(mm: Piper.MessageMatch, private val showHeaderMatch: Boolean, parent: Component) : ConfigDialog(parent, "Filter editor") { 1008 | private val prefixField = HexASCIITextField("prefix", mm.prefix, this) 1009 | private val postfixField = HexASCIITextField("postfix", mm.postfix, this) 1010 | private val cciw = CollapsedCommandInvocationWidget(this, mm.cmd, CommandInvocationPurpose.MATCH_FILTER) 1011 | private val chmw = CollapsedHeaderMatchWidget(this, mm.header) 1012 | private val cbNegation = JComboBox(MatchNegation.values()) 1013 | private val regExpWidget: RegExpWidget 1014 | private val cbInScope: JCheckBox? 1015 | private val andAlsoPanel = MatchListEditor("All of these apply: [AND]", mm.andAlsoList) 1016 | private val orElsePanel = MatchListEditor("Any of these apply: [OR]", mm.orElseList) 1017 | 1018 | init { 1019 | cs.gridwidth = 4 1020 | 1021 | panel.add(cbNegation, cs) 1022 | cbNegation.selectedItem = if (mm.negation) MatchNegation.NEGATED else MatchNegation.NORMAL 1023 | 1024 | cs.gridwidth = 1 1025 | 1026 | prefixField .addWidgets("Starts with: ", cs, panel) 1027 | postfixField.addWidgets( "Ends with: ", cs, panel) 1028 | regExpWidget = RegExpWidget(mm.regex, panel, cs) 1029 | 1030 | if (showHeaderMatch) chmw.buildGUI(panel, cs) 1031 | 1032 | cciw.buildGUI(panel, cs) 1033 | 1034 | cbInScope = if (showHeaderMatch) createFullWidthCheckBox("request is in Burp Suite scope", mm.inScope, panel, cs) else null 1035 | 1036 | val spList = JSplitPane() 1037 | spList.leftComponent = andAlsoPanel 1038 | spList.rightComponent = orElsePanel 1039 | 1040 | addFullWidthComponent(spList, panel, cs) 1041 | 1042 | cs.gridy++ 1043 | } 1044 | 1045 | override fun processGUI(): Piper.MessageMatch { 1046 | val builder = Piper.MessageMatch.newBuilder() 1047 | 1048 | if ((cbNegation.selectedItem as MatchNegation).negation) builder.negation = true 1049 | 1050 | builder.postfix = postfixField.getByteString() 1051 | builder.prefix = prefixField.getByteString() 1052 | 1053 | if (regExpWidget.hasPattern()) builder.regex = regExpWidget.toRegularExpression() 1054 | 1055 | if (chmw.value != null) builder.header = chmw.value 1056 | 1057 | if (cbInScope != null && cbInScope.isSelected) builder.inScope = true 1058 | 1059 | val cmd = cciw.value 1060 | if (cmd != null) { 1061 | try { 1062 | cmd.checkDependencies() 1063 | } catch (c: DependencyException) { 1064 | if (JOptionPane.showConfirmDialog(panel, "${c.message}\n\nAre you sure you want to save this?", 1065 | "Confirmation", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) throw CancelClosingWindow() 1066 | } 1067 | builder.cmd = cmd 1068 | } 1069 | 1070 | builder.addAllAndAlso(andAlsoPanel.items) 1071 | builder.addAllOrElse ( orElsePanel.items) 1072 | 1073 | return builder.build() 1074 | } 1075 | 1076 | inner class MatchListEditor(caption: String, source: List) : ClipboardOwner, 1077 | ListEditor(fillDefaultModel(source), this, caption) { 1078 | override fun addDialog(): Piper.MessageMatch? = MessageMatchDialog(Piper.MessageMatch.getDefaultInstance(), 1079 | showHeaderMatch = showHeaderMatch, parent = this).showGUI() 1080 | 1081 | override fun editDialog(value: Piper.MessageMatch): Piper.MessageMatch? = 1082 | MessageMatchDialog(value, showHeaderMatch = showHeaderMatch, parent = this).showGUI() 1083 | 1084 | override fun toHumanReadable(value: Piper.MessageMatch): String = 1085 | value.toHumanReadable(negation = false, hideParentheses = true) 1086 | 1087 | val items: Iterable 1088 | get() = model.toIterable() 1089 | 1090 | private val btnCopy = JButton("Copy") 1091 | private val btnPaste = JButton("Paste") 1092 | 1093 | override fun updateBtnEnableDisableState() { 1094 | super.updateBtnEnableDisableState() 1095 | updateEnableDisableBtnState() 1096 | } 1097 | 1098 | private fun updateEnableDisableBtnState() { 1099 | btnCopy.isEnabled = listWidget.selectedIndices.isNotEmpty() 1100 | } 1101 | 1102 | init { 1103 | btnCopy.addActionListener { 1104 | Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection( 1105 | Dump(DumpSettingsBuilder().build()).dumpToString(listWidget.selectedValue.toMap())), this) 1106 | } 1107 | btnPaste.addActionListener { 1108 | val s = Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String ?: return@addActionListener 1109 | val ls = Load(LoadSettingsBuilder().build()) 1110 | try { 1111 | model.addElement(messageMatchFromMap(ls.loadFromString(s) as Map)) 1112 | } catch (e: Exception) { 1113 | JOptionPane.showMessageDialog(this, e.message) 1114 | } 1115 | } 1116 | pnToolbar.add(btnCopy) 1117 | pnToolbar.add(btnPaste) 1118 | updateEnableDisableBtnState() 1119 | } 1120 | 1121 | override fun lostOwnership(p0: Clipboard?, p1: Transferable?) {} /* ClipboardOwner */ 1122 | } 1123 | } 1124 | 1125 | fun createRemoveButton(listWidget: JList, listModel: DefaultListModel): JButton { 1126 | val btn = JButton("Remove") 1127 | btn.isEnabled = listWidget.selectedIndices.isNotEmpty() 1128 | listWidget.addListSelectionListener { 1129 | btn.isEnabled = listWidget.selectedIndices.isNotEmpty() 1130 | } 1131 | btn.addActionListener { 1132 | listWidget.selectedIndices.reversed().map(listModel::remove) 1133 | } 1134 | return btn 1135 | } 1136 | 1137 | fun fillDefaultModel(source: Iterable, model: DefaultListModel = DefaultListModel()): DefaultListModel = 1138 | fillDefaultModel(source.asSequence(), model) 1139 | fun fillDefaultModel(source: Sequence, model: DefaultListModel = DefaultListModel()): DefaultListModel { 1140 | model.clear() 1141 | source.forEach(model::addElement) 1142 | return model 1143 | } 1144 | 1145 | fun showModalDialog(width: Int, height: Int, widget: Component, caption: String, dialog: JDialog, parent: Component?) { 1146 | with(dialog) { 1147 | defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE 1148 | add(widget) 1149 | setSize(width, height) 1150 | setLocationRelativeTo(parent) 1151 | title = caption 1152 | isModal = true 1153 | isVisible = true 1154 | } 1155 | } --------------------------------------------------------------------------------