diff --git a/buildBinariesLinux.sh b/buildBinariesLinux.sh index 0573782..315c271 100755 --- a/buildBinariesLinux.sh +++ b/buildBinariesLinux.sh @@ -1,5 +1,5 @@ #!/bin/bash -version=1.2.1 +version=1.3.0 # Windows amd64 goos=windows diff --git a/go.mod b/go.mod index eed5020..f354fdd 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/Hackmanit/Web-Cache-Vulnerability-Scanner go 1.21 require ( - github.com/fatih/color v1.16.0 - golang.org/x/net v0.22.0 - golang.org/x/time v0.5.0 + github.com/fatih/color v1.18.0 + golang.org/x/net v0.30.0 + golang.org/x/time v0.7.0 moul.io/http2curl v1.0.0 ) @@ -13,6 +13,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/smartystreets/goconvey v1.8.1 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 74d004c..4f4a97a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -13,15 +13,15 @@ github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGB github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= diff --git a/pkg/config.go b/pkg/config.go index eb2170d..e6941c4 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -24,6 +24,7 @@ type ( UseHTTP bool CLDiff int HMDiff int + SkipTimebased bool CacheHeader string DisableColor bool IgnoreStatus []int diff --git a/pkg/deception.go b/pkg/deception.go index 7634474..1e4e076 100644 --- a/pkg/deception.go +++ b/pkg/deception.go @@ -16,18 +16,44 @@ func TestWebCacheDeception() reportResult { repResult.Technique = "Cache Deception" // cacheable extensions: class, css, jar, js, jpg, jpeg, gif, ico, png, bmp, pict, csv, doc, docx, xls, xlsx, ps, pdf, pls, ppt, pptx, tif, tiff, ttf, otf, webp, woff, woff2, svg, svgz, eot, eps, ejs, swf, torrent, midi, mid + appendings := []string{ - "/.css", // Path parameter - "/nonexistent.css", // Path parameter - "/../nonexistent.css", // Path traversal - "/%2e%2e/nonexistent.css", // Encoded path traversal - "%0Anonexistent.css", // Encoded Newline - "%00nonexistent.css", // Encoded Newline - "%3Bnonexistent.css", // Encoded Semicolon - "%23nonexistent.css", // Encoded Pound - "%3Fname = valnonexistent.css", // Encoded Question Mark - "%26name=valnonexistent.css", // Encoded Ampersand + "/.css", // Path parameter + "/nonexistent1.css", // Path parameter + "/../nonexistent2.css", // Path traversal + "/%2e%2e/nonexistent3.css", // Encoded path traversal + "%0Anonexistent4.css", // Encoded Newline + "%00nonexistent5.css", // Encoded Null Byte + "%09nonexistent6.css", // Encoded Tab + "%3Bnonexistent7.css", // Encoded Semicolon + "%23nonexistent8.css", // Encoded Pound + "%3Fname=valnonexistent9.css", // Encoded Question Mark + "%26name=valnonexistent10.css", // Encoded Ampersand + ";nonexistent11.css", // Semicolon + "?nonexistent12.css", // Question Mark + "&nonexistent13.css", // Ampersand + "%0A%2f%2e%2e%2fresources%2fnonexistent1.css", // Encoded Path Traversal to static directory using Encoded Newline + "%00%2f%2e%2e%2fresources%2fnonexistent2.css", // Encoded Path Traversal to static directory using Encoded Null Byte + "%09%2f%2e%2e%2fresources%2fnonexistent3.css", // Encoded Path Traversal to static directory using Encoded Tab + "%3B%2f%2e%2e%2fresources%2fnonexistent4.css", // Encoded Path Traversal to static directoryEncoded using Semicolon + "%23%2f%2e%2e%2fresources%2fnonexistent5.css", // Encoded Path Traversal to static directory using Encoded Pound + "%3F%2f%2e%2e%2fresources%2fnonexistent6.css", // Encoded Path Traversal to static directory using Encoded Question Mark + "%26%2f%2e%2e%2fresources%2fnonexistent7.css", // Encoded Path Traversal to static directory using Encoded Ampersand + ";%2f%2e%2e%2fresources%2fnonexistent8.css", // Encoded Path Traversal to static directory using Semicolon + "?%2f%2e%2e%2fresources%2fnonexistent9.css", // Encoded Path Traversal to static directoy using Question Mark + "&%2f%2e%2e%2fresources%2fnonexistent10.css", // Encoded Path Traversal to static directory using Ampersand + "%0A%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?a", // Encoded Path Traversal to robots.txt using Encoded Newline + "%00%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?b", // Encoded Path Traversal to robots.txt directory using Encoded Null Byte + "%09%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?c", // Encoded Path Traversal to robots.txt directory using Encoded Tab + "%3B%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?d", // Encoded Path Traversal to robots.txt directoryEncoded using Semicolon + "%23%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?e", // Encoded Path Traversal to robots.txt directory using Encoded Pound + "%3F%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?f", // Encoded Path Traversal to robots.txt directory using Encoded Question Mark + "%26%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?g", // Encoded Path Traversal to robots.txt directory using Encoded Ampersand + ";%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?h", // Encoded Path Traversal to robots.txt directory using Semicolon + "?%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?i", // Encoded Path Traversal to robots.txt directoy using Question Mark + "&%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2frobots.txt?j", // Encoded Path Traversal to robots.txt directory using Ampersand } + // TODO add "Exploiting normalization by the origin server" cache deception which needs to prepend something before the url path PrintVerbose("Testing for Web Cache Deception\n", NoColor, 1) @@ -65,7 +91,13 @@ func webCacheDeceptionTemplate(repResult *reportResult, appendStr string) error Timeout: http.DefaultClient.Timeout, } setRequest(req, false, "", http.Cookie{}) - resp, err = newClient.Do(req) + _, err = newClient.Do(req) + if err != nil { + msg = fmt.Sprintf("webCacheDeceptionTemplate: %s: newClient.Do: %s\n", appendStr, err.Error()) + PrintVerbose(msg, Yellow, 1) + return errors.New(msg) + } + resp, err = newClient.Do(req) // send request 2 times so it'll return a cache hit if deception was successful! if err != nil { msg = fmt.Sprintf("webCacheDeceptionTemplate: %s: newClient.Do: %s\n", appendStr, err.Error()) PrintVerbose(msg, Yellow, 1) @@ -93,7 +125,34 @@ func webCacheDeceptionTemplate(repResult *reportResult, appendStr string) error } repRequest.CurlCommand = command.String() - indicValue := strings.TrimSpace(strings.ToLower(resp.Header.Get(Config.Website.Cache.Indicator))) + var indicValue string + if Config.Website.Cache.Indicator == "" { // check if now a cache indicator exists + customCacheHeader := strings.ToLower(Config.CacheHeader) + for key, val := range resp.Header { + switch strings.ToLower(key) { + case "x-cache", "cf-cache-status", "x-drupal-cache", "x-varnish-cache", "akamai-cache-status", "server-timing", "x-iinfo", "x-nc", "x-hs-cf-cache-status", "x-proxy-cache", "x-cache-hits", "x-cache-status", "x-cache-info", "x-rack-cache", "cdn_cache_status", "x-akamai-cache", "x-akamai-cache-remote", "x-cache-remote", customCacheHeader: + // CacheHeader flag might not be set (=> ""). Continue in this case + if key == "" { + continue + } + Config.Website.Cache.Indicator = key + msg := fmt.Sprintf("%s: %s header was found: %s \n", req.URL, key, val) + PrintVerbose(msg, NoColor, 1) + addHitMissIndicatorMap(strings.ToLower(key)) + case "age": + // only set it it wasn't set to x-cache or sth. similar beforehand + if Config.Website.Cache.Indicator == "" { + Config.Website.Cache.Indicator = key + msg := fmt.Sprintf("%s: %s header was found: %s\n", req.URL, key, val) + PrintVerbose(msg, NoColor, 1) + addHitMissIndicatorMap(strings.ToLower("age")) + } + } + } + } + + indicValue = strings.TrimSpace(strings.ToLower(resp.Header.Get(Config.Website.Cache.Indicator))) + // check if there's a cache hit and if the body didn't change (otherwise it could be a cached error page, for example) if checkCacheHit(indicValue) && string(body) == Config.Website.Body { repResult.Vulnerable = true diff --git a/pkg/flags.go b/pkg/flags.go index fdefd32..a0a3d49 100644 --- a/pkg/flags.go +++ b/pkg/flags.go @@ -67,11 +67,13 @@ func ParseFlags(vers string) { appendBoolean(&generalOptions, &Config.Force, "force", "f", false, "Perform the tests no matter if there is a cache or even the cachebuster works or not") appendString(&generalOptions, &ignoreStatus, - "ignorestatus", "is", "", "Specify a custom cache header") + "ignorestatus", "is", "", "Ignore a specific status code for cache poisoning") appendInt(&generalOptions, &Config.CLDiff, "contentlengthdifference", "cldiff", 0, "Threshold for reporting possible Finding, when 'poisoned' response differs more from the original length. Default is 0 (don't check)") appendInt(&generalOptions, &Config.HMDiff, "hitmissdifference", "hmdiff", 30, "Threshold for time difference between cache hit and cache miss responses. Default is 30") + appendBoolean(&generalOptions, &Config.SkipTimebased, + "skiptimebased", "stime", false, "Skip checking if a repsonse gets cached by measuring time differences") appendString(&generalOptions, &Config.CacheHeader, "cacheheader", "ch", "", "Specify a custom cache header") appendBoolean(&generalOptions, &Config.DisableColor, diff --git a/pkg/recon.go b/pkg/recon.go index b272322..5d38645 100644 --- a/pkg/recon.go +++ b/pkg/recon.go @@ -158,9 +158,6 @@ func CheckCache(stat string) (CacheStruct, bool, []error) { cache.Indicator = key msg := fmt.Sprintf("%s header was found: %s\n", key, val) PrintVerbose(msg, NoColor, 1) - if cache.Indicator == "" { - cache.Indicator = "age" - } addHitMissIndicatorMap(strings.ToLower("age")) } } @@ -355,6 +352,10 @@ func cachebusterCookie(cache *CacheStruct) []error { if cache.Indicator == "" { // No Cache Indicator was found. So time will be used as Indicator + if Config.SkipTimebased { + continue + } + var newCookie http.Cookie var cb string for ii := 0; ii < 5*2; ii++ { @@ -563,6 +564,10 @@ func cachebusterHeader(cache *CacheStruct) []error { if cache.Indicator == "" { // No Cache Indicator was found. So time will be used as Indicator + if Config.SkipTimebased { + continue + } + var cb string for ii := 0; ii < 5*2; ii++ { weburl := Config.Website.Url.String() @@ -775,6 +780,10 @@ func cachebusterParameter(cache *CacheStruct) error { if cache.Indicator == "" { // No Cache Indicator was found. So time will be used as Indicator + if Config.SkipTimebased { + return nil + } + var urlCb string for i := 0; i < 5*2; i++ { if i%2 == 0 { @@ -958,6 +967,10 @@ func cachebusterHTTPMethod(cache *CacheStruct) []error { if cache.Indicator == "" { // No Cache Indicator was found. So time will be used as Indicator + if Config.SkipTimebased { + continue + } + skip := false for ii := 0; ii < 5*2; ii++ { weburl := Config.Website.Url.String() diff --git a/web-cache-vulnerability-scanner.go b/web-cache-vulnerability-scanner.go index 1682173..0ce274b 100644 --- a/web-cache-vulnerability-scanner.go +++ b/web-cache-vulnerability-scanner.go @@ -15,7 +15,7 @@ import ( "golang.org/x/net/http2" ) -const version = "1.2.1" +const version = "1.3.0" var ( currentDate string @@ -339,7 +339,7 @@ func runTests(rec int, u string, progress string, foundUrls *[]string, stat stri msg = addSeparator("Web Cache Deception") pkg.PrintVerbose(msg, pkg.NoColor, 1) - if alwaysMiss { // test for Web Cache Deception + if alwaysMiss || pkg.Config.Website.Cache.Indicator == "" { pkg.Statistics[stat+"deceptiontested"]++ repWebsite.Results = append(repWebsite.Results, pkg.TestWebCacheDeception()) } else {