Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit aef5d66

Browse files
authored
Merge pull request #2597 from delvelabs/generate-har
Generate HAR
2 parents b622c25 + b6969df commit aef5d66

7 files changed

Lines changed: 329 additions & 0 deletions

File tree

lib/controller/checks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def checkSqlInjection(place, parameter, value):
117117

118118
while tests:
119119
test = tests.pop(0)
120+
threadData.requestCollector.reset()
120121

121122
try:
122123
if kb.endDetection:
@@ -700,6 +701,7 @@ def genCmpPayload():
700701
injection.data[stype].matchRatio = kb.matchRatio
701702
injection.data[stype].trueCode = trueCode
702703
injection.data[stype].falseCode = falseCode
704+
injection.data[stype].collectedRequests = threadData.requestCollector.obtain()
703705

704706
injection.conf.textOnly = conf.textOnly
705707
injection.conf.titles = conf.titles

lib/core/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,6 +2601,9 @@ def logHTTPTraffic(requestLogMsg, responseLogMsg):
26012601
"""
26022602
Logs HTTP traffic to the output file
26032603
"""
2604+
threadData = getCurrentThreadData()
2605+
assert threadData.requestCollector is not None, "Request collector should be initialized by now"
2606+
threadData.requestCollector.collectRequest(requestLogMsg, responseLogMsg)
26042607

26052608
if not conf.trafficFile:
26062609
return

lib/core/option.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
from lib.request.rangehandler import HTTPRangeHandler
150150
from lib.request.redirecthandler import SmartRedirectHandler
151151
from lib.request.templates import getPageTemplate
152+
from lib.utils.collect import RequestCollectorFactory
152153
from lib.utils.crawler import crawl
153154
from lib.utils.deps import checkDependencies
154155
from lib.utils.search import search
@@ -1844,6 +1845,7 @@ def _setConfAttributes():
18441845
conf.scheme = None
18451846
conf.tests = []
18461847
conf.trafficFP = None
1848+
conf.requestCollectorFactory = None
18471849
conf.wFileType = None
18481850

18491851
def _setKnowledgeBaseAttributes(flushAll=True):
@@ -2228,6 +2230,11 @@ def _setTrafficOutputFP():
22282230

22292231
conf.trafficFP = openFile(conf.trafficFile, "w+")
22302232

2233+
def _setupRequestCollector():
2234+
conf.requestCollectorFactory = RequestCollectorFactory(collect=conf.collectRequests)
2235+
threadData = getCurrentThreadData()
2236+
threadData.requestCollector = conf.requestCollectorFactory.create()
2237+
22312238
def _setDNSServer():
22322239
if not conf.dnsDomain:
22332240
return
@@ -2604,6 +2611,7 @@ def init():
26042611
_setTamperingFunctions()
26052612
_setWafFunctions()
26062613
_setTrafficOutputFP()
2614+
_setupRequestCollector()
26072615
_resolveCrossReferences()
26082616
_checkWebSocket()
26092617

lib/core/optiondict.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@
197197
"binaryFields": "string",
198198
"charset": "string",
199199
"checkInternet": "boolean",
200+
"collectRequests": "string",
200201
"crawlDepth": "integer",
201202
"crawlExclude": "string",
202203
"csvDel": "string",

lib/core/threads.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def reset(self):
3838
Resets thread data model
3939
"""
4040

41+
self.requestCollector = None
42+
4143
self.disableStdOut = False
4244
self.hashDBCursor = None
4345
self.inTransaction = False

lib/parse/cmdline.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,10 @@ def cmdLineParser(argv=None):
632632
action="store_true",
633633
help="Never ask for user input, use the default behaviour")
634634

635+
general.add_option("--collect-requests", dest="collectRequests",
636+
action="store_true",
637+
help="Collect requests in HAR format")
638+
635639
general.add_option("--binary-fields", dest="binaryFields",
636640
help="Result fields having binary values (e.g. \"digest\")")
637641

lib/utils/collect.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Copyright (c) 2006-2017 sqlmap developers (http://sqlmap.org/)
5+
See the file 'doc/COPYING' for copying permission
6+
"""
7+
8+
from BaseHTTPServer import BaseHTTPRequestHandler
9+
from httplib import HTTPResponse
10+
from StringIO import StringIO
11+
import base64
12+
import re
13+
14+
from lib.core.data import logger
15+
from lib.core.settings import VERSION
16+
17+
18+
class RequestCollectorFactory:
19+
20+
def __init__(self, collect=False):
21+
self.collect = collect
22+
23+
def create(self):
24+
collector = RequestCollector()
25+
26+
if not self.collect:
27+
collector.collectRequest = self._noop
28+
else:
29+
logger.info("Request collection is enabled.")
30+
31+
return collector
32+
33+
@staticmethod
34+
def _noop(*args, **kwargs):
35+
pass
36+
37+
38+
class RequestCollector:
39+
40+
def __init__(self):
41+
self.reset()
42+
43+
def collectRequest(self, requestMessage, responseMessage):
44+
self.messages.append(RawPair(requestMessage, responseMessage))
45+
46+
def reset(self):
47+
self.messages = []
48+
49+
def obtain(self):
50+
if self.messages:
51+
return {"log": {
52+
"version": "1.2",
53+
"creator": {"name": "SQLMap", "version": VERSION},
54+
"entries": [pair.toEntry().toDict() for pair in self.messages],
55+
}}
56+
57+
58+
class RawPair:
59+
60+
def __init__(self, request, response):
61+
self.request = request
62+
self.response = response
63+
64+
def toEntry(self):
65+
return Entry(request=Request.parse(self.request),
66+
response=Response.parse(self.response))
67+
68+
69+
class Entry:
70+
71+
def __init__(self, request, response):
72+
self.request = request
73+
self.response = response
74+
75+
def toDict(self):
76+
return {
77+
"request": self.request.toDict(),
78+
"response": self.response.toDict(),
79+
}
80+
81+
82+
class Request:
83+
84+
def __init__(self, method, path, httpVersion, headers, postBody=None, raw=None, comment=None):
85+
self.method = method
86+
self.path = path
87+
self.httpVersion = httpVersion
88+
self.headers = headers or {}
89+
self.postBody = postBody
90+
self.comment = comment
91+
self.raw = raw
92+
93+
@classmethod
94+
def parse(cls, raw):
95+
request = HTTPRequest(raw)
96+
return cls(method=request.command,
97+
path=request.path,
98+
httpVersion=request.request_version,
99+
headers=request.headers,
100+
postBody=request.rfile.read(),
101+
comment=request.comment,
102+
raw=raw)
103+
104+
@property
105+
def url(self):
106+
host = self.headers.get('Host', 'unknown')
107+
return "http://%s%s" % (host, self.path)
108+
109+
def toDict(self):
110+
out = {
111+
"httpVersion": self.httpVersion,
112+
"method": self.method,
113+
"url": self.url,
114+
"headers": [dict(name=key, value=value) for key, value in self.headers.items()],
115+
"comment": self.comment,
116+
}
117+
if self.postBody:
118+
contentType = self.headers.get('Content-Type')
119+
out["postData"] = {
120+
"mimeType": contentType,
121+
"text": self.postBody,
122+
}
123+
return out
124+
125+
126+
class Response:
127+
128+
extract_status = re.compile(r'\((\d{3}) (.*)\)')
129+
130+
def __init__(self, httpVersion, status, statusText, headers, content, raw=None, comment=None):
131+
self.raw = raw
132+
self.httpVersion = httpVersion
133+
self.status = status
134+
self.statusText = statusText
135+
self.headers = headers
136+
self.content = content
137+
self.comment = comment
138+
139+
@classmethod
140+
def parse(cls, raw):
141+
altered = raw
142+
comment = None
143+
144+
if altered.startswith("HTTP response ["):
145+
io = StringIO(raw)
146+
first_line = io.readline()
147+
parts = cls.extract_status.search(first_line)
148+
status_line = "HTTP/1.0 %s %s" % (parts.group(1), parts.group(2))
149+
remain = io.read()
150+
altered = status_line + "\n" + remain
151+
comment = first_line
152+
153+
response = HTTPResponse(FakeSocket(altered))
154+
response.begin()
155+
return cls(httpVersion="HTTP/1.1" if response.version == 11 else "HTTP/1.0",
156+
status=response.status,
157+
statusText=response.reason,
158+
headers=response.msg,
159+
content=response.read(-1),
160+
comment=comment,
161+
raw=raw)
162+
163+
def toDict(self):
164+
content = {
165+
"mimeType": self.headers.get('Content-Type'),
166+
"text": self.content,
167+
}
168+
169+
binary = set(['\0', '\1'])
170+
if any(c in binary for c in self.content):
171+
content["encoding"] = "base64"
172+
content["text"] = base64.b64encode(self.content)
173+
174+
return {
175+
"httpVersion": self.httpVersion,
176+
"status": self.status,
177+
"statusText": self.statusText,
178+
"headers": [dict(name=key, value=value) for key, value in self.headers.items()],
179+
"content": content,
180+
"comment": self.comment,
181+
}
182+
183+
184+
class FakeSocket:
185+
# Original source:
186+
# https://stackoverflow.com/questions/24728088/python-parse-http-response-string
187+
188+
def __init__(self, response_text):
189+
self._file = StringIO(response_text)
190+
191+
def makefile(self, *args, **kwargs):
192+
return self._file
193+
194+
195+
class HTTPRequest(BaseHTTPRequestHandler):
196+
# Original source:
197+
# https://stackoverflow.com/questions/4685217/parse-raw-http-headers
198+
199+
def __init__(self, request_text):
200+
self.comment = None
201+
self.rfile = StringIO(request_text)
202+
self.raw_requestline = self.rfile.readline()
203+
204+
if self.raw_requestline.startswith("HTTP request ["):
205+
self.comment = self.raw_requestline
206+
self.raw_requestline = self.rfile.readline()
207+
208+
self.error_code = self.error_message = None
209+
self.parse_request()
210+
211+
def send_error(self, code, message):
212+
self.error_code = code
213+
self.error_message = message
214+
215+
216+
if __name__ == '__main__':
217+
import unittest
218+
219+
class RequestParseTest(unittest.TestCase):
220+
221+
def test_basic_request(self):
222+
req = Request.parse("GET /test HTTP/1.0\r\n"
223+
"Host: test\r\n"
224+
"Connection: close")
225+
self.assertEqual("GET", req.method)
226+
self.assertEqual("/test", req.path)
227+
self.assertEqual("close", req.headers['Connection'])
228+
self.assertEqual("test", req.headers['Host'])
229+
self.assertEqual("HTTP/1.0", req.httpVersion)
230+
231+
def test_with_request_as_logged_by_sqlmap(self):
232+
raw = "HTTP request [#75]:\nPOST /create.php HTTP/1.1\nHost: 127.0.0.1\nAccept-encoding: gzip,deflate\nCache-control: no-cache\nContent-type: application/x-www-form-urlencoded; charset=utf-8\nAccept: */*\nUser-agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10\nCookie: PHPSESSID=65c4a9cfbbe91f2d975d50ce5e8d1026\nContent-length: 138\nConnection: close\n\nname=test%27%29%3BSELECT%20LIKE%28%27ABCDEFG%27%2CUPPER%28HEX%28RANDOMBLOB%280.0.10000%2F2%29%29%29%29--&csrfmiddlewaretoken=594d26cfa3fad\n" # noqa
233+
req = Request.parse(raw)
234+
self.assertEqual("POST", req.method)
235+
self.assertEqual("138", req.headers["Content-Length"])
236+
self.assertIn("csrfmiddlewaretoken", req.postBody)
237+
self.assertEqual("HTTP request [#75]:\n", req.comment)
238+
239+
class RequestRenderTest(unittest.TestCase):
240+
def test_render_get_request(self):
241+
req = Request(method="GET",
242+
path="/test.php",
243+
headers={"Host": "example.com", "Content-Length": "0"},
244+
httpVersion="HTTP/1.1",
245+
comment="Hello World")
246+
out = req.toDict()
247+
self.assertEqual("GET", out["method"])
248+
self.assertEqual("http://example.com/test.php", out["url"])
249+
self.assertIn({"name": "Host", "value": "example.com"}, out["headers"])
250+
self.assertEqual("Hello World", out["comment"])
251+
self.assertEqual("HTTP/1.1", out["httpVersion"])
252+
253+
def test_render_with_post_body(self):
254+
req = Request(method="POST",
255+
path="/test.php",
256+
headers={"Host": "example.com",
257+
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"},
258+
httpVersion="HTTP/1.1",
259+
postBody="name=test&csrfmiddlewaretoken=594d26cfa3fad\n")
260+
out = req.toDict()
261+
self.assertEqual(out["postData"], {
262+
"mimeType": "application/x-www-form-urlencoded; charset=utf-8",
263+
"text": "name=test&csrfmiddlewaretoken=594d26cfa3fad\n",
264+
})
265+
266+
class ResponseParseTest(unittest.TestCase):
267+
def test_parse_standard_http_response(self):
268+
raw = "HTTP/1.1 404 Not Found\nContent-length: 518\nX-powered-by: PHP/5.6.30\nContent-encoding: gzip\nExpires: Thu, 19 Nov 1981 08:52:00 GMT\nVary: Accept-Encoding\nUri: http://127.0.0.1/\nServer: Apache/2.4.10 (Debian)\nConnection: close\nPragma: no-cache\nCache-control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\nDate: Fri, 23 Jun 2017 16:18:17 GMT\nContent-type: text/html; charset=UTF-8\n\n<!doctype html>\n<html>Test</html>\n" # noqa
269+
resp = Response.parse(raw)
270+
self.assertEqual(resp.status, 404)
271+
self.assertEqual(resp.statusText, "Not Found")
272+
273+
def test_parse_response_as_logged_by_sqlmap(self):
274+
raw = "HTTP response [#74] (200 OK):\nContent-length: 518\nX-powered-by: PHP/5.6.30\nContent-encoding: gzip\nExpires: Thu, 19 Nov 1981 08:52:00 GMT\nVary: Accept-Encoding\nUri: http://127.0.0.1/\nServer: Apache/2.4.10 (Debian)\nConnection: close\nPragma: no-cache\nCache-control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\nDate: Fri, 23 Jun 2017 16:18:17 GMT\nContent-type: text/html; charset=UTF-8\n\n<!doctype html>\n<html>Test</html>\n" # noqa
275+
resp = Response.parse(raw)
276+
self.assertEqual(resp.status, 200)
277+
self.assertEqual(resp.statusText, "OK")
278+
self.assertEqual(resp.headers["Content-Length"], "518")
279+
self.assertIn("Test", resp.content)
280+
self.assertEqual("HTTP response [#74] (200 OK):\n", resp.comment)
281+
282+
class ResponseRenderTest(unittest.TestCase):
283+
def test_simple_page_encoding(self):
284+
resp = Response(status=200, statusText="OK",
285+
httpVersion="HTTP/1.1",
286+
headers={"Content-Type": "text/html"},
287+
content="<html>\n<body>Hello</body>\n</html>")
288+
out = resp.toDict()
289+
self.assertEqual(200, out["status"])
290+
self.assertEqual("OK", out["statusText"])
291+
self.assertIn({"name": "Content-Type", "value": "text/html"}, out["headers"])
292+
self.assertEqual(out["content"], {
293+
"mimeType": "text/html",
294+
"text": "<html>\n<body>Hello</body>\n</html>",
295+
})
296+
297+
def test_simple_body_contains_binary_data(self):
298+
resp = Response(status=200, statusText="OK",
299+
httpVersion="HTTP/1.1",
300+
headers={"Content-Type": "application/octet-stream"},
301+
content="test\0abc")
302+
out = resp.toDict()
303+
self.assertEqual(out["content"], {
304+
"encoding": "base64",
305+
"mimeType": "application/octet-stream",
306+
"text": "dGVzdABhYmM=",
307+
})
308+
309+
unittest.main(buffer=False)

0 commit comments

Comments
 (0)