|
| 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