|
10 | 10 | from __future__ import print_function |
11 | 11 |
|
12 | 12 | import base64 |
| 13 | +import hashlib |
| 14 | +import hmac |
13 | 15 | import json |
14 | 16 | import os |
15 | 17 | import random |
|
38 | 40 | except (IOError, OSError): |
39 | 41 | pass |
40 | 42 |
|
| 43 | +# Self-contained JSON Web Token forge/parse for the '/jwt' endpoint, so '--jwt' has a live target in the |
| 44 | +# vuln-test. The signing secret is a common one (crackable from the shipped wordlist), the endpoint accepts |
| 45 | +# unsigned 'alg':'none' tokens (VULN) and reflects 'kid' into a SQL error (injectable key lookup). |
| 46 | +JWT_SECRET = "secret" |
| 47 | + |
| 48 | +def _jwt_b64url(data): |
| 49 | + return base64.urlsafe_b64encode(data).rstrip(b'=').decode() |
| 50 | + |
| 51 | +def _jwt_forge(header, payload, secret): |
| 52 | + seg = lambda obj: _jwt_b64url(json.dumps(obj, separators=(',', ':')).encode(UNICODE_ENCODING)) |
| 53 | + signingInput = "%s.%s" % (seg(header), seg(payload)) |
| 54 | + signature = _jwt_b64url(hmac.new(secret.encode(UNICODE_ENCODING), signingInput.encode(UNICODE_ENCODING), hashlib.sha256).digest()) |
| 55 | + return "%s.%s" % (signingInput, signature) |
| 56 | + |
| 57 | +def _jwt_parse(token): |
| 58 | + try: |
| 59 | + header, payload, signature = token.split('.') |
| 60 | + pad = lambda value: value + '=' * (-len(value) % 4) |
| 61 | + return json.loads(base64.urlsafe_b64decode(pad(header))), json.loads(base64.urlsafe_b64decode(pad(payload))), signature |
| 62 | + except Exception: |
| 63 | + return None |
| 64 | + |
| 65 | +JWT_TOKEN = _jwt_forge({"alg": "HS256", "typ": "JWT", "kid": "key1"}, {"user": "guest", "role": "user", "exp": 9999999999}, JWT_SECRET) |
| 66 | + |
41 | 67 | if PY3: |
42 | 68 | from http.client import FORBIDDEN |
43 | 69 | from http.client import INTERNAL_SERVER_ERROR |
@@ -1003,6 +1029,28 @@ def do_REQUEST(self): |
1003 | 1029 | self.wfile.write(output.encode(UNICODE_ENCODING, "ignore")) |
1004 | 1030 | return |
1005 | 1031 |
|
| 1032 | + if self.url == "/jwt": |
| 1033 | + self.send_response(OK) |
| 1034 | + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) |
| 1035 | + self.send_header("Connection", "close") |
| 1036 | + self.end_headers() |
| 1037 | + |
| 1038 | + parsed = _jwt_parse(self.params.get("session", "")) |
| 1039 | + output = "<html><body>access denied. please sign in.</body></html>" |
| 1040 | + if parsed: |
| 1041 | + header, payload, _ = parsed |
| 1042 | + kid = header.get("kid") |
| 1043 | + if hasattr(kid, "count") and kid.count("'") % 2 == 1: # str/unicode (py2/py3), not an int/dict |
| 1044 | + # VULNERABLE: 'kid' feeds a key-lookup query unsanitized -> a lone quote breaks it |
| 1045 | + output = "<html><body>You have an error in your SQL syntax near '%s'</body></html>" % kid |
| 1046 | + elif (header.get("alg") or "").lower() == "none": |
| 1047 | + output = "<html><body>welcome back, %s. secret area.</body></html>" % payload.get("user") # VULN: unsigned accepted |
| 1048 | + elif _jwt_forge(header, payload, JWT_SECRET) == self.params.get("session"): |
| 1049 | + output = "<html><body>welcome back, %s. secret area.</body></html>" % payload.get("user") |
| 1050 | + |
| 1051 | + self.wfile.write(output.encode(UNICODE_ENCODING, "ignore")) |
| 1052 | + return |
| 1053 | + |
1006 | 1054 | if self.url == "/csrf": |
1007 | 1055 | if self.params.get("csrf_token") == _csrf_token: |
1008 | 1056 | self.url = "/" |
|
0 commit comments