Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ jobs:
# scan: "jwt.not_verified"
- challenge: "jwt-null-signature"
scan: "jwt.null_signature"
# - challenge: "jwt-psychic-signature"
# scan: "jwt.psychic_signature"
- challenge: "jwt-weak-hmac-secret"
scan: "jwt.weak_secret"

Expand Down
23 changes: 23 additions & 0 deletions challenges/jwt-psychic-signature/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM golang:1.26 AS builder

WORKDIR /app

COPY common/ ./common/
COPY challenges/jwt-psychic-signature/ ./challenges/jwt-psychic-signature/

WORKDIR /app/challenges/jwt-psychic-signature
RUN CGO_ENABLED=0 GOWORK=off GOOS=linux go build -o /jwt-psychic-signature .

FROM gcr.io/distroless/static-debian11:nonroot AS runner

WORKDIR /

COPY --from=builder --chown=nonroot:nonroot /app/challenges/jwt-psychic-signature/keys /keys
COPY --from=builder --chown=nonroot:nonroot /jwt-psychic-signature /usr/bin/jwt-psychic-signature

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["jwt-psychic-signature"]
CMD ["serve"]
15 changes: 15 additions & 0 deletions challenges/jwt-psychic-signature/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/cerberauth/api-vulns-challenges/challenges/jwt-psychic-signature

go 1.26

require github.com/golang-jwt/jwt/v5 v5.3.1

require github.com/spf13/cobra v1.10.2 // indirect

require (
github.com/cerberauth/api-vulns-challenges/common v0.0.0-00010101000000-000000000000
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
)

replace github.com/cerberauth/api-vulns-challenges/common => ../../common
13 changes: 13 additions & 0 deletions challenges/jwt-psychic-signature/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Binary file not shown.
5 changes: 5 additions & 0 deletions challenges/jwt-psychic-signature/keys/private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIERJaTSxhW7C9N1fFHpqFhnJx9s77kcKPXVRzSEUxbZLoAoGCCqGSM49
AwEHoUQDQgAEY6ffpEN0/ohvRhhzP6l3Yc0B7NDgIiq6DJY+Qev3sXSFbWHnnhEd
RlqVMCS7kpSF+/M6J+/eBKSZTKuyrQqh3A==
-----END EC PRIVATE KEY-----
4 changes: 4 additions & 0 deletions challenges/jwt-psychic-signature/keys/public_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6ffpEN0/ohvRhhzP6l3Yc0B7NDg
Iiq6DJY+Qev3sXSFbWHnnhEdRlqVMCS7kpSF+/M6J+/eBKSZTKuyrQqh3A==
-----END PUBLIC KEY-----
39 changes: 39 additions & 0 deletions challenges/jwt-psychic-signature/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"os"
"path"
"time"

"github.com/cerberauth/api-vulns-challenges/challenges/jwt-psychic-signature/serve"
"github.com/cerberauth/api-vulns-challenges/common"
"github.com/golang-jwt/jwt/v5"
)

func generateToken() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}

privateKeyBytes, err := os.ReadFile(path.Join(cwd, "keys", "private_key.pem"))
if err != nil {
return "", err
}

key, err := jwt.ParseECPrivateKeyFromPEM(privateKeyBytes)
if err != nil {
return "", err
}

return jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"sub": "2cb307ba-bb46-4194-854f-4774046d9c9b",
"name": "John Doe",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
}).SignedString(key)
}

func main() {
common.Execute(serve.RunServer, common.NewJwtCmd(generateToken))
}
123 changes: 123 additions & 0 deletions challenges/jwt-psychic-signature/serve/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package serve

import (
"crypto/ecdsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"math/big"
"net/http"
"os"
"path"
"strings"
"time"

"github.com/cerberauth/api-vulns-challenges/common"
"github.com/golang-jwt/jwt/v5"
)

// vulnerableECDSAVerify mimics CVE-2022-21449: missing check that r,s ∈ [1,N-1].
// When s=0, the modular inverse is undefined; Java's implementation collapsed
// this to a zero point whose x-coordinate equaled r=0, making verification pass.
func vulnerableECDSAVerify(pub *ecdsa.PublicKey, hash []byte, r, s *big.Int) bool {
if s.Sign() == 0 {
return r.Sign() == 0
}
return ecdsa.Verify(pub, hash, r, s)
}

func readPublicKey() (*ecdsa.PublicKey, error) {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}

publicKeyBytes, err := os.ReadFile(path.Join(cwd, "keys", "public_key.pem"))
if err != nil {
return nil, err
}

key, err := jwt.ParseECPublicKeyFromPEM(publicKeyBytes)
if err != nil {
return nil, err
}
return key, nil
}

func RunServer(port string) {
publicKey, err := readPublicKey()
if err != nil {
log.Fatal(err)
}

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tokenString, ok := common.ExtractBearerToken(r)
if !ok {
w.WriteHeader(401)
return
}

parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
w.WriteHeader(401)
return
}

headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
w.WriteHeader(401)
return
}
var header map[string]interface{}
if err := json.Unmarshal(headerBytes, &header); err != nil {
w.WriteHeader(401)
return
}
if alg, _ := header["alg"].(string); alg != "ES256" {
w.WriteHeader(401)
return
}

sigBytes, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil || len(sigBytes) != 64 {
w.WriteHeader(401)
return
}

r2 := new(big.Int).SetBytes(sigBytes[:32])
s2 := new(big.Int).SetBytes(sigBytes[32:])

hash := sha256.Sum256([]byte(parts[0] + "." + parts[1]))

if !vulnerableECDSAVerify(publicKey, hash[:], r2, s2) {
w.WriteHeader(401)
return
}

payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
w.WriteHeader(401)
return
}
var claims map[string]interface{}
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
w.WriteHeader(401)
return
}

exp, ok := claims["exp"].(float64)
if !ok || time.Now().Unix() > int64(exp) {
fmt.Println("token expired")
w.WriteHeader(401)
return
}

w.WriteHeader(204)
})

log.Println("Server started at port", port)
log.Fatal(http.ListenAndServe(":"+port, common.SecurityHeadersMiddleware(mux)))
}
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use (
./challenges/jwt-kid-sql-injection
./challenges/jwt-not-verified
./challenges/jwt-null-signature
./challenges/jwt-psychic-signature
./challenges/jwt-strong-eddsa-key
./challenges/jwt-weak-hmac-secret
./challenges/jwt-weak-rsa-key
Expand Down
Loading