Skip to content

Commit 4bf549f

Browse files
authored
Improve WAF detection (#241)
1 parent 561e801 commit 4bf549f

File tree

11 files changed

+435
-122
lines changed

11 files changed

+435
-122
lines changed

cmd/main.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func run(ctx context.Context, logger *logrus.Logger) error {
115115

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

118-
name, vendor, err := detector.DetectWAF(ctx)
118+
name, vendor, checkFunc, err := detector.DetectWAF(ctx)
119119
if err != nil {
120120
return errors.Wrap(err, "couldn't detect")
121121
}
@@ -126,6 +126,7 @@ func run(ctx context.Context, logger *logrus.Logger) error {
126126
"vendor": vendor,
127127
}).Info("WAF was identified. Force enabling `--followCookies' and `--renewSession' options")
128128

129+
cfg.CheckBlockFunc = checkFunc
129130
cfg.FollowCookies = true
130131
cfg.RenewSession = true
131132
cfg.WAFName = fmt.Sprintf("%s (%s)", name, vendor)

internal/config/config.go

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package config
22

3+
import "github.com/wallarm/gotestwaf/internal/scanner/detectors"
4+
35
type Config struct {
46
URL string `mapstructure:"url"`
57
WebSocketURL string `mapstructure:"wsURL"`
@@ -37,4 +39,6 @@ type Config struct {
3739
AddHeader string `mapstructure:"addHeader"`
3840
AddDebugHeader bool `mapstructure:"addDebugHeader"`
3941
OpenAPIFile string `mapstructure:"openapiFile"`
42+
43+
CheckBlockFunc detectors.Check
4044
}

internal/scanner/detector.go

+99-32
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,28 @@ const (
2525
)
2626

2727
type WAFDetector struct {
28-
client *http.Client
29-
headers map[string]string
30-
hostHeader string
31-
target string
28+
clientSettings *ClientSettings
29+
headers map[string]string
30+
hostHeader string
31+
target string
32+
}
33+
34+
type ClientSettings struct {
35+
dnsResolver *dnscache.Resolver
36+
insecureSkipVerify bool
37+
idleConnTimeout time.Duration
38+
maxIdleConns int
39+
maxIdleConnsPerHost int
40+
proxyURL *url.URL
3241
}
3342

3443
func NewDetector(cfg *config.Config, dnsResolver *dnscache.Resolver) (*WAFDetector, error) {
35-
tr := &http.Transport{
36-
DialContext: dnscache.DialFunc(dnsResolver, nil),
37-
TLSClientConfig: &tls.Config{InsecureSkipVerify: !cfg.TLSVerify},
38-
IdleConnTimeout: time.Duration(cfg.IdleConnTimeout) * time.Second,
39-
MaxIdleConns: cfg.MaxIdleConns,
40-
MaxIdleConnsPerHost: cfg.MaxIdleConns, // net.http hardcodes DefaultMaxIdleConnsPerHost to 2!
44+
clientSettings := &ClientSettings{
45+
dnsResolver: dnsResolver,
46+
insecureSkipVerify: !cfg.TLSVerify,
47+
idleConnTimeout: time.Duration(cfg.IdleConnTimeout) * time.Second,
48+
maxIdleConns: cfg.MaxIdleConns,
49+
maxIdleConnsPerHost: cfg.MaxIdleConns,
4150
}
4251

4352
if cfg.Proxy != "" {
@@ -46,17 +55,7 @@ func NewDetector(cfg *config.Config, dnsResolver *dnscache.Resolver) (*WAFDetect
4655
return nil, errors.Wrap(err, "couldn't parse proxy URL")
4756
}
4857

49-
tr.Proxy = http.ProxyURL(proxyURL)
50-
}
51-
52-
jar, err := cookiejar.New(nil)
53-
if err != nil {
54-
return nil, errors.Wrap(err, "couldn't create cookie jar")
55-
}
56-
57-
client := &http.Client{
58-
Transport: tr,
59-
Jar: jar,
58+
clientSettings.proxyURL = proxyURL
6059
}
6160

6261
target, err := url.Parse(cfg.URL)
@@ -73,20 +72,71 @@ func NewDetector(cfg *config.Config, dnsResolver *dnscache.Resolver) (*WAFDetect
7372
}
7473

7574
return &WAFDetector{
76-
client: client,
77-
headers: configuredHeaders,
78-
hostHeader: configuredHeaders["Host"],
79-
target: GetTargetURLStr(target),
75+
clientSettings: clientSettings,
76+
headers: configuredHeaders,
77+
hostHeader: configuredHeaders["Host"],
78+
target: GetTargetURLStr(target),
8079
}, nil
8180
}
8281

83-
// doRequest sends HTTP-request with malicious payload to trigger WAF.
82+
func (w *WAFDetector) getHttpClient() (*http.Client, error) {
83+
tr := &http.Transport{
84+
DialContext: dnscache.DialFunc(w.clientSettings.dnsResolver, nil),
85+
TLSClientConfig: &tls.Config{InsecureSkipVerify: w.clientSettings.insecureSkipVerify},
86+
IdleConnTimeout: w.clientSettings.idleConnTimeout,
87+
MaxIdleConns: w.clientSettings.maxIdleConns,
88+
MaxIdleConnsPerHost: w.clientSettings.maxIdleConns, // net.http hardcodes DefaultMaxIdleConnsPerHost to 2!
89+
}
90+
91+
if w.clientSettings.proxyURL != nil {
92+
tr.Proxy = http.ProxyURL(w.clientSettings.proxyURL)
93+
}
94+
95+
jar, err := cookiejar.New(nil)
96+
if err != nil {
97+
return nil, errors.Wrap(err, "couldn't create cookie jar")
98+
}
99+
100+
client := &http.Client{
101+
Transport: tr,
102+
Jar: jar,
103+
}
104+
105+
return client, nil
106+
}
107+
108+
// doRequest sends HTTP-request without malicious payload to trigger WAF.
84109
func (w *WAFDetector) doRequest(ctx context.Context) (*http.Response, error) {
85110
req, err := http.NewRequestWithContext(ctx, http.MethodGet, w.target, nil)
86111
if err != nil {
87112
return nil, errors.Wrap(err, "couldn't create request")
88113
}
89114

115+
for header, value := range w.headers {
116+
req.Header.Set(header, value)
117+
}
118+
req.Host = w.hostHeader
119+
120+
client, err := w.getHttpClient()
121+
if err != nil {
122+
return nil, errors.Wrap(err, "couldn't create HTTP client")
123+
}
124+
125+
resp, err := client.Do(req)
126+
if err != nil {
127+
return nil, errors.Wrap(err, "failed to sent request")
128+
}
129+
130+
return resp, nil
131+
}
132+
133+
// doMaliciousRequest sends HTTP-request with malicious payload to trigger WAF.
134+
func (w *WAFDetector) doMaliciousRequest(ctx context.Context) (*http.Response, error) {
135+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, w.target, nil)
136+
if err != nil {
137+
return nil, errors.Wrap(err, "couldn't create request")
138+
}
139+
90140
queryParams := req.URL.Query()
91141
queryParams.Add("a", xssPayload)
92142
queryParams.Add("b", sqliPayload)
@@ -101,7 +151,12 @@ func (w *WAFDetector) doRequest(ctx context.Context) (*http.Response, error) {
101151
}
102152
req.Host = w.hostHeader
103153

104-
resp, err := w.client.Do(req)
154+
client, err := w.getHttpClient()
155+
if err != nil {
156+
return nil, errors.Wrap(err, "couldn't create HTTP client")
157+
}
158+
159+
resp, err := client.Do(req)
105160
if err != nil {
106161
return nil, errors.Wrap(err, "failed to sent request")
107162
}
@@ -111,19 +166,31 @@ func (w *WAFDetector) doRequest(ctx context.Context) (*http.Response, error) {
111166

112167
// DetectWAF performs WAF identification. Returns WAF name and vendor after
113168
// the first positive match.
114-
func (w *WAFDetector) DetectWAF(ctx context.Context) (name, vendor string, err error) {
169+
func (w *WAFDetector) DetectWAF(ctx context.Context) (name, vendor string, checkFunc detectors.Check, err error) {
115170
resp, err := w.doRequest(ctx)
116171
if err != nil {
117-
return "", "", errors.Wrap(err, "couldn't identify WAF")
172+
return "", "", nil, errors.Wrap(err, "couldn't perform request without attack")
118173
}
119174

120175
defer resp.Body.Close()
121176

177+
respToAttack, err := w.doMaliciousRequest(ctx)
178+
if err != nil {
179+
return "", "", nil, errors.Wrap(err, "couldn't perform request with attack")
180+
}
181+
182+
defer respToAttack.Body.Close()
183+
184+
resps := &detectors.Responses{
185+
Resp: resp,
186+
RespToAttack: respToAttack,
187+
}
188+
122189
for _, d := range detectors.Detectors {
123-
if d.IsWAF(resp) {
124-
return d.WAFName, d.Vendor, nil
190+
if d.IsWAF(resps) {
191+
return d.WAFName, d.Vendor, d.Check, nil
125192
}
126193
}
127194

128-
return "", "", nil
195+
return "", "", nil, nil
129196
}

internal/scanner/detectors/akamai.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ func KonaSiteDefender() *Detector {
66
Vendor: "Akamai",
77
}
88

9-
d.Checks = []Check{
10-
CheckHeader("Server", "AkamaiGHost"),
11-
}
9+
d.Check = Or(
10+
CheckHeader("Server", "AkamaiGHost", false),
11+
CheckHeader("Server", "AkamaiGHost", true),
12+
)
1213

1314
return d
1415
}

internal/scanner/detectors/checks.go

+106-9
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,30 @@ import (
55
"io"
66
"net/http"
77
"regexp"
8+
"strings"
89
)
910

11+
type Responses struct {
12+
Resp *http.Response
13+
RespToAttack *http.Response
14+
}
15+
1016
// Check performs some check on the response with a fixed condition.
11-
type Check func(resp *http.Response) bool
17+
type Check func(resps *Responses) bool
1218

1319
// CheckStatusCode compare response status code with given value.
14-
func CheckStatusCode(status int) Check {
15-
f := func(resp *http.Response) bool {
20+
// Default value for attack parameter is true.
21+
func CheckStatusCode(status int, attack bool) Check {
22+
f := func(resps *Responses) bool {
23+
resp := resps.Resp
24+
if attack {
25+
resp = resps.RespToAttack
26+
}
27+
28+
if resp == nil {
29+
return false
30+
}
31+
1632
if resp.StatusCode == status {
1733
return true
1834
}
@@ -23,11 +39,49 @@ func CheckStatusCode(status int) Check {
2339
return f
2440
}
2541

42+
// CheckReason match status reason value with regex.
43+
// Default value for attack parameter is true.
44+
func CheckReason(regex string, attack bool) Check {
45+
re := regexp.MustCompile(regex)
46+
47+
f := func(resps *Responses) bool {
48+
resp := resps.Resp
49+
if attack {
50+
resp = resps.RespToAttack
51+
}
52+
53+
if resp == nil {
54+
return false
55+
}
56+
57+
reasonIndex := strings.Index(resp.Status, " ")
58+
reason := resp.Status[reasonIndex+1:]
59+
60+
if re.MatchString(reason) {
61+
return true
62+
}
63+
64+
return false
65+
}
66+
67+
return f
68+
}
69+
2670
// CheckHeader match header value with regex.
27-
func CheckHeader(header, regex string) Check {
71+
// Default value for attack parameter is false.
72+
func CheckHeader(header, regex string, attack bool) Check {
2873
re := regexp.MustCompile(regex)
2974

30-
f := func(resp *http.Response) bool {
75+
f := func(resps *Responses) bool {
76+
resp := resps.Resp
77+
if attack {
78+
resp = resps.RespToAttack
79+
}
80+
81+
if resp == nil {
82+
return false
83+
}
84+
3185
values := resp.Header.Values(header)
3286
if values == nil {
3387
return false
@@ -46,15 +100,26 @@ func CheckHeader(header, regex string) Check {
46100
}
47101

48102
// CheckCookie match Set-Cookie header values with regex.
49-
func CheckCookie(regex string) Check {
50-
return CheckHeader("Set-Cookie", regex)
103+
// Default value for attack parameter is false.
104+
func CheckCookie(regex string, attack bool) Check {
105+
return CheckHeader("Set-Cookie", regex, attack)
51106
}
52107

53108
// CheckContent match body value with regex.
54-
func CheckContent(regex string) Check {
109+
// Default value for attack parameter is true.
110+
func CheckContent(regex string, attack bool) Check {
55111
re := regexp.MustCompile(regex)
56112

57-
f := func(resp *http.Response) bool {
113+
f := func(resps *Responses) bool {
114+
resp := resps.Resp
115+
if attack {
116+
resp = resps.RespToAttack
117+
}
118+
119+
if resp == nil {
120+
return false
121+
}
122+
58123
body, err := io.ReadAll(resp.Body)
59124
if err != nil {
60125
return false
@@ -73,3 +138,35 @@ func CheckContent(regex string) Check {
73138

74139
return f
75140
}
141+
142+
// And combines the checks with AND logic,
143+
// so each test must be true to return true.
144+
func And(checks ...Check) Check {
145+
f := func(resps *Responses) bool {
146+
for _, check := range checks {
147+
if !check(resps) {
148+
return false
149+
}
150+
}
151+
152+
return true
153+
}
154+
155+
return f
156+
}
157+
158+
// Or combines the checks with OR logic,
159+
// so at least one test must be true to return true.
160+
func Or(checks ...Check) Check {
161+
f := func(resps *Responses) bool {
162+
for _, check := range checks {
163+
if check(resps) {
164+
return true
165+
}
166+
}
167+
168+
return false
169+
}
170+
171+
return f
172+
}

0 commit comments

Comments
 (0)