Files
Dwi Siswanto accfe3551b fix(js): make SSHClient.Close a no-op without a connection
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]>
2026-11-06 09:40:26 +07:00

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
}