Initial commit
This commit is contained in:
commit
0873cadc4c
0
.idea/.gitignore
generated
vendored
Normal file
0
.idea/.gitignore
generated
vendored
Normal file
6
.idea/discord.xml
generated
Normal file
6
.idea/discord.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/sshoneypot.iml" filepath="$PROJECT_DIR$/.idea/sshoneypot.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
110
.idea/workspace.xml
generated
Normal file
110
.idea/workspace.xml
generated
Normal file
@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="84062d71-ebe8-4cc4-8ed7-9375510de988" name="Default Changelist" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="FileTemplateManagerImpl">
|
||||
<option name="RECENT_TEMPLATES">
|
||||
<list>
|
||||
<option value="Go Application" />
|
||||
<option value="Go File" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="GOROOT" url="file:///usr/lib/go" />
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="GitSEFilterConfiguration">
|
||||
<file-type-list>
|
||||
<filtered-out-file-type name="LOCAL_BRANCH" />
|
||||
<filtered-out-file-type name="REMOTE_BRANCH" />
|
||||
<filtered-out-file-type name="TAG" />
|
||||
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
|
||||
</file-type-list>
|
||||
</component>
|
||||
<component name="GoLibraries">
|
||||
<option name="indexEntireGoPath" value="false" />
|
||||
</component>
|
||||
<component name="ProjectId" id="1pZZ5aRCduBrvaNLZvRCKVbHVxi" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="DefaultGoTemplateProperty" value="Go File" />
|
||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
||||
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||
<property name="dart.analysis.tool.window.visible" value="false" />
|
||||
<property name="go.import.settings.migrated" value="true" />
|
||||
<property name="go.sdk.automatically.set" value="true" />
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/sshoneypot/info" />
|
||||
<recent name="$PROJECT_DIR$/sshoneypot" />
|
||||
<recent name="$PROJECT_DIR$" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager" selected="Go Build.go build github.com/bytedream/sshoneypot">
|
||||
<configuration name="go build github.com/bytedream/sshoneypot" type="GoApplicationRunConfiguration" factoryName="Go Application" temporary="true" nameIsGenerated="true">
|
||||
<module name="sshoneypot" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/bytedream/sshoneypot" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="dump_fs" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="sshoneypot" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/dump_fs.py" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="Go Build.go build github.com/bytedream/sshoneypot" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="oldMeFiltersMigrated" value="true" />
|
||||
</component>
|
||||
</project>
|
143
README.md
Normal file
143
README.md
Normal file
@ -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=<client> user=root
|
||||
sshd[8128]: Failed password for root from <client> port 37510 ssh2
|
||||
sshd[8128]: Received disconnect from <client> port 37510:11: [preauth]
|
||||
sshd[8128]: Disconnected from <client> port 37510 [preauth]
|
||||
sshd[8141]: Received disconnect from <client> port 59353:11: [preauth]
|
||||
sshd[8141]: Disconnected from <client> port 59353 [preauth]
|
||||
sshd[8151]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=<client> user=root
|
||||
sshd[8151]: Failed password for root from <client> port 63785 ssh2
|
||||
sshd[8159]: Received disconnect from <client> port 24889:11: [preauth]
|
||||
sshd[8159]: Disconnected from <client> 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=<client>:45782
|
||||
Login: client=<client>:45782, user="root", password="cisco"
|
||||
Established SSH connection: client=<client>:45782
|
||||
New channel: clinet=<client>:45782, type=direct-tcpip, payload={DestinationAddress:<something> DestinationPort:110 SourceAddress:192.168.0.1 SourcePort:0}
|
||||
Failed to read from channel: EOF
|
||||
New channel: clinet=<client>:45782, type=direct-tcpip, payload={DestinationAddress:<something> DestinationPort:143 SourceAddress:192.168.0.1 SourcePort:0}
|
||||
Failed to read from channel: EOF
|
||||
New channel: clinet=<client>:45782, type=direct-tcpip, payload={DestinationAddress:<something> DestinationPort:587 SourceAddress:192.168.0.1 SourcePort:0}
|
||||
Failed to read from channel: EOF
|
||||
New channel: clinet=<client>:45782, type=direct-tcpip, payload={DestinationAddress:<something> DestinationPort:587 SourceAddress:192.168.0.1 SourcePort:0}
|
||||
Failed to read from channel: EOF
|
||||
New channel: clinet=<client>:45782, type=session, payload=[]
|
||||
Request: client=<client>:45782, channel=session, type=exec, payload={Command:/sbin/ifconfig}
|
||||
Failed to read from terminal: EOF
|
||||
New channel: clinet=<client>:45782, type=session, payload=[]
|
||||
Request: client=<client>:45782, channel=session, type=exec, payload={Command:cat /proc/meminfo}
|
||||
Failed to read from terminal: EOF
|
||||
New channel: clinet=<client>:45782, type=session, payload=[]
|
||||
Request: client=<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=<client>:45782, type=session, payload=[]
|
||||
Request: client=<client>:45782, channel=session, type=exec, payload={Command:cat /proc/version}
|
||||
Failed to read from terminal: EOF
|
||||
New channel: clinet=<client>:45782, type=session, payload=[]
|
||||
Request: client=<client>:45782, channel=session, type=exec, payload={Command:uptime}
|
||||
Failed to read from terminal: EOF
|
||||
Disconnect: client=<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
|
63
dump_fs.py
Normal file
63
dump_fs.py
Normal file
@ -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+'))
|
41
main.go
Normal file
41
main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
27
ssh.key
Executable file
27
ssh.key
Executable file
@ -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-----
|
112
sshoneypot/handle.go
Normal file
112
sshoneypot/handle.go
Normal file
@ -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
|
||||
}
|
538
sshoneypot/handler/fs.go
Normal file
538
sshoneypot/handler/fs.go
Normal file
@ -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: <https://www.gnu.org/software/coreutils/>
|
||||
Report cat translation bugs to <https://translationproject.org/team/>
|
||||
Full documentation at: <https://www.gnu.org/software/coreutils/cat>
|
||||
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 FORMAT1<newline>FORMAT2,
|
||||
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: <https://www.gnu.org/software/coreutils/>
|
||||
Report ls translation bugs to <https://translationproject.org/team/>
|
||||
Full documentation at: <https://www.gnu.org/software/coreutils/ls>
|
||||
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: <https://www.gnu.org/software/coreutils/>
|
||||
Report mkdir translation bugs to <https://translationproject.org/team/>
|
||||
Full documentation at: <https://www.gnu.org/software/coreutils/mkdir>
|
||||
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: <https://www.gnu.org/software/coreutils/>
|
||||
Report rm translation bugs to <https://translationproject.org/team/>
|
||||
Full documentation at: <https://www.gnu.org/software/coreutils/rm>
|
||||
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: <https://www.gnu.org/software/coreutils/>
|
||||
Report touch translation bugs to <https://translationproject.org/team/>
|
||||
Full documentation at: <https://www.gnu.org/software/coreutils/touch>
|
||||
or available locally via: info '(coreutils) touch invocation'`
|
||||
}
|
27
sshoneypot/handler/handler.go
Normal file
27
sshoneypot/handler/handler.go
Normal file
@ -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)
|
||||
}
|
382
sshoneypot/handler/net.go
Normal file
382
sshoneypot/handler/net.go
Normal file
@ -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] <interface> [[<AF>] <address>]
|
||||
[add <address>[/<prefixlen>]]
|
||||
[del <address>[/<prefixlen>]]
|
||||
[[-]broadcast [<address>]] [[-]pointopoint [<address>]]
|
||||
[netmask <address>] [dstaddr <address>] [tunnel <address>]
|
||||
[outfill <NN>] [keepalive <NN>]
|
||||
[hw <HW> <address>] [mtu <NN>]
|
||||
[[-]trailers] [[-]arp] [[-]allmulti]
|
||||
[multicast] [[-]promisc]
|
||||
[mem_start <NN>] [io_addr <NN>] [irq <NN>] [media <type>]
|
||||
[txqueuelen <NN>]
|
||||
[[-]dynamic]
|
||||
[up|down] ...
|
||||
|
||||
<HW>=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)
|
||||
<AF>=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<UP,BROADCAST,RUNNING,MULTICAST> 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<UP,LOOPBACK,RUNNING> 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 <bug-wget@gnu.org>
|
||||
and/or open issues at https://savannah.gnu.org/bugs/?func=additem&group=wget.`
|
||||
}
|
12
sshoneypot/handler/out.go
Normal file
12
sshoneypot/handler/out.go
Normal file
@ -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...), " "))
|
||||
}
|
||||
}
|
1
sshoneypot/handler/permission.go
Normal file
1
sshoneypot/handler/permission.go
Normal file
@ -0,0 +1 @@
|
||||
package handler
|
7
sshoneypot/handler/screen.go
Normal file
7
sshoneypot/handler/screen.go
Normal file
@ -0,0 +1,7 @@
|
||||
package handler
|
||||
|
||||
import "github.com/bytedream/sshoneypot/sshoneypot/info"
|
||||
|
||||
func Clear(info *info.Info) {
|
||||
info.Write("\033[H\033[2J")
|
||||
}
|
401
sshoneypot/info/fs.go
Normal file
401
sshoneypot/info/fs.go
Normal file
@ -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
|
||||
}
|
28
sshoneypot/info/handler.go
Normal file
28
sshoneypot/info/handler.go
Normal file
@ -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(){}
|
109
sshoneypot/info/info.go
Normal file
109
sshoneypot/info/info.go
Normal file
@ -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...))
|
||||
}
|
179
sshoneypot/info/terminal.go
Normal file
179
sshoneypot/info/terminal.go
Normal file
@ -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{},
|
||||
}
|
||||
}
|
||||
|
41
sshoneypot/logging.go
Normal file
41
sshoneypot/logging.go
Normal file
@ -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))
|
||||
}
|
45
sshoneypot/rsa.go
Normal file
45
sshoneypot/rsa.go
Normal file
@ -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
|
||||
}
|
362
sshoneypot/sshoneypot.go
Normal file
362
sshoneypot/sshoneypot.go
Normal file
@ -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
|
||||
}
|
Reference in New Issue
Block a user