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
31 changes: 14 additions & 17 deletions tools/GetLAPSPassword/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions tools/GetLAPSPassword/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}