2021 年 6 月 29 日,一个非常严重的 Windows 打印服务漏洞作为 0day 被曝光,漏洞基础评分 8.8 分,发布于 GitHub 上(现已删除)。 该漏洞就是著名的 PrintNightmare: CVE-2021-34527,危险程度甚至高于永恒之蓝。
34527 漏洞几乎作用于 Windows 7、Windows Server 2008 之后的所有版本,详细信息参见 [官方网站] 。
从危害角度来说,攻击者可以使用普通用户认证,以管理员权限远程执行任意代码。 就漏洞利用难度而言,该漏洞非常容易利用,因此危害极大。
从漏洞特征来说,34527 漏洞基于漏洞 CVE-2021-1675。 而 1675 漏洞是一个本地提权和远程代码执行漏洞,它和 34527 漏洞有着非常相似的地方。
在了解漏洞的工作原理之前,我们应该对 Windows 打印后台处理程序的体系架构有个大致的了解,这样能够方便我们理清漏洞涉及的各个模块之间的关系。
spooler 体系架构可以用 图 1 来表示:
具体来说,后台打印程序用于管理打印任务,它由以下部件组成:
- winspool.drv
-
提供给用户的动态链接库文件。 该文件定义了 spooler 相关的 Win32 API 供用户调用。 里面的 API 均采用远程过程调用的形式来获取服务。
- spoolsv.exe
-
spoolsv.exe 担当体系中服务器的角色,作为第一个处理 API 调用的程序。 如此设计是为了让 print spooler 既能处理本地打印作业,又能无差别处理远程打印作业。
- spoolsv.dll
-
路由程序。 它将 spoolsv.exe 接收的打印请求,向各个打印提供程序发起,并决定由哪个打印提供程序最终处理该请求。 它的作用就是区分打印任务是远程任务还是本地任务。 在远程机器上它就会将任务固定分配给本地打印提供程序。
- localspl.dll
-
本地打印提供程序。 打印提供程序主要任务是解决打印任务管理的需求,绝大多数的 API 都是在这个模块内实现的。
根据上述理论来继续研究,举个例子,当我调用 AddPrinterDriverEx 函数时(CVE-2021-1675),会经过以下流程:
首先该函数实际上是个宏,会根据本地的编译环境选择 Unicode 版本(W)或 Ansi 版本(A),如 图 2:
但不管是宽字符还是窄字符版本,其实结果都没差,因为 Windows 内核的字符串是使用 Unicode 编码的,所以最终 Ansi 版本的调用都会转换成 Unicode 版本的调用,如 图 3:
当 Ansi 函数的参数均转换成 Unicode 版本后,就调用了一个函数(图 4):
而该函数实际上就是 Unicode 版本的 AddPrinterDriverEx(图 5):
进入 Unicode 版本的函数内部后,首先会根据 Level 的数值对函数参数类型做一个选择:
该漏洞中我们会设置 Level 为 2,也就是选择参数 pDriverInfo 的类型为 DRIVER_INFO_2 结构体。 然后接下来 Windows 就会对函数参数做一个处理,处理完成后会通过远程过程调用继续处理该 API:
微软的远程过程调用机制基于 DCE 标准进行构建。 通俗来解释,远程过程调用就是在远程系统上运行进程,这些进程都是程序员或者系统预先定义好的。
RPC 的具体做法就是将想要远程调用的函数序列化,通过网络将它传送至远程系统上,远程系统再将之反序列化,并执行之。 在微软构建的体系中,TCP/IP 和 SMB 是通常被选择来传输 RPC 调用的协议。
想要使用 MSRPC,首先得定义调用的函数的 IDL 接口描述,然后使用 MIDL 工具生成客户端和服务端相应的序列化程序 stub。 而对一些 Win32 API 来说,它本身就定义好了服务端 stub,我们就可以只生成并使用客户端 stub 就够了。
MSRPC 使用 UUID 来标识某一类型的协议,如 MS-RPRN 就用来描述远程打印协议,所有跟远程打印相关的函数均属于该协议的一部分,MSPRC 使用 UUID 12345678-1234-ABCD-EF00-0123456789AB
来标识该协议(图 6):
接着,可以继续在此连接基础上,使用 operator number 来标识协议内的函数,以此来远程调用,比如 AddPrinterDriverEx 就是使用 89 来标识自己(图 7):
在使用 MSRPC 的过程中,有两点需要值得注意:
Important
|
As you can see in your output, the scripts are trying to connect to port 135 (endpoint mapper) in order to get the TCP/IP port where the DCOM endpoint is listening (that is a dynamic port).
— SecureAuthCorp/impacket issue #412
|
从 图 8 中可以看出,spoolsv.exe 会调用这些函数,而从函数内部分析来看,该模块内除了初始化外并没有完成什么操作。 最后,该模块会调用 pLocalProvidor 指向的函数,也就是 localspl.dll 模块内的函数 LocalAddPrinterDriverEx。 localspl 作为一个本地打印提供程序,确实是实现 API 功能的模块。
首先 图 9 说明了该模块会验证 spooler 是否正常运行,然后就跳转到函数 SplAddPrinterDriverEx。
图 10 函数内部是判断 AddPrinterDriverEx 函数能否执行成功的重要位置。 前半部分不用看,因为 WPP 是跟日志相关的技术,暂且跳过。
后半部分微软定义了一个变量 v12,是个标志位来判断函数是继续执行还是直接退出。
从 图 11 可以看出,继续执行的条件有两种:一是 v12 为 0 也就是 bittest 判断成功,二是 Validate 成功。 而 Validate 是对权限的校验,没有办法很好地绕过。 里面有一个 API 是 OpenProcessToken,表示需要在接下来的进程中提升权限,如果不是管理员身份是做不到这点的。
所以想要继续执行就只能绕过 bittest 的判断了,而被判断的数 a4——函数的第 4 个参数,就是在官网中有着说明的参数 [dwFileCopyFlags] :
Name/value | Description |
---|---|
APD_STRICT_UPGRADE 0x00000001 |
Add the replacement printer driver only if none of the files of the replacement driver are older than any corresponding files of the currently installed driver. |
APD_STRICT_DOWNGRADE 0x00000002 |
Add the replacement printer driver only if none of the files of the currently installed driver are older than any corresponding files of the replacement driver. |
APD_COPY_ALL_FILES 0x00000004 |
Add the printer driver and copy all the files in the driver directory. File time stamps MUST be ignored. |
APD_COPY_NEW_FILES 0x00000008 |
Add the printer driver and copy the files in the driver directory that are newer than any of the corresponding files that are currently in use. |
APD_COPY_FROM_DIRECTORY 0x00000010 |
Add the printer driver by using the fully qualified file names that are specified in the \_DRIVER_INFO_6 structure. If this flag is specified, one of the other copy flags in this bit field MUST be specified. |
APD_DONT_COPY_FILES_TO_CLUSTER 0x00001000 |
When adding a printer driver to a print server cluster, do not copy the driver files to the shared cluster disk. |
APD_COPY_TO_ALL_SPOOLERS 0x00002000 |
Add the printer driver to cluster spooler servers. |
APD_INSTALL_WARNED_DRIVER 0x00008000 |
Add the printer driver, even if it is in the server’s List of Warned Printer Drivers. |
APD_RETURN_BLOCKING_STATUS_CODE 0x00010000 |
Specifies the implementation-specific error code to return if the printer driver is blocked from installation by server policy. |
bittest 16 就是验证变量的第 16 位是否为 1,对应参数的值就是 0x8000(APD_INSTALL_WARNED_DRIVER)。 根据释义也可以看出,该参数的意思是不加验证的添加打印机驱动到服务器上。
据说 1675 被修复之前,该参数还没有出现在官方文档中,这也不难看出漏洞的所在。
当添加打印机驱动这个方法真正执行时,如果选择的是 DRIVER_INFO_2 结构体,则会发生下面几件事:
-
分别打开 DriverFile、ConfigFile 和 DataFile,确认这三个文件是否存在。 其中只有 DataFile 允许是 UNC 路径。
-
如果三个文件均存在,则将它们拷贝至目录 C:\Windows\System32\spool\drivers\x64\3\New 下面,如 图 12、图 13 所示:
-
拷贝至该目录下的原因是要执行相应的文件,3 代表了这个打印机驱动是 v3 类型的驱动。 先将新文件拷贝至 New 目录下面,防止覆盖掉 3 目录下的文件。 如果 3 下面有着同名文件,则把同名文件放到 Old 目录下面做一个备份,再将 New 下面的文件拷贝至 3 下面覆盖文件。 这点我们可以在第二次执行 RpcAddPrinterDriverEx 方法的时候就能观察到:
根据这一机制,我们可以将远程路径的文件保存为本地路径的文件,因为函数参数中驱动文件和配置文件参数都只能是本地路径,只有数据文件参数才能是远程路径。
-
根据 [官方文档],pConfigFile 是设备驱动的配置动态链接库,因此需要加载一次来初始化。 这一点从实际运行情况中也得到了证实。
如此一来,只要编写一个恶意 dll,将恶意代码放到 dll 入口点来执行,就能执行任意代码,并且还是管理员权限。
-
CreateInternalDriverFileArray()函数根据文件操作标志来决定是否检查spool驱动目录。 如果a5 flag被标志为False,驱动加载函数只会检查用户目录中是否包含要拷贝的驱动文件。 否则,函数会尝试到spool驱动目录寻找目标驱动。 这就要求 dwFileCopyFlags 需要同时设置参数 APD_COPY_FROM_DIRECTORY。
Tip
|
总结一下,恶意代码的利用思路为:把恶意 dll 作为 configfile 来初始化,会以管理员权限执行任意代码。 只需要在攻击方的主机开启一个 samba 共享,让被攻击方以 datafile 的形式下载该恶意程序到本机,并执行该程序。 |
该程序基于 docker 构建,可以有效地解决环境依赖问题。
首先使用者下载 compose 文件到自己的目录下,然后在该目录下建立一个 share 的文件夹,作为挂载目录使用。 使用者可以将恶意程序放在 share 文件夹内,该文件夹会作为 samba 路径 smb 共享出去。
接着,使用者在终端输入命令
docker-compose up -d
即可启动集群(一个容器)。 用户可自行进入容器内操作,容器中的环境均已经配置好。
或者也可以直接在终端输入命令
docker-compose run my_cve python main.py -h
来执行命令。
感谢 [cube0x0] 开源的代码供我参考!
2021 年 6 月 8 日,微软对 CVE-2021-1675 漏洞进行了修补,具体的修改如下:
尽管微软在 RpcAddPrinterDriverEx 函数上打了补丁,但是我们依然可以通过 RpcAsyncAddPrinterDriver 远程调用进行绕过。 如 图 24 所示,客户端的该函数是直接对服务端的远程调用:
服务端会先给线程分配空间,然后继续调用,如 图 25 所示:
继续调用的函数中会将参数都压入堆栈中,以线程的方式运行 YAddPrinterDriverEx(图 26):
这样就成功绕过微软对 RpcAddPrinterDriverEx 的补丁了。
也就是说,通过远程调用 RpcAsyncAddPrinterDriver 函数,就能继续以管理员身份执行任意代码。
另外,据 [James Forshaw] 声明,该补丁对 Token 的检验还存在着问题,同时在彻底禁用了 UAC 的机器上该补丁将会无效。 不过我本人对这方面的机制不是很了解,因此不做过多的评价。
-
添加对 RpcAsyncAddPrinterDriver 远程调用的支持,以绕过微软 6 月 8 日的补丁,实现 CVE-2021-34527 exp。
-
理解 UAC 机制,完善这篇漏洞分析。
-
[官方网站] Windows Print Spooler Remote Code Execution Vulnerability https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-34527
-
[dwFileCopyFlags] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/b96cc497-59e5-4510-ab04-5484993b259b
-
[Windows PrintNightmare 漏洞(CVE-2021-34527) 和补丁分析] https://www.freebuf.com/vuls/279876.html
-
[gentilkiwi/mimikatz] https://github.com/gentilkiwi/mimikatz
-
[官方文档] DRIVER_INFO_2 structure https://docs.microsoft.com/en-us/windows/win32/printdocs/driver-info-2
-
[cube0x0] https://github.com/cube0x0
-
[James Forshaw] https://twitter.com/tiraniddo/status/1410726790994169857
-
[官方声明] KB5005010: Restricting installation of new printer drivers after applying the July 6, 2021 updates https://support.microsoft.com/en-us/topic/kb5005010-restricting-installation-of-new-printer-drivers-after-applying-the-july-6-2021-updates-31b91c02-05bc-4ada-a7ea-183b129578a7