1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# This file is part of the Wapiti project (http://wapiti.sourceforge.io) 4# Copyright (C) 2008-2020 Nicolas Surribas 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19import os 20import sys 21from os.path import splitext, join as path_join 22from urllib.parse import quote 23from collections import defaultdict 24from enum import Enum 25from math import ceil 26import random 27from types import GeneratorType, FunctionType 28from binascii import hexlify 29 30from requests.exceptions import RequestException, ReadTimeout 31 32from wapitiCore.net.web import Request 33 34 35modules = [ 36 "mod_crlf", 37 "mod_exec", 38 "mod_file", 39 "mod_sql", 40 "mod_xss", 41 "mod_backup", 42 "mod_htaccess", 43 "mod_blindsql", 44 "mod_permanentxss", 45 "mod_nikto", 46 "mod_delay", 47 "mod_buster", 48 "mod_shellshock", 49 "mod_methods", 50 "mod_ssrf", 51 "mod_redirect", 52 "mod_xxe" 53] 54 55commons = ["blindsql", "exec", "file", "permanentxss", "redirect", "sql", "xss", "ssrf"] 56 57 58class PayloadType(Enum): 59 pattern = 1 60 time = 2 61 get = 3 62 post = 4 63 file = 5 64 65 66COMMON_ANNOYING_PARAMETERS = ( 67 "__VIEWSTATE", 68 "__VIEWSTATEENCRYPTED", 69 "__VIEWSTATEGENERATOR", 70 "__EVENTARGUMENT", 71 "__EVENTTARGET", 72 "__EVENTVALIDATION", 73 "ASPSESSIONID", 74 "ASP.NET_SESSIONID", 75 "JSESSIONID", 76 "CFID", 77 "CFTOKEN" 78) 79 80 81class Attack: 82 """This class represents an attack, it must be extended for any class which implements a new type of attack""" 83 84 name = "attack" 85 86 do_get = True 87 do_post = True 88 89 # List of modules (strings) that must be launched before the current module 90 # Must be defined in the code of the module 91 require = [] 92 93 BASE_DIR = os.path.dirname(sys.modules["wapitiCore"].__file__) 94 CONFIG_DIR = os.path.join(BASE_DIR, "config", "attacks") 95 PAYLOADS_FILE = None 96 97 # Color codes 98 STD = "\033[0;0m" 99 RED = "\033[0;31m" 100 GREEN = "\033[0;32m" 101 ORANGE = "\033[0;33m" 102 YELLOW = "\033[1;33m" 103 BLUE = "\033[1;34m" 104 MAGENTA = "\033[0;35m" 105 CYAN = "\033[0;36m" 106 GB = "\033[0;30m\033[47m" 107 108 allowed = [ 109 'php', 'html', 'htm', 'xml', 'xhtml', 'xht', 'xhtm', 110 'asp', 'aspx', 'php3', 'php4', 'php5', 'txt', 'shtm', 111 'shtml', 'phtm', 'phtml', 'jhtml', 'pl', 'jsp', 'cfm', 112 'cfml', 'py' 113 ] 114 115 # The priority of the module, from 0 (first) to 10 (last). Default is 5 116 PRIORITY = 5 117 118 def __init__(self, crawler, persister, logger, attack_options): 119 super().__init__() 120 self._session_id = "".join([random.choice("0123456789abcdefghjijklmnopqrstuvwxyz") for __ in range(0, 6)]) 121 self.crawler = crawler 122 self.persister = persister 123 self.add_vuln = persister.add_vulnerability 124 self.add_anom = persister.add_anomaly 125 self.payload_reader = PayloadReader(attack_options) 126 self.options = attack_options 127 128 # List of attack urls already launched in the current module 129 self.attacked_get = [] 130 self.attacked_post = [] 131 132 self.verbose = 0 133 self.color = 0 134 135 # List of modules (objects) that must be launched before the current module 136 # Must be left empty in the code 137 self.deps = [] 138 139 self._logger = logger 140 self.log = self._logger.log 141 self.log_blue = self._logger.log_blue 142 self.log_cyan = self._logger.log_cyan 143 self.log_green = self._logger.log_green 144 self.log_magenta = self._logger.log_magenta 145 self.log_orange = self._logger.log_orange 146 self.log_red = self._logger.log_red 147 self.log_white = self._logger.log_white 148 self.log_yellow = self._logger.log_yellow 149 150 def set_verbose(self, verbose): 151 self.verbose = verbose 152 153 def set_color(self): 154 self.color = 1 155 156 @property 157 def payloads(self): 158 """Load the payloads from the specified file""" 159 if self.PAYLOADS_FILE: 160 return self.payload_reader.read_payloads(path_join(self.CONFIG_DIR, self.PAYLOADS_FILE)) 161 return [] 162 163 def load_require(self, dependencies: list = None): 164 self.deps = dependencies 165 166 @property 167 def attack_level(self): 168 return self.options.get("level", 1) 169 170 @property 171 def internal_endpoint(self): 172 return self.options.get("internal_endpoint", "https://wapiti3.ovh/") 173 174 @property 175 def external_endpoint(self): 176 return self.options.get("external_endpoint", "http://wapiti3.ovh") 177 178 @property 179 def must_attack_query_string(self): 180 return self.attack_level == 2 181 182 def attack(self): 183 raise NotImplementedError("Override me bro") 184 185 def get_mutator(self): 186 methods = "" 187 if self.do_get: 188 methods += "G" 189 if self.do_post: 190 methods += "PF" 191 192 return Mutator( 193 methods=methods, 194 payloads=self.payloads, 195 qs_inject=self.must_attack_query_string, 196 skip=self.options.get("skipped_parameters") 197 ) 198 199 def does_timeout(self, request): 200 try: 201 self.crawler.send(request) 202 except ReadTimeout: 203 return True 204 except RequestException: 205 pass 206 return False 207 208 209class Mutator: 210 def __init__( 211 self, methods="FGP", payloads=None, qs_inject=False, max_queries_per_pattern: int = 1000, 212 parameters=None, # Restrict attack to a whitelist of parameters 213 skip=None # Must not attack those parameters (blacklist) 214 ): 215 self._mutate_get = "G" in methods.upper() 216 self._mutate_file = "F" in methods.upper() 217 self._mutate_post = "P" in methods.upper() 218 self._payloads = payloads 219 self._qs_inject = qs_inject 220 self._attacks_per_url_pattern = defaultdict(int) 221 self._max_queries_per_pattern = max_queries_per_pattern 222 self._parameters = parameters if isinstance(parameters, list) else [] 223 self._skip_list = skip if isinstance(skip, set) else set() 224 self._attack_hashes = set() 225 self._skip_list.update(COMMON_ANNOYING_PARAMETERS) 226 227 def iter_payloads(self): 228 # raise tuples of (payloads, flags) 229 if isinstance(self._payloads, tuple): 230 yield self._payloads 231 elif isinstance(self._payloads, list) or isinstance(self._payloads, GeneratorType): 232 yield from self._payloads 233 elif isinstance(self._payloads, FunctionType): 234 result = self._payloads() 235 if isinstance(result, GeneratorType): 236 yield from result 237 else: 238 yield result 239 240 def estimate_requests_count(self, request: Request): 241 estimation = len(request) if isinstance(self._payloads, tuple) else len(request) * len(self._payloads) 242 if self._qs_inject and request.method == "GET" and len(request) == 0: 243 # Injection directly in query string is made only on GET requests with no parameters in URL 244 estimation += len(self._payloads) 245 return estimation 246 247 def mutate(self, request: Request): 248 get_params = request.get_params 249 post_params = request.post_params 250 file_params = request.file_params 251 referer = request.referer 252 253 # estimation = self.estimate_requests_count(request) 254 # 255 # if self._attacks_per_url_pattern[request.hash_params] + estimation > self._max_queries_per_pattern: 256 # # Otherwise (pattern already attacked), make sure we don't exceed maximum allowed 257 # return 258 # 259 # self._attacks_per_url_pattern[request.hash_params] += estimation 260 261 for params_list in [get_params, post_params, file_params]: 262 if params_list is get_params and not self._mutate_get: 263 continue 264 265 if params_list is post_params and not self._mutate_post: 266 continue 267 268 if params_list is file_params and not self._mutate_file: 269 continue 270 271 for i in range(len(params_list)): 272 param_name = quote(params_list[i][0]) 273 274 if self._skip_list and param_name in self._skip_list: 275 continue 276 277 if self._parameters and param_name not in self._parameters: 278 continue 279 280 saved_value = params_list[i][1] 281 if saved_value is None: 282 saved_value = "" 283 284 if params_list is file_params: 285 params_list[i][1] = ["__PAYLOAD__", params_list[i][1][1]] 286 else: 287 params_list[i][1] = "__PAYLOAD__" 288 289 attack_pattern = Request( 290 request.path, 291 method=request.method, 292 get_params=get_params, 293 post_params=post_params, 294 file_params=file_params 295 ) 296 297 if hash(attack_pattern) not in self._attack_hashes: 298 self._attack_hashes.add(hash(attack_pattern)) 299 300 for payload, original_flags in self.iter_payloads(): 301 302 # no quoting: send() will do it for us 303 payload = payload.replace("[FILE_NAME]", request.file_name) 304 payload = payload.replace("[FILE_NOEXT]", splitext(request.file_name)[0]) 305 306 if isinstance(request.path_id, int): 307 payload = payload.replace("[PATH_ID]", str(request.path_id)) 308 309 payload = payload.replace( 310 "[PARAM_AS_HEX]", 311 hexlify(param_name.encode("utf-8", errors="replace")).decode() 312 ) 313 314 # Flags from iter_payloads should be considered as mutable (even if it's ot the case) 315 # so let's copy them just to be sure we don't mess with them. 316 flags = set(original_flags) 317 318 if params_list is file_params: 319 if "[EXTVALUE]" in payload: 320 if "." not in saved_value[0][:-1]: 321 # Nothing that looks like an extension, skip the payload 322 continue 323 payload = payload.replace("[EXTVALUE]", saved_value[0].rsplit(".", 1)[-1]) 324 325 payload = payload.replace("[VALUE]", saved_value[0]) 326 payload = payload.replace("[DIRVALUE]", saved_value[0].rsplit('/', 1)[0]) 327 params_list[i][1][0] = payload 328 flags.add(PayloadType.file) 329 else: 330 if "[EXTVALUE]" in payload: 331 if "." not in saved_value[:-1]: 332 # Nothing that looks like an extension, skip the payload 333 continue 334 payload = payload.replace("[EXTVALUE]", saved_value.rsplit(".", 1)[-1]) 335 336 payload = payload.replace("[VALUE]", saved_value) 337 payload = payload.replace("[DIRVALUE]", saved_value.rsplit('/', 1)[0]) 338 params_list[i][1] = payload 339 if params_list is get_params: 340 flags.add(PayloadType.get) 341 else: 342 flags.add(PayloadType.post) 343 344 evil_req = Request( 345 request.path, 346 method=request.method, 347 get_params=get_params, 348 post_params=post_params, 349 file_params=file_params, 350 referer=referer, 351 link_depth=request.link_depth 352 ) 353 yield evil_req, param_name, payload, flags 354 355 params_list[i][1] = saved_value 356 357 if not get_params and request.method == "GET" and self._qs_inject: 358 attack_pattern = Request( 359 "{}?__PAYLOAD__".format(request.path), 360 method=request.method, 361 referer=referer, 362 link_depth=request.link_depth 363 ) 364 365 if hash(attack_pattern) not in self._attack_hashes: 366 self._attack_hashes.add(hash(attack_pattern)) 367 368 for payload, original_flags in self.iter_payloads(): 369 # Ignore payloads reusing existing parameter values 370 if "[VALUE]" in payload: 371 continue 372 373 if "[DIRVALUE]" in payload: 374 continue 375 376 payload = payload.replace("[FILE_NAME]", request.file_name) 377 payload = payload.replace("[FILE_NOEXT]", splitext(request.file_name)[0]) 378 379 if isinstance(request.path_id, int): 380 payload = payload.replace("[PATH_ID]", str(request.path_id)) 381 382 payload = payload.replace( 383 "[PARAM_AS_HEX]", 384 hexlify(b"QUERY_STRING").decode() 385 ) 386 387 flags = set(original_flags) 388 389 evil_req = Request( 390 "{}?{}".format(request.path, quote(payload)), 391 method=request.method, 392 referer=referer, 393 link_depth=request.link_depth 394 ) 395 flags.add(PayloadType.get) 396 397 yield evil_req, "QUERY_STRING", payload, flags 398 399 400class FileMutator: 401 def __init__(self, payloads=None, parameters=None, skip=None): 402 self._payloads = payloads 403 self._attack_hashes = set() 404 self._parameters = parameters if isinstance(parameters, list) else [] 405 self._skip_list = skip if isinstance(skip, set) else set() 406 407 def iter_payloads(self): 408 # raise tuples of (payloads, flags) 409 if isinstance(self._payloads, tuple): 410 yield self._payloads 411 elif isinstance(self._payloads, list) or isinstance(self._payloads, GeneratorType): 412 yield from self._payloads 413 elif isinstance(self._payloads, FunctionType): 414 result = self._payloads() 415 if isinstance(result, GeneratorType): 416 yield from result 417 else: 418 yield result 419 420 def mutate(self, request: Request): 421 get_params = request.get_params 422 post_params = request.post_params 423 referer = request.referer 424 425 for i in range(len(request.file_params)): 426 new_params = request.file_params 427 param_name = new_params[i][0] 428 429 if self._skip_list and param_name in self._skip_list: 430 continue 431 432 if self._parameters and param_name not in self._parameters: 433 continue 434 435 for payload, original_flags in self.iter_payloads(): 436 437 # no quoting: send() will do it for us 438 payload = payload.replace("[FILE_NAME]", request.file_name) 439 payload = payload.replace("[FILE_NOEXT]", splitext(request.file_name)[0]) 440 441 if isinstance(request.path_id, int): 442 payload = payload.replace("[PATH_ID]", str(request.path_id)) 443 444 payload = payload.replace( 445 "[PARAM_AS_HEX]", 446 hexlify(param_name.encode("utf-8", errors="replace")).decode() 447 ) 448 449 # Flags from iter_payloads should be considered as mutable (even if it's ot the case) 450 # so let's copy them just to be sure we don't mess with them. 451 flags = set(original_flags) 452 453 new_params[i][1] = ["content.xml", payload, "text/xml"] 454 flags.add(PayloadType.file) 455 456 evil_req = Request( 457 request.path, 458 method=request.method, 459 get_params=get_params, 460 post_params=post_params, 461 file_params=new_params, 462 referer=referer, 463 link_depth=request.link_depth 464 ) 465 yield evil_req, param_name, payload, flags 466 467 468class PayloadReader: 469 """Class for reading and writing in text files""" 470 471 def __init__(self, options): 472 self._timeout = options["timeout"] 473 self._endpoint_url = options.get("external_endpoint", "http://wapiti3.ovh/") 474 475 def read_payloads(self, filename): 476 """returns a array""" 477 lines = [] 478 try: 479 with open(filename, errors="ignore") as f: 480 for line in f: 481 clean_line, flags = self.process_line(line) 482 if clean_line: 483 lines.append((clean_line, flags)) 484 except IOError as exception: 485 print(exception) 486 return lines 487 488 def process_line(self, line): 489 flags = set() 490 clean_line = line.strip(" \n") 491 clean_line = clean_line.replace("[TAB]", "\t") 492 clean_line = clean_line.replace("[LF]", "\n") 493 clean_line = clean_line.replace("[FF]", "\f") # Form feed 494 clean_line = clean_line.replace("[TIME]", str(int(ceil(self._timeout)) + 1)) 495 clean_line = clean_line.replace("[EXTERNAL_ENDPOINT]", self._endpoint_url) 496 497 payload_type = PayloadType.pattern 498 if "[TIMEOUT]" in clean_line: 499 payload_type = PayloadType.time 500 clean_line = clean_line.replace("[TIMEOUT]", "") 501 502 clean_line = clean_line.replace("\\0", "\0") 503 504 flags.add(payload_type) 505 return clean_line, flags 506 507 508if __name__ == "__main__": 509 510 mutator = Mutator(payloads=[("INJECT", set()), ("ATTACK", set())], qs_inject=True, max_queries_per_pattern=16) 511 res1 = Request( 512 "http://httpbin.org/post?var1=a&var2=b", 513 post_params=[['post1', 'c'], ['post2', 'd']] 514 ) 515 516 res2 = Request( 517 "http://httpbin.org/post?var1=a&var2=z", 518 post_params=[['post1', 'c'], ['post2', 'd']] 519 ) 520 521 res3 = Request( 522 "http://httpbin.org/get?login=admin&password=letmein", 523 ) 524 525 assert res1.hash_params == res2.hash_params 526 527 for evil_request, param_name, payload, flags in mutator.mutate(res1): 528 print(evil_request) 529 print(flags) 530 531 print('') 532 print("#"*50) 533 print('') 534 535 for evil_request, param_name, payload, flags in mutator.mutate(res2): 536 print(evil_request) 537 538 print('') 539 print("#"*50) 540 print('') 541 542 def iterator(): 543 yield "abc", set() 544 yield "def", set() 545 546 mutator = Mutator(payloads=iterator, qs_inject=True, max_queries_per_pattern=16) 547 for evil_request, param_name, payload, flags in mutator.mutate(res3): 548 print(evil_request) 549 550 print('') 551 print("#"*50) 552 print('') 553 554 def random_string(): 555 """Create a random unique ID that will be used to test injection.""" 556 # doesn't uppercase letters as BeautifulSoup make some data lowercase 557 return "w" + "".join([random.choice("0123456789abcdefghjijklmnopqrstuvwxyz") for __ in range(0, 9)]), set() 558 559 mutator = Mutator(payloads=random_string, qs_inject=True, max_queries_per_pattern=16) 560 for evil_request, param_name, payload, flags in mutator.mutate(res3): 561 print(evil_request) 562 print("Payload is", payload) 563 564 mutator = Mutator(methods="G", payloads=[("INJECT", set()), ("ATTACK", set())], qs_inject=True, parameters=["var1"]) 565 assert len(list(mutator.mutate(res1))) == 2 566