├── 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 | 
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 | 
13 |
14 | If you have Burp Pro, issues will also appear inside *Scanner* tab. Interesting things will be highlighted.
15 |
16 | 
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 | 
38 |
39 | 3. Click **Install button**
40 |
41 | 
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 | 
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 | 
54 |
55 | 5. Burp WP should appear inside `Burp Extensions list`. Also you will see new tab.
56 |
57 | 
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 | 
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 | 
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**. 
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 | 
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 | 
223 |
224 | There are 3 generators:
225 | 1. WordPress Plugins
226 | 2. WordPress Themes
227 | 3. WordPress Plugins and themes
228 |
229 |
230 | 
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 | 
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 |
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==
--------------------------------------------------------------------------------