├── LICENSE ├── README.md └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Sagen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-jwt-cracker 2 | Concurrent HS256 JWT token brute force cracker, inspired by https://github.com/lmammino/jwt-cracker 3 | 4 | This is realistically only effective to crack JWT with weak secrets. It also only currently works with HMAC-SHA256 signatures. 5 | 6 | It should be slightly faster than it's inspiration, as it uses a new goroutine for each generated and compared hash. Could be made faster if it was generating secrets in more than one goroutine. 7 | 8 | Feel free to create a pull request with an improvement or fix :smile: 9 | 10 | ## Usage 11 | ``` 12 | Usage of go-jwt-cracker: 13 | -alphabet string 14 | The alphabet to use for the brute force (default "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 15 | -maxlen int 16 | The max length of the string generated during the brute force (default 12) 17 | -prefix string 18 | A string that is always prefixed to the secret 19 | -suffix string 20 | A string that is always suffixed to the secret 21 | -token string 22 | The full HS256 jwt token to crack 23 | ``` 24 | 25 | ## Example 26 | Cracking a token generated with [jwt.io](https://jwt.io): 27 | 28 | ```bash 29 | go-jwt-cracker -token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" -alphabet "abcdefghijklmnopqrstuwxyz" -maxlen 6 30 | ``` 31 | 32 | ### Output 33 | 34 | ``` 35 | Parsed JWT: 36 | - Algorithm: HS256 37 | - Type: JWT 38 | - Payload: {"sub":"1234567890","name":"John Doe","iat":1516239022} 39 | - Signature (hex): 5db3df6c81cc23a6ab67763ddb60618d6810cd65dc5cdaf3d2882d5617c4776a 40 | 41 | There are 254313150 combinations to attempt 42 | Cracking JWT secret... 43 | Attempts: 100000 44 | Attempts: 200000 45 | Attempts: 300000 46 | ... 47 | Attempts: 184500000 48 | Attempts: 184600000 49 | Attempts: 184700000 50 | Found secret in 184776821 attempts: secret 51 | ``` 52 | 53 | ### Time spent 54 | - Intel Core i7-4790k @ 4.38GHz - around 4.5 minutes 55 | - Intel Xeon E3-1270 V2 @ 3.50GHz - around 15 minutes -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "flag" 12 | "fmt" 13 | "math/big" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | type jwtHeader struct { 19 | Algorithm string `json:"alg"` 20 | Type string `json:"typ"` 21 | } 22 | 23 | type jwt struct { 24 | header *jwtHeader 25 | payload string 26 | message, signature []byte 27 | } 28 | 29 | func parseJWT(input string) (*jwt, error) { 30 | parts := strings.Split(input, ".") 31 | decodedParts := make([][]byte, len(parts)) 32 | if len(parts) != 3 { 33 | return nil, errors.New("invalid jwt: does not contain 3 parts (header, payload, signature)") 34 | } 35 | for i := range parts { 36 | decodedParts[i] = make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[i]))) 37 | if _, err := base64.RawURLEncoding.Decode(decodedParts[i], []byte(parts[i])); err != nil { 38 | return nil, err 39 | } 40 | } 41 | var parsedHeader jwtHeader 42 | if err := json.Unmarshal(decodedParts[0], &parsedHeader); err != nil { 43 | return nil, err 44 | } 45 | return &jwt{ 46 | header: &parsedHeader, 47 | payload: string(decodedParts[1]), 48 | message: []byte(parts[0] + "." + parts[1]), 49 | signature: decodedParts[2], 50 | }, nil 51 | } 52 | 53 | func generateSignature(message, secret []byte) []byte { 54 | hasher := hmac.New(sha256.New, secret) 55 | hasher.Write(message) 56 | return hasher.Sum(nil) 57 | } 58 | 59 | func generateSecrets(alphabet string, n int, wg *sync.WaitGroup, done chan struct{}) <-chan string { 60 | if n <= 0 { 61 | return nil 62 | } 63 | 64 | c := make(chan string) 65 | 66 | wg.Add(1) 67 | go func() { 68 | defer close(c) 69 | var helper func(string) 70 | helper = func(input string) { 71 | if len(input) == n { 72 | return 73 | } 74 | select { 75 | case <-done: 76 | return 77 | default: 78 | } 79 | for _, char := range alphabet { 80 | s := input + string(char) 81 | c <- s 82 | helper(s) 83 | } 84 | } 85 | helper("") 86 | wg.Done() 87 | }() 88 | 89 | return c 90 | } 91 | 92 | func main() { 93 | token := flag.String("token", "", "The full HS256 jwt token to crack") 94 | alphabet := flag.String("alphabet", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", "The alphabet to use for the brute force") 95 | prefix := flag.String("prefix", "", "A string that is always prefixed to the secret") 96 | suffix := flag.String("suffix", "", "A string that is always suffixed to the secret") 97 | maxLength := flag.Int("maxlen", 12, "The max length of the string generated during the brute force") 98 | flag.Parse() 99 | 100 | if *token == "" { 101 | fmt.Println("Parameter token is empty\n") 102 | flag.Usage() 103 | return 104 | } 105 | if *alphabet == "" { 106 | fmt.Println("Parameter alphabet is empty\n") 107 | flag.Usage() 108 | return 109 | } 110 | if *maxLength == 0 { 111 | fmt.Println("Parameter maxlen is 0\n") 112 | flag.Usage() 113 | return 114 | } 115 | 116 | parsed, err := parseJWT(*token) 117 | if err != nil { 118 | fmt.Printf("Could not parse JWT: %v\n", err) 119 | return 120 | } 121 | 122 | fmt.Printf("Parsed JWT:\n- Algorithm: %s\n- Type: %s\n- Payload: %s\n- Signature (hex): %s\n\n", 123 | parsed.header.Algorithm, 124 | parsed.header.Type, 125 | parsed.payload, 126 | hex.EncodeToString(parsed.signature)) 127 | 128 | if strings.ToUpper(parsed.header.Algorithm) != "HS256" { 129 | fmt.Println("Unsupported algorithm") 130 | return 131 | } 132 | 133 | combinations := big.NewInt(0) 134 | for i := 1; i <= *maxLength; i++ { 135 | alen, mlen := big.NewInt(int64(len(*alphabet))), big.NewInt(int64(i)) 136 | combinations.Add(combinations, alen.Exp(alen, mlen, nil)) 137 | } 138 | fmt.Printf("There are %s combinations to attempt\nCracking JWT secret...\n", combinations.String()) 139 | 140 | done := make(chan struct{}) 141 | wg := &sync.WaitGroup{} 142 | var found bool 143 | var attempts uint64 144 | for secret := range generateSecrets(*alphabet, *maxLength, wg, done) { 145 | wg.Add(1) 146 | go func(s string, i uint64) { 147 | select { 148 | case <-done: 149 | wg.Done() 150 | return 151 | default: 152 | } 153 | if bytes.Equal(parsed.signature, generateSignature(parsed.message, []byte(*prefix+s+*suffix))) { 154 | fmt.Printf("Found secret in %d attempts: %s\n", attempts, *prefix+s+*suffix) 155 | found = true 156 | close(done) 157 | } 158 | wg.Done() 159 | }(secret, attempts) 160 | 161 | attempts++ 162 | if attempts%100000 == 0 { 163 | fmt.Printf("Attempts: %d\n", attempts) 164 | } 165 | } 166 | wg.Wait() 167 | if !found { 168 | fmt.Printf("No secret found in %d attempts\n", attempts) 169 | } 170 | } 171 | --------------------------------------------------------------------------------