Fleet: Device lock PIN can be predicted if lock time is known
Description
Fleet is open source device management software. In versions prior to 4.80.1, Fleet generated device lock and wipe PINs using a predictable algorithm based solely on the current Unix timestamp. Because no secret key or additional entropy was used, the resulting PIN could potentially be derived if the approximate time the device was locked is known. Fleet’s device lock and wipe commands generate a 6-digit PIN that is displayed to administrators for unlocking a device. In affected versions, this PIN was deterministically derived from the current timestamp. An attacker with physical possession of a locked device and knowledge of the approximate time the lock command was issued could theoretically predict the correct PIN within a limited search window. However, successful exploitation is constrained by multiple factors: Physical access to the device is required, the approximate lock time must be known, the operating system enforces rate limiting on PIN entry attempts, attempts would need to be spread over, and device wipe operations would typically complete before sufficient attempts could be made. As a result, this issue does not allow remote exploitation, fleet-wide compromise, or bypass of Fleet authentication controls. Version 4.80.1 contains a patch. No known workarounds are available.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/fleetdm/fleet/v4Go | < 4.80.1 | 4.80.1 |
Affected products
1Patches
105ca0693621eAdd missing unit test (#38439)
3 files changed · +26 −12
server/mdm/apple/commander.go+8 −2 modified@@ -116,7 +116,10 @@ func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, } // No pending lock, create a new one - unlockPIN = GenerateRandomPin(6) + unlockPIN, err = GenerateRandomPin(6) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "generating random PIN for DeviceLock") + } raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> @@ -236,7 +239,10 @@ func (svc *MDMAppleCommander) DisableLostMode(ctx context.Context, host *fleet.H } func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host, uuid string) error { - pin := GenerateRandomPin(6) + pin, err := GenerateRandomPin(6) + if err != nil { + return ctxerr.Wrap(ctx, err, "generating random PIN for EraseDevice") + } raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0">
server/mdm/apple/util.go+10 −10 modified@@ -5,14 +5,12 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/x509" - "encoding/binary" "encoding/pem" "fmt" "math" "net/url" "path" "strings" - "time" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" @@ -36,14 +34,16 @@ func EncodeCertRequestPEM(cert *x509.CertificateRequest) []byte { return pem.EncodeToMemory(pemBlock) } -// GenerateRandomPin generates a `lenght`-digit PIN number that takes into -// account the current time as described in rfc4226 (for one time passwords) +// GenerateRandomPin generates a `length`-digit random PIN number // -// The implementation details have been mostly taken from https://github.com/pquerna/otp -func GenerateRandomPin(length int) string { - counter := uint64(time.Now().Unix()) //nolint:gosec // dismiss G115 - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, counter) +// The implementation details for converting the randomness to a PIN +// have been mostly taken from https://github.com/pquerna/otp +func GenerateRandomPin(length int) (string, error) { + buf := make([]byte, 16) + _, err := rand.Read(buf) + if err != nil { + return "", err + } m := sha256.New() m.Write(buf) sum := m.Sum(nil) @@ -54,7 +54,7 @@ func GenerateRandomPin(length int) string { (int(sum[offset+3]) & 0xff)) v := int32(value % int64(math.Pow10(length))) //nolint:gosec // dismiss G115 f := fmt.Sprintf("%%0%dd", length) - return fmt.Sprintf(f, v) + return fmt.Sprintf(f, v), nil } // FmtErrorChain formats Command error message for macOS MDM v1
server/mdm/apple/util_test.go+8 −0 modified@@ -36,3 +36,11 @@ func TestMDMAppleEnrollURL(t *testing.T) { require.Equal(t, tt.expectedURL, enrollURL) } } + +func TestGenerateRandomPin(t *testing.T) { + for i := 1; i <= 100; i++ { + pin, err := GenerateRandomPin(i) + require.NoError(t, err) + require.Len(t, pin, i) + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-ppwx-5jq7-px2wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-23999ghsaADVISORY
- github.com/fleetdm/fleet/commit/05ca0693621e6671fb95dfc5437b9f9ee6dd7047ghsaWEB
- github.com/fleetdm/fleet/security/advisories/GHSA-ppwx-5jq7-px2wghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.