Skip to content

Commit

Permalink
Add WAF identifier
Browse files Browse the repository at this point in the history
  • Loading branch information
svkirillov committed Jul 27, 2022
1 parent d273d2e commit b17276d
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 36 deletions.
1 change: 1 addition & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func parseFlags() error {
flag.Int("idleConnTimeout", 2, "The maximum amount of time a keep-alive connection will live")
flag.Bool("followCookies", false, "If true, use cookies sent by the server. May work only with --maxIdleConns=1")
flag.Bool("renewSession", false, "Renew cookies before each test. Should be used with --followCookies flag")
flag.Bool("disableWafIdentification", false, "Disable WAF identification")
flag.Int("blockStatusCode", 403, "HTTP status code that WAF uses while blocking requests")
flag.IntSlice("passStatusCode", []int{200, 404}, "HTTP response status code that WAF uses while passing requests")
flag.String("blockRegex", "",
Expand Down
26 changes: 26 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,32 @@ func run(ctx context.Context, logger *logrus.Logger) error {

db := db.NewDB(testCases)

if !cfg.DisableWafIdentification {
detector, err := scanner.NewDetector(cfg)
if err != nil {
return errors.Wrap(err, "couldn't create WAF detector")
}

logger.Info("Try to identify WAF solution")

name, vendor, err := detector.DetectWAF(ctx)
if err != nil {
return errors.Wrap(err, "couldn't detect")
}

if name != "" && vendor != "" {
logger.WithFields(logrus.Fields{
"solution": name,
"vendor": vendor,
}).Info("WAF was identified. Force enabling `--followCookies' and `--renewSession' options")

cfg.FollowCookies = true
cfg.RenewSession = true
} else {
logger.Info("WAF was not identified")
}
}

s, err := scanner.New(logger, cfg, db, templates, router, false)
if err != nil {
return errors.Wrap(err, "couldn't create scanner")
Expand Down
63 changes: 32 additions & 31 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
package config

type Config struct {
URL string `mapstructure:"url"`
WebSocketURL string `mapstructure:"wsURL"`
GRPCPort uint16 `mapstructure:"grpcPort"`
HTTPHeaders map[string]string `mapstructure:"headers"`
TLSVerify bool `mapstructure:"tlsVerify"`
Proxy string `mapstructure:"proxy"`
MaxIdleConns int `mapstructure:"maxIdleConns"`
MaxRedirects int `mapstructure:"maxRedirects"`
IdleConnTimeout int `mapstructure:"idleConnTimeout"`
FollowCookies bool `mapstructure:"followCookies"`
RenewSession bool `mapstructure:"renewSession"`
BlockStatusCode int `mapstructure:"blockStatusCode"`
PassStatusCode []int `mapstructure:"passStatusCode"`
BlockRegex string `mapstructure:"blockRegex"`
PassRegex string `mapstructure:"passRegex"`
NonBlockedAsPassed bool `mapstructure:"nonBlockedAsPassed"`
Workers int `mapstructure:"workers"`
RandomDelay int `mapstructure:"randomDelay"`
SendDelay int `mapstructure:"sendDelay"`
ReportPath string `mapstructure:"reportPath"`
ReportName string `mapstructure:"reportName"`
ReportFormat string `mapstructure:"reportFormat"`
TestCase string `mapstructure:"testCase"`
TestCasesPath string `mapstructure:"testCasesPath"`
TestSet string `mapstructure:"testSet"`
WAFName string `mapstructure:"wafName"`
IgnoreUnresolved bool `mapstructure:"ignoreUnresolved"`
BlockConnReset bool `mapstructure:"blockConnReset"`
SkipWAFBlockCheck bool `mapstructure:"skipWAFBlockCheck"`
AddHeader string `mapstructure:"addHeader"`
OpenAPIFile string `mapstructure:"openapiFile"`
URL string `mapstructure:"url"`
WebSocketURL string `mapstructure:"wsURL"`
GRPCPort uint16 `mapstructure:"grpcPort"`
HTTPHeaders map[string]string `mapstructure:"headers"`
TLSVerify bool `mapstructure:"tlsVerify"`
Proxy string `mapstructure:"proxy"`
MaxIdleConns int `mapstructure:"maxIdleConns"`
MaxRedirects int `mapstructure:"maxRedirects"`
IdleConnTimeout int `mapstructure:"idleConnTimeout"`
FollowCookies bool `mapstructure:"followCookies"`
RenewSession bool `mapstructure:"renewSession"`
DisableWafIdentification bool `mapstructure:"disableWafIdentification"`
BlockStatusCode int `mapstructure:"blockStatusCode"`
PassStatusCode []int `mapstructure:"passStatusCode"`
BlockRegex string `mapstructure:"blockRegex"`
PassRegex string `mapstructure:"passRegex"`
NonBlockedAsPassed bool `mapstructure:"nonBlockedAsPassed"`
Workers int `mapstructure:"workers"`
RandomDelay int `mapstructure:"randomDelay"`
SendDelay int `mapstructure:"sendDelay"`
ReportPath string `mapstructure:"reportPath"`
ReportName string `mapstructure:"reportName"`
ReportFormat string `mapstructure:"reportFormat"`
TestCase string `mapstructure:"testCase"`
TestCasesPath string `mapstructure:"testCasesPath"`
TestSet string `mapstructure:"testSet"`
WAFName string `mapstructure:"wafName"`
IgnoreUnresolved bool `mapstructure:"ignoreUnresolved"`
BlockConnReset bool `mapstructure:"blockConnReset"`
SkipWAFBlockCheck bool `mapstructure:"skipWAFBlockCheck"`
AddHeader string `mapstructure:"addHeader"`
OpenAPIFile string `mapstructure:"openapiFile"`
}
2 changes: 1 addition & 1 deletion internal/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func printConsoleReportTable(s *db.Statistics, reportTime time.Time, wafName str
fmt.Fprintf(&buffer, "\nPositive Tests:\n")

// Positive cases summary table
posTable := tablewriter.NewWriter(os.Stdout)
posTable := tablewriter.NewWriter(&buffer)
posTable.SetHeader(baseHeader)
for index := range baseHeader {
posTable.SetColMinWidth(index, colMinWidth)
Expand Down
103 changes: 103 additions & 0 deletions internal/scanner/detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package scanner

import (
"context"
"crypto/tls"
"net/http"
"net/url"
"time"

"github.com/pkg/errors"

"github.com/wallarm/gotestwaf/internal/config"
"github.com/wallarm/gotestwaf/internal/scanner/detectors"
)

const (
xssPayload = `<script>alert("XSS");</script>`
sqliPayload = `UNION SELECT ALL FROM information_schema AND ' or SLEEP(5) or '`
lfiPayload = `../../../../etc/passwd`
rcePayload = `/bin/cat /etc/passwd; ping 127.0.0.1; curl google.com`
xxePayload = `<!ENTITY xxe SYSTEM "file:///etc/shadow">]><pwn>&hack;</pwn>`
)

type WAFDetector struct {
client *http.Client
target string
}

func NewDetector(cfg *config.Config) (*WAFDetector, error) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: !cfg.TLSVerify},
IdleConnTimeout: time.Duration(cfg.IdleConnTimeout) * time.Second,
MaxIdleConns: cfg.MaxIdleConns,
MaxIdleConnsPerHost: cfg.MaxIdleConns, // net.http hardcodes DefaultMaxIdleConnsPerHost to 2!
}

if cfg.Proxy != "" {
proxyURL, err := url.Parse(cfg.Proxy)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse proxy URL")
}

tr.Proxy = http.ProxyURL(proxyURL)
}

client := &http.Client{
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}

target, err := url.Parse(cfg.URL)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse URL")
}

return &WAFDetector{
client: client,
target: GetTargetURL(target),
}, nil
}

// doRequest sends HTTP-request with malicious payload to trigger WAF.
func (w *WAFDetector) doRequest(ctx context.Context) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, w.target, nil)
if err != nil {
return nil, errors.Wrap(err, "couldn't create request")
}

queryParams := req.URL.Query()
queryParams.Add("a", xssPayload)
queryParams.Add("b", sqliPayload)
queryParams.Add("c", lfiPayload)
queryParams.Add("d", rcePayload)
queryParams.Add("d", xxePayload)

req.URL.RawQuery = queryParams.Encode()

resp, err := w.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "failed to sent request")
}

return resp, nil
}

// DetectWAF performs WAF identification. Returns WAF name and vendor after
// the first positive match.
func (w *WAFDetector) DetectWAF(ctx context.Context) (name, vendor string, err error) {
resp, err := w.doRequest(ctx)
if err != nil {
return "", "", errors.Wrap(err, "couldn't identify WAF")
}

for _, d := range detectors.Detectors {
if d.IsWAF(resp) {
return d.WAFName, d.Vendor, nil
}
}

return "", "", nil
}
14 changes: 14 additions & 0 deletions internal/scanner/detectors/akamai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package detectors

func KonaSiteDefender() *Detector {
d := &Detector{
WAFName: "Kona SiteDefender",
Vendor: "Akamai",
}

d.Checks = []Check{
CheckHeader("Server", "AkamaiGHost"),
}

return d
}
71 changes: 71 additions & 0 deletions internal/scanner/detectors/checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package detectors

import (
"net/http"
"regexp"
)

// Check performs some check on the response with a fixed condition.
type Check func(resp *http.Response) bool

// CheckStatusCode compare response status code with given value.
func CheckStatusCode(status int) Check {
f := func(resp *http.Response) bool {
if resp.StatusCode == status {
return true
}

return false
}

return f
}

// CheckHeader match header value with regex.
func CheckHeader(header, regex string) Check {
re := regexp.MustCompile(regex)

f := func(resp *http.Response) bool {
values := resp.Header.Values(header)
if values == nil {
return false
}

for i := range values {
if re.MatchString(values[i]) {
return true
}
}

return false
}

return f
}

// CheckCookie match Set-Cookie header values with regex.
func CheckCookie(regex string) Check {
return CheckHeader("Set-Cookie", regex)
}

// CheckContent match body value with regex.
func CheckContent(regex string) Check {
re := regexp.MustCompile(regex)

f := func(resp *http.Response) bool {
var body []byte

_, err := resp.Body.Read(body)
if err != nil {
return false
}

if re.Match(body) {
return true
}

return false
}

return f
}
38 changes: 38 additions & 0 deletions internal/scanner/detectors/detectors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package detectors

import "net/http"

// Detector contains names of WAF solution and vendor, and checks to detect that
// solution by response.
type Detector struct {
WAFName string
Vendor string

Checks []Check
}

func (d *Detector) GetWAFName() string {
return d.WAFName
}

func (d *Detector) GetVendor() string {
return d.Vendor
}

func (d *Detector) IsWAF(resp *http.Response) bool {
for _, check := range d.Checks {
if check(resp) {
return true
}
}

return false
}

// Detectors is the list of all available WAF detectors. The checks are performed
// in the given order.
var Detectors = []*Detector{
KonaSiteDefender(),
Incapsula(),
SecureSphere(),
}
34 changes: 34 additions & 0 deletions internal/scanner/detectors/imperva.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package detectors

func SecureSphere() *Detector {
d := &Detector{
WAFName: "SecureSphere",
Vendor: "Imperva Inc.",
}

d.Checks = []Check{
CheckContent("<(title|h2)>Error"),
CheckContent("The incident ID is"),
CheckContent("This page can't be displayed"),
CheckContent("Contact support for additional information"),
}

return d
}

func Incapsula() *Detector {
d := &Detector{
WAFName: "Incapsula",
Vendor: "Imperva Inc.",
}

d.Checks = []Check{
CheckCookie("^incap_ses.*?="),
CheckCookie("^visid_incap.*?="),
CheckContent("incapsula incident id"),
CheckContent("powered by incapsula"),
CheckContent("/_Incapsula_Resource"),
}

return d
}
Loading

0 comments on commit b17276d

Please sign in to comment.