istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/fuzz/fuzz_test.go (about) 1 //go:build integfuzz 2 // +build integfuzz 3 4 // Copyright Istio Authors 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 18 package fuzz 19 20 import ( 21 "context" 22 "fmt" 23 "strings" 24 "testing" 25 26 "istio.io/istio/pkg/test/framework" 27 "istio.io/istio/pkg/test/framework/components/namespace" 28 "istio.io/istio/pkg/test/kube" 29 "istio.io/istio/tests/common/jwt" 30 ) 31 32 const ( 33 apacheServer = "apache" 34 nginxServer = "nginx" 35 tomcatServer = "tomcat" 36 37 dotdotpwn = "dotdotpwn" 38 wfuzz = "wfuzz" 39 40 authzDenyPolicy = ` 41 apiVersion: security.istio.io/v1 42 kind: AuthorizationPolicy 43 metadata: 44 name: policy-deny 45 spec: 46 action: DENY 47 rules: 48 - to: 49 - operation: 50 paths: ["/private/secret.html"] 51 ` 52 jwtTool = "jwttool" 53 requestAuthnPolicy = ` 54 apiVersion: security.istio.io/v1 55 kind: RequestAuthentication 56 metadata: 57 name: jwt 58 spec: 59 jwtRules: 60 - issuer: "test-issuer-1@istio.io" 61 jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.10/tests/common/jwt/jwks.json" 62 - issuer: "test-issuer-2@istio.io" 63 jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.10/tests/common/jwt/jwks.json" 64 ` 65 ) 66 67 var ( 68 // Known unsupported path parameter ("/bla;foo") normalization for Tomcat. 69 dotdotPwnIgnoreTomcat = []string{ 70 "/../private/secret.html;index.html <- VULNERABLE!", 71 "/../private/secret.html;index.htm <- VULNERABLE!", 72 "/..%5Cprivate%5Csecret.html;index.html <- VULNERABLE!", 73 "/..%5Cprivate%5Csecret.html;index.htm <- VULNERABLE!", 74 } 75 // Known unsupported path parameter ("/bla;foo") normalization for Tomcat. 76 wfuzzIgnoreTomcat = []string{ 77 `%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 78 `%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 79 `%2e%2e/%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 80 `%2e%2e/%2e%2e/%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 81 `%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 82 `%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 83 `%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 84 `%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 85 `%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 86 `%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 87 `%2e%2e\\%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 88 `%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 89 `%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 90 `%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 91 `%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 92 `%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%2e%2e\\%70%72%69%76%61%74%65/%73%65%63%72%65%74%2e%68%74%6d%6c;%69%6e%64%65%78%2e%68%74%6d%6c`, 93 } 94 ) 95 96 func deploy(t framework.TestContext, name, ns, yaml string) { 97 t.ConfigIstio().File(ns, yaml).ApplyOrFail(t) 98 if _, err := kube.WaitUntilPodsAreReady(kube.NewPodFetch(t.Clusters().Default(), ns, "app="+name)); err != nil { 99 t.Fatalf("Wait for pod %s failed: %v", name, err) 100 } 101 t.Logf("deploy %s is ready", name) 102 } 103 104 func waitService(t framework.TestContext, name, ns string) { 105 if _, _, err := kube.WaitUntilServiceEndpointsAreReady(t.Clusters().Default().Kube(), ns, name); err != nil { 106 t.Fatalf("Wait for service %s failed: %v", name, err) 107 } 108 t.Logf("service %s is ready", name) 109 } 110 111 func ignoreTomcat(t framework.TestContext, line string, ignores []string) bool { 112 for _, ignore := range ignores { 113 if strings.Contains(line, ignore) { 114 t.Logf("ignored known unsupported normalization: %s", ignore) 115 return true 116 } 117 } 118 return false 119 } 120 121 func runFuzzer(t framework.TestContext, fuzzer, ns, server string) { 122 pods, err := t.Clusters().Default().PodsForSelector(context.TODO(), ns, "app="+fuzzer) 123 if err != nil { 124 t.Fatalf("failed to get %s pod: %v", fuzzer, err) 125 } 126 t.Logf("running %s test against the %s (should normally complete in 60 seconds)...", fuzzer, server) 127 128 switch fuzzer { 129 case dotdotpwn: 130 // Run the dotdotpwn fuzz testing against the http server ("-m http") on port 8080 ("-x 8080"), other arguments: 131 // "-C": Continue if no data was received from host. 132 // "-d 1": depth of traversals. 133 // "-t 50": time in milliseconds between each test. 134 // "-f private/secret.html": specific filename to fetch after bypassing the authorization policy. 135 // "-k secret_data_leaked": text pattern to match in the response. 136 // "-r %s.txt": report filename. 137 command := fmt.Sprintf(`./run.sh -m http -h %s -x 8080 -C -d 1 -t 50 -f private/secret.html -k secret_data_leaked -r %s.txt`, server, server) 138 stdout, stderr, err := t.Clusters().Default().PodExec(pods.Items[0].Name, ns, dotdotpwn, command) 139 if err != nil { 140 t.Fatalf("failed to run dotdotpwn: %v", err) 141 } 142 t.Logf("%s\n%s\n", stdout, stderr) 143 t.Logf("dotdotpwn fuzz test completed for %s", server) 144 145 var errLines []string 146 for _, line := range strings.Split(stdout, "\n") { 147 if strings.Contains(line, "<- VULNERABLE") { 148 if server == tomcatServer && ignoreTomcat(t, line, dotdotPwnIgnoreTomcat) { 149 continue 150 } 151 errLines = append(errLines, line) 152 } 153 } 154 if len(errLines) != 0 { 155 t.Errorf("found potential policy bypass requests, please read the log for more details:\n- %s", strings.Join(errLines, "\n- ")) 156 } else { 157 t.Logf("no potential policy bypass requests found") 158 } 159 case wfuzz: 160 // Run the wfuzz fuzz with the following parameters: 161 // -z file,wordlist/dirTraversal.txt,<...>: Fuzz based on the basic directory traversal patterns with various encodings (see details below). 162 // -f %s.out,csv -o csv: Output result in csv format. 163 // --ss secret_data_leaked: Show responses with the specified regex within the content. 164 // -t 5: Specify the number of concurrent connections. 165 // %s:8080/FUZZ: target server. 166 command := fmt.Sprintf("wfuzz -z file,wordlist/dirTraversal.txt,"+ 167 "doble_nibble_hex-"+ // Replaces ALL characters in string using the %%dd%dd escape. 168 "double_urlencode-"+ // Applies a double encode to special characters in string using the %25xx escape. 169 // Letters, digits, and the characters '_.-' are never quoted. 170 "first_nibble_hex-"+ // Replaces ALL characters in string using the %%dd? escape. 171 "second_nibble_hex-"+ // Replaces ALL characters in string using the %?%dd escape. 172 "uri_double_hex-"+ // Encodes ALL characters using the %25xx escape. 173 "uri_hex-"+ // Encodes ALL characters using the %xx escape. 174 "uri_triple_hex-"+ // Encodes ALL characters using the %25%xx%xx escape. 175 "uri_unicode-"+ // Replaces ALL characters in string using the %u00xx escape. 176 "urlencode-"+ // Replace special characters in string using the %xx escape. 177 // Letters, digits, and the characters '_.-' are never quoted. 178 "utf8 "+ // Replaces ALL characters in string using the \u00xx escape. 179 "-f %s.out,csv -o csv -c -v --ss secret_data_leaked -t 5 %s:8080/FUZZ", server, server) 180 stdout, stderr, err := t.Clusters().Default().PodExec(pods.Items[0].Name, ns, wfuzz, command) 181 if err != nil { 182 t.Fatalf("failed to run wfuzz: %v", err) 183 } 184 t.Logf("%s\n%s\n", stdout, stderr) 185 t.Logf("wfuzz test completed for %s", server) 186 187 var errLines []string 188 for _, line := range strings.Split(stdout, "\n") { 189 if strings.Contains(line, ",200,") { 190 if server == tomcatServer && ignoreTomcat(t, line, wfuzzIgnoreTomcat) { 191 continue 192 } 193 errLines = append(errLines, line) 194 } 195 } 196 if len(errLines) != 0 { 197 t.Errorf("found potential policy bypass requests, please read the log for more details:\n- %s", strings.Join(errLines, "\n- ")) 198 } else { 199 t.Logf("no potential policy bypass requests found") 200 } 201 202 default: 203 t.Fatalf("unknown fuzzer %s", fuzzer) 204 } 205 } 206 207 func TestFuzzAuthorization(t *testing.T) { 208 framework.NewTest(t). 209 Run(func(t framework.TestContext) { 210 ns := "fuzz-authz" 211 namespace.ClaimOrFail(t, t, ns) 212 213 t.ConfigIstio().YAML(ns, authzDenyPolicy).ApplyOrFail(t) 214 t.Logf("authorization policy applied") 215 216 deploy(t, dotdotpwn, ns, "fuzzers/dotdotpwn/dotdotpwn.yaml") 217 t.ConfigIstio().File(ns, "fuzzers/wfuzz/wordlist.yaml").ApplyOrFail(t) 218 deploy(t, wfuzz, ns, "fuzzers/wfuzz/wfuzz.yaml") 219 220 deploy(t, apacheServer, ns, "backends/apache/apache.yaml") 221 deploy(t, nginxServer, ns, "backends/nginx/nginx.yaml") 222 deploy(t, tomcatServer, ns, "backends/tomcat/tomcat.yaml") 223 waitService(t, apacheServer, ns) 224 waitService(t, nginxServer, ns) 225 waitService(t, tomcatServer, ns) 226 for _, fuzzer := range []string{dotdotpwn, wfuzz} { 227 t.NewSubTest(fuzzer).Run(func(t framework.TestContext) { 228 for _, target := range []string{apacheServer, nginxServer, tomcatServer} { 229 t.NewSubTest(target).Run(func(t framework.TestContext) { 230 runFuzzer(t, fuzzer, ns, target) 231 }) 232 } 233 }) 234 } 235 }) 236 } 237 238 func runJwtToolTest(t framework.TestContext, ns, server, jwtToken string) { 239 pods, err := t.Clusters().Default().PodsForSelector(context.TODO(), ns, "app="+jwtTool) 240 if err != nil { 241 t.Fatalf("failed to get jwttool pod: %v", err) 242 } 243 t.Logf("running jwttool fuzz test against the %s (should normally complete in 10 seconds)...", server) 244 245 // Run the jwttool fuzz testing with "--mode at" to run all tests: 246 // - JWT Attack Playbook 247 // - Fuzz existing claims to force errors 248 // - Fuzz common claims 249 commands := []string{ 250 "./run.sh", 251 "--targeturl", 252 fmt.Sprintf("http://%s:8080/private/secret.html", server), 253 "--noproxy", 254 "--headers", 255 fmt.Sprintf("Authorization: Bearer %s", jwtToken), 256 "--mode", 257 "at", 258 } 259 stdout, stderr, err := t.Clusters().Default().PodExecCommands(pods.Items[0].Name, ns, jwtTool, commands) 260 if err != nil { 261 t.Fatalf("failed to run jwttool: %v", err) 262 } 263 t.Logf("%s\n%s\n", stdout, stderr) 264 t.Logf("jwttool fuzz test completed for %s", server) 265 266 if !strings.Contains(stdout, "Prescan: original token Response Code: 200") { 267 t.Fatalf("could not find prescan check, please make sure the jwt_tool.py completed successfully") 268 } 269 errCases := []string{} 270 scanStarted := false 271 for _, line := range strings.Split(stdout, "\n") { 272 if scanStarted { 273 // First check the response is a valid test case. 274 if strings.Contains(line, "jwttool_") && !strings.Contains(line, "(should always be valid)") { 275 // Then add it to errCases if the test case has a response code other than 401. 276 if !strings.Contains(line, "Response Code: 401") { 277 errCases = append(errCases, line) 278 } 279 } 280 } else if strings.Contains(line, "LAUNCHING SCAN") { 281 scanStarted = true 282 } 283 } 284 if len(errCases) != 0 { 285 t.Errorf("found %d potential policy bypass requests:\n- %s", len(errCases), strings.Join(errCases, "\n- ")) 286 } else { 287 t.Logf("no potential policy bypass requests found") 288 } 289 } 290 291 func TestRequestAuthentication(t *testing.T) { 292 framework.NewTest(t). 293 Run(func(t framework.TestContext) { 294 ns := "fuzz-jwt" 295 namespace.ClaimOrFail(t, t, ns) 296 297 t.ConfigIstio().YAML(ns, requestAuthnPolicy).ApplyOrFail(t) 298 t.Logf("request authentication policy applied") 299 300 // We don't care about the actual backend for JWT test, one backend is good enough. 301 deploy(t, apacheServer, ns, "backends/apache/apache.yaml") 302 deploy(t, jwtTool, ns, "fuzzers/jwt_tool/jwt_tool.yaml") 303 waitService(t, apacheServer, ns) 304 testCases := []struct { 305 name string 306 baseToken string 307 }{ 308 {"TokenIssuer1", jwt.TokenIssuer1}, 309 {"TokenIssuer1WithAud", jwt.TokenIssuer1WithAud}, 310 {"TokenIssuer1WithAzp", jwt.TokenIssuer1WithAzp}, 311 {"TokenIssuer2", jwt.TokenIssuer2}, 312 {"TokenIssuer1WithNestedClaims1", jwt.TokenIssuer1WithNestedClaims1}, 313 {"TokenIssuer1WithNestedClaims2", jwt.TokenIssuer1WithNestedClaims2}, 314 {"TokenIssuer2WithSpaceDelimitedScope", jwt.TokenIssuer2WithSpaceDelimitedScope}, 315 } 316 for _, tc := range testCases { 317 t.NewSubTest(tc.name).Run(func(t framework.TestContext) { 318 runJwtToolTest(t, ns, apacheServer, tc.baseToken) 319 }) 320 } 321 }) 322 }