估计最近一段时间不再更新 blog,要更新的话也是对之前的文章修修补补。
0x00 前言
SCShell
是一款无文件横向移动的工具,主要依赖于 ChangeServiceConfigA
函数,用于修改 Windows 服务的配置。该工具的优点在于它不会通过 SMB
协议去执行身份验证。该工具是通过 DCE/RPC
协议进行的。
因为是远程修改服务进行的,所以它不需要注册或创建服务。也不会删除远程系统上的任何文件。
与直接使用 sc.exe
的差别在于:如果当前进程无远程主机的权限,则需要使用 SMB
协议进行身份验证,后续步骤两者相同。
0x01 函数 API 介绍
1.1、LogonUserA
该函数是使用用户和明文密码登陆到本地计算机,无法登陆远程计算机。如果函数成功,则会受到表示已登陆用户的令牌的句柄,然后可以使用此令牌句柄模拟指定用户。
1 | BOOL LogonUserA( |
lpszUsername:指向以空字符结尾的字符串的指针,该字符串指定用户的名称,也就是要登陆的用户账号。
lpszDomain:指向以空字符结尾的字符串的指针,该字符串指定该账户的域或服务器的名称。如果此参数为 NULL,则必须以 UPN 格式指定用户名。如果此参数为 “.”,则表示使用本地账户来验证。
lpszPassword:指向以空字符结尾的字符串的指针,该字符串指向 lpszUsername 中指定用户的明文密码。结束使用后,可调用 SecureZeroMemory) 函数以清除内存中的密码。
dwLogonType:要执行的登陆操作的类型,共计 7 个类型。此处使用
LOGON32_LOGON_NEW_CREDENTIALS
,该登陆类型允许调用方克隆其当前令牌,并为出站连接指定新的凭据。新的登陆会话具有相同的本地标识符,但对其他网络连接使用不同的凭据。dwLogonProvider:指定登陆提供程序,共计 3 个值。此处使用系统标准登陆提供程序:
LOGON32_PROVIDER_DEFAULT
phToken:指向句柄变量的指针,该变量接收代码指定用户的令牌的句柄。
返回值:非零值
1.2、ImpersonateLoggedOnUserA
与 LogonUserA
函数相呼应。
1 | BOOL ImpersonateLoggedOnUser( |
- hToken:代表已登陆用户的主或模拟 Access token 的句柄。
- 返回值:非零值
1.3、OpenSCManagerA
建立到指定计算机上的服务控制管理器的连接,并打开指定的服务控制管理器数据库。
1 | SC_HANDLE OpenSCManagerA( |
- lpMachineName: 目标计算机名称,如果指针为 NULL或指向空字符串,则该函数将连接本地计算机上的服务控制管理器。
- lpDatabaseName:服务控制管理器数据库的名称。默认情况下此参数应设置为
SERVICES_ACTIVE_DATABASE
。 - dwDesiredAccess:对服务控制管理器的访问。此处应为
SC_MANAGER_ALL_ACCESS
,囊括了列表中的所有权限。有关访问权限列表可参阅【服务安全和访问权限】 - 返回值: 返回指定服务控制管理器数据库的句柄。
1.4、OpenServiceA
通过服务控制管理器打开现有服务。
1 | SC_HANDLE OpenServiceA( |
- hSCManager:服务控制管理器数据库的句柄。
- lpServiceName:要打开的服务的名称。该参数是由
CreateService
函数的lpServiceName
参数指定的名称,而不是用户界面应用程序显示的用于标识服务显示名称。 - dwDesiredAccess:访问服务。此处应为
SERVICE_ALL_ACCESS
,囊括了列表中的所有权限。有关访问权限列表可参阅【服务安全和访问权限】 - 返回值: 返回指定服务的句柄。
1.5、QueryServiceConfigA
检索指定服务的配置参数。
1 | BOOL QueryServiceConfigA( |
- hService:指定服务的句柄。
- lpServiceConfig:指向接收服务配置信息的缓冲区的指针。该数组的最大大小为 8KB。若要确定所需的大小,请设置为 NULL,而
cbBufSize
指定为 0。 - cbBufSize:指向的缓冲区大小(以字节为单位)。
- pcbBytesNeeded:如果函数失败并显示
ERROR_INSUFFICIENT_BUFFER
,则该变量的指针将接收存储所有配置信息所需的字节数。 - 返回值: 非零值。
1.6、ChangeServiceConfigA
更改指定服务的配置参数。
1 | BOOL ChangeServiceConfigA( |
- hService:指定服务的句柄。
- dwServiceType:服务类型。如果不更改现有服务类型,则指定
SERVICE_NO_CHANGE
。 - dwStartType:服务启动选项。如果不更改现有的启动类型,则指定
SERVICE_NO_CHANGE
。因为要执行命令,所以使用SERVICE_DEMAND_START
,后续调用StartService
函数时启动服务。 - dwErrorControl:如果此服务无法启动,应该采取措施应对这个错误。此处应为
SERVICE_ERROR_IGNORE
,忽略该错误并继续启动操作。 - lpBinaryPathName:服务二进制文件的标准现有路径。如果不更改现有路径,请指定 NULL。如果路径包含空格,则必须使用引号括起来。该路径中还可包含二进制文件的参数。
- lpLoadOrderGroup:该服务所属的负载排列组的名称。如果不更改现有组,请指定 NULL。
- lpdwTagId:指向变量的指针,该变量接收在
lpLoadOrderGroup
参数指定的组中唯一的标记值。如果不更改现有标签,请指定 NULL。 - lpDependencies:指向双 NULL 终止的数组,该数组以空分隔的服务或装入顺序组的名称分隔,系统必须在启动该服务之前才能启动这些名称。(对组的依赖性意味着,在尝试启动该组的所有成员之后,如果该组的至少一个成员正在运行,则该服务可以运行。)如果不更改现有的依赖性,则指定 NULL。
- lpServiceStartName:服务将在其下运行的帐户的名称。如果不更改现有帐户名,则指定 NULL。
- lpPassword:
lpServiceStartName
参数指定的帐户名的密码。如果不更改现有密码,请指定 NULL。 - lpDisplayName:用来为其用户标识服务的显示名称。如果不更改现有的显示名称,则指定 NULL。
- 返回值: 非零值。
1.7、StartServiceA
启动指定服务。
1 | BOOL StartServiceA( |
- hService:要启动的服务的句柄。由
OpenService
返回。 - dwNumServiceArgs:
lpServiceArgVectors
数组中的字符串数。如果lpServiceArgVectors
为 NULL,则此参数可以为 0。 - lpServiceArgVectors:以空值结尾的字符串将作为参数传递给服务的
ServiceMain
函数。如果没有参数,则此参数可以为 NULL。 - 返回值: 非零值。
0x02 技术细节
2.1、时序图
这一整个过程与 PsExec
操作服务的步骤大部分相同,区别仅是因为 SCshell
是更改配置,而 PsExec
是创建服务。以下内容是作者的代码实现(典型的 API 调用方式编程)。
2.2、登陆
使用 LogonUserA
登陆。后使用 ImpersonateLoggedOnUser
。
1 | if(username != NULL) { |
2.3、Service Manager
一旦当前进程获取了正确的身份验证,即可使用 OpenSCManagerA
打开远程机器的服务控制管理器,并且获取其数据库的句柄。
1 | SC_HANDLE schManager = OpenSCManagerA(targetHost, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS); |
与该数据库句柄进行交互即可。通过该数据库句柄,使用 OpenServiceA
打开需要的现有服务。比如作者在演示中使用的是 XblAuthManager
服务。后续使用中,为了更方便使用,可以选择一些较为通用的服务代替。
1 | printf("Opening %s\n", serviceName); |
之所以要查询服务的配置信息,是为了获取原始服务二进制的路径。
1 | DWORD dwSize = 0; |
使用 ChangeServiceConfigA
更改已打开的服务的配置。通常可以直接使用 C:\Windows\System32\cmd.exe args1 args2
这样的配置替换掉二进制文件路径中的内容。
1 | bResult = ChangeServiceConfigA(schService, SERVICE_NO_CHANGE, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, payload, NULL, NULL, NULL, NULL, NULL, NULL); |
通过调用 StartService
函数时启动已更改配置的服务。
1 | bResult = StartServiceA(schService, 0, NULL); |
最后再次使用 ChangeServiceConfigA
还原服务的配置。
1 | if(dwLpqscSize) { |
整个过程还是比较简单的。
0x03 C# 实现
能在 MSDN 中找到的函数,在 pinvoke.net
中基本能找到对相应的例子,所以直接引用即可。
1 | static void Main(string[] args) |
运行结果:
运行流量:
可以较为明显的看到是 DCE/RPC
协议进行的,且整个过程的 API 调用看得清清楚楚。
项目源码已发布至 Github,请注意查收:SharpSCShell,并将此源码合并至作者的 SCShell
0x04 日志
使用用户凭证连接目标时,会留下正常的登陆日志,4624。同时如果服务超时,也会产生 7009 日志。当然,如果直接使用 ELK 监视事件 ID 4657,也是有惊喜的,而且很明显。
该工具经过测试,当防火墙打开时无法连接,且大部分命令无法执行,原因有待探究。
0x05 WMIC
WMIC
与 SCShell
有类似的功能。
步骤1: 获取目标服务的当前 pathName
,以便我们在运行命令后就可以将其还原(在本例中为 XblAuthManager
)
1 | wmic /user:DOMAIN\USERNAME /password:PASSWORD /node:TARGET_IP service where name='XblAuthManager' get pathName |
步骤2:将 pathName
更改为要运行的任何命令
1 | wmic /user:DOMAIN\USERNAME /password:PASSWORD /node:TARGET_IP service where name='XblAuthManager' call change PathName="C:\Windows\Microsoft.Net\Framework\v4.0.30319\MSBuild.exe C:\testPayload.xml" |
步骤3:启动修改后的服务
1 | wmic /user:DOMAIN\USERNAME /password:PASSWORD /node:TARGET_IP service where name='XblAuthManager' call startservice |
步骤4:将服务 pathName
更改回其原始值
1 | wmic /user:DOMAIN\USERNAME /password:PASSWORD /node:TARGET_IP service where name='XblAuthManager' call change PathName="C:\Windows\system32\svchost.exe -k netsvcs" |
这一部分本来打算另起一文,在写 WMI 放入,但想想之后的文章还是留在知识星球进行自我沉淀。