Skip to content

Commit 212e0f5

Browse files
Merge pull request #71 from cerberauth/jwt-hmac-weak-secret
feat: perform dictionnary attack against jwt with hmac alg
2 parents 2dd49ea + 9452f0a commit 212e0f5

File tree

6 files changed

+147
-9
lines changed

6 files changed

+147
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ In this example, each line represents a detected vulnerability, severity level (
8383
The scanner is capable of detecting the following vulnerabilities:
8484
* JWT `none` algorithm accepted
8585
* JWT not verified
86-
* JWT weak secret used
86+
* JWT blank or weak secret used with HMAC algorithm
8787
* JWT null signature accepted
8888

8989
The scanner also detects the following security best practices:

jwt/hmac_alg.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package jwt
2+
3+
import jwtlib "github.com/golang-jwt/jwt/v5"
4+
5+
func (j *JWTWriter) IsHMACAlg() bool {
6+
_, ok := j.Token.Method.(*jwtlib.SigningMethodHMAC)
7+
return ok
8+
}

jwt/hmac_alg_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package jwt_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/cerberauth/vulnapi/jwt"
7+
jwtlib "github.com/golang-jwt/jwt/v5"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestIsHMACAlgWithHS256(t *testing.T) {
12+
token := jwtlib.New(jwtlib.SigningMethodHS256)
13+
tokenString, _ := token.SignedString([]byte(""))
14+
jwtWriter, err := jwt.NewJWTWriter(tokenString)
15+
16+
assert.NoError(t, err)
17+
assert.True(t, jwtWriter.IsHMACAlg())
18+
}
19+
20+
func TestIsHMACAlgWithRSA(t *testing.T) {
21+
privateKeyData := []byte(`
22+
-----BEGIN PRIVATE KEY-----
23+
MC4CAQAwBQYDK2VwBCIEIKwkZA+Y19xPuGLCkk+JkrTnCo5bQvJcf0MdC73xg473
24+
-----END PRIVATE KEY-----
25+
`)
26+
27+
key, _ := jwtlib.ParseEdPrivateKeyFromPEM(privateKeyData)
28+
token := jwtlib.New(jwtlib.SigningMethodEdDSA)
29+
tokenString, _ := token.SignedString(key)
30+
jwtWriter, err := jwt.NewJWTWriter(tokenString)
31+
32+
assert.NoError(t, err)
33+
assert.False(t, jwtWriter.IsHMACAlg())
34+
}

scan/jwt/weak_secret.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/cerberauth/vulnapi/internal/scan"
77
"github.com/cerberauth/vulnapi/jwt"
88
"github.com/cerberauth/vulnapi/report"
9+
"github.com/cerberauth/vulnapi/seclist"
910
)
1011

1112
const (
@@ -14,6 +15,10 @@ const (
1415
WeakSecretVulnerabilityDescription = "JWT secret is weak and can be easily guessed."
1516
)
1617

18+
var defaultJwtSecretDictionary = []string{"secret", "password", "123456", "changeme", "admin", "token"}
19+
20+
const jwtSecretDictionarySeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/scraped-JWT-secrets.txt"
21+
1722
func BlankSecretScanHandler(operation *request.Operation, ss auth.SecurityScheme) (*report.ScanReport, error) {
1823
r := report.NewScanReport()
1924
if !ShouldBeScanned(ss) {
@@ -44,10 +49,49 @@ func BlankSecretScanHandler(operation *request.Operation, ss auth.SecurityScheme
4449
return r, nil
4550
}
4651

47-
func DictSecretScanHandler(o *request.Operation, ss auth.SecurityScheme) (*report.ScanReport, error) {
52+
func WeakHMACSecretScanHandler(o *request.Operation, ss auth.SecurityScheme) (*report.ScanReport, error) {
4853
r := report.NewScanReport()
54+
if !ShouldBeScanned(ss) {
55+
return r, nil
56+
}
4957

50-
// TODO: Use a dictionary attack to try finding the secret
58+
valueWriter := ss.GetValidValueWriter().(*jwt.JWTWriter)
59+
if !valueWriter.IsHMACAlg() {
60+
return r, nil
61+
}
62+
63+
jwtSecretDictionary := defaultJwtSecretDictionary
64+
if secretDictionnaryFromSeclist, err := seclist.NewSecListFromURL("JWT Secrets Dictionnary", jwtSecretDictionarySeclistUrl); err == nil {
65+
jwtSecretDictionary = secretDictionnaryFromSeclist.Items
66+
}
67+
68+
for _, secret := range jwtSecretDictionary {
69+
newToken, err := valueWriter.SignWithKey([]byte(secret))
70+
if err != nil {
71+
return r, nil
72+
}
73+
74+
if newToken != valueWriter.Token.Raw {
75+
continue
76+
}
77+
78+
ss.SetAttackValue(newToken)
79+
vsa, err := scan.ScanURL(o, &ss)
80+
r.AddScanAttempt(vsa)
81+
if err != nil {
82+
return r, err
83+
}
84+
85+
if err := scan.DetectNotExpectedResponse(vsa.Response); err != nil {
86+
r.AddVulnerabilityReport(&report.VulnerabilityReport{
87+
SeverityLevel: WeakSecretVulnerabilitySeverityLevel,
88+
Name: WeakSecretVulnerabilityName,
89+
Description: WeakSecretVulnerabilityDescription,
90+
Operation: o,
91+
})
92+
break
93+
}
94+
}
5195

5296
r.End()
5397

scan/jwt/weak_secret_test.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,9 @@ import (
1111
)
1212

1313
func TestBlankSecretScanHandlerWithoutJwt(t *testing.T) {
14-
httpmock.Activate()
15-
defer httpmock.DeactivateAndReset()
16-
1714
securityScheme := auth.NewNoAuthSecurityScheme()
1815
operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil)
1916

20-
httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(405, nil))
21-
2217
report, err := jwt.BlankSecretScanHandler(operation, securityScheme)
2318

2419
assert.NoError(t, err)
@@ -42,3 +37,60 @@ func TestBlankSecretScanHandler(t *testing.T) {
4237
assert.Equal(t, 1, httpmock.GetTotalCallCount())
4338
assert.False(t, report.HasVulnerabilityReport())
4439
}
40+
41+
func TestWeakHMACSecretScanHandlerWithoutJwt(t *testing.T) {
42+
securityScheme := auth.NewNoAuthSecurityScheme()
43+
operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil)
44+
45+
report, err := jwt.WeakHMACSecretScanHandler(operation, securityScheme)
46+
47+
assert.NoError(t, err)
48+
assert.Equal(t, 0, httpmock.GetTotalCallCount())
49+
assert.False(t, report.HasVulnerabilityReport())
50+
}
51+
52+
func TestWeakHMACSecretScanHandlerWithJWTUsingOtherAlg(t *testing.T) {
53+
token := "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMxMjMifQ.vLBmArLmAKEshqJa3px6qYfrkAfiwBrKPs5dCMxqj9bdiEKR5W4o0Srxt6VHZKzsxIGMTTsqpW21lKnYsLw5DA"
54+
securityScheme := auth.NewAuthorizationBearerSecurityScheme("token", &token)
55+
operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil)
56+
57+
report, err := jwt.WeakHMACSecretScanHandler(operation, securityScheme)
58+
59+
assert.NoError(t, err)
60+
assert.Equal(t, 0, httpmock.GetTotalCallCount())
61+
assert.False(t, report.HasVulnerabilityReport())
62+
}
63+
64+
func TestWeakHMACSecretScanHandlerWithWeakJWT(t *testing.T) {
65+
httpmock.Activate()
66+
defer httpmock.DeactivateAndReset()
67+
68+
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M"
69+
securityScheme := auth.NewAuthorizationBearerSecurityScheme("token", &token)
70+
operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil)
71+
72+
httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(104, nil))
73+
74+
report, err := jwt.WeakHMACSecretScanHandler(operation, securityScheme)
75+
76+
assert.NoError(t, err)
77+
assert.Equal(t, 1, httpmock.GetTotalCallCount())
78+
assert.True(t, report.HasVulnerabilityReport())
79+
}
80+
81+
func TestWeakHMACSecretScanHandlerWithStrongerJWT(t *testing.T) {
82+
httpmock.Activate()
83+
defer httpmock.DeactivateAndReset()
84+
85+
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MWUarT7Q4e5DqnZbdr7VKw3rx9VW-CrvoVkfpllS4CY"
86+
securityScheme := auth.NewAuthorizationBearerSecurityScheme("token", &token)
87+
operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil)
88+
89+
httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(104, nil))
90+
91+
report, err := jwt.WeakHMACSecretScanHandler(operation, securityScheme)
92+
93+
assert.NoError(t, err)
94+
assert.Equal(t, 0, httpmock.GetTotalCallCount())
95+
assert.False(t, report.HasVulnerabilityReport())
96+
}

scan/vulns.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func (s *Scan) WithJWTNullSignatureScan() *Scan {
1515
}
1616

1717
func (s *Scan) WithWeakJwtSecretScan() *Scan {
18-
return s.AddOperationScanHandler(jwt.BlankSecretScanHandler).AddOperationScanHandler(jwt.DictSecretScanHandler)
18+
return s.AddOperationScanHandler(jwt.BlankSecretScanHandler).AddOperationScanHandler(jwt.WeakHMACSecretScanHandler)
1919
}
2020

2121
func (s *Scan) WithAllVulnsScans() *Scan {

0 commit comments

Comments
 (0)