mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-09 12:21:25 -10:00
Calling Close on an SSHClient that never connected, or whose Connect failed, dereferenced a nil *ssh.Client. goja turned the panic into a request error, so the template stopped at Close and returned nothing. Close also kept the closed client. On a client reused across login attempts, a later Close then failed with "use of closed network connection", and Run reported a transport error instead of "no connection". Close now returns true when no connection is open, and clears the connection after closing it. It returns true rather than an error because goja throws a returned error as a JS exception, which would still abort the template. Fixes: #8070 Signed-off-by: Dwi Siswanto <[email protected]>
290 lines
7.6 KiB
Go
290 lines
7.6 KiB
Go
package ssh
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/projectdiscovery/gologger"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
|
"github.com/projectdiscovery/utils/errkit"
|
|
"github.com/zmap/zgrab2/lib/ssh"
|
|
)
|
|
|
|
type (
|
|
// SSHClient is a client for SSH servers.
|
|
// Internally client uses github.com/zmap/zgrab2/lib/ssh driver.
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// ```
|
|
SSHClient struct {
|
|
// connection is nil when no connection is open.
|
|
connection *ssh.Client
|
|
timeout time.Duration
|
|
}
|
|
)
|
|
|
|
// precompiled regex patterns
|
|
var (
|
|
passwordQuestionPattern = regexp.MustCompile(`(?i)(pass(word|phrase|code)?|pin)`)
|
|
usernameQuestionPattern = regexp.MustCompile(`(?i)(user(name)?|login)`)
|
|
)
|
|
|
|
// SetTimeout sets the timeout for the SSH connection in seconds
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// client.SetTimeout(10);
|
|
// ```
|
|
func (c *SSHClient) SetTimeout(sec int) {
|
|
c.timeout = time.Duration(sec) * time.Second
|
|
}
|
|
|
|
// Connect tries to connect to provided host and port
|
|
// with provided username and password with ssh.
|
|
// Returns state of connection and error. If error is not nil,
|
|
// state will be false
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// const connected = client.Connect('acme.com', 22, 'username', 'password');
|
|
// ```
|
|
func (c *SSHClient) Connect(ctx context.Context, host string, port int, username, password string) (bool, error) {
|
|
executionId := ctx.Value("executionId").(string)
|
|
conn, err := connect(&connectOptions{
|
|
Host: host,
|
|
Port: port,
|
|
User: username,
|
|
Password: password,
|
|
ExecutionId: executionId,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
c.connection = conn
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// ConnectWithKey tries to connect to provided host and port
|
|
// with provided username and private_key.
|
|
// Returns state of connection and error. If error is not nil,
|
|
// state will be false
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// const privateKey = `-----BEGIN RSA PRIVATE KEY----- ...`;
|
|
// const connected = client.ConnectWithKey('acme.com', 22, 'username', privateKey);
|
|
// ```
|
|
func (c *SSHClient) ConnectWithKey(ctx context.Context, host string, port int, username, key string) (bool, error) {
|
|
executionId := ctx.Value("executionId").(string)
|
|
conn, err := connect(&connectOptions{
|
|
Host: host,
|
|
Port: port,
|
|
User: username,
|
|
PrivateKey: key,
|
|
ExecutionId: executionId,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
c.connection = conn
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// ConnectSSHInfoMode tries to connect to provided host and port
|
|
// with provided host and port
|
|
// Returns HandshakeLog and error. If error is not nil,
|
|
// state will be false
|
|
// HandshakeLog is a struct that contains information about the
|
|
// ssh connection
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// const info = client.ConnectSSHInfoMode('acme.com', 22);
|
|
// log(to_json(info));
|
|
// ```
|
|
func (c *SSHClient) ConnectSSHInfoMode(ctx context.Context, host string, port int) (*ssh.HandshakeLog, error) {
|
|
executionId := ctx.Value("executionId").(string)
|
|
return memoizedconnectSSHInfoMode(&connectOptions{
|
|
Host: host,
|
|
Port: port,
|
|
ExecutionId: executionId,
|
|
})
|
|
}
|
|
|
|
// Run tries to open a new SSH session, then tries to execute
|
|
// the provided command in said session
|
|
// Returns string and error. If error is not nil,
|
|
// state will be false
|
|
// The string contains the command output
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// client.Connect('acme.com', 22, 'username', 'password');
|
|
// const output = client.Run('id');
|
|
// log(output);
|
|
// ```
|
|
func (c *SSHClient) Run(cmd string) (string, error) {
|
|
if c.connection == nil {
|
|
return "", errkit.New("no connection")
|
|
}
|
|
session, err := c.connection.NewSession()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() {
|
|
_ = session.Close()
|
|
}()
|
|
|
|
data, err := session.Output(cmd)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(data), nil
|
|
}
|
|
|
|
// Close closes the SSH connection and destroys the client
|
|
// Returns the success state and error. If error is not nil,
|
|
// state will be false. Close does nothing and returns true
|
|
// if no connection is open.
|
|
// @example
|
|
// ```javascript
|
|
// const ssh = require('nuclei/ssh');
|
|
// const client = new ssh.SSHClient();
|
|
// client.Connect('acme.com', 22, 'username', 'password');
|
|
// const closed = client.Close();
|
|
// ```
|
|
func (c *SSHClient) Close() (bool, error) {
|
|
if c.connection == nil {
|
|
return true, nil
|
|
}
|
|
|
|
err := c.connection.Close()
|
|
c.connection = nil
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// unexported functions
|
|
type connectOptions struct {
|
|
Host string
|
|
Port int
|
|
User string
|
|
Password string
|
|
PrivateKey string
|
|
Timeout time.Duration // default 10s
|
|
ExecutionId string
|
|
}
|
|
|
|
func (c *connectOptions) validate() error {
|
|
if c.Host == "" {
|
|
return errkit.New("host is required")
|
|
}
|
|
if c.Port <= 0 {
|
|
return errkit.New("port is required")
|
|
}
|
|
if !protocolstate.IsHostAllowed(c.ExecutionId, c.Host) {
|
|
// host is not valid according to network policy
|
|
return protocolstate.ErrHostDenied.Msgf(c.Host)
|
|
}
|
|
if c.Timeout == 0 {
|
|
c.Timeout = 10 * time.Second
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// @memo
|
|
func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
|
|
if err := opts.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := new(ssh.HandshakeLog)
|
|
|
|
sshConfig := ssh.MakeSSHConfig()
|
|
sshConfig.Timeout = 10 * time.Second
|
|
sshConfig.ConnLog = data
|
|
sshConfig.DontAuthenticate = true
|
|
sshConfig.BannerCallback = func(banner string) error {
|
|
data.Banner = strings.TrimSpace(banner)
|
|
return nil
|
|
}
|
|
rhost := fmt.Sprintf("%s:%d", opts.Host, opts.Port)
|
|
client, err := ssh.Dial("tcp", rhost, sshConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = client.Close()
|
|
}()
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func connect(opts *connectOptions) (*ssh.Client, error) {
|
|
if err := opts.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conf := &ssh.ClientConfig{
|
|
User: opts.User,
|
|
Auth: []ssh.AuthMethod{},
|
|
Timeout: opts.Timeout,
|
|
}
|
|
|
|
if len(opts.Password) > 0 {
|
|
conf.Auth = append(conf.Auth, ssh.Password(opts.Password))
|
|
|
|
cb := func(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
|
answers = make([]string, len(questions))
|
|
filledCount := 0
|
|
for i, question := range questions {
|
|
challenge := map[string]any{"user": user, "instruction": instruction, "question": question, "echo": echos[i]}
|
|
gologger.Debug().Msgf("SSH keyboard-interactive question %d/%d: %s", i+1, len(questions), vardump.DumpVariables(challenge))
|
|
if !echos[i] && passwordQuestionPattern.MatchString(question) {
|
|
answers[i] = opts.Password
|
|
filledCount++
|
|
} else if echos[i] && usernameQuestionPattern.MatchString(question) {
|
|
answers[i] = opts.User
|
|
filledCount++
|
|
}
|
|
}
|
|
gologger.Debug().Msgf("SSH keyboard-interactive: %d/%d questions filled", filledCount, len(questions))
|
|
return answers, nil
|
|
}
|
|
conf.Auth = append(conf.Auth, ssh.KeyboardInteractiveChallenge(cb))
|
|
}
|
|
|
|
if len(opts.PrivateKey) > 0 {
|
|
signer, err := ssh.ParsePrivateKey([]byte(opts.PrivateKey))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conf.Auth = append(conf.Auth, ssh.PublicKeys(signer))
|
|
}
|
|
|
|
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client, nil
|
|
}
|