SQLTruncScanner.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #
  2. # BurpSQLTruncationScanner - Scan for potential SQL truncation attack vectors.
  3. #
  4. # Copyright (c) 2020 Frans Hendrik Botes (InitRoot)
  5. # Verions 0.1
  6. #
  7. from burp import IBurpExtender, IScannerCheck, IScanIssue, ITab, IBurpExtenderCallbacks, IExtensionHelpers, IContextMenuFactory, IContextMenuInvocation, IHttpRequestResponse
  8. from java.io import PrintWriter
  9. from java.net import URL
  10. from java.util import ArrayList, List
  11. from java.util.regex import Matcher, Pattern
  12. import binascii
  13. from javax import swing
  14. from java.awt import Font, Color
  15. import sys
  16. import time
  17. import threading
  18. import base64
  19. import re
  20. from array import array
  21. import json
  22. #Global Issue List
  23. issueList = ArrayList()
  24. class BurpExtender(IBurpExtender, IScannerCheck, IContextMenuFactory, IHttpRequestResponse, IBurpExtenderCallbacks):
  25. def registerExtenderCallbacks(self, callbacks):
  26. sys.stdout = callbacks.getStdout()
  27. self._callbacks = callbacks
  28. self._helpers = callbacks.getHelpers()
  29. callbacks.setExtensionName("SQLTruncScanner")
  30. callbacks.issueAlert("SQL Truncation Scanner Enabled")
  31. stdout = PrintWriter(callbacks.getStdout(), True)
  32. stderr = PrintWriter(callbacks.getStderr(), True)
  33. callbacks.registerContextMenuFactory(self)
  34. print ("SQL Truncation Scanner loaded.")
  35. print ("Copyright (c) 2020 Frans Hendrik Botes (InitRoot)")
  36. self.httpTraffic = None
  37. self.resp = None
  38. #Set Parameters for Original Reqquest to Restruct
  39. self.orgHost = None
  40. self.orgPort = None
  41. self.orgProtoChoice = None
  42. self.orgHeaders = None
  43. self.orgParams = None
  44. self.orgMethod = None
  45. self.orgURLPath = None
  46. self.orgContType = None
  47. self.orgBaseline = None
  48. #Create context menu entry
  49. def createMenuItems(self, invocation):
  50. self.context = invocation
  51. itemContext = invocation.getSelectedMessages()
  52. if itemContext > 0:
  53. menuList = ArrayList()
  54. menuItem = swing.JMenuItem(
  55. "Scan with SQLTruncScanner", None, actionPerformed=self.start_scan)
  56. menuList.add(menuItem)
  57. return menuList
  58. return None
  59. # We are ready to start a scan for the specific request
  60. def start_scan(self, event):
  61. try:
  62. #For later use lets get what user selected.
  63. #Let's get what the user selected, only compatible with one item at a time. ADD check for multiple items and throw error
  64. scanIssues = self._callbacks.getScanIssues(None)
  65. httpTraffic = self.context.getSelectedMessages()
  66. print (len(httpTraffic))
  67. httpRequest = [item.request.tostring()
  68. for item in httpTraffic]
  69. orignalRequest = ''.join(httpRequest)
  70. #Rebuild the request and fetch response to calculate baseline value
  71. self.buildRequest(orignalRequest, httpTraffic)
  72. #We have the baseline, time to start fuzzing
  73. paramFuzzer = fuzzParams(httpTraffic, self._helpers, self._callbacks)
  74. thread = threading.Thread(target=paramFuzzer.fuzzParams(self.orgHost, self.orgPort, self.orgProtoChoice,
  75. self.orgHeaders, self.orgParams, self.orgMethod, self.orgURLPath, self.orgContType, self.orgBaseline), args=())
  76. thread.daemon = True
  77. thread.start()
  78. print (str(len(issueList)))
  79. # issue = ScanIssue(httpTraffic[0], self._helpers)
  80. #self._callbacks.addScanIssue(issue)
  81. #paramFuzzer.fuzzParams(self.orgHost,self.orgPort,self.orgProtoChoice,self.orgHeaders,self.orgParams,self.orgMethod,self.orgURLPath,self.orgContType,self.orgBaseline)
  82. except UnicodeEncodeError:
  83. print("Error in URL decode.")
  84. return None
  85. def buildRequest(self, strRequest, httpTraffMsg):
  86. stdout = PrintWriter(self._callbacks.getStdout(), True)
  87. strorignalRequest = strRequest
  88. #Get data about the request that was right clicked
  89. for item in httpTraffMsg:
  90. try:
  91. httpService = item.getHttpService()
  92. self.httpTraffic = httpService
  93. host = httpService.host
  94. port = httpService.port
  95. protocol = httpService.protocol
  96. protoChoice = True if protocol.lower() == 'https' else False
  97. #Parse the text area that should contain an HTTP request.
  98. requestInfo = self._helpers.analyzeRequest(strorignalRequest)
  99. #Request data
  100. headers = requestInfo.getHeaders()
  101. bodyOffset = requestInfo.bodyOffset
  102. body = strorignalRequest[bodyOffset:]
  103. for (i, header) in enumerate(headers):
  104. if header.lower().startswith("content-type:"):
  105. content_type = header.split(":")[1].lower().strip()
  106. method = headers[0].split(" ")[0]
  107. urlpath = headers[0].split(" ")[1]
  108. #Debugging area for output and parsing
  109. #stdout.println(str(body))
  110. #stdout.println(str(headers))
  111. #stdout.println(str(method))
  112. #stdout.println(str(content_type))
  113. #stdout.println(str(urlpath))
  114. #Identify and parse parameters in the request
  115. if method == "GET":
  116. stdout.println("[!] GET REQUEST IDENTIFIED")
  117. body = urlpath.split("?")[1]
  118. #print(body)
  119. params = dict(x.split('=') for x in body.split('&'))
  120. else:
  121. #Add logic here for the handling parameters in body and JSON content
  122. if "json" in str(content_type) or "JSON" in str(content_type):
  123. stdout.println("[!] JSON REQUEST IDENTIFIED")
  124. #print(body)
  125. #print("[!] BODY DONE")
  126. params = json.loads(body)
  127. #print(body)
  128. #print(params)
  129. else:
  130. stdout.println("[!] POST REQUEST IDENTIFIED")
  131. #print(body)
  132. params = dict(x.split('=') for x in body.split('&'))
  133. #print(params)
  134. stdout.println("[!] PARAMETERS IDENTIFIED!")
  135. stdout.println(params)
  136. stdout.println("[!] DETERMINING BASELINE")
  137. baselineInt = self.baseline(host, port, protoChoice, headers, params, method, urlpath, content_type)
  138. #Assign Parameters for Fuzzing
  139. self.orgHost = host
  140. self.orgPort = port
  141. self.orgProtoChoice = protoChoice
  142. self.orgHeaders = headers
  143. self.orgParams = params
  144. self.orgMethod = method
  145. self.orgURLPath = urlpath
  146. self.orgContType = content_type
  147. self.orgBaseline = baselineInt
  148. except Exception as ex:
  149. stdout.println("Problem parsing the request data" + "\n")
  150. stdout.println(ex)
  151. return
  152. def consolidateDuplicateIssues(self, existingIssue, newIssue):
  153. if (existingIssue.getIssueName() == newIssue.getIssueName()):
  154. return -1
  155. else:
  156. return 0
  157. def extensionUnloaded(self):
  158. print ("SQL Truncation Scanner unloaded")
  159. return
  160. def postRequest(self, headers, body, args_):
  161. #Needed: args=[host,port,protoChoice,request]
  162. stdout = PrintWriter(self._callbacks.getStdout(), True)
  163. request = self._helpers.buildHttpMessage(headers, body)
  164. args_.append(request)
  165. t = threading.Thread(target=self.makeRequest, args=args_)
  166. t.daemon = True
  167. t.start()
  168. t.join()
  169. def makeRequest(self, host, port, protoChoice, request):
  170. stdout = PrintWriter(self._callbacks.getStdout(), True)
  171. try:
  172. self.resp = self._callbacks.makeHttpRequest(
  173. host,
  174. port,
  175. protoChoice,
  176. request
  177. )
  178. except Exception as ex:
  179. stdout.println(ex)
  180. def baseline(self, host, port, protoChoice, headers, body, method, urlpath, content_type):
  181. if "json" not in content_type.lower():
  182. new_body = ""
  183. new_body += '&'.join("%s=%s" % (key, str(val)) for (key, val) in body.iteritems())
  184. #print(new_body)
  185. if method == "GET":
  186. url1 = urlpath.split("?")[0]
  187. url2 = "?" + str(new_body)
  188. headers[0] = "GET " + str(url1) + str(url2) + " HTTP/1.1"
  189. self.getRequest(headers, [host, port, protoChoice])
  190. else:
  191. self.postRequest(headers, new_body, [host, port, protoChoice])
  192. #Here we take the lengh and status code of the body returned as a baseline
  193. originalReq = self._helpers.analyzeRequest(self.resp)
  194. reqResponse = self._helpers.analyzeResponse(self.resp)
  195. reqRespTxt = self.resp.tostring()
  196. respStatusCode = reqResponse.getStatusCode()
  197. resbodyOffset = reqResponse.getBodyOffset()
  198. respbodyLen = len(reqRespTxt[resbodyOffset:])
  199. baselineData = str(respStatusCode) + str(respbodyLen)
  200. print(baselineData)
  201. return baselineData
  202. class fuzzParams():
  203. def __init__(self, reqres, helpers, callbacks):
  204. self.helpers = helpers
  205. self.reqres = reqres
  206. self.callbacks = callbacks
  207. def postRequest(self, headers, body, args_):
  208. #Needed: args=[host,port,protoChoice,request]
  209. stdout = PrintWriter(self.callbacks.getStdout(), True)
  210. request = self.helpers.buildHttpMessage(headers, body)
  211. args_.append(request)
  212. t = threading.Thread(target=self.makeRequest, args=args_)
  213. t.daemon = True
  214. t.start()
  215. t.join()
  216. def makeRequest(self, host, port, protoChoice, request):
  217. stdout = PrintWriter(self.callbacks.getStdout(), True)
  218. try:
  219. self.resp = self.callbacks.makeHttpRequest(
  220. host,
  221. port,
  222. protoChoice,
  223. request
  224. )
  225. except Exception as ex:
  226. stdout.println(ex)
  227. def fuzzParams(self, host, port, protoChoice, headers, body, method, urlpath, content_type, baseline):
  228. stdout = PrintWriter(self.callbacks.getStdout(), True)
  229. stdout.println("[!] FUZZING "+ str(len(body)) + " PARAMETERS")
  230. issueList.clear()
  231. payloadSet = {"5": ' 00', "10": ' 00', "15": ' 00', "20": ' 00', "30": ' 00', "40": ' 00'}
  232. #Let's loop through each parameter
  233. for param in body:
  234. stdout.println(" [-] FUZZING: " + str(param))
  235. fuzzParameter = str(param)
  236. for payLSD in payloadSet:
  237. stdout.println(" [-] PAYLOAD: " + payLSD)
  238. payload = payloadSet[payLSD]
  239. bodd = body
  240. bodd[fuzzParameter] = bodd[fuzzParameter] + payload
  241. if "json" not in content_type.lower():
  242. new_body = ""
  243. new_body += '&'.join("%s=%s" % (key, str(val))
  244. for (key, val) in bodd.iteritems())
  245. #print(" " + new_body)
  246. if method == "GET":
  247. url1 = urlpath.split("?")[0]
  248. url2 = "?" + str(new_body)
  249. headers[0] = "GET " + str(url1) + str(url2) + " HTTP/1.1"
  250. self.getRequest(headers, [host, port, protoChoice])
  251. else:
  252. self.postRequest(headers, new_body, [host, port, protoChoice])
  253. #Here we take the lengh and status code of the body returned as a baseline
  254. reqFuzzResponse = self.helpers.analyzeResponse(self.resp)
  255. reqFuzzReq = self.helpers.analyzeRequest(self.resp)
  256. reqFuzzRespTxt = self.resp.tostring()
  257. respFuzzStatusCode = reqFuzzResponse.getStatusCode()
  258. resFuzzbodyOffset = reqFuzzResponse.getBodyOffset()
  259. respFuzzbodyLen = len(reqFuzzRespTxt[resFuzzbodyOffset:])
  260. fuzzResponseData = str(respFuzzStatusCode) + str(respFuzzbodyLen)
  261. print(" " + fuzzResponseData)
  262. if fuzzResponseData != baseline:
  263. stdout.println(" [+] POSSIBLE INJECTION DETECTED")
  264. issue = ScanIssue(
  265. self.reqres[0], reqFuzzReq, "SQL Truncation Scanner", fuzzParameter + " | " + payLSD, "High")
  266. self.callbacks.addScanIssue(issue)
  267. return