├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .travis.yml
├── Dockerfile
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE
├── README.md
├── appveyor.yml
├── cmd
├── createfs
│ ├── createFSZip.go
│ ├── createFSZip_darwin.go
│ ├── createFSZip_test.go
│ ├── createFSZip_unix.go
│ └── createFSZip_windows.go
└── syrup
│ └── main.go
├── cmdOutput
├── lscpu
└── lspci
├── commands.txt
├── config.yaml
├── filesystem.zip
├── go.mod
├── go.sum
├── group
├── logs
├── .gitignore
└── sessions
│ └── .gitignore
├── net
└── conn.go
├── os
├── account.go
├── command
│ ├── cat.go
│ ├── id.go
│ ├── ls.go
│ ├── pwd.go
│ ├── scp.go
│ ├── uname.go
│ ├── uptime.go
│ ├── wget.go
│ └── whoami.go
├── shell.go
└── sys.go
├── passwd
├── sftp
├── attrflag_string.go
├── packettype_string.go
├── sftp.go
├── sftp_test.go
├── statuscode_string.go
└── types.go
├── ssh.go
├── util
├── abuseipdb
│ └── report.go
├── elastichook.go
└── termlogger
│ ├── asciicast.go
│ ├── logger.go
│ ├── types.go
│ └── uml.go
└── virtualfs
├── file.go
├── fileinfo.go
├── filesystem.go
└── filesystem_test.go
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **Environment**
17 | - OS: [e.g. Ubuntu Trusty]
18 | - Version [e.g. Latest/0.6/Docker Nightly]
19 |
20 | **Additional context**
21 | Add any other context about the problem here.
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | logs/**
3 | authorized_keys
4 | debug
5 | id_rsa
6 | vendor/**
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | sudo: false
3 |
4 | go:
5 | - 1.9.x
6 | - 1.10.x
7 | - "1.11"
8 | - master
9 |
10 | script:
11 | - go test -v ./...
12 |
13 | before_deploy:
14 | # Script copied from https://github.com/zabawaba99/go-travis-github-release/
15 | - PLATFORMS=(darwin/amd64 linux/amd64 windows/amd64 linux/arm)
16 | - GOARM=7
17 | - |
18 | for PLATFORM in "${PLATFORMS[@]}"; do
19 | echo "Building $PLATFORM"
20 | GOOS=${PLATFORM%/*}
21 | GOARCH=${PLATFORM#*/}
22 | if [ "$GOOS" = "windows" ]; then
23 | build_syrup_cmd="GOOS=$GOOS GOARCH=$GOARCH go build -o sshsyrup.exe -ldflags '-w -s' ./cmd/syrup"
24 | build_createfs_cmd="GOOS=$GOOS GOARCH=$GOARCH go build -o createfs.exe -ldflags '-w -s' ./cmd/createfs"
25 | else
26 | build_syrup_cmd="CGO_ENABLED=0 GOARM=$GOARM GOOS=$GOOS GOARCH=$GOARCH go build -o sshsyrup -ldflags '-w -s' ./cmd/syrup"
27 | build_createfs_cmd="CGO_ENABLED=0 GOARM=$GOARM GOOS=$GOOS GOARCH=$GOARCH go build -o createfs -ldflags '-w -s' ./cmd/createfs"
28 | fi
29 | if ! eval $build_syrup_cmd; then
30 | echo "Failed building sshsyrup for $PLATFORM" && return 1
31 | fi
32 | if ! eval $build_createfs_cmd; then
33 | echo "Failed building createfs for $PLATFORM" && return 1
34 | fi
35 | if [ "$GOOS" = "windows" ]; then
36 | zip sshsyrup-${TRAVIS_TAG}-${GOOS}-${GOARCH}.zip sshsyrup.exe createfs.exe config.yaml logs/.gitignore logs/sessions/.gitignore cmdOutput/*
37 | else
38 | tar cvzf sshsyrup-${TRAVIS_TAG}-${GOOS}-${GOARCH}.tar.gz sshsyrup createfs config.yaml logs/.gitignore logs/sessions/.gitignore cmdOutput/*
39 | fi
40 | done
41 | - ls
42 |
43 | deploy:
44 | provider: releases
45 | api_key: $AUTH_TOKEN
46 | file:
47 | - "sshsyrup-${TRAVIS_TAG}-darwin-amd64.tar.gz"
48 | - "sshsyrup-${TRAVIS_TAG}-linux-amd64.tar.gz"
49 | - "sshsyrup-${TRAVIS_TAG}-linux-arm.tar.gz"
50 | - "sshsyrup-${TRAVIS_TAG}-windows-amd64.zip"
51 |
52 | skip_cleanup: true
53 | on:
54 | condition: $TRAVIS_GO_VERSION =~ ^1\.11(\.[0-9]+)?$
55 | branch: master
56 | tags: true
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.11 AS builder
2 |
3 | # Copy the code from the host and compile it
4 | WORKDIR /syrup
5 | ENV GO111MODULE=on
6 | COPY . ./
7 | RUN go get ./...
8 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-s -w" -installsuffix nocgo -o /sshsyrup ./cmd/syrup
9 | RUN ssh-keygen -t rsa -q -f id_rsa -N "" && cp id_rsa id_rsa.pub /
10 | RUN cp -r commands.txt config.yaml group passwd filesystem.zip cmdOutput /
11 |
12 | FROM scratch
13 | COPY --from=builder /config.yaml ./
14 | COPY --from=builder /filesystem.zip ./
15 | COPY --from=builder /group ./
16 | COPY --from=builder /passwd ./
17 | COPY --from=builder /id_rsa ./
18 | COPY --from=builder /commands.txt ./
19 | COPY --from=builder /sshsyrup ./
20 | COPY --from=builder /cmdOutput ./cmdOutput
21 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
22 |
23 | ENTRYPOINT ["./sshsyrup"]
24 |
25 | EXPOSE 22/tcp
26 |
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | name = "github.com/fsnotify/fsnotify"
6 | packages = ["."]
7 | revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
8 | version = "v1.4.7"
9 |
10 | [[projects]]
11 | branch = "master"
12 | name = "github.com/hashicorp/hcl"
13 | packages = [
14 | ".",
15 | "hcl/ast",
16 | "hcl/parser",
17 | "hcl/scanner",
18 | "hcl/strconv",
19 | "hcl/token",
20 | "json/parser",
21 | "json/scanner",
22 | "json/token"
23 | ]
24 | revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8"
25 |
26 | [[projects]]
27 | name = "github.com/juju/ratelimit"
28 | packages = ["."]
29 | revision = "59fac5042749a5afb9af70e813da1dd5474f0167"
30 | version = "1.0.1"
31 |
32 | [[projects]]
33 | name = "github.com/magiconair/properties"
34 | packages = ["."]
35 | revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73"
36 | version = "v1.7.4"
37 |
38 | [[projects]]
39 | name = "github.com/mattn/go-colorable"
40 | packages = ["."]
41 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
42 | version = "v0.0.9"
43 |
44 | [[projects]]
45 | name = "github.com/mattn/go-isatty"
46 | packages = ["."]
47 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
48 | version = "v0.0.3"
49 |
50 | [[projects]]
51 | name = "github.com/mattn/go-shellwords"
52 | packages = ["."]
53 | revision = "02e3cf038dcea8290e44424da473dd12be796a8a"
54 | version = "v1.0.3"
55 |
56 | [[projects]]
57 | branch = "master"
58 | name = "github.com/mitchellh/mapstructure"
59 | packages = ["."]
60 | revision = "a4e142e9c047c904fa2f1e144d9a84e6133024bc"
61 |
62 | [[projects]]
63 | name = "github.com/pelletier/go-toml"
64 | packages = ["."]
65 | revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8"
66 | version = "v1.1.0"
67 |
68 | [[projects]]
69 | name = "github.com/rifflock/lfshook"
70 | packages = ["."]
71 | revision = "1fdc019a35147ddbb3d25aedf713ad6d1430c144"
72 | version = "v2.2"
73 |
74 | [[projects]]
75 | name = "github.com/sirupsen/logrus"
76 | packages = ["."]
77 | revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
78 | version = "v1.0.4"
79 |
80 | [[projects]]
81 | name = "github.com/spf13/afero"
82 | packages = [
83 | ".",
84 | "mem"
85 | ]
86 | revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c"
87 | version = "v1.0.2"
88 |
89 | [[projects]]
90 | name = "github.com/spf13/cast"
91 | packages = ["."]
92 | revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
93 | version = "v1.1.0"
94 |
95 | [[projects]]
96 | branch = "master"
97 | name = "github.com/spf13/jwalterweatherman"
98 | packages = ["."]
99 | revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394"
100 |
101 | [[projects]]
102 | name = "github.com/spf13/pflag"
103 | packages = ["."]
104 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
105 | version = "v1.0.0"
106 |
107 | [[projects]]
108 | name = "github.com/spf13/viper"
109 | packages = ["."]
110 | revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
111 | version = "v1.0.0"
112 |
113 | [[projects]]
114 | branch = "master"
115 | name = "golang.org/x/crypto"
116 | packages = [
117 | "curve25519",
118 | "ed25519",
119 | "ed25519/internal/edwards25519",
120 | "internal/chacha20",
121 | "poly1305",
122 | "ssh",
123 | "ssh/terminal"
124 | ]
125 | revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b"
126 |
127 | [[projects]]
128 | branch = "master"
129 | name = "golang.org/x/sys"
130 | packages = [
131 | "unix",
132 | "windows"
133 | ]
134 | revision = "af50095a40f9041b3b38960738837185c26e9419"
135 |
136 | [[projects]]
137 | branch = "master"
138 | name = "golang.org/x/text"
139 | packages = [
140 | "internal/gen",
141 | "internal/triegen",
142 | "internal/ucd",
143 | "transform",
144 | "unicode/cldr",
145 | "unicode/norm"
146 | ]
147 | revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3"
148 |
149 | [[projects]]
150 | branch = "v2"
151 | name = "gopkg.in/yaml.v2"
152 | packages = ["."]
153 | revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
154 |
155 | [solve-meta]
156 | analyzer-name = "dep"
157 | analyzer-version = 1
158 | inputs-digest = "13d33bb12e25bdb7338e3d2b7c04d47ba37a0566e556ab6979c5ff42b9e3964f"
159 | solver-name = "gps-cdcl"
160 | solver-version = 1
161 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 |
22 |
23 | [[constraint]]
24 | name = "github.com/imdario/mergo"
25 | version = "0.2.4"
26 |
27 | [[constraint]]
28 | name = "github.com/mattn/go-colorable"
29 | version = "0.0.9"
30 |
31 | [[constraint]]
32 | name = "github.com/rifflock/lfshook"
33 | version = "2.2.0"
34 |
35 | [[constraint]]
36 | name = "github.com/sirupsen/logrus"
37 | version = "1.0.4"
38 |
39 | [[constraint]]
40 | branch = "master"
41 | name = "golang.org/x/crypto"
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://travis-ci.org/mkishere/sshsyrup) [](https://ci.appveyor.com/project/mkishere/sshsyrup/branch/master)
2 | # Syrup
3 | A SSH honeypot with rich features written in Go
4 |
5 | ### Features
6 | - SSH self-defined accounts and passwords, also allow any logins
7 | - Fake shell. Records shell sessions and upload to [asciinema.org](https://asciinema.org) (Or, if you wish, can log as [UML-compatible](http://user-mode-linux.sourceforge.net/old/tty_logging.html) format)
8 | - Virtual Filesystem for browsing and fooling intruder
9 | - SFTP/SCP support for uploading/downloading files
10 | - Logs client key fingerprints
11 | - Logs in JSON format for easy parsing
12 | - Push activities to [ElasticSearch](https://www.elastic.co) for analysis and storage
13 | - Record local and remote host when client attempt to create port redirection
14 | - Structure allows [extending command sets](https://github.com/mkishere/sshsyrup/wiki/Writing-new-commands) with ease
15 |
16 | ### See Recorded Session in Action!
17 | [](https://asciinema.org/a/yu8fdSXn6v9EV0ozdSjNNN5NJ)
18 |
19 | ### Requirements
20 | #### Running
21 | - Linux, Mac or Windows (I've only tested in Windows/WSL/Linux on ARMv7, the other platforms should work as expected)
22 | #### Building
23 | - Go 1.9+ and [dep](https://github.com/golang/dep), or
24 | - Go 1.11+ and Git
25 |
26 | ### Download
27 | You may find the pre-build packages for various platform on the [release](https://github.com/mkishere/sshsyrup/releases) tab. If you find the platform you need is not on the list, you can follow the building procedure in the next section.
28 |
29 | ### Building
30 | #### Go pre-1.11/1.11 with `GO111MODULE=auto`:
31 | ```
32 | go get -u github.com/mkishere/sshsyrup
33 | cd ~/go/src/github.com/mkishere/sshsyrup
34 | dep ensure
35 | go build -ldflags "-s -w" -o sshsyrup ./cmd/syrup
36 | go build -ldflags "-s -w" -o createfs ./cmd/createfs
37 | ```
38 |
39 | #### Go 1.11 with `GO111MODULE=on`:
40 | Currently building executable with `GO111MODULE=on` is [a bit tricky](https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module) in Go 1.11 with module, here is how to do it if you want to leave module on:
41 | ```
42 | git clone https://github.com/mkishere/sshsyrup/
43 | go build -ldflags "-s -w" -o sshsyrup ./cmd/syrup
44 | go build -ldflags "-s -w" -o createfs ./cmd/createfs
45 | ```
46 |
47 | ### Setting up for the first run
48 | * Modify _config.yaml_. Here is a sample configuration
49 | ```yaml
50 | server:
51 | addr: 0.0.0.0 # Host IP
52 | port: 22 # Port listen to
53 | allowRandomUser: false # Allow random user
54 | speed: 0 # Connection max speed in kb/s
55 | processDelay: 0 # Artifical delay after server returns responses in ms
56 | timeout: 0 # Connection timeout, 0 for none
57 | ```
58 | * Prepare the virtual filesystem image by downloading the filesystem.zip from master branch or create your own by running
59 | ```
60 | ./createfs -p / -o filesystem.zip
61 | ```
62 |
63 | Since we'll need to read every file from the directory, it will take some time to load.
64 | _For Windows, since there are no user/group information, the file/directory owner will always be root._
65 |
66 | Alternatively, you can create your own image file by using `zip` in Linux (or any compatible zip utility file that is capable preserving _uid_/_gid_, symbolic links and timestamps in zip file). After all the image created is a standard zip file. Theoretically you can zip your entire filesystem into a zip file and hosted in Syrup, but remember to exclude sensitive files like `/etc/passwd`
67 |
68 | * Prepare user and passwd file
69 | Put _passwd_ and _group_ file in the same directory as config.json. The format of both files are the same as their [real-life counterpart](http://www.linfo.org/etc_passwd.html) in _/etc_, except that passwd also stores the password in the second field of each line, and asterisk(*) in password field can be used to denote matching any password.
70 | * Generate SSH private key and renamed as _id\_rsa_ and put it in the same directory
71 | ```
72 | ssh-keygen -t rsa
73 | ```
74 | * Start the server
75 | ```
76 | ./sshsyrup
77 | ```
78 |
79 | ### Running from a Docker instance
80 |
81 | A Docker image based on the latest build:
82 | ```
83 | docker pull mkishere/sshsyrup
84 | ```
85 |
86 | By default the internal sshsyrup listens on 22.
87 | ```
88 | docker run -d mkishere/sshsyrup
89 | ```
90 |
91 | The following example shows how you can customize stuff while running Syrup in container:
92 | ```sh
93 | docker run -d -p 9999:22 \
94 | -v /path/to/vfs/image.zip:/filesystem.zip \
95 | -v /path/to/config.yaml:/config.yaml \
96 | -v /path/to/logfiles:/logs \
97 | -v /path/to/group:/group \
98 | -v /path/to/passwd:/passwd \
99 | -v /path/to/private_key:/id_rsa \
100 | -v /path/to/commands.txt:/commands.txt \
101 | -v /path/to/command_output:/cmdOutput \
102 | mkishere/sshsyrup
103 | ```
104 | But you may want to map to port 22 to make your honeypot easier to find.
105 |
106 | If you want to see what happens (logs) in the Docker instance, get the instance id (`docker ps`) and then
107 | run `docker logs -f YOUR_INSTANCE_ID`.
108 |
109 | ### Configuration parameters
110 | Check out [config.yaml](https://github.com/mkishere/sshsyrup/blob/master/config.yaml)
111 |
112 | ### Logging
113 | By default Syrup will create a logging file in _logs/_ directory with file name _activity.log_ in JSON format.
114 |
115 | Please note that Syrup will no longer append dates to log files. Use a proper log rotation tool (e.g. _logrotate_) to do the work.
116 |
117 | Also, each terminal session (the shell) will be logged into a separate file under logs/sessions in [asciinema v2 format](https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md).
118 |
119 | ### Extending Syrup
120 | Syrup comes with a framework that helps to implement command easier. By implementing the [Command](https://github.com/mkishere/sshsyrup/blob/dfd91b14bd64f43e8100e3e0fbd6357f29b1708b/os/sys.go#L37) interface you can create your own command and being executed by intruders connecting to your honeypot. For more details refer to the [wiki](https://github.com/mkishere/sshsyrup/wiki/Writing-new-commands).
121 |
122 | If your command prints static output every time, you can put the output in _cmdOutput/_, and Syrup will print that when client type the command in terminal.
123 |
124 | ### Contributing
125 | Feel free to submit feature request/bug report via the GitHub issue tracker.
126 |
127 | For submitting PR, do the following steps:
128 | 1. Fork
129 | 2. Create a branch for the feature/bugfix containing your changes on your fork
130 | 3. Submit PR with your branch
131 |
132 | It is advised that creating an issue to discuss the matter in advance if your change is large :)
133 |
134 | ### TODO
135 | - Minimal set of POSIX commands/utilities
136 | - Shell parser
137 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: "{build}"
2 |
3 | platform: x64
4 |
5 | clone_folder: c:\gopath\src\github.com\mkishere\sshsyrup
6 |
7 | environment:
8 | GOPATH: c:\gopath
9 |
10 | install:
11 | - echo %PATH%
12 | - echo %GOPATH%
13 | - git submodule update --init --recursive
14 | - go version
15 | - go env
16 | - go get -v -t ./...
17 |
18 | build_script:
19 | - go build -v ./...
--------------------------------------------------------------------------------
/cmd/createfs/createFSZip.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/zip"
5 | "encoding/binary"
6 | "flag"
7 | "fmt"
8 | "io"
9 | "math/bits"
10 | "os"
11 | "path/filepath"
12 | "strings"
13 | "time"
14 | )
15 |
16 | type zeroSizefileInfo struct {
17 | fi os.FileInfo
18 | }
19 |
20 | var (
21 | zipFile string
22 | dir string
23 | stripData bool
24 | skip string
25 | inputFile string
26 | )
27 |
28 | const (
29 | MTIME = 1
30 | ATIME = 2
31 | CTIME = 4
32 | )
33 |
34 | func (z zeroSizefileInfo) Sys() interface{} { return z.fi.Sys() }
35 | func (z zeroSizefileInfo) Size() int64 { return 0 }
36 | func (z zeroSizefileInfo) IsDir() bool { return z.fi.IsDir() }
37 | func (z zeroSizefileInfo) Name() string { return z.fi.Name() }
38 | func (z zeroSizefileInfo) Mode() os.FileMode { return z.fi.Mode() }
39 | func (z zeroSizefileInfo) ModTime() time.Time { return z.fi.ModTime() }
40 |
41 | func init() {
42 | flag.StringVar(&zipFile, "o", "", "Output zip file")
43 | flag.StringVar(&dir, "p", "", "Starting position for the import path")
44 | flag.BoolVar(&stripData, "b", true, "Strip file content, if set to true the program will only read metadata from filesystem and skip actual file content in archive.")
45 | flag.StringVar(&skip, "k", "", "Paths to be skipped for indexing, separated by semicolons")
46 | flag.StringVar(&inputFile, "i", "", "Input file for building the image")
47 | }
48 |
49 | func writeExtraUnixInfo(uid, gid uint32, atime, mtime, ctime int64) (b []byte) {
50 | b = make([]byte, 15)
51 | binary.LittleEndian.PutUint16(b, 0x7875)
52 | binary.LittleEndian.PutUint16(b[2:], 11)
53 | b[4] = 1
54 | b[5] = 4
55 | binary.LittleEndian.PutUint32(b[6:], uid)
56 | b[10] = 4
57 | binary.LittleEndian.PutUint32(b[11:], gid)
58 | var flag uint
59 | if mtime != 0 {
60 | flag |= MTIME
61 | }
62 | if atime != 0 {
63 | flag |= ATIME
64 | }
65 | if ctime != 0 {
66 | flag |= CTIME
67 | }
68 | if flag > 0 {
69 | bLen := bits.OnesCount(flag) * 4
70 | tb := make([]byte, bLen+5)
71 | binary.LittleEndian.PutUint16(tb, 0x5455)
72 | binary.LittleEndian.PutUint16(tb[2:], uint16(bLen+1))
73 | tb[4] = byte(flag)
74 | pos := 5
75 | // For compatibility we are going to use uint32 instead of uint64.
76 | // To be fixed in 2038 :)
77 | if flag&MTIME != 0 {
78 | binary.LittleEndian.PutUint32(tb[pos:], uint32(mtime))
79 | pos += 4
80 | }
81 | if flag&ATIME != 0 {
82 | binary.LittleEndian.PutUint32(tb[pos:], uint32(atime))
83 | pos += 4
84 | }
85 | if flag&CTIME != 0 {
86 | binary.LittleEndian.PutUint32(tb[pos:], uint32(ctime))
87 | }
88 | b = append(b, tb...)
89 | }
90 | return
91 | }
92 |
93 | func main() {
94 | flag.Parse()
95 | if len(dir) == 0 {
96 | fmt.Println("Missing parameter -p. See -help")
97 | return
98 | }
99 | if len(zipFile) == 0 {
100 | fmt.Println("Missing parameter -o. See -help")
101 | return
102 | }
103 | skipPath := []string{}
104 | if len(skip) > 0 {
105 | skipPath = strings.Split(skip, ";")
106 | }
107 | f, err := os.OpenFile(zipFile, os.O_CREATE|os.O_WRONLY, os.ModeExclusive)
108 | if err != nil {
109 | if os.IsExist(err) {
110 | fmt.Println("File already exists")
111 | } else {
112 | fmt.Printf("Cannot create file. Reason:%v", err)
113 | }
114 | return
115 | }
116 | defer f.Close()
117 | archive := zip.NewWriter(f)
118 | defer archive.Close()
119 |
120 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
121 | if path == dir {
122 | return nil
123 | }
124 | for _, v := range skipPath {
125 | if strings.HasPrefix(path, v) {
126 | return filepath.SkipDir
127 | }
128 | }
129 | if info == nil {
130 | fmt.Printf("Skipping %v for nil FileInfo\n", path)
131 | return nil
132 | }
133 | fmt.Printf("Writing %v (%v)\n", path, info.Name())
134 | if err != nil {
135 | fmt.Println(err)
136 | return nil
137 | }
138 | if stripData && !info.IsDir() {
139 | info = zeroSizefileInfo{fi: info}
140 | }
141 | header, err := zip.FileInfoHeader(info)
142 | header.Name = strings.TrimPrefix(path, dir+"/")
143 | header.Name = strings.TrimPrefix(path, "/")
144 | header.Extra = writeExtraUnixInfo(getExtraInfo(info))
145 | fmt.Printf("Filename to be written:%v\n", header.Name)
146 | if err != nil {
147 | fmt.Println(err)
148 | return nil
149 | }
150 | if info.IsDir() {
151 | header.Name += "/"
152 | } else if info.Size() > 0 || info.Mode()&os.ModeSymlink != 0 {
153 | header.Method = zip.Deflate
154 | }
155 |
156 | filepath.Base(dir)
157 | w, err := archive.CreateHeader(header)
158 |
159 | if err != nil {
160 | fmt.Println(err)
161 | return nil
162 | }
163 | if info.Mode()&os.ModeSymlink != 0 {
164 | dst, err := os.Readlink(path)
165 | if err != nil {
166 | return nil
167 | }
168 | _, err = w.Write([]byte(dst))
169 | if err != nil {
170 | return nil
171 | }
172 | } else if !stripData {
173 | file, err := os.Open(path)
174 | if err != nil {
175 | fmt.Println(err)
176 | return nil
177 | }
178 | defer file.Close()
179 | _, err = io.Copy(w, file)
180 | fmt.Println(err)
181 | return nil
182 | }
183 |
184 | return nil
185 | })
186 | err = archive.Close()
187 | if err != nil {
188 | fmt.Println(err)
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/cmd/createfs/createFSZip_darwin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | )
7 |
8 | func getExtraInfo(info os.FileInfo) (uid, gid uint32, atime, mtime, ctime int64) {
9 | stat := info.Sys().(*syscall.Stat_t)
10 | uid = stat.Uid
11 | gid = stat.Gid
12 | atime = stat.Atimespec.Sec
13 | mtime = stat.Mtimespec.Sec
14 | ctime = stat.Ctimespec.Sec
15 |
16 | return
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/createfs/createFSZip_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestZipExtraHeader(t *testing.T) {
10 | b := writeExtraUnixInfo(0, 0, 0, 0, 0)
11 | if bytes.Compare([]byte{117, 120, 11, 00, 01, 04, 00, 00, 00, 00, 04, 00, 00, 00, 00}, b) != 0 {
12 | t.Errorf("Byte array issue: %v", b)
13 | }
14 | }
15 |
16 | func TestTimeHeader(t *testing.T) {
17 | b := writeExtraUnixInfo(0, 0, time.Now().Unix(), 0, 0)
18 | t.Log(b)
19 | }
20 |
--------------------------------------------------------------------------------
/cmd/createfs/createFSZip_unix.go:
--------------------------------------------------------------------------------
1 | // +build !windows,!darwin
2 |
3 | package main
4 |
5 | import (
6 | "os"
7 | "syscall"
8 | )
9 |
10 | func getExtraInfo(info os.FileInfo) (uid, gid uint32, atime, mtime, ctime int64) {
11 | stat := info.Sys().(*syscall.Stat_t)
12 | uid = stat.Uid
13 | gid = stat.Gid
14 | atime, _ = stat.Atim.Unix()
15 | mtime, _ = stat.Mtim.Unix()
16 | ctime, _ = stat.Ctim.Unix()
17 |
18 | return
19 | }
20 |
--------------------------------------------------------------------------------
/cmd/createfs/createFSZip_windows.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "time"
7 | )
8 |
9 | func getExtraInfo(info os.FileInfo) (uid, gid uint32, atime, mtime, ctime int64) {
10 | fileAttr := info.Sys().(syscall.Win32FileAttributeData)
11 | atime = time.Unix(0, fileAttr.LastAccessTime.Nanoseconds()).Unix()
12 | mtime = time.Unix(0, fileAttr.LastWriteTime.Nanoseconds()).Unix()
13 | ctime = time.Unix(0, fileAttr.CreationTime.Nanoseconds()).Unix()
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/syrup/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io/ioutil"
7 | "math/rand"
8 | "os"
9 | "path"
10 | "runtime"
11 | "time"
12 |
13 | colorable "github.com/mattn/go-colorable"
14 | syrup "github.com/mkishere/sshsyrup"
15 | honeyos "github.com/mkishere/sshsyrup/os"
16 | _ "github.com/mkishere/sshsyrup/os/command"
17 | "github.com/mkishere/sshsyrup/util"
18 | "github.com/rifflock/lfshook"
19 | log "github.com/sirupsen/logrus"
20 | "github.com/spf13/pflag"
21 | "github.com/spf13/viper"
22 | )
23 |
24 | var (
25 | configPath string
26 | )
27 |
28 | func init() {
29 | pflag.StringVarP(&configPath, "config", "c", ".", "Specify the working directory")
30 |
31 | viper.SetDefault("server.addr", "0.0.0.0")
32 | viper.SetDefault("server.port", 2222)
33 | viper.SetDefault("server.allowRandomUser", true)
34 | viper.SetDefault("server.ident", "SSH-2.0-OpenSSH_6.8p1")
35 | viper.SetDefault("server.maxTries", 3)
36 | viper.SetDefault("server.allowRetryLogin", false)
37 | viper.SetDefault("server.retryDelay", time.Duration(time.Millisecond*2000))
38 | viper.SetDefault("server.maxConnections", 10)
39 | viper.SetDefault("server.maxConnPerHost", 2)
40 | viper.SetDefault("server.timeout", time.Duration(time.Minute*10))
41 | viper.SetDefault("server.speed", 0)
42 | viper.SetDefault("server.processDelay", 0)
43 | viper.SetDefault("server.hostname", "spr1139")
44 | viper.SetDefault("server.commandList", "commands.txt")
45 | viper.SetDefault("server.sessionLogFmt", "asciinema")
46 | viper.SetDefault("server.banner", "banner.txt")
47 | viper.SetDefault("server.privateKey", "id_rsa")
48 | viper.SetDefault("server.portRedirection", "disable")
49 | viper.SetDefault("server.commandOutputDir", "cmdOutput")
50 | viper.SetDefault("virtualfs.imageFile", "filesystem.zip")
51 | viper.SetDefault("virtualfs.uidMappingFile", "passwd")
52 | viper.SetDefault("virtualfs.gidMappingFile", "group")
53 | viper.SetDefault("virtualfs.savedFileDir", "tempdir")
54 | viper.SetDefault("asciinema.apiEndpoint", "https://asciinema.org")
55 | }
56 |
57 | func main() {
58 | pflag.Parse()
59 | viper.SetEnvPrefix("sshsyrup")
60 | viper.AddConfigPath(configPath)
61 | viper.AddConfigPath(".")
62 | viper.SetConfigName("config")
63 | err := viper.ReadInConfig()
64 | if err != nil {
65 | fmt.Fprintf(os.Stderr, "Cannot find config file at %v", configPath)
66 | return
67 | }
68 |
69 | viper.AutomaticEnv()
70 | if runtime.GOOS == "windows" {
71 | log.SetFormatter(&log.TextFormatter{ForceColors: true})
72 | log.SetOutput(colorable.NewColorableStdout())
73 | }
74 | pathMap := lfshook.PathMap{
75 | log.InfoLevel: "logs/activity.log",
76 | }
77 | if _, err = os.Stat("logs"); os.IsNotExist(err) {
78 | err = os.MkdirAll("logs/sessions", 0755)
79 | if err != nil {
80 | os.Exit(1)
81 | }
82 | }
83 | log.AddHook(lfshook.NewHook(
84 | pathMap,
85 | &log.JSONFormatter{},
86 | ))
87 |
88 | // See if logstash is enabled
89 | if viper.IsSet("elastic.endPoint") {
90 |
91 | hook := util.NewElasticHook(viper.GetString("elastic.endPoint"), viper.GetString("elastic.index"), viper.GetString("elastic.pipeline"))
92 | if err != nil {
93 | log.WithError(err).Fatal("Cannot hook with Elastic")
94 | }
95 | log.AddHook(hook)
96 | }
97 |
98 | err = honeyos.LoadGroups(path.Join(configPath, viper.GetString("virtualfs.uidMappingFile")))
99 | if err != nil {
100 | log.Errorf("Cannot load group mapping file %v", path.Join(configPath, viper.GetString("virtualfs.uidMappingFile")))
101 | }
102 | // Load command list
103 | honeyos.RegisterFakeCommand(readFiletoArray(path.Join(configPath, viper.GetString("server.commandList"))))
104 | // Load command output list
105 | cmdOutputPath := viper.GetString("server.commandOutputDir")
106 | if dp, err := os.Open(cmdOutputPath); err == nil {
107 | fileList, err := dp.Readdir(-1)
108 | if err == nil {
109 | for _, fi := range fileList {
110 | if !fi.IsDir() {
111 | honeyos.RegisterCommandOutput(fi.Name(), path.Join(cmdOutputPath, fi.Name()))
112 | }
113 | }
114 | }
115 | }
116 | // Randomize seed
117 | rand.Seed(time.Now().Unix())
118 |
119 | key, err := ioutil.ReadFile(path.Join(configPath, viper.GetString("server.privateKey")))
120 | if err != nil {
121 | log.WithError(err).Fatal("Failed to load private key")
122 | }
123 |
124 | syrupServer := syrup.NewServer(configPath, key)
125 |
126 | syrupServer.ListenAndServe()
127 |
128 | }
129 |
130 | func readFiletoArray(path string) []string {
131 | f, err := os.Open(path)
132 | if err != nil {
133 | return []string{}
134 | }
135 | defer f.Close()
136 | sc := bufio.NewScanner(f)
137 | var lines []string
138 | for sc.Scan() {
139 | lines = append(lines, sc.Text())
140 | }
141 | return lines
142 | }
143 |
--------------------------------------------------------------------------------
/cmdOutput/lscpu:
--------------------------------------------------------------------------------
1 | Architecture: x86_64
2 | CPU op-mode(s): 32-bit, 64-bit
3 | Byte Order: Little Endian
4 | CPU(s): 1
5 | On-line CPU(s) list: 0
6 | Thread(s) per core: 1
7 | Core(s) per socket: 1
8 | Socket(s): 1
9 | NUMA node(s): 1
10 | Vendor ID: GenuineIntel
11 | CPU family: 6
12 | Model: 63
13 | Model name: Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz
14 | Stepping: 2
15 | CPU MHz: 2400.016
16 | BogoMIPS: 4800.16
17 | Hypervisor vendor: Xen
18 | Virtualization type: full
19 | L1d cache: 32K
20 | L1i cache: 32K
21 | L2 cache: 256K
22 | L3 cache: 30720K
23 | NUMA node0 CPU(s): 0
24 | Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm cpuid_fault invpcid_single pti fsgsbase bmi1 avx2 smep bmi2 erms invpcid xsaveopt
--------------------------------------------------------------------------------
/cmdOutput/lspci:
--------------------------------------------------------------------------------
1 | 00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
2 | 00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
3 | 00:01.1 IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)
4 | 00:02.0 VGA compatible controller: InnoTek System Graphics Adapter
5 | 00:03.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 02)
6 | 00:04.0 Multimedia audio controller: Intel Corporation 82801AA AC'97 Audio Controller (rev 01)
7 | 00:05.0 USB controller: Apple Inc. KeyLargo/Intrepid USB
8 | 00:06.0 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 08)
9 | 00:0d.0 SATA controller: Intel Corporation 82801HM/HEM (ICH8M/ICH8M-E) SATA Controller [AHCI mode] (rev 02)
10 |
--------------------------------------------------------------------------------
/commands.txt:
--------------------------------------------------------------------------------
1 | arch
2 | base64
3 | basename
4 | cal
5 | cat
6 | chcon
7 | chgrp
8 | chmod
9 | chown
10 | cksum
11 | chroot
12 | comm
13 | cp
14 | csplit
15 | cut
16 | date
17 | dd
18 | df
19 | dir
20 | dircolors
21 | dirname
22 | du
23 | echo
24 | env
25 | expand
26 | expr
27 | factor
28 | false
29 | fmt
30 | fold
31 | groups
32 | head
33 | hostid
34 | id
35 | install
36 | join
37 | link
38 | ln
39 | logname
40 | ls
41 | md5sum
42 | mkdir
43 | mkfifo
44 | mknod
45 | mktemp
46 | mv
47 | nice
48 | nl
49 | nohup
50 | nproc
51 | numfmt
52 | od
53 | paste
54 | pathchk
55 | pinky
56 | pr
57 | printenv
58 | printf
59 | ptx
60 | pwd
61 | readlink
62 | realpath
63 | rm
64 | rmdir
65 | runcon
66 | seq
67 | sha1sum
68 | sha224sum
69 | sha256sum
70 | sha384sum
71 | sha512sum
72 | shred
73 | shuf
74 | sleep
75 | sort
76 | split
77 | stat
78 | stdbuf
79 | stty
80 | sum
81 | sync
82 | tac
83 | tail
84 | tee
85 | test
86 | timeout
87 | touch
88 | tr
89 | true
90 | truncate
91 | tsort
92 | tty
93 | uname
94 | unexpand
95 | uniq
96 | unlink
97 | uptime
98 | users
99 | vdir
100 | wc
101 | who
102 | whoami
103 | xxd
104 | yes
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | # Host IP
3 | addr: 0.0.0.0
4 |
5 | # Port Syrup listening to
6 | port: 22
7 |
8 | # Fake hostname to be displayed in various commands and prompt
9 | hostname: spr1139
10 |
11 | # Allow random user to login
12 | allowRandomUser: false
13 |
14 | # Connection max speed in kb/s, 0 for unlimited
15 | speed: 0
16 |
17 | # Artifical delay after server returns responses in ms. No delay if set to 0
18 | processDelay: 0
19 |
20 | # Connection timeout, 0 for none
21 | timeout: 0
22 |
23 | # SSH identification string. Will be shown to clients when they connect
24 | ident: SSH-2.0-OpenSSH_6.8p1
25 |
26 | # Max tries allowed for password authentication, if client could not pass
27 | # authentication they will be disconnected
28 | maxTries: 3
29 |
30 | # Allow client login after they pass max retry count isntead of disconnecting them
31 | allowRetryLogin: false
32 |
33 | # Delay between password authentication failure and next retry
34 | retryDelay: 2s
35 |
36 | # Max connections the server can allowed simultaneously
37 | maxConnections: 10
38 |
39 | # Max connections can allowed per host simultaneously
40 | maxConnPerHost: 2
41 |
42 | # Connection timeout after
43 | timeout: 10m
44 |
45 | # commandList points to a text file containing available commands to the honeypot. The shell will
46 | # returns Segmentation fault/other random errors instead of file/command not found
47 | commandList: commands.txt
48 |
49 | # Session logging format. Can be either asciinema or uml
50 | sessionLogFmt: asciinema
51 |
52 | # Banner to be displayed while login
53 | banner: banner.txt
54 |
55 | # SSH private key
56 | privateKey: id_rsa
57 |
58 | # Redirect connection to specific host when client request SSH tunneling. Available values are:
59 | # disabled: Port redirection request will be rejected
60 | # direct: Will connect to client specified IP and port, same as in a standard SSH server
61 | # map: Ignore the host client provides and map the port defined in portRedirectionMap parameter. See below
62 | portRedirection: disabled
63 |
64 | # portRedirectionMap define a mapping from client requested ports to destination. For below example, if client
65 | # requests tunneling to remote host port 25, Syrup will connect them to 192.168.1.117:25 instead
66 | portRedirectionMap:
67 | 25: 192.168.1.117:25
68 | 443: 192.168.1.117:447
69 |
70 | # commandOutputDir points to directories containing text files with command name as their filename. When client
71 | # type in console it will display the content of the file
72 | commandOutputDir: cmdOutput
73 |
74 | # Max size allowed for SCP/SFTP file upload in bytes, unlimited if set to 0
75 | receiveFileSizeLimit: 0
76 |
77 | virtualfs:
78 | # imageFile is a zip file archive containing the files that would be seen in the virtual filesystem
79 | imageFile: filesystem.zip
80 |
81 | # uidMappingFile is the username and password file. Format is same as /etc/passwd except that it accepts asterisk(*)
82 | # as wildcard
83 | uidMappingFile: passwd
84 |
85 | # gidMappingFile is the group mapping file. Format same as /etc/group
86 | gidMappingFile: group
87 |
88 | # savedFileDir stores files written by client to the virtual filesystem
89 | savedFileDir: tempdir
90 |
91 | # asciinema (https://asciinema.org) is a service that stores and show recorded terminal sessions
92 | # asciinema:
93 | # apiEndpoint points to asciinema.org for uploading client sessions
94 | # apiEndpoint: https://asciinema.org
95 |
96 | # modify the apiKey to your own asciinema key instead
97 | # apiKey: xx
98 |
99 | # ElasticSearch endpoint, uncomment and modify the endpoint for Syrup to post logs to Elastic
100 | # elastic:
101 | # endPoint: http://localhost:8080/
102 |
103 | # Index for Syrup to post the log to
104 | # index: syrup
105 |
106 | # Pipeline process for the log to go through. E.g. for doing geolocation resolve
107 | # pipeline: ipProc
108 |
109 | # (Currently not implemented)
110 | # API key for posting the IP of client to the AbuseIPDB (https://www.abuseipdb.com)
111 | # Remove the comment and put in your API key if you want to enable this feature
112 | # abuseIPDB:
113 | # apiKey: xxxxxxx
--------------------------------------------------------------------------------
/filesystem.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victpork/sshsyrup/4f50f01a4dd11eeb3d795a3dea46c2624d5f79ab/filesystem.zip
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mkishere/sshsyrup
2 |
3 | require (
4 | github.com/BurntSushi/toml v0.3.0 // indirect
5 | github.com/davecgh/go-spew v1.1.1 // indirect
6 | github.com/fsnotify/fsnotify v1.4.7 // indirect
7 | github.com/golang/protobuf v1.2.0 // indirect
8 | github.com/google/pprof v0.0.0-20181002142953-f36417847b1c // indirect
9 | github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb // indirect
10 | github.com/hpcloud/tail v1.0.0 // indirect
11 | github.com/ianlancetaylor/demangle v0.0.0-20180714043527-fcd258a6f0b4 // indirect
12 | github.com/juju/ratelimit v1.0.1
13 | github.com/kr/pretty v0.1.0 // indirect
14 | github.com/magiconair/properties v1.7.4 // indirect
15 | github.com/mattn/go-colorable v0.0.9
16 | github.com/mattn/go-isatty v0.0.3 // indirect
17 | github.com/mattn/go-shellwords v1.0.3
18 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 // indirect
19 | github.com/onsi/ginkgo v1.6.0 // indirect
20 | github.com/onsi/gomega v1.4.1 // indirect
21 | github.com/pelletier/go-toml v1.1.0 // indirect
22 | github.com/pmezard/go-difflib v1.0.0 // indirect
23 | github.com/rifflock/lfshook v0.0.0-20171219153109-1fdc019a3514
24 | github.com/sirupsen/logrus v1.0.4
25 | github.com/spf13/afero v1.0.2
26 | github.com/spf13/cast v1.1.0 // indirect
27 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect
28 | github.com/spf13/pflag v1.0.0
29 | github.com/spf13/viper v1.0.0
30 | github.com/stretchr/testify v1.2.2 // indirect
31 | golang.org/x/arch v0.0.0-20180920145803-b19384d3c130 // indirect
32 | golang.org/x/crypto v0.0.0-20180123095555-3d37316aaa6b
33 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d // indirect
34 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
35 | golang.org/x/sys v0.0.0-20180122081959-af50095a40f9 // indirect
36 | golang.org/x/text v0.0.0-20171227012246-e19ae1496984 // indirect
37 | gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
38 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
39 | gopkg.in/fsnotify.v1 v1.4.7 // indirect
40 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
41 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
42 | gopkg.in/yaml.v2 v2.0.0 // indirect
43 | )
44 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
2 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
6 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
7 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
8 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
9 | github.com/google/pprof v0.0.0-20181002142953-f36417847b1c h1:gIPn6u19B7jF7guvt28xKMpVht1vlhroy/a9mtiKb/o=
10 | github.com/google/pprof v0.0.0-20181002142953-f36417847b1c/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
11 | github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb h1:1OvvPvZkn/yCQ3xBcM8y4020wdkMXPHLB4+NfoGWh4U=
12 | github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
13 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
14 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
15 | github.com/ianlancetaylor/demangle v0.0.0-20180714043527-fcd258a6f0b4 h1:eWmTY5/yaZWgZR+HjyGOCXgM++IEwo/KgxxtYhai4LU=
16 | github.com/ianlancetaylor/demangle v0.0.0-20180714043527-fcd258a6f0b4/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
17 | github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
18 | github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
19 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
20 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
22 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
24 | github.com/magiconair/properties v1.7.4 h1:UVo0TkHGd4lQSN1dVDzs9URCIgReuSIcCXpAVB9nZ80=
25 | github.com/magiconair/properties v1.7.4/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
26 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
27 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
28 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
29 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
30 | github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk=
31 | github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
32 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
33 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
34 | github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
35 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
36 | github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
37 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
38 | github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM=
39 | github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
42 | github.com/rifflock/lfshook v0.0.0-20171219153109-1fdc019a3514 h1:a0R0Z5Uy5ZwEUiJOz9wxfFf46Vy9VOQNGFR1v4ddiZ4=
43 | github.com/rifflock/lfshook v0.0.0-20171219153109-1fdc019a3514/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
44 | github.com/sirupsen/logrus v1.0.4 h1:gzbtLsZC3Ic5PptoRG+kQj4L60qjK7H7XszrU163JNQ=
45 | github.com/sirupsen/logrus v1.0.4/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
46 | github.com/spf13/afero v1.0.2 h1:5bRmqmInNmNFkI9NG9O0Xc/Lgl9wOWWUUA/O8XZqTCo=
47 | github.com/spf13/afero v1.0.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
48 | github.com/spf13/cast v1.1.0 h1:0Rhw4d6C8J9VPu6cjZLIhZ8+aAOHcDvGeKn+cq5Aq3k=
49 | github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
50 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig=
51 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
52 | github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI=
53 | github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
54 | github.com/spf13/viper v1.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I=
55 | github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
56 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
57 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
58 | golang.org/x/arch v0.0.0-20180920145803-b19384d3c130 h1:Vsc61gop4hfHdzQNolo6Fi/sw7TnJ2yl3ZR4i7bYirs=
59 | golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
60 | golang.org/x/crypto v0.0.0-20180123095555-3d37316aaa6b h1:VqIuNRBMdkwj3QmFfZdCw5Mzlv4BFaMda+dzdi9gAIQ=
61 | golang.org/x/crypto v0.0.0-20180123095555-3d37316aaa6b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
62 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
63 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
64 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
65 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
66 | golang.org/x/sys v0.0.0-20180122081959-af50095a40f9 h1:ne3QBDn7ziARHn26Dk8yZ5TB4yf7growSFMsxd8BrGQ=
67 | golang.org/x/sys v0.0.0-20180122081959-af50095a40f9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
68 | golang.org/x/text v0.0.0-20171227012246-e19ae1496984 h1:ulYJn/BqO4fMRe1xAQzWjokgjsQLPpb21GltxXHI3fQ=
69 | golang.org/x/text v0.0.0-20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
70 | gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
71 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
72 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
73 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
74 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
75 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
76 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
77 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
78 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
79 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
80 | gopkg.in/yaml.v2 v2.0.0 h1:uUkhRGrsEyx/laRdeS6YIQKIys8pg+lRSRdVMTYjivs=
81 | gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
82 |
--------------------------------------------------------------------------------
/group:
--------------------------------------------------------------------------------
1 | root:x:0:
2 | daemon:x:1:
3 | bin:x:2:
4 | sys:x:3:
5 | adm:x:4:syslog
6 | tty:x:5:
7 | disk:x:6:
8 | lp:x:7:
9 | mail:x:8:
10 | news:x:9:
11 | uucp:x:10:
12 | man:x:12:
13 | proxy:x:13:
14 | kmem:x:15:
15 | dialout:x:20:
16 | fax:x:21:
17 | voice:x:22:
18 | cdrom:x:24:
19 | floppy:x:25:
20 | tape:x:26:
21 | sudo:x:27:testuser
22 | audio:x:29:
23 | dip:x:30:
24 | www-data:x:33:
25 | backup:x:34:
26 | operator:x:37:
27 | list:x:38:
28 | irc:x:39:
29 | src:x:40:
30 | gnats:x:41:
31 | shadow:x:42:
32 | utmp:x:43:
33 | video:x:44:
34 | sasl:x:45:
35 | plugdev:x:46:
36 | staff:x:50:
37 | games:x:60:
38 | users:x:100:
39 | nogroup:x:65534:
40 | libuuid:x:101:
41 | netdev:x:102:
42 | crontab:x:103:
43 | syslog:x:104:
44 | fuse:x:105:
45 | messagebus:x:106:
46 | mlocate:x:107:
47 | ssh:x:108:
48 | admin:x:110:
49 | testuser:x:1000:
50 |
--------------------------------------------------------------------------------
/logs/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victpork/sshsyrup/4f50f01a4dd11eeb3d795a3dea46c2624d5f79ab/logs/.gitignore
--------------------------------------------------------------------------------
/logs/sessions/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/victpork/sshsyrup/4f50f01a4dd11eeb3d795a3dea46c2624d5f79ab/logs/sessions/.gitignore
--------------------------------------------------------------------------------
/net/conn.go:
--------------------------------------------------------------------------------
1 | package net
2 |
3 | import (
4 | "io"
5 | "net"
6 | "sync"
7 | "time"
8 |
9 | limit "github.com/juju/ratelimit"
10 | )
11 |
12 | type throttledConntection struct {
13 | net.Conn
14 | lr io.Reader
15 | lw io.Writer
16 | Timeout time.Duration
17 | }
18 |
19 | // IPConnCount keep tracks of how many connections allowed per IP
20 | type IPConnCount struct {
21 | lock sync.RWMutex
22 | m map[string]int
23 | }
24 |
25 | // NewThrottledConnection creates a throttled connection which is done by
26 | // https://github.com/juju/ratelimit
27 | func NewThrottledConnection(conn net.Conn, speed int64, timeout time.Duration) net.Conn {
28 | if speed > 0 {
29 | bucket := limit.NewBucketWithQuantum(time.Second, speed, speed)
30 | lr := limit.Reader(conn, bucket)
31 | lw := limit.Writer(conn, bucket)
32 | return &throttledConntection{conn, lr, lw, timeout}
33 | }
34 | return &throttledConntection{conn, nil, nil, timeout}
35 | }
36 |
37 | func (tc *throttledConntection) Read(p []byte) (int, error) {
38 | if tc.Timeout > 0 {
39 | defer tc.Conn.SetReadDeadline(time.Now().Add(tc.Timeout))
40 | }
41 | if tc.lr != nil {
42 | return tc.lr.Read(p)
43 | }
44 | return tc.Conn.Read(p)
45 | }
46 |
47 | func (tc *throttledConntection) Write(p []byte) (int, error) {
48 | if tc.lw != nil {
49 | return tc.Conn.Write(p)
50 | }
51 | return tc.Conn.Write(p)
52 | }
53 |
54 | func NewIPConnCount() *IPConnCount {
55 | return &IPConnCount{m: make(map[string]int)}
56 | }
57 |
58 | func (ipc *IPConnCount) Read(clientIP string) int {
59 | ipc.lock.RLock()
60 | defer ipc.lock.RUnlock()
61 | return ipc.m[clientIP] - 1
62 | }
63 |
64 | func (ipc *IPConnCount) IncCount(clientIP string) int {
65 | ipc.lock.Lock()
66 | defer ipc.lock.Unlock()
67 | return ipc.m[clientIP] - 1
68 | }
69 |
70 | func (ipc *IPConnCount) DecCount(clientIP string) {
71 | ipc.lock.Lock()
72 | defer ipc.lock.Unlock()
73 |
74 | if ipc.m[clientIP] > 0 {
75 | ipc.m[clientIP]--
76 | } else {
77 | delete(ipc.m, clientIP)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/os/account.go:
--------------------------------------------------------------------------------
1 | package os
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "math/rand"
7 | "os"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | type User struct {
13 | UID int
14 | GID int
15 | Name string
16 | Password string
17 | Homedir string
18 | Info string
19 | Shell string
20 | }
21 |
22 | type Group struct {
23 | GID int
24 | Name string
25 | Userlist []string
26 | }
27 |
28 | var (
29 | users = make(map[int]User)
30 | usernameMapping = make(map[string]User)
31 | groups = make(map[int]Group)
32 | )
33 |
34 | func LoadUsers(userFile string) error {
35 | f, err := os.OpenFile(userFile, os.O_RDONLY, 0666)
36 | if err != nil {
37 | return err
38 | }
39 | defer f.Close()
40 | sc := bufio.NewScanner(f)
41 | for sc.Scan() {
42 | fields := strings.Split(sc.Text(), ":")
43 | uid, err := strconv.Atoi(fields[2])
44 | if err != nil {
45 | return err
46 | }
47 | gid, err := strconv.Atoi(fields[3])
48 | if err != nil {
49 | return err
50 | }
51 | userObj := User{
52 | UID: uid,
53 | GID: gid,
54 | Name: fields[0],
55 | Password: fields[1],
56 | Info: fields[4],
57 | Homedir: fields[5],
58 | Shell: fields[6],
59 | }
60 | users[uid] = userObj
61 | usernameMapping[fields[0]] = userObj
62 | }
63 |
64 | return nil
65 | }
66 |
67 | func LoadGroups(groupFile string) error {
68 | f, err := os.OpenFile(groupFile, os.O_RDONLY, 0666)
69 | if err != nil {
70 | return err
71 | }
72 | defer f.Close()
73 | sc := bufio.NewScanner(f)
74 | for sc.Scan() {
75 | fields := strings.Split(sc.Text(), ":")
76 | gid, err := strconv.Atoi(fields[2])
77 | if err != nil {
78 | return err
79 | }
80 | groups[gid] = Group{
81 | GID: gid,
82 | Name: fields[0],
83 | }
84 | }
85 |
86 | return nil
87 | }
88 |
89 | func IsUserExist(user string) (pass string, exists bool) {
90 | userObj, exists := usernameMapping[user]
91 | if !exists {
92 | return
93 | }
94 | return userObj.Password, exists
95 | }
96 |
97 | func GetUser(name string) User {
98 | return usernameMapping[name]
99 | }
100 |
101 | func GetUserByID(id int) User {
102 | return users[id]
103 | }
104 |
105 | func GetGroupByID(id int) Group {
106 | return groups[id]
107 | }
108 |
109 | func CreateUser(name, password string) (newUser User, e error) {
110 | if _, exists := usernameMapping[name]; exists {
111 | return newUser, errors.New("User already exists")
112 | }
113 | newUser = User{
114 | Name: name,
115 | UID: 1000 + rand.Intn(15),
116 | GID: 100,
117 | Password: password,
118 | Shell: "/bin/bash",
119 | Homedir: "/home/" + name,
120 | }
121 | usernameMapping[name] = newUser
122 | users[newUser.UID] = newUser
123 | return
124 | }
125 |
--------------------------------------------------------------------------------
/os/command/cat.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "path"
8 |
9 | honeyos "github.com/mkishere/sshsyrup/os"
10 | )
11 |
12 | type cat struct{}
13 |
14 | func init() {
15 | honeyos.RegisterCommand("cat", cat{})
16 | }
17 |
18 | func (c cat) GetHelp() string {
19 | return ""
20 | }
21 |
22 | func (c cat) Exec(args []string, sys honeyos.Sys) int {
23 | if len(args) == 0 {
24 | return 0
25 | }
26 | filePath := args[0]
27 | if !path.IsAbs(filePath) {
28 | filePath = path.Join(sys.Getcwd(), filePath)
29 | }
30 | f, err := sys.FSys().OpenFile(filePath, os.O_RDONLY, os.ModeType)
31 | if err != nil {
32 | if err == os.ErrNotExist {
33 | fmt.Fprintf(sys.Out(), "cat: %v: No such file or directory\n", args[0])
34 | return 1
35 | } else if err == os.ErrPermission {
36 | fmt.Fprintf(sys.Out(), "cat: %v: Permission denied\n", args[0])
37 | return 1
38 | }
39 | }
40 | io.Copy(sys.Out(), f)
41 | return 0
42 | }
43 |
44 | func (c cat) Where() string {
45 | return "/bin/pwd"
46 | }
47 |
--------------------------------------------------------------------------------
/os/command/id.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/mkishere/sshsyrup/os"
7 | )
8 |
9 | type id struct{}
10 |
11 | func init() {
12 | os.RegisterCommand("id", id{})
13 | }
14 |
15 | func (i id) GetHelp() string {
16 | return ""
17 | }
18 |
19 | func (i id) Exec(args []string, sys os.Sys) int {
20 | uid := sys.CurrentUser()
21 | gid := sys.CurrentGroup()
22 |
23 | user := os.GetUserByID(uid)
24 | group := os.GetGroupByID(gid)
25 | fmt.Fprintf(sys.Out(), "uid=%d(%s) gid=%d(%s) groups=%d(%s)\n", uid, user.Name, gid, group.Name, gid, group.Name)
26 | return 0
27 | }
28 |
29 | func (i id) Where() string {
30 | return "/bin/uname"
31 | }
32 |
--------------------------------------------------------------------------------
/os/command/ls.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "sort"
7 | "strings"
8 |
9 | honeyos "github.com/mkishere/sshsyrup/os"
10 | "github.com/mkishere/sshsyrup/virtualfs"
11 | "github.com/spf13/pflag"
12 | )
13 |
14 | type ls struct{}
15 | type lsFileInfoSort []os.FileInfo
16 |
17 | func init() {
18 | honeyos.RegisterCommand("ls", ls{})
19 | }
20 |
21 | func (cmd ls) GetHelp() string {
22 | return ""
23 | }
24 |
25 | func (cmd ls) Where() string {
26 | return "/bin/ls"
27 | }
28 |
29 | func (cmd ls) Exec(args []string, sys honeyos.Sys) int {
30 | flag := pflag.NewFlagSet("arg", pflag.PanicOnError)
31 | flag.SetOutput(sys.Out())
32 | lMode := flag.BoolP("", "l", false, "use a long listing format")
33 | err := flag.Parse(args)
34 | f := flag.Args()
35 | var path string
36 | if len(f) > 0 {
37 | path = f[len(f)-1]
38 | } else {
39 | path = sys.Getcwd()
40 | }
41 |
42 | dir, err := sys.FSys().Open(path)
43 | if err != nil {
44 | fmt.Fprintf(sys.Out(), "ls: cannot access %v: No such file or directory\n", path)
45 | return 1
46 | }
47 | if *lMode {
48 | dir, err := dir.Readdir(-1)
49 | sortDir := lsFileInfoSort(dir)
50 | if err != nil {
51 | fmt.Fprintf(sys.Out(), "ls: cannot access %v: No such file or directory\n", path)
52 | return 1
53 | }
54 | sort.Sort(sortDir)
55 | for _, dir := range sortDir {
56 | fmt.Fprintln(sys.Out(), getLsString(dir))
57 | }
58 | } else {
59 |
60 | // Sort directory list
61 | dirName, err := dir.Readdirnames(-1)
62 | if err != nil {
63 | fmt.Fprintf(sys.Out(), "ls: cannot access %v: No such file or directory\n", path)
64 | return 1
65 | }
66 | maxlen := 0
67 | for _, d := range dirName {
68 | if len(d) > maxlen {
69 | maxlen = len(d)
70 | }
71 | }
72 | sort.Strings(dirName)
73 |
74 | itemPerRow := int(sys.Width()/(maxlen+1) - 1)
75 |
76 | for i := 0; i < len(dirName); i++ {
77 | if (i+1)%itemPerRow == 0 {
78 | fmt.Fprint(sys.Out(), "\n")
79 | }
80 | fmt.Fprintf(sys.Out(), "%v%v ", dirName[i], strings.Repeat(" ", maxlen-len(dirName[i])))
81 | }
82 | fmt.Fprint(sys.Out(), "\n")
83 | }
84 | return 0
85 | }
86 |
87 | func (fi lsFileInfoSort) Len() int { return len(fi) }
88 |
89 | func (fi lsFileInfoSort) Swap(i, j int) { fi[i], fi[j] = fi[j], fi[i] }
90 |
91 | func (fi lsFileInfoSort) Less(i, j int) bool { return fi[i].Name() < fi[j].Name() }
92 |
93 | func getLsString(fi os.FileInfo) string {
94 | uid, gid, _, _ := virtualfs.GetExtraInfo(fi)
95 | uName := honeyos.GetUserByID(uid).Name
96 | gName := honeyos.GetGroupByID(gid).Name
97 |
98 | size := fi.Size()
99 | if fi.IsDir() {
100 | size = 4096
101 | }
102 | return fmt.Sprintf("%v 1 %-8s %-8s %8d %v %v", strings.ToLower(fi.Mode().String()), uName, gName,
103 | size, fi.ModTime().Format("Jan 02 15:04"), fi.Name())
104 | }
105 |
--------------------------------------------------------------------------------
/os/command/pwd.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/mkishere/sshsyrup/os"
7 | )
8 |
9 | type pwd struct{}
10 |
11 | func init() {
12 | os.RegisterCommand("pwd", pwd{})
13 | }
14 |
15 | func (p pwd) GetHelp() string {
16 | return ""
17 | }
18 |
19 | func (p pwd) Exec(args []string, sys os.Sys) int {
20 | fmt.Fprintln(sys.Out(), sys.Getcwd())
21 | return 0
22 | }
23 |
24 | func (p pwd) Where() string {
25 | return "/bin/pwd"
26 | }
27 |
--------------------------------------------------------------------------------
/os/command/scp.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "os"
9 | pathlib "path"
10 | "strconv"
11 | "strings"
12 |
13 | log "github.com/sirupsen/logrus"
14 | "github.com/spf13/afero"
15 | "github.com/spf13/pflag"
16 | "github.com/spf13/viper"
17 | )
18 |
19 | type SCP struct {
20 | Fs afero.Fs
21 | log *log.Entry
22 | buf *bufio.ReadWriter
23 | }
24 |
25 | const (
26 | scp_OK byte = iota
27 | scp_ERR
28 | scp_FATAL
29 | )
30 |
31 | // NewSCP creates SCP instance for doing scp operations
32 | func NewSCP(ch io.ReadWriter, fs afero.Fs, log *log.Entry) *SCP {
33 | scp := &SCP{
34 | Fs: fs,
35 | log: log,
36 | }
37 | bufReader := bufio.NewReader(ch)
38 | bufWriter := bufio.NewWriter(ch)
39 | scp.buf = bufio.NewReadWriter(bufReader, bufWriter)
40 | return scp
41 | }
42 |
43 | // Main is the function for outside (the main routine) to invoke scp
44 | func (scp *SCP) Main(args []string, quit chan<- int) {
45 | flag := pflag.NewFlagSet("args", pflag.ContinueOnError)
46 | flag.SetOutput(scp.buf)
47 | toMode := flag.BoolP("to", "t", false, "To(Sink) mode")
48 | fromMode := flag.BoolP("from", "f", false, "From(Source) mode")
49 | recursive := flag.BoolP("", "r", false, "Recursive")
50 | flag.MarkHidden("to")
51 | flag.MarkHidden("from")
52 | err := flag.Parse(args)
53 | if err != nil {
54 | quit <- 1
55 | return
56 | }
57 |
58 | var res int
59 | if *toMode && *fromMode || !*toMode && !*fromMode {
60 | quit <- 1
61 | return
62 | } else if *toMode {
63 | res = scp.sinkMode(flag.Arg(0), *recursive)
64 | } else if *fromMode {
65 | res = scp.sourceMode(flag.Arg(0), *recursive)
66 | }
67 | quit <- res
68 | }
69 |
70 | // sinkMode is the function to receive files/commands from the client side
71 | func (scp *SCP) sinkMode(path string, isRecursive bool) int {
72 | scp.sendReply(scp_OK)
73 | cwd := path
74 | for {
75 | cmd, err := scp.buf.ReadString('\n')
76 | if err != nil && err != io.EOF {
77 | scp.log.WithError(err).Error("Error")
78 | return 1
79 | }
80 | if err == io.EOF {
81 | return 0
82 | }
83 | scp.log.Debug(cmd, []byte(cmd))
84 | switch cmd[0] {
85 | case 'C':
86 | args := strings.Split(cmd[:len(cmd)-1], " ")
87 | if len(args) < 3 {
88 | scp.sendReply(scp_ERR)
89 | continue
90 | }
91 | mode, err := strconv.ParseInt(args[0][1:], 8, 0)
92 | size, err := strconv.ParseInt(args[1], 10, 0)
93 | if err != nil {
94 | scp.sendReply(scp_ERR)
95 | continue
96 | }
97 | // Reject file size larger than limit
98 | if limit := int64(viper.GetSizeInBytes("server.receiveFileSizeLimit")); limit > 0 && size > limit {
99 | scp.sendReply(scp_ERR)
100 | continue
101 | }
102 |
103 | realPath := pathlib.Join(cwd, args[2])
104 | f, err := scp.Fs.OpenFile(realPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(mode))
105 | if err != nil {
106 | scp.sendReply(scp_ERR)
107 | continue
108 | }
109 |
110 | scp.sendReply(scp_OK)
111 | n, err := io.CopyN(f, scp.buf, int64(size))
112 | scp.log.WithFields(log.Fields{
113 | "path": realPath,
114 | "size": n,
115 | }).Infof("Server Received file %v %v bytes", realPath, n)
116 | if err != nil && err != io.EOF || n != size {
117 | scp.sendReply(scp_ERR)
118 | continue
119 | }
120 | // Discard the EOF following the file
121 | _, err = scp.buf.Discard(1)
122 | if err != nil {
123 | scp.sendReply(scp_ERR)
124 | }
125 | scp.sendReply(scp_OK)
126 | f.Close()
127 | case 'D':
128 | args := strings.Split(cmd[:len(cmd)-1], " ")
129 | if len(args) < 3 {
130 | scp.sendReply(scp_ERR)
131 | continue
132 | }
133 | mode, err := strconv.ParseInt(args[0][1:], 8, 0)
134 | if err != nil {
135 | scp.sendReply(scp_ERR)
136 | continue
137 | }
138 | cwd = pathlib.Join(cwd, args[2])
139 | err = scp.Fs.MkdirAll(cwd, 0755)
140 | if err != nil {
141 | scp.sendReply(scp_ERR)
142 | continue
143 | }
144 | scp.log.WithField("path", cwd).Infof("Server Created directory with mode %v", mode)
145 | scp.sendReply(scp_OK)
146 | case 'E':
147 | cwd = pathlib.Dir(cwd)
148 | scp.sendReply(scp_OK)
149 | case 'T':
150 | scp.sendReply(scp_OK)
151 | default:
152 | scp.sendReply(scp_ERR)
153 | }
154 |
155 | }
156 | }
157 |
158 | // sourceMode is the function to send files/commands to the client side
159 | func (scp *SCP) sourceMode(path string, isRecursive bool) int {
160 | cwd := path
161 | dirLevel := 0
162 | if isRecursive {
163 | fs := afero.Afero{scp.Fs}
164 | fs.Walk(path, func(p string, info os.FileInfo, err error) error {
165 | p = strings.Replace(p, "\\", "/", -1)
166 | if !strings.HasPrefix(p, cwd) {
167 | scp.buf.WriteString("E\n")
168 | scp.log.Debug("Server sending cmd:E")
169 | if b, err := scp.buf.ReadByte(); b != 0 || err != nil && err != io.EOF {
170 | if b != 0 {
171 | err = errors.New("Client side error")
172 | }
173 | return err
174 | }
175 | dirLevel--
176 | }
177 | if info.IsDir() {
178 | scp.buf.WriteString(fmt.Sprintf("D%04o 0 %v\n", info.Mode()&os.ModePerm, info.Name()))
179 | scp.log.Debugf("Server sending cmd:D%04o 0 %v", info.Mode()&os.ModePerm, info.Name())
180 | scp.buf.Flush()
181 | if b, err := scp.buf.ReadByte(); b != 0 || err != nil && err != io.EOF {
182 | if b != 0 {
183 | err = errors.New("Client side error")
184 | }
185 | return err
186 | }
187 | cwd = p
188 | dirLevel++
189 | } else {
190 | err := scp.sendFile(p, info)
191 | if err != nil {
192 | return err
193 | }
194 | cwd = pathlib.Dir(p)
195 | }
196 | return nil
197 | })
198 | for dirLevel > 0 {
199 | scp.buf.WriteString("E\n")
200 | scp.buf.Flush()
201 | scp.log.Debug("Server sending cmd:E")
202 | if b, err := scp.buf.ReadByte(); b != 0 || err != nil && err != io.EOF {
203 | if b != 0 {
204 | return 1
205 | }
206 | }
207 | dirLevel--
208 | }
209 | } else {
210 | fi, err := scp.Fs.Stat(path)
211 | if err != nil {
212 | scp.sendReply(scp_ERR)
213 | }
214 | err = scp.sendFile(path, fi)
215 | if err != nil {
216 | scp.sendReply(scp_ERR)
217 | }
218 | }
219 | return 0
220 | }
221 |
222 | func (scp *SCP) sendReply(reply byte) {
223 | scp.log.Debugf("Server Replying %v", reply)
224 | scp.buf.Write([]byte{reply})
225 | scp.buf.Flush()
226 | }
227 |
228 | func (scp *SCP) sendFile(p string, fi os.FileInfo) error {
229 | scp.buf.WriteString(fmt.Sprintf("C%04o %v %v\n", fi.Mode(), fi.Size(), fi.Name()))
230 | scp.log.Debugf("Server sending cmd:C%04o %v %v", fi.Mode(), fi.Size(), fi.Name())
231 | scp.log.WithField("file", p).Info("Server sending file")
232 | scp.buf.Flush()
233 | if b, err := scp.buf.ReadByte(); b != 0 || err != nil && err != io.EOF {
234 | if b != 0 {
235 | err = errors.New("Client side error")
236 | }
237 | return err
238 | }
239 | f, err := scp.Fs.OpenFile(p, os.O_RDONLY, 0777)
240 | if err != nil {
241 | return err
242 | }
243 | defer f.Close()
244 | _, err = io.Copy(scp.buf, f)
245 | if err != nil {
246 | return err
247 | }
248 | err = scp.buf.WriteByte(0)
249 | scp.buf.Flush()
250 | if err != nil {
251 | return err
252 | }
253 | if b, err := scp.buf.ReadByte(); err != nil || b != scp_OK {
254 | return err
255 | }
256 | return nil
257 | }
258 |
--------------------------------------------------------------------------------
/os/command/uname.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/mkishere/sshsyrup/os"
9 | "github.com/spf13/pflag"
10 | )
11 |
12 | type uname struct{}
13 |
14 | const (
15 | unameKName = "Linux"
16 | unameKRel = "4.4.0-43-generic"
17 | unameKVer = "#129-Ubuntu SMP Thu Mar 17 20:17:14 UTC 2017"
18 | unameMach = "x86-64"
19 | unameProc = "x86-64"
20 | unameHWPlat = "x86-64"
21 | unameOS = "GNU/Linux"
22 | )
23 |
24 | func init() {
25 | os.RegisterCommand("uname", uname{})
26 | }
27 |
28 | func (un uname) GetHelp() string {
29 | return ""
30 | }
31 |
32 | func (un uname) Exec(args []string, sys os.Sys) int {
33 | flag := pflag.NewFlagSet("arg", pflag.ContinueOnError)
34 | all := flag.BoolP("all", "a", false,
35 | "print all information, in the following order,\n except omit -p and -i if unknown:")
36 | kName := flag.BoolP("kernel-name", "s", false, "print the kernel name")
37 | nName := flag.BoolP("nodename", "n", false, "print the network node hostname")
38 | kRel := flag.BoolP("kernel-release", "r", false, "print the kernel release")
39 | kVer := flag.BoolP("kernel-version", "v", false, "print the kernel version")
40 | mach := flag.BoolP("machine", "m", false, "print the machine hardware name")
41 | proc := flag.BoolP("processor", "p", false, "print the processor type or \"unknown\"")
42 | hwPlat := flag.BoolP("hardware-platform", "i", false, "print the hardware platform or \"unknown\"")
43 | help := flag.Bool("help", false, "display this help and exit")
44 | ver := flag.Bool("version", false, "output version information and exit")
45 | os := flag.BoolP("operating-system", "o", false, "print the operating system")
46 | flag.SetOutput(sys.Out())
47 | flag.Usage = func() {
48 | fmt.Fprintf(sys.Out(), "Usage: uname [OPTION]...\n")
49 | fmt.Fprintf(sys.Out(), "Print certain system information. With no OPTION, same as -s.\n\n")
50 | flag.PrintDefaults()
51 | fmt.Fprintln(sys.Out(), "\nReport uname bugs to bug-coreutils@gnu.org")
52 | fmt.Fprintln(sys.Out(), "GNU coreutils home page: ")
53 | fmt.Fprintln(sys.Out(), "General help using GNU software: ")
54 | fmt.Fprintln(sys.Out(), "For complete documentation, run: info coreutils 'uname invocation'")
55 | }
56 | err := flag.Parse(args)
57 | if err != nil {
58 | return 1
59 | }
60 | if *all {
61 | fmt.Fprintf(sys.Out(), "%v %v %v %v %v %v %v %v\n", unameKName, sys.Hostname(), unameKRel,
62 | unameKVer, unameMach, unameProc, unameHWPlat, unameOS)
63 | } else if len(args) == 0 {
64 | fmt.Fprintln(sys.Out(), unameKName)
65 | } else if *ver {
66 | fmt.Fprint(sys.Out(), un.PrintVer())
67 | } else if *help {
68 | flag.Usage()
69 | } else {
70 | var uNameStr bytes.Buffer
71 | switch {
72 | case *kName:
73 | uNameStr.WriteString(unameKName)
74 | fallthrough
75 | case *nName:
76 | uNameStr.WriteString(" " + sys.Hostname())
77 | fallthrough
78 | case *kRel:
79 | uNameStr.WriteString(" " + unameKRel)
80 | fallthrough
81 | case *kVer:
82 | uNameStr.WriteString(" " + unameKVer)
83 | fallthrough
84 | case *mach:
85 | uNameStr.WriteString(" " + unameMach)
86 | fallthrough
87 | case *proc:
88 | uNameStr.WriteString(" " + unameProc)
89 | fallthrough
90 | case *hwPlat:
91 | uNameStr.WriteString(" " + unameHWPlat)
92 | fallthrough
93 | case *os:
94 | uNameStr.WriteString(" " + unameOS)
95 | }
96 | fmt.Fprintln(sys.Out(), strings.TrimSpace(uNameStr.String()))
97 | }
98 | return 0
99 | }
100 |
101 | func (un uname) Where() string {
102 | return "/bin/uname"
103 | }
104 |
105 | func (un uname) PrintVer() string {
106 | return "uname (GNU coreutils) 8.21\n" +
107 | "Copyright (C) 2013 Free Software Foundation, Inc.\n" +
108 | "License GPLv3+: GNU GPL version 3 or later .\n" +
109 | "This is free software: you are free to change and redistribute it.\n" +
110 | "There is NO WARRANTY, to the extent permitted by law.\n\n" +
111 | "Written by David MacKenzie."
112 | }
113 |
--------------------------------------------------------------------------------
/os/command/uptime.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "time"
7 |
8 | "github.com/mkishere/sshsyrup/os"
9 | )
10 |
11 | type uptime struct{}
12 |
13 | func init() {
14 | os.RegisterCommand("uptime", uptime{})
15 | }
16 |
17 | func (uptime) GetHelp() string {
18 | return ""
19 | }
20 |
21 | func (uptime) Exec(args []string, sys os.Sys) int {
22 | currTime := time.Now()
23 | last5 := rand.Float32() + 9
24 | last10 := rand.Float32() + 9
25 | last15 := rand.Float32() + 9
26 | fmt.Fprintf(sys.Out(), "%v up 3 days, 3 users, load average: %.2f, %.2f, %.2f\n", currTime.Format("03:04:05"), last5, last10, last15)
27 | return 0
28 | }
29 |
30 | func (uptime) Where() string {
31 | return "/usr/bin/uptime"
32 | }
33 |
--------------------------------------------------------------------------------
/os/command/wget.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net"
7 | "net/http"
8 | urllib "net/url"
9 | "path"
10 | "strconv"
11 | "strings"
12 | "time"
13 |
14 | "github.com/mkishere/sshsyrup/os"
15 | "github.com/spf13/afero"
16 | "github.com/spf13/pflag"
17 | )
18 |
19 | type wget struct{}
20 |
21 | func init() {
22 | os.RegisterCommand("wget", wget{})
23 |
24 | }
25 | func (wg wget) GetHelp() string {
26 | return ""
27 | }
28 |
29 | func printTs() string {
30 | return time.Now().Format("2006-01-02 03:04:05")
31 | }
32 |
33 | func (wg wget) Exec(args []string, sys os.Sys) int {
34 | flag := pflag.NewFlagSet("arg", pflag.ContinueOnError)
35 | out := flag.String("O", "", "write documents to FILE.")
36 | quiet := flag.BoolP("quiet", "q", false, "quiet (no output).")
37 | flag.SetOutput(sys.Out())
38 | err := flag.Parse(args)
39 | f := flag.Args()
40 | if len(args) == 0 {
41 | fmt.Fprintln(sys.Out(), "wget: missing URL\nUsage: wget [OPTION]... [URL]...\n\nTry `wget --help' for more options.")
42 | return 1
43 | }
44 | url := strings.TrimSpace(f[0])
45 | if !strings.Contains(url, "://") {
46 | url = "http://" + url
47 | }
48 | urlobj, err := urllib.Parse(url)
49 | if err != nil {
50 | fmt.Fprintln(sys.Out(), "Malformed URL")
51 | return 1
52 | }
53 | if !*quiet {
54 | if urlobj.Scheme != "http" && urlobj.Scheme != "https" {
55 | fmt.Fprintf(sys.Out(), "Resolving %v (%v)... failed: Name or service not known.\n", urlobj.Scheme, urlobj.Scheme)
56 | fmt.Fprintf(sys.Out(), "wget: unable to resolve host address ‘%v’\n", urlobj.Scheme)
57 | return 1
58 | }
59 | fmt.Fprintf(sys.Out(), "--%v-- %v\n", printTs(), url)
60 | }
61 | ip, err := net.LookupIP(urlobj.Hostname())
62 | if err != nil {
63 | // handle error
64 | fmt.Fprintln(sys.Err(), err)
65 | }
66 | if !*quiet {
67 | fmt.Fprintf(sys.Out(), "Resolving %v (%v)... %v\n", urlobj.Hostname(), urlobj.Hostname(), ip)
68 | }
69 | resp, err := http.Get(url)
70 | if err != nil {
71 | // handle error
72 | fmt.Fprintln(sys.Err(), err)
73 | return 1
74 | }
75 | if !*quiet {
76 | fmt.Fprintf(sys.Out(), "Connecting to %v (%v)|%v|:80... connected\n", urlobj.Hostname(), urlobj.Hostname(), ip[0])
77 | mimeType := resp.Header.Get("Content-Type")
78 | fmt.Fprintln(sys.Out(), "HTTP request sent, awaiting response... 200 OK")
79 | fmt.Fprintf(sys.Out(), "Length: unspecified [%v]\n", mimeType[:strings.LastIndex(mimeType, ";")])
80 | }
81 | defer resp.Body.Close()
82 | b, err := ioutil.ReadAll(resp.Body)
83 | if err != nil {
84 | //handle
85 | fmt.Fprintln(sys.Err(), err)
86 | return 1
87 | }
88 | if *out == "" {
89 | *out = "index.html"
90 | }
91 | if !*quiet {
92 | fmt.Fprintf(sys.Out(), "Saving to: ‘%v’\n\n", *out)
93 | fmt.Fprintf(sys.Out(), "[ <=>%v ] %v --.-K/s in 0.1s\n", strings.Repeat(" ", sys.Width()-38), format(len(b)))
94 | }
95 | af := afero.Afero{sys.FSys()}
96 |
97 | p := *out
98 | if !path.IsAbs(p) {
99 | p = path.Join(sys.Getcwd(), p)
100 | }
101 | err = af.WriteFile(p, b, 0666)
102 | if err != nil {
103 | fmt.Fprintln(sys.Err(), err)
104 | return 1
105 | }
106 | if !*quiet {
107 | fmt.Fprintf(sys.Out(), "%v (0.5 KB/s) - ‘%v’ saved[%v]\n", printTs(), *out, format(len(b)))
108 | }
109 | return 0
110 | }
111 |
112 | func (wg wget) Where() string {
113 | return "/usr/bin/wget"
114 | }
115 |
116 | // Idea from https://stackoverflow.com/a/31046325
117 | func format(n int) string {
118 | in := strconv.Itoa(n)
119 | out := make([]byte, len(in)+len(in)/3)
120 |
121 | for i, j, k := len(in)-1, len(out)-1, 0; ; i, j = i-1, j-1 {
122 | out[j] = in[i]
123 | if i == 0 {
124 | return string(out)
125 | }
126 | if k++; k == 3 {
127 | j, k = j-1, 0
128 | out[j] = ','
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/os/command/whoami.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/mkishere/sshsyrup/os"
7 | )
8 |
9 | type whoami struct{}
10 |
11 | func init() {
12 | os.RegisterCommand("whoami", whoami{})
13 | }
14 |
15 | func (whoami) GetHelp() string {
16 | return ""
17 | }
18 |
19 | func (whoami) Exec(args []string, sys os.Sys) int {
20 | id := sys.CurrentUser()
21 | u := os.GetUserByID(id)
22 | fmt.Fprintln(sys.Out(), u.Name)
23 | return 0
24 | }
25 |
26 | func (whoami) Where() string {
27 | return "/usr/bin/whoami"
28 | }
29 |
--------------------------------------------------------------------------------
/os/shell.go:
--------------------------------------------------------------------------------
1 | package os
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "strings"
7 |
8 | "github.com/mattn/go-shellwords"
9 | "github.com/mkishere/sshsyrup/util/termlogger"
10 | log "github.com/sirupsen/logrus"
11 | "golang.org/x/crypto/ssh/terminal"
12 | )
13 |
14 | type Shell struct {
15 | log *log.Entry
16 | termSignal chan<- int
17 | terminal *terminal.Terminal
18 | sys *System
19 | DelayFunc func()
20 | }
21 |
22 | func NewShell(sys *System, ipSrc string, log *log.Entry, termSignal chan<- int) *Shell {
23 |
24 | return &Shell{
25 | log: log,
26 | termSignal: termSignal,
27 | sys: sys,
28 | }
29 | }
30 |
31 | func (sh *Shell) HandleRequest(hook termlogger.LogHook) {
32 |
33 | tLog := termlogger.NewLogger(hook, sh.sys.In(), sh.sys.Out(), sh.sys.Err())
34 | defer tLog.Close()
35 |
36 | sh.terminal = terminal.NewTerminal(struct {
37 | io.Reader
38 | io.Writer
39 | }{
40 | tLog.In(),
41 | tLog.Out(),
42 | }, "$ ")
43 | defer func() {
44 | if r := recover(); r != nil {
45 | sh.log.Errorf("Recovered from panic %v", r)
46 | sh.termSignal <- 1
47 | }
48 | }()
49 | shellParser := shellwords.NewParser()
50 | shellParser.ParseBacktick = false
51 | for {
52 | cmd, err := sh.terminal.ReadLine()
53 | if len(strings.TrimSpace(cmd)) > 0 {
54 | sh.log.WithField("cmd", cmd).Infof("User input command %v", cmd)
55 | }
56 | if sh.DelayFunc != nil {
57 | sh.DelayFunc()
58 | }
59 | if err != nil {
60 | if err.Error() == "EOF" {
61 | sh.log.WithError(err).Info("Client disconnected from server")
62 | sh.termSignal <- 0
63 | return
64 | }
65 | sh.log.WithError(err).Error("Error when reading terminal")
66 | break
67 | }
68 |
69 | pos := 0
70 | for pos < len(cmd) {
71 | cmdList, err := shellParser.Parse(cmd[pos:])
72 | if err != nil {
73 | sh.terminal.Write([]byte(cmd[pos:] + ": command not found"))
74 | break
75 | }
76 | for i, cmdComp := range cmdList {
77 | if strings.Contains(cmdComp, "=") {
78 | envVar := strings.SplitN(cmdComp, "=", 1)
79 | sh.sys.SetEnv(envVar[0], envVar[1])
80 | } else {
81 | sh.ExecCmd(strings.Join(cmdList[i:], " "), tLog)
82 | break
83 | }
84 | }
85 | if shellParser.Position == -1 {
86 | break
87 | }
88 | pos += shellParser.Position + 1
89 | }
90 | }
91 | }
92 |
93 | func (sh *Shell) SetSize(width, height int) error {
94 | sh.sys.width = width
95 | sh.sys.height = height
96 | return sh.terminal.SetSize(width, height)
97 | }
98 |
99 | func (sh *Shell) ExecCmd(cmd string, tLog termlogger.StdIOErr) {
100 | cmd = strings.TrimSpace(cmd)
101 | switch {
102 |
103 | case strings.TrimSpace(cmd) == "":
104 | //Do nothing
105 | case cmd == "logout", cmd == "exit":
106 | sh.log.Infof("User logged out")
107 | sh.terminal.Write([]byte("logout\n"))
108 | sh.terminal.SetPrompt("")
109 | sh.termSignal <- 0
110 | return
111 | case strings.HasPrefix(cmd, "cd"):
112 | args := strings.Split(cmd, " ")
113 | if len(args) > 1 {
114 | err := sh.sys.Chdir(args[1])
115 | if err != nil {
116 | sh.terminal.Write([]byte(fmt.Sprintf("-bash: cd: %v: No such file or directory\n", args[1])))
117 | }
118 | }
119 | case strings.HasPrefix(cmd, "export"):
120 |
121 | default:
122 | args := strings.Split(cmd, " ")
123 | n, err := sh.sys.exec(args[0], args[1:], tLog)
124 | if err != nil {
125 | sh.terminal.Write([]byte(fmt.Sprintf("%v: command not found\n", args[0])))
126 | } else {
127 | sh.sys.envVars["?"] = string(n)
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/os/sys.go:
--------------------------------------------------------------------------------
1 | package os
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "os"
9 | pathlib "path"
10 |
11 | "github.com/mkishere/sshsyrup/util/termlogger"
12 |
13 | log "github.com/sirupsen/logrus"
14 | "github.com/spf13/afero"
15 | "golang.org/x/crypto/ssh"
16 | )
17 |
18 | var (
19 | crlf = []byte{'\r', '\n'}
20 | )
21 |
22 | var (
23 | funcMap = make(map[string]Command)
24 | fakeFuncList = make(map[string]string)
25 | )
26 |
27 | var (
28 | errMsgList = map[string]struct{}{
29 | "Segmentation fault": struct{}{},
30 | "Permission denied": struct{}{},
31 | }
32 | )
33 |
34 | // Command interface allow classes to simulate real executable
35 | // that have access to standard I/O, filesystem, arguments, EnvVars,
36 | // and cwd
37 | type Command interface {
38 | GetHelp() string
39 | Exec(args []string, sys Sys) int
40 | Where() string
41 | }
42 |
43 | // System provides what most of os/sys does in the honeyport
44 | type System struct {
45 | userId int
46 | cwd string
47 | fSys afero.Fs
48 | sshChan ssh.Channel
49 | envVars map[string]string
50 | width, height int
51 | log *log.Entry
52 | sessionLog termlogger.LogHook
53 | hostName string
54 | }
55 |
56 | type Sys interface {
57 | Getcwd() string
58 | Chdir(path string) error
59 | In() io.Reader
60 | Out() io.Writer
61 | Err() io.Writer
62 | Environ() (env []string)
63 | SetEnv(key, value string) error
64 | FSys() afero.Fs
65 | Width() int
66 | Height() int
67 | CurrentUser() int
68 | CurrentGroup() int
69 | Hostname() string
70 | }
71 | type stdoutWrapper struct {
72 | io.Writer
73 | }
74 |
75 | type sysLogWrapper struct {
76 | termlogger.StdIOErr
77 | *System
78 | }
79 |
80 | func (sys *sysLogWrapper) In() io.Reader { return sys.StdIOErr.In() }
81 | func (sys *sysLogWrapper) Out() io.Writer { return stdoutWrapper{sys.StdIOErr.Out()} }
82 | func (sys *sysLogWrapper) Err() io.Writer { return stdoutWrapper{sys.StdIOErr.Err()} }
83 |
84 | // NewSystem initializer a system object containing current user context: ID,
85 | // home directory, terminal dimensions, etc.
86 | func NewSystem(user, host string, fs afero.Fs, channel ssh.Channel, width, height int, log *log.Entry) *System {
87 | if _, exists := IsUserExist(user); !exists {
88 | CreateUser(user, "password")
89 | }
90 | aferoFs := afero.Afero{fs}
91 | if exists, _ := aferoFs.DirExists(usernameMapping[user].Homedir); !exists {
92 | aferoFs.MkdirAll(usernameMapping[user].Homedir, 0755)
93 | }
94 |
95 | return &System{
96 | cwd: usernameMapping[user].Homedir,
97 | fSys: aferoFs,
98 | envVars: map[string]string{},
99 | sshChan: channel,
100 | width: width,
101 | height: height,
102 | log: log,
103 | userId: usernameMapping[user].UID,
104 | hostName: host,
105 | }
106 | }
107 |
108 | // Getcwd gets current working directory
109 | func (sys *System) Getcwd() string {
110 | return sys.cwd
111 | }
112 |
113 | // Chdir change current working directory
114 | func (sys *System) Chdir(path string) error {
115 | if !pathlib.IsAbs(path) {
116 | path = sys.cwd + "/" + path
117 | }
118 | path = pathlib.Clean(path)
119 | if exists, err := afero.DirExists(sys.fSys, path); err == nil && !exists {
120 | return os.ErrNotExist
121 | } else if err != nil {
122 | return err
123 | }
124 | sys.cwd = path
125 | return nil
126 | }
127 |
128 | func (sys *System) CurrentUser() int { return sys.userId }
129 |
130 | func (sys *System) CurrentGroup() int {
131 | u := GetUserByID(sys.userId)
132 | return u.GID
133 | }
134 |
135 | func (sys *System) Hostname() string {
136 | return sys.hostName
137 | }
138 |
139 | // In returns a io.Reader that represent stdin
140 | func (sys *System) In() io.Reader { return sys.sshChan }
141 |
142 | // Out returns a io.Writer that represent stdout
143 | func (sys *System) Out() io.Writer {
144 | return stdoutWrapper{sys.sshChan}
145 | }
146 |
147 | func (sys *System) Err() io.Writer {
148 | return stdoutWrapper{sys.sshChan.Stderr()}
149 | }
150 |
151 | func (sys *System) IOStream() io.ReadWriter { return sys.sshChan }
152 |
153 | func (sys *System) FSys() afero.Fs { return sys.fSys }
154 |
155 | func (sys *System) Width() int { return sys.width }
156 |
157 | func (sys *System) Height() int { return sys.height }
158 |
159 | // Write replace \n with \r\n before writing to the underlying io.Writer.
160 | // Copied from golang.org/x/crypto/ssh/terminal
161 | func (sw stdoutWrapper) Write(buf []byte) (n int, err error) {
162 | for len(buf) > 0 {
163 | i := bytes.IndexByte(buf, '\n')
164 | todo := len(buf)
165 | if i >= 0 {
166 | todo = i
167 | }
168 |
169 | var nn int
170 | nn, err = sw.Writer.Write(buf[:todo])
171 | n += nn
172 | if err != nil {
173 | return n, err
174 | }
175 | buf = buf[todo:]
176 |
177 | if i >= 0 {
178 | if _, err = sw.Writer.Write(crlf); err != nil {
179 | return n, err
180 | }
181 | n++
182 | buf = buf[1:]
183 | }
184 | }
185 |
186 | return n, nil
187 | }
188 |
189 | func (sys *System) Environ() (env []string) {
190 | env = make([]string, 0, len(sys.envVars))
191 | for k, v := range sys.envVars {
192 | env = append(env, fmt.Sprintf("%v=%v", k, v))
193 | }
194 | return
195 | }
196 |
197 | func (sys *System) SetEnv(key, value string) error {
198 | sys.envVars[key] = value
199 | return nil
200 | }
201 |
202 | func (sys *System) Exec(path string, args []string) (int, error) {
203 | return sys.exec(path, args, nil)
204 | }
205 |
206 | func (sys *System) exec(path string, args []string, io termlogger.StdIOErr) (int, error) {
207 | cmd := pathlib.Base(path)
208 | if execFunc, ok := funcMap[cmd]; ok {
209 |
210 | defer func() {
211 | if r := recover(); r != nil {
212 | sys.log.WithFields(log.Fields{
213 | "cmd": path,
214 | "args": args,
215 | "error": r,
216 | }).Error("Command has crashed")
217 | sys.Err().Write([]byte("Segmentation fault\n"))
218 | }
219 | }()
220 | var res int
221 | // If logger is not nil, redirect IO to it
222 | if io != nil {
223 | loggedSys := &sysLogWrapper{io, sys}
224 | res = execFunc.Exec(args, loggedSys)
225 | } else {
226 | res = execFunc.Exec(args, sys)
227 | }
228 | return res, nil
229 | } else if output, inList := fakeFuncList[cmd]; inList {
230 | // Print random error message
231 | // Make use of golang map random nature :)
232 | if len(output) == 0 {
233 | return printRandomError(sys)
234 | }
235 | // Read file and write output
236 | content, err := ioutil.ReadFile(output)
237 | if err != nil {
238 | return printRandomError(sys)
239 | }
240 | sys.Out().Write(content)
241 | return 0, nil
242 | }
243 |
244 | return 127, &os.PathError{Op: "exec", Path: path, Err: os.ErrNotExist}
245 | }
246 |
247 | // RegisterCommand puts the command implementation into map so
248 | // it can be invoked from command line
249 | func RegisterCommand(name string, cmd Command) {
250 | funcMap[name] = cmd
251 | funcMap[cmd.Where()] = cmd
252 | }
253 |
254 | // RegisterFakeCommand put commands into register so that when
255 | // typed in terminal they will print out SegFault
256 | func RegisterFakeCommand(cmdList []string) {
257 | for i := range cmdList {
258 | fakeFuncList[cmdList[i]] = ""
259 | }
260 | }
261 |
262 | // RegisterCommandOutput gets the file content and associate
263 | // it with the command provided. So that once triggered in
264 | // console the content will be displayed
265 | func RegisterCommandOutput(cmd, pathToOutput string) {
266 | fakeFuncList[cmd] = pathToOutput
267 | }
268 |
269 | func printRandomError(sys *System) (int, error) {
270 | for msg := range errMsgList {
271 | sys.Err().Write([]byte(msg + "\n"))
272 | break
273 | }
274 | return 1, nil
275 | }
276 |
--------------------------------------------------------------------------------
/passwd:
--------------------------------------------------------------------------------
1 | root:*:0:0:root:/root:/bin/bash
2 | daemon:*:1:1:daemon:/usr/sbin:/usr/sbin/nologin
3 | bin:*:2:2:bin:/bin:/usr/sbin/nologin
4 | sys:*:3:3:sys:/dev:/usr/sbin/nologin
5 | sync:*:4:65534:sync:/bin:/bin/sync
6 | games:*:5:60:games:/usr/games:/usr/sbin/nologin
7 | man:*:6:12:man:/var/cache/man:/usr/sbin/nologin
8 | lp:*:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
9 | mail:*:8:8:mail:/var/mail:/usr/sbin/nologin
10 | news:*:9:9:news:/var/spool/news:/usr/sbin/nologin
11 | uucp:*:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
12 | proxy:*:13:13:proxy:/bin:/usr/sbin/nologin
13 | www-data:*:33:33:www-data:/var/www:/usr/sbin/nologin
14 | backup:*:34:34:backup:/var/backups:/usr/sbin/nologin
15 | list:*:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
16 | irc:*:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
17 | gnats:*:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
18 | nobody:*:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
19 | libuuid:*:100:101::/var/lib/libuuid:
20 | syslog:*:101:104::/home/syslog:/bin/false
21 | messagebus:*:102:106::/var/run/dbus:/bin/false
22 | sshd:*:104:65534::/var/run/sshd:/usr/sbin/nologin
23 | pollinate:*:105:1::/var/cache/pollinate:/bin/false
24 | testuser:*:1000:1000:"",,,:/home/testuser:/bin/bash
25 |
--------------------------------------------------------------------------------
/sftp/attrflag_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type AttrFlag"; DO NOT EDIT.
2 |
3 | package sftp
4 |
5 | import "strconv"
6 |
7 | const (
8 | _AttrFlag_name_0 = "SSH_FILEXFER_ATTR_SIZESSH_FILEXFER_ATTR_UIDGID"
9 | _AttrFlag_name_1 = "SSH_FILEXFER_ATTR_PERMISSIONS"
10 | _AttrFlag_name_2 = "SSH_FILEXFER_ATTR_ACMODTIME"
11 | _AttrFlag_name_3 = "SSH_FILEXFER_ATTR_EXTENDED"
12 | )
13 |
14 | var (
15 | _AttrFlag_index_0 = [...]uint8{0, 22, 46}
16 | _AttrFlag_index_1 = [...]uint8{0, 29}
17 | _AttrFlag_index_2 = [...]uint8{0, 27}
18 | _AttrFlag_index_3 = [...]uint8{0, 26}
19 | )
20 |
21 | func (i AttrFlag) String() string {
22 | switch {
23 | case 1 <= i && i <= 2:
24 | i -= 1
25 | return _AttrFlag_name_0[_AttrFlag_index_0[i]:_AttrFlag_index_0[i+1]]
26 | case i == 4:
27 | return _AttrFlag_name_1
28 | case i == 8:
29 | return _AttrFlag_name_2
30 | case i == 2147483648:
31 | return _AttrFlag_name_3
32 | default:
33 | return "AttrFlag(" + strconv.FormatInt(int64(i), 10) + ")"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sftp/packettype_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type PacketType"; DO NOT EDIT.
2 |
3 | package sftp
4 |
5 | import "strconv"
6 |
7 | const (
8 | _PacketType_name_0 = "SSH_FXP_INITSSH_FXP_VERSIONSSH_FXP_OPENSSH_FXP_CLOSESSH_FXP_READSSH_FXP_WRITESSH_FXP_LSTATSSH_FXP_FSTATSSH_FXP_SETSTATSSH_FXP_FSETSTATSSH_FXP_OPENDIRSSH_FXP_READDIRSSH_FXP_REMOVESSH_FXP_MKDIRSSH_FXP_RMDIRSSH_FXP_REALPATHSSH_FXP_STATSSH_FXP_RENAMESSH_FXP_READLINKSSH_FXP_LINKSSH_FXP_BLOCKSSH_FXP_UNBLOCK"
9 | _PacketType_name_1 = "SSH_FXP_STATUSSSH_FXP_HANDLESSH_FXP_DATASSH_FXP_NAMESSH_FXP_ATTRS"
10 | _PacketType_name_2 = "SSH_FXP_EXTENDEDSSH_FXP_EXTENDED_REPLY"
11 | )
12 |
13 | var (
14 | _PacketType_index_0 = [...]uint16{0, 12, 27, 39, 52, 64, 77, 90, 103, 118, 134, 149, 164, 178, 191, 204, 220, 232, 246, 262, 274, 287, 302}
15 | _PacketType_index_1 = [...]uint8{0, 14, 28, 40, 52, 65}
16 | _PacketType_index_2 = [...]uint8{0, 16, 38}
17 | )
18 |
19 | func (i PacketType) String() string {
20 | switch {
21 | case 1 <= i && i <= 22:
22 | i -= 1
23 | return _PacketType_name_0[_PacketType_index_0[i]:_PacketType_index_0[i+1]]
24 | case 101 <= i && i <= 105:
25 | i -= 101
26 | return _PacketType_name_1[_PacketType_index_1[i]:_PacketType_index_1[i+1]]
27 | case 201 <= i && i <= 202:
28 | i -= 201
29 | return _PacketType_name_2[_PacketType_index_2[i]:_PacketType_index_2[i+1]]
30 | default:
31 | return "PacketType(" + strconv.FormatInt(int64(i), 10) + ")"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sftp/sftp.go:
--------------------------------------------------------------------------------
1 | package sftp
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "io"
7 | "os"
8 | "strconv"
9 | "sync"
10 |
11 | pathlib "path"
12 |
13 | honeyos "github.com/mkishere/sshsyrup/os"
14 | "github.com/mkishere/sshsyrup/virtualfs"
15 | log "github.com/sirupsen/logrus"
16 | "github.com/spf13/afero"
17 | "github.com/spf13/viper"
18 | )
19 |
20 | type sftpMsg struct {
21 | Type PacketType
22 | ReqID uint32
23 | Payload []byte
24 | }
25 |
26 | type Sftp struct {
27 | conn io.ReadWriter
28 | vfs afero.Afero
29 | cwd string
30 | quit chan<- int
31 | fileHandleMap map[int]afero.File
32 | nextHandle int
33 | lock sync.RWMutex
34 | dirCache map[int]*dirContent
35 | fileEOFCache map[int]bool
36 | log *log.Entry
37 | }
38 |
39 | type dirContent struct {
40 | offset int
41 | fi []os.FileInfo
42 | }
43 |
44 | const (
45 | entriesPerFetch = 120
46 | )
47 |
48 | func (sftp *Sftp) GetRealPath(path string) string {
49 | if !pathlib.IsAbs(path) {
50 | path = sftp.cwd + "/" + path
51 | }
52 | return pathlib.Clean(path)
53 | }
54 |
55 | func NewSftp(conn io.ReadWriter, vfs afero.Fs, user string, log *log.Entry, quitSig chan<- int) *Sftp {
56 | u := honeyos.GetUser(user)
57 | fs := afero.Afero{vfs}
58 | if exists, _ := fs.DirExists(u.Homedir); !exists {
59 | fs.MkdirAll(u.Homedir, 0755)
60 | }
61 | return &Sftp{
62 | conn: conn,
63 | vfs: fs,
64 | cwd: u.Homedir,
65 | quit: quitSig,
66 | fileHandleMap: map[int]afero.File{},
67 | nextHandle: 0,
68 | dirCache: map[int]*dirContent{},
69 | log: log,
70 | fileEOFCache: map[int]bool{},
71 | }
72 | }
73 |
74 | func (sftp *Sftp) HandleRequest() {
75 | defer sftp.cleanUp()
76 | for {
77 | req, err := readRequest(sftp.conn)
78 | if err != nil {
79 | // Other side has disconnect, signal channel level to close
80 | if err == io.EOF {
81 | defer func() { sftp.quit <- 0 }()
82 | } else {
83 | defer func() { sftp.quit <- 1 }()
84 | }
85 | break
86 | }
87 | if req.Type == SSH_FXP_DATA || req.Type == SSH_FXP_WRITE {
88 | sftp.log.Debugf("Req:%v Seq:%d Payload(Len:%v)", req.Type, req.ReqID, len(req.Payload))
89 | } else {
90 | sftp.log.Debugf("Req:%v Seq:%d Payload(Len:%v):%v", req.Type, req.ReqID, len(req.Payload), req.Payload)
91 | }
92 | switch req.Type {
93 | case SSH_FXP_INIT:
94 | sftp.sendReply(sftp.conn, createInit())
95 | case SSH_FXP_REALPATH:
96 | path := byteToStr(req.Payload)
97 | path = sftp.GetRealPath(path)
98 | sftp.log.WithField("path", path).Infof("Retrieving realpath")
99 | fi, err := sftp.vfs.Fs.Stat(path)
100 | if err != nil {
101 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_NO_SUCH_FILE))
102 | continue
103 | } else {
104 | b, err := createNamePacket([]string{path}, []os.FileInfo{fi})
105 | if err != nil {
106 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
107 | continue
108 | }
109 | sftp.sendReply(sftp.conn, sftpMsg{
110 | Type: SSH_FXP_NAME,
111 | ReqID: req.ReqID,
112 | Payload: b,
113 | })
114 | }
115 | case SSH_FXP_OPENDIR:
116 | path := byteToStr(req.Payload)
117 | sftp.log.WithField("path", path).Infof("Opening directory")
118 | if len(path) == 0 {
119 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_BAD_MESSAGE))
120 | continue
121 | }
122 | path = sftp.GetRealPath(path)
123 | fileHn, err := sftp.Open(path)
124 | if err != nil {
125 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_NO_SUCH_FILE))
126 | continue
127 | }
128 | b := make([]byte, 4+len(fileHn))
129 | strToByte(b, fileHn)
130 | sftp.sendReply(sftp.conn, sftpMsg{
131 | ReqID: req.ReqID,
132 | Type: SSH_FXP_HANDLE,
133 | Payload: b,
134 | })
135 | case SSH_FXP_READDIR:
136 | handle := byteToStr(req.Payload)
137 | b, err := sftp.readDir(handle)
138 | if err != nil {
139 | if err == io.EOF {
140 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_EOF))
141 | } else {
142 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
143 | }
144 | continue
145 | }
146 | sftp.sendReply(sftp.conn, sftpMsg{
147 | Type: SSH_FXP_NAME,
148 | ReqID: req.ReqID,
149 | Payload: b,
150 | })
151 | case SSH_FXP_CLOSE:
152 | handle := byteToStr(req.Payload)
153 | err := sftp.close(handle)
154 | if err != nil {
155 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
156 | continue
157 | }
158 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_OK))
159 | case SSH_FXP_LSTAT, SSH_FXP_STAT:
160 | // Currently we don't distinguish symlink
161 | path := byteToStr(req.Payload)
162 | if len(path) == 0 {
163 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_BAD_MESSAGE))
164 | continue
165 | }
166 | path = sftp.GetRealPath(path)
167 | fi, err := sftp.vfs.Stat(path)
168 | if err != nil {
169 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_NO_SUCH_FILE))
170 | continue
171 | }
172 | b := make([]byte, 32)
173 | fileAttrToByte(b, fi)
174 | sftp.sendReply(sftp.conn, sftpMsg{
175 | Type: SSH_FXP_ATTRS,
176 | ReqID: req.ReqID,
177 | Payload: b,
178 | })
179 | case SSH_FXP_FSTAT:
180 | handle := byteToStr(req.Payload)
181 | b, err := sftp.readStat(handle)
182 | if err != nil {
183 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
184 | continue
185 | }
186 | sftp.sendReply(sftp.conn, sftpMsg{
187 | ReqID: req.ReqID,
188 | Type: SSH_FXP_ATTRS,
189 | Payload: b,
190 | })
191 | case SSH_FXP_OPEN:
192 | fileName := byteToStr(req.Payload)
193 | pos := len(fileName) + 4
194 | pFlags := binary.BigEndian.Uint32(req.Payload[pos:])
195 | pos += 4
196 | handle, err := sftp.openFile(fileName, FileFlag(pFlags), req.Payload[pos:])
197 | if err != nil {
198 | log.WithError(err).Error("Cannot create handle")
199 | if err == os.ErrNotExist {
200 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_NO_SUCH_FILE))
201 | } else {
202 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
203 | }
204 | continue
205 | }
206 | b := make([]byte, 4+len(handle))
207 | strToByte(b, handle)
208 | sftp.sendReply(sftp.conn, sftpMsg{
209 | ReqID: req.ReqID,
210 | Type: SSH_FXP_HANDLE,
211 | Payload: b,
212 | })
213 | case SSH_FXP_READ:
214 | handle := byteToStr(req.Payload)
215 | pos := len(handle) + 4
216 | offset := binary.BigEndian.Uint64(req.Payload[pos:])
217 | pos += 8
218 | dataLen := int(binary.BigEndian.Uint32(req.Payload[pos:]))
219 | b, err := sftp.ReadFile(handle, int64(offset), dataLen)
220 | if err == io.EOF {
221 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_EOF))
222 | continue
223 | } else if err != nil {
224 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
225 | continue
226 | }
227 | bLen := make([]byte, 4)
228 | binary.BigEndian.PutUint32(bLen, uint32(len(b)))
229 | sftp.sendReply(sftp.conn, sftpMsg{
230 | ReqID: req.ReqID,
231 | Type: SSH_FXP_DATA,
232 | Payload: append(bLen, b...),
233 | })
234 | case SSH_FXP_WRITE:
235 | handle := byteToStr(req.Payload)
236 | pos := len(handle) + 4
237 | offset := binary.BigEndian.Uint64(req.Payload[pos:])
238 | pos += 8
239 | dataLen := int(binary.BigEndian.Uint32(req.Payload[pos:]))
240 | pos += 4
241 | // Limit total file size
242 | if limit := uint64(viper.GetSizeInBytes("server.receiveFileSizeLimit")); limit > 0 && offset+uint64(dataLen) > limit {
243 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
244 | continue
245 | }
246 | err := sftp.writeFile(handle, req.Payload[pos:pos+dataLen], int64(offset))
247 | if err != nil {
248 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
249 | continue
250 | }
251 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_OK))
252 | case SSH_FXP_MKDIR:
253 | path := byteToStr(req.Payload)
254 | err := sftp.Mkdir(path, req.Payload[len(path):])
255 | if err != nil {
256 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_FAILURE))
257 | continue
258 | }
259 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_OK))
260 | case SSH_FXP_RMDIR, SSH_FXP_SETSTAT, SSH_FXP_REMOVE, SSH_FXP_RENAME:
261 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_OK))
262 | default:
263 | sftp.sendReply(sftp.conn, createStatusMsg(req.ReqID, SSH_FX_BAD_MESSAGE))
264 | }
265 |
266 | }
267 | }
268 |
269 | func (sftp *Sftp) Open(path string) (string, error) {
270 | sftp.lock.Lock()
271 | defer sftp.lock.Unlock()
272 | file, err := sftp.vfs.Open(path)
273 |
274 | if err != nil {
275 | return "", err
276 | }
277 | hnd := sftp.nextHandle
278 | sftp.fileHandleMap[hnd] = file
279 | sftp.nextHandle++
280 | sftp.log.WithField("path", file.Name()).Infof("Reading directory")
281 | return strconv.Itoa(hnd), nil
282 | }
283 |
284 | func (sftp *Sftp) close(hnd string) error {
285 | sftp.lock.Lock()
286 | defer sftp.lock.Unlock()
287 | hndInt, err := strconv.Atoi(hnd)
288 | if err != nil {
289 | return err
290 | }
291 | file, exists := sftp.fileHandleMap[hndInt]
292 | if !exists {
293 | return os.ErrNotExist
294 | }
295 | err = file.Close()
296 | if err != nil {
297 | return err
298 | }
299 |
300 | delete(sftp.fileHandleMap, hndInt)
301 | delete(sftp.dirCache, hndInt)
302 | delete(sftp.fileEOFCache, hndInt)
303 | return nil
304 | }
305 | func (sftp *Sftp) readDir(hnd string) ([]byte, error) {
306 | sftp.lock.RLock()
307 | defer sftp.lock.RUnlock()
308 | hndInt, err := strconv.Atoi(hnd)
309 | if err != nil {
310 | return nil, err
311 | }
312 | file, exists := sftp.fileHandleMap[hndInt]
313 | if !exists {
314 | return nil, os.ErrNotExist
315 | }
316 | // TODO Do pagination here till afero officially supports it
317 | if sftp.dirCache == nil {
318 | sftp.dirCache = make(map[int]*dirContent)
319 | }
320 | dir, exists := sftp.dirCache[hndInt]
321 | if !exists {
322 | fi, err := file.Readdir(-1)
323 | if err != nil {
324 | return nil, err
325 | }
326 | dir = &dirContent{0, fi}
327 | sftp.dirCache[hndInt] = dir
328 | }
329 | if dir.offset > len(sftp.dirCache[hndInt].fi) {
330 | return nil, io.EOF
331 | }
332 | bound := dir.offset + entriesPerFetch
333 | defer func(b int) { dir.offset = b }(bound)
334 | if bound > len(sftp.dirCache[hndInt].fi) {
335 | bound = len(sftp.dirCache[hndInt].fi)
336 | }
337 | return createNamePacket(nil, sftp.dirCache[hndInt].fi[dir.offset:bound])
338 | }
339 |
340 | func (sftp *Sftp) readStat(hnd string) ([]byte, error) {
341 | sftp.lock.RLock()
342 | defer sftp.lock.RUnlock()
343 | hndInt, err := strconv.Atoi(hnd)
344 | if err != nil {
345 | return nil, err
346 | }
347 | file, exists := sftp.fileHandleMap[hndInt]
348 | if !exists {
349 | return nil, os.ErrNotExist
350 | }
351 | fi, err := file.Stat()
352 | if err != nil {
353 | return nil, err
354 | }
355 | b := make([]byte, 32)
356 | fileAttrToByte(b, fi)
357 | return b, nil
358 | }
359 |
360 | func readRequest(r io.Reader) (sftpMsg, error) {
361 | b := make([]byte, 4)
362 | if size, err := io.ReadFull(r, b); err != nil || size < 4 {
363 | return sftpMsg{}, err
364 | }
365 | l := binary.BigEndian.Uint32(b)
366 | b = make([]byte, l)
367 | if _, err := io.ReadFull(r, b); err != nil {
368 | return sftpMsg{}, err
369 | }
370 | rplyMsg := sftpMsg{
371 | Type: PacketType(b[0]),
372 | }
373 | if PacketType(b[0]) == SSH_FXP_INIT {
374 | rplyMsg.Payload = b[1:]
375 | } else {
376 | rplyMsg.ReqID = binary.BigEndian.Uint32(b[1:])
377 | rplyMsg.Payload = b[5:]
378 | }
379 | return rplyMsg, nil
380 | }
381 |
382 | func (sftp *Sftp) sendReply(w io.Writer, reply sftpMsg) {
383 | payloadLen := uint32(len(reply.Payload) + 1)
384 | if reply.ReqID > 0 {
385 | payloadLen += 4
386 | }
387 | b := make([]byte, payloadLen+4)
388 | binary.BigEndian.PutUint32(b, payloadLen)
389 | b[4] = byte(reply.Type)
390 | if reply.ReqID > 0 {
391 | binary.BigEndian.PutUint32(b[5:], reply.ReqID)
392 | copy(b[9:], reply.Payload)
393 | } else {
394 | copy(b[5:], reply.Payload)
395 | }
396 | sftp.log.Debugf("Reply:%v Seq:%v Payload(Len:%v):%v", reply.Type, reply.ReqID, len(reply.Payload), reply.Payload)
397 | w.Write(b)
398 | }
399 |
400 | func getLsString(fi os.FileInfo) string {
401 | uid, gid, _, _ := virtualfs.GetExtraInfo(fi)
402 | uName := honeyos.GetUserByID(uid).Name
403 | gName := honeyos.GetGroupByID(gid).Name
404 |
405 | size := fi.Size()
406 | if fi.IsDir() {
407 | size = 4096
408 | }
409 | return fmt.Sprintf("%v 1 %-8s %-8s %8d %v %v", fi.Mode(), uName, gName,
410 | size, fi.ModTime().Format("Jan 02 15:04"), fi.Name())
411 | }
412 |
413 | func (sftp *Sftp) openFile(file string, flag FileFlag, attr []byte) (string, error) {
414 | sftp.lock.Lock()
415 | defer sftp.lock.Unlock()
416 |
417 | intflag := 0
418 | if flag&SSH_FXF_CREAT != 0 {
419 | intflag |= os.O_CREATE
420 | }
421 | if flag&SSH_FXF_APPEND != 0 {
422 | intflag |= os.O_APPEND
423 | }
424 | if flag&SSH_FXF_READ != 0 && flag&SSH_FXF_WRITE != 0 {
425 | intflag |= os.O_RDWR
426 | } else if flag&SSH_FXF_READ != 0 {
427 | intflag |= os.O_RDONLY
428 | } else if flag&SSH_FXF_WRITE != 0 {
429 | intflag |= os.O_WRONLY
430 | }
431 | if flag&SSH_FXF_TRUNC != 0 {
432 | intflag |= os.O_TRUNC
433 | }
434 | var f afero.File
435 | var err error
436 | if flag&SSH_FXF_CREAT != 0 {
437 | f, err = sftp.vfs.Create(file)
438 | } else {
439 | f, err = sftp.vfs.OpenFile(file, intflag, byteToFileMode(attr))
440 | }
441 | if err != nil {
442 | return "", err
443 | }
444 | hnd := sftp.nextHandle
445 | sftp.fileHandleMap[hnd] = f
446 | sftp.nextHandle++
447 | return strconv.Itoa(hnd), nil
448 | }
449 |
450 | func (sftp *Sftp) ReadFile(handle string, offset int64, n int) ([]byte, error) {
451 | hnd, err := strconv.Atoi(handle)
452 | if err != nil {
453 | return nil, err
454 | }
455 | sftp.lock.RLock()
456 | defer sftp.lock.RUnlock()
457 | if sftp.fileEOFCache[hnd] {
458 | return nil, io.EOF
459 | }
460 | fp, exists := sftp.fileHandleMap[hnd]
461 | if !exists {
462 | return nil, os.ErrNotExist
463 | }
464 | sftp.log.WithField("path", fp.Name()).Info("Reading file")
465 | b := make([]byte, n)
466 | bRead, err := fp.ReadAt(b, offset)
467 | if err == io.EOF {
468 | sftp.fileEOFCache[hnd] = true
469 | } else if err != nil {
470 | return nil, err
471 | }
472 | return b[:bRead], nil
473 | }
474 |
475 | func (sftp *Sftp) writeFile(handle string, b []byte, offset int64) error {
476 | hnd, err := strconv.Atoi(handle)
477 | if err != nil {
478 | return err
479 | }
480 | sftp.lock.RLock()
481 | defer sftp.lock.RUnlock()
482 | fp, exists := sftp.fileHandleMap[hnd]
483 | if !exists {
484 | return os.ErrNotExist
485 | }
486 | sftp.log.WithField("path", fp.Name()).Info("Writing file")
487 | n, err := fp.WriteAt(b, offset)
488 | if len(b) != n || err != nil {
489 | return err
490 | }
491 | return nil
492 | }
493 |
494 | func (sftp *Sftp) closeFile(handle string) error {
495 | hnd, err := strconv.Atoi(handle)
496 | if err != nil {
497 | return err
498 | }
499 | sftp.lock.Lock()
500 | defer sftp.lock.Unlock()
501 | fp, exists := sftp.fileHandleMap[hnd]
502 | if !exists {
503 | return os.ErrNotExist
504 | }
505 | sftp.log.WithField("path", fp.Name()).Info("Closing file")
506 | err = fp.Close()
507 | if err != nil {
508 | return err
509 | }
510 | delete(sftp.fileHandleMap, hnd)
511 | return nil
512 | }
513 |
514 | func (sftp *Sftp) Mkdir(path string, attr []byte) error {
515 | sftp.log.WithField("path", path).Infof("Creating directory with permission %v", byteToFileMode(attr))
516 | return sftp.vfs.Mkdir(path, 0755)
517 | }
518 |
519 | func (sftp *Sftp) cleanUp() {
520 | if len(sftp.fileHandleMap) > 0 {
521 | for _, file := range sftp.fileHandleMap {
522 | file.Close()
523 | }
524 | }
525 |
526 | if er := recover(); er != nil {
527 | log.Error("Recover from parsing error: ", er)
528 | }
529 | sftp.quit <- 1
530 | }
531 |
--------------------------------------------------------------------------------
/sftp/sftp_test.go:
--------------------------------------------------------------------------------
1 | package sftp
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "os"
8 |
9 | "github.com/mkishere/sshsyrup/virtualfs"
10 | )
11 |
12 | func TestStrToByte(t *testing.T) {
13 | b := make([]byte, 9)
14 | strToByte(b, "Hello")
15 | if bytes.Compare(b, []byte{0, 0, 0, 5, 72, 101, 108, 108, 111}) != 0 {
16 | t.Errorf("Result mismatch: %v", b)
17 | }
18 | }
19 |
20 | func TestNamePacket(t *testing.T) {
21 | vfs, err := virtualfs.NewVirtualFS("../filesystem.zip")
22 | if err != nil {
23 | t.Fatal(err)
24 | }
25 | fi, err := vfs.Stat("/home/mk")
26 | _, err = createNamePacket([]string{"/home/mk"}, []os.FileInfo{fi})
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/sftp/statuscode_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type StatusCode"; DO NOT EDIT.
2 |
3 | package sftp
4 |
5 | import "strconv"
6 |
7 | const _StatusCode_name = "SSH_FX_OKSSH_FX_EOFSSH_FX_NO_SUCH_FILESSH_FX_PERMISSION_DENIEDSSH_FX_FAILURESSH_FX_BAD_MESSAGESSH_FX_NO_CONNECTIONSSH_FX_CONNECTION_LOSTSSH_FX_OP_UNSUPPORTEDSSH_FX_INVALID_HANDLESSH_FX_NO_SUCH_PATHSSH_FX_FILE_ALREADY_EXISTSSSH_FX_WRITE_PROTECTSSH_FX_NO_MEDIASSH_FX_NO_SPACE_ON_FILESYSTEMSSH_FX_QUOTA_EXCEEDEDSSH_FX_UNKNOWN_PRINCIPALSSH_FX_LOCK_CONFLICTSSH_FX_DIR_NOT_EMPTYSSH_FX_NOT_A_DIRECTORYSSH_FX_INVALID_FILENAMESSH_FX_LINK_LOOPSSH_FX_CANNOT_DELETESSH_FX_INVALID_PARAMETERSSH_FX_FILE_IS_A_DIRECTORYSSH_FX_BYTE_RANGE_LOCK_CONFLICTSSH_FX_BYTE_RANGE_LOCK_REFUSEDSSH_FX_DELETE_PENDINGSSH_FX_FILE_CORRUPTSSH_FX_OWNER_INVALIDSSH_FX_GROUP_INVALIDSSH_FX_NO_MATCHING_BYTE_RANGE_LOCK"
8 |
9 | var _StatusCode_index = [...]uint16{0, 9, 19, 38, 62, 76, 94, 114, 136, 157, 178, 197, 223, 243, 258, 287, 308, 332, 352, 372, 394, 417, 433, 453, 477, 503, 534, 564, 585, 604, 624, 644, 678}
10 |
11 | func (i StatusCode) String() string {
12 | if i >= StatusCode(len(_StatusCode_index)-1) {
13 | return "StatusCode(" + strconv.FormatInt(int64(i), 10) + ")"
14 | }
15 | return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]]
16 | }
17 |
--------------------------------------------------------------------------------
/sftp/types.go:
--------------------------------------------------------------------------------
1 | package sftp
2 |
3 | import (
4 | "encoding/binary"
5 | "errors"
6 | "os"
7 | "syscall"
8 |
9 | "github.com/mkishere/sshsyrup/virtualfs"
10 | )
11 |
12 | type fxp_realpath struct {
13 | OrigPath string
14 | }
15 |
16 | type fxp_name []virtualfs.FileInfo
17 |
18 | type PacketType byte
19 |
20 | const (
21 | SSH_FXP_INIT PacketType = iota + 1
22 | SSH_FXP_VERSION
23 | SSH_FXP_OPEN
24 | SSH_FXP_CLOSE
25 | SSH_FXP_READ
26 | SSH_FXP_WRITE
27 | SSH_FXP_LSTAT
28 | SSH_FXP_FSTAT
29 | SSH_FXP_SETSTAT
30 | SSH_FXP_FSETSTAT
31 | SSH_FXP_OPENDIR
32 | SSH_FXP_READDIR
33 | SSH_FXP_REMOVE
34 | SSH_FXP_MKDIR
35 | SSH_FXP_RMDIR
36 | SSH_FXP_REALPATH
37 | SSH_FXP_STAT
38 | SSH_FXP_RENAME
39 | SSH_FXP_READLINK
40 | SSH_FXP_LINK
41 | SSH_FXP_BLOCK
42 | SSH_FXP_UNBLOCK
43 | )
44 | const (
45 | SSH_FXP_STATUS PacketType = iota + 101
46 | SSH_FXP_HANDLE
47 | SSH_FXP_DATA
48 | SSH_FXP_NAME
49 | SSH_FXP_ATTRS
50 | )
51 | const (
52 | SSH_FXP_EXTENDED PacketType = iota + 201
53 | SSH_FXP_EXTENDED_REPLY
54 | )
55 |
56 | type AttrFlag uint32
57 |
58 | const (
59 | SSH_FILEXFER_ATTR_SIZE AttrFlag = 1 << iota
60 | SSH_FILEXFER_ATTR_UIDGID
61 | SSH_FILEXFER_ATTR_PERMISSIONS
62 | SSH_FILEXFER_ATTR_ACMODTIME
63 | SSH_FILEXFER_ATTR_EXTENDED AttrFlag = 0x80000000
64 | )
65 |
66 | type FileFlag uint32
67 |
68 | const (
69 | SSH_FXF_READ FileFlag = 1 << iota
70 | SSH_FXF_WRITE
71 | SSH_FXF_APPEND
72 | SSH_FXF_CREAT
73 | SSH_FXF_TRUNC
74 | SSH_FXF_EXCL
75 | )
76 |
77 | type StatusCode uint32
78 |
79 | const (
80 | SSH_FX_OK StatusCode = iota
81 | SSH_FX_EOF
82 | SSH_FX_NO_SUCH_FILE
83 | SSH_FX_PERMISSION_DENIED
84 | SSH_FX_FAILURE
85 | SSH_FX_BAD_MESSAGE
86 | SSH_FX_NO_CONNECTION
87 | SSH_FX_CONNECTION_LOST
88 | SSH_FX_OP_UNSUPPORTED
89 | )
90 |
91 | func ToByte(data interface{}) (b []byte) {
92 | switch data.(type) {
93 | case fxp_name:
94 | fiArray := data.(fxp_name)
95 | b = make([]byte, 4)
96 | binary.BigEndian.PutUint32(b, uint32(len(fiArray)))
97 |
98 | }
99 | return
100 | }
101 |
102 | func strToByte(b []byte, s string) {
103 | _ = b[3+len(s)]
104 | binary.BigEndian.PutUint32(b, uint32(len(s)))
105 | copy(b[4:], []byte(s))
106 | }
107 |
108 | func byteToStr(b []byte) string {
109 | strLen := binary.BigEndian.Uint32(b)
110 | return string(b[4 : 4+strLen])
111 | }
112 |
113 | // fileAttrToByte writes a byte array of size 36 into b
114 | func fileAttrToByte(b []byte, fi os.FileInfo) {
115 | uid, gid, atime, mtime := virtualfs.GetExtraInfo(fi)
116 |
117 | _ = b[31]
118 | binary.BigEndian.PutUint32(b, uint32(SSH_FILEXFER_ATTR_SIZE|
119 | SSH_FILEXFER_ATTR_UIDGID|
120 | SSH_FILEXFER_ATTR_PERMISSIONS|
121 | SSH_FILEXFER_ATTR_ACMODTIME))
122 |
123 | if fi.IsDir() {
124 | binary.BigEndian.PutUint64(b[4:], 4096)
125 | } else {
126 | binary.BigEndian.PutUint64(b[4:], uint64(fi.Size()))
127 | }
128 | binary.BigEndian.PutUint32(b[12:], uint32(uid))
129 | binary.BigEndian.PutUint32(b[16:], uint32(gid))
130 | binary.BigEndian.PutUint32(b[20:], fileModeToBit(fi.Mode()))
131 | binary.BigEndian.PutUint32(b[24:], uint32(atime.Unix()))
132 | binary.BigEndian.PutUint32(b[28:], uint32(mtime.Unix()))
133 | }
134 |
135 | func byteToFileMode(b []byte) os.FileMode {
136 | flag := AttrFlag(binary.BigEndian.Uint32(b))
137 | var fileMode os.FileMode = 0000
138 | if flag&SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
139 |
140 | }
141 | return fileMode
142 | }
143 |
144 | func createInit() sftpMsg {
145 | payload := make([]byte, 94)
146 | payload[3] = 3
147 | strToByte(payload[4:], "posix-rename@openssh.com")
148 | strToByte(payload[32:], "1")
149 | strToByte(payload[37:], "statvfs@openssh.com")
150 | strToByte(payload[60:], "2")
151 | strToByte(payload[65:], "fstatvfs@openssh.com")
152 | strToByte(payload[89:], "2")
153 |
154 | return sftpMsg{
155 | Type: SSH_FXP_VERSION,
156 | Payload: payload,
157 | }
158 | }
159 |
160 | func createNamePacket(names []string, fileInfo []os.FileInfo) ([]byte, error) {
161 | if names == nil {
162 | names = make([]string, len(fileInfo))
163 | for i, fi := range fileInfo {
164 | names[i] = fi.Name()
165 | }
166 | } else if len(names) != len(fileInfo) {
167 | return nil, errors.New("name and fileinfo does not match")
168 | }
169 | b := make([]byte, 4)
170 | binary.BigEndian.PutUint32(b, uint32(len(names)))
171 | for i := range names {
172 | // Shortname: len(name) + 4
173 | // Longname: len(name) + 55 + 4
174 | // FileInfo: 32
175 | longName := getLsString(fileInfo[i])
176 | fileInfoB := make([]byte, 8+len(names[i])+len(longName)+32)
177 | strToByte(fileInfoB, names[i])
178 | //fmt.Printf("After short:%v\n", fileInfoB)
179 | strToByte(fileInfoB[4+len(names[i]):], longName)
180 | //fmt.Printf("After long:%v\n", fileInfoB)
181 | fileAttrToByte(fileInfoB[8+len(names[i])+len(longName):], fileInfo[i])
182 | //fmt.Printf("After attr:%v\n", fileInfoB)
183 | b = append(b, fileInfoB...)
184 | }
185 |
186 | return b, nil
187 | }
188 |
189 | func createStatusMsg(reqID uint32, statusCode StatusCode) sftpMsg {
190 | stsMsgs := []string{
191 | "Success", /* SSH_FX_OK */
192 | "End of file", /* SSH_FX_EOF */
193 | "No such file", /* SSH_FX_NO_SUCH_FILE */
194 | "Permission denied", /* SSH_FX_PERMISSION_DENIED */
195 | "Failure", /* SSH_FX_FAILURE */
196 | "Bad message", /* SSH_FX_BAD_MESSAGE */
197 | "No connection", /* SSH_FX_NO_CONNECTION */
198 | "Connection lost", /* SSH_FX_CONNECTION_LOST */
199 | "Operation unsupported",
200 | }
201 | stsMsg := stsMsgs[statusCode]
202 | strBuf := make([]byte, len(stsMsg)+8)
203 | binary.BigEndian.PutUint32(strBuf, uint32(statusCode))
204 | strToByte(strBuf[4:], stsMsg)
205 | msg := sftpMsg{
206 | Type: SSH_FXP_STATUS,
207 | ReqID: reqID,
208 | Payload: strBuf,
209 | }
210 | return msg
211 | }
212 |
213 | // fromFileMode converts from the os.FileMode specification to sftp filemode bits
214 | // Copied from https://github.com/pkg/sftp/
215 | func fileModeToBit(mode os.FileMode) uint32 {
216 | ret := uint32(0)
217 |
218 | if mode&os.ModeDevice != 0 {
219 | if mode&os.ModeCharDevice != 0 {
220 | ret |= syscall.S_IFCHR
221 | } else {
222 | ret |= syscall.S_IFBLK
223 | }
224 | }
225 | if mode&os.ModeDir != 0 {
226 | ret |= syscall.S_IFDIR
227 | }
228 | if mode&os.ModeSymlink != 0 {
229 | ret |= syscall.S_IFLNK
230 | }
231 | if mode&os.ModeNamedPipe != 0 {
232 | ret |= syscall.S_IFIFO
233 | }
234 | if mode&os.ModeSetgid != 0 {
235 | ret |= syscall.S_ISGID
236 | }
237 | if mode&os.ModeSetuid != 0 {
238 | ret |= syscall.S_ISUID
239 | }
240 | if mode&os.ModeSticky != 0 {
241 | ret |= syscall.S_ISVTX
242 | }
243 | if mode&os.ModeSocket != 0 {
244 | ret |= syscall.S_IFSOCK
245 | }
246 |
247 | if mode&os.ModeType == 0 {
248 | ret |= syscall.S_IFREG
249 | }
250 | ret |= uint32(mode & os.ModePerm)
251 |
252 | return ret
253 | }
254 |
--------------------------------------------------------------------------------
/ssh.go:
--------------------------------------------------------------------------------
1 | package sshsyrup
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/binary"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "math/rand"
11 | "net"
12 | "path"
13 | "strconv"
14 | "strings"
15 | "time"
16 |
17 | "github.com/spf13/viper"
18 |
19 | netconn "github.com/mkishere/sshsyrup/net"
20 | os "github.com/mkishere/sshsyrup/os"
21 | "github.com/mkishere/sshsyrup/os/command"
22 | "github.com/mkishere/sshsyrup/sftp"
23 | "github.com/mkishere/sshsyrup/util/abuseipdb"
24 | "github.com/mkishere/sshsyrup/util/termlogger"
25 | "github.com/mkishere/sshsyrup/virtualfs"
26 | log "github.com/sirupsen/logrus"
27 | "github.com/spf13/afero"
28 | "golang.org/x/crypto/ssh"
29 | )
30 |
31 | const (
32 | logTimeFormat string = "20060102"
33 | )
34 |
35 | // SSHSession stores SSH session info
36 | type SSHSession struct {
37 | user string
38 | src net.Addr
39 | clientVersion string
40 | sshChan <-chan ssh.NewChannel
41 | log *log.Entry
42 | sys *os.System
43 | term string
44 | fs afero.Fs
45 | }
46 |
47 | type envRequest struct {
48 | Name string
49 | Value string
50 | }
51 |
52 | type ptyRequest struct {
53 | Term string
54 | Width uint32
55 | Height uint32
56 | PWidth uint32
57 | PHeight uint32
58 | Modes string
59 | }
60 | type winChgRequest struct {
61 | Width uint32
62 | Height uint32
63 | }
64 |
65 | type tunnelRequest struct {
66 | RemoteHost string
67 | RemotePort uint32
68 | LocalHost string
69 | LocalPort uint32
70 | }
71 |
72 | type Server struct {
73 | sshCfg *ssh.ServerConfig
74 | vfs afero.Fs
75 | }
76 |
77 | var (
78 | ipConnCnt *netconn.IPConnCount = netconn.NewIPConnCount()
79 | )
80 |
81 | // NewSSHSession create new SSH connection based on existing socket connection
82 | func NewSSHSession(nConn net.Conn, sshConfig *ssh.ServerConfig, vfs afero.Fs) (*SSHSession, error) {
83 | conn, chans, reqs, err := ssh.NewServerConn(nConn, sshConfig)
84 | if err != nil {
85 | return nil, err
86 | }
87 | clientIP, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
88 | logger := log.WithFields(log.Fields{
89 | "user": conn.User(),
90 | "srcIP": clientIP,
91 | "port": port,
92 | "clientStr": string(conn.ClientVersion()),
93 | "sessionId": base64.StdEncoding.EncodeToString(conn.SessionID()),
94 | })
95 | logger.Infof("New SSH connection with client")
96 |
97 | go ssh.DiscardRequests(reqs)
98 | return &SSHSession{
99 | user: conn.User(),
100 | src: conn.RemoteAddr(),
101 | clientVersion: string(conn.ClientVersion()),
102 | sshChan: chans,
103 | log: logger,
104 | fs: vfs,
105 | }, nil
106 | }
107 |
108 | func (s *SSHSession) handleNewSession(newChan ssh.NewChannel) {
109 |
110 | channel, requests, err := newChan.Accept()
111 | if err != nil {
112 | s.log.WithError(err).Error("Could not accept channel")
113 | return
114 | }
115 | var sh *os.Shell
116 | go func(in <-chan *ssh.Request, channel ssh.Channel) {
117 | quitSignal := make(chan int, 1)
118 | for {
119 | select {
120 | case req := <-in:
121 | if req == nil {
122 | return
123 | }
124 | switch req.Type {
125 | case "winadj@putty.projects.tartarus.org", "simple@putty.projects.tartarus.org":
126 | //Do nothing here
127 | case "pty-req":
128 | // Of coz we are not going to create a PTY here as we are honeypot.
129 | // We are creating a pseudo-PTY
130 | var ptyreq ptyRequest
131 | if err := ssh.Unmarshal(req.Payload, &ptyreq); err != nil {
132 | s.log.WithField("reqType", req.Type).WithError(err).Errorln("Cannot parse user request payload")
133 | req.Reply(false, nil)
134 | } else {
135 | s.log.WithField("reqType", req.Type).Infof("User requesting pty(%v %vx%v)", ptyreq.Term, ptyreq.Width, ptyreq.Height)
136 |
137 | s.sys = os.NewSystem(s.user, viper.GetString("server.hostname"), s.fs, channel, int(ptyreq.Width), int(ptyreq.Height), s.log)
138 | s.term = ptyreq.Term
139 | req.Reply(true, nil)
140 | }
141 | case "env":
142 | var envReq envRequest
143 | if err := ssh.Unmarshal(req.Payload, &envReq); err != nil {
144 | req.Reply(false, nil)
145 | } else {
146 | s.log.WithFields(log.Fields{
147 | "reqType": req.Type,
148 | "envVarName": envReq.Name,
149 | "envVarValue": envReq.Value,
150 | }).Infof("User sends envvar:%v=%v", envReq.Name, envReq.Value)
151 | req.Reply(true, nil)
152 | }
153 | case "shell":
154 | s.log.WithField("reqType", req.Type).Info("User requesting shell access")
155 | if s.sys == nil {
156 | s.sys = os.NewSystem(s.user, viper.GetString("server.hostname"), s.fs, channel, 80, 24, s.log)
157 | }
158 |
159 | sh = os.NewShell(s.sys, s.src.String(), s.log.WithField("module", "shell"), quitSignal)
160 |
161 | // Create delay function if exists
162 | if viper.GetInt("server.processDelay") > 0 {
163 | sh.DelayFunc = func() {
164 | r := 500
165 | sleepTime := viper.GetInt("server.processDelay") - r + rand.Intn(2*r)
166 | time.Sleep(time.Millisecond * time.Duration(sleepTime))
167 | }
168 | }
169 | // Create hook for session logger (For recording session to UML/asciinema)
170 | var hook termlogger.LogHook
171 | if viper.GetString("server.sessionLogFmt") == "asciinema" {
172 | asciiLogParams := map[string]string{
173 | "TERM": s.term,
174 | "USER": s.user,
175 | "SRC": s.src.String(),
176 | }
177 | hook, err = termlogger.NewAsciinemaHook(s.sys.Width(), s.sys.Height(),
178 | viper.GetString("asciinema.apiEndpoint"), viper.GetString("asciinema.apiKey"), asciiLogParams,
179 | fmt.Sprintf("logs/sessions/%v-%v.cast", s.user, termlogger.LogTimeFormat))
180 |
181 | } else if viper.GetString("server.sessionLogFmt") == "uml" {
182 | hook, err = termlogger.NewUMLHook(0, fmt.Sprintf("logs/sessions/%v-%v.ulm.log", s.user, time.Now().Format(logTimeFormat)))
183 | } else {
184 | log.Errorf("Session Log option %v not recognized", viper.GetString("server.sessionLogFmt"))
185 | }
186 | if err != nil {
187 | log.Errorf("Cannot create %v log file", viper.GetString("server.sessionLogFmt"))
188 | }
189 | // The need of a goroutine here is that PuTTY will wait for reply before acknowledge it enters shell mode
190 | go sh.HandleRequest(hook)
191 | req.Reply(true, nil)
192 | case "subsystem":
193 | subsys := string(req.Payload[4:])
194 | s.log.WithFields(log.Fields{
195 | "reqType": req.Type,
196 | "subSystem": subsys,
197 | }).Infof("User requested subsystem %v", subsys)
198 | if subsys == "sftp" {
199 | sftpSrv := sftp.NewSftp(channel, s.fs,
200 | s.user, s.log.WithField("module", "sftp"), quitSignal)
201 | go sftpSrv.HandleRequest()
202 | req.Reply(true, nil)
203 | } else {
204 | req.Reply(false, nil)
205 | }
206 | case "window-change":
207 | s.log.WithField("reqType", req.Type).Info("User shell window size changed")
208 | if sh != nil {
209 | winChg := &winChgRequest{}
210 | if err := ssh.Unmarshal(req.Payload, winChg); err != nil {
211 | req.Reply(false, nil)
212 | }
213 | sh.SetSize(int(winChg.Width), int(winChg.Height))
214 | }
215 | case "exec":
216 | cmd := string(req.Payload[4:])
217 | s.log.WithFields(log.Fields{
218 | "reqType": req.Type,
219 | "cmd": cmd,
220 | }).Info("User request remote exec")
221 | args := strings.Split(cmd, " ")
222 | var sys *os.System
223 | if s.sys == nil {
224 | sys = os.NewSystem(s.user, viper.GetString("server.hostname"), s.fs, channel, 80, 24, s.log)
225 | } else {
226 | sys = s.sys
227 | }
228 | if strings.HasPrefix(args[0], "scp") {
229 | scp := command.NewSCP(channel, s.fs, s.log.WithField("module", "scp"))
230 | go scp.Main(args[1:], quitSignal)
231 | req.Reply(true, nil)
232 | continue
233 | }
234 | n, err := sys.Exec(args[0], args[1:])
235 | if err != nil {
236 | channel.Write([]byte(fmt.Sprintf("%v: command not found\r\n", cmd)))
237 | }
238 | quitSignal <- n
239 | req.Reply(true, nil)
240 | default:
241 | s.log.WithField("reqType", req.Type).Infof("Unknown channel request type %v", req.Type)
242 | }
243 | case ret := <-quitSignal:
244 | s.log.Info("User closing channel")
245 | defer closeChannel(channel, ret)
246 | return
247 | }
248 | }
249 | }(requests, channel)
250 | }
251 |
252 | func (s *SSHSession) handleNewConn() {
253 | // Service the incoming Channel channel.
254 | for newChannel := range s.sshChan {
255 | s.log.WithField("chanType", newChannel.ChannelType()).Info("User created new session channel")
256 | switch newChannel.ChannelType() {
257 | case "direct-tcpip", "forwarded-tcpip":
258 | var treq tunnelRequest
259 | err := ssh.Unmarshal(newChannel.ExtraData(), &treq)
260 | if err != nil {
261 | s.log.WithError(err).Error("Cannot unmarshal port forwarding data")
262 | newChannel.Reject(ssh.UnknownChannelType, "Corrupt payload")
263 | }
264 | s.log.WithFields(log.Fields{
265 | "remoteHost": treq.RemoteHost,
266 | "remotePort": treq.RemotePort,
267 | "localHost": treq.LocalHost,
268 | "localPort": treq.LocalPort,
269 | "chanType": newChannel.ChannelType(),
270 | }).Info("Trying to establish connection with port forwarding")
271 | if newChannel.ChannelType() == "forwarded-tcpip" {
272 | newChannel.Reject(ssh.Prohibited, "Port forwarding disabled")
273 | continue
274 | }
275 | var host string
276 | switch viper.GetString("server.portRedirection") {
277 | case "disable":
278 | newChannel.Reject(ssh.Prohibited, "Port forwarding disabled")
279 | continue
280 | case "map":
281 | portMap := viper.GetStringMap("server.portRedirectionMap")
282 | host = portMap[strconv.Itoa(int(treq.RemotePort))].(string)
283 | case "direct":
284 | host = fmt.Sprintf("%v:%v", treq.RemoteHost, treq.RemotePort)
285 | }
286 | if len(host) > 0 {
287 | ch, req, err := newChannel.Accept()
288 | if err != nil {
289 | newChannel.Reject(ssh.ResourceShortage, "Cannot create new channel")
290 | }
291 | go ssh.DiscardRequests(req)
292 | go func() {
293 | s.log.WithFields(log.Fields{
294 | "host": host,
295 | }).Infoln("Creating connection to remote server")
296 | conn, err := net.Dial("tcp", host)
297 | if err != nil {
298 | s.log.WithFields(log.Fields{
299 | "host": host,
300 | }).WithError(err).Error("Cannot create connection")
301 | newChannel.Reject(ssh.ConnectionFailed, "Cannot establish connection")
302 | return
303 | }
304 | go io.Copy(conn, ch)
305 | go io.Copy(ch, conn)
306 | }()
307 | } else {
308 | newChannel.Reject(ssh.ConnectionFailed, "Malformed channel request")
309 | }
310 | case "session":
311 | go s.handleNewSession(newChannel)
312 | default:
313 | newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
314 | s.log.WithField("chanType", newChannel.ChannelType()).Infof("Unknown channel type %v", newChannel.ChannelType())
315 | continue
316 | }
317 | }
318 | }
319 |
320 | func CreateSessionHandler(c <-chan net.Conn, sshConfig *ssh.ServerConfig, vfs afero.Fs) {
321 | for conn := range c {
322 | sshConfig.PasswordCallback = PasswordChallenge(viper.GetInt("server.maxTries"))
323 | sshSession, err := NewSSHSession(conn, sshConfig, vfs)
324 | clientIP, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
325 | abuseipdb.CreateProfile(clientIP)
326 | abuseipdb.AddCategory(clientIP, abuseipdb.SSH, abuseipdb.Hacking)
327 | if err != nil {
328 | log.WithFields(log.Fields{
329 | "srcIP": clientIP,
330 | "port": port,
331 | }).WithError(err).Error("Error establishing SSH connection")
332 | } else {
333 | sshSession.handleNewConn()
334 | }
335 | //conn.Close()
336 | ipConnCnt.DecCount(clientIP)
337 | abuseipdb.UploadReport(clientIP)
338 | }
339 | }
340 |
341 | func closeChannel(ch ssh.Channel, signal int) {
342 | b := make([]byte, 4)
343 | binary.BigEndian.PutUint32(b, uint32(signal))
344 | ch.SendRequest("exit-status", false, b)
345 | ch.Close()
346 | }
347 |
348 | func NewServer(configPath string, hostKey []byte) (s Server) {
349 | // Read banner
350 | bannerFile, err := ioutil.ReadFile(path.Join(configPath, viper.GetString("server.banner")))
351 | if err != nil {
352 | bannerFile = []byte{}
353 | }
354 |
355 | // Initalize VFS
356 | backupFS := afero.NewBasePathFs(afero.NewOsFs(), viper.GetString("virtualfs.savedFileDir"))
357 | zipfs, err := virtualfs.NewVirtualFS(path.Join(configPath, viper.GetString("virtualfs.imageFile")))
358 | if err != nil {
359 | log.Error("Cannot create virtual filesystem")
360 | }
361 | vfs := afero.NewCopyOnWriteFs(zipfs, backupFS)
362 | err = os.LoadUsers(path.Join(configPath, viper.GetString("virtualfs.uidMappingFile")))
363 | if err != nil {
364 | log.Errorf("Cannot load user mapping file %v", path.Join(configPath, viper.GetString("virtualfs.uidMappingFile")))
365 | }
366 |
367 | s = Server{
368 | &ssh.ServerConfig{
369 | PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
370 | clientIP, port, _ := net.SplitHostPort(c.RemoteAddr().String())
371 | log.WithFields(log.Fields{
372 | "user": c.User(),
373 | "srcIP": clientIP,
374 | "port": port,
375 | "pubKeyType": key.Type(),
376 | "pubKeyFingerprint": base64.StdEncoding.EncodeToString(key.Marshal()),
377 | "authMethod": "publickey",
378 | }).Info("User trying to login with key")
379 | return nil, errors.New("Key rejected, revert to password login")
380 | },
381 |
382 | ServerVersion: viper.GetString("server.ident"),
383 | MaxAuthTries: viper.GetInt("server.maxTries"),
384 | BannerCallback: func(c ssh.ConnMetadata) string {
385 |
386 | return string(bannerFile)
387 | },
388 | },
389 | vfs,
390 | }
391 | private, err := ssh.ParsePrivateKey(hostKey)
392 | if err != nil {
393 | log.WithError(err).Fatal("Failed to parse private key")
394 | }
395 | s.sshCfg.AddHostKey(private)
396 |
397 | return s
398 | }
399 |
400 | func PasswordChallenge(tries int) func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
401 | triesLeft := tries
402 | return func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
403 | clientIP, port, _ := net.SplitHostPort(c.RemoteAddr().String())
404 | log.WithFields(log.Fields{
405 | "user": c.User(),
406 | "srcIP": clientIP,
407 | "port": port,
408 | "authMethod": "password",
409 | "password": string(pass),
410 | }).Info("User trying to login with password")
411 |
412 | successPerm := &ssh.Permissions{
413 | Extensions: map[string]string{
414 | "permit-agent-forwarding": "yes",
415 | },
416 | }
417 | stpass, userExists := os.IsUserExist(c.User())
418 | if userExists && stpass == string(pass) {
419 | // Password match
420 | return successPerm, nil
421 | } else if userExists && (stpass != string(pass) || stpass == "*") || viper.GetBool("server.allowRandomUser") {
422 | if viper.GetBool("server.allowRetryLogin") {
423 | if triesLeft == 1 {
424 | return successPerm, nil
425 | }
426 | triesLeft--
427 | } else {
428 | return successPerm, nil
429 | }
430 | }
431 | time.Sleep(viper.GetDuration("server.retryDelay"))
432 | return nil, fmt.Errorf("password rejected for %q", c.User())
433 | }
434 | }
435 |
436 | func (sc Server) ListenAndServe() {
437 | connChan := make(chan net.Conn)
438 | // Create pool of workers to handle connections
439 | for i := 0; i < viper.GetInt("server.maxConnections"); i++ {
440 | go CreateSessionHandler(connChan, sc.sshCfg, sc.vfs)
441 | }
442 |
443 | listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", viper.GetString("server.addr"), viper.GetInt("server.port")))
444 | if err != nil {
445 | log.WithError(err).Fatal("Could not create listening socket")
446 | }
447 | defer listener.Close()
448 |
449 | for {
450 | nConn, err := listener.Accept()
451 | host, port, _ := net.SplitHostPort(nConn.RemoteAddr().String())
452 | log.WithFields(log.Fields{
453 | "srcIP": host,
454 | "port": port,
455 | }).Info("Connection established")
456 | if err != nil {
457 | log.WithError(err).Error("Failed to accept incoming connection")
458 | continue
459 | }
460 | cnt := ipConnCnt.Read(host)
461 | if cnt >= viper.GetInt("server.maxConnPerHost") {
462 | nConn.Close()
463 | continue
464 | } else {
465 | ipConnCnt.IncCount(host)
466 | }
467 | tConn := netconn.NewThrottledConnection(nConn, viper.GetInt64("server.speed"), viper.GetDuration("server.timeout"))
468 | connChan <- tConn
469 | }
470 | }
471 |
--------------------------------------------------------------------------------
/util/abuseipdb/report.go:
--------------------------------------------------------------------------------
1 | package abuseipdb
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "errors"
7 | "fmt"
8 | "io/ioutil"
9 | "net/http"
10 | "os"
11 | "strings"
12 |
13 | "github.com/spf13/viper"
14 | )
15 |
16 | const (
17 | endPoint = "https://www.abuseipdb.com"
18 | )
19 |
20 | var (
21 | reportMap = make(map[string]*Profile)
22 | )
23 |
24 | type Category int
25 |
26 | const (
27 | FraudOrder Category = iota + 3
28 | DDosAttack
29 | FTPBruteForce
30 | PingOfDeath
31 | Phishing
32 | FraudVoIP
33 | OpenProxy
34 | WebSpam
35 | EmailSpam
36 | BlogSpam
37 | VPNIP
38 | PortScan
39 | Hacking
40 | SQLInjection
41 | Spoofing
42 | BruteForce
43 | BadWebBot
44 | ExploitedHost
45 | WebAppAttack
46 | SSH
47 | IoTTargeted
48 | )
49 |
50 | type Profile struct {
51 | IP string
52 | cat map[Category]struct{}
53 | comment bytes.Buffer
54 | }
55 |
56 | func CreateProfile(ip string) {
57 | reportMap[ip] = createProfile(ip)
58 | }
59 |
60 | func AddCategory(ip string, cat ...Category) {
61 | reportMap[ip].AddCategory(cat)
62 | }
63 |
64 | func UploadReport(ip string) {
65 | profile := reportMap[ip]
66 | profile.Report()
67 | }
68 |
69 | // ReportIP report to AbuseIPDB regarding IP activities
70 | func reportIP(ip, reason string, cat []int) error {
71 | apikey := viper.GetString("abuseIPDB.apiKey")
72 | if len(apikey) == 0 {
73 | return errors.New("API Key empty")
74 | }
75 | arrToStr := func(arr []int) string {
76 | return strings.Trim(strings.Replace(fmt.Sprint(arr), " ", ",", -1), "[]")
77 | }
78 | url := fmt.Sprintf("%v/report/json?key=%v&category=%v&comment=%v&ip=%v", endPoint, apikey, arrToStr(cat), reason, ip)
79 | fmt.Println(url)
80 | rsp, err := http.Get(url)
81 | if err != nil {
82 | return err
83 | }
84 | defer rsp.Body.Close()
85 | body, err := ioutil.ReadAll(rsp.Body)
86 | if err != nil {
87 | return err
88 | }
89 | if !strings.Contains(string(body), "\"success\":true") {
90 | return errors.New(string(body))
91 | }
92 | return nil
93 | }
94 |
95 | func createProfile(ip string) *Profile {
96 | return &Profile{
97 | IP: ip,
98 | cat: map[Category]struct{}{SSH: struct{}{}},
99 | }
100 | }
101 |
102 | func (p *Profile) CheckCommand(cmd string) {
103 | // Extract URL the string is trying to get
104 | switch {
105 | case strings.Contains(cmd, "wget"), strings.Contains(cmd, "curl"):
106 | p.cat[20] = struct{}{}
107 | p.comment.WriteString("Attempt to download malicious scripts; ")
108 | }
109 | }
110 |
111 | func (p *Profile) AddCategory(cat []Category) {
112 | for _, c := range cat {
113 | p.cat[c] = struct{}{}
114 | }
115 | }
116 |
117 | func (p *Profile) AddReason(reason string) {
118 | p.comment.WriteString(reason)
119 | }
120 |
121 | func (p *Profile) Report() error {
122 | var catArr []int
123 | for cat := range p.cat {
124 | catArr = append(catArr, int(cat))
125 | }
126 | return reportIP(p.IP, p.comment.String(), catArr)
127 | }
128 |
129 | // LoadRules load report rules file into memory
130 | func LoadRules(ruleFilePath string) error {
131 | fp, err := os.Open(ruleFilePath)
132 | if err != nil {
133 | return err
134 | }
135 | sc := bufio.NewScanner(fp)
136 | for sc.Scan() {
137 | line := sc.Text()
138 | if strings.HasPrefix(line, "#") {
139 | continue
140 | }
141 |
142 | }
143 | return nil
144 | }
145 |
--------------------------------------------------------------------------------
/util/elastichook.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net/http"
7 | "strings"
8 | "time"
9 |
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | type ElasticHook struct {
14 | url string
15 | formatter log.JSONFormatter
16 | }
17 |
18 | type elasticRes struct {
19 | Result string `json:"result"`
20 | }
21 |
22 | func NewElasticHook(endPt, index, pipeline string) log.Hook {
23 | if strings.LastIndex(endPt, "/") != len(endPt)-1 {
24 | endPt += "/"
25 | }
26 |
27 | url := endPt + index + "/_doc/"
28 | if len(pipeline) > 0 {
29 | url += "?pipeline=" + pipeline
30 | }
31 | return &ElasticHook{
32 | url: url,
33 | formatter: log.JSONFormatter{},
34 | }
35 | }
36 |
37 | func (eh *ElasticHook) Fire(entry *log.Entry) error {
38 | b, err := eh.formatter.Format(entry)
39 | if err != nil {
40 | return nil
41 | }
42 |
43 | req, _ := http.NewRequest("POST", eh.url, bytes.NewBuffer(b))
44 | req.Header.Set("Content-Type", "application/json")
45 | req.Header.Add("User-Agent", "SyrupSSH/1.0.0")
46 | htClient := &http.Client{
47 | Timeout: time.Second * 10,
48 | }
49 | rsp, err := htClient.Do(req)
50 | body := &bytes.Buffer{}
51 | _, err = body.ReadFrom(rsp.Body)
52 | if err != nil {
53 | return err
54 | }
55 | rsp.Body.Close()
56 | res := elasticRes{}
57 | json.Unmarshal(body.Bytes(), &res)
58 | return nil
59 | }
60 |
61 | func (eh *ElasticHook) Levels() []log.Level {
62 | return []log.Level{log.InfoLevel}
63 | }
64 |
--------------------------------------------------------------------------------
/util/termlogger/asciicast.go:
--------------------------------------------------------------------------------
1 | package termlogger
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "mime/multipart"
10 | "net/http"
11 | "os"
12 | "time"
13 |
14 | log "github.com/sirupsen/logrus"
15 | )
16 |
17 | type asciiCast struct {
18 | Version int `json:"version"`
19 | Width int `json:"width"`
20 | Height int `json:"height"`
21 | Timestamp int64 `json:"timestamp"`
22 | Command string `json:"command"`
23 | Title string `json:"title"`
24 | Env map[string]string `json:"env"`
25 | }
26 |
27 | type AsciinemaHook struct {
28 | data asciiCast
29 | fileName string
30 | createTime time.Time
31 | userName string
32 | apikey string
33 | apiEndpoint string
34 | elapse time.Duration
35 | htClient *http.Client
36 | }
37 |
38 | const (
39 | LogTimeFormat string = "20060102-150405"
40 | input = "i"
41 | output = "o"
42 | )
43 |
44 | // NewAsciinemaHook creates a new Asciinema hook
45 | func NewAsciinemaHook(width, height int, apiEndPt, apiKey string, params map[string]string, fileName string) (LogHook, error) {
46 | now := time.Now()
47 | header := asciiCast{
48 | Version: 2,
49 | Width: width,
50 | Height: height,
51 | Timestamp: now.Unix(),
52 | Title: fmt.Sprintf("%v@%v - %v", params["USER"], params["SRC"], now.Format(LogTimeFormat)),
53 | Env: map[string]string{
54 | "TERM": "vt100",
55 | "SHELL": "/bin/sh",
56 | },
57 | }
58 | for k, v := range params {
59 | header.Env[k] = v
60 | }
61 | aLog := &AsciinemaHook{
62 | data: header,
63 | createTime: now,
64 | apikey: apiKey,
65 | apiEndpoint: apiEndPt,
66 | userName: "syrupSSH",
67 | }
68 | aLog.fileName = aLog.createTime.Format(fileName)
69 | if len(aLog.apikey) > 0 {
70 | aLog.htClient = &http.Client{
71 | Timeout: time.Second * 10,
72 | }
73 | }
74 | b, err := json.Marshal(aLog.data)
75 | if err != nil {
76 | log.WithField("data", aLog.data).WithError(err).Errorf("Error when marshalling log data")
77 | return nil, err
78 | }
79 | b = append(b, '\r', '\n')
80 | if err = ioutil.WriteFile(aLog.fileName, b, 0600); err != nil {
81 | log.WithField("path", aLog.fileName).WithError(err).Errorf("Error when writing log file")
82 | return nil, err
83 | }
84 |
85 | return aLog, nil
86 | }
87 |
88 | func (aLog *AsciinemaHook) Fire(entry *log.Entry) error {
89 | file, err := os.OpenFile(aLog.fileName, os.O_APPEND|os.O_WRONLY, 0666)
90 | defer file.Close()
91 | if err != nil {
92 | return err
93 | }
94 | diff := entry.Time.Sub(aLog.createTime)
95 | if escStr, err := json.Marshal(entry.Message); err == nil {
96 | file.WriteString(fmt.Sprintf("[%f, \"%v\", %v]\r\n", diff.Seconds(), entry.Data["dir"], string(escStr)))
97 | } else {
98 | return err
99 | }
100 | return nil
101 | }
102 |
103 | // Upload the written file to asciinema server
104 | func (aLog *AsciinemaHook) upload() (string, error) {
105 | file, err := os.Open(aLog.fileName)
106 | defer file.Close()
107 | if err != nil {
108 | return "", err
109 | }
110 | buf := &bytes.Buffer{}
111 | writer := multipart.NewWriter(buf)
112 | filePart, err := writer.CreateFormFile("asciicast", "ascii.cast")
113 | _, err = io.Copy(filePart, file)
114 | writer.Close()
115 | req, _ := http.NewRequest("POST", aLog.apiEndpoint+"/api/asciicasts", buf)
116 | req.SetBasicAuth(aLog.userName, aLog.apikey)
117 | req.Header.Set("Content-Type", writer.FormDataContentType())
118 | req.Header.Add("User-Agent", "SyrupSSH/1.0.0")
119 | rsp, err := aLog.htClient.Do(req)
120 | if err != nil {
121 | return "", err
122 | }
123 | body := &bytes.Buffer{}
124 | _, err = body.ReadFrom(rsp.Body)
125 | if err != nil {
126 | return "", err
127 | }
128 | rsp.Body.Close()
129 | return string(body.Bytes()), err
130 | }
131 |
132 | // Close the STDOut keystroke channel for logging
133 | func (aLog *AsciinemaHook) Close() error {
134 | log.Debug("ASCIICastLog.Close() called")
135 | aLog.elapse = time.Since(aLog.createTime)
136 | // Upload cast to asciinema.org if key is filled and elapsed time > 5 seconds
137 | if len(aLog.apikey) > 0 && aLog.elapse > time.Second*5 {
138 | url, err := aLog.upload()
139 | if err != nil {
140 | log.WithError(err).Error("Log failed to upload")
141 | return err
142 | }
143 | log.WithField("url", url).Info("Log uploaded to URL")
144 | }
145 | return nil
146 | }
147 |
148 | func (aLog *AsciinemaHook) Levels() []log.Level {
149 | return []log.Level{log.InfoLevel}
150 | }
151 |
--------------------------------------------------------------------------------
/util/termlogger/logger.go:
--------------------------------------------------------------------------------
1 | package termlogger
2 |
3 | import (
4 | "io"
5 |
6 | log "github.com/sirupsen/logrus"
7 | )
8 |
9 | // ioLogWrapper logs terminal keystrokes
10 | type ioLogWrapper struct {
11 | keylog *log.Logger
12 | hook LogHook
13 | in io.Reader
14 | out, err io.Writer
15 | }
16 |
17 | type StdIOErr interface {
18 | In() io.Reader
19 | Out() io.Writer
20 | Err() io.Writer
21 | Close() error
22 | }
23 |
24 | type logWriter struct {
25 | *log.Entry
26 | }
27 |
28 | func (lw logWriter) Write(p []byte) (int, error) {
29 | lw.Info(string(p))
30 | return len(p), nil
31 | }
32 |
33 | // NewLogger creates a Logger instance
34 | func NewLogger(logHook LogHook, in io.Reader, out, err io.Writer) StdIOErr {
35 | tl := &ioLogWrapper{
36 | keylog: log.New(),
37 | hook: logHook,
38 | }
39 | tl.keylog.SetLevel(log.InfoLevel)
40 | tl.keylog.Out = DummyWriter{}
41 | tl.keylog.AddHook(logHook)
42 | inLogStream := logWriter{tl.keylog.WithField("dir", input)}
43 | outLogStream := logWriter{tl.keylog.WithField("dir", output)}
44 | tl.in = io.TeeReader(in, inLogStream)
45 | tl.out = io.MultiWriter(out, outLogStream)
46 | tl.err = io.MultiWriter(err, outLogStream)
47 | return tl
48 | }
49 |
50 | func (tl *ioLogWrapper) In() io.Reader {
51 | return tl.in
52 | }
53 |
54 | func (tl *ioLogWrapper) Out() io.Writer {
55 | return tl.out
56 | }
57 |
58 | func (tl *ioLogWrapper) Err() io.Writer {
59 | return tl.err
60 | }
61 |
62 | func (tl *ioLogWrapper) Close() error {
63 | return tl.hook.Close()
64 | }
65 |
--------------------------------------------------------------------------------
/util/termlogger/types.go:
--------------------------------------------------------------------------------
1 | package termlogger
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | type LogHook interface {
10 | io.Closer
11 | logrus.Hook
12 | }
13 |
14 | // DummyWriter is a writer that discard everything that writes in
15 | // Consider like writing into /dev/null
16 | type DummyWriter struct{}
17 |
18 | func (DummyWriter) Write(p []byte) (int, error) { return len(p), nil }
19 |
--------------------------------------------------------------------------------
/util/termlogger/uml.go:
--------------------------------------------------------------------------------
1 | package termlogger
2 |
3 | import (
4 | "encoding/binary"
5 | "os"
6 | "time"
7 |
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | // umlLogHeader is the data header for UML compatible log
12 | type umlLogHeader struct {
13 | op int32
14 | tty uint32
15 | len int32
16 | dir TTYDirection
17 | sec uint32
18 | usec uint32
19 | }
20 |
21 | // UmlLog is the instance for storing logging information, like io
22 | type UMLHook struct {
23 | tty uint32
24 | name string
25 | stdout chan []byte
26 | }
27 |
28 | // TTYDirection specifies the direction of data
29 | type TTYDirection int32
30 |
31 | const (
32 | ttyLogOpen = 1
33 | ttyLogClose = 2
34 | ttyLogWrite = 3
35 | )
36 |
37 | const (
38 | // TTYRead Indicates reading from terminal
39 | TTYRead TTYDirection = 1
40 | // TTYWrite Indicates writing to terminal
41 | TTYWrite TTYDirection = 2
42 | )
43 |
44 | // NewUMLHook creates a new logrus hook instance and will create the UML log file
45 | func NewUMLHook(ttyID uint32, logFile string) (LogHook, error) {
46 | file, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
47 | defer file.Close()
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | t := &UMLHook{
53 | tty: ttyID,
54 | name: logFile,
55 | stdout: make(chan []byte, 100),
56 | }
57 | now := time.Now()
58 | header := umlLogHeader{
59 | op: ttyLogOpen,
60 | tty: t.tty,
61 | len: 0,
62 | dir: 0,
63 | sec: uint32(now.Unix()), //For compatibility, works till 2038
64 | usec: uint32(now.UnixNano()), //For compatibility, works till 2038
65 | }
66 | err = binary.Write(file, binary.LittleEndian, header)
67 | if err != nil {
68 | return nil, err
69 | }
70 | return t, nil
71 | }
72 |
73 | func (uLog *UMLHook) Fire(entry *log.Entry) error {
74 | file, err := os.OpenFile(uLog.name, os.O_APPEND|os.O_WRONLY, 0666)
75 | defer file.Close()
76 | if err != nil {
77 | return err
78 | }
79 | size := len([]byte(entry.Message))
80 | var ttyDir TTYDirection
81 | if entry.Data["dir"] == "i" {
82 | ttyDir = TTYRead
83 | } else {
84 | ttyDir = TTYWrite
85 | }
86 | header := umlLogHeader{
87 | op: ttyLogWrite,
88 | tty: uLog.tty,
89 | len: int32(size),
90 | dir: ttyDir,
91 | sec: uint32(entry.Time.Unix()), //For compatibility, works till 2038
92 | usec: uint32(entry.Time.UnixNano()), //For compatibility, works till 2038
93 | }
94 | err = binary.Write(file, binary.LittleEndian, header)
95 | if err != nil {
96 | return err
97 | }
98 | _, err = file.Write([]byte(entry.Message))
99 | if err != nil {
100 | return err
101 | }
102 | return nil
103 | }
104 |
105 | // Close closes the log file for writing UML logs
106 | func (uLog *UMLHook) Close() error {
107 | now := time.Now()
108 | file, _ := os.OpenFile(uLog.name, os.O_APPEND|os.O_WRONLY, 0666)
109 | defer file.Close()
110 | header := umlLogHeader{
111 | op: ttyLogClose,
112 | tty: uLog.tty,
113 | len: 0,
114 | dir: 0,
115 | sec: uint32(now.Unix()), //For compatibility, works till 2038
116 | usec: uint32(now.UnixNano()), //For compatibility, works till 2038
117 | }
118 | binary.Write(file, binary.LittleEndian, header)
119 | return nil
120 | }
121 |
122 | func (uLog *UMLHook) Levels() []log.Level {
123 | return log.AllLevels
124 | }
125 |
--------------------------------------------------------------------------------
/virtualfs/file.go:
--------------------------------------------------------------------------------
1 | package virtualfs
2 |
3 | // Mostly referenced from https://github.com/hillu/afero
4 | import (
5 | "archive/zip"
6 | "io"
7 | "os"
8 | "syscall"
9 |
10 | "github.com/spf13/afero"
11 | )
12 |
13 | type File struct {
14 | os.FileInfo
15 | zipFile *zip.File
16 | children map[string]*File
17 | reader io.ReadCloser
18 | SymLink string
19 | closed bool
20 | offset int64
21 | buf []byte
22 | dirOffset int
23 | }
24 |
25 | func (f *File) fillBuffer(offset int64) (err error) {
26 | if f.reader == nil {
27 | if f.reader, err = f.zipFile.Open(); err != nil {
28 | return
29 | }
30 | }
31 | if offset > int64(f.zipFile.UncompressedSize64) {
32 | offset = int64(f.zipFile.UncompressedSize64)
33 | err = io.EOF
34 | }
35 | if len(f.buf) >= int(offset) {
36 | return
37 | }
38 | buf := make([]byte, int(offset)-len(f.buf))
39 | n, _ := io.ReadFull(f.reader, buf)
40 | if n > 0 {
41 | f.buf = append(f.buf, buf[:n]...)
42 | }
43 | return
44 | }
45 |
46 | func (f *File) Close() (err error) {
47 | f.zipFile = nil
48 | f.closed = true
49 | f.buf = nil
50 | if f.reader != nil {
51 | err = f.reader.Close()
52 | f.reader = nil
53 | }
54 | return
55 | }
56 |
57 | func (f *File) Read(p []byte) (n int, err error) {
58 | if f.FileInfo.IsDir() {
59 | return 0, syscall.EISDIR
60 | }
61 | if f.closed {
62 | return 0, afero.ErrFileClosed
63 | }
64 | err = f.fillBuffer(f.offset + int64(len(p)))
65 | n = copy(p, f.buf[f.offset:])
66 | f.offset += int64(len(p))
67 | return
68 | }
69 |
70 | func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
71 | if f.FileInfo.IsDir() {
72 | return 0, syscall.EISDIR
73 | }
74 | if f.closed {
75 | return 0, afero.ErrFileClosed
76 | }
77 | err = f.fillBuffer(off + int64(len(p)))
78 | n = copy(p, f.buf[int(off):])
79 | return
80 | }
81 |
82 | func (f *File) Seek(offset int64, whence int) (int64, error) {
83 | if f.FileInfo.IsDir() {
84 | return 0, syscall.EISDIR
85 | }
86 | if f.closed {
87 | return 0, afero.ErrFileClosed
88 | }
89 | switch whence {
90 | case os.SEEK_SET:
91 | case os.SEEK_CUR:
92 | offset += f.offset
93 | case os.SEEK_END:
94 | offset += int64(f.zipFile.UncompressedSize64)
95 | default:
96 | return 0, syscall.EINVAL
97 | }
98 | if offset < 0 || offset > int64(f.zipFile.UncompressedSize64) {
99 | return 0, afero.ErrOutOfRange
100 | }
101 | f.offset = offset
102 | return offset, nil
103 | }
104 |
105 | func (f *File) Write(p []byte) (n int, err error) {
106 | return 0, os.ErrPermission
107 | }
108 | func (f *File) WriteAt(p []byte, off int64) (n int, err error) {
109 | return 0, os.ErrPermission
110 | }
111 |
112 | /* func (f *File) Name() string {
113 | return f.
114 | } */
115 |
116 | func (f *File) Readdir(n int) ([]os.FileInfo, error) {
117 | m := f.children
118 | nArr := make([]os.FileInfo, 0, len(m))
119 | for _, node := range m {
120 | nArr = append(nArr, node)
121 | }
122 | if n <= 0 || n >= len(m) {
123 | return nArr, nil
124 | }
125 | return nArr[:n], nil
126 | }
127 | func (f *File) Readdirnames(n int) ([]string, error) {
128 | m := f.children
129 | nArr := make([]string, 0, len(m))
130 | for name, _ := range m {
131 | nArr = append(nArr, name)
132 | }
133 | if n <= 0 || n >= len(m) {
134 | return nArr, nil
135 | }
136 | return nArr[:n], nil
137 | }
138 | func (f *File) Stat() (os.FileInfo, error) {
139 | return f, nil
140 | }
141 | func (f *File) Sync() error {
142 | return nil
143 | }
144 | func (f *File) Truncate(size int64) error {
145 | return os.ErrPermission
146 | }
147 | func (f *File) WriteString(s string) (ret int, err error) {
148 | return 0, os.ErrPermission
149 | }
150 |
--------------------------------------------------------------------------------
/virtualfs/fileinfo.go:
--------------------------------------------------------------------------------
1 | package virtualfs
2 |
3 | import (
4 | "archive/zip"
5 | "encoding/binary"
6 | "os"
7 | "time"
8 | )
9 |
10 | type FileInfo struct {
11 | os.FileInfo
12 | extraInfo ZipExtraInfo
13 | }
14 |
15 | type ZipExtraInfo struct {
16 | zh *zip.FileHeader
17 | ctime time.Time
18 | atime time.Time
19 | mtime time.Time
20 | uid int
21 | gid int
22 | }
23 |
24 | type unixFileInfo struct {
25 | UID int
26 | GID int
27 | }
28 |
29 | type unixTimestampInfo struct {
30 | ModTime time.Time
31 | AccTime time.Time
32 | CreTime time.Time
33 | }
34 |
35 | func (fi FileInfo) Sys() interface{} {
36 | zipHeader := fi.FileInfo.Sys().(*zip.FileHeader)
37 |
38 | unixInfo, ts := readExtraHeader(zipHeader.Extra)
39 | finfo := ZipExtraInfo{
40 | zh: fi.FileInfo.Sys().(*zip.FileHeader),
41 | ctime: ts.CreTime,
42 | atime: ts.AccTime,
43 | mtime: ts.ModTime,
44 | uid: unixInfo.UID,
45 | gid: unixInfo.GID,
46 | }
47 | return finfo
48 | }
49 |
50 | func readExtraHeader(dataField []byte) (fileInfo unixFileInfo, tsInfo unixTimestampInfo) {
51 | for pos := 0; pos < len(dataField); {
52 | fieldID := binary.LittleEndian.Uint16(dataField[pos : pos+2])
53 | pos += 2
54 | fieldLen := binary.LittleEndian.Uint16(dataField[pos : pos+2])
55 | pos += 2
56 | switch fieldID {
57 | case 0x5455: // Modification timestamp
58 | // Referenced from https://github.com/koron/go-zipext/blob/master/zipext.go
59 | tsInfo = unixTimestampInfo{}
60 | flag := dataField[pos]
61 | pos++
62 | if flag&0x01 != 0 && len(dataField)-pos >= 4 {
63 | tsInfo.ModTime = time.Unix(int64(binary.LittleEndian.Uint32(dataField[pos:])), 0)
64 | pos += 4
65 | }
66 | if flag&0x02 != 0 && len(dataField)-pos >= 4 && fieldLen > 5 {
67 | tsInfo.AccTime = time.Unix(int64(binary.LittleEndian.Uint32(dataField[pos:])), 0)
68 | pos += 4
69 | }
70 | if flag&0x04 != 0 && len(dataField)-pos >= 4 && fieldLen > 9 {
71 | tsInfo.CreTime = time.Unix(int64(binary.LittleEndian.Uint32(dataField[pos:])), 0)
72 | pos += 4
73 | }
74 | case 0x7875: // UNIX UID/GID
75 | fileInfo = unixFileInfo{}
76 | pos++ // skip version field
77 | uidLen := int(dataField[pos])
78 | pos++
79 | fileInfo.UID = int(readVariableInt(dataField[pos : pos+uidLen]))
80 | pos += uidLen
81 | gidLen := int(dataField[pos])
82 | pos++
83 | fileInfo.GID = int(readVariableInt(dataField[pos : pos+gidLen]))
84 | pos += gidLen
85 | default:
86 | //Skip the whole field
87 | pos += int(fieldLen)
88 | }
89 |
90 | }
91 | return
92 | }
93 |
94 | func readVariableInt(field []byte) uint32 {
95 | switch len(field) {
96 | case 4:
97 | return binary.LittleEndian.Uint32(field)
98 | case 8:
99 | return uint32(binary.LittleEndian.Uint64(field))
100 | }
101 | return 0
102 | }
103 |
104 | func (zInfo ZipExtraInfo) UID() int {
105 | return zInfo.uid
106 | }
107 |
108 | func (zInfo ZipExtraInfo) GID() int {
109 | return zInfo.gid
110 | }
111 |
112 | func (zInfo ZipExtraInfo) Atime() time.Time {
113 | return zInfo.atime
114 | }
115 |
116 | func (zInfo ZipExtraInfo) Mtime() time.Time {
117 | return zInfo.mtime
118 | }
119 |
120 | func (zInfo ZipExtraInfo) Ctime() time.Time {
121 | return zInfo.ctime
122 | }
123 |
124 | func GetExtraInfo(fi os.FileInfo) (uid, gid int, aTime, mTime time.Time) {
125 |
126 | switch p := fi.Sys().(type) {
127 | case ZipExtraInfo:
128 | uid = p.UID()
129 | gid = p.GID()
130 | aTime = p.Atime()
131 | mTime = p.Mtime()
132 | default:
133 | uid = 0
134 | gid = 0
135 | }
136 | return
137 | }
138 |
--------------------------------------------------------------------------------
/virtualfs/filesystem.go:
--------------------------------------------------------------------------------
1 | package virtualfs
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "io"
7 | "os"
8 | pathlib "path"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 |
13 | "github.com/spf13/afero"
14 | )
15 |
16 | type VirtualFS struct {
17 | root *File
18 | }
19 |
20 | type rootInfo struct{}
21 |
22 | func (rootInfo) Name() string { return string(filepath.Separator) }
23 | func (rootInfo) Size() int64 { return 0 }
24 | func (rootInfo) Mode() os.FileMode { return os.ModeDir | os.ModePerm }
25 | func (rootInfo) ModTime() time.Time { return time.Now() }
26 | func (rootInfo) IsDir() bool { return true }
27 | func (rootInfo) Sys() interface{} { return nil }
28 |
29 | // NewVirtualFS initalized the tree, which creates the root directory
30 | func NewVirtualFS(zipFile string) (afero.Fs, error) {
31 | r, err := zip.OpenReader(zipFile)
32 | if err != nil {
33 | return nil, err
34 | }
35 | defer r.Close()
36 | vfs := &VirtualFS{
37 | root: &File{
38 | FileInfo: rootInfo{},
39 | children: make(map[string]*File),
40 | },
41 | }
42 | for _, f := range r.File {
43 | vfs.createNode(f)
44 | }
45 | return vfs, nil
46 | }
47 |
48 | func (t *VirtualFS) Name() string {
49 | return "zipFS"
50 | }
51 |
52 | func (t *VirtualFS) createNode(f *zip.File) error {
53 | n := &File{
54 | zipFile: f,
55 | FileInfo: FileInfo{FileInfo: f.FileInfo()},
56 | }
57 | if n.Mode()&os.ModeDir != 0 {
58 | n.children = make(map[string]*File)
59 | } else if n.Mode()&os.ModeSymlink != 0 {
60 | rd, err := f.Open()
61 | if err != nil {
62 | return err
63 | }
64 | buf := bytes.NewBuffer(nil)
65 | io.Copy(buf, rd)
66 | n.SymLink = buf.String()
67 | rd.Close()
68 | }
69 | dir, nodeName := pathlib.Split("/" + strings.TrimSuffix(f.Name, "/"))
70 | var parent *File
71 | var err error
72 | parent, err = t.fetchNode(dir, false)
73 | if err != nil {
74 | return err
75 | }
76 | parent.children[nodeName] = n
77 |
78 | return nil
79 | }
80 |
81 | // Mkdir creates a new directory according to the path argument passed in
82 | func (t *VirtualFS) Mkdir(path string, mode os.FileMode) error {
83 | return &os.PathError{Op: "mkdir", Err: os.ErrPermission, Path: path}
84 | }
85 |
86 | // MkdirAll creates a new directory according to the path argument passed in
87 | func (t *VirtualFS) MkdirAll(path string, mode os.FileMode) error {
88 | return &os.PathError{Op: "mkdir", Err: os.ErrPermission, Path: path}
89 | }
90 |
91 | func (t *VirtualFS) Remove(path string) error {
92 | return &os.PathError{Op: "remove", Err: os.ErrPermission, Path: path}
93 | }
94 |
95 | func (t *VirtualFS) RemoveAll(path string) error {
96 | return &os.PathError{Op: "remove", Err: os.ErrPermission, Path: path}
97 | }
98 |
99 | func (t *VirtualFS) Rename(new, old string) error {
100 | return &os.PathError{Op: "rename", Err: os.ErrPermission, Path: old}
101 | }
102 |
103 | func (t *VirtualFS) fetchNode(path string, followSymLink bool) (*File, error) {
104 | if strings.HasPrefix(path, "\\") {
105 | path = strings.Replace(path, "\\", "/", -1)
106 | }
107 | path = pathlib.Clean(path)
108 | cwd := t.root
109 |
110 | if path == "/" {
111 | return t.root, nil
112 | }
113 | dirs := strings.Split(path, "/")
114 | for _, nodeName := range dirs[1:] {
115 | if cwd.Mode()&os.ModeSymlink != 0 && followSymLink {
116 | var err error
117 | cwd, err = t.fetchNode(cwd.SymLink, true)
118 | if err != nil {
119 | return nil, err
120 | }
121 | }
122 | node, nodeExists := cwd.children[nodeName]
123 | if !nodeExists {
124 | return nil, &os.PathError{Op: "open", Err: os.ErrNotExist, Path: path}
125 | }
126 | cwd = node
127 | }
128 | return cwd, nil
129 | }
130 |
131 | func (t *VirtualFS) Create(path string) (afero.File, error) {
132 | return nil, &os.PathError{Op: "create", Err: os.ErrPermission, Path: path}
133 | }
134 |
135 | func (t *VirtualFS) Open(path string) (afero.File, error) {
136 | n, err := t.fetchNode(path, false)
137 | if err != nil {
138 | return nil, err
139 | }
140 | return n, nil
141 | }
142 |
143 | func (t *VirtualFS) OpenFile(path string, flag int, mode os.FileMode) (afero.File, error) {
144 | node, err := t.fetchNode(path, true)
145 | if err != nil {
146 | return nil, err
147 | }
148 | return node, nil
149 | }
150 |
151 | func (t *VirtualFS) Stat(path string) (os.FileInfo, error) {
152 | n, err := t.fetchNode(path, true)
153 | if err != nil {
154 | return nil, err
155 | }
156 | return n.FileInfo, nil
157 | }
158 |
159 | func (t *VirtualFS) Chmod(path string, mode os.FileMode) error {
160 | return &os.PathError{Op: "chmod", Err: os.ErrPermission, Path: path}
161 | }
162 |
163 | func (t *VirtualFS) Chtimes(path string, modTime, accTime time.Time) error {
164 | return &os.PathError{Op: "chtimes", Err: os.ErrPermission, Path: path}
165 | }
166 |
--------------------------------------------------------------------------------
/virtualfs/filesystem_test.go:
--------------------------------------------------------------------------------
1 | package virtualfs
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestCreateFS(t *testing.T) {
9 | vfs, err := NewVirtualFS("../filesystem.zip")
10 | if err != nil {
11 | t.Fatal(err)
12 | }
13 | bootDir, err := vfs.Open("/boot")
14 | if err != nil {
15 | t.Error(err)
16 | }
17 | dirNames, err := bootDir.Readdirnames(10)
18 | if err != nil {
19 | t.Error(err)
20 | }
21 | if len(dirNames) != 6 {
22 | t.Error("Dir don't match")
23 | }
24 | }
25 |
26 | func TestFsStat(t *testing.T) {
27 | vfs, err := NewVirtualFS("../filesystem.zip")
28 | if err != nil {
29 | t.Fatal(err)
30 | }
31 | fi, err := vfs.Stat("/home/mk")
32 | if err != nil {
33 | t.Fatal(err)
34 | }
35 | sysType := fmt.Sprintf("%T", fi.Sys())
36 | if sysType != "virtualfs.ZipExtraInfo" {
37 | t.Error(sysType)
38 | }
39 | if fi.Name() != "mk" {
40 | t.Error(fi.Name())
41 | }
42 | }
43 |
--------------------------------------------------------------------------------