Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/js/libs/ssh/ssh.go
2843 views
1
package ssh
2
3
import (
4
"context"
5
"fmt"
6
"regexp"
7
"strings"
8
"time"
9
10
"github.com/projectdiscovery/gologger"
11
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
12
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
13
"github.com/projectdiscovery/utils/errkit"
14
"github.com/zmap/zgrab2/lib/ssh"
15
)
16
17
type (
18
// SSHClient is a client for SSH servers.
19
// Internally client uses github.com/zmap/zgrab2/lib/ssh driver.
20
// @example
21
// ```javascript
22
// const ssh = require('nuclei/ssh');
23
// const client = new ssh.SSHClient();
24
// ```
25
SSHClient struct {
26
connection *ssh.Client
27
timeout time.Duration
28
}
29
)
30
31
// precompiled regex patterns
32
var (
33
passwordQuestionPattern = regexp.MustCompile(`(?i)(pass(word|phrase|code)?|pin)`)
34
usernameQuestionPattern = regexp.MustCompile(`(?i)(user(name)?|login)`)
35
)
36
37
// SetTimeout sets the timeout for the SSH connection in seconds
38
// @example
39
// ```javascript
40
// const ssh = require('nuclei/ssh');
41
// const client = new ssh.SSHClient();
42
// client.SetTimeout(10);
43
// ```
44
func (c *SSHClient) SetTimeout(sec int) {
45
c.timeout = time.Duration(sec) * time.Second
46
}
47
48
// Connect tries to connect to provided host and port
49
// with provided username and password with ssh.
50
// Returns state of connection and error. If error is not nil,
51
// state will be false
52
// @example
53
// ```javascript
54
// const ssh = require('nuclei/ssh');
55
// const client = new ssh.SSHClient();
56
// const connected = client.Connect('acme.com', 22, 'username', 'password');
57
// ```
58
func (c *SSHClient) Connect(ctx context.Context, host string, port int, username, password string) (bool, error) {
59
executionId := ctx.Value("executionId").(string)
60
conn, err := connect(&connectOptions{
61
Host: host,
62
Port: port,
63
User: username,
64
Password: password,
65
ExecutionId: executionId,
66
})
67
if err != nil {
68
return false, err
69
}
70
c.connection = conn
71
72
return true, nil
73
}
74
75
// ConnectWithKey tries to connect to provided host and port
76
// with provided username and private_key.
77
// Returns state of connection and error. If error is not nil,
78
// state will be false
79
// @example
80
// ```javascript
81
// const ssh = require('nuclei/ssh');
82
// const client = new ssh.SSHClient();
83
// const privateKey = `-----BEGIN RSA PRIVATE KEY----- ...`;
84
// const connected = client.ConnectWithKey('acme.com', 22, 'username', privateKey);
85
// ```
86
func (c *SSHClient) ConnectWithKey(ctx context.Context, host string, port int, username, key string) (bool, error) {
87
executionId := ctx.Value("executionId").(string)
88
conn, err := connect(&connectOptions{
89
Host: host,
90
Port: port,
91
User: username,
92
PrivateKey: key,
93
ExecutionId: executionId,
94
})
95
96
if err != nil {
97
return false, err
98
}
99
c.connection = conn
100
101
return true, nil
102
}
103
104
// ConnectSSHInfoMode tries to connect to provided host and port
105
// with provided host and port
106
// Returns HandshakeLog and error. If error is not nil,
107
// state will be false
108
// HandshakeLog is a struct that contains information about the
109
// ssh connection
110
// @example
111
// ```javascript
112
// const ssh = require('nuclei/ssh');
113
// const client = new ssh.SSHClient();
114
// const info = client.ConnectSSHInfoMode('acme.com', 22);
115
// log(to_json(info));
116
// ```
117
func (c *SSHClient) ConnectSSHInfoMode(ctx context.Context, host string, port int) (*ssh.HandshakeLog, error) {
118
executionId := ctx.Value("executionId").(string)
119
return memoizedconnectSSHInfoMode(&connectOptions{
120
Host: host,
121
Port: port,
122
ExecutionId: executionId,
123
})
124
}
125
126
// Run tries to open a new SSH session, then tries to execute
127
// the provided command in said session
128
// Returns string and error. If error is not nil,
129
// state will be false
130
// The string contains the command output
131
// @example
132
// ```javascript
133
// const ssh = require('nuclei/ssh');
134
// const client = new ssh.SSHClient();
135
// client.Connect('acme.com', 22, 'username', 'password');
136
// const output = client.Run('id');
137
// log(output);
138
// ```
139
func (c *SSHClient) Run(cmd string) (string, error) {
140
if c.connection == nil {
141
return "", errkit.New("no connection")
142
}
143
session, err := c.connection.NewSession()
144
if err != nil {
145
return "", err
146
}
147
defer func() {
148
_ = session.Close()
149
}()
150
151
data, err := session.Output(cmd)
152
if err != nil {
153
return "", err
154
}
155
156
return string(data), nil
157
}
158
159
// Close closes the SSH connection and destroys the client
160
// Returns the success state and error. If error is not nil,
161
// state will be false
162
// @example
163
// ```javascript
164
// const ssh = require('nuclei/ssh');
165
// const client = new ssh.SSHClient();
166
// client.Connect('acme.com', 22, 'username', 'password');
167
// const closed = client.Close();
168
// ```
169
func (c *SSHClient) Close() (bool, error) {
170
if err := c.connection.Close(); err != nil {
171
return false, err
172
}
173
return true, nil
174
}
175
176
// unexported functions
177
type connectOptions struct {
178
Host string
179
Port int
180
User string
181
Password string
182
PrivateKey string
183
Timeout time.Duration // default 10s
184
ExecutionId string
185
}
186
187
func (c *connectOptions) validate() error {
188
if c.Host == "" {
189
return errkit.New("host is required")
190
}
191
if c.Port <= 0 {
192
return errkit.New("port is required")
193
}
194
if !protocolstate.IsHostAllowed(c.ExecutionId, c.Host) {
195
// host is not valid according to network policy
196
return protocolstate.ErrHostDenied.Msgf(c.Host)
197
}
198
if c.Timeout == 0 {
199
c.Timeout = 10 * time.Second
200
}
201
return nil
202
}
203
204
// @memo
205
func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
206
if err := opts.validate(); err != nil {
207
return nil, err
208
}
209
210
data := new(ssh.HandshakeLog)
211
212
sshConfig := ssh.MakeSSHConfig()
213
sshConfig.Timeout = 10 * time.Second
214
sshConfig.ConnLog = data
215
sshConfig.DontAuthenticate = true
216
sshConfig.BannerCallback = func(banner string) error {
217
data.Banner = strings.TrimSpace(banner)
218
return nil
219
}
220
rhost := fmt.Sprintf("%s:%d", opts.Host, opts.Port)
221
client, err := ssh.Dial("tcp", rhost, sshConfig)
222
if err != nil {
223
return nil, err
224
}
225
defer func() {
226
_ = client.Close()
227
}()
228
229
return data, nil
230
}
231
232
func connect(opts *connectOptions) (*ssh.Client, error) {
233
if err := opts.validate(); err != nil {
234
return nil, err
235
}
236
237
conf := &ssh.ClientConfig{
238
User: opts.User,
239
Auth: []ssh.AuthMethod{},
240
Timeout: opts.Timeout,
241
}
242
243
if len(opts.Password) > 0 {
244
conf.Auth = append(conf.Auth, ssh.Password(opts.Password))
245
246
cb := func(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
247
answers = make([]string, len(questions))
248
filledCount := 0
249
for i, question := range questions {
250
challenge := map[string]any{"user": user, "instruction": instruction, "question": question, "echo": echos[i]}
251
gologger.Debug().Msgf("SSH keyboard-interactive question %d/%d: %s", i+1, len(questions), vardump.DumpVariables(challenge))
252
if !echos[i] && passwordQuestionPattern.MatchString(question) {
253
answers[i] = opts.Password
254
filledCount++
255
} else if echos[i] && usernameQuestionPattern.MatchString(question) {
256
answers[i] = opts.User
257
filledCount++
258
}
259
}
260
gologger.Debug().Msgf("SSH keyboard-interactive: %d/%d questions filled", filledCount, len(questions))
261
return answers, nil
262
}
263
conf.Auth = append(conf.Auth, ssh.KeyboardInteractiveChallenge(cb))
264
}
265
266
if len(opts.PrivateKey) > 0 {
267
signer, err := ssh.ParsePrivateKey([]byte(opts.PrivateKey))
268
if err != nil {
269
return nil, err
270
}
271
conf.Auth = append(conf.Auth, ssh.PublicKeys(signer))
272
}
273
274
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf)
275
if err != nil {
276
return nil, err
277
}
278
return client, nil
279
}
280
281