深入剖析Windows本地提权(应用篇)
深入剖析Windows本地提权(应用篇)

3.从常规用户到管理员账户(BypassUAC)

这一方面的实现已经是经久不衰的话题,这里笔者要讨论的可能也猜到了,就是BypassUAC,当然在这里可能不只有BypassUAC实现了从常规用户到管理员用户,但目前的主流手段仍然是BypassUAC

UAC是微软Microsoft Windows Vista以后版本引入的一种安全机制。其原理是通知用户是否对应用程序使用硬盘驱动器和系统文件授权,以达到帮助阻止恶意程序(有时也称为“恶意软件”)损坏系统的效果。

通过UAC,应用程序和任务可始终在非管理员帐户的安全上下文中运行,除非管理员特别授予管理员级别的系统访问权限。UAC 可以阻止未经授权的应用程序自动进行安装,并防止无意中更改系统设置。

下图清晰描述了如何根据是否启用UAC以及应用程序是否具有UAC清单来运行应用

在开启了UAC之后,如果用户是标准用户, Windows会给用户分配一个标准Access Token而如果用户以管理员权限登陆,会生成两份访问令牌,一份是完整的管理员访问令牌(Full Access Token),一份是标准用户令牌,令牌相关知识已经在前文提到,这里就不在过多赘述

在绝大多数BypassUAC的实现中,笔者认为利用方式主要可以分为两大类:

1、各类UAC白名单程序的DLL劫持(Dll Hijack)
2、各类提升权限的COM接口利用(Elevated COM interface)

这里首先以第二种方式为例,结合实际BypassUAC组件来分析如何从常规用户到管理员账户进行提权

3.1 UAC的逆向分析

首先我们需要知道的是在任务管理器中使用管理员身份启动的进程的父进程是explorer,但是在explorer中KERNELBASE!CreateProcessW位置下断点,使用管理员权限运行程序,并不会断下,而正常启动程序则可以正常断下来,这说明elevated的程序很有可能并非是由explorer拉起的。

而explorer本身也并非一个SYSTEM权限的进程,如果提权过程是由explorer过程发起,因此也不可能达到提权的目的。为此我们先在KERNELBASE!CreateProcessW下入断点随后正常启动一个程序,查看调用栈分析:

由文件名可知该函数在windows_storage.dll中,进行反编译查看该函数的实现过程:
发现CInvokeCreateProcessVerb::CallCreateProcess实际上会去调用AicLaunchAdminProcess

我们跟进这个函数:

可以看到这里实际上进行了RPC的通信,我们根据绑定句柄的UUID值可以找到相应的接口,这里使用Rpcview进行查看:

其实我们在服务中也可以看到appinfo的服务,对应的描述和UAC的实现非常类似

这里得到了对应接口的DLL为appinfo.dll,我们拖入到IDA进行分析该DLL:
我们发现在该dll中的导出函数多以使用RPCRT4较多:

RPC Functions(Remote Procedure Call)使得一个程序可以调用另一计算机的子程序
本地过程调用(LPC,Local Procedure Call)则是在本机进程间进行通讯。

而在之前的了解中已经知道在UAC验证中是使用AiLaunchAdminProcess这个API实现的,根据接口地址找到一个对应函数RAiLaunchAdminProcess,在该函数的实现中找到了AiLaunchAdminProcess:

其中封装了CreateProcessAsUser函数来进行提权操作,因此其实在整个UAC提升权限的过程中是appinfo服务完成了提升权限的处理

3.2 定位可利用的BypassUAC

到这里当然还没有结束,我们知道在UAC过程中会弹出认证框,因此我们还需要对整个弹窗流程进行分析:
弹窗的整体流程是
RAiLaunchAdminProcess -> AiCheckLUA -> AiLaunchConsentUI
其中根据名称很容易想到AiLaunchConsentUI应该就是实现弹窗的过程,因此我们再继续跟进该函数进行分析:

swprintf_s(Buffer, 0x3Dui64, L"consent.exe %u %u %p", CurrentProcessId, phkResult, a2);

然后调用AiLaunchProcess来启动consent.exe,也就是真正绘制uac窗口的程序:

由此我们得知consent.exe程序是用来绘制uac窗口的程序,因此将该程序进行逆向分析,通过分析解决如下问题

  • 在consent绘制的uac窗口上,我们可以看到要进行权限提升的程序的路径,命令行等等相关信息,consent是如何获取这些信息的?


consent的命令行中传入了父进程的pid(appinfo服务的进程pid),一个结构体长度以及一个指向结构体的指针,随后consent调用NtReadVirtualMemory从父进程的内存中读取结构体的内容,这个结构体中就包含了需要特权提升的进程信息。

  • consent程序如何将用户的操作反馈回给appinfo服务进程?

同样通过调用NtWriteVirtualMemory写内存的方式从而传递到appinfo服务的内存当中

通过分析找到了决定是否弹窗的关键函数为:

跟进发现里面对COM组件进行了判断,判断依据主要是COM组件的Elevation属性:

继续返回到appinfo.dll中查看逻辑,我们已经知道BypassUAC最主要也就是两种方式:
1、各类UAC白名单程序的DLL劫持(Dll Hijack)
2、各类提升权限的COM接口利用(Elevated COM interface)

找到了对应判断第一种的函数AiIsEXESafeToAutoApprove:

继续跟进该函数:

此时得到了官方支持的具有autoElevate的可执行文件名单,也就是UAC白名单

3.3 UAC是如何进行权限提升的

谈到这里,可能还是得回到consent程序中来看,其实整个UAC的过程就是appinfo和consent横跳的过程,接CuiCheckElevationAutoApprovalMedium判断完成之后通过LABLE_78调用NtQueryInformationToken来获取一个权限提升的令牌

不过在此之前,判断是通过CuiGetTokenForApp拿到已经是高权限的TokenHandle,由于逆向水平不高,就没有继续跟进该函数进一步底层分析

之后通过NtDuplicateObjectNtWriteVirtualMemory将提升权限后的令牌写回至appinfo服务进程中

至此,UAC的整个过程应该也就差不多完成,最后来一个整体的总结

请求进程将要请求的进程cmdline和进程路径通过LPC接口传递给appinfo的RAiLuanchAdminProcess函数,该函数首先验证路径是否在白名单中,并将结果传递给consent.exe进程,该进程验证被请求的进程签名以及发起者的权限是否符合要求,然后决定是否弹出UAC框让用户进行确认。这个UAC框会创建新的安全桌面,屏蔽之前的界面。同时这个UAC框进程是SYSTEM权限进程,其他普通进程也无法和其进行通信交互。用户确认之后,会调用CreateProcessAsUser函数以管理员权限启动请求的进程。

3.4 ICMLuaUtil方式提权

COM提升名称(COM Elevation Moniker)技术允许运行在用户帐户控制(UAC)下的应用程序用提升权限的方法来激活COM类,以此提升COM接口权限。

我们可以从UACME项目来查看对应的利用方式,该组件对应项目中实现的具体函数名称为:ucmCMLuaUtilShellExecMethod

UACME项目到目前为止总结了60多种绕过UAC的方式,并且列出具备auto-elevate能力的UAC白名单程序或接口。

该函数的源码如下:

NTSTATUS ucmCMLuaUtilShellExecMethod(
    _In_ LPWSTR lpszExecutable
)
{
    NTSTATUS    MethodResult = STATUS_ACCESS_DENIED;
    HRESULT     r = E_FAIL, hr_init;
    BOOL        bApprove = FALSE;
    ICMLuaUtil* CMLuaUtil = NULL;

    hr_init = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

    do {

        //
        // Potential fix check.
        //
        if (supIsConsentApprovedInterface(T_CLSID_CMSTPLUA, &bApprove)) {
            if (bApprove == FALSE) {
                MethodResult = STATUS_NOINTERFACE;
                break;
            }
        }

        r = ucmAllocateElevatedObject(
            T_CLSID_CMSTPLUA,
            &IID_ICMLuaUtil,
            CLSCTX_LOCAL_SERVER,
            (void**)&CMLuaUtil);

        if (r != S_OK)
            break;

        if (CMLuaUtil == NULL) {
            r = E_OUTOFMEMORY;
            break;
        }

        r = CMLuaUtil->lpVtbl->ShellExec(CMLuaUtil,
            lpszExecutable,
            NULL,
            NULL,
            SEE_MASK_DEFAULT,
            SW_SHOW);

        if (SUCCEEDED(r))
            MethodResult = STATUS_SUCCESS;

    } while (FALSE);

    if (CMLuaUtil != NULL) {
        CMLuaUtil->lpVtbl->Release(CMLuaUtil);
    }

    if (hr_init == S_OK)
        CoUninitialize();

    return MethodResult;
}

结合源码可知这里利用的是CMSTPLUA组件ICMLuaUtil接口,来搜索对应的CLSID,对应{3E5FC7F9-9A51-4367-9063-A120244FBEC7}


在前文我们已经了解UAC的整个过程,知道在consent程序会调用CuiIsCOMClassAutoApprovable函数判断是否存在自动权限提升的COM组件,其中判断的标志就是搜寻elevation属性中是否为auto Approval

而该组件是具有该属性的,因此如果想要利用COM组件提权,必须满足的条件之一就是Elevation属性中的Enabled跟Auto Approval为True

这里我们使用的是ICMLuaUtil接口,因此继续分析cmlua.dll

我们主要来看该DLL的虚函数表是否有能够利用的函数

函数原型如下:

__int64 __fastcall CCMLuaUtil::ShellExec(
        CCMLuaUtil *this,
        const unsigned __int16 *a2,
        const unsigned __int16 *a3,
        const unsigned __int16 *a4,
        unsigned int a5,
        unsigned int a6)
    /*    
    pExecInfo.lpFile = a2;
    pExecInfo.lpParameters = a3;
    pExecInfo.lpDirectory = a4;
    pExecInfo.fMask = a5;
    pExecInfo.nShow = a6;
    */

因此当我们创建并实例化该接口时,调用接口的ShellExec方法创建指定进程从而BypassUAC

这里我们通过C#写了一个利用该组件提权的程序,其核心代码如下:

当编译完成运行时却发现程序依然还是会弹出UAC框进行验证,这是因为如果执行COM提升名称(COM Elevation Moniker)代码的程序身份是不可信的,则会触发UAC弹窗,这里我们的程序并没有经过验证

默认能够绕过UAC的文件或者程序需要满足如下三个条件:

  • 程序配置为自动提升权限,以管理员权限执行
  • 程序包含签名
  • 从受信任的目录(c:\windows\system32)执行

因此我们要利用系统的可信进程去进行调用,可以选择的有rundll32.exe、explorer.exe等,只需要把创建COM组件的代码以及执行你想执行的命令的代码,放到可信任进程里面去执行,这样就可以BypassUAC。

对应的第一种方式就是生成DLL文件,将实现都放在DLLmain中,调用rundll32.exe或者cmstp.exe来执行该DLL文件,不过考虑到会有文件落地,并且rundll32.exe本身已经被很多AV监控,因此该方法不是首选方法。

第二种方法引出另一个技术,叫MasqueradePEB

进程的信息,包括命令行参数、图像位置、加载的模块等存储在进程控制块中,并且该结构是可以在用户空间访问和修改的,通过修改PEB进程控制块来欺骗系统认为该进程是一个合法进程

注意在为自己的进程修改进程控制块过程中不需要SeDebugPrivilege权限

而UAC在判断系统进程是否可信,判断依据是PEB结构,因此在使用COM组件提权之前我们将进程信息伪装成可信程序,例如c:\windows\explorer.exe等就能够BypassUAC
当调用PEBMasq.MasqueradePEB伪装成explorer.exe

我们执行该程序发现现在并不会弹出UAC框并且成功获得管理员权限的cmd进程:

3.5 利用白名单程序实现BypassUAC

利用白名单程序的本质实际上是劫持注册表,这种方法主要是通过寻找autoElevated属性为true的程序,修改其注册表\shell\open\command的值,改成我们想要执行的paylaod,在该值中指明的字段会在这类程序运行时自动执行,类似于默认程序打开,当你以后运行该程序时,这个command命令都会自动执行。

这里我们以Windows 10中Fodhelper.exe为例,该程序是一个autoelevate程序

使用ProcessMonitor监控打开该进程后该进程的相关操作:

启动fodhelper.exe时,会在注册表中执行以下检查:

HKCU:\Software\Classes\ms-settings\shell\open\command
HKCU:\Software\Classes\ms-settings\shell\open\command\DelegateExecute
HKCU:\Software\Classes\ms-settings\shell\open\command\(default)

由于这些注册表项不存在,用户可以在注册表中创建此结构,以便操纵fodhelper以绕过用户帐户控制 (UAC) 执行具有更高权限的命令。

当再次运行fodhelper后将执行命令并打开提升的 PowerShell 会话:

但是在利用过程中值得注意的是需要确保DelegateExecute项是存在的,否则也无法成功BypassUAC

如何寻找具有autoPriv的白名单
其实这一类程序都满足一定的条件:

  1. 可执行文件必须经过Windows Publisher的数字签名,Windows Publisher是用于对Windows附带的所有代码进行签名的证书(仅由 Microsoft进行签名是不够的,因此Windows未附带的Microsoft软件不包括在内);
  2. 可执行文件必须位于其中一个为数不多的“安全”目录中。安全目录是指标准用户无法修改的目录,并且它们包括 %SystemRoot%\System32(例如,\Windows\System32)及其大多数子目录、%SystemRoot%\Ehome,以及 %ProgramFiles%下的少许目录(其中包括Windows DefenderWindows notepad)。

这里可以直接使用命令:

strings.exe  -s *.exe | findstr /i "autoElevate" 

来获取manifest文件中申明autoElevate的程序,也可以通过UACME项目中uacinfo.exe来获得相关程序信息

4. 从服务账户到SYSTEM

之所以需要单独划分为从服务账户到SYSTEM权限,主要是因为在服务账户中通常具有SeImpersonatePrivilege权限,即使是IIS、Sqlserver用户等也同样具有该权限,在渗透测试中从web端或者数据库来打点也是极为常见的,因此如何进行存在SeImpersonatePrivilege权限的提权也是一直研究的热点。

以下用户拥有SeImpersonatePrivilege权限:

  • 本地管理员账户(不包括管理员组普通账户)和本地服务帐户
  • 由SCM启动的服务

服务账户在Windows权限模型中本身就拥有很高的权限,所以微软不认为这是一个漏洞

讨论到这里,首先需要介绍的就是目前使用最广泛的Potato系列,其实所有的Potato系列本质上都是relay,如果是远程提权则是relay,用于本地也可以称为是reflection

4.2 HotPotato

2016年1月中旬,来自FoxGlove Security安全团队的breenmachine在博客中介绍了一种被称为Hot Potato的漏洞利用技术。在默认配置下,Hot Potato能够利用Windows操作系统的已知缺陷来获取本地计算机的最高控制权限,受影响的操作系统包括Windows 7/8/10 和 Windows Server 2008/2012

有关详细的原理讲述可以参考原作者的文章,在这里的核心思想是通过NTLM中继(基于HTTP->SMB中继)和NBNS欺骗。

在此之前我们需要知道windows解析域名的顺序是:

Hosts
DNS (cache / server) 
LLMNR
NBNS

1.LLMNR

LLMNR 是一种基于协议域名系统(DNS)数据包的格式,使得两者的IPv4和IPv6的主机进行名称解析为同一本地链路上的主机,因此也称作多播DNS。监听的端口为UDP/5355,支持IPv4和IPv6 ,并且在Linux上也实现了此协议。其解析名称的特点为端到端,IPv4 的广播地址为224.0.0.252,IPv6的广播地址为FF02:0:0:0:0:0:1:3或FF02::1:3

当局域网中的DNS服务器不可用时,DNS客户端会使用LLMNR本地链路多播名称解析来解析本地网段上的主机的名称,直到网络连接恢复正常为止。

LLMNR 进行名称解析的过程为:

1.检查本地 NetBIOS 缓存
2.如果缓存中没有则会像当前子网域发送广播
3.当前子网域的其他主机收到并检查广播包,如果没有主机响应则请求失败

也就是说LLMNR并不需要一个服务器,而是采用广播包的形式,去询问DNS,跟ARP很像,因此也存在类似arp投毒等问题的出现

举个简单的例子:
当我们通过net use尝试去建立一个不存在IPC链接或者是尝试和攻击机建立IPC链接的时候:

当本机在Hosts文件里面没有找到,通过DNS解析失败。就会通过LLMNR协议进行广播,前文已经说过LLMNR的广播地址对应的就是224.0.0.252

2.NBNS

全称是NetBIOS Name Service
NetBIOS 协议进行名称解析的过程如下:

检查本地 NetBIOS 缓存
如果缓存中没有请求的名称且已配置了 WINS 服务器,接下来则会向 WINS 服务器发出请求
如果没有配置 WINS 服务器或 WINS 服务器无响应则会向当前子网域发送广播
如果发送广播后无任何主机响应则会读取本地的 lmhosts 文件

需要注意的是这里和ARP类似,会在本地所有主机进行广播,因此当攻击者相应这条广播消息时,发送方便会认为自己查找的目标就是这个响应者

NBNS包有1个2字节的TXID字段,必须进行请求\响应的匹配,在这里我们假定没有权限进行流量的监听,也就不知道是在哪一个端口进行通信,但由于是通过UDP进行传输,因此可以通过1-65535之间进行泛洪猜测。

前文提到如果网络中有DNS记录,此时就不会用到NBNS协议,作者使用称为UDP端口耗尽的技术来强制系统上的所有DNS查找失败,当请求已经没有可用的UDP资源时,DNS查找失败便会通过NBNS进行广播查询

3.设置假的WPAD服务器

wpad全称是Web Proxy Auto-Discovery Protocol,通过让浏览器自动发现代理服务器,定位代理配置文件PAC(在下文也叫做PAC文件或者wpad.dat),下载编译并运行,最终自动使用代理访问网络。

它在本地网络上搜索名为wpad的计算机以找到该文件。然后执行以下步骤:

1.如果配置了DHCP服务器,则客户端从DHCP服务器中检索wpad.dat文件(如果成功,则执行步骤4)。
2.wpad.corpdomain.com查询被发送到DNS服务器以查找分发Wpad配置的设备。(如果成功,则执行第4步)。
3.发送WPAD的LLMNR或NBNS查询(如果成功,请转到第4步,否则无法使用代理)
4.下载wpad.dat并使用它。

在下面的流量捕获中,机器以广播方式发送NBNS数据包,请求wpad.dat:

在 Windows 中,系统默认会通过访问URLhttp://wpad/wpad.dat自动尝试检测网络代理设置配置,并且适用于某些Windows服务,例如Windows更新

当然是http://wpad/wpad.dat不会存在于所有网络上,因为主机名wpad不一定存在于DNS名称服务器中。然而,正如我们在上面看到的,我们可以使用NBNS欺骗来伪造主机名

凭借欺骗NBNS响应的能力,我们可以将 NBNS 欺骗器定位在127.0.0.1。我们用主机WPAD或WPAD.DOMAIN.TLD的NBNS响应数据包淹没目标机器(我们自己的机器),我们说WPAD主机的IP地址为127.0.0.1。

同时,我们在127.0.0.1本地运行一个HTTP服务器。当它收到对http://wpad/wpad.dat的请求时,它会响应如下:

使得所有流量都通过127.0.0.1上运行的服务器重定向

NTLM中继

NTLM中继是一种众所周知但经常被误解的针对 Windows NTLM 身份验证的攻击。NTLM 协议容易受到中间人攻击。如果攻击者可以欺骗用户尝试使用 NTLM 对其机器进行身份验证,他可以将该身份验证尝试中继到另一台机器

这种攻击的旧版本让受害者尝试使用带有NTLM身份验证的SMB协议向攻击者进行身份验证。然后攻击者会将这些凭据转发回受害者的计算机,并使用类似“psexec”的技术获得远程访问权限。

微软通过使用已经在进行中的挑战来禁止相同协议的NTLM身份验证来修补这个问题。这意味着从一台主机到其自身的SMB->SMB NTLM中继将不再起作用。然而,跨协议攻击,如HTTP->SMB仍然可以中继成功

在Potato漏洞利用中,所有HTTP请求都通过302重定向重定向到 http://localhost/GETHASHESxxxxx,其中xxxxx对应一个唯一标识符。请求http://localhost/GETHASHESxxxxx响应NTLM身份验证的 401 请求。

这样我们就可以将所有NTLM凭据中继到本地SMB侦听器以创建运行用户定义命令的新系统服务。

当有问题的HTTP请求来自高权限帐户时,例如,当它是来自Windows更新服务的请求时,那么将以“NT AUTHORITY\SYSTEM”权限运行

最原始的Potato其本质就是一个NTLM Relay,最后附上一张整体的流程图

局限性

Microsoft通过使用已经在进行中的质询来禁止相同协议的NTLM身份验证来修补此问题 (MS16-075)。这意味着从一台主机到其自身的SMB->SMB NTLM中继将不再起作用。MS16-077 WPAD名称解析将不使用NetBIOS 并且在请求 PAC 文件时不发送凭据,因此WAPD Attack已修补。

4.3 Rotten Potato

相比于Hotpotato利用NTLM Relay中继到SMB,最后调用svcctl.CreateServiceW创建指定的服务,Rotten Potato主要利用远程过程调用(RPC)通过CoGetInstanceFromIStorage进行NTLM中继,在本地协商得到NT AUTHORITY\SYSTEM帐户的安全令牌,在模拟安全令牌后调用CreateProcessWithToken等API来使用指定的令牌来创建进程

“`c#
public static void BootstrapComMarshal()
{
IStorage stg = ComUtils.CreateStorage();
// Use a known local system service COM server, in this cast BITSv1
Guid clsid = new Guid(“4991d34b-80a1-4291-83b6-3328366b9097”);

TestClass c = new TestClass(stg, String.Format(“{0}[{1}]”, “127.0.0.1”, 6666)); // ip and port

MULTI_QI[] qis = new MULTI_QI[1];

qis[0].pIID = ComUtils.IID_IUnknownPtr;
qis[0].pItf = null;
qis[0].hr = 0;

CoGetInstanceFromIStorage(null, ref clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, c, 1,qis);
}

在这样一段代码中,`CoGetInstanceFromIStorage`调用尝试从调用者指定的位置获取指定对象的实例

这里我们告诉COM我们想要一个`BITS`对象的实例,我们想要从 127.0.0.1端口 6666加载它,注意`TestClass`实际上是一个IStorage对象的实例,

因此对应的COM组件会和本地6666端口进行通信,同时监听该端口,如果我们以正确的方式回复,我们就会让以`SYSTEM`帐户运行的COM组件尝试向我们`NTLM`身份验证。

这里作者使用非常巧妙的一点来避免不同版本的Windows RPC报文的问题,即将我们在TCP端口6666上从COM接收到的任何数据包中继回TCP端口`135`上的本地 `Windows RPC 监听器`。由于我们收到的这些数据包是有效RPC对话的一部分,无论我们使用的是什么版本的Windows都会做出适当的反应。然后,我们可以使用从 135上的`Windows RPC`收到的这些数据包作为我们对COM的回复的模板。

来看一下对应Poc产生的数据情况
![](https://md.buptmerak.cn/uploads/upload_dd28d930be5982211c248edc4edfe99d.png)

我们收到的第一个数据包(数据包7)是在端口6666上传入的(来自与本地端口对话的COM)。接下来,我们将相同的数据包(数据包9)中继到`TCP 135`上的 RPC。然后在数据包11中,我们从RPC(TCP 135)收到回复,在数据包13中,我们将该回复中继到COM。

重复上述过程,直到进行NTLM身份验证

#### NTLM中继和令牌协商

![](https://md.buptmerak.cn/uploads/upload_e9f75615975906bfbd233f22aa883387.png)

左边蓝色的是COM组件在TCP端口6666上发送给我们的数据包。右边红色的是我们将使用从这些数据包中提取的数据进行的**Windows API**调用。

为了使用 NTLM 身份验证在本地协商安全令牌,必须首先调用函数`AcquireCredentialsHandle`来获取我们需要的数据结构的句柄。
![](https://md.buptmerak.cn/uploads/upload_6800f0966c7b2a0c79cad956057ab31d.png)

其中`SECPKG_CRED_BOTH`代表着验证传入凭据或使用本地凭据准备传出令牌。此标志启用其他两个标志,`hCred`指向CredHandle结构的指针,用于接收凭证句柄。

接下来,我们调用`AcceptSecurityContext`,该函数的输入将是NTLM类型 1(协商)消息。输出将是NTLM类型(挑战)消息,该消息被发送回尝试进行身份验证的客户端,在本例中为DCOM。

在 RPC 和 COM 之间中继了几个数据包后,最终 COM 将尝试通过发送 NTLM type1(协商)消息来尝试向我们发起 NTLM 身份验证,此时我们将从该数据包中提取的 NTLM type1(协商)消息作为输入传递给`AcquireCredentialsHandle`函数:

![](https://md.buptmerak.cn/uploads/upload_92337dd39b347e180d678e076e63b5ad.png)


之前我们将NTLM type1(协商)数据包转发到端口 135 上的 RPC,现在它会回复一个NTLM的挑战包,但是我们不能简单地将这个数据包转发回 COM,分别看一下135端口RPC产生的挑战包和本地6666端口发送的挑战包:
![](https://md.buptmerak.cn/uploads/upload_87ba09808b12176498d377024ac4fcc2.png)


请注意突出显示的字段“NTLM Server Challenge”及其下方的字段“Reserved”,它们的值不同,前文提到了`AcceptSecurityContext`该调用的输出是一条`NTLM Challenge`,因此在这里我们需要将得到的输出结果进行替换,这样才会使得对`AcceptSecurityContext`的成功调用

当我们将修改后的 NTLM type 2(协商)数据包转发到 COM,其中`Challenge/Reserved`字段与“AcceptSecurityContext”的输出相匹配,COM组件会向我们发送回NTLM type 3(身份验证)数据包,我们使用它来对`AcceptSecurityContext`进行最终调用。
![](https://md.buptmerak.cn/uploads/upload_a1b5200ea38f8d1a7a1eb508c6f57282.png)


最后我们以一张图来总结一下整个Rotten Potato提权的大体流程:
![](https://md.buptmerak.cn/uploads/upload_026d9aa2e0af7028bf022cf2e2ff6070.png)


- 1.使用`CoGetInstanceFromIStorage`API 调用欺骗 RPC 对代理进行身份验证。在此调用中指定了代理 IP/端口。
- 2. RPC 向代理发送NTLM 协商包。
- 3.代理依赖的NTLM协商到RPC在端口135,被用作模板。同时,调用`AcceptSecurityContext`以强制进行本地身份验证。
- 4&5 得到`RPC 135`和`AcceptSecurityContext`的NTLM 挑战包后进行部分替换以匹配本地协商并转发到RPC,也就是到步骤6
- 7&8 RPC对使用`AcceptSecurityContext`得到的挑战包进行相应
- 9 模拟令牌创建进程,实现提权

#### 局限

在[https://decoder.cloud/2018/10/29/no-more-rotten-juicy-potato](https://)提到了关于相关更新的修复,其中主要为两点:

- DCOM 不与我们的本地侦听器交谈,即现在DCOM组件不会和除135端口以外的其他端口进行通信,这意味着无法通过6666端口充当中间人

- 将数据包发送到我们控制下侦听端口 135 的主机,然后将数据转发到我们本地的 COM 侦听器是行不通的。问题是在这种情况下,客户端不会协商本地身份验证。

因此,此技术不适用于 >= Windows 10 1809 和 Windows Server 2019 的版本

### 4.4 Pipepotato(BadPotato)

其实`Pipepotato`也就是`Printspoofer`利用,有关该提权利用方式以及原理介绍可以参考笔者之前的文章[PrintSpoofer提权原理探究](https://www.anquanke.com/post/id/254904),在这里再次对其进行部分引用。

回到前面遇到的问题上,我们知道在Win 10中已经做出了调整,利用IStorage COM组件只允许和135端口进行通信,意味着中间人攻击已经失效,我们无法进行重放,因此Pipepotato作者把目光放到了管道上,而笔者个人认为利用管道模拟的方式更加简洁并且高效
管道可以有两种类型:

- **匿名管道** – 匿名管道通常在父进程和子进程之间传输数据。它们通常用于在子进程与其父进程之间重定向标准输入和输出。
- **命名管道** – 命名管道可以在不相关的进程之间传输数据,前提是管道的权限授予对客户端进程的适当访问权限。

命名管道服务器线程可以调用`ImpersonateNamedPipeClient`函数来假定连接到管道客户端的用户的访问令牌。例如,命名管道服务器可以提供对管道服务器具有特权访问权限的数据库或文件系统的访问。当管道客户端向服务器发送请求时,服务器模拟客户端并尝试访问受保护的数据库。然后系统会根据客户端的安全级别授予或拒绝服务器的访问权限。当服务器完成时,它使用`RevertToSelf`函数恢复其原始安全令牌。

**该模拟级别决定了在模拟客户端服务器可以执行的操作。默认情况下,服务器在 SecurityImpersonation模拟级别进行模拟**。


该利用的核心原理是通过打印机漏洞**强迫运行Spooler服务的任何主机通过Kerberos或者NTLM向攻击者选择的目标发起身份认证请求**,这里可以强迫一个特权进程来访问我们的恶意管道,当我们拥有`SecurityImpersonation`权限时便可以通过模拟管道安全上下文的方式,来模拟该特权进程,得到该特权进程的模拟令牌,通过将其转化为主令牌的方式最终调用`CreateProcessWithToken`等方式来以`SYSTEM`权限运行程序

已有较为完善的工具:
**https://github.com/itm4n/PrintSpoofer**

该手段由于并不是微软承认的一个漏洞,因此实际上还是比较实用和不受限制的,但是由于近年来在PrintNightmare爆发之后,很多企业会选择关闭spoolss服务,因此使得Printerbug失效,从而导致pipePotato的失效

### 4.5 PetitPotam提权

从名字上看该方式并不是传统的Potato,但是从利用手法上,笔者仍然认为其本质也可以通过模拟管道客户端进而实现提权,并且PetitPotam更多用于域横向渗透中,同时也能够用于本地提权(LPE)

MS-EFSR里面有个函数`EfsRpcOpenFileRaw`

long EfsRpcOpenFileRaw(
[in] handle_t binding_h,
[out] PEXIMPORT_CONTEXT_HANDLE* hContext,
[in, string] wchar_t* FileName,
[in] long Flags
);

他的作用是打开服务器上的加密对象以进行备份或还原,服务器上的加密对象由FileName参数指定,FileName的类型是UncPath。

当指定格式为`\\IP\C$`的时候,lsass.exe服务就会去访问`\\IP\pipe\srvsrv`

![](https://md.buptmerak.cn/uploads/upload_d73e04689185357ff45d870853ebc123.png)


有关于该协议的完成IDL文件:
[https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-efsr/4a25b8e1-fd90-41b6-9301-62ed71334436](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-efsr/4a25b8e1-fd90-41b6-9301-62ed71334436)

这里是通过调用RPC接口的方式实现的

Microsoft 远程过程调用 (RPC) 定义了一种用于创建分布式客户端/服务器程序的强大技术。RPC 运行时存根和库管理大多数与网络协议和通信相关的进程。这使您能够专注于应用程序的细节而不是网络的细节。

有关RPC编程以及介绍将会放在下一部分漏洞挖掘的时候进行重点展示,相关内容可以参考MSDN:
[https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page](https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page)


MS-EFSR的调用有`\pipe\lsarpc`和`\pipe\efsrpc`两种方法,其中

**\pipe\lsarpc**的服务器接口必须是UUID`[c681d488-d850-11d0-8c52-00c04fd90f7e]`

**\pipe\efsrpc**的服务器接口必须是UUID `[df1941c5-fe89-4e79-bf10-463657acf44d]`

但是通过`efsrpc`的方式并不支持,也就是说无法以普通用户的身份来调用该接口的方法,而`\pipe\lsarpc`则可以,因此在构造过程中我们通常使用`\pipe\lsarpc`接口

#### 结合管道实现提权

类似于printspoofer,前文我们提到当指定格式为`\\IP\C$`的时候,lsass.exe服务就会去访问`\\IP\pipe\srvsrv`,而这个管道我们是无法创建的,因此我们同样需要使用UNC路径的一些`trick`,如果能够使得`lsass.exe`访问我们自己创建的恶意管道,在拥有`SeImpersonatePrivilege`的情况下,便可以模拟管道客户端安全上下文的方式,以`lsass.exe`所属用户的权限创建进程,而该进程是系统权限(SYSTEM)

事实上,我们确实可以通过构造类似这样的UNC路径欺骗系统:

\\127.0.0.1/pipe/crispr\C$\x

![](https://md.buptmerak.cn/uploads/upload_63d64c3af3613d8ac41495cf1f5c4cda.png)

可以看到,通过利用特定UNC路径成功欺骗`lsass.exe`进程去连接指定的管道,因此为了实现本地提权,我们只需要创建这样一个特定管道去模拟RPC客户端安全上下文即可

鉴于目前利用PetitPotam实现本地提权的工具较少,笔者实现了利用`PetitPotam`进行本地提权:
[https://github.com/crisprss/PetitPotam](https://github.com/crisprss/PetitPotam)

遍历了可以利用的函数,最后发现如下几种接口函数用于本地提权:

1.EfsRpcOpenFileRaw (fixed with CVE-2021-36942)
2: EfsRpcEncryptFileSrv_Downlevel
3: EfsRpcDecryptFileSrv_Downlevel
4: EfsRpcQueryUsersOnFile_Downlevel
5: EfsRpcQueryRecoveryAgents_Downlevel
6: EfsRpcRemoveUsersFromFile_Downlevel
7: EfsRpcAddUsersToFile_Downlevel

![](https://md.buptmerak.cn/uploads/upload_e06ee88cca6c424845abbcaf8680cd24.png)

不过没有使用`CreateProcessAsUserW`因此在创建进程过程中并不会以`interactive`的方式
![](https://md.byr.moe/uploads/upload_ea5a7f8aafc6f9c547b487d34d1e2888.png)
有兴趣的师傅可以对此进行优化,笔者在后期也会进行相关优化

## 5.从常规用户到SYSTEM

在一般的提权文章中很少会有关于从普通用户提权到SYSTEM,究其原因笔者认为,一方面是因为类似这样的提权比较频繁的出现在win sys内核漏洞中,另一方面是因为利用协议层或者逻辑层面而言去利用进而提权也是非常巧妙的,很具技巧性和机遇性

而在很多提权的思路中,利用与文件相关的操作进行提权的手法是笔者认为非常巧妙的,在这里笔者主要通过对于一个提权手法的展现来探讨一些这些**非常规提权手段**

### 5.1 CVE-2020-0787 

该漏洞的原因主要是出于Windows 后台智能传输服务
![](https://md.buptmerak.cn/uploads/upload_f032a5dccc6d3ff31355d54aa0ad3cf1.png)


>程序员和系统管理员使用后台智能传输服务 (BITS) 从 HTTP Web 服务器和 SMB 文件共享下载文件或将文件上传到 HTTP Web 服务器。BITS 会考虑传输成本和网络使用情况,尽量减少对用户前台工作的影响。BITS 还处理网络中断、暂停和自动恢复传输,即使在重新启动后也是如此。BITS 包括用于创建和管理传输的 PowerShell cmdlet 以及 BitsAdmin 命令行实用程序。

后台智能传输服务公开了几个 COM 对象,我们可以使用`OleViewDotNet`列出这些对象以及相关信息

![](https://md.buptmerak.cn/uploads/upload_26ad42b55956de9cb9d3416d599629d3.png)

在这里,我们将重点介绍后台智能传输 (BIT) 控制类1.0和`Legacy BIT 控制类`及其主要接口,分别为`IBackgroundCopyManager和IBackgroundCopyMgr`
![](https://md.buptmerak.cn/uploads/upload_81327072488012ab0060b834664f6c38.png)


MSDN中给出了关于BITS的使用
[https://docs.microsoft.com/en-us/windows/win32/bits/bits-dot-net](https://)
以下步骤显示了如何使用后台智能传输服务 (BITS)接口执行文件传输

连接到 BITS 服务
创建转移作业
将文件添加到作业
开始工作
确定 BITS 是否成功传输文件
完成工作

在`BIT Legacy Background Intelligent Transfer Control Class`中我们可以通过:

CoCreateInstance(CLSID_69AD4AEE-51BE-439B-A92C-86AE490E8B30) -> IBackgroundCopyQMgr*
|__ IBackgroundCopyQMgr::CreateGroup() -> IBackgroundCopyGroup*
|__ IBackgroundCopyGroup::CreateJob() -> IBackgroundCopyJob1*
|__ IBackgroundCopyJob1::AddFiles(FILESETINFO)
|__ IBackgroundCopyJob1::Resume()
|__ IBackgroundCopyJob1::Complete()

- 创建BIT 控制类的实例通过`CoCreateInstance()`得到请求指向`IBackgroundCopyQMgr`接口的指针
- 利用该指针创建一个组`IBackgroundCopyQMgr::CreateGroup()`
- 通过调用创建一个作业`IBackgroundCopyGroup::CreateJob()`以获取指向`IBackgroundCopyJob1`接口的指针
- 通过调用将文件添加到作业中,该调IBackgroundCopyJob1::AddFiles()
- 最后,由于作业是SUSPENDED状态,必须在作业状态为`TRANSFERRED`时调用IBackgroundCopyJob1::Resume()和IBackgroundCopyJob1::Complete()

而重点就在`IBackgroundCopyGroup`的文档差异上,在[MSDN提供的参考文档中](https://docs.microsoft.com/en-us/windows/win32/api/qmgr/nn-qmgr-ibackgroundcopygroup)提供了13种方法,而使用`OleViewDotNet`查看时却发现存在15中方法:
![](https://md.buptmerak.cn/uploads/upload_76f3eeb7e1ec3f76311a9f59dd6f71e5.png)

我们知道相应的头文件是`Qmgr.h`,因此我们直接定位到头文件中进行查看:
![](https://md.buptmerak.cn/uploads/upload_d77dc3622297988d4c94e526518f59f2.png)

我们可以看到两个未公开的方法:`QueryNewJobInterface()和SetNotificationPointer()`

作者进行了如下的操作,其实就是通过`QueryNewJobInterface`指向的这个未知接口来替换之前`IBackgroundCopyJob1`完成Resume和Complete操作
![](https://md.buptmerak.cn/uploads/upload_2ffd9a8587ce93e2c2bc2fd6356ee7b8.png)

从作者给出的Poc上也能清晰地知道其操作,对于Complete的实现也是同样
![](https://md.buptmerak.cn/uploads/upload_73568ca92ed19a3713ad199d824d62ea.png)


然后使用`Process Monitor`来观察,看到该服务在目标目录中创建了一个 TMP 文件,并尝试打开作为参数给出的本地文件
![](https://md.buptmerak.cn/uploads/upload_8e8cda7d25495738d639011cfe803f97.png)

一旦我们调用该`Resume()`函数,该服务就会开始读取目标文件`\\127.0.0.1\C$\Windows\System32\drivers\etc\hosts`并将其内容写入TMP文件`C:\Temp\BITF046.tmp`
最后,TMP 文件被重命名为test.txt调用MoveFileEx(),并且此时并不会模拟当前用户,而是以服务用户本身的权限进行操作,即SYSTEM,这是漏洞所在
![](https://md.buptmerak.cn/uploads/upload_82c69ea5586a29f1d7fe2814859e6aec.png)

然而一旦涉及到文件写和读的操作,便一定涉及到文件符号链接和挂载的问题,而这些问题就有可能导致我们能进行任意文件的读和写

#### 文件操作

James Forshaw(@tiraniddo)在NTFS文件系统和Windows操作系统内部完成了开创性工作,他在发表了许多相关的文章(`symlinks、hardlinks、NTPathConversion、DirCreate2FileRead、FileWrite2EoP、AccessModeMismatch`),并在Infiltrate和SyScan。他提出了几种滥用Windows文件系统和路径解析功能的技术(大致总结如下),并在开源的[symboliclink-testing-tools](https://github.com/googleprojectzero/symboliclink-testing-tools)工具包和NtApiDotNet库中实现了这些技术。

在这里介绍两种与该漏洞相关的文件操作

#### 文件锁

oplock是一种可以放置在文件上的锁,当其他进程想要访问该文件时,它可以被告知—同时延迟这些进程的访问,以便锁定进程可以在解除锁之前让文件处于适当的状态。oplocks最初是为通过SMB缓存客户端-服务器文件访问而设计的,可以通过调用文件句柄上的特定控制代码设置oplock。

你可以通过锁定一个试图打开的文件或目录来轻松地赢得与进程的竞争。

SetOpLock工具可以让你创建这些锁,并阻止对文件或目录的访问,直到你按回车键释放锁。它让你在读、写和放行oplock之间进行选择。
![](https://md.buptmerak.cn/uploads/upload_b4946dc4acf0b9e307cb74fb1f853976.png)

可以看到这个文件已经被击中,并且只有我们释放后才有机会读取文件的内容:
![](https://md.buptmerak.cn/uploads/upload_a4c50cfa89e1df72396d563c8061ef3c.png)


#### 对象符号链接管理器

虽然NTFS确实提供了文件系统的符号链接,但在Windows上,无权限的用户不能在文件系统上创建符号链接:它需要`SeCreateSymbolicLinkPrivilege`,默认情况下,只有管理员才能获得该权限。

不过,无权限的用户可以在Windows的 “对象管理器 “中创建符号链接,顾名思义,它可以管理进程、部分和文件等对象。对象管理器使用符号链接,例如驱动器字母和命名管道与相应设备相关联。用户可以在可写对象目录中创建对象符号链接,如`\RPC CONTROL\`,这些符号链接可以指向任意路径—包括文件系统中的路径—无论该路径当前是否存在。

当与NTFS连接点结合时,对象符号链接会做一些有趣的事情。事实上,一个无权限的用户可以将一个挂载点与该目录中的对象管理器符号连接起来,解析到`\RPC CONTROL\目录`

因此我们可以通过这种方式使得`C:\tmp\b2\test1.txt解析为C:\tmp\b3\test.txt`

![](https://md.buptmerak.cn/uploads/upload_68c4c0ea67acd67fa640e91e0b75ac5d.png)


>需要注意的是我们必须对这些文件拥有可写权限,并且第一个文件需要是空文件,而第二个文件要是存在的文件

回到上文,既然我们知道了当调用`Resume()`函数,该服务就会开始读取目标文件并且写入TMP文件,那如何利用有关文件的操作来进行任意文件写入?

#### 如何利用

这里直接介绍,这个想法是为服务提供一个文件夹的路径,该文件夹最初将用作另一个“物理”目录的连接点。我们使用本地文件创建一个新作业以“下载”并在 TMP 文件上设置 Oplock。恢复作业后,该服务将开始写入TMP文件,同时模拟RPC客户端并将命中 Oplock。然后我们需要做的就是将挂载点切换到对象目录并创建两个符号链接。TMP文件将指向我们拥有的任何文件,“本地”文件将指向文件System32夹中的新 DLL 文件。最后,在释放 Oplock 后,服务将继续写入原始 TMP 文件,但它将通过我们的两个符号链接执行最后的移动操作。

**1.首先我们创建如下的目录结构**:

C:\workspace
|__ bait
|__ mountpoint
|__ FakeDll.dll

mountpoint目录的目的是从连接到bait目录切换到连接到`RPC Control对象`目录。FakeDll.dll是我们想要移动到受限位置的文件,例如`C:\Windows\System32\`

2.**创建挂载点**
从`C:\workspace\mountpoint`创建一个挂载点到`C:\workspace\bait`,之所以创建这个挂载点是由于我们要确定tmp文件的确切名称,切换挂载点后这样可以在这个文件夹(bait)当中找到名为 BIT*.tmp 的文件。

3.**创建一个新的工作**
我们将使用`Legacy Control Class`提供的接口来创建具有以下参数的新作业。

Target URL: \127.0.0.1\C$\Windows\System32\drivers\etc\hosts
Local file: C:\workspace\mountpoint\test.txt

由于之前创建的结点,本地文件的真实路径将是`C:\workspace\bait\test.txt`

4.**找到TMP文件并设置一个Oplock**
将文件添加到作业队列时,该服务会立即创建一个 TMP 文件。由于它有一个“随机”名称,我们必须列出目录的内容bait才能找到它。在这里,我们应该找到一个像BIT1337.tmp. 一旦我们有了名字,我们就可以在文件上设置一个Oplock。

5.**job继续,等待Oplock**
一旦作业恢复,服务将打开TMP文件进行写入并触发 Oplock。这种技术允许我们暂停操作

6.此时切换挂载点
在这一步之前:

TMP file = C:\workspace\mountpoint\BIT1337.tmp -> C:\workspace\bait\BIT1337.tmp
Local file = C:\workspace\mountpoint\test.txt -> C:\workspace\bait\test.txt

我们切换挂载点并创建符号链接,在切换之前首先

- 删除了mountpoint文件夹下面的挂载点 ( ReparsePoint::DeleteMountPoint 删除了挂载点)
- 重新将mountpoint挂载到 \RPC Control 上 ( ReparsePoint::CreateMountPoint 创建了新的挂载点)

C:\workspace\mountpoint -> \RPC Control
Symlink #1: \RPC Control\BIT1337.tmp -> C:\workspace\FakeDll.dll
Symlink #2: \RPC Control\test.txt -> C:\Windows\System32\FakeDll.dll

在这一步之后:

TMP file = C:\workspace\mountpoint\BIT1337.tmp -> C:\workspace\FakeDll.dll
Local file = C:\workspace\mountpoint\test.txt -> C:\Windows\System32\FakeDll.dll

“`

因此本来该服务是将C:\workspace\mountpoint\BIT1337.tmp调用MoveFileEx()函数重命名为C:\workspace\mountpoint\test.txt,而由于重新挂载并且创建符号链接,服务其实是将C:\workspace\FakeDll.dll调用MoveFileEx()重命名为C:\Windows\System32\FakeDll.dll,因此,我们的 DLL 将被移动到该System32文件夹中。

进行本地提权

最终使用了UsoDllLoader获得SYSTEMshell,这是作者发现的一个基于DLL劫持的方式进行的本地提权,在Windows10之后,微软提供了一种名叫Update Session Orchestrator的服务。通过这个服务能够作为一个普通用户去用COM和系统服务通信,并开始一个”update scan” 去进行更新,作者发现usoclient.exe可以实现更新的目的,并且他在开始更新的时候还会尝试去加载一个不存在的DLLwindowscoredeviceinfo.dll,因此我们利用任意文件写入的方式,可以写入我们编译好的恶意DLL到C:\Windows\System32\windowscoredeviceinfo.dll中实现提权。

详细思路和原理可以参考作者的文章:
https://itm4n.github.io/usodllloader-part1/
https://itm4n.github.io/usodllloader-part2/

6.总结

由于Windows本地提权的方式太过繁杂,笔者在这里主要以介绍近年国内外最为火热和范围最广的提权方式,并且尝试从不同角度来探讨提权的多样性,从NTLM Relay、RPC接口方法、管道模拟客户端、BypassUAC、以及利用系统缺陷导致以系统权限实现文件的相关读写来进行提权,如有不当之处还请指正,在下一篇文章中笔者将通过披露最近挖掘的相关提权漏洞来分享漏洞挖掘经历,不过提权基于named pipe,略显鸡肋,在此只是抛砖引玉分享一些思路供大家参考


相关实现:
https://github.com/crisprss/PetitPotam
参考文章:
https://itm4n.github.io/cve-2020-0787-windows-bits-eop/
https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html
https://foxglovesecurity.com/2016/09/26/rotten-potato-privilege-escalation-from-service-accounts-to-system/
https://www.sentinelone.com/labs/relaying-potatoes-another-unexpected-privilege-escalation-vulnerability-in-windows-rpc-protocol/
https://troopers.de/downloads/troopers19/TROOPERS19_AD_Abusing_privileged_file_operations.pdf
https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/local-accounts
https://decoder.cloud/2020/05/04/from-network-service-to-system/

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇