漏洞重现

准备工作

工具:

  • 攻击机:kali 192.168.107.128

  • 靶机:windows server 2003 192.168.107.130

  • 软件:nmap,msf

image-20240526131521749

image-20240526134548567

需要两边进行确认主机网络正常通信,可能有防火墙或者其他原因导致网络不通

开始攻击

nmap扫描

先使用namp进行主机存活测试,nmap一般使用流程:

主机->端口->端口对应服务类型及版本->操作系统

nmap -sP 192.168.107.0/24 #扫描网段存活主机

image-20240526135151872

多出了两台,应该是虚拟机内部网卡啥的,感觉也不像,算了先把这个漏洞弄完

nmap 192.168.107.130 -p- #扫描靶机开放端口1-65535

image-20240526135836862

该漏洞需要利用445端口远程连接,所以必须要保证打开

nmap -O 192.168.56.132 #扫描操作系统类型

image-20240526140431474

系统为winodws2003_server_sp2

现在搜集了大多信息可以直接用脚本对靶机进行漏洞发现

nmap --script vuln 192.168.107.130 #三种检测方式,这是最常使用的发现常见漏洞

image-20240526141319418

存在好几个漏洞发现有010漏洞存在

现在可以开始使用关于该漏洞脚本进行攻击了

msf攻击

官方解释:

Metasploit(MSF)是一个免费的、可下载的框架,通过它可以很容易地获取、开发并对计算机软件漏洞实施攻击。

它本身附带数百个已知软件漏洞,是一款专业级漏洞攻击工具

因为只要掌握MSF的使用方法,每个人都可以使用MSF来攻击那些未打过补丁或者刚刚打过补丁的漏洞。

即将漏洞利用脚本程序化,只需要更改其中参数即可

search ms17-010 #搜索攻击脚本是否存在

image-20240526142342198

show options #展示参数配置选项

image-20240526143839734

run

image-20240526144147675

发现解释器变成了meterpreter,可以使用命令对靶机不现在应该是肉鸡了开始操控

漏洞分析

由上面可知漏洞与445端口和SMB文件共享协议有关,事实上是由SMB中函数强制转换引起的缓冲区溢出,除此之外还利用了两个小的漏洞控制缓冲区的分配

SMB 协议是应用程序级别的网络协议,主要用于共享打印机、文件访问、串行端口以及网络上的节点之间的其他通信。 SMB 主要由Windows 系统使用,是一种经过验证的进程间通信机制,可以在多种协议上运行。 在Oracle Solaris OS 中,SMB 协议主要用于共享打印机。 所有这些进程都在网络上执行。

从靶机目录C:\Windows\System32\drivers取出文件srv.sys,问题函数就是在这里面

srv.sys

前置知识:

1
2
3
4
5
6
7
8
9
10
11
12
struct _FEA{  //用于文件扩展属性结构
BYTE fEA; //标记位
BYTE cbNAME; //文件扩展字符串长度
USHORT cbValue; //记录值的长度
UCHAR AttributeNAME[cbname];
UCHAR ATTributeValue[cbvalue];
}FEA;
struct _FEALIST{
ULONG cbList; //记录FEAList总长度
_FEA list[1];
}FEALIST, *PFEALIST;

image-20240529013147107

理论上索引不能为变量,但事实上使用的时候是将结构体这么看做的

SrvOs2FeaListToNt的作用是将Os2 _Fea列表转化为NT Fea列表,漏洞触发点也恰好位于这个函数中

函数 SrvOs2FeaListToNt 的功能是:

  1. 接收一个 OS/2 格式的 FEA 列表并将其转换为 NT 格式。
  2. 计算转换后的 FEA 列表的大小。
  3. 如果转换后的大小不为零,则分配一个非分页池来存储转换后的列表。
  4. 根据需要处理每个 FEA 项目,并将它们转换为 NT 格式。
  5. 释放分配的内存。

静态分析

image-20240527221258010

函数四个参数表示

  • 指向Os2Fea列表
  • 传换完成存放地址
  • 指向需要为新结构分配的空间大小
  • 发生错误时被赋值
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
__int64 __fastcall SrvOs2FeaListToNt(_FEALIST *a1, __int64 *a2, unsigned int *a3, _WORD *a4)
{
_FEA *v4; // rdi
_FEA *v5; // r11
__int16 v7; // r12
unsigned int v8; // er8
_FEA *v10; // r10
__int64 v12; // rdx
__int64 v13; // rcx
__int64 result; // rax
__int64 v15; // rax
__int64 *v16; // rcx
unsigned __int64 v17; // rbp
_FEA *v18; // r15
_DWORD *v19; // r13
_BYTE *v20; // rbx
__int64 v21; // r11
__int64 v22; // rcx
__int64 v23; // rcx

v4 = a1->list; // 指向列表的—_FEA元素
v5 = (_FEA *)((char *)a1 + a1->cbList); // 指向列表末尾
v7 = 0;
v8 = 0;
v10 = a1->list; // 指向_Fea
if ( a1->list < v5 ) // 列表存在且含有元素
{
while ( &v10[1] < v5 ) // 没有判断list[0],从list[1]开始
{
v12 = v10->cbValue;
v13 = v10->cbNAME;
// FEA.list[2]小于总长度,不太理解为什么v13 +v2 +5=sizeof(FEA),v13和v12指的应该是对应字符串长度和值长度,和FEA本身大小有什么干系,感觉应该是FEA结构体不全,\n这里只是知道FEA header头,具体字符串和值在FEA.cbvalue后面
if ( &v10[1].cbNAME + v13 + v12 > (BYTE *)v5 )
break;
v10 = (_FEA *)((char *)v10 + v13 + v12 + 5);// cbname不包含null,所以为4+1,类型长度+null,v10 =cbname.list[2]
v8 += (v13 + v12 + 12) & 0xFFFFFFFC; // 结果左移两位对齐保存,计算需要的NT FEA值
if ( v10 >= v5 ) // 因为上面判断过是否大于所以这里只能是=,既然等于那就可以直接跳出了
goto LABEL_7;
}
// 修复cblist,按理说应该正好相等,也是离谱就是修复给修出bug了,cblist本来为ulong,4byte,这里都是word,高位没法被赋值
LOWORD(a1->cbList) = (_WORD)v10 - (_WORD)a1;
}
LABEL_7:
*a3 = v8; // 返回得到的转换大小值
if ( v8 ) // 为0说明FEA为空
{
v15 = SrvAllocateNonPagedPool(v8);
v16 = a2;
*a2 = v15;
v17 = v15;
if ( v15 )
{
v18 = (_FEA *)((char *)a1 + a1->cbList - 5);
v19 = (_DWORD *)v15;
if ( v4 > v18 )
{
LABEL_19:
if ( v4 == (_FEA *)((char *)a1 + a1->cbList) )
{
*v19 = 0;
result = 0i64;
}
else
{
v22 = *v16;
*a4 = v7 - (_WORD)a1;
SrvFreeNonPagedPool(v22);
result = 3221225473i64;
}
}
else
{
while ( (v4->fEA & 0x7F) == 0 )
{
*(_BYTE *)(v17 + 4) = v4->fEA;
*(_BYTE *)(v17 + 5) = v4->cbNAME;
*(_WORD *)(v17 + 6) = v4->cbValue;
v19 = (_DWORD *)v17;
v7 = (__int16)v4;
memmove((void *)(v17 + 8), &v4[1], v4->cbNAME);
v20 = (_BYTE *)(*(unsigned __int8 *)(v17 + 5) + v17 + 8);
*v20 = 0;
memmove(v20 + 1, &v4[1].cbNAME + *(unsigned __int8 *)(v17 + 5), *(unsigned __int16 *)(v17 + 6));
v21 = *(unsigned __int16 *)(v17 + 6);
*(_DWORD *)v17 = ((v21 + (_DWORD)v20 + 4) & 0xFFFFFFFC) - v17;
v17 = (unsigned __int64)&v20[v21 + 4] & 0xFFFFFFFFFFFFFFFCui64;
v4 = (_FEA *)((char *)v4 + v4->cbValue + (unsigned __int64)v4->cbNAME + 5);
if ( v4 > v18 )
{
v16 = a2;
goto LABEL_19;
}
}
v23 = *a2;
*a4 = (_WORD)v4 - (_WORD)a1;
SrvFreeNonPagedPool(v23);
result = 3221225485i64;
}
}
else
{
if ( (unsigned int)SrvWmiEnableLevel >= 2 && (SrvWmiEnableFlags & 1) != 0 && KeGetCurrentIrql() < 2u )
{
DbgPrint_0("SrvOs2FeaListToNt: Unable to allocate %d bytes from nonpaged pool.", *a3);
DbgPrint_0("\n");
}
result = 3221225989i64;
}
}
else
{
*a4 = 0;
result = 0xC098F0FFi64;
}
return result;
}

LOWORD(a1->cbList) = (_WORD)v10 - (_WORD)a1;

所以关键在这,给于了多余空间,即溢出

Trans2

相关函数:

  • SMB_COM_TRANSACTION2 子命令支持一组更丰富的服务器端文件系统语义。这些所谓的“Trans2 子命令”允许客户端设置和检索扩展属性键/值对、使用长文件名(比原始 8.3 格式的名称更长)以及执行目录搜索等任务。

  • SMB_COM_NT_TRANSACT 子命令扩展了SMB_COM_TRANSACTION2(第 2.2.4.46 节)提供的文件系统功能访问,并且还允许传输非常大的参数和数据块。SMB_COM_NT_TRANSACT 消息可能会超出单个 SMB 消息的最大大小(由MaxBufferSize会话参数的值决定 )。在这种情况下,客户端将使用一个或多个 SMB_COM_NT_TRANSACT_SECONDARY 消息来传输 初始消息中容纳不下的事务数据参数字节。

访问流程

1
2
3
4
5
6
7
8
9
10
11
// 初始事务请求
send_smb_command(SMB_COM_TRANSACTION2, initial_data);

if (initial_data_size > MaxBufferSize) {
// 如果初始数据超过最大缓冲区大小,使用子命令继续发送
while (remaining_data_to_send > 0) {
send_smb_command(SECONDARY_sub_command, next_chunk_of_data);
remaining_data_to_send -= chunk_size;
}
}

trans2允许传输word参数类型数据大小,transact允许dword参数类型数据大小,他们对应子命令也是如此,但数据传输时并没有做函数验证判断,默认以最后一个子命令类型判断调用函数为trans2还是transact,这样就可能出现

SMB_COM_NT_TRANSACT ( Dword ) 后跟 SMB_COM_TRANSACTION2_SECONDARY ( Word ) 时,就会发生这种情况。这会导致错误地解析数据,就好像它最初来自 SMB_COM_TRANSACTION2 类型的事务一样。

同时只有最后一个secondary子命令被发送才会执行后面代码

Non-paged Pool Allocation Bug

利用识别错误的请求数据结构读取错误的SMB_DATA->ByteCount偏移

图片描述

通过SMB_COM_SESSION_SETUP_ANDX可以将本该由GetExtendSecurityParameters函数换成调用NtSecrityParameters函数,因而将原ntm2当做了ntm1解析,但两者数据大小并不一样,因而解析对应值发生了变化,而后会为NatiuveOS和NativeLanMan分配读取ByteCount空间

image-20240529134405065

利用这个错误通过发送一个数据包,在非分页池中可以分配大缓冲区后又被释放,在该攻击中起占位作用,为后续NtFea分配到该释放位置

动态验证

VM打开设置闪退有点问题,之后修好再补上调试内容,win2003虚拟机坏了,换了一台win7 x64专业版

内核调试

通过串行端口管道连接win7

image-20240602165713391

kali攻击

image-20240602165826976

srv.sys

计算的两个字节结果会被存入rsi指向两个字节中,但取值却是四个字节

image-20240603011148406

漏洞利用

前置概念

1
2
3
4
5
6
7
物理内存:RAM(随机存取存储器),拥有比硬盘更高速的读写速度
虚拟内存:搁硬盘扩展内存区域,即使用下面的内存分页机制,但速度慢些
内存分页机制:系统为了有效利用物理内存,会将不常用物理内存放入磁盘中页面文件,再次使用需要先从磁盘中加载出来
非分页内存池:常驻于物理内存中,数据可以立即被访问,多用于驱动与系统内核
SRVNET 驱动: Windows 操作系统中的一个网络驱动程序,专门用于处理服务器网络通信。srvnet.sys的一个实例和程序
内存对齐:内存对齐是指数据在内存中存储时的地址必须满足某些对齐要求,例如双字(4字节)数据必须存储在地址为4的倍数的位置上。这种对齐要求可以提高内存访问效率和系统性能。否则可能报错或者崩溃
srvnet标头:标头有两个重要的成员,其一`MDL` 结构体下有StartVa表示下一个srvnet客户端连接发送数据将被映射内存位置;另一个**`pSrvNetWskStruct`**下有一个HanderFunction函数指针,当srvnet连接被关闭时会自动执行该函数指针指向函数

制作缓冲区

直接溢出的区域是随机没有对齐的,需要通过多次分配释放,构建出对齐非分页堆,即内存池喷射

每次srvnet.sys连接被创建都会申请一个缓存区用来存放传输数据,且缓存区都含有srvnet标头,当连接被关闭,缓存区被释放,srv.sys创建时也是这样

  • 首先创建srv连接,会申请Fea堆块
  • 使用trans2填充Fea内容,但不发送最后一个secondary数据包,这样由srv中的错误计算就暂时不会被执行
  • 创建多个srvnet连接,这样可以同时占用多个缓存区,增加溢出到srvnet标头机会(完全可以后面创建连接为什么要在这里提前建立)
  • 利用Non-paged这个漏洞申请一块和第一个漏洞转化后的NtFea一样大的堆块,用于NtFea的占位
  • 继续创建多个srvnet连接直到有缓存被分配到占位堆块前面紧挨着以方便溢出时覆盖
  • 释放占位堆块
  • 发送最后一个secondary数据包,会激发第二步NtFea的转化,此时会为他分配占位释放堆块存放
  • NtFea会溢出覆盖srvnet中MDL和**pSrvNetWskStruct**
  • 创建含自定义数据的新srvnet连接,他会被映射入MDL记录地址
  • 关闭srvnet连接,会自动执行pSrvNetWskStruct结构中的HanderFunction指向函数,而在覆盖时,这被改为了shellcode地址

pSrvNetWskStruct指向的结构被包含在新srvnet连接自定义数据中伪造的SrveNetWskStruck结构,shellcode紧跟着结构体后面

MDL指定地址应该为堆中具有可执行权限的内存,例如HAL’Heap

image-20240530230122376

借用大神的图片,溢出代码被执行时需要接收到最后一个数据包,所以先发送SMB请求前面的数据报文不断申请修整缓冲块被分配位置,便于溢出时申请空间是对齐的,发送最后一个数据包,在这个数据包的内容将覆盖溢出区域,也是MDL的指定位置处,shellcode将会被写入这块区域

然后更改DisconnectHandleFunc指针链

srvnet!SrvNetWskReceiveComplete执行时会自动调用该地址

流量分析

可恶,分析一半环境崩了,电脑直接宕机了

MVIMG_20240603_120159

可以看到图中正在发送srv的transact请求但没有发送最后一个包

后面直接借用第三个链接的图吧

image-20240603121141752

可以发现和上面说的一样,出现三个不同缓冲区,然后缓冲区一在最后一个secondary包发送后会映射到第三个缓存区位置,如果有第二个和第三个刚好挨着就会出现RCE

遗留问题

  • 为什么创建连接会申请堆块
  • trans2错误解析数据之后是怎么在攻击中起作用的,猜测应该也是依靠word数据被解析成dword造成数据的溢出
  • 对trans2和Non bug没进行完动态验证

参考链接: