├── BappDescription.html ├── BappManifest.bmf ├── LICENSE ├── README.md ├── burp_wp.py ├── data ├── admin_ajax.json └── admin_ajax.json.sha512 ├── images ├── bapp_store_1.png ├── bapp_store_2.png ├── debug_mode.png ├── install_burp_wp.png ├── install_jython.png ├── installed.png ├── intruder_attack.png ├── intruder_choose_payload.png ├── intruder_position.png ├── intruder_send.png ├── logo.svg ├── options.png ├── usage.png ├── usage_pro.png └── wp_ajax.png └── version.sig /BappDescription.html: -------------------------------------------------------------------------------- 1 | Find known vulnerabilities in WordPress plugins and themes using WPScan database. 2 | 3 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: 77a12b2966844f04bba032de5744cd35 2 | ExtensionType: 2 3 | Name: WordPress Scanner 4 | RepoName: wordpress-scanner 5 | ScreenVersion: 1.2a 6 | SerialVersion: 4 7 | MinPlatformVersion: 0 8 | ProOnly: False 9 | Author: Kacper Szurek 10 | ShortDescription: Find known vulnerabilities in WordPress plugins and themes using WPScan database. 11 | EntryPoint: burp_wp.py 12 | BuildCommand: 13 | SupportedProducts: Pro, Community 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kacper Szurek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Burp WP a.k.a. WordPress Scanner 2 | 3 | ![Burp WP](images/logo.svg) 4 | 5 | Find known vulnerabilities in WordPress plugins and themes using Burp Suite proxy. 6 | 7 | **TL;DR: [WPScan](https://wpscan.org/) like plugin for [Burp](https://portswigger.net/) by [Kacper Szurek](https://security.szurek.pl/)**. 8 | 9 | # Usage 10 | [Install](#installation) extension. Browse WordPress sites through Burp proxy. Vulnerable plugins and themes will appear on the issue list. 11 | 12 | ![Usage](images/usage.png) 13 | 14 | If you have Burp Pro, issues will also appear inside *Scanner* tab. Interesting things will be highlighted. 15 | 16 | ![Usage pro](images/usage_pro.png) 17 | 18 | 19 | # Table of contents 20 | 21 | * [Usage](#usage) 22 | * [Installation](#installation) 23 | * [Issue type](#issue-type) 24 | * [Options](#options) 25 | * [Offline database](#offline-database) 26 | * [Intruder payload generator](#intruder-payload-generator) 27 | * [Detect plugins using wp-ajax.php](#detect-plugins-using-wp-ajaxphp) 28 | * [License](#license) 29 | * [Changelog](#changelog) 30 | 31 | # Installation 32 | 33 | WordPress Scanner is available inside [BApp Store](https://portswigger.net/bappstore). 34 | 1. Inside Burp go to **Extender->BApp Store** 35 | 2. Choose WordPress Scanner 36 | 37 | ![WordPress Scanner](images/bapp_store_1.png) 38 | 39 | 3. Click **Install button** 40 | 41 | ![WordPress Scanner Install](images/bapp_store_2.png) 42 | 43 | You can also install Burp WP manually: 44 | 45 | 1. Download [Jython](http://www.jython.org/downloads.html) standalone JAR, for example version [2.7](http://search.maven.org/remotecontent?filepath=org/python/jython-standalone/2.7.0/jython-standalone-2.7.0.jar) 46 | 2. Go to **Extender->Options**. Set path inside `Location of Jython standalone JAR file` 47 | 48 | ![Install Jython](images/install_jython.png) 49 | 50 | 3. Download [newest Burp WP](https://raw.githubusercontent.com/kacperszurek/burp_wp/master/burp_wp.py) 51 | 4. Go to **Extender->Extensions**. Click **Add**. Set `Extension type` to `Python`. Set path inside `Extension file`. 52 | 53 | ![Install Burp WP](images/install_burp_wp.png) 54 | 55 | 5. Burp WP should appear inside `Burp Extensions list`. Also you will see new tab. 56 | 57 | ![Installed extension](images/installed.png) 58 | 59 | # Issue type 60 | 61 | There are 3 types: 62 | 63 | 1. Default type (always enabled) 64 | ``` 65 | {issue_type} inside {(plugin|theme)} {plugin_name} version {detected_version} 66 | ``` 67 | 68 | It has `High severity`. If version is detected using `readme.txt`, `Certain confidence` is set. Otherwise we use `Firm confidence`. 69 | 70 | 2. Plugin vulnerabilities regarding detected version (option 4 enabled) 71 | ``` 72 | Potential {issue_type} inside {(plugin|theme)} {plugin_name} fixed in {version_number} 73 | ``` 74 | 75 | It has `Information severity` and `Certain confidence`. 76 | 77 | 3. Print info about discovered plugins (option 5 enabled) 78 | ``` 79 | Found {(plugin|theme)} {plugin_name} 80 | ``` 81 | 82 | or if plugin version is detected: 83 | 84 | ``` 85 | Found {(plugin|theme)} {plugin_name} version {detected_version} 86 | ``` 87 | 88 | It has `Information severity` and `Certain confidence` if is detected. Otherwise `Firm confidence` is used. 89 | 90 | # Options 91 | 92 | ![Options](images/options.png) 93 | 94 | 1. Update button 95 | 96 | List of vulnerable plugins and themes is downloaded from [WPscan](https://wpscan.org/). Before downloading, `sha512` of files is being checked to see if there is a new version available. 97 | 98 | This button also checks if new [Burp WP version exist](https://raw.githubusercontent.com/kacperszurek/burp_wp/master/version.sig) and allows simple auto update mechanism. 99 | 100 | 2. Use readme.txt for detecting plugins version 101 | 102 | Sometimes it's possible to detect plugin version through its resource because some of them have `?ver=` string. 103 | 104 | For example: 105 | 106 | ``` 107 | http://www.example.com/wp-content/plugins/contact-form-7/includes/css/styles.css?ver=4.9.2 108 | ``` 109 | 110 | Version can be checked using simple regular expression: 111 | 112 | ``` 113 | re.compile("ver=([0-9\.]+)", re.IGNORECASE) 114 | ``` 115 | 116 | But this approach is very *buggy*. Instead more advanced heuristics are used. 117 | 118 | Most plugins contains `readme.txt` file: 119 | 120 | ``` 121 | === Plugin Name === 122 | Donate link: http://example.com/ 123 | Stable tag: 4.3 124 | 125 | Here is a short description of the plugin. 126 | 127 | == Changelog == 128 | 129 | = 1.0 = 130 | * A change since the previous version. 131 | ``` 132 | 133 | So current plugin version can be obtained from `Stable tag` or `Changelog`. 134 | 135 | This idea is from [WPScan versionable.rb](https://github.com/wpscanteam/wpscan/blob/master/lib/common/models/wp_item/versionable.rb). 136 | 137 | 3. Scan full response body 138 | 139 | By default only request URL is used for finding plugins and themes. 140 | 141 | This works just fine but in some cases you may want to parse full response body. Use with caution as this might be slow. 142 | 143 | 4. Print all plugin vulnerabilities regarding detected version 144 | 145 | By default issue is only added when vulnerable plugin version is detected `plugin_version < fixed_version`. 146 | 147 | If you want to print all known vulnerabilities for detected plugin regarding its version - use this option. 148 | 149 | 5. Print info about discovered plugins even if they don't have known vulnerabilities 150 | 151 | Normally plugins/themes which are not vulnerable are ignored. 152 | 153 | If you want to have information about installed plugins on given website, even if they are not vulnerable - use this option. 154 | 155 | 6. Enable auto update 156 | 157 | Auto update database once per 24 h. 158 | 159 | It also checks if new Burp WP version exists. 160 | 161 | 7. Enable debug mode 162 | 163 | For development purpose. 164 | 165 | You can see output inside: **Extender->Extensions->Burp WP->Output tab** 166 | 167 | ![Debug mode](images/debug_mode.png) 168 | 169 | 8. What detect 170 | 171 | Decide if you want to search for vulnerable plugins, themes or both. 172 | 173 | 9. Custom wp-content 174 | 175 | Detection mechanism is based on `wp-content` string. 176 | 177 | But it can be [changed](https://codex.wordpress.org/Editing_wp-config.php#Moving_wp-content_folder) by website owner. Here you can customize this option. 178 | 179 | 10. Clear issues list button 180 | 181 | This button will remove all issues from issues list inside extension tab. 182 | 183 | 11. Force update button 184 | 185 | Similar to `Update button` but it downloads new database even if newest one is already installed. 186 | 187 | 12. Reset settings to default 188 | 189 | Restore extension state to factory defaults. 190 | 191 | 13. Discover plugins using wp-ajax.php 192 | See [Detect plugins using wp-ajax.php](#detect-plugins-using-wp-ajaxphp). 193 | 194 | # Offline database 195 | All vulnerabilities are provided by [WPscan](https://wpscan.org/) - see [Vulnerability Database](https://wpvulndb.com). 196 | 197 | Burp WP supports offline mode. 198 | 199 | If you operate from high-security network without Internet access you can easily copy database file from normal Burp WP instance to your offline one. 200 | 201 | Then use `Choose file` option. 202 | 203 | If it's valid Burp WP database it will be imported automatically. 204 | 205 | # Intruder payload generator 206 | Because proxy requests and responses are used it's not possible to discover all plugins and themes installed on a specific website. 207 | 208 | You can try to get more information manually using intruder payload generator. 209 | 210 | Right click on URL inside **Proxy->HTTP history** and choose **Send to Burp WP Intruder**. ![Send to intruder](images/intruder_send.png) 211 | 212 | This will replace request method to GET, remove all parameters and set payload position marker. 213 | 214 | Now go to **Intruder->Tab X->Positions**. Correct URL so it points to WordPress homepage. 215 | 216 | ![Intruder positions](images/intruder_position.png) 217 | 218 | Inside **Payloads** tab uncheck **Payload encoding** so `/` won't be converted to `%2f`. 219 | 220 | Then set **Payload type** to **Extension generated**. Now click **Select generator**: 221 | 222 | ![Intruder choose payload](images/intruder_choose_payload.png) 223 | 224 | There are 3 generators: 225 | 1. WordPress Plugins 226 | 2. WordPress Themes 227 | 3. WordPress Plugins and themes 228 | 229 | 230 | ![Intruder attack](images/intruder_attack.png) 231 | 232 | # Detect plugins using wp-ajax.php 233 | This is new technique available since Burp WP 0.2. 234 | 235 | It discovers plugins based on calls to `wp-admin/admin-ajax.php` endpoint. 236 | 237 | Custom [action database](https://github.com/kacperszurek/burp_wp/blob/master/data/admin_ajax.json) is used for this. 238 | 239 | Basically when plugin send request to `/admin-ajax.php?action=akismet_recheck_queue` Burp WP makes reverse lookup in action database. 240 | 241 | ![Wp-ajax detection technique](images/wp_ajax.png) 242 | 243 | # License 244 | MIT License 245 | 246 | Copyright (c) 2018 Kacper Szurek 247 | 248 | Permission is hereby granted, free of charge, to any person obtaining a copy 249 | of this software and associated documentation files (the "Software"), to deal 250 | in the Software without restriction, including without limitation the rights 251 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 252 | copies of the Software, and to permit persons to whom the Software is 253 | furnished to do so, subject to the following conditions: 254 | 255 | The above copyright notice and this permission notice shall be included in all 256 | copies or substantial portions of the Software. 257 | 258 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 259 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 260 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 261 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 262 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 263 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 264 | SOFTWARE. 265 | 266 | The WPScan data is licensed separately. Please find the WPScan license [here](https://raw.githubusercontent.com/wpscanteam/wpscan/master/LICENSE). 267 | 268 | # Changelog 269 | 270 | * 0.2 - Add discovery plugins using `wp-ajax.php?action` 271 | * 0.1.1 - Updates are downloaded through Burp proxy, fix clear list issues button, implement doPassiveScan function 272 | * 0.1 - Beta version 273 | -------------------------------------------------------------------------------- /burp_wp.py: -------------------------------------------------------------------------------- 1 | # ____ _ _ ____ ____ __ ______ 2 | # | __ )| | | | _ \| _ \ \ \ / / _ \ 3 | # | _ \| | | | |_) | |_) | \ \ /\ / /| |_) | 4 | # | |_) | |_| | _ <| __/ \ V V / | __/ 5 | # |____/ \___/|_| \_\_| \_/\_/ |_| 6 | # 7 | # MIT License 8 | # 9 | # Copyright (c) 2018 Kacper Szurek 10 | import collections 11 | import hashlib 12 | import json 13 | import os 14 | import re 15 | import shutil 16 | import threading 17 | import time 18 | import traceback 19 | import urlparse 20 | from array import array 21 | from base64 import b64encode, b64decode 22 | from collections import defaultdict 23 | from distutils.version import LooseVersion 24 | from itertools import chain 25 | from threading import Lock 26 | 27 | from burp import IBurpExtender 28 | from burp import IBurpExtenderCallbacks 29 | from burp import IContextMenuFactory 30 | from burp import IHttpListener 31 | from burp import IIntruderPayloadGenerator 32 | from burp import IIntruderPayloadGeneratorFactory 33 | from burp import IMessageEditorController 34 | from burp import IParameter 35 | from burp import IScanIssue 36 | from burp import ITab 37 | from burp import IScannerCheck 38 | 39 | from java.awt import Component 40 | from java.awt import Cursor 41 | from java.awt import Desktop 42 | from java.awt import Dimension 43 | from java.awt.event import ActionListener 44 | from java.awt.event import ItemEvent 45 | from java.awt.event import ItemListener 46 | from java.awt.event import MouseAdapter 47 | from java.net import URL, URI 48 | from java.security import KeyFactory 49 | from java.security import Signature 50 | from java.security.spec import X509EncodedKeySpec 51 | from java.util import ArrayList 52 | from javax.swing import BoxLayout 53 | from javax.swing import JButton 54 | from javax.swing import JCheckBox 55 | from javax.swing import JComboBox 56 | from javax.swing import JEditorPane 57 | from javax.swing import JFileChooser 58 | from javax.swing import JLabel 59 | from javax.swing import JMenuItem 60 | from javax.swing import JOptionPane 61 | from javax.swing import JPanel 62 | from javax.swing import JProgressBar 63 | from javax.swing import JScrollPane 64 | from javax.swing import JSplitPane 65 | from javax.swing import JTabbedPane 66 | from javax.swing import JTable 67 | from javax.swing import JTextField 68 | from javax.swing.event import DocumentListener 69 | from javax.swing.table import AbstractTableModel 70 | from org.python.core.util import StringUtil 71 | 72 | BURP_WP_VERSION = '0.2' 73 | INTERESTING_CODES = [200, 401, 403, 301] 74 | DB_NAME = "burp_wp_database.db" 75 | 76 | 77 | class BurpExtender(IBurpExtender, IHttpListener, ITab, IContextMenuFactory, IMessageEditorController, IScannerCheck): 78 | config = {} 79 | 80 | def print_debug(self, message): 81 | if self.config.get('debug', False): 82 | self.callbacks.printOutput(message) 83 | 84 | def registerExtenderCallbacks(self, callbacks): 85 | self.callbacks = callbacks 86 | 87 | self.callbacks.printOutput("WordPress Scanner version {}".format(BURP_WP_VERSION)) 88 | 89 | self.helpers = callbacks.getHelpers() 90 | 91 | self.initialize_config() 92 | 93 | self.callbacks.setExtensionName("WordPress Scanner") 94 | 95 | # createMenuItems 96 | self.callbacks.registerContextMenuFactory(self) 97 | 98 | # processHttpMessage 99 | self.callbacks.registerHttpListener(self) 100 | 101 | self.callbacks.registerIntruderPayloadGeneratorFactory(IntruderPluginsGenerator(self)) 102 | self.callbacks.registerIntruderPayloadGeneratorFactory(IntruderThemesGenerator(self)) 103 | self.callbacks.registerIntruderPayloadGeneratorFactory(IntruderPluginsThemesGenerator(self)) 104 | 105 | # doPassiveScan 106 | self.callbacks.registerScannerCheck(self) 107 | 108 | self.initialize_variables() 109 | self.initialize_gui() 110 | 111 | # getTabCaption, getUiComponent 112 | # This must be AFTER panel_main initialization 113 | self.callbacks.addSuiteTab(self) 114 | 115 | self.initialize_database() 116 | 117 | def initialize_config(self): 118 | temp_config = self.callbacks.loadExtensionSetting("config") 119 | if temp_config and len(temp_config) > 10: 120 | try: 121 | self.config = json.loads(b64decode(temp_config)) 122 | self.print_debug("[+] initialize_config configuration: {}".format(self.config)) 123 | except: 124 | self.print_debug("[-] initialize_config cannot load configuration: {}".format(traceback.format_exc())) 125 | else: 126 | self.print_debug("[+] initialize_config new configuration") 127 | self.config = {'active_scan': True, 'database_path': os.path.join(os.getcwd(), DB_NAME), 128 | 'wp_content': 'wp-content', 'full_body': False, 'all_vulns': False, 'scan_type': 1, 129 | 'debug': False, 'auto_update': True, 'last_update': 0, 'sha_plugins': '', 'sha_themes': '', 130 | 'sha_admin_ajax': '', 'print_info': False, 'admin_ajax': True, 'update_burp_wp': '0'} 131 | 132 | def initialize_variables(self): 133 | self.is_burp_pro = True if "Professional" in self.callbacks.getBurpVersion()[0] else False 134 | self.regexp_version_number = re.compile("ver=([0-9.]+)", re.IGNORECASE) 135 | self.regexp_stable_tag = re.compile(r"(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)", re.IGNORECASE) 136 | self.regexp_version_from_changelog = re.compile( 137 | r"[=]+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[ \ta-z0-9().\-,]*[=]+", 138 | re.IGNORECASE) 139 | 140 | self.list_issues = ArrayList() 141 | self.lock_issues = Lock() 142 | self.lock_update_database = Lock() 143 | 144 | self.database = {'plugins': collections.OrderedDict(), 'themes': collections.OrderedDict(), 'admin_ajax': {}} 145 | self.list_plugins_on_website = defaultdict(list) 146 | 147 | def initialize_gui(self): 148 | class CheckboxListener(ItemListener): 149 | def __init__(self, extender, name): 150 | self.extender = extender 151 | self.name = name 152 | 153 | def itemStateChanged(self, e): 154 | if e.getStateChange() == ItemEvent.SELECTED: 155 | self.extender.update_config(self.name, True) 156 | else: 157 | self.extender.update_config(self.name, False) 158 | 159 | class ComboboxListener(ActionListener): 160 | def __init__(self, extender, name): 161 | self.extender = extender 162 | self.name = name 163 | 164 | def actionPerformed(self, action_event): 165 | selected = self.extender.combobox_scan_type.getSelectedItem().get_key() 166 | self.extender.update_config(self.name, selected) 167 | 168 | class TextfieldListener(DocumentListener): 169 | def __init__(self, extender): 170 | self.extender = extender 171 | 172 | def changedUpdate(self, document): 173 | self._do(document) 174 | 175 | def removeUpdate(self, document): 176 | self._do(document) 177 | 178 | def insertUpdate(self, document): 179 | self._do(document) 180 | 181 | def _do(self, document): 182 | wp_content = self.extender.textfield_wp_content.getText().replace("/", "") 183 | self.extender.update_config('wp_content', wp_content) 184 | 185 | class CopyrightMouseAdapter(MouseAdapter): 186 | def __init__(self, url): 187 | self.url = URI.create(url) 188 | 189 | def mouseClicked(self, event): 190 | if Desktop.isDesktopSupported() and Desktop.getDesktop().isSupported(Desktop.Action.BROWSE): 191 | try: 192 | Desktop.getDesktop().browse(self.url) 193 | except: 194 | self._print_debug("[-] CopyrightMouseAdapter: {}".format(traceback.format_exc())) 195 | 196 | class ComboboxItem: 197 | def __init__(self, key, val): 198 | self._key = key 199 | self._val = val 200 | 201 | def get_key(self): 202 | return self._key 203 | 204 | # Set label inside ComboBox 205 | def __repr__(self): 206 | return self._val 207 | 208 | panel_upper = JPanel() 209 | panel_upper.setLayout(BoxLayout(panel_upper, BoxLayout.Y_AXIS)) 210 | 211 | panel_update = JPanel() 212 | panel_update.setLayout(BoxLayout(panel_update, BoxLayout.X_AXIS)) 213 | panel_update.setAlignmentX(Component.LEFT_ALIGNMENT) 214 | 215 | self.button_update = JButton("Update", actionPerformed=self.button_update_on_click) 216 | self.button_update.setAlignmentX(Component.LEFT_ALIGNMENT) 217 | panel_update.add(self.button_update) 218 | 219 | self.progressbar_update = JProgressBar() 220 | self.progressbar_update.setMaximumSize(self.progressbar_update.getPreferredSize()) 221 | self.progressbar_update.setAlignmentX(Component.LEFT_ALIGNMENT) 222 | panel_update.add(self.progressbar_update) 223 | 224 | self.label_update = JLabel() 225 | self.label_update.setAlignmentX(Component.LEFT_ALIGNMENT) 226 | panel_update.add(self.label_update) 227 | 228 | panel_upper.add(panel_update) 229 | 230 | checkbox_active_scan = JCheckBox("Use readme.txt for detecting plugins version. This option sends additional request to website", 231 | self.config.get('active_scan', False)) 232 | checkbox_active_scan.addItemListener(CheckboxListener(self, "active_scan")) 233 | panel_upper.add(checkbox_active_scan) 234 | 235 | checkbox_full_body = JCheckBox("Scan full response body (normally we check only URL)", 236 | self.config.get('full_body', False)) 237 | checkbox_full_body.addItemListener(CheckboxListener(self, "full_body")) 238 | panel_upper.add(checkbox_full_body) 239 | 240 | checkbox_all_vulns = JCheckBox("Print all plugin vulnerabilities regarding detected version", 241 | self.config.get('all_vulns', False)) 242 | checkbox_all_vulns.addItemListener(CheckboxListener(self, "all_vulns")) 243 | panel_upper.add(checkbox_all_vulns) 244 | 245 | checkbox_print_info = JCheckBox( 246 | "Print info about discovered plugins even if they don't have known vulnerabilities", 247 | self.config.get('print_info', False)) 248 | checkbox_print_info.addItemListener(CheckboxListener(self, "print_info")) 249 | panel_upper.add(checkbox_print_info) 250 | 251 | checkbox_admin_ajax = JCheckBox( 252 | "Discover plugins using wp-ajax.php?action= technique", 253 | self.config.get('admin_ajax', True)) 254 | checkbox_admin_ajax.addItemListener(CheckboxListener(self, "admin_ajax")) 255 | panel_upper.add(checkbox_admin_ajax) 256 | 257 | checkbox_auto_update = JCheckBox("Enable auto update", self.config.get('auto_update', True)) 258 | checkbox_auto_update.addItemListener(CheckboxListener(self, "auto_update")) 259 | panel_upper.add(checkbox_auto_update) 260 | 261 | checkbox_debug = JCheckBox("Enable debug mode", self.config.get('debug', False)) 262 | checkbox_debug.addItemListener(CheckboxListener(self, "debug")) 263 | panel_upper.add(checkbox_debug) 264 | 265 | panel_what_detect = JPanel() 266 | panel_what_detect.setLayout(BoxLayout(panel_what_detect, BoxLayout.X_AXIS)) 267 | panel_what_detect.setAlignmentX(Component.LEFT_ALIGNMENT) 268 | 269 | label_what_detect = JLabel("What detect: ") 270 | label_what_detect.setAlignmentX(Component.LEFT_ALIGNMENT) 271 | panel_what_detect.add(label_what_detect) 272 | 273 | self.combobox_scan_type = JComboBox() 274 | self.combobox_scan_type.addItem(ComboboxItem(1, "Plugins and Themes")) 275 | self.combobox_scan_type.addItem(ComboboxItem(2, "Only plugins")) 276 | self.combobox_scan_type.addItem(ComboboxItem(3, "Only themes")) 277 | self.combobox_scan_type.addActionListener(ComboboxListener(self, "scan_type")) 278 | self.combobox_scan_type.setMaximumSize(Dimension(200, 30)) 279 | self.combobox_scan_type.setAlignmentX(Component.LEFT_ALIGNMENT) 280 | panel_what_detect.add(self.combobox_scan_type) 281 | 282 | label_wp_content = JLabel("Custom wp-content:") 283 | label_wp_content.setAlignmentX(Component.LEFT_ALIGNMENT) 284 | panel_what_detect.add(label_wp_content) 285 | 286 | self.textfield_wp_content = JTextField(self.config.get('wp_content', 'wp-content')) 287 | self.textfield_wp_content.getDocument().addDocumentListener(TextfieldListener(self)) 288 | self.textfield_wp_content.setMaximumSize(Dimension(250, 30)) 289 | self.textfield_wp_content.setAlignmentX(Component.LEFT_ALIGNMENT) 290 | panel_what_detect.add(self.textfield_wp_content) 291 | 292 | panel_upper.add(panel_what_detect) 293 | 294 | panel_choose_file = JPanel() 295 | panel_choose_file.setLayout(BoxLayout(panel_choose_file, BoxLayout.X_AXIS)) 296 | panel_choose_file.setAlignmentX(Component.LEFT_ALIGNMENT) 297 | 298 | label_database_path = JLabel("Database path: ") 299 | label_database_path.setAlignmentX(Component.LEFT_ALIGNMENT) 300 | panel_choose_file.add(label_database_path) 301 | 302 | button_choose_file = JButton("Choose file", actionPerformed=self.button_choose_file_on_click) 303 | button_choose_file.setAlignmentX(Component.LEFT_ALIGNMENT) 304 | panel_choose_file.add(button_choose_file) 305 | 306 | self.textfield_database_path = JTextField(self.config.get('database_path', DB_NAME)) 307 | self.textfield_database_path.setEditable(False) 308 | self.textfield_database_path.setMaximumSize(Dimension(250, 30)) 309 | self.textfield_database_path.setAlignmentX(Component.LEFT_ALIGNMENT) 310 | panel_choose_file.add(self.textfield_database_path) 311 | 312 | panel_upper.add(panel_choose_file) 313 | 314 | panel_buttons = JPanel() 315 | panel_buttons.setLayout(BoxLayout(panel_buttons, BoxLayout.X_AXIS)) 316 | panel_buttons.setAlignmentX(Component.LEFT_ALIGNMENT) 317 | 318 | button_clear_issues = JButton("Clear issues list", actionPerformed=self.button_clear_issues_on_click) 319 | panel_buttons.add(button_clear_issues) 320 | 321 | button_force_update = JButton("Force update", actionPerformed=self.button_force_update_on_click) 322 | panel_buttons.add(button_force_update) 323 | 324 | button_reset_to_default = JButton("Reset settings to default", 325 | actionPerformed=self.button_reset_to_default_on_click) 326 | panel_buttons.add(button_reset_to_default) 327 | 328 | panel_upper.add(panel_buttons) 329 | 330 | panel_copyright = JPanel() 331 | panel_copyright.setLayout(BoxLayout(panel_copyright, BoxLayout.X_AXIS)) 332 | panel_copyright.setAlignmentX(Component.LEFT_ALIGNMENT) 333 | 334 | label_copyright1 = JLabel("WordPress Scanner {}".format(BURP_WP_VERSION)) 335 | label_copyright1.putClientProperty("html.disable", None) 336 | label_copyright1.setAlignmentX(Component.LEFT_ALIGNMENT) 337 | label_copyright1.setCursor(Cursor(Cursor.HAND_CURSOR)) 338 | label_copyright1.addMouseListener(CopyrightMouseAdapter("https://github.com/kacperszurek/burp_wp")) 339 | label_copyright1.setMaximumSize(label_copyright1.getPreferredSize()) 340 | panel_copyright.add(label_copyright1) 341 | 342 | label_copyright2 = JLabel(" by Kacper Szurek.") 343 | label_copyright2.putClientProperty("html.disable", None) 344 | label_copyright2.setAlignmentX(Component.LEFT_ALIGNMENT) 345 | label_copyright2.setCursor(Cursor(Cursor.HAND_CURSOR)) 346 | label_copyright2.addMouseListener(CopyrightMouseAdapter("https://security.szurek.pl/")) 347 | label_copyright2.setMaximumSize(label_copyright2.getPreferredSize()) 348 | panel_copyright.add(label_copyright2) 349 | 350 | label_copyright3 = JLabel( 351 | " Vulnerabilities database by WPScan") 352 | label_copyright3.putClientProperty("html.disable", None) 353 | label_copyright3.setAlignmentX(Component.LEFT_ALIGNMENT) 354 | label_copyright3.setCursor(Cursor(Cursor.HAND_CURSOR)) 355 | label_copyright3.addMouseListener(CopyrightMouseAdapter("https://wpscan.org/")) 356 | panel_copyright.add(label_copyright3) 357 | 358 | panel_upper.add(panel_copyright) 359 | 360 | self.table_issues = IssuesTableModel(self) 361 | 362 | table_issues_details = IssuesDetailsTable(self, self.table_issues) 363 | table_issues_details.setAutoCreateRowSorter(True) 364 | panel_center = JScrollPane(table_issues_details) 365 | 366 | self.panel_bottom = JTabbedPane() 367 | self.panel_bottom_request1 = self.callbacks.createMessageEditor(self, True) 368 | self.panel_bottom_response1 = self.callbacks.createMessageEditor(self, True) 369 | self.panel_bottom_request2 = self.callbacks.createMessageEditor(self, True) 370 | self.panel_bottom_response2 = self.callbacks.createMessageEditor(self, True) 371 | 372 | self.panel_bottom_advisory = JEditorPane() 373 | self.panel_bottom_advisory.setEditable(False) 374 | self.panel_bottom_advisory.setEnabled(True) 375 | self.panel_bottom_advisory.setContentType("text/html") 376 | 377 | self.panel_bottom.addTab("Advisory", JScrollPane(self.panel_bottom_advisory)) 378 | self.panel_bottom.addTab("Request 1", JScrollPane(self.panel_bottom_request1.getComponent())) 379 | self.panel_bottom.addTab("Response 1", JScrollPane(self.panel_bottom_response1.getComponent())) 380 | self.panel_bottom.addTab("Request 2", JScrollPane(self.panel_bottom_request2.getComponent())) 381 | self.panel_bottom.addTab("Response 2", JScrollPane(self.panel_bottom_response2.getComponent())) 382 | 383 | split_panel_upper = JSplitPane(JSplitPane.VERTICAL_SPLIT, panel_upper, panel_center) 384 | self.panel_main = JSplitPane(JSplitPane.VERTICAL_SPLIT, split_panel_upper, self.panel_bottom) 385 | 386 | def initialize_database(self): 387 | last_update = time.strftime("%d-%m-%Y %H:%M", time.localtime(self.config.get('last_update', 0))) 388 | update_started = False 389 | 390 | if self.config.get('auto_update', True): 391 | if (self.config.get('last_update', 0) + (60 * 60 * 24)) < int(time.time()): 392 | self.print_debug("[*] initialize_database Last check > 24h") 393 | self.button_update_on_click(None) 394 | update_started = True 395 | else: 396 | self.print_debug("[*] initialize_database last update: {}".format(last_update)) 397 | 398 | database_path = self.config.get('database_path', DB_NAME) 399 | self.print_debug("[*] initialize_database database path: {}".format(database_path)) 400 | if os.path.exists(database_path): 401 | try: 402 | with open(database_path, "rb") as fp: 403 | self.database = json.load(fp) 404 | themes_length = len(self.database['themes']) 405 | plugins_length = len(self.database['plugins']) 406 | admin_ajax_length = len(self.database.get('admin_ajax', {})) 407 | update_text = "Themes: {}, Plugins: {}, Admin ajax: {}, Last update: {}".format(themes_length, plugins_length, admin_ajax_length, 408 | last_update) 409 | self.label_update.setText(update_text) 410 | except Exception as e: 411 | self.label_update.setText("Cannot load database: {}".format(e)) 412 | self.print_debug("[-] initialize_database cannot load database: {}".format(traceback.format_exc())) 413 | if not update_started: 414 | self.button_force_update_on_click(None) 415 | else: 416 | self.print_debug("[-] initialize_database database does not exist") 417 | if not update_started: 418 | self.button_force_update_on_click(None) 419 | 420 | def button_force_update_on_click(self, msg): 421 | self.print_debug("[+] button_force_update_on_click") 422 | 423 | self.update_config('sha_plugins', '') 424 | self.update_config('sha_themes', '') 425 | 426 | self.button_update_on_click(None) 427 | 428 | def button_reset_to_default_on_click(self, msg): 429 | self.print_debug("[+] button_reset_to_default_on_click") 430 | self.callbacks.saveExtensionSetting("config", "") 431 | JOptionPane.showMessageDialog(self.panel_main, "Please reload extension") 432 | self.callbacks.unloadExtension() 433 | 434 | def clear_issues(self): 435 | if not self.lock_issues.acquire(False): 436 | self.print_debug("[*] clear_issues cannot acquire lock") 437 | return 438 | try: 439 | self.print_debug("[+] clear_issues lock acquired") 440 | row = self.list_issues.size() 441 | if row > 0: 442 | self.list_issues.clear() 443 | self.table_issues.fireTableRowsDeleted(0, (row-1)) 444 | self.panel_bottom_advisory.setText("") 445 | self.panel_bottom_request1.setMessage("", True) 446 | self.panel_bottom_response1.setMessage("", False) 447 | self.panel_bottom_request2.setMessage("", True) 448 | self.panel_bottom_response2.setMessage("", False) 449 | self.list_plugins_on_website.clear() 450 | except: 451 | self.print_debug("[+] clear_issues error: {}".format(traceback.format_exc())) 452 | finally: 453 | self.lock_issues.release() 454 | self.print_debug("[+] clear_issues lock release") 455 | 456 | def button_clear_issues_on_click(self, msg): 457 | self.print_debug("[+] button_clear_issues_on_click") 458 | threading.Thread(target=self.clear_issues).start() 459 | 460 | def button_update_on_click(self, msg): 461 | threading.Thread(target=self.update_database_wrapper).start() 462 | 463 | def button_choose_file_on_click(self, msg): 464 | file_chooser = JFileChooser() 465 | return_value = file_chooser.showSaveDialog(self.panel_main) 466 | if return_value == JFileChooser.APPROVE_OPTION: 467 | selected_file = file_chooser.getSelectedFile() 468 | old_file_path = self.config.get('database_path', DB_NAME) 469 | file_path = selected_file.getPath() 470 | if file_path == old_file_path: 471 | self.print_debug("[+] button_choose_file_on_click the same database file") 472 | return 473 | 474 | if selected_file.exists(): 475 | try: 476 | with open(file_path, "rb") as fp: 477 | temp_load = json.load(fp) 478 | if "themes" in temp_load and "plugins" in temp_load: 479 | self.database = temp_load 480 | self.textfield_database_path.setText(file_path) 481 | self.update_config('database_path', file_path) 482 | self.update_config('last_update', int(time.time())) 483 | self.print_debug("[+] button_choose_file_on_click offline database installed") 484 | return 485 | except: 486 | self.print_debug("[+] button_choose_file_on_click cannot load offline database: {}".format( 487 | traceback.format_exc())) 488 | 489 | result = JOptionPane.showConfirmDialog(self.panel_main, "The file exists, overwrite?", "Existing File", 490 | JOptionPane.YES_NO_OPTION) 491 | if result != JOptionPane.YES_OPTION: 492 | return 493 | 494 | self.textfield_database_path.setText(file_path) 495 | self.print_debug("[+] button_choose_file_on_click new database path, force update") 496 | self.update_config('database_path', file_path) 497 | self.button_force_update_on_click(None) 498 | 499 | def update_config(self, key, val): 500 | try: 501 | self.config[key] = val 502 | temp_config = b64encode(json.dumps(self.config, ensure_ascii=False)) 503 | self.callbacks.saveExtensionSetting("config", temp_config) 504 | self.print_debug("[+] Config updated for key {}".format(key)) 505 | if key == "last_update": 506 | last_update = time.strftime("%d-%m-%Y %H:%M", time.localtime(self.config.get('last_update', 0))) 507 | themes_length = len(self.database['themes']) 508 | plugins_length = len(self.database['plugins']) 509 | admin_ajax_length = len(self.database.get('admin_ajax', {})) 510 | update_text = "Themes: {}, Plugins: {}, Admin ajax: {}, Last update: {}".format(themes_length, plugins_length, admin_ajax_length, 511 | last_update) 512 | self.label_update.setText(update_text) 513 | self.print_debug("[*] {}".format(update_text)) 514 | except: 515 | self.print_debug("[-] update_config: {}".format(traceback.format_exc())) 516 | 517 | def update_database_wrapper(self): 518 | if not self.lock_update_database.acquire(False): 519 | self.print_debug("[*] update_database update already running") 520 | return 521 | try: 522 | self.button_update.setEnabled(False) 523 | 524 | self.print_debug("[+] update_database update started") 525 | if self._update_database(): 526 | try: 527 | with open(self.config.get('database_path'), "wb") as fp: 528 | json.dump(self.database, fp) 529 | self.update_config('last_update', int(time.time())) 530 | except: 531 | self.print_debug("[-] update_database cannot save database: {}".format(traceback.format_exc())) 532 | return 533 | 534 | self.print_debug("[+] update_database update finish") 535 | except: 536 | self.print_debug("[+] update_database update error") 537 | finally: 538 | self.lock_update_database.release() 539 | self.progressbar_update.setValue(100) 540 | self.progressbar_update.setStringPainted(True) 541 | self.button_update.setEnabled(True) 542 | 543 | def _make_http_request_wrapper(self, original_url): 544 | try: 545 | java_url = URL(original_url) 546 | request = self.helpers.buildHttpRequest(java_url) 547 | response = self.callbacks.makeHttpRequest(java_url.getHost(), 443, True, request) 548 | response_info = self.helpers.analyzeResponse(response) 549 | if response_info.getStatusCode() in INTERESTING_CODES: 550 | return self.helpers.bytesToString(response)[response_info.getBodyOffset():].encode("latin1") 551 | else: 552 | self.print_debug("[-] _make_http_request_wrapper request failed") 553 | return None 554 | except: 555 | self.print_debug("[-] _make_http_request_wrapper failed: {}".format(traceback.format_exc())) 556 | return None 557 | 558 | def _update_database(self): 559 | dict_files = {'plugins': 'https://data.wpscan.org/plugins.json', 560 | 'themes': 'https://data.wpscan.org/themes.json', 561 | 'admin_ajax': 'https://raw.githubusercontent.com/kacperszurek/burp_wp/master/data/admin_ajax.json'} 562 | 563 | progress_divider = len(dict_files) * 2 564 | progress_adder = 0 565 | for _type, url in dict_files.iteritems(): 566 | try: 567 | temp_database = collections.OrderedDict() 568 | 569 | sha_url = "{}.sha512".format(url) 570 | sha_original = self._make_http_request_wrapper(sha_url) 571 | if not sha_original: 572 | return False 573 | 574 | if self.config.get('sha_{}'.format(_type), '') == sha_original: 575 | self.print_debug('[*] _update_database the same hash for {}, skipping update'.format(_type)) 576 | progress_adder += int(100 / len(dict_files)) 577 | continue 578 | 579 | self.progressbar_update.setValue(int(100/progress_divider)+progress_adder) 580 | self.progressbar_update.setStringPainted(True) 581 | 582 | downloaded_data = self._make_http_request_wrapper(url) 583 | if not downloaded_data: 584 | return False 585 | 586 | hash_sha512 = hashlib.sha512() 587 | hash_sha512.update(downloaded_data) 588 | downloaded_sha = hash_sha512.hexdigest() 589 | 590 | if sha_original != downloaded_sha: 591 | self.print_debug( 592 | "[-] _update_database hash mismatch for {}, should be: {} is: {}".format(_type, sha_original, 593 | downloaded_sha)) 594 | return False 595 | 596 | try: 597 | loaded_json = json.loads(downloaded_data) 598 | except: 599 | self.print_debug( 600 | "[-] _update_database cannot decode json for {}: {}".format(_type, traceback.format_exc())) 601 | return False 602 | 603 | if _type == 'admin_ajax': 604 | temp_database = loaded_json 605 | else: 606 | i = 0 607 | progress_adder += int(100 / progress_divider) 608 | json_length = len(loaded_json) 609 | for name in loaded_json: 610 | bugs = [] 611 | i += 1 612 | if i % 1000 == 0: 613 | percent = int((i * 100. / json_length) / 4) + progress_adder 614 | self.progressbar_update.setValue(percent) 615 | self.progressbar_update.setStringPainted(True) 616 | # No bugs 617 | if len(loaded_json[name]['vulnerabilities']) == 0: 618 | continue 619 | 620 | for vulnerability in loaded_json[name]['vulnerabilities']: 621 | bug = {'id': vulnerability['id'], 'title': vulnerability['title'].encode('utf-8'), 622 | 'vuln_type': vulnerability['vuln_type'].encode('utf-8'), 'reference': ''} 623 | 624 | if 'references' in vulnerability: 625 | if 'url' in vulnerability['references']: 626 | references = [] 627 | for reference_url in vulnerability['references']['url']: 628 | references.append(reference_url.encode('utf-8')) 629 | if len(references) != 0: 630 | bug['reference'] = references 631 | if 'cve' in vulnerability: 632 | bug['cve'] = vulnerability['cve'].encode('utf-8') 633 | if 'exploitdb' in vulnerability: 634 | bug['exploitdb'] = vulnerability['exploitdb'][0].encode('utf-8') 635 | # Sometimes there is no fixed in or its None 636 | if 'fixed_in' in vulnerability and vulnerability['fixed_in']: 637 | bug['fixed_in'] = vulnerability['fixed_in'].encode('utf-8') 638 | else: 639 | bug['fixed_in'] = '0' 640 | bugs.append(bug) 641 | temp_database[name] = bugs 642 | 643 | progress_adder += int(100 / progress_divider) 644 | self.database[_type] = temp_database 645 | self.update_config('sha_{}'.format(_type), sha_original) 646 | except: 647 | self.print_debug("_update_database parser error for {}: {}".format(_type, traceback.format_exc())) 648 | return False 649 | 650 | return True 651 | 652 | def scan_type_check(self, messageInfo, as_thread): 653 | if as_thread: 654 | if self.config.get('scan_type', 1) == 1: 655 | threading.Thread(target=self.check_url_or_body, args=(messageInfo, "plugins",)).start() 656 | threading.Thread(target=self.check_url_or_body, args=(messageInfo, "themes",)).start() 657 | elif self.config.get('scan_type', 1) == 2: 658 | threading.Thread(target=self.check_url_or_body, args=(messageInfo, "plugins",)).start() 659 | elif self.config.get('scan_type', 1) == 3: 660 | threading.Thread(target=self.check_url_or_body, args=(messageInfo, "themes",)).start() 661 | 662 | if self.config.get('admin_ajax', True): 663 | threading.Thread(target=self.check_admin_ajax, args=(messageInfo,)).start() 664 | else: 665 | issues = [] 666 | if self.config.get('scan_type', 1) == 1: 667 | issues += self.check_url_or_body(messageInfo, "plugins") 668 | issues += (self.check_url_or_body(messageInfo, "themes") or []) 669 | elif self.config.get('scan_type', 1) == 2: 670 | issues += self.check_url_or_body(messageInfo, "plugins") 671 | elif self.config.get('scan_type', 1) == 3: 672 | issues += (self.check_url_or_body(messageInfo, "themes") or []) 673 | 674 | if self.config.get('admin_ajax', True): 675 | issues += self.check_admin_ajax(messageInfo) 676 | 677 | return issues 678 | 679 | # implement IScannerCheck 680 | def doPassiveScan(self, baseRequestResponse): 681 | return self.scan_type_check(baseRequestResponse, False) 682 | 683 | def consolidateDuplicateIssues(self, existingIssue, newIssue): 684 | return 1 685 | 686 | # implement IHttpListener 687 | def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): 688 | if self.is_burp_pro or messageIsRequest: 689 | return 690 | 691 | # We are interested only with valid requests 692 | response = self.helpers.analyzeResponse(messageInfo.getResponse()) 693 | if response.getStatusCode() not in INTERESTING_CODES: 694 | return 695 | 696 | if toolFlag == IBurpExtenderCallbacks.TOOL_PROXY: 697 | self.scan_type_check(messageInfo, True) 698 | 699 | def check_url_or_body(self, base_request_response, _type): 700 | if self.config.get('full_body', False): 701 | return self.check_body(base_request_response, _type) 702 | else: 703 | return self.check_url(base_request_response, _type) 704 | 705 | def check_url(self, base_request_response, _type): 706 | try: 707 | wp_content_pattern = bytearray( 708 | "{}/{}/".format(self.config.get('wp_content', 'wp-content'), _type)) 709 | 710 | url = str(self.helpers.analyzeRequest(base_request_response).getUrl()) 711 | wp_content_begin_in_url = self.helpers.indexOf(url, wp_content_pattern, True, 0, len(url)) 712 | if wp_content_begin_in_url == -1: 713 | return [] 714 | 715 | regexp_plugin_name = re.compile( 716 | "{}/{}/([A-Za-z0-9_-]+)".format(self.config.get('wp_content', 'wp-content'), _type), re.IGNORECASE) 717 | plugin_name_regexp = regexp_plugin_name.search(url) 718 | if plugin_name_regexp: 719 | current_domain_not_normalized = url[0:wp_content_begin_in_url] 720 | current_domain = self.normalize_url(current_domain_not_normalized) 721 | plugin_name = plugin_name_regexp.group(1).lower() 722 | 723 | if self.is_unique_plugin_on_website(current_domain, plugin_name): 724 | version_type = 'active' 725 | [version_number, version_request] = self.active_scan(current_domain_not_normalized, _type, 726 | plugin_name, 727 | base_request_response) 728 | 729 | request = base_request_response.getRequest() 730 | wp_content_begin = self.helpers.indexOf(request, wp_content_pattern, True, 0, len(request)) 731 | markers = [ 732 | array('i', [wp_content_begin, wp_content_begin + len(wp_content_pattern) + len(plugin_name)])] 733 | 734 | if version_number == '0': 735 | version_number_regexp = self.regexp_version_number.search(url) 736 | if version_number_regexp: 737 | version_number = version_number_regexp.group(1).rstrip(".") 738 | version_type = 'passive' 739 | 740 | version_number_begin = self.helpers.indexOf(request, 741 | self.helpers.stringToBytes(version_number), 742 | True, 0, 743 | len(request)) 744 | markers.append( 745 | array('i', [version_number_begin, version_number_begin + len(version_number)])) 746 | 747 | return self.is_vulnerable_plugin_version(self.callbacks.applyMarkers(base_request_response, markers, None), 748 | _type, plugin_name, version_number, version_type, version_request) 749 | return [] 750 | except: 751 | self.print_debug("[-] check_url error: {}".format(traceback.format_exc())) 752 | return [] 753 | 754 | def check_admin_ajax(self, base_request_response): 755 | admin_ajax_pattern = bytearray("admin-ajax.php") 756 | analyzed_request = self.helpers.analyzeRequest(base_request_response) 757 | url = str(analyzed_request.getUrl()) 758 | 759 | is_admin_ajax = self.helpers.indexOf(url, admin_ajax_pattern, False, 0, len(url)) 760 | if is_admin_ajax == -1: 761 | return [] 762 | 763 | issues = [] 764 | parameters = analyzed_request.getParameters() 765 | for parameter in parameters: 766 | if parameter.getName() == 'action': 767 | action_value = parameter.getValue() 768 | self.print_debug("[+] check_admin_ajax action_value: {}".format(action_value)) 769 | plugins_list = self.database.get('admin_ajax', {}).get(action_value, None) 770 | if plugins_list: 771 | current_domain_not_normalized = url[0:is_admin_ajax] 772 | current_domain = self.normalize_url(current_domain_not_normalized) 773 | 774 | for plugin_name in plugins_list: 775 | if self.is_unique_plugin_on_website(current_domain, plugin_name): 776 | issues += self.is_vulnerable_plugin_version(base_request_response, "plugins", plugin_name, '0', 'passive', None) 777 | 778 | break 779 | 780 | return issues 781 | 782 | def check_body(self, base_request_response, _type): 783 | response = base_request_response.getResponse() 784 | wp_content_pattern = bytearray( 785 | "{}/{}/".format(self.config.get('wp_content', 'wp-content'), _type)) 786 | matches = self.find_pattern_in_data(response, wp_content_pattern) 787 | if not matches: 788 | return [] 789 | 790 | url = str(self.helpers.analyzeRequest(base_request_response).getUrl()) 791 | current_domain = self.normalize_url(url) 792 | 793 | regexp_plugin_name = re.compile( 794 | "{}/{}/([A-Za-z0-9_-]+)".format(self.config.get('wp_content', 'wp-content'), _type), re.IGNORECASE) 795 | 796 | issues = [] 797 | for wp_content_start, wp_content_stop in matches: 798 | # For performance reason only part of reponse 799 | response_partial_after = self.helpers.bytesToString( 800 | self.array_slice_bytes(response, wp_content_start, wp_content_stop + 100)) 801 | 802 | plugin_name_regexp = regexp_plugin_name.search(response_partial_after) 803 | if plugin_name_regexp: 804 | plugin_name = plugin_name_regexp.group(1).lower() 805 | if self.is_unique_plugin_on_website(current_domain, plugin_name): 806 | response_partial_before = self.helpers.bytesToString( 807 | self.array_slice_bytes(response, wp_content_start - 100, wp_content_start)).lower() 808 | 809 | markers = [array('i', [wp_content_start, wp_content_stop + len(plugin_name)])] 810 | 811 | version_type = 'active' 812 | version_number = '0' 813 | version_request = None 814 | 815 | url_begin_index = response_partial_before.rfind('http://') 816 | if url_begin_index == -1: 817 | url_begin_index = response_partial_before.rfind('https://') 818 | if url_begin_index == -1: 819 | url_begin_index = response_partial_before.rfind('//') 820 | 821 | if url_begin_index != -1: 822 | [version_number, version_request] = self.active_scan( 823 | response_partial_before[url_begin_index:], 824 | _type, plugin_name, base_request_response) 825 | 826 | if version_number == '0': 827 | # https://stackoverflow.com/questions/30020184/how-to-find-the-first-index-of-any-of-a-set-of-characters-in-a-string 828 | url_end_index = next( 829 | (i for i, ch in enumerate(response_partial_after) if ch in {"'", "\"", ")"}), 830 | None) 831 | if url_end_index: 832 | 833 | url_end = response_partial_after[0:url_end_index] 834 | version_number_regexp = self.regexp_version_number.search(url_end) 835 | if version_number_regexp: 836 | version_number = version_number_regexp.group(1).rstrip(".") 837 | version_type = 'passive' 838 | 839 | version_marker_start = url_end.find(version_number) 840 | markers.append(array('i', [wp_content_start + version_marker_start, 841 | wp_content_start + version_marker_start + len( 842 | version_number)])) 843 | 844 | issues += self.is_vulnerable_plugin_version(self.callbacks.applyMarkers(base_request_response, None, markers), 845 | _type, plugin_name, version_number, version_type, version_request) 846 | 847 | return issues 848 | def find_pattern_in_data(self, data, pattern): 849 | matches = [] 850 | start = 0 851 | data_length = len(data) 852 | pattern_length = len(pattern) 853 | while start < data_length: 854 | # indexOf(byte[] data, byte[] pattern, boolean caseSensitive, int from, int to) 855 | start = self.helpers.indexOf(data, pattern, False, start, data_length) 856 | if start == -1: 857 | break 858 | matches.append(array('i', [start, start + pattern_length])) 859 | start += pattern_length 860 | 861 | return matches 862 | 863 | def array_slice_bytes(self, _bytes, start, stop): 864 | byte_length = len(_bytes) 865 | if stop > byte_length: 866 | stop = byte_length 867 | if start < 0: 868 | start = 0 869 | 870 | temp = [] 871 | for i in xrange(start, stop): 872 | temp.append(_bytes[i]) 873 | return array('b', temp) 874 | 875 | def normalize_url(self, url): 876 | parsed_url = urlparse.urlparse(url) 877 | current_domain = parsed_url.netloc 878 | # Domain may looks like www.sth.pl:80, so here we normalize this 879 | if current_domain.startswith('www.'): 880 | current_domain = current_domain[4:] 881 | if ":" in current_domain: 882 | current_domain = current_domain.split(":")[0] 883 | self.print_debug("[*] normalize_url before: {}, after: {}".format(url, current_domain)) 884 | return current_domain 885 | 886 | def add_issue_wrapper(self, issue): 887 | self.lock_issues.acquire() 888 | row = self.list_issues.size() 889 | self.list_issues.add(issue) 890 | self.table_issues.fireTableRowsInserted(row, row) 891 | self.lock_issues.release() 892 | return issue 893 | 894 | def active_scan(self, current_domain, _type, plugin_name, base_request_response): 895 | current_version = '0' 896 | readme_http_request = None 897 | markers = None 898 | 899 | if self.config.get('active_scan', False): 900 | url = str(self.helpers.analyzeRequest(base_request_response).getUrl()).lower() 901 | self.print_debug("Current domain: {}, URL: {}".format(current_domain, url)) 902 | if current_domain.startswith('//'): 903 | if url.startswith('http://'): 904 | current_domain = 'http://' + current_domain[2:] 905 | else: 906 | current_domain = 'https://' + current_domain[2:] 907 | elif not current_domain.startswith('http'): 908 | if url.startswith('http://'): 909 | current_domain = 'http://' + current_domain 910 | else: 911 | current_domain = 'https://' + current_domain 912 | 913 | readme_url = "{}{}/{}/{}/readme.txt".format(current_domain, self.config.get('wp_content', 'wp-content'), 914 | _type, plugin_name) 915 | 916 | self.print_debug("[*] active_scan readme_url: {}".format(readme_url)) 917 | try: 918 | if url.endswith('readme.txt'): 919 | # This might be potential recursion, so don't make another request here 920 | return ['0', None] 921 | 922 | readme_request = self.helpers.buildHttpRequest(URL(readme_url)) 923 | readme_http_request = self.callbacks.makeHttpRequest(base_request_response.getHttpService(), 924 | readme_request) 925 | readme_response = readme_http_request.getResponse() 926 | 927 | readme_response_info = self.helpers.analyzeResponse(readme_response) 928 | 929 | if readme_response_info.getStatusCode() in INTERESTING_CODES: 930 | # Idea from wpscan\lib\common\models\wp_item\versionable.rb 931 | readme_content = self.helpers.bytesToString(readme_response) 932 | regexp_stable_tag = self.regexp_stable_tag.search(readme_content) 933 | 934 | if regexp_stable_tag: 935 | stable_tag = regexp_stable_tag.group(1) 936 | current_version = stable_tag 937 | markers = [array('i', [regexp_stable_tag.start(1), regexp_stable_tag.end(1)])] 938 | self.print_debug("[*] active_scan stable tag: {}".format(stable_tag)) 939 | 940 | changelog_regexp = self.regexp_version_from_changelog.finditer(readme_content) 941 | for version_match in changelog_regexp: 942 | version = version_match.group(1) 943 | if LooseVersion(version) > LooseVersion(current_version): 944 | self.print_debug("[*] active_scan newer version: {}".format(version)) 945 | current_version = version 946 | markers = [array('i', [version_match.start(1), version_match.end(1)])] 947 | 948 | if markers: 949 | readme_http_request = self.callbacks.applyMarkers(readme_http_request, None, markers) 950 | except: 951 | self.print_debug( 952 | "[-] active_scan for {} error: {}".format(readme_url, traceback.format_exc())) 953 | return ['0', None] 954 | return [current_version, readme_http_request] 955 | 956 | def is_unique_plugin_on_website(self, url, plugin_name): 957 | if plugin_name not in self.list_plugins_on_website[url]: 958 | self.list_plugins_on_website[url].append(plugin_name) 959 | self.print_debug("[+] is_unique_plugin_on_website URL: {}, plugin: {}".format(url, plugin_name)) 960 | return True 961 | 962 | return False 963 | 964 | def parse_bug_details(self, bug, plugin_name, _type): 965 | content = "ID: {}
Title: {}
Type: {}
".format( 966 | bug['id'], bug['id'], bug['title'], bug['vuln_type']) 967 | if 'reference' in bug: 968 | content += "References:
" 969 | for reference in bug['reference']: 970 | content += "{}
".format(reference, reference) 971 | if 'cve' in bug: 972 | content += "CVE: {}
".format(bug['cve']) 973 | if 'exploitdb' in bug: 974 | content += "Exploit Database: {}
".format( 975 | bug['exploitdb'], bug['exploitdb']) 976 | if 'fixed_in' in bug: 977 | content += "Fixed in version: {}
".format(bug['fixed_in']) 978 | content += "WordPress URL: https://wordpress.org/{type}/{plugin_name}".format( 979 | type=_type, plugin_name=plugin_name) 980 | return content 981 | 982 | def is_vulnerable_plugin_version(self, base_request_response, _type, plugin_name, version_number, version_type, 983 | version_request): 984 | has_vuln = False 985 | issues = [] 986 | if version_type == 'active' and version_number != '0': 987 | requests = [base_request_response, version_request] 988 | else: 989 | requests = [base_request_response] 990 | 991 | url = self.helpers.analyzeRequest(base_request_response).getUrl() 992 | 993 | if plugin_name in self.database[_type]: 994 | self.print_debug( 995 | "[*] is_vulnerable_plugin_version check {} {} version {}".format(_type, plugin_name, version_number)) 996 | for bug in self.database[_type][plugin_name]: 997 | if bug['fixed_in'] == '0' or ( 998 | version_number != '0' and LooseVersion(version_number) < LooseVersion(bug['fixed_in'])): 999 | self.print_debug( 1000 | "[+] is_vulnerable_plugin_version vulnerability inside {} version {}".format(plugin_name, 1001 | version_number)) 1002 | has_vuln = True 1003 | issues.append(self.add_issue_wrapper(CustomScanIssue( 1004 | url, 1005 | requests, 1006 | "{} inside {} {} version {}".format(bug['vuln_type'], _type[:-1], plugin_name, version_number), 1007 | self.parse_bug_details(bug, plugin_name, _type), 1008 | "High", "Certain" if version_type == 'active' else "Firm"))) 1009 | elif self.config.get('all_vulns', False): 1010 | self.print_debug( 1011 | "[+] is_vulnerable_plugin_version potential vulnerability inside {} version {}".format( 1012 | plugin_name, version_number)) 1013 | has_vuln = True 1014 | issues.append(self.add_issue_wrapper(CustomScanIssue( 1015 | url, 1016 | requests, 1017 | "Potential {} inside {} {} fixed in {}".format(bug['vuln_type'], _type[:-1], plugin_name, 1018 | bug['fixed_in']), 1019 | self.parse_bug_details(bug, plugin_name, _type), 1020 | "Information", "Certain"))) 1021 | 1022 | if not has_vuln and self.config.get('print_info', False): 1023 | print_info_details = "Found {} {}".format(_type[:-1], plugin_name) 1024 | if version_number != '0': 1025 | print_info_details += " version {}".format(version_number) 1026 | self.print_debug("[+] is_vulnerable_plugin_version print info: {}".format(print_info_details)) 1027 | issues.append(self.add_issue_wrapper(CustomScanIssue( 1028 | url, 1029 | requests, 1030 | print_info_details, 1031 | "{}
https://wordpress.org/{type}/{plugin_name}".format( 1032 | print_info_details, type=_type, plugin_name=plugin_name), 1033 | "Information", "Certain" if version_type == 'active' and version_number != '0' else "Firm"))) 1034 | 1035 | return issues 1036 | 1037 | def createMenuItems(self, invocation): 1038 | return [JMenuItem("Send to WordPress Scanner Intruder", 1039 | actionPerformed=lambda x, inv=invocation: self.menu_send_to_intruder_on_click(inv))] 1040 | 1041 | def menu_send_to_intruder_on_click(self, invocation): 1042 | response = invocation.getSelectedMessages()[0] 1043 | http_service = response.getHttpService() 1044 | request = response.getRequest() 1045 | analyzed_request = self.helpers.analyzeRequest(response) 1046 | 1047 | for param in analyzed_request.getParameters(): 1048 | # Remove all POST and GET parameters 1049 | if param.getType() == IParameter.PARAM_COOKIE: 1050 | continue 1051 | request = self.helpers.removeParameter(request, param) 1052 | 1053 | # Convert to GET 1054 | is_post = self.helpers.indexOf(request, bytearray("POST"), True, 0, 4) 1055 | if is_post != -1: 1056 | request = self.helpers.toggleRequestMethod(request) 1057 | 1058 | # Add backslash to last part of url 1059 | url = str(analyzed_request.getUrl()) 1060 | if not url.endswith("/"): 1061 | request_string = self.helpers.bytesToString(request) 1062 | # We are finding HTTP version protocol 1063 | http_index = request_string.find(" HTTP") 1064 | new_request_string = request_string[0:http_index] + "/" + request_string[http_index:] 1065 | request = self.helpers.stringToBytes(new_request_string) 1066 | 1067 | http_index_new_request = self.helpers.indexOf(request, bytearray(" HTTP"), True, 0, len(request)) 1068 | matches = [array('i', [http_index_new_request, http_index_new_request])] 1069 | 1070 | self.callbacks.sendToIntruder(http_service.getHost(), http_service.getPort(), 1071 | True if http_service.getProtocol() == "https" else False, request, matches) 1072 | 1073 | # implement IMessageEditorController 1074 | def getHttpService(self): 1075 | return self._current_advisory_entry.getHttpService() 1076 | 1077 | def getRequest(self): 1078 | return self._current_advisory_entry.getRequest() 1079 | 1080 | def getResponse(self): 1081 | return self._current_advisory_entry.getResponse() 1082 | 1083 | # implement ITab 1084 | def getTabCaption(self): 1085 | return "WordPress Scanner" 1086 | 1087 | def getUiComponent(self): 1088 | return self.panel_main 1089 | 1090 | 1091 | class CustomScanIssue(IScanIssue): 1092 | def __init__(self, url, http_messages, name, detail, severity, confidence): 1093 | self._url = url 1094 | self._http_messages = http_messages 1095 | self._name = name 1096 | self._detail = detail 1097 | # High, Medium, Low, Information, False positive 1098 | self._severity = severity 1099 | # Certain, Firm, Tentative 1100 | self._confidence = confidence 1101 | 1102 | def getUrl(self): 1103 | return self._url 1104 | 1105 | def getIssueName(self): 1106 | return self._name 1107 | 1108 | def getIssueType(self): 1109 | return 0 1110 | 1111 | def getSeverity(self): 1112 | return self._severity 1113 | 1114 | def getConfidence(self): 1115 | return self._confidence 1116 | 1117 | def getIssueBackground(self): 1118 | pass 1119 | 1120 | def getRemediationBackground(self): 1121 | pass 1122 | 1123 | def getIssueDetail(self): 1124 | return self._detail 1125 | 1126 | def getRemediationDetail(self): 1127 | pass 1128 | 1129 | def getHttpMessages(self): 1130 | return self._http_messages 1131 | 1132 | def getHttpService(self): 1133 | return self.getHttpMessages()[0].getHttpService() 1134 | 1135 | def getRequest(self, number): 1136 | if len(self._http_messages) > number: 1137 | return self._http_messages[number].getRequest() 1138 | else: 1139 | return "" 1140 | 1141 | def getResponse(self, number): 1142 | if len(self._http_messages) > number: 1143 | return self._http_messages[number].getResponse() 1144 | else: 1145 | return "" 1146 | 1147 | def getHost(self): 1148 | host = "{}://{}".format(self.getHttpService().getProtocol(), self.getHttpService().getHost()) 1149 | port = self.getHttpService().getPort() 1150 | if port not in [80, 443]: 1151 | host += ":{}".format(port) 1152 | return host 1153 | 1154 | def getPath(self): 1155 | url = str(self.getUrl()) 1156 | spliced = url.split("/") 1157 | return "/" + "/".join(spliced[3:]) 1158 | 1159 | 1160 | class IssuesDetailsTable(JTable): 1161 | def __init__(self, extender, model): 1162 | self._extender = extender 1163 | self.setModel(model) 1164 | 1165 | def changeSelection(self, row, col, toggle, extend): 1166 | model_row = self.convertRowIndexToModel(row) 1167 | self.current_issue = self._extender.list_issues.get(model_row) 1168 | 1169 | issue_details = self.current_issue.getIssueDetail() 1170 | self._extender.panel_bottom_advisory.setText(issue_details) 1171 | self._extender.panel_bottom_request1.setMessage(self.current_issue.getRequest(0), True) 1172 | self._extender.panel_bottom_response1.setMessage(self.current_issue.getResponse(0), False) 1173 | 1174 | request2 = self.current_issue.getRequest(1) 1175 | if request2 != "": 1176 | self._extender.panel_bottom.setEnabledAt(3, True) 1177 | self._extender.panel_bottom.setEnabledAt(4, True) 1178 | self._extender.panel_bottom_request2.setMessage(request2, True) 1179 | self._extender.panel_bottom_response2.setMessage(self.current_issue.getResponse(1), False) 1180 | else: 1181 | self._extender.panel_bottom.setEnabledAt(3, False) 1182 | self._extender.panel_bottom.setEnabledAt(4, False) 1183 | 1184 | JTable.changeSelection(self, row, col, toggle, extend) 1185 | 1186 | 1187 | class IssuesTableModel(AbstractTableModel): 1188 | def __init__(self, extender): 1189 | self._extender = extender 1190 | 1191 | def getRowCount(self): 1192 | try: 1193 | return self._extender.list_issues.size() 1194 | except: 1195 | return 0 1196 | 1197 | def getColumnCount(self): 1198 | return 5 1199 | 1200 | def getColumnName(self, column_index): 1201 | if column_index == 0: 1202 | return "Issue type" 1203 | elif column_index == 1: 1204 | return "Host" 1205 | elif column_index == 2: 1206 | return "Path" 1207 | elif column_index == 3: 1208 | return "Severity" 1209 | elif column_index == 4: 1210 | return "Confidence" 1211 | 1212 | def getValueAt(self, row_index, column_index): 1213 | advisory_entry = self._extender.list_issues.get(row_index) 1214 | if column_index == 0: 1215 | return advisory_entry.getIssueName() 1216 | elif column_index == 1: 1217 | return advisory_entry.getHost() 1218 | elif column_index == 2: 1219 | return advisory_entry.getPath() 1220 | elif column_index == 3: 1221 | return advisory_entry.getSeverity() 1222 | elif column_index == 4: 1223 | return advisory_entry.getConfidence() 1224 | 1225 | 1226 | class IntruderPluginsGenerator(IIntruderPayloadGeneratorFactory): 1227 | def __init__(self, generator): 1228 | self.generator = generator 1229 | 1230 | def getGeneratorName(self): 1231 | return "WordPress Plugins" 1232 | 1233 | def createNewInstance(self, attack): 1234 | return IntruderPayloadGenerator(self.generator, "plugins") 1235 | 1236 | 1237 | class IntruderThemesGenerator(IIntruderPayloadGeneratorFactory): 1238 | def __init__(self, generator): 1239 | self.generator = generator 1240 | 1241 | def getGeneratorName(self): 1242 | return "WordPress Themes" 1243 | 1244 | def createNewInstance(self, attack): 1245 | return IntruderPayloadGenerator(self.generator, "themes") 1246 | 1247 | 1248 | class IntruderPluginsThemesGenerator(IIntruderPayloadGeneratorFactory): 1249 | def __init__(self, generator): 1250 | self.generator = generator 1251 | 1252 | def getGeneratorName(self): 1253 | return "WordPress Plugins and Themes" 1254 | 1255 | def createNewInstance(self, attack): 1256 | return IntruderPayloadGeneratorMixed(self.generator) 1257 | 1258 | 1259 | class IntruderPayloadGenerator(IIntruderPayloadGenerator): 1260 | def __init__(self, extender, _type): 1261 | self.payload_index = 0 1262 | self.extender = extender 1263 | self.type = _type 1264 | self.iterator = self.extender.database[self.type].iteritems() 1265 | self.iterator_length = len(self.extender.database[self.type]) 1266 | self.extender.print_debug("[+] Start intruder for {}, has {} payloads".format(self.type, self.iterator_length)) 1267 | 1268 | def hasMorePayloads(self): 1269 | return self.payload_index < self.iterator_length 1270 | 1271 | def getNextPayload(self, base_value): 1272 | if self.payload_index <= self.iterator_length: 1273 | try: 1274 | k, v = self.iterator.next() 1275 | self.payload_index += 1 1276 | return "{}/{}/{}/".format(self.extender.config.get('wp_content', 'wp-content'), self.type, k) 1277 | 1278 | except StopIteration: 1279 | pass 1280 | 1281 | def reset(self): 1282 | self.payload_index = 0 1283 | 1284 | 1285 | class IntruderPayloadGeneratorMixed(IIntruderPayloadGenerator): 1286 | def __init__(self, extender): 1287 | self.payload_index = 0 1288 | self.extender = extender 1289 | self.iterator = chain(self.extender.database["themes"].iteritems(), 1290 | self.extender.database["plugins"].iteritems()) 1291 | self.iterator_themes_length = len(self.extender.database["themes"]) 1292 | self.iterator_length = (self.iterator_themes_length + len(self.extender.database["plugins"])) 1293 | self.extender.print_debug("[+] Start mixed intruder, has {} payloads".format(self.iterator_length)) 1294 | 1295 | def hasMorePayloads(self): 1296 | return self.payload_index <= self.iterator_length 1297 | 1298 | def getNextPayload(self, base_value): 1299 | if self.payload_index < self.iterator_length: 1300 | try: 1301 | k, v = self.iterator.next() 1302 | self.payload_index += 1 1303 | 1304 | if self.payload_index <= self.iterator_themes_length: 1305 | return "{}/{}/{}/".format(self.extender.config.get('wp_content', 'wp-content'), "themes", k) 1306 | else: 1307 | return "{}/{}/{}/".format(self.extender.config.get('wp_content', 'wp-content'), "plugins", k) 1308 | except StopIteration: 1309 | pass 1310 | 1311 | def reset(self): 1312 | self.payload_index = 0 1313 | -------------------------------------------------------------------------------- /data/admin_ajax.json.sha512: -------------------------------------------------------------------------------- 1 | 1499980dbec8088ebf57d8f41f1e2bf2967630c081168bebb60d9b19c50d617873b2c3cecec9622059f2bdfedbaebde28ae72d3632dabd59390f5a9ea023e699 -------------------------------------------------------------------------------- /images/bapp_store_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/bapp_store_1.png -------------------------------------------------------------------------------- /images/bapp_store_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/bapp_store_2.png -------------------------------------------------------------------------------- /images/debug_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/debug_mode.png -------------------------------------------------------------------------------- /images/install_burp_wp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/install_burp_wp.png -------------------------------------------------------------------------------- /images/install_jython.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/install_jython.png -------------------------------------------------------------------------------- /images/installed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/installed.png -------------------------------------------------------------------------------- /images/intruder_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/intruder_attack.png -------------------------------------------------------------------------------- /images/intruder_choose_payload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/intruder_choose_payload.png -------------------------------------------------------------------------------- /images/intruder_position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/intruder_position.png -------------------------------------------------------------------------------- /images/intruder_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/intruder_send.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 67 | 77 | 78 | 83 | 89 | 95 | BURP 107 | 119 | WP 131 | 132 | 133 | -------------------------------------------------------------------------------- /images/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/options.png -------------------------------------------------------------------------------- /images/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/usage.png -------------------------------------------------------------------------------- /images/usage_pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/usage_pro.png -------------------------------------------------------------------------------- /images/wp_ajax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/wordpress-scanner/0f61e883a69ab9f512e927a80e73d47f875eadc3/images/wp_ajax.png -------------------------------------------------------------------------------- /version.sig: -------------------------------------------------------------------------------- 1 | {"version_number": "0.1", "url": "https://raw.githubusercontent.com/kacperszurek/burp_wp/master/burp_wp.py", "sha256": "13c81cca2016c071792018b7a63ff47baf8dc402ab01f887207d344451e71ec8", "changelog": "Beta 0.1 release"} 2 | MCwCFHBROHKJw4vgy3UKBVFOz1a+LKd6AhRAW7QmTah85PM1lAWEuRAVICyIAA== --------------------------------------------------------------------------------