diff --git a/tools/GetLAPSPassword/main.go b/tools/GetLAPSPassword/main.go index a19d145..e838280 100644 --- a/tools/GetLAPSPassword/main.go +++ b/tools/GetLAPSPassword/main.go @@ -284,22 +284,16 @@ func decryptLAPSv2Password(encryptedBlob []byte, target session.Target, creds *s } } - // Parse the decrypted JSON - // Format: {"n":"Administrator","t":"2024-01-01T00:00:00","p":"password"} - // The plaintext may have trailing data (18 bytes of metadata) - jsonData := plaintext - if len(jsonData) > 18 { - // Try to find the end of JSON object - for i := len(jsonData) - 1; i >= 0; i-- { - if jsonData[i] == '}' { - jsonData = jsonData[:i+1] - break - } - } - } - - // Convert from UTF-16LE to string - jsonStr := utf16ToString(jsonData) + // Decode the UTF-16LE plaintext. utf16ToString stops at the first NUL, so + // any NUL-terminated trailing metadata is dropped; parseLAPSv2JSON tolerates + // any remaining trailing bytes after the JSON object. + // + // Note: the previous approach scanned the raw UTF-16LE bytes backwards for a + // '}' (0x7D) and truncated there. Because '}' encodes as the two bytes + // 7D 00, that truncation kept the low byte but dropped the high 00, leaving + // an orphaned byte that utf16ToString discarded - silently deleting the + // closing brace and breaking every parse. + jsonStr := utf16ToString(plaintext) username, password, ok := parseLAPSv2JSON(jsonStr) if !ok { @@ -342,7 +336,10 @@ func parseLAPSv2JSON(jsonStr string) (username, password string, ok bool) { T string `json:"t"` // timestamp P string `json:"p"` // password } - if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + // Use a streaming decoder rather than json.Unmarshal: Windows LAPS may append + // trailing metadata after the JSON object. Unmarshal rejects trailing data, + // while Decode reads exactly one value and ignores whatever follows. + if err := json.NewDecoder(strings.NewReader(jsonStr)).Decode(&data); err != nil { return "", "", false } return data.N, data.P, true diff --git a/tools/GetLAPSPassword/main_test.go b/tools/GetLAPSPassword/main_test.go new file mode 100644 index 0000000..61adbf0 --- /dev/null +++ b/tools/GetLAPSPassword/main_test.go @@ -0,0 +1,73 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/binary" + "testing" + "unicode/utf16" +) + +// utf16le encodes s as UTF-16LE bytes, matching how Windows LAPS stores the +// decrypted password blob. +func utf16le(s string) []byte { + u := utf16.Encode([]rune(s)) + b := make([]byte, len(u)*2) + for i, r := range u { + binary.LittleEndian.PutUint16(b[i*2:], r) + } + return b +} + +// TestDecryptedBlobParsing guards the UTF-16LE decode + JSON parse path. The +// '}' that closes the object encodes as the two bytes 7D 00; a prior version +// truncated on the 7D and dropped the trailing 00, silently deleting the brace +// and failing every parse. Windows LAPS also appends trailing metadata after +// the object, which must not break the parse. +func TestDecryptedBlobParsing(t *testing.T) { + const wantUser = "Administrator" + const wantPass = "++Jf%0uQs4(amVm@c5K@" + obj := `{"n":"Administrator","t":"1dd1597e9faef4e","p":"++Jf%0uQs4(amVm@c5K@"}` + + cases := []struct { + name string + blob []byte + }{ + {"bare object", utf16le(obj)}, + {"nul terminated", append(utf16le(obj), 0x00, 0x00)}, + {"nul then metadata", append(append(utf16le(obj), 0x00, 0x00), 0xde, 0xad, 0xbe, 0xef)}, + {"metadata without nul", append(utf16le(obj), utf16le("trailing-metadata")...)}, + {"password containing brace", utf16le(`{"n":"Administrator","t":"1dd","p":"pa}ss"}`)}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := utf16ToString(tc.blob) + user, pass, ok := parseLAPSv2JSON(s) + if !ok { + t.Fatalf("parse failed; decoded=%q", s) + } + if tc.name == "password containing brace" { + if user != "Administrator" || pass != "pa}ss" { + t.Fatalf("got (%q,%q), want (Administrator, pa}ss)", user, pass) + } + return + } + if user != wantUser || pass != wantPass { + t.Fatalf("got (%q,%q), want (%q,%q)", user, pass, wantUser, wantPass) + } + }) + } +}