Skip to content

Commit 4376f7d

Browse files
Adding NTLM Type 2 Message Decoding (#545)
1 parent 12e83a4 commit 4376f7d

3 files changed

Lines changed: 294 additions & 2 deletions

File tree

framework.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ import (
7272
"time"
7373

7474
"github.com/Masterminds/semver"
75-
7675
"github.com/vulncheck-oss/go-exploit/c2"
7776
"github.com/vulncheck-oss/go-exploit/c2/channel"
7877
"github.com/vulncheck-oss/go-exploit/cli"

transform/encode.go

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,176 @@ import (
1212
"unicode/utf16"
1313

1414
"golang.org/x/text/cases"
15+
"golang.org/x/text/encoding/unicode"
1516
"golang.org/x/text/language"
1617

1718
"github.com/vulncheck-oss/go-exploit/output"
1819
)
1920

21+
// Non-public function, used to parse individual target info types from NTLMType2 messages.
22+
func decodeNTLM2TargetInfo(targetInfoBytes []byte) ([]TargetInfo, bool) {
23+
/* This helper only supports the standard string-based AV_PAIR types defined for NTLM TargetInfo.
24+
Known and expected types:
25+
Type 0: Terminator subblock (0x0000)
26+
Type 1: Server name (UTF-16LE string, e.g., "server")
27+
Type 2: Domain name (UTF-16LE string, e.g., "domain.com")
28+
Type 3: DNS Server Name subblock (UTF-16LE string, e.g., "server.domain.com")
29+
Type 4: DNS Domain Name (UTF-16LE string, e.g., "domain.com")
30+
All messages processed by this function are expected to end with a type 0 terminator block. Other AV_PAIR
31+
types (e.g., timestamps, flags) are not handled here and may require different parsing logic. */
32+
cursor := 0
33+
34+
retArr := []TargetInfo{}
35+
totalLen := len(targetInfoBytes)
36+
if totalLen < 4 {
37+
output.PrintFrameworkError("Failed to decode target info: TargetInfo bytes buffer is too short")
38+
39+
return []TargetInfo{}, false
40+
}
41+
42+
// Making sure this ends with a terminator block (0x00000000)
43+
if !bytes.Equal(targetInfoBytes[totalLen-4:], []byte{0x00, 0x00, 0x00, 0x00}) {
44+
output.PrintFrameworkError("Decoding NTLM Type 2 Target Info failed, invalid targetinfo buffer provided, the provided buffer does not end with a terminator block")
45+
46+
return []TargetInfo{}, false
47+
}
48+
49+
// A terminator-only TargetInfo (4 bytes) is a valid empty AV-pair list in NTLM.
50+
if totalLen == 4 {
51+
return retArr, true
52+
}
53+
54+
// Actual targetinfo parsing
55+
for cursor < totalLen-4 {
56+
if cursor+4 > totalLen-1 {
57+
output.PrintFrameworkError("Invalid TargetInfo, not enough data in the buffer to parse")
58+
59+
return retArr, true
60+
}
61+
62+
// Grabbing type
63+
targetInfoType := int(binary.LittleEndian.Uint16(targetInfoBytes[cursor : cursor+2]))
64+
if targetInfoType == 0 { // Should not really hit/caution
65+
output.PrintFrameworkWarn("Hit terminator block prematurely during TargetInfo decoding of NTLM Type 2 Message")
66+
67+
return retArr, true
68+
}
69+
70+
// Decoding the value, after length check
71+
targetInfoLen := binary.LittleEndian.Uint16(targetInfoBytes[cursor+2 : cursor+4])
72+
if (cursor + 4 + int(targetInfoLen)) > totalLen-1 {
73+
output.PrintFrameworkError("Decoding NTLM Type 2 Target Info failed, Invalid target info length provided")
74+
75+
return []TargetInfo{}, false
76+
}
77+
78+
value, ok := DecodeUTF16LE(targetInfoBytes[cursor+4 : cursor+4+int(targetInfoLen)])
79+
if !ok {
80+
return []TargetInfo{}, false
81+
}
82+
83+
retArr = append(retArr, TargetInfo{Type: targetInfoType, Value: value})
84+
cursor = cursor + 4 + int(targetInfoLen)
85+
}
86+
87+
return retArr, true
88+
}
89+
90+
type TargetInfo struct {
91+
Type int
92+
Value string
93+
}
94+
95+
type NTLMType2Message struct {
96+
TargetName string
97+
TargetInfoArr []TargetInfo
98+
Challenge []byte
99+
Flags uint32
100+
}
101+
102+
// Decodes an NTLM Type 2 message, Spec reference: https://curl.se/rfc/ntlm.html.
103+
// The input value should be a base64 value.
104+
// Returns an NTLMType2Message struct as a value.
105+
func DecodeNTLMType2(stringMessage string) (NTLMType2Message, bool) {
106+
returnMessage := NTLMType2Message{TargetName: "", TargetInfoArr: nil, Challenge: nil, Flags: 0}
107+
message := []byte(DecodeBase64(stringMessage))
108+
if len(message) < 32 {
109+
output.PrintFrameworkError("NTLMType2Decode Failed: Message too short")
110+
111+
return NTLMType2Message{}, false
112+
}
113+
114+
if !bytes.Equal(message[:8], []byte{0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00}) {
115+
output.PrintFrameworkError("NTLMType2Decode Failed: Invalid NTLMSSP signature")
116+
117+
return NTLMType2Message{}, false
118+
}
119+
120+
if !bytes.Equal(message[8:12], []byte{0x02, 0x00, 0x00, 0x00}) {
121+
output.PrintFrameworkError("NTLMType2Decode Failed: Message type is not 2")
122+
123+
return NTLMType2Message{}, false
124+
}
125+
126+
// mandatory value parsing
127+
targetNameLen := uint32(binary.LittleEndian.Uint16(message[12:14]))
128+
allocatedSize := uint32(binary.LittleEndian.Uint16(message[14:16]))
129+
targetNameOffset := binary.LittleEndian.Uint32(message[16:20])
130+
returnMessage.Flags = binary.LittleEndian.Uint32(message[20:24])
131+
returnMessage.Challenge = message[24:32]
132+
133+
// optional value parsing
134+
if (returnMessage.Flags & 0x00800000) != 0 { // Negotiate target info is set
135+
if int(targetNameOffset+targetNameLen) > len(message)-1 {
136+
output.PrintFrameworkError("NTLMType2Decode Failed: Message buffer is too short, may be corrupted/truncated")
137+
138+
return NTLMType2Message{}, false
139+
}
140+
141+
targetInfoData := message[targetNameOffset : targetNameOffset+targetNameLen]
142+
if uint32(len(targetInfoData)) != allocatedSize {
143+
output.PrintFrameworkError("NTLMType2Decode Failed: targetInfoData does not match reported allocated size")
144+
145+
return NTLMType2Message{}, false
146+
}
147+
targetInfoLen := uint32(binary.LittleEndian.Uint16(message[40:42]))
148+
targetInfoOffset := binary.LittleEndian.Uint32(message[44:48])
149+
150+
targetName, ok := DecodeUTF16LE(message[targetNameOffset : targetNameOffset+targetNameLen])
151+
if !ok {
152+
return NTLMType2Message{}, false
153+
}
154+
155+
targetInfoBuf := message[targetInfoOffset : targetInfoOffset+targetInfoLen]
156+
targetInfoArr, ok := decodeNTLM2TargetInfo(targetInfoBuf)
157+
if !ok {
158+
output.PrintFrameworkError("NTLMType2Decode Failed: could not parse target info")
159+
160+
return NTLMType2Message{}, false
161+
}
162+
163+
returnMessage.TargetName = targetName
164+
returnMessage.TargetInfoArr = targetInfoArr
165+
output.PrintfFrameworkDebug("NTLM Type 2: Parsed TargetName: %s", returnMessage.TargetName)
166+
output.PrintfFrameworkDebug("NTLM Type 2: Parsed TargetInfo Data: %v", returnMessage.TargetInfoArr)
167+
}
168+
169+
return returnMessage, true
170+
}
171+
172+
// Converts a provided byte buffer to a UTF-16LE decoded string.
173+
func DecodeUTF16LE(data []byte) (string, bool) {
174+
decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
175+
utf8bytes, err := decoder.Bytes(data)
176+
if err != nil {
177+
output.PrintfFrameworkError("Failed to decode input: %v", err)
178+
179+
return "", false
180+
}
181+
182+
return string(utf8bytes), true
183+
}
184+
20185
func StringToUnicodeByteArray(s string) []byte {
21186
//nolint:prealloc
22187
var byteArray []byte
@@ -164,9 +329,11 @@ func Title(s string) string {
164329
// URL encode every character in the provided string.
165330
func URLEncodeString(inputString string) string {
166331
encodedChars := ""
332+
var encodedCharsSb315 strings.Builder
167333
for _, char := range inputString {
168-
encodedChars += "%" + strconv.FormatInt(int64(char), 16)
334+
encodedCharsSb315.WriteString("%" + strconv.FormatInt(int64(char), 16))
169335
}
336+
encodedChars += encodedCharsSb315.String()
170337

171338
return encodedChars
172339
}

transform/encode_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,139 @@
11
package transform
22

33
import (
4+
"encoding/binary"
5+
"encoding/hex"
6+
"reflect"
47
"testing"
58
)
69

710
const (
811
urlTestString = "Theskyabovetheportwasthecoloroftelevision,tunedtoadeadchannel.\xff\xff\xff\x3e\xfe\x00"
912
)
1013

14+
func TestDecodeUTF16LE(t *testing.T) {
15+
inputHex := "44004f004d00410049004e00"
16+
inputBytes, err := hex.DecodeString(inputHex)
17+
if err != nil {
18+
t.Fatal("Failed to decode test case")
19+
}
20+
decodedString, ok := DecodeUTF16LE(inputBytes)
21+
if !ok {
22+
t.Fatal("Failed to decode")
23+
}
24+
25+
if !reflect.DeepEqual("DOMAIN", decodedString) {
26+
t.Fatalf("Unexpected decoded value: %q", decodedString)
27+
}
28+
29+
t.Logf("Decoded UTF16LE String: %q", decodedString)
30+
}
31+
32+
func TestDecodeTargetInfo(t *testing.T) {
33+
inputHex := "02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"
34+
inputBytes, err := hex.DecodeString(inputHex)
35+
if err != nil {
36+
t.Fatal("Failed to decode test case")
37+
}
38+
targetInfoArr, ok := decodeNTLM2TargetInfo(inputBytes)
39+
if !ok {
40+
t.Fatal("Failed to decode")
41+
}
42+
43+
if !reflect.DeepEqual(targetInfoArr, []TargetInfo{
44+
{Type: 2, Value: "DOMAIN"},
45+
{Type: 1, Value: "SERVER"},
46+
{Type: 4, Value: "domain.com"},
47+
{Type: 3, Value: "server.domain.com"},
48+
}) {
49+
t.Fatalf("Unexpected decoded value: %v", targetInfoArr)
50+
}
51+
52+
t.Logf("Decoded Type 2 TargetInfo: %v", targetInfoArr)
53+
}
54+
55+
func TestDecodeNTLMType2Truncated(t *testing.T) {
56+
inputHex := "4e544c4d53535000020000000c000c0030000000010281000123456789abcdef0000000000000000620062003c000000"
57+
inputBytes, err := hex.DecodeString(inputHex)
58+
if err != nil {
59+
t.Fatal("Failed to decode test case")
60+
}
61+
inputBase64 := EncodeBase64(string(inputBytes))
62+
_, ok := DecodeNTLMType2(inputBase64)
63+
if ok {
64+
t.Fatal("Test case should have failed, but decoding was reported as successful")
65+
}
66+
t.Log("Truncated message failed to decode as expected")
67+
}
68+
69+
func TestDecodeNTLMType2(t *testing.T) {
70+
inputHex := "4e544c4d53535000020000000c000c0030000000010281000123456789abcdef0000000000000000620062003c00000044004f004d00410049004e0002000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"
71+
inputBytes, err := hex.DecodeString(inputHex)
72+
if err != nil {
73+
t.Fatal("Failed to decode test case")
74+
}
75+
inputBase64 := EncodeBase64(string(inputBytes))
76+
message, ok := DecodeNTLMType2(inputBase64)
77+
if !ok {
78+
t.Fatal("Failed to decode")
79+
}
80+
81+
if !reflect.DeepEqual(message, NTLMType2Message{
82+
TargetName: "DOMAIN",
83+
TargetInfoArr: []TargetInfo{
84+
{Type: 2, Value: "DOMAIN"},
85+
{Type: 1, Value: "SERVER"},
86+
{Type: 4, Value: "domain.com"},
87+
{Type: 3, Value: "server.domain.com"},
88+
},
89+
Challenge: []byte("\x01#Eg\x89\xab\xcd\xef"),
90+
Flags: binary.LittleEndian.Uint32([]byte("\x01\x02\x81\x00")),
91+
}) {
92+
t.Fatalf("Unexpected decoded value: %#v", message)
93+
}
94+
95+
t.Logf("Decoded Type 2 Message: %#v", message)
96+
}
97+
98+
func TestDecodeNTLMType2Overflow(t *testing.T) {
99+
inputHex := "4e544c4d53535000020000000c000c0030000000010281000123456789abcdef0000000000000000620062003c00000044004f004d00410049004e000200120044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"
100+
inputBytes, err := hex.DecodeString(inputHex)
101+
if err != nil {
102+
t.Fatal("Failed to decode test case")
103+
}
104+
inputBase64 := EncodeBase64(string(inputBytes))
105+
_, ok := DecodeNTLMType2(inputBase64)
106+
if ok {
107+
t.Fatal("Invalid NTLM Message decoded successfully, this should not happen")
108+
}
109+
110+
t.Log("Properly failed on invalid message")
111+
}
112+
113+
func TestDecodeNTLMType2Minimal(t *testing.T) {
114+
inputHex := "4e544c4d53535000020000000000000000000000020200000123456789abcdef"
115+
inputBytes, err := hex.DecodeString(inputHex)
116+
if err != nil {
117+
t.Fatal("Failed to decode test case")
118+
}
119+
inputBase64 := EncodeBase64(string(inputBytes))
120+
message, ok := DecodeNTLMType2(inputBase64)
121+
if !ok {
122+
t.Fatal("Failed to decode")
123+
}
124+
125+
if !reflect.DeepEqual(message, NTLMType2Message{
126+
TargetName: "",
127+
TargetInfoArr: nil,
128+
Challenge: []byte("\x01\x23\x45\x67\x89\xab\xcd\xef"),
129+
Flags: binary.LittleEndian.Uint32([]byte("\x02\x02\x00\x00")),
130+
}) {
131+
t.Fatalf("Unexpected decoded value: %#v", message)
132+
}
133+
134+
t.Logf("Decoded Type 2 Message: %#v", message)
135+
}
136+
11137
func TestURLEncodeString(t *testing.T) {
12138
encoded := URLEncodeString("foo !~")
13139

0 commit comments

Comments
 (0)