Skip to content

Commit 51e098d

Browse files
committed
Add scanner for Bleichenbacher oracle (ROBOT)
1 parent 02e81d1 commit 51e098d

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
Some TLS implementations handle errors processing RSA key exchanges and encryption (PKCS #1 v1.5 messages) in a broken way that leads an adaptive chosen-chiphertext attack. Attackers cannot recover a server's private key, but they can decrypt and sign messages with it. A strong oracle occurs when the TLS server does not strictly check message formatting and needs less than a million requests on average to decode a given ciphertext. A weak oracle server strictly checks message formatting and often requires many more requests to perform the attack.
2+
3+
## Vulnerable Applications
4+
5+
* F5 BIG-IP 11.6.0-11.6.2 (fixed in 11.6.2 HF1), 12.0.0-12.1.2 HF1 (fixed in 12.1.2 HF2), or 13.0.0-13.0.0 HF2 (fixed in 13.0.0 HF3) (CVE 2017-6168)
6+
* Citrix NetScaler Gateway 10.5 before build 67.13, 11.0 before build 71.22, 11.1 before build 56.19, and 12.0 before build 53.22 (CVE 2017-17382)
7+
* Radware Alteon firmware 31.0.0.0-31.0.3.0 (CVE 2017-17427)
8+
* Cisco ACE (CVE 2017-17428)
9+
* Cisco ASA 5500 series (CVE 2017-12373)
10+
* Bouncy Castle TLS < 1.0.3 configured to use the Java Cryptography Engine (CVE 2017-13098)
11+
* Erlang < 20.1.7, < 19.3.6.4, < 18.3.4.7 (CVE 2017-1000385)
12+
* WolfSSL < 3.12.2 (CVE 2017-13099)
13+
* MatrixSSL 3.8.3 (CVE 2016-6883)
14+
* Oracle Java <= 7u7, <= 6u35, <= 5u36, <= 1.4.2_38 (CVE 2012-5081)
15+
* IBM Domino
16+
* Palo Alto PAN-OS
17+
18+
(source: [https://robotattack.org/#patches](https://robotattack.org/#patches))
19+
20+
## Extra requirements
21+
22+
This module requires a working Python 3 install with the `cryptography` and `gmpy2` packages installed (e.g. via `pip3 install cryptography gmpy2`).
23+
24+
## Verification Steps
25+
26+
Perhaps the easiest way to reproduce is to install an older version of Erlang on Linux (the stock `erlang` package on Ubuntu 17.10 and before is unpatched), and run the [ssl_hello_world](https://github.com/ninenines/cowboy/tree/master/examples/ssl_hello_world) example from Cowboy (additionally requires `git` and `make`, be sure to use the 1.1.x branch for Erlang < 19).
27+
28+
```
29+
msf4 > use auxiliary/scanner/ssl/robot
30+
msf4 auxiliary(scanner/ssl/robot) > set RHOSTS 192.168.244.128
31+
RHOSTS => 192.168.244.128
32+
msf4 auxiliary(scanner/ssl/robot) > set RPORT 8443
33+
RPORT => 8443
34+
msf4 auxiliary(scanner/ssl/robot) > set VERBOSE true
35+
VERBOSE => true
36+
msf4 auxiliary(scanner/ssl/robot) > run
37+
38+
[*] Running for 192.168.244.128...
39+
[*] 192.168.244.128:8443 - Scanning host for Bleichenbacher oracle
40+
[*] 192.168.244.128:8443 - RSA N: 0xcdb5b51a3102cc751cfd6493a8b8801aa8c235c711e6c6954beca8cf648f461a68c9fd3fa81ad7e41634b739a0a33a138917c4e300a2543f7d09cf83ae9fc5338f6be04a59768708a2fa6b98e9affe0c24a23f79cda03a3ca367d4e7660e9da1c09b17d999b79296c65194f18c392471c9a051be048cbeea347abbb1a42d8af5
41+
[*] 192.168.244.128:8443 - RSA e: 0x10001
42+
[*] 192.168.244.128:8443 - Modulus size: 1024 bits, 128 bytes
43+
[+] 192.168.244.128:8443 - Vulnerable: (strong) oracle found TLSv1.2 with standard message flow
44+
[*] 192.168.244.128:8443 - Result of good request: TLS alert 10 of length 7
45+
[*] 192.168.244.128:8443 - Result of bad request 1 (wrong first bytes): TLS alert 51 of length 7
46+
[*] 192.168.244.128:8443 - Result of bad request 2 (wrong 0x00 position): TLS alert 10 of length 7
47+
[*] 192.168.244.128:8443 - Result of bad request 3 (missing 0x00): TLS alert 51 of length 7
48+
[*] 192.168.244.128:8443 - Result of bad request 4 (bad TLS version): TLS alert 10 of length 7
49+
[*] Scanned 1 of 1 hosts (100% complete)
50+
[*] Auxiliary module execution completed
51+
msf4 auxiliary(scanner/ssl/robot) >
52+
```
53+
54+
## Options
55+
56+
The scanner takes the normal `RHOSTS` and `RPORT` options to specify the hosts to scan on the port on which to scan them. In addition, it takes two options for the TLS behaviour: `cipher_group` and `timeout`.
57+
58+
The `cipher_group` option:
59+
60+
Select the ciphers to use to negotiate: all TLS_RSA ciphers (`all`, the default), TLS_RSA_WITH_AES_128_CBC_SHA (`cbc`), or TLS-RSA-WITH-AES-128-GCM-SHA256 (`gcm`).
61+
62+
```
63+
set cipher_group gcm
64+
```
65+
66+
The `timeout` option:
67+
68+
Set the interval to wait before considering the TLS connection timed out. The default is 5 seconds.
69+
70+
```
71+
set timeout 10
72+
```
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

Comments
 (0)