【域渗透】域内会话收集

2018 年3月,BooldHound 发表了篇博文 SharpHound: Target Selection and API Usage 。这里面介绍了一些关于收集 BooldHound 所需信息的收集方法及所使用的 API。而本文主要介绍两个关于收集会话信息的 API及利用远程注册表的方法(翻译+补充),也是对【域渗透】获取域环境内用户登录信息 的一个补充。

0x00 前言

使用过BooldHound 的朋友都知道,它所呈现出来非常多的信息,基本能满足日常信息收集所需的数据,但是之前使用的时候,就很惊讶,它们的会话关系数据来源是怎么来的,所幸后来官方给了解释。

0x01 会话收集

BooldHound 公开了三种不同的查询计算机会话信息的方法,都是从检查 445 端口开始的。

1)、NetSessionEnum

它不允许直接查询是谁登陆了此工作站,但是它允许查询是谁在访问此工作站的网络资源(例如文件共享)时所创建的网络会话,从而知道来自何处。当然这里最好的查询对象是域控 + 文件共享服务器。此函数不需要高权限。

该函数原型是:

它需要 9 个参数,我们只需了解其中的 5 个参数即可:

  • servername:该字符串指定要在其上执行函数的远程服务器的DNS或NetBIOS名称。如果此参数为NULL,则使用本地计算机。
  • UncClientName:该字符串指定要为其返回信息的计算机会话的名称。如果此参数为 NULL,则 NetSessionEnum 将返回服务器上所有计算机会话的信息。
  • username:该字符串指定要为其返回信息的用户的名称。如果此参数为 NULL,则 NetSessionEnum 将返回所有用户的信息。
  • level:指定数据的信息级别。
  • bufptr:指向接收数据的缓冲区的指针。此数据的格式取决于 level 参数的值。此缓冲区由系统分配,必须使用 NetApiBufferFree 函数释放 。请注意,即使函数因 ERROR_MORE_DATA 而失败,也必须释放缓冲区。

而此 API 的调用示例为:

1
2
string server = "rcoil.me"
nStatus = NetSessionEnum(server, null, null, 10, out Bufptr, -1, ref dwEntriesread, ref dwTotalentries, ref dwResume_handle);

其中,level 的数值为10,是唯一以未经身份验证的方式获取所需数据的级别。

它会返回如下内容:

1
2
3
4
sesi10_cname - 192.10.22.102
sesi10_username - RcoIl
sesi10_time - 0
sesi10_idle_time - 0

关键源码如下:

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
/// <summary>
/// 返回指定服务器的所有 SESSIONS。返回 SESSION_INFO_10 结构的数组。
/// https://www.pinvoke.net/default.aspx/netapi32/NetSessionEnum.html
/// </summary>
/// <param name="server">默认所有域内机器,隐形目标:域控制器+共享服务器</param>
/// <returns>SESSION_INFO_10 STRUCTURE ARRAY</returns>
public static SESSION_INFO_10[] EnumSessions(string server)
{
IntPtr Bufptr;
int nStatus = 0;
Int32 dwEntriesread = 0, dwTotalentries = 0, dwResume_handle = 0;

Bufptr = (IntPtr)Marshal.SizeOf(typeof(SESSION_INFO_10));
SESSION_INFO_10[] results = new SESSION_INFO_10[0];
do
{
nStatus = NetSessionEnum(server, null, null, 10, out Bufptr, -1, ref dwEntriesread, ref dwTotalentries, ref dwResume_handle);
results = new SESSION_INFO_10[dwEntriesread];
if (nStatus == (int)NERR.ERROR_MORE_DATA || nStatus == (int)NERR.NERR_Success)
{
Int32 p = Bufptr.ToInt32();
for (int i = 0; i < dwEntriesread; i++)
{

SESSION_INFO_10 si = (SESSION_INFO_10)Marshal.PtrToStructure(new IntPtr(p), typeof(SESSION_INFO_10));
results[i] = si;
p += Marshal.SizeOf(typeof(SESSION_INFO_10));
}
//NetApiBufferFree(BufPtr);
}
// 释放先前从进程的非托管内存分配的内存。
Marshal.FreeHGlobal(Bufptr);
}
while (nStatus == (int)NERR.ERROR_MORE_DATA);

return results;
}

2)、NetWkstaUserEnum

NetWkstaUserEnum 功能可以列出当前登录到该工作站的所有用户的信息。此列表包括交互式、服务和批量登录。此函数需要主机的管理权限或这域管权限,适用于自检使用。

该函数原型是:

它需要 7个参数,我们只需了解其中的 3 个参数即可:

  • servername:该字符串指定要在其上执行函数的远程服务器的DNS或NetBIOS名称。如果此参数为NULL,则使用本地计算机。
  • level:指定数据的信息级别。
  • bufptr:指向接收数据的缓冲区的指针。此数据的格式取决于 level 参数的值。此缓冲区由系统分配,必须使用 NetApiBufferFree 函数释放 。请注意,即使函数因 ERROR_MORE_DATA 而失败,也必须释放缓冲区。

而此 API 的调用示例为:

1
2
string server = "rcoil.me"
nStatus = NetWkstaUserEnum(server, 1, out Bufptr, 32768, out dwEntriesread, out dwTotalentries, ref dwResumehandle);

其中,level 的数值为1,是因为 1 比 0 返回的数据多,因此选择了 1。

它会返回如下内容:

1
2
3
4
wkui1_username - Administrator
wkui1_logon_domain - RDC_1
wkui1_oth_domains -
wkui1_logon_server - RCOIL

关键源码如下:

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
/// <summary>
/// API 调用的第二个参数是 API 调用的级别,其中 1 返回的数据多于 0,所以选择 1进行测试
/// https://www.pinvoke.net/default.aspx/netapi32/netwkstauserenum.html
/// </summary>
/// <param name="server">默认所有域内机器,隐形目标:域控制器+共享服务器</param>
/// <returns></returns>
public static WKSTA_USER_INFO_1[] EnumWkstaUser(string server)
{
IntPtr Bufptr;
int nStatus = 0;
Int32 dwEntriesread = 0, dwTotalentries = 0, dwResumehandle = 0;

Bufptr = (IntPtr)Marshal.SizeOf(typeof(WKSTA_USER_INFO_1));
WKSTA_USER_INFO_1[] results = new WKSTA_USER_INFO_1[0];
do
{
nStatus = NetWkstaUserEnum(server, 1, out Bufptr, 32768, out dwEntriesread, out dwTotalentries, ref dwResumehandle);
results = new WKSTA_USER_INFO_1[dwEntriesread];
if ((nStatus == NERR_SUCCESS) || (nStatus == ERROR_MORE_DATA))
{
if (dwEntriesread > 0)
{
IntPtr pstruct = Bufptr;
for (int i = 0; i < dwEntriesread; i++)
{
WKSTA_USER_INFO_1 wui1 = (WKSTA_USER_INFO_1)Marshal.PtrToStructure(pstruct, typeof(WKSTA_USER_INFO_1));
results[i] = wui1;
pstruct = (IntPtr)((int)pstruct + Marshal.SizeOf(typeof(WKSTA_USER_INFO_1)));
}
}
}

if (Bufptr != IntPtr.Zero)
NetApiBufferFree(Bufptr);

} while (nStatus == ERROR_MORE_DATA);
return results;
}

3)、远程注册表

此方法仅为辅助启用。尝试打开远程注册表的用户配置单元(如果已启用),并将查找与 SID 格式匹配的子项,这些对应于登录用户将获取的 SID 转换成用户名即可。一般来说,需要域管权限去操作,而在极少数情况下,无需管理员权限即可访问此数据,这个得看脸。

此处使用 OpenRemoteBaseKey() 函数。

它需要 2 个参数:

  • RegistryHive:来自 RegistryHive 的枚举。这里选择 Users,表示 HKEY_USERS
  • string:表示远程主机。

此函数的调用示例为:

1
var key = RegistryKey.OpenRemoteBaseKey(RegistryHive.Users, server);

它会返回如下内容:

1
HKEY_USERS

然后读取 HKEY_USERS 的键项就可以了。关键源码如下:

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
/// <summary>
/// 利用 OpenRemoteBaseKey 读取 HKEY_USERS 的键项
/// </summary>
/// <param name="server"></param>
/// <returns></returns>
private static IEnumerable<string> GetRegistryLoggedOn(string server)
{

var users = new List<string>();
try
{
// 远程打开注册表配置单元,如果它不是我们当前的配置单元
RegistryKey key = RegistryKey.OpenRemoteBaseKey(RegistryHive.Users, server);

// 找到与我们的正则表达式匹配的所有子项
var filtered = key.GetSubKeyNames().Where(sub => SidRegex.IsMatch(sub));

foreach (var subkey in filtered)
{
users.Add(subkey);
}
}
catch (Exception)
{
yield break;
}

foreach (var user in users.Distinct())
{
yield return user;
}
}

最后将 用户的SID 转成用户名即可。

1
string Username = new SecurityIdentifier(regSID).Translate(typeof(NTAccount)).ToString();

4)、结果

0x02 备忘录

API 调用 协议 端口 RPC 接口 UUID 命名管道 RPC 方法
NetSessionEnum.aspx) [MS-SRVS]: Server Service Remote Protocol TCP 445 4B324FC8-1670-01D3-1278-5A47BF6EE188 \PIPE\srvsvc NetrSessionEnum
NetWkstaUserEnum.aspx) [MS-WKST]: Workstation Service Remote Protocol TCP 445 6BFFD098-A112-3610-9833-46C3F87E345A \PIPE\wkssvc NetrWkstaUserEnum

Github 项目地址:SharpDomainSession ,此项目仅供实验使用 Demo。如想更进一步利用,可移至 SharpHound

!坚持技术分享,您的支持将鼓励我继续创作!