├── .gitignore ├── honeypot.db ├── setup.sh ├── database.sql ├── honeypot.service ├── README.md ├── LICENSE └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | id_rsa 2 | id_rsa.pub 3 | loot.csv -------------------------------------------------------------------------------- /honeypot.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaJc01/ssh-honeypot/HEAD/honeypot.db -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | go get -u "github.com/gliderlabs/ssh" 2 | go get -u "golang.org/x/crypto/ssh" 3 | go get -u "golang.org/x/crypto/ssh/terminal" 4 | go get -u "github.com/integrii/flaggy" 5 | ssh-keygen -f ./id_rsa 6 | go build -o server main.go -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Command ( 2 | CommandID INTEGER PRIMARY KEY AUTOINCREMENT, 3 | Username TEXT, 4 | RemoteIP TEXT, 5 | Command TEXT, 6 | Timestamp TEXT 7 | ); 8 | 9 | CREATE TABLE Login ( 10 | LoginID INTEGER PRIMARY KEY AUTOINCREMENT, 11 | Username TEXT, 12 | Password TEXT, 13 | RemoteIP TEXT, 14 | RemoteVersion TEXT, 15 | Timestamp TEXT 16 | ); 17 | -------------------------------------------------------------------------------- /honeypot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SSH Honeypot 3 | 4 | [Service] 5 | User=james 6 | Group=james 7 | WorkingDirectory=/home/james/honeypot/ 8 | ExecStart=/home/james/honeypot/server fakeshell -p 22 -f "OpenSSH_8.2p1 Ubuntu-4ubuntu0.1" -H ubuntu-prod -C 9 | Restart=always 10 | RestartSec=5 11 | StandardOutput=file:/home/james/honeypot/log_out.txt 12 | StandardError=file:/home/james/honeypot/log_err.txt 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TryHackMe SSH Honeypot 2 | SSH Honeypot that gathers attempted creds, IP addresses and versions. 3 | The SSH server will either issue a warning, or drop the attacker into a fake shell. 4 | 5 | ## Loot File Format 6 | The logging now logs to an SQLite database, with schema available in database.sql 7 | 8 | ## Fake Shell 9 | The fake shell will print a bash command not found error for every command entered, except exit. 10 | You can enable logging of these commands with the -C flag. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 James 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gliderlabs/ssh" 13 | "github.com/integrii/flaggy" 14 | _ "github.com/mattn/go-sqlite3" 15 | gossh "golang.org/x/crypto/ssh" 16 | "golang.org/x/crypto/ssh/terminal" 17 | ) 18 | 19 | var ( 20 | database *sql.DB 21 | doCommandLogging bool = false 22 | hostname string = "kali" 23 | message string = "Don't blindly SSH into every VM you see." 24 | lootChan chan (loginData) 25 | cmdChan chan (command) 26 | ) 27 | 28 | const buffSize = 5 29 | 30 | type loginData struct { 31 | username string 32 | password string 33 | remoteIP string 34 | remoteVersion string 35 | timestamp string 36 | } 37 | 38 | type command struct { 39 | username string 40 | remoteIP string 41 | command string 42 | timestamp string 43 | } 44 | 45 | func main() { 46 | var ( 47 | lport uint = 2222 48 | lhost net.IP = net.ParseIP("0.0.0.0") 49 | keyPath string = "id_rsa" 50 | fingerprint string = "OpenSSH_8.2p1 Debian-4" 51 | ) 52 | databasePointer, err := sql.Open("sqlite3", "honeypot.db") 53 | if err != nil { 54 | log.Println(err.Error()) 55 | log.Fatal("Database connection failed") 56 | } 57 | database = databasePointer 58 | lootChan = make(chan (loginData), buffSize) 59 | cmdChan = make(chan (command), buffSize) 60 | flaggy.UInt(&lport, "p", "port", "Local port to listen for SSH on") 61 | flaggy.IP(&lhost, "i", "interface", "IP address for the interface to listen on") 62 | flaggy.String(&keyPath, "k", "key", "Path to private key for SSH server") 63 | flaggy.String(&fingerprint, "f", "fingerprint", "SSH Fingerprint, excluding the SSH-2.0- prefix") 64 | 65 | fakeShellSubcommand := flaggy.NewSubcommand("fakeshell") 66 | fakeShellSubcommand.String(&hostname, "H", "hostname", "Hostname for fake shell prompt") 67 | fakeShellSubcommand.Bool(&doCommandLogging, "C", "logcmd", "Log user commands within the fake shell?") 68 | warnSubcommand := flaggy.NewSubcommand("warn") 69 | warnSubcommand.String(&message, "m", "message", "Warning message to be sent after authentication") 70 | 71 | flaggy.AttachSubcommand(fakeShellSubcommand, 1) 72 | flaggy.AttachSubcommand(warnSubcommand, 1) 73 | flaggy.Parse() 74 | if !fakeShellSubcommand.Used && !warnSubcommand.Used { 75 | flaggy.ShowHelpAndExit("No subcommand supplied") 76 | } 77 | log.SetPrefix("SSH - ") 78 | privKeyBytes, err := ioutil.ReadFile(keyPath) 79 | if err != nil { 80 | log.Panicln("Error reading privkey:\t", err.Error()) 81 | } 82 | privateKey, err := gossh.ParsePrivateKey(privKeyBytes) 83 | if err != nil { 84 | log.Panicln("Error parsing privkey:\t", err.Error()) 85 | } 86 | server := &ssh.Server{ 87 | Addr: fmt.Sprintf("%s:%v", lhost.String(), lport), 88 | Handler: func() ssh.Handler { 89 | if warnSubcommand.Used { 90 | return sshHandler 91 | } 92 | return fakeTerminal 93 | }(), 94 | Version: fingerprint, 95 | PasswordHandler: passwordHandler, 96 | } 97 | server.AddHostKey(privateKey) 98 | go threadsafeLootLogger() 99 | log.Println("Started loot logger") 100 | if doCommandLogging { 101 | go threadsafeCommandLogger() 102 | log.Println("Started command logger") 103 | } 104 | log.Println("Started Honeypot SSH server on", server.Addr) 105 | log.Fatal(server.ListenAndServe()) 106 | } 107 | 108 | // These functions ensure only one thread is writing to the DB at once. 109 | // Each handler runs in parallel so we cannot write from that thread safely. 110 | func threadsafeLootLogger() { 111 | for { 112 | logLoot(<-lootChan) 113 | } 114 | } 115 | 116 | func threadsafeCommandLogger() { 117 | for { 118 | logCommand(<-cmdChan) 119 | } 120 | } 121 | 122 | func logCommand(cmd command) { 123 | statement, err := database.Prepare( 124 | "INSERT INTO Command(Username, RemoteIP, Command, Timestamp) values(?,?,?,?)") 125 | if err != nil { 126 | log.Println(err.Error()) 127 | } 128 | _, err = statement.Exec( 129 | cmd.username, 130 | cmd.remoteIP, 131 | cmd.command, 132 | cmd.timestamp) 133 | if err != nil { 134 | log.Println(err.Error()) 135 | } 136 | } 137 | 138 | func logLoot(data loginData) { //TODO quoted string username 139 | statement, err := database.Prepare( 140 | "INSERT INTO Login(Username, Password, RemoteIP, RemoteVersion, Timestamp) values(?,?,?,?,?)") 141 | if err != nil { 142 | log.Println(err.Error()) 143 | } 144 | _, err = statement.Exec( 145 | data.username, 146 | data.password, 147 | data.remoteIP, 148 | data.remoteVersion, 149 | data.timestamp) 150 | if err != nil { 151 | log.Println(err.Error()) 152 | } 153 | } 154 | 155 | func sshHandler(s ssh.Session) { 156 | s.Write([]byte(message + "\n")) 157 | } 158 | 159 | func fakeTerminal(s ssh.Session) { 160 | commandLine := s.RawCommand() 161 | if s.RawCommand() != "" { //If the attacker sets a command with ssh -C 162 | cmdChan <- command{ 163 | username: s.User(), 164 | remoteIP: s.RemoteAddr().String(), 165 | command: commandLine, 166 | timestamp: fmt.Sprint(time.Now().Unix())} 167 | } 168 | term := terminal.NewTerminal(s, fmt.Sprintf("%s@%s:~$ ", s.User(), hostname)) 169 | go func(s ssh.Session) { //timeout sessions to save CPU. 170 | time.Sleep(time.Second * 30) 171 | s.Close() 172 | }(s) 173 | for { 174 | commandLine, err := term.ReadLine() 175 | if err != nil { 176 | s.Close() 177 | break 178 | } 179 | commandLineSlice := strings.Split(commandLine, " ") 180 | if commandLineSlice[0] == "exit" { 181 | break 182 | } 183 | if commandLineSlice[0] != "" { 184 | if doCommandLogging { 185 | cmdChan <- command{ 186 | username: s.User(), 187 | remoteIP: s.RemoteAddr().String(), 188 | command: commandLine, 189 | timestamp: fmt.Sprint(time.Now().Unix())} 190 | } 191 | term.Write([]byte(fmt.Sprintf("bash: %s: command not found\n", commandLineSlice[0]))) 192 | } 193 | } 194 | s.Close() 195 | } 196 | 197 | func passwordHandler(context ssh.Context, password string) bool { 198 | data := loginData{ 199 | username: context.User(), 200 | password: password, 201 | remoteIP: context.RemoteAddr().String(), 202 | remoteVersion: context.ClientVersion(), 203 | timestamp: fmt.Sprint(time.Now().Unix())} 204 | //logLoot(data) 205 | lootChan <- data 206 | return true 207 | } 208 | --------------------------------------------------------------------------------