commit 0873cadc4c89abb430daea71c9a3132572e4b271 Author: bytedream Date: Thu Apr 28 20:01:08 2022 +0200 Initial commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..cd711a0 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2a77abf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..c5d391d --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..524ebb5 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# ⚠️ UNFINISHED PROJECT ⚠️ + +> A try to write a own ssh honeypot. Higly inspirated by [sshesame](https://github.com/jaksi/sshesame). + +# sshoneypot + +Go 1.10 + +**sshoneypot** easy is a fake ssh server that lets everyone connect, logs their activity and can be implemented easily in your project, or can be used as a standalone application. +The ssh server has a emulated, full functional linux filesystem. For more details about the filesystem see [here](info/fs.go). +It also contains some basic linux commands like `cd`, `ls` or `stat`. You can add commands by yourself too, see [here](#own-commands) how. + +The project itself is just a library, but you can run it standalone via [docker](#Docker). + +## Docker + +## Own commands + +If the standard commands aren't enough, you can easily implement you owns + +```go +package main + +func main() { +} +``` + +## Warning +This software, just like any other, might contain bugs. Given the popular nature of SSH, you probably shouldn't run it unsupervised as root on a production server on port 22. Use common sense. + +## Motivation +I was just curious what all these guys were up to: +``` +sshd[8128]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost= user=root +sshd[8128]: Failed password for root from port 37510 ssh2 +sshd[8128]: Received disconnect from port 37510:11: [preauth] +sshd[8128]: Disconnected from port 37510 [preauth] +sshd[8141]: Received disconnect from port 59353:11: [preauth] +sshd[8141]: Disconnected from port 59353 [preauth] +sshd[8151]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost= user=root +sshd[8151]: Failed password for root from port 63785 ssh2 +sshd[8159]: Received disconnect from port 24889:11: [preauth] +sshd[8159]: Disconnected from port 24889 [preauth] +``` + +## Details +`sshesame` accepts and logs +* every password authentication request, +* every SSH channel open request and +* every SSH request + +**without actually executing anything on the host**. + +For more details, read the [relevant RFC](https://tools.ietf.org/html/rfc4254). + +## Installing +### From source +* [Install go](https://golang.org/doc/install) (version 1.4 or newer required) +* `go get -u github.com/jaksi/sshesame` + +### Snap +`snap install sshesame` + +Package created and maintained by [chadmiller](https://github.com/chadmiller). + +You can find the package [here](https://code.launchpad.net/~privacy-squad/+junk/sshesame-snap). + +## Examples + +```go +package main + +import ( + "fmt" + "github.com/bytedream/sshoneypot/sshoneypot" +) + +func main() { + fmt.Println("aa") +} +``` + +## Usage +``` +$ sshesame -h +Usage of sshesame: + -host_key string + a file containing a private key to use + -json_logging + enable logging in JSON + -listen_address string + the local address to listen on (default "localhost") + -port uint + the port number to listen on (default 2022) + -server_version string + The version identification of the server (RFC 4253 section 4.2 requires that this string start with "SSH-2.0-") (default "SSH-2.0-sshesame") +``` +Consider creating a private key to use with sshesame, for example using `ssh-keygen`. + +## Example output +``` +Connection: client=:45782 +Login: client=:45782, user="root", password="cisco" +Established SSH connection: client=:45782 +New channel: clinet=:45782, type=direct-tcpip, payload={DestinationAddress: DestinationPort:110 SourceAddress:192.168.0.1 SourcePort:0} +Failed to read from channel: EOF +New channel: clinet=:45782, type=direct-tcpip, payload={DestinationAddress: DestinationPort:143 SourceAddress:192.168.0.1 SourcePort:0} +Failed to read from channel: EOF +New channel: clinet=:45782, type=direct-tcpip, payload={DestinationAddress: DestinationPort:587 SourceAddress:192.168.0.1 SourcePort:0} +Failed to read from channel: EOF +New channel: clinet=:45782, type=direct-tcpip, payload={DestinationAddress: DestinationPort:587 SourceAddress:192.168.0.1 SourcePort:0} +Failed to read from channel: EOF +New channel: clinet=:45782, type=session, payload=[] +Request: client=:45782, channel=session, type=exec, payload={Command:/sbin/ifconfig} +Failed to read from terminal: EOF +New channel: clinet=:45782, type=session, payload=[] +Request: client=:45782, channel=session, type=exec, payload={Command:cat /proc/meminfo} +Failed to read from terminal: EOF +New channel: clinet=:45782, type=session, payload=[] +Request: client=:45782, channel=session, type=exec, payload={Command:2>/dev/null sh -c 'cat /lib/libdl.so* || cat /lib/librt.so* || cat /bin/cat || cat /sbin/ifconfig'} +Failed to read from terminal: EOF +New channel: clinet=:45782, type=session, payload=[] +Request: client=:45782, channel=session, type=exec, payload={Command:cat /proc/version} +Failed to read from terminal: EOF +New channel: clinet=:45782, type=session, payload=[] +Request: client=:45782, channel=session, type=exec, payload={Command:uptime} +Failed to read from terminal: EOF +Disconnect: client=:45782 +``` +So what happened here? +* A client logged in with the user "root" and the password "cisco" +* Using TCP/IP forwarding over SSH, they tried to connect to a few remote mail servers over POP3 (port 110), IMAP (port 143) and Submission (port 587) +* They tried to execute a few commands to get some information about the host + +Again, if you're interested in the technical details of SSH, read the [RFC](https://tools.ietf.org/html/rfc4254). + +## Inspired + +This project was inspired from some the following projects + +- [sshesame](https://github.com/jaksi/sshesame) (another go based fake ssh server) + +## Implementation diff --git a/dump_fs.py b/dump_fs.py new file mode 100644 index 0000000..73b9c2a --- /dev/null +++ b/dump_fs.py @@ -0,0 +1,63 @@ +#!/usr/bin/python3 +import json +import os +import sys + + +excluded = ('/tmp', '/proc') + + +def info(file: str) -> list: + try: + stat = os.stat(file) + # this error may occur when temporary files are being passed as argument and deleted before `os.stat(...)` can read it + except FileNotFoundError: + import stat + stat.S_ISBLK() + return None + # occurs when you do not have the rights to read the given file + except PermissionError: + sys.stderr.write(f"Permission denied: '{file}'\n") + return None + + type = int(oct(stat.st_mode)[2:-4]) + + size = stat.st_size + links = stat.st_nlink + permissions = int(oct(stat.st_mode)[-4:], 8) + + creation_timestamp = int(stat.st_ctime) + # access_timestamp = stat.st_atime + # modification_timestamp = stat.st_mtime + + user_id = stat.st_uid if stat.st_uid in [0, 1000] else 1000 + group_id = stat.st_gid if stat.st_gid in [0, 1000] else 1000 + + return [type, size, links, permissions, creation_timestamp, user_id, group_id] + + +if __name__ == '__main__': + fs = {'files': {}, 'info': info('/')} + for root, dirs, files in os.walk('/'): + + # checks if the path is in the `excluded` list and if so, it skips this iteration + if root.startswith(excluded): + continue + + fs_root = fs['files'] + for dir in root.split(os.sep): + if dir.startswith('/'): + dir = dir[1:] + if dir != "": + fs_root = fs_root[dir]['files'] + + for dir in dirs: + specs = info(os.path.join(root, dir)) + fs_root[dir] = {'files': {}, 'info': specs} + + for file in files: + specs = info(os.path.join(root, file)) + if specs: + fs_root[file] = specs + + json.dump(fs, open('fs.json', 'w+')) diff --git a/main.go b/main.go new file mode 100644 index 0000000..384e698 --- /dev/null +++ b/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/bytedream/sshoneypot/sshoneypot" + "github.com/bytedream/sshoneypot/sshoneypot/info" + "golang.org/x/crypto/ssh" + "io/fs" + "io/ioutil" + "os" +) + +func main() { + //fmt.Println(strings.Split("/etc/aaa", string(os.PathSeparator))[1:]) + + /*fs, err := sshoneypot.LoadFSFromJson("fs.json") + if err != nil { + panic(err) + } + + if file, ok := fs.GetFile("/etc"); ok { + d, _ := file.(sshoneypot.Directory) + fmt.Println(d.Files) + //fmt.Println(file.(sshoneypot.Directory).Files) + }*/ + + var key ssh.Signer + if _, err := os.Stat("ssh.key"); os.IsNotExist(err) { + privateKey, _ := sshoneypot.GenerateSSHKey() + ioutil.WriteFile("ssh.key", privateKey, fs.ModePerm) + key, _ = sshoneypot.LoadSSHKey(privateKey) + } else { + key, _ = sshoneypot.LoadSSHKeyFromFile("ssh.key") + } + + filesystem, _ := info.LoadFSFromJson("fs.json") + filesystem.Manipulate = true + sshServer := sshoneypot.DefaultSSHoneypot(filesystem, key) + if err := sshServer.Serve(); err != nil { + panic(err) + } +} diff --git a/ssh.key b/ssh.key new file mode 100755 index 0000000..38289a4 --- /dev/null +++ b/ssh.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAwOnBoYINHWj92prwhQHzLv3pDHAdqui6R4Ap7aWccSEM8fh7 +4xaqnDSCTPnkndec2t9wkR0rKNcharUSg9RUDyFBTcXeFlDEnEIHQW60iX6WnMAn +FUu2dqdX2SkXOvmERBkeQhukYQGrmTLY9KChgMUrLT698G12BiR3QdnwsfIu0ICO +PvICqYqn27J78UAArHveHzdxL2nOZhsEFaI5HawqZDc3BRy62yNO/u1Vx+FlbzuY +ToYSWYHvItzOFEkQudeJLXwBc6q1tZyLrmriLUek78MXIa3cZxeA6QbQkxPcVvG6 +ALTgJ5kVS/ryr2VdrvCZs6qeQhxzVkmf+UnY7wIDAQABAoIBACSlAqwMSTN5+yao +YeHSIgCeKMO9FFWfyarFVLGY0OPIdG4OVnInnxb2/n1ixqOZDmmhIf/eu5ERdH6R +kMfL8H/DQGVmna49f2tzO2+ZeN/ZVQDHm2T3MdzOIujUXl5MgWxyHQZPu+TVbWQ3 +fXDShnsweOgqT+g40r+N871licwzMUrEmfygYzvQtHmEnb0QS3r46kn45D/uPq/w +Cw29R7yibiaBLlGZdOHHMW4A9svWvJfOjTH5+6Rs/rINcWLhIqdV6XQoKYVizjnb +8u138/6Xenhq8+9gzJyTUfApBzN+t8lm+bwnuYH5EhGoNv8rPLk90dwHMbXJIkJA +pSQrSxkCgYEA0KL0Gz1ozdlLYfBtXfxQmg1tiehtEBM+2Zn9znvCVNG08r/MQu81 +pyXIfhJWYYveFskLnG0XzS7Bj36V5SwONUtfUzw4ymLUJGknhRzMb/vQgWUcQrWO +wglgG8rQskkjzHR3gLIB4Hiv9o/9k8w0Bv/oa+lrQ1DInqPQGNtxIJsCgYEA7LUG +mpY5z9xxXF7GinqkFa0iVQ84zYwQ/eyHQfGiap80dgjTEnaEN2cLvl8aGBoXkwiV +Lcc/zbk0ftYPMXMwpYDXkeHY99IMwu0eicyOuckYB45n+q9ZsJPiEzkBwVRIdeEQ +PBSzFoPw1KXwe/T5xvPo6272lcF/wSIJLdo+fD0CgYAxGVg3HOGQKAX8e3dRefKB +/oz7um4ILW9KCFpZgHiAO4XI5ugsDF1lA5hGSwx3ElJmrFOGMYo7aDh3C4Q9FXwW +gLFjRjXbMxzXoMODKP7Xj7xG50OaU13QPiKXB8jLXDkHgZUp/TpB2EUY0lQLty+7 +QfgxhRIQGm/MHaL4ZpQd5wKBgA9ArvdBHOKPAuL/3G1J787RxDeU7oUYNHQYTLLs +HhoeviOo9+jlqCllw3T17dmFvOUllW9IuozIFWDi7EG0eXsArWNiGTgG2fmpi+E3 +RC8mjRzXiU23BOGC9ftlHf7WLoEiCojEkLGvuNILC08BfyLZzrV1BgCqYlAQoGTK +/mHFAoGAIVxgfCZsZ4+Nsik8T3eNstI5tytwyU4y27eQEpDvuzmYCMR0/F+6lj9z +nPBLMv5ELCQf98lotTLthKv2jRN45Xv9jZ+mu/4fRwdJXaEdc8vt72fX6HXU1Gzt +ZXGzkt+hJufUxLN6u3xgGqcRWBxs/xKvAcSEAkkqKy8RgCnNqXk= +-----END RSA PRIVATE KEY----- diff --git a/sshoneypot/handle.go b/sshoneypot/handle.go new file mode 100644 index 0000000..0d52540 --- /dev/null +++ b/sshoneypot/handle.go @@ -0,0 +1,112 @@ +package sshoneypot + +import ( + "errors" + "fmt" + "github.com/bytedream/sshoneypot/sshoneypot/info" + "golang.org/x/crypto/ssh" +) + + +type payloadWindowChange struct { + Width, Height uint32 + + // always 0 + PixelWidth, PixelHeight uint32 +} + +type payloadPty struct { + Term string + Width, Height uint32 + + // always 0 + PixelWidth, PixelHeight uint32 + + Modes []byte +} + + +func handleRequest(requests <-chan *ssh.Request, term *info.Termina) { + for request := range requests { + switch request.Type { + case "pty-req": + var pty payloadPty + ssh.Unmarshal(request.Payload, &pty) + + term.SetSize(int(pty.Width), int(pty.Height)) + case "window-change": + var windowChange payloadWindowChange + ssh.Unmarshal(request.Payload, &windowChange) + + term.SetSize(int(windowChange.Width), int(windowChange.Height)) + } + // sends a reply if wanted + if request.WantReply { + request.Reply(true, nil) + } + } +} + +func parseCommand(command string) ([]string, error) { + var args []string + state := "start" + current := "" + quote := "\"" + escapeNext := true + for i := 0; i < len(command); i++ { + c := command[i] + + if state == "quotes" { + if string(c) != quote { + current += string(c) + } else { + args = append(args, current) + current = "" + state = "start" + } + continue + } + + if escapeNext { + current += string(c) + escapeNext = false + continue + } + + switch c { + case '\\': + escapeNext = true + continue + case '"', '\'': + state = "quotes" + quote = string(c) + continue + } + + if state == "arg" { + if c == ' ' || c == '\t' { + args = append(args, current) + current = "" + state = "start" + } else { + current += string(c) + } + continue + } + + if c != ' ' && c != '\t' { + state = "arg" + current += string(c) + } + } + + if state == "quotes" { + return []string{}, errors.New(fmt.Sprintf("Unclosed quote in command line: %s", command)) + } + + if current != "" { + args = append(args, current) + } + + return args, nil +} diff --git a/sshoneypot/handler/fs.go b/sshoneypot/handler/fs.go new file mode 100644 index 0000000..09422ed --- /dev/null +++ b/sshoneypot/handler/fs.go @@ -0,0 +1,538 @@ +package handler + +import ( + "fmt" + "github.com/bytedream/sshoneypot/sshoneypot/info" + "math/rand" + "os" + "sort" + "strings" +) + +func basicFileCommandParser(info *info.Info, help string, availableOptions... string) (options []string, files []string, ok bool) { + if len(info.Args) == 0 { + info.Errorln(missingOperand(info)) + return nil, nil, false + } else { + for _, arg := range info.Args { + if strings.HasPrefix(arg, "-") { + hasArg := len(availableOptions) == 0 + + if arg == "--help" { + info.Writeln(help) + return nil, nil, false + } + for _, option := range availableOptions { + if arg == option { + hasArg = true + break + } + } + options = append(options, arg) + + if !hasArg{ + info.Errorlnf("%s: unrecognized option '%s'", info.Command, arg) + info.Errorlnf("Try '%s --help' for more information.", info.Command) + return nil, nil, false + } + } else { + files = append(files, arg) + } + } + } + return options, files, true +} + +type Cat struct { + info.SimpleHandler +} + +func (cat Cat) Handle(cmdInfo *info.Info) { + if multipleArgs(cmdInfo) { + var paths []string + for _, arg := range cmdInfo.Args { + if strings.HasPrefix(arg, "-") { + switch arg { + case "--help": + cmdInfo.Writeln(cat.help()) + return + } + } else { + paths = append(paths, arg) + } + } + + for _, path := range paths { + if file, ok := cmdInfo.FS.GetFile(path, false); ok { + if file.IsDir() { + cmdInfo.Writelnf("%s:%s: Is a directory", cmdInfo.Command, path) + } else { + realFile := file.(info.File) + bytes := make([]byte, realFile.Size) + // this will make the file content always the same + rand.Seed(realFile.Size + realFile.CreationTimestamp) + + rand.Read(bytes) + cmdInfo.Writeln(string(bytes)) + } + } else { + cmdInfo.Writeln(noSuchFileOrDirectory(path, cmdInfo)) + } + } + } +} + +func (cat Cat) help() string { + return ` + Usage: cat [OPTION]... [FILE]... + Concatenate FILE(s) to standard output. + + With no FILE, or when FILE is -, read standard input. + + -A, --show-all equivalent to -vET + -b, --number-nonblank number nonempty output lines, overrides -n + -e equivalent to -vE + -E, --show-ends display $ at end of each line + -n, --number number all output lines + -s, --squeeze-blank suppress repeated empty output lines + -t equivalent to -vT + -T, --show-tabs display TAB characters as ^I + -u (ignored) + -v, --show-nonprinting use ^ and M- notation, except for LFD and TAB + --help display this help and exit + --version output version information and exit + + Examples: + cat f - g Output f's contents, then standard input, then g's contents. + cat Copy standard input to standard output. + + GNU coreutils online help: + Report cat translation bugs to + Full documentation at: + or available locally via: info '(coreutils) cat invocation'` +} + +type CD struct { + info.SimpleHandler +} + +func (cd CD) Handle(cmdInfo *info.Info) { + if _, files, ok := basicFileCommandParser(cmdInfo, cd.help()); ok { + if len(files) != 1 { + return + } else { + path := files[0] + if _, ok := cmdInfo.FS.GetExplicitDirectory(path, true); ok { + if cmdInfo.Terminal != nil { + cmdInfo.Terminal.SetPrompt(fmt.Sprintf("%s:%s $ ", cmdInfo.User.User(), cmdInfo.FS.Cwd().Name)) + } + } else { + cmdInfo.Writelnf("-bash: cd: %s: No such file or directory", path) + } + } + } +} + +func (cd CD) help() string { + return `cd: cd [-L|[-P [-e]] [-@]] [dir] + Change the shell working directory. + + Change the current directory to DIR. The default DIR is the value of the + HOME shell variable. + + The variable CDPATH defines the search path for the directory containing + DIR. Alternative directory names in CDPATH are separated by a colon (:). + A null directory name is the same as the current directory. If DIR begins + with a slash (/), then CDPATH is not used. + + If the directory is not found, and the shell option 'cdable_vars' is set, + the word is assumed to be a variable name. If that variable has a value, + its value is used for DIR. + + Options: + -L force symbolic links to be followed: resolve symbolic + links in DIR after processing instances of '..' + -P use the physical directory structure without following + symbolic links: resolve symbolic links in DIR before + processing instances of '..' + -e if the -P option is supplied, and the current working + directory cannot be determined successfully, exit with + a non-zero status + -@ on systems that support it, present a file with extended + attributes as a directory containing the file attributes + + The default is to follow symbolic links, as if '-L' were specified. + '..' is processed by removing the immediately previous pathname component + back to a slash or the beginning of DIR. + + Exit Status: + Returns 0 if the directory is changed, and if $PWD is set successfully when + -P is used; non-zero otherwise.` +} + +type LS struct { + info.SimpleHandler +} + +func (ls LS) Handle(cmdInfo *info.Info) { + if len(cmdInfo.Args) == 0 { + cmdInfo.Args = []string{""} + } + + if _, files, ok := basicFileCommandParser(cmdInfo, ls.help()); ok { + var errors []string + dirs := map[string]string{} + + for _, dir := range files { + f, ok := cmdInfo.FS.GetFile(dir, false) + if !ok { + errors = append(errors, fmt.Sprintf("ls: cannot access '%s': No such file or directory", dir)) + continue + } + switch f.(type) { + case info.Directory: + var length int + var files []string + for _, file := range f.(info.Directory).Files { + if l := len(file.Name); length < l { + length = l + } + files = append(files, file.Name) + } + length++ + + sort.Strings(files) + + var builder strings.Builder + var perline int + if cmdInfo.Terminal.Width > 0 { + perline = int(cmdInfo.Terminal.Width) / length + } else { + perline = 80 / length + } + + for i, file := range files { + if i % perline == 0 && i != 0 { + builder.WriteString("\n") + } + builder.WriteString(file + strings.Repeat(" ", length - len(file))) + } + + dirs[dir] = builder.String() + case info.File: + dirs[dir] = dir + } + } + + if len(dirs) == 1 && len(errors) == 0 { + for _, v := range dirs { + cmdInfo.Writeln(v) + } + } else { + var builder strings.Builder + for _, err := range errors { + builder.WriteString(err) + builder.WriteString("\n") + } + if len(errors) > 0 && len(dirs) > 0 { + builder.WriteString("\n") + } + for k, v := range dirs { + builder.WriteString(fmt.Sprintf("%s:\n", k)) + builder.WriteString(v) + builder.WriteString("\n") + } + cmdInfo.Write(builder.String()) + } + } +} + +func (ls LS) help() string { + return `Usage: ls [OPTION]... [FILE]... + List information about the FILEs (the current directory by default). + Sort entries alphabetically if none of -cftuvSUX nor --sort is specified. + + Mandatory arguments to long options are mandatory for short options too. + -a, --all do not ignore entries starting with . + -A, --almost-all do not list implied . and .. + --author with -l, print the author of each file + -b, --escape print C-style escapes for nongraphic characters + --block-size=SIZE with -l, scale sizes by SIZE when printing them; + e.g., '--block-size=M'; see SIZE format below + -B, --ignore-backups do not list implied entries ending with ~ + -c with -lt: sort by, and show, ctime (time of last + modification of file status information); + with -l: show ctime and sort by name; + otherwise: sort by ctime, newest first + -C list entries by columns + --color[=WHEN] colorize the output; WHEN can be 'always' (default + if omitted), 'auto', or 'never'; more info below + -d, --directory list directories themselves, not their contents + -D, --dired generate output designed for Emacs' dired mode + -f do not sort, enable -aU, disable -ls --color + -F, --classify append indicator (one of */=>@|) to entries + --file-type likewise, except do not append '*' + --format=WORD across -x, commas -m, horizontal -x, long -l, + single-column -1, verbose -l, vertical -C + --full-time like -l --time-style=full-iso + -g like -l, but do not list owner + --group-directories-first + group directories before files; + can be augmented with a --sort option, but any + use of --sort=none (-U) disables grouping + -G, --no-group in a long listing, don't print group names + -h, --human-readable with -l and -s, print sizes like 1K 234M 2G etc. + --si likewise, but use powers of 1000 not 1024 + -H, --dereference-command-line + follow symbolic links listed on the command line + --dereference-command-line-symlink-to-dir + follow each command line symbolic link + that points to a directory + --hide=PATTERN do not list implied entries matching shell PATTERN + (overridden by -a or -A) + --hyperlink[=WHEN] hyperlink file names; WHEN can be 'always' + (default if omitted), 'auto', or 'never' + --indicator-style=WORD append indicator with style WORD to entry names: + none (default), slash (-p), + file-type (--file-type), classify (-F) + -i, --inode print the index number of each file + -I, --ignore=PATTERN do not list implied entries matching shell PATTERN + -k, --kibibytes default to 1024-byte blocks for disk usage; + used only with -s and per directory totals + -l use a long listing format + -L, --dereference when showing file information for a symbolic + link, show information for the file the link + references rather than for the link itself + -m fill width with a comma separated list of entries + -n, --numeric-uid-gid like -l, but list numeric user and group IDs + -N, --literal print entry names without quoting + -o like -l, but do not list group information + -p, --indicator-style=slash + append / indicator to directories + -q, --hide-control-chars print ? instead of nongraphic characters + --show-control-chars show nongraphic characters as-is (the default, + unless program is 'ls' and output is a terminal) + -Q, --quote-name enclose entry names in double quotes + --quoting-style=WORD use quoting style WORD for entry names: + literal, locale, shell, shell-always, + shell-escape, shell-escape-always, c, escape + (overrides QUOTING_STYLE environment variable) + -r, --reverse reverse order while sorting + -R, --recursive list subdirectories recursively + -s, --size print the allocated size of each file, in blocks + -S sort by file size, largest first + --sort=WORD sort by WORD instead of name: none (-U), size (-S), + time (-t), version (-v), extension (-X) + --time=WORD with -l, show time as WORD instead of default + modification time: atime or access or use (-u); + ctime or status (-c); also use specified time + as sort key if --sort=time (newest first) + --time-style=TIME_STYLE time/date format with -l; see TIME_STYLE below + -t sort by modification time, newest first + -T, --tabsize=COLS assume tab stops at each COLS instead of 8 + -u with -lt: sort by, and show, access time; + with -l: show access time and sort by name; + otherwise: sort by access time, newest first + -U do not sort; list entries in directory order + -v natural sort of (version) numbers within text + -w, --width=COLS set output width to COLS. 0 means no limit + -x list entries by lines instead of by columns + -X sort alphabetically by entry extension + -Z, --context print any security context of each file + -1 list one file per line. Avoid '\n' with -q or -b + --help display this help and exit + --version output version information and exit + + The SIZE argument is an integer and optional unit (example: 10K is 10*1024). + Units are K,M,G,T,P,E,Z,Y (powers of 1024) or KB,MB,... (powers of 1000). + + The TIME_STYLE argument can be full-iso, long-iso, iso, locale, or +FORMAT. + FORMAT is interpreted like in date(1). If FORMAT is FORMAT1FORMAT2, + then FORMAT1 applies to non-recent files and FORMAT2 to recent files. + TIME_STYLE prefixed with 'posix-' takes effect only outside the POSIX locale. + Also the TIME_STYLE environment variable sets the default style to use. + + Using color to distinguish file types is disabled both by default and + with --color=never. With --color=auto, ls emits color codes only when + standard output is connected to a terminal. The LS_COLORS environment + variable can change the settings. Use the dircolors command to set it. + + Exit status: + 0 if OK, + 1 if minor problems (e.g., cannot access subdirectory), + 2 if serious trouble (e.g., cannot access command-line argument). + + GNU coreutils online help: + Report ls translation bugs to + Full documentation at: + or available locally via: info '(coreutils) ls invocation'` +} + +type Mkdir struct { + info.SimpleHandler +} + +func (mkdir Mkdir) Handle(cmdInfo *info.Info) { + if _, files, ok := basicFileCommandParser(cmdInfo, mkdir.help()); ok { + for _, file := range files { + switch _, err := cmdInfo.FS.CreateDirectory(file); err { + case os.ErrExist: + cmdInfo.Writelnf("%s: cannot create directory '%s': File exists", cmdInfo.Command, file) + case os.ErrPermission: + cmdInfo.Writelnf("%s: cannot create directory '%s': Permission denied", cmdInfo.Command, file) + } + } + } +} + +func (mkdir Mkdir) help() string { + return ` + Usage: mkdir [OPTION]... DIRECTORY... + Create the DIRECTORY(ies), if they do not already exist. + + Mandatory arguments to long options are mandatory for short options too. + -m, --mode=MODE set file mode (as in chmod), not a=rwx - umask + -p, --parents no error if existing, make parent directories as needed + -v, --verbose print a message for each created directory + -Z set SELinux security context of each created directory + to the default type + --context[=CTX] like -Z, or if CTX is specified then set the SELinux + or SMACK security context to CTX + --help display this help and exit + --version output version information and exit + + GNU coreutils online help: + Report mkdir translation bugs to + Full documentation at: + or available locally via: info '(coreutils) mkdir invocation'` +} + +type RM struct { + info.SimpleHandler +} + +func (rm RM) Handle(cmdInfo *info.Info) { + if options, files, ok := basicFileCommandParser(cmdInfo, rm.help()); ok { + var dir bool + for _, arg := range options { + switch arg { + case "-r", "-R", "--recursive": + dir = true + } + } + + for _, fileName := range files { + if cmdInfo.FS.Manipulate { + if file, ok := cmdInfo.FS.GetFile(fileName, false); ok { + if file.IsDir() && !dir { + cmdInfo.Writelnf("rm: cannot remove '%s': Is a directory", fileName) + continue + } + file.Remove() + } else { + cmdInfo.Writelnf("rm: cannot remove '%s': No such file or directory", fileName) + } + } else { + cmdInfo.Writef("rm: remove write-protected file '%s'? ", fileName) + switch line, _ := cmdInfo.Read(); line { + case "y", "Y": + cmdInfo.Writelnf("rm: cannot remove '%s': Operation not permitted", fileName) + } + } + } + } +} + +func (rm RM) help() string { + return ` + Usage: rm [OPTION]... [FILE]... + Remove (unlink) the FILE(s). + + -f, --force ignore nonexistent files and arguments, never prompt + -i prompt before every removal + -I prompt once before removing more than three files, or + when removing recursively; less intrusive than -i, + while still giving protection against most mistakes + --interactive[=WHEN] prompt according to WHEN: never, once (-I), or + always (-i); without WHEN, prompt always + --one-file-system when removing a hierarchy recursively, skip any + directory that is on a file system different from + that of the corresponding command line argument + --no-preserve-root do not treat '/' specially + --preserve-root[=all] do not remove '/' (default); + with 'all', reject any command line argument + on a separate device from its parent + -r, -R, --recursive remove directories and their contents recursively + -d, --dir remove empty directories + -v, --verbose explain what is being done + --help display this help and exit + --version output version information and exit + + By default, rm does not remove directories. Use the --recursive (-r or -R) + option to remove each listed directory, too, along with all of its contents. + + To remove a file whose name starts with a '-', for example '-foo', + use one of these commands: + rm -- -foo + + rm ./-foo + + Note that if you use rm to remove a file, it might be possible to recover + some of its contents, given sufficient expertise and/or time. For greater + assurance that the contents are truly unrecoverable, consider using shred. + + GNU coreutils online help: + Report rm translation bugs to + Full documentation at: + or available locally via: info '(coreutils) rm invocation' + ` +} + +type Touch struct { + info.SimpleHandler +} + +func (touch Touch) Handle(cmdInfo *info.Info) { + if _, files, ok := basicFileCommandParser(cmdInfo, touch.help()); ok { + for _, file := range files { + if _, err := cmdInfo.FS.CreateFile(file); err == os.ErrPermission { + cmdInfo.Writelnf("%s: cannot touch '%s': Permission denied", cmdInfo.Command, file) + } + } + } +} + +func (touch Touch) help() string { + return `Usage: touch [OPTION]... FILE... + Update the access and modification times of each FILE to the current time. + + A FILE argument that does not exist is created empty, unless -c or -h + is supplied. + + A FILE argument string of - is handled specially and causes touch to + change the times of the file associated with standard output. + + Mandatory arguments to long options are mandatory for short options too. + -a change only the access time + -c, --no-create do not create any files + -d, --date=STRING parse STRING and use it instead of current time + -f (ignored) + -h, --no-dereference affect each symbolic link instead of any referenced + file (useful only on systems that can change the + timestamps of a symlink) + -m change only the modification time + -r, --reference=FILE use this file's times instead of current time + -t STAMP use [[CC]YY]MMDDhhmm[.ss] instead of current time + --time=WORD change the specified time: + WORD is access, atime, or use: equivalent to -a + WORD is modify or mtime: equivalent to -m + --help display this help and exit + --version output version information and exit + + Note that the -d and -t options accept different time-date formats. + + GNU coreutils online help: + Report touch translation bugs to + Full documentation at: + or available locally via: info '(coreutils) touch invocation'` +} diff --git a/sshoneypot/handler/handler.go b/sshoneypot/handler/handler.go new file mode 100644 index 0000000..ae95fe7 --- /dev/null +++ b/sshoneypot/handler/handler.go @@ -0,0 +1,27 @@ +package handler + +import ( + "fmt" + "github.com/bytedream/sshoneypot/sshoneypot/info" +) + +func Default(info *info.Info) { + info.Writelnf("-bash: %s: command not found", info.Command) +} + +func multipleArgs(info *info.Info) bool { + if len(info.Args) == 0 { + info.Writeln(missingOperand(info)) + return false + } else { + return true + } +} + +func missingOperand(info *info.Info) string { + return fmt.Sprintf("%s: missing operand", info.Command) +} + +func noSuchFileOrDirectory(file string, info *info.Info) string { + return fmt.Sprintf("%s: %s: No such file or directory", info.Command, file) +} diff --git a/sshoneypot/handler/net.go b/sshoneypot/handler/net.go new file mode 100644 index 0000000..3e91e9d --- /dev/null +++ b/sshoneypot/handler/net.go @@ -0,0 +1,382 @@ +package handler + +import ( + "fmt" + "github.com/bytedream/sshoneypot/sshoneypot/info" + "github.com/pborman/getopt" + "math/rand" + "mime" + "net" + "regexp" + "strconv" + "strings" + "time" +) + +func randomIp() string { + return fmt.Sprintf("%d.%d.%d.%d", rand.Intn(254), rand.Intn(254), rand.Intn(254), rand.Intn(254)) +} + +func Ifconfig(cmdInfo *info.Info) { + for _, arg := range cmdInfo.Args { + if arg == "--help" { + cmdInfo.Writeln(`Usage: + ifconfig [-a] [-v] [-s] [[]
] + [add
[/]] + [del
[/]] + [[-]broadcast [
]] [[-]pointopoint [
]] + [netmask
] [dstaddr
] [tunnel
] + [outfill ] [keepalive ] + [hw
] [mtu ] + [[-]trailers] [[-]arp] [[-]allmulti] + [multicast] [[-]promisc] + [mem_start ] [io_addr ] [irq ] [media ] + [txqueuelen ] + [[-]dynamic] + [up|down] ... + + =Hardware Type. + List of possible hardware types: + loop (Local Loopback) slip (Serial Line IP) cslip (VJ Serial Line IP) + slip6 (6-bit Serial Line IP) cslip6 (VJ 6-bit Serial Line IP) adaptive (Adaptive Serial Line IP) + ash (Ash) ether (Ethernet) ax25 (AMPR AX.25) + netrom (AMPR NET/ROM) rose (AMPR ROSE) tunnel (IPIP Tunnel) + ppp (Point-to-Point Protocol) hdlc ((Cisco)-HDLC) lapb (LAPB) + arcnet (ARCnet) dlci (Frame Relay DLCI) frad (Frame Relay Access Device) + sit (IPv6-in-IPv4) fddi (Fiber Distributed Data Interface) hippi (HIPPI) + irda (IrLAP) ec (Econet) x25 (generic X.25) + eui64 (Generic EUI-64) + =Address family. Default: inet + List of possible address families: + unix (UNIX Domain) inet (DARPA Internet) inet6 (IPv6) + ax25 (AMPR AX.25) netrom (AMPR NET/ROM) rose (AMPR ROSE) + ipx (Novell IPX) ddp (Appletalk DDP) ec (Econet) + ash (Ash) x25 (CCITT X.25)`) + return + } + } + cmdInfo.Writeln(`eth0: flags=4163 mtu 1500 + inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255 + ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet) + RX packets 13 bytes 1182 (1.1 KiB) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 0 bytes 0 (0.0 B) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + + lo: flags=73 mtu 65536 + inet 127.0.0.1 netmask 255.0.0.0 + loop txqueuelen 1000 (Local Loopback) + RX packets 0 bytes 0 (0.0 B) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 0 bytes 0 (0.0 B) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0`) +} + +type Ping struct { + info.SimpleHandler +} + +func (ping Ping) Handle(cmdInfo *info.Info) { + opts := getopt.New() + + count := opts.Int('c', 4, "count") + interval := opts.String('i', "1", "interval") + packetsize := opts.Int('s', 56, "packetsize") + opts.Getopt(cmdInfo.FullArgs, nil) + + duration, err := strconv.ParseFloat(*interval, 64) + if err != nil { + cmdInfo.Writelnf("%s: bad timing interval", cmdInfo.Command) + return + } + + address := opts.Arg(opts.NArgs() - 1) + ip := address + if net.ParseIP(address) == nil { + ip = randomIp() + } + + cmdInfo.Writelnf("PING %s (%s) %d data bytes.", address, ip, *packetsize) + for i := 0; i < *count; i++ { + resultTime := rand.Float64() * 10 + cmdInfo.Writelnf("%d bytes from %s (%s): icmp_seq=%d ttl=119 time=%.2f", *packetsize + 8, address, ip, i + 1, resultTime) + time.Sleep(time.Duration(duration) * time.Second) + } +} + +func (ping Ping) help() string { + return `Usage: ping [-aAbBdDfhLnOqrRUvV64] [-c count] [-i interval] [-I interface] + [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos] + [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option] + [-w deadline] [-W timeout] [hop1 ...] destination + Usage: ping -6 [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface] + [-l preload] [-m mark] [-M pmtudisc_option] + [-N nodeinfo_option] [-p pattern] [-Q tclass] [-s packetsize] + [-S sndbuf] [-t ttl] [-T timestamp_option] [-w deadline] + [-W timeout] destination` +} + +type Wget struct { + info.SimpleHandler +} + +func (wget Wget) Handle(cmdInfo *info.Info) { + if _, urls, ok := basicFileCommandParser(cmdInfo, wget.help()); ok { + regex := regexp.MustCompile("/((([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\\+\\$,\\w]+@)[A-Za-z0-9.-]+)((?:\\/[\\+~%\\/.\\w-_]*)?\\??(?:[-\\+=&;%@.\\w_]*)#?(?:[\\w]*))?)/") + for _, url := range urls { + var port uint32 + schemeUrl := url + + if strings.HasPrefix(url, "http://") { + port = 80 + } else if strings.HasPrefix(url, "https://") { + port = 433 + } else { + schemeUrl = "http://" + url + port = 433 + } + + currentTime := time.Now().Format("2006-01-02 15:04:05") + + if regex.MatchString(url) { + // generates a random ip + + mimeType := mime.TypeByExtension(url) + if mimeType == "" { + mimeType = "text/html" + } + + ip := randomIp() + + cmdInfo.Writelnf(`--%s-- %s + Connecting to %s (%s)|%s|:%d... connected." + HTTP request sent, awaiting response... 200 OK + Length: unspecified [%s] + Saving to: '%s'`, currentTime, schemeUrl, url, url, ip, port, mimeType, url) + + // TODO: download bar + + // sleeps a random time to imitate the fetching process + time.Sleep(time.Duration(rand.Intn(5000)) * time.Millisecond) + + if cmdInfo.Terminal.Width >= 60 { + loadingBar := 1 + if cmdInfo.Terminal.Width > 60 { + loadingBar += int((cmdInfo.Terminal.Width - 60) / 2) + } + } + } else { + cmdInfo.Writelnf(`--%s-- %s + Resolving %s (%s)... failed: Name or service not known. + wget: unable to resolve host address '%s'`, currentTime, schemeUrl, url, url, url) + } + } + } +} + +func (wget Wget) help() string { + return `GNU Wget 1.20.1, a non-interactive network retriever. + Usage: wget [OPTION]... [URL]... + + Mandatory arguments to long options are mandatory for short options too. + + Startup: + -V, --version display the version of Wget and exit + -h, --help print this help + -b, --background go to background after startup + -e, --execute=COMMAND execute a '.wgetrc'-style command + + Logging and input file: + -o, --output-file=FILE log messages to FILE + -a, --append-output=FILE append messages to FILE + -d, --debug print lots of debugging information + -q, --quiet quiet (no output) + -v, --verbose be verbose (this is the default) + -nv, --no-verbose turn off verboseness, without being quiet + --report-speed=TYPE output bandwidth as TYPE. TYPE can be bits + -i, --input-file=FILE download URLs found in local or external FILE + -F, --force-html treat input file as HTML + -B, --base=URL resolves HTML input-file links (-i -F) + relative to URL + --config=FILE specify config file to use + --no-config do not read any config file + --rejected-log=FILE log reasons for URL rejection to FILE + + Download: + -t, --tries=NUMBER set number of retries to NUMBER (0 unlimits) + --retry-connrefused retry even if connection is refused + --retry-on-http-error=ERRORS comma-separated list of HTTP errors to retry + -O, --output-document=FILE write documents to FILE + -nc, --no-clobber skip downloads that would download to + existing files (overwriting them) + --no-netrc don't try to obtain credentials from .netrc + -c, --continue resume getting a partially-downloaded file + --start-pos=OFFSET start downloading from zero-based position OFFSET + --progress=TYPE select progress gauge type + --show-progress display the progress bar in any verbosity mode + -N, --timestamping don't re-retrieve files unless newer than + local + --no-if-modified-since don't use conditional if-modified-since get + requests in timestamping mode + --no-use-server-timestamps don't set the local file's timestamp by + the one on the server + -S, --server-response print server response + --spider don't download anything + -T, --timeout=SECONDS set all timeout values to SECONDS + --dns-timeout=SECS set the DNS lookup timeout to SECS + --connect-timeout=SECS set the connect timeout to SECS + --read-timeout=SECS set the read timeout to SECS + -w, --wait=SECONDS wait SECONDS between retrievals + --waitretry=SECONDS wait 1..SECONDS between retries of a retrieval + --random-wait wait from 0.5*WAIT...1.5*WAIT secs between retrievals + --no-proxy explicitly turn off proxy + -Q, --quota=NUMBER set retrieval quota to NUMBER + --bind-address=ADDRESS bind to ADDRESS (hostname or IP) on local host + --limit-rate=RATE limit download rate to RATE + --no-dns-cache disable caching DNS lookups + --restrict-file-names=OS restrict chars in file names to ones OS allows + --ignore-case ignore case when matching files/directories + -4, --inet4-only connect only to IPv4 addresses + -6, --inet6-only connect only to IPv6 addresses + --prefer-family=FAMILY connect first to addresses of specified family, + one of IPv6, IPv4, or none + --user=USER set both ftp and http user to USER + --password=PASS set both ftp and http password to PASS + --ask-password prompt for passwords + --use-askpass=COMMAND specify credential handler for requesting + username and password. If no COMMAND is + specified the WGET_ASKPASS or the SSH_ASKPASS + environment variable is used. + --no-iri turn off IRI support + --local-encoding=ENC use ENC as the local encoding for IRIs + --remote-encoding=ENC use ENC as the default remote encoding + --unlink remove file before clobber + --xattr turn on storage of metadata in extended file attributes + + Directories: + -nd, --no-directories don't create directories + -x, --force-directories force creation of directories + -nH, --no-host-directories don't create host directories + --protocol-directories use protocol name in directories + -P, --directory-prefix=PREFIX save files to PREFIX/.. + --cut-dirs=NUMBER ignore NUMBER remote directory components + + HTTP options: + --http-user=USER set http user to USER + --http-password=PASS set http password to PASS + --no-cache disallow server-cached data + --default-page=NAME change the default page name (normally + this is 'index.html'.) + -E, --adjust-extension save HTML/CSS documents with proper extensions + --ignore-length ignore 'Content-Length' header field + --header=STRING insert STRING among the headers + --compression=TYPE choose compression, one of auto, gzip and none. (default: none) + --max-redirect maximum redirections allowed per page + --proxy-user=USER set USER as proxy username + --proxy-password=PASS set PASS as proxy password + --referer=URL include 'Referer: URL' header in HTTP request + --save-headers save the HTTP headers to file + -U, --user-agent=AGENT identify as AGENT instead of Wget/VERSION + --no-http-keep-alive disable HTTP keep-alive (persistent connections) + --no-cookies don't use cookies + --load-cookies=FILE load cookies from FILE before session + --save-cookies=FILE save cookies to FILE after session + --keep-session-cookies load and save session (non-permanent) cookies + --post-data=STRING use the POST method; send STRING as the data + --post-file=FILE use the POST method; send contents of FILE + --method=HTTPMethod use method "HTTPMethod" in the request + --body-data=STRING send STRING as data. --method MUST be set + --body-file=FILE send contents of FILE. --method MUST be set + --content-disposition honor the Content-Disposition header when + choosing local file names (EXPERIMENTAL) + --content-on-error output the received content on server errors + --auth-no-challenge send Basic HTTP authentication information + without first waiting for the server's + challenge + + HTTPS (SSL/TLS) options: + --secure-protocol=PR choose secure protocol, one of auto, SSLv2, + SSLv3, TLSv1, TLSv1_1, TLSv1_2 and PFS + --https-only only follow secure HTTPS links + --no-check-certificate don't validate the server's certificate + --certificate=FILE client certificate file + --certificate-type=TYPE client certificate type, PEM or DER + --private-key=FILE private key file + --private-key-type=TYPE private key type, PEM or DER + --ca-certificate=FILE file with the bundle of CAs + --ca-directory=DIR directory where hash list of CAs is stored + --crl-file=FILE file with bundle of CRLs + --pinnedpubkey=FILE/HASHES Public key (PEM/DER) file, or any number + of base64 encoded sha256 hashes preceded by + 'sha256//' and separated by ';', to verify + peer against + + --ciphers=STR Set the priority string (GnuTLS) or cipher list string (OpenSSL) directly. + Use with care. This option overrides --secure-protocol. + The format and syntax of this string depend on the specific SSL/TLS engine. + HSTS options: + --no-hsts disable HSTS + --hsts-file path of HSTS database (will override default) + + FTP options: + --ftp-user=USER set ftp user to USER + --ftp-password=PASS set ftp password to PASS + --no-remove-listing don't remove '.listing' files + --no-glob turn off FTP file name globbing + --no-passive-ftp disable the "passive" transfer mode + --preserve-permissions preserve remote file permissions + --retr-symlinks when recursing, get linked-to files (not dir) + + FTPS options: + --ftps-implicit use implicit FTPS (default port is 990) + --ftps-resume-ssl resume the SSL/TLS session started in the control connection when + opening a data connection + --ftps-clear-data-connection cipher the control channel only; all the data will be in plaintext + --ftps-fallback-to-ftp fall back to FTP if FTPS is not supported in the target server + WARC options: + --warc-file=FILENAME save request/response data to a .warc.gz file + --warc-header=STRING insert STRING into the warcinfo record + --warc-max-size=NUMBER set maximum size of WARC files to NUMBER + --warc-cdx write CDX index files + --warc-dedup=FILENAME do not store records listed in this CDX file + --no-warc-compression do not compress WARC files with GZIP + --no-warc-digests do not calculate SHA1 digests + --no-warc-keep-log do not store the log file in a WARC record + --warc-tempdir=DIRECTORY location for temporary files created by the + WARC writer + + Recursive download: + -r, --recursive specify recursive download + -l, --level=NUMBER maximum recursion depth (inf or 0 for infinite) + --delete-after delete files locally after downloading them + -k, --convert-links make links in downloaded HTML or CSS point to + local files + --convert-file-only convert the file part of the URLs only (usually known as the basename) + --backups=N before writing file X, rotate up to N backup files + -K, --backup-converted before converting file X, back up as X.orig + -m, --mirror shortcut for -N -r -l inf --no-remove-listing + -p, --page-requisites get all images, etc. needed to display HTML page + --strict-comments turn on strict (SGML) handling of HTML comments + + Recursive accept/reject: + -A, --accept=LIST comma-separated list of accepted extensions + -R, --reject=LIST comma-separated list of rejected extensions + --accept-regex=REGEX regex matching accepted URLs + --reject-regex=REGEX regex matching rejected URLs + --regex-type=TYPE regex type (posix|pcre) + -D, --domains=LIST comma-separated list of accepted domains + --exclude-domains=LIST comma-separated list of rejected domains + --follow-ftp follow FTP links from HTML documents + --follow-tags=LIST comma-separated list of followed HTML tags + --ignore-tags=LIST comma-separated list of ignored HTML tags + -H, --span-hosts go to foreign hosts when recursive + -L, --relative follow relative links only + -I, --include-directories=LIST list of allowed directories + --trust-server-names use the name specified by the redirection + URL's last component + -X, --exclude-directories=LIST list of excluded directories + -np, --no-parent don't ascend to the parent directory + + Email bug reports, questions, discussions to + and/or open issues at https://savannah.gnu.org/bugs/?func=additem&group=wget.` +} diff --git a/sshoneypot/handler/out.go b/sshoneypot/handler/out.go new file mode 100644 index 0000000..c964617 --- /dev/null +++ b/sshoneypot/handler/out.go @@ -0,0 +1,12 @@ +package handler + +import ( + "github.com/bytedream/sshoneypot/sshoneypot/info" + "strings" +) + +func Echo(cmdInfo *info.Info) { + if options, files, ok := basicFileCommandParser(cmdInfo, "--help"); ok { + cmdInfo.Writeln(strings.Join(append(options, files...), " ")) + } +} diff --git a/sshoneypot/handler/permission.go b/sshoneypot/handler/permission.go new file mode 100644 index 0000000..abeebd1 --- /dev/null +++ b/sshoneypot/handler/permission.go @@ -0,0 +1 @@ +package handler diff --git a/sshoneypot/handler/screen.go b/sshoneypot/handler/screen.go new file mode 100644 index 0000000..b1f8a5b --- /dev/null +++ b/sshoneypot/handler/screen.go @@ -0,0 +1,7 @@ +package handler + +import "github.com/bytedream/sshoneypot/sshoneypot/info" + +func Clear(info *info.Info) { + info.Write("\033[H\033[2J") +} diff --git a/sshoneypot/info/fs.go b/sshoneypot/info/fs.go new file mode 100644 index 0000000..f0881c1 --- /dev/null +++ b/sshoneypot/info/fs.go @@ -0,0 +1,401 @@ +package info + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "time" +) + +// file structure extracted from a debian buster docker container including: +// - curl 7.64.0 +// - net-tools +// - OpenSSH_7.9p1 +// - python3.7.3 +// - wget 1.20.1 +// - and all dependencies of the installed packages + +const ( + TypeDirectory = 4 + TypeCharacterDevice = 2 + TypeBlockDevice = 6 + TypeRegularFile = 10 + TypeFifo = 1 + TypeSymbolicLink = 12 + TypeSocketFile = 14 +) + +type BasicFile interface { + // This returns the directory in which the file is located. + // Be careful to not mix it up with Directory.Files + Directory() (Directory, bool) + RawDirectory() map[string]interface{} + + // If the file a dir + IsDir() bool + + // Removes the file and all sub files if the file is a directory + Remove() +} + +type Directory struct { + BasicFile + File + + Files []File +} + +func (dir Directory) Directory() (Directory, bool) {return dir.File.Directory()} +func (dir Directory) RawDirectory() map[string]interface{} { + return dir.dir +} +func (dir Directory) IsDir() bool { + return true +} +func (dir Directory) Remove() { + dir.File.Remove() +} + +type File struct { + BasicFile + fs *FileSystem + dir map[string]interface{} + location string + + Type uint8 + + Size int64 + Links int + Permissions os.FileMode + + Name string + CreationTimestamp int64 + UserID uint16 + GroupID uint16 +} + +func (f File) Directory() (Directory, bool) { + if dir, ok, _ := f.fs.getOrCreateFile(filepath.Dir(f.location), false, false, false); !ok { + return Directory{}, false + } else { + return dir.(Directory), true + } +} +func (f File) RawDirectory() map[string]interface{} {return f.dir} +func (f File) IsDir() bool { + return false +} +func (f File) Remove() { + parent, _ := filepath.Split(f.location) + dir, _ := f.fs.GetExplicitDirectory(parent, false) + delete(dir.dir["files"].(map[string]interface{}), strings.TrimSuffix(f.Name, string(os.PathSeparator))) +} + +func (f File) Location() string { + return f.location +} + +type FileSystem struct { + fs map[string]interface{} + info []interface{} + + currentDir string + rawCurrentDir map[string]interface{} + + Manipulate bool + + root bool + + fromFile string +} + +func (fs *FileSystem) Close() error { + if fs.Manipulate && fs.fromFile != "" { + file, err := os.Open(fs.fromFile) + if err != nil { + return err + } + return json.NewEncoder(file).Encode(fs.fs) + } + return nil +} + +func (fs *FileSystem) CreateDirectory(path string) (Directory, error) { + if fs.Manipulate { + dir, exist, created := fs.getOrCreateFile(path, false, true, false) + if exist { + return Directory{}, os.ErrExist + } else if !created { + return Directory{}, os.ErrPermission + } else { + return dir.(Directory), nil + } + } else { + return Directory{}, os.ErrPermission + } +} + +func (fs *FileSystem) CreateFile(path string) (File, error) { + if fs.Manipulate { + file, exist, created := fs.getOrCreateFile(path, false, true, false) + if exist { + return File{}, os.ErrExist + } else if !created { + return File{}, os.ErrPermission + } else { + return file.(File), nil + } + } else { + return File{}, os.ErrPermission + } +} + +func (fs *FileSystem) Cwd() Directory { + dir, _ := fs.GetFile("", false) + return dir.(Directory) +} + +func (fs *FileSystem) Exists(path string) bool { + _, exists, _ := fs.getOrCreateFile(path, false, false, false) + return exists +} + +func (fs *FileSystem) GetFile(path string, follow bool) (BasicFile, bool) { + file, ok, _ := fs.getOrCreateFile(path, follow, false, false) + return file, ok +} + +func (fs *FileSystem) GetExplicitDirectory(path string, follow bool) (Directory, bool) { + if file, ok, _ := fs.getOrCreateFile(path, follow, false, false); ok { + f, ok := file.(Directory) + return f, ok + } else { + return Directory{}, false + } +} + +func (fs *FileSystem) GetExplicitFile(path string) (File, bool) { + if file, ok, _ := fs.getOrCreateFile(path, false, false, false); ok { + f, ok := file.(File) + return f, ok + } else { + return File{}, false + } +} + +func (fs *FileSystem) Walk(path string, walkFunc func(path string, f BasicFile)) bool { + if dir, ok := fs.GetExplicitDirectory(path, false); ok { + resultDir := dir.RawDirectory()["files"].(map[string]interface{}) + for name, details := range resultDir { + switch details.(type) { + case []interface{}: + walkFunc(path, fs.generateFileInfo(resultDir, path, name, details.([]interface{}))) + case map[string]interface{}: + resultDir = details.(map[string]interface{})["files"].(map[string]interface{}) + resultInfo := details.(map[string]interface{})["info"].([]interface{}) + walkFunc(path, Directory{ + File: fs.generateFileInfo(resultDir, path, name, resultInfo), + Files: fs.directoryFiles(resultDir, path), + }) + } + } + return true + } else { + return false + } +} + +func (fs *FileSystem) generateFileInfo(resultDir map[string]interface{}, location string, name string, rawInfos []interface{}) File { + permissions := os.FileMode(uint32(rawInfos[3].(float64))) + + if uint8(rawInfos[0].(float64)) == 4 { + permissions |= os.ModeDir + } + + if permissions.IsDir() && !strings.HasSuffix(name, "/") { + name += "/" + } + if strings.Contains(name, " ") && !strings.HasPrefix(name, "'") && !strings.HasSuffix(name, "'") { + name = "'" + name + "'" + } + + return File{ + fs: fs, + dir: resultDir, + location: location, + + Type: uint8(rawInfos[0].(float64)), + Size: int64(rawInfos[1].(float64)), + Links: int(rawInfos[2].(float64)), + Permissions: permissions, + + Name: name, + CreationTimestamp: int64(rawInfos[4].(float64)), + UserID: uint16(rawInfos[5].(float64)), + GroupID: uint16(rawInfos[6].(float64)), + } +} + +func (fs *FileSystem) getOrCreateFile(path string, follow bool, create bool, createDir bool) (file BasicFile, exists bool, created bool) { + var isFile bool + rawDirectory := fs.rawCurrentDir + resultDir := fs.rawCurrentDir["files"].(map[string]interface{}) + var resultFile []interface{} + + if !strings.HasPrefix(path, "/") { + path = filepath.Join(fs.currentDir, path) + } + + location, name := filepath.Split(path) + splitPath := strings.Split(path, string(os.PathSeparator))[1:] + + if path == "/" { + resultDir = fs.fs + resultFile = fs.info + + location = "" + name = "/" + splitPath = make([]string, 0) + } + + for i, s := range splitPath { + if s == "" { + splitPath = append(splitPath[:i], splitPath[i+1:]...) + } + } + + for _, file := range splitPath { + if dir, ok := resultDir[file]; ok { + if preCurrentFile, ok := dir.(map[string]interface{}); ok { + rawDirectory = preCurrentFile + resultDir = preCurrentFile["files"].(map[string]interface{}) + resultFile = preCurrentFile["info"].([]interface{}) + } else if preCurrentFile, ok := dir.([]interface{}); ok { + isFile = true + resultFile = preCurrentFile + break + } + } else { + return nil, false, false + } + } + + if resultFile == nil { + if create { + timestamp := time.Now().Unix() + file := File{ + fs: fs, + dir: resultDir, + location: location, + + Type: TypeRegularFile, + Size: 0, + // i dont really know why its 1, linux logic i think + Links: 1, + Permissions: os.ModeType, + + Name: name, + CreationTimestamp: timestamp, + UserID: 1000, + GroupID: 1000, + } + if fs.root { + file.UserID = 0 + file.GroupID = 0 + } + if createDir { + file.Type = TypeDirectory + // i dont really know why its 2, linux logic i think + file.Links = 2 + file.Permissions = os.ModeDir + resultDir[name] = map[string]interface{} {"files": make([]string, 0), + "info": []interface{} {file.Type, file.Size, file.Links, file.Permissions, file.CreationTimestamp, file.UserID, file.GroupID}} + return Directory{ + BasicFile: file, + Files: make([]File, 0), + }, false, true + } else { + resultDir[name] = []interface{} {file.Type, file.Size, file.Links, file.Permissions, file.CreationTimestamp, file.UserID, file.GroupID} + } + if follow && strings.HasPrefix(path, "/") { + fs.currentDir = path + } else { + fs.currentDir = filepath.Join(fs.currentDir, path) + } + return file, false, true + } else { + return nil, false, false + } + } + + file = fs.generateFileInfo(rawDirectory, location, name, resultFile) + + if !isFile { + var files []File + + for fname, info := range resultDir { + switch info.(type) { + // case normal file + case []interface{}: + info := info.([]interface{}) + files = append(files, fs.generateFileInfo(rawDirectory, location, fname, info)) + // case dir + case map[string]interface{}: + info := info.(map[string]interface{})["info"].([]interface{}) + files = append(files, fs.generateFileInfo(rawDirectory, location, fname, info)) + } + } + + if follow { + fs.currentDir = path + } + + return Directory{ + File: file.(File), + Files: fs.directoryFiles(resultDir, location), + }, true, false + } else { + return file, true, false + } +} + +func (fs *FileSystem) directoryFiles(rawDirectory map[string]interface{}, location string) []File { + var files []File + + for fname, info := range rawDirectory { + switch info.(type) { + // case normal file + case []interface{}: + info := info.([]interface{}) + files = append(files, fs.generateFileInfo(rawDirectory, location, fname, info)) + // case dir + case map[string]interface{}: + info := info.(map[string]interface{})["info"].([]interface{}) + files = append(files, fs.generateFileInfo(rawDirectory, location, fname, info)) + } + } + + return files +} + +func LoadFSFromJson(fsFile string) (*FileSystem, error) { + file, err := os.Open(fsFile) + if err != nil { + return nil, err + } + + fs := make(map[string]interface{}) + if err := json.NewDecoder(file).Decode(&fs); err != nil { + return nil, err + } + return &FileSystem{ + fs: fs["files"].(map[string]interface{}), + info: fs["info"].([]interface{}), + + rawCurrentDir: fs, + currentDir: "/", + + fromFile: fsFile, + }, nil +} diff --git a/sshoneypot/info/handler.go b/sshoneypot/info/handler.go new file mode 100644 index 0000000..a4c78fd --- /dev/null +++ b/sshoneypot/info/handler.go @@ -0,0 +1,28 @@ +package info + +type Handler interface { + //AutoCompleteOptions returns the extra arguments for a command (e.g. the `-a` or `-R` option in the `ls` command) + AutoCompleteOptions() []string + + //AutoCompleteRequest gets called if the client request auto complete (e.g. if double pressing the `tab` key) + AutoCompleteRequest(info *Info) (accepted bool) + + //Handle will execute / handle the command the client has entered + Handle(info *Info) + + //Kill gets called if the user hard stops the command (ctrl + c) + Kill() +} + +type SimpleHandler struct { + Handler +} + +func (sh SimpleHandler) AutoCompleteOptions() []string { + return make([]string, 0) +} +func (sh SimpleHandler) AutoCompleteRequest(info *Info) (accepted bool) { + return false +} +func (sh SimpleHandler) Handle(info *Info) {} +func (sh SimpleHandler) Kill(){} diff --git a/sshoneypot/info/info.go b/sshoneypot/info/info.go new file mode 100644 index 0000000..3f4c4f2 --- /dev/null +++ b/sshoneypot/info/info.go @@ -0,0 +1,109 @@ +package info + +import ( + "fmt" + "golang.org/x/crypto/ssh" + "golang.org/x/term" + "net" +) + +type Termina struct { + *term.Terminal + + // The current prompt string + Prompt string + + Width, Height uint32 +} + +func (t *Termina) SetPrompt(prompt string) { + t.Prompt = prompt + + t.Terminal.SetPrompt(prompt) +} + +type Info struct { + Conn net.Conn + Terminal *Termina + + FS *FileSystem + User *ssh.ServerConn + + FullArgs []string + Command string + Args []string + + Stdout bool + Stderr bool +} + +func (info Info) Read() (content string, err error) { + if info.Terminal != nil { + oldPrompt := info.Terminal.Prompt + info.Terminal.SetPrompt("") + content, err = info.Terminal.ReadLine() + info.Terminal.SetPrompt(oldPrompt) + } else { + var buffer []byte + _, err = info.Conn.Read(buffer) + content = string(buffer) + } + return content, err +} + +func (info Info) ReadPrompt() (content string, err error) { + if info.Terminal != nil { + content, err = info.Terminal.ReadLine() + } else { + var buffer []byte + _, err = info.Conn.Read(buffer) + content = string(buffer) + } + return content, err +} + +func (info Info) Write(content string) (err error) { + if info.Stdout { + if info.Terminal != nil { + _, err = info.Terminal.Write([]byte(content)) + } else { + _, err = info.Conn.Write([]byte(content)) + } + } + return err +} + +func (info Info) Writef(format string, args... interface{}) error { + return info.Write(fmt.Sprintf(format, args...)) +} + +func (info Info) Writeln(content string) (err error) { + return info.Write(content + "\n") +} + +func (info Info) Writelnf(format string, args... interface{}) (err error) { + return info.Writeln(fmt.Sprintf(format, args...)) +} + +func (info Info) Error(content string) (err error) { + if info.Stderr { + if info.Terminal != nil { + _, err = info.Terminal.Write([]byte(content)) + } else { + _, err = info.Conn.Write([]byte(content)) + } + } + return err +} + +func (info Info) Errorf(format string, args... interface{}) error { + return info.Error(fmt.Sprintf(format, args...)) +} + +func (info Info) Errorln(content string) (err error) { + return info.Error(content + "\n") +} + +func (info Info) Errorlnf(format string, args... interface{}) (err error) { + return info.Errorln(fmt.Sprintf(format, args...)) +} diff --git a/sshoneypot/info/terminal.go b/sshoneypot/info/terminal.go new file mode 100644 index 0000000..dc06ea2 --- /dev/null +++ b/sshoneypot/info/terminal.go @@ -0,0 +1,179 @@ +package info + +import ( + "errors" + "fmt" + "golang.org/x/crypto/ssh" + "strconv" + "sync" +) + +type Terminal struct { + // the chan + channel ssh.Channel + + // The current prompt string + prompt string + + width, height int + + isListening bool + buffer []byte + + lock *sync.Mutex +} + +const ( + keyCtrlC = 3 + keyCtrlD = 4 + keyEnter = 13 + keyEscape = 27 + keyReturn = 127 + keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota + keyUp + keyDown + keyLeft + keyRight + keyAltLeft + keyAltRight + keyHome + keyEnd + keyDeleteWord + keyDeleteLine + keyClearScreen + keyPasteStart + keyPasteEnd +) + +var ( + CtrlC = errors.New("CtrlC") + CtrlD = errors.New("CtrlD") +) + +func (term *Terminal) GetSize() (width int, height int) { + return term.width, term.height +} + +func (term *Terminal) SetSize(width int, height int) { + term.width = width + term.height = height +} + +func (term *Terminal) GetPrompt() string { + return term.prompt +} + +func (term *Terminal) SetPrompt(prompt string) { + term.prompt = prompt +} + +func (term *Terminal) ReadLine() (string, error) { + var content []byte + for { + data, _ := term.listen() + for _, b := range data { + fmt.Println(b) + switch []rune(data) { + case keyCtrlD: + if len(content) == 0 { + return "", CtrlD + } + case keyEnter: + term.channel.Write([]byte("\r\n" + term.prompt)) + return string(data), nil + case keyReturn: + if len(content) - 1 > len(term.prompt) { + content = content[:len(content) - 1] + term.MoveCursor(0, 0, 1, 0) + term.channel.Write(content) + } + case keyRight: + fmt.Println("aaa") + default: + content = append(content, b) + term.channel.Write(data) + } + } + } +} + +func (term *Terminal) Write(data []byte) (n int, err error) { + return term.channel.Write(data) +} + +func (term *Terminal) MoveCursor(up, down, left, right int) { + var m []rune + + // 1 unit up can be expressed as ^[[A or ^[A + // 5 units up can be expressed as ^[[5A + + if up == 1 { + m = append(m, keyEscape, '[', 'A') + } else if up > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(up))...) + m = append(m, 'A') + } + + if down == 1 { + m = append(m, keyEscape, '[', 'B') + } else if down > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(down))...) + m = append(m, 'B') + } + + if right == 1 { + m = append(m, keyEscape, '[', 'C') + } else if right > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(right))...) + m = append(m, 'C') + } + + if left == 1 { + m = append(m, keyEscape, '[', 'D') + } else if left > 1 { + m = append(m, keyEscape, '[') + m = append(m, []rune(strconv.Itoa(left))...) + m = append(m, 'D') + } + + term.Write([]byte(string(m))) +} + +func (term *Terminal) ListenCtrlC() chan bool { + ctrlc := make(chan bool, 1) + go func() { + if term.isListening { + return + } + for { + data, err := term.listen() + if err == CtrlC { + ctrlc <- false + return + } + if term.isListening { + ctrlc <- false + term.buffer = data + } + } + }() + return ctrlc +} + +func (term *Terminal) listen() ([]byte, error) { + buffer := make([]byte, 512) + n, err := term.channel.Read(buffer) + return buffer[:n], err +} + +func NewTerminal(channel ssh.Channel) *Terminal { + return &Terminal{ + channel: channel, + + lock: &sync.Mutex{}, + } +} + diff --git a/sshoneypot/logging.go b/sshoneypot/logging.go new file mode 100644 index 0000000..aeda16f --- /dev/null +++ b/sshoneypot/logging.go @@ -0,0 +1,41 @@ +package sshoneypot + +import ( + "fmt" + "log" +) + +// A simple high changeable logging interface +type Logging struct { + enable bool +} + +func (l *Logging) Info(content string) { + if l.enable { + log.Println(content) + } +} + +func (l *Logging) Infof(format string, args... interface{}) { + log.Printf(fmt.Sprintf(format, args)) +} + +func (l *Logging) Warn(content string) { + if l.enable { + log.Println(content) + } +} + +func (l *Logging) Warnf(format string, args... interface{}) { + log.Printf(fmt.Sprintf(format, args)) +} + +func (l *Logging) Error(content string) { + if l.enable { + log.Println(content) + } +} + +func (l *Logging) Errorf(format string, args... interface{}) { + l.Error(fmt.Sprintf(format, args)) +} diff --git a/sshoneypot/rsa.go b/sshoneypot/rsa.go new file mode 100644 index 0000000..7eaabce --- /dev/null +++ b/sshoneypot/rsa.go @@ -0,0 +1,45 @@ +package sshoneypot + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "golang.org/x/crypto/ssh" + "io/ioutil" +) + +// LoadSSHKey loads a ssh key out of bytes +func LoadSSHKey(keyBytes []byte) (ssh.Signer, error) { + // parse the read key bytes + key, err := ssh.ParsePrivateKey(keyBytes) + if err != nil { + return nil, err + } + return key, nil +} + +// LoadSSHKeyFromFile loads a ssh key out of a file +func LoadSSHKeyFromFile(file string) (ssh.Signer, error) { + keyBytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + return LoadSSHKey(keyBytes) +} + +// GenerateSSHKey generates a new rsa ssh key +func GenerateSSHKey() ([]byte, error) { + // create a key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + privateKeyBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + } + return pem.EncodeToMemory(&privateKeyBlock), nil +} diff --git a/sshoneypot/sshoneypot.go b/sshoneypot/sshoneypot.go new file mode 100644 index 0000000..de79548 --- /dev/null +++ b/sshoneypot/sshoneypot.go @@ -0,0 +1,362 @@ +package sshoneypot + +import ( + "errors" + "fmt" + "github.com/bytedream/sshoneypot/sshoneypot/handler" + "github.com/bytedream/sshoneypot/sshoneypot/info" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" + "io" + "net" + "os" + "path/filepath" + "runtime" + "sort" + "strings" +) + +const keyTab = 9 + +type SSHoneypot struct { + AllowedUsers []string + AllowedPasswords []string + + Panic bool + Logger *Logging + Handler map[string]info.Handler + FS *info.FileSystem + + Port uint16 + ServerConfig *ssh.ServerConfig +} + +func (honeypot SSHoneypot) AddHandler(name string, handler info.Handler, aliases... string) error { + for _, name := range append([]string{name}, aliases...) { + if _, exist := honeypot.Handler[name]; exist { + return errors.New("The handler name '" + name + "' already exists") + } + honeypot.Handler[name] = handler + } + return nil +} + +type HandlerFunc func(info *info.Info) + +//simpleHandler is a easy implementation of Handler / SimpleHandler for the AddHandlerFunc method below +type simpleHandler struct { + info.Handler + + HandlerName string + handleFunc HandlerFunc +} + +func (sh simpleHandler) AutoCompleteOptions() []string { + return make([]string, 0) +} +func (sh simpleHandler) AutoCompleteRequest(info *info.Info) (accepted bool) { + return false +} +func (sh simpleHandler) Handle(info *info.Info) { + sh.handleFunc(info) +} +func (sh simpleHandler) Kill() {} + +func (honeypot SSHoneypot) AddHandlerFunc(name string, handler HandlerFunc, aliases... string) error { + return honeypot.AddHandler(name, simpleHandler{handleFunc: handler}, aliases...) +} + +// Serve starts the ssh server and serve it until the program ends +func (honeypot SSHoneypot) Serve() error { + // starts the ssh listener + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", honeypot.Port)) + if err != nil { + return err + } + defer listener.Close() + + for { + conn, err := listener.Accept() + if err != nil { + honeypot.Logger.Warnf("Failed to accept new client %s: %s\n", conn.RemoteAddr(), err) + continue + } + go honeypot.handleConn(conn) + } +} + +func (honeypot SSHoneypot) handleConn(conn net.Conn) { + defer conn.Close() + + serverConn, channels, requests, err := ssh.NewServerConn(conn, honeypot.ServerConfig) + if err != nil { + honeypot.Logger.Warnf("Failed to create new connection with %s: %s\n", conn.RemoteAddr(), err) + return + } + term := &info.Termina{} + go handleRequest(requests, term) + for channel := range channels { + go honeypot.handleChannel(conn, channel, serverConn, term) + } + honeypot.Logger.Infof("%s closed the connection\n", conn.RemoteAddr()) +} + +func (honeypot SSHoneypot) handleChannel(conn net.Conn, newChannel ssh.NewChannel, serverConn *ssh.ServerConn, term *info.Termina) { + channel, request, err := newChannel.Accept() + if err != nil { + return + } + + for { + info.NewTerminal(channel).ReadLine() + } + + term.Terminal = terminal.NewTerminal(channel, "") + term.SetPrompt(fmt.Sprintf("%s:%s $ ", serverConn.User(), "/")) + term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) { + if key == keyTab { + if command, err := parseCommand(line); len(command) > 0 && err == nil { + if cmd, exists := honeypot.Handler[command[0]]; exists { + cmdInfo := &info.Info{Conn: conn, Terminal: term, FS: honeypot.FS, User: serverConn, FullArgs: command, Command: command[0], Args: command[1:], Stdout: true, Stderr: true} + if cmd.AutoCompleteRequest != nil && !cmd.AutoCompleteRequest(cmdInfo){ + if options := cmd.AutoCompleteOptions(); options != nil && len(options) == 0 { + newLine, newPos, ok = honeypot.fileAutoComplete(cmdInfo, line) + } else if len(options) == 1 { + newLine = line + options[0] + } + } + } + } + } + return + } + go handleRequest(request, term) + for { + line, err := term.ReadLine() + if err != nil { + if err == io.EOF { + if honeypot.OnClose(channel) { + return + } + } + } else { + fmt.Println(line) + /*if !honeypot.handleLine(line, conn, channel, serverConn, term) { + if honeypot.OnClose(channel) { + return + } + }*/ + } + } +} + +func (honeypot SSHoneypot) handleLine(line string, conn net.Conn, channel ssh.Channel, serverConn *ssh.ServerConn, term *info.Termina) string { + command, err := parseCommand(line) + if err == nil && len(command) > 0 { + clientInfo := &info.Info{Conn: conn, Terminal: term, FS: honeypot.FS, User: serverConn, FullArgs: command, Command: command[0], Args: command[1:], Stdout: true, Stderr: true} + var redirect bool + for i, arg := range clientInfo.Args { + if redirect { + redirect = false + clientInfo.Args = append(clientInfo.Args[:i], clientInfo.Args[i+1:]...) + } + switch arg { + case "2>", "&>": + clientInfo.Stderr = false + fallthrough + case ">", "1>", ">>": + clientInfo.Stdout = false + + clientInfo.Args = append(clientInfo.Args[:i], clientInfo.Args[i+1:]...) + } + } + if redirect { + clientInfo.Errorln("-bash: syntax error near unexpected token 'newline'") + return "" + } + + finished := make(chan bool, 1) + newLine := "" + + go func() { + for { + var buffer = make([]byte, 512) + n, _ := channel.Read(buffer) + select { + case <- finished: + newLine = string(buffer[:n]) + return + default: + if buffer[0] == 3 { + clientInfo.Stderr = false + clientInfo.Stdout = false + finished <- true + } + } + } + }() + + if commandHandler, ok := honeypot.Handler[command[0]]; ok { + if honeypot.Panic { + commandHandler.Handle(clientInfo) + } else { + func() { + defer func() { + if r := recover(); r != nil { + buffer := make([]byte, 2048) + written := runtime.Stack(buffer, false) + honeypot.Logger.Warnf("Unexpected panic: %s\n%s", r, buffer[:written]) + } + }() + commandHandler.Handle(clientInfo) + }() + } + + // the following part is the implementation of ctrl + c / ctrl + d when a process is running. + // sadly the ssh library doesn't support ctrl + c / some other keys to handle their input directly, + // so a second empty terminal has to be opened. + // and because of this ugly implementation, the key up / down buttons to get the recent entered command are not working :/ + + + + return newLine + } else { + handler.Default(clientInfo) + } + } + return "" +} + +func (honeypot SSHoneypot) fileAutoComplete(cmdInfo *info.Info, line string) (newLine string, newPos int, ok bool) { + var names []string + + if len(cmdInfo.Args) > 0 { + var dir info.Directory + search := cmdInfo.Args[len(cmdInfo.Args) - 1] + + if strings.HasPrefix(search, string(os.PathSeparator)) { + dir, _ = honeypot.FS.GetExplicitDirectory("/", false) + } else { + dir = honeypot.FS.Cwd() + } + + for _, file := range dir.Files { + if strings.HasPrefix(file.Name, search) && file.Name != search { + names = append(names, file.Name) + } + } + if len(names) == 1 && strings.HasPrefix(search, string(os.PathSeparator)) { + names[0] = filepath.Join(string(os.PathSeparator), names[0]) + } else if len(names) == 0 && strings.Contains(search, string(os.PathSeparator)) { + parent, _ := filepath.Split(search) + if dir, ok := honeypot.FS.GetExplicitDirectory(parent, false); ok { + for _, file := range dir.Files { + if strings.HasPrefix(filepath.Join(parent, file.Name), search) && file.Name != search { + names = append(names, file.Name) + } + } + } + if len(names) == 1 { + names[0] = filepath.Join(parent, names[0]) + string(os.PathSeparator) + } + } + } else { + + dir := honeypot.FS.Cwd() + for _, file := range dir.Files { + names = append(names, file.Name) + } + } + + if len(names) == 0 { + return + } else if len(names) == 1 { + ok = true + if len(cmdInfo.Args) == 0 { + newLine += names[0] + } else { + newLine = line[:strings.LastIndex(line, " ")] + " " + names[0] + } + newPos = len(newLine) + } else { + honeypot.OptionsAutoComplete(names, cmdInfo, line) + } + return +} + +// OptionsAutoComplete will show all given options in the console. +// e.g. all files in the current directory when double pressing `tab` after the `ls` command +func (honeypot SSHoneypot) OptionsAutoComplete(options []string, info *info.Info, line string) { + sort.Strings(options) + + var length int + for _, option := range options { + if l := len(option); length < l { + length = l + } + } + length++ + + var builder strings.Builder + var perline int + if info.Terminal.Width > 0 { + perline = int(info.Terminal.Width) / length + } else { + perline = 80 / length + } + builder.WriteString(info.Terminal.Prompt + line + "\n") + for i, option := range options { + if i % perline == 0 && i != 0 { + builder.WriteString("\n") + } + builder.WriteString(option + strings.Repeat(" ", length - len(option))) + } + builder.WriteString("\n") + + info.Write(builder.String()) +} + +// --- The following methods were created to allow user to manipulate the ssh process as they wish --- // + +// OnClose gets called when the client send an exit signal. +// If the return bool is true, the channel will be closed an the client disconnected +func (honeypot SSHoneypot) OnClose(channel ssh.Channel) (close bool) { + channel.SendRequest("exit-status", false, ssh.Marshal(struct {Status uint32}{Status: 0})) + channel.Close() + return true +} + +// --- // + +func DefaultSSHoneypot(fs *info.FileSystem, key ssh.Signer) SSHoneypot { + serverConfig := &ssh.ServerConfig{ + PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { + return nil, nil + }, + ServerVersion: "SSH-2.0-OpenSSH_8.5", + } + serverConfig.AddHostKey(key) + + honeypot := SSHoneypot{ + Logger: &Logging{enable: true}, + Port: 2222, + ServerConfig: serverConfig, + + Handler: map[string]info.Handler{}, + + FS: fs, + } + honeypot.AddHandler("cat", handler.Cat{}, "/bin/cat") + honeypot.AddHandler("cd", handler.CD{}) + honeypot.AddHandler("ls", handler.LS{}, "/bin/ls") + honeypot.AddHandler("mkdir", handler.Mkdir{}, "/bin/mkdir") + honeypot.AddHandler("rm", handler.RM{}, "/bin/rm") + + honeypot.AddHandlerFunc("clear", handler.Clear, "/bin/clear") + honeypot.AddHandlerFunc("echo", handler.Echo, "/bin/echo") + + honeypot.AddHandlerFunc("ifconfig", handler.Ifconfig, "/sbin/ifconfig") + honeypot.AddHandler("ping", handler.Ping{}, "/bin/ping", "/bin/ping4", "/bin/ping6") + + return honeypot +}