2023-09-16 16:02:17 +05:30
|
|
|
package ssh
|
|
|
|
|
|
|
|
|
|
import (
|
2025-07-09 14:47:26 -05:00
|
|
|
"context"
|
2023-09-16 16:02:17 +05:30
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
2025-08-20 05:28:23 +05:30
|
|
|
"github.com/projectdiscovery/utils/errkit"
|
2023-09-16 16:02:17 +05:30
|
|
|
"github.com/zmap/zgrab2/lib/ssh"
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-07 21:45:40 +05:30
|
|
|
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');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// ```
|
|
|
|
|
SSHClient struct {
|
|
|
|
|
connection *ssh.Client
|
|
|
|
|
timeout time.Duration
|
|
|
|
|
}
|
|
|
|
|
)
|
2023-12-07 19:38:32 +03:00
|
|
|
|
|
|
|
|
// SetTimeout sets the timeout for the SSH connection in seconds
|
2024-02-07 21:45:40 +05:30
|
|
|
// @example
|
|
|
|
|
// ```javascript
|
|
|
|
|
// const ssh = require('nuclei/ssh');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// client.SetTimeout(10);
|
|
|
|
|
// ```
|
2023-12-07 19:38:32 +03:00
|
|
|
func (c *SSHClient) SetTimeout(sec int) {
|
|
|
|
|
c.timeout = time.Duration(sec) * time.Second
|
2023-11-23 19:37:45 +01:00
|
|
|
}
|
2023-09-16 16:02:17 +05:30
|
|
|
|
|
|
|
|
// 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
|
2024-02-07 21:45:40 +05:30
|
|
|
// @example
|
|
|
|
|
// ```javascript
|
|
|
|
|
// const ssh = require('nuclei/ssh');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// const connected = client.Connect('acme.com', 22, 'username', 'password');
|
|
|
|
|
// ```
|
2025-07-09 14:47:26 -05:00
|
|
|
func (c *SSHClient) Connect(ctx context.Context, host string, port int, username, password string) (bool, error) {
|
|
|
|
|
executionId := ctx.Value("executionId").(string)
|
2023-12-07 19:38:32 +03:00
|
|
|
conn, err := connect(&connectOptions{
|
2025-07-09 14:47:26 -05:00
|
|
|
Host: host,
|
|
|
|
|
Port: port,
|
|
|
|
|
User: username,
|
|
|
|
|
Password: password,
|
|
|
|
|
ExecutionId: executionId,
|
2023-12-07 19:38:32 +03:00
|
|
|
})
|
2023-09-16 16:02:17 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
2024-02-07 21:45:40 +05:30
|
|
|
c.connection = conn
|
2023-09-16 16:02:17 +05:30
|
|
|
|
|
|
|
|
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
|
2024-02-07 21:45:40 +05:30
|
|
|
// @example
|
|
|
|
|
// ```javascript
|
|
|
|
|
// const ssh = require('nuclei/ssh');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// const privateKey = `-----BEGIN RSA PRIVATE KEY----- ...`;
|
|
|
|
|
// const connected = client.ConnectWithKey('acme.com', 22, 'username', privateKey);
|
|
|
|
|
// ```
|
2025-07-09 14:47:26 -05:00
|
|
|
func (c *SSHClient) ConnectWithKey(ctx context.Context, host string, port int, username, key string) (bool, error) {
|
|
|
|
|
executionId := ctx.Value("executionId").(string)
|
2023-12-07 19:38:32 +03:00
|
|
|
conn, err := connect(&connectOptions{
|
2025-07-09 14:47:26 -05:00
|
|
|
Host: host,
|
|
|
|
|
Port: port,
|
|
|
|
|
User: username,
|
|
|
|
|
PrivateKey: key,
|
|
|
|
|
ExecutionId: executionId,
|
2023-12-07 19:38:32 +03:00
|
|
|
})
|
|
|
|
|
|
2023-09-16 16:02:17 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
2024-02-07 21:45:40 +05:30
|
|
|
c.connection = conn
|
2023-09-16 16:02:17 +05:30
|
|
|
|
|
|
|
|
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
|
2024-02-07 21:45:40 +05:30
|
|
|
// @example
|
|
|
|
|
// ```javascript
|
|
|
|
|
// const ssh = require('nuclei/ssh');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// const info = client.ConnectSSHInfoMode('acme.com', 22);
|
|
|
|
|
// log(to_json(info));
|
|
|
|
|
// ```
|
2025-07-09 14:47:26 -05:00
|
|
|
func (c *SSHClient) ConnectSSHInfoMode(ctx context.Context, host string, port int) (*ssh.HandshakeLog, error) {
|
|
|
|
|
executionId := ctx.Value("executionId").(string)
|
2024-03-01 16:10:18 +03:00
|
|
|
return memoizedconnectSSHInfoMode(&connectOptions{
|
2025-07-09 14:47:26 -05:00
|
|
|
Host: host,
|
|
|
|
|
Port: port,
|
|
|
|
|
ExecutionId: executionId,
|
2023-12-07 19:38:32 +03:00
|
|
|
})
|
2023-09-16 16:02:17 +05:30
|
|
|
}
|
|
|
|
|
|
2023-11-23 19:37:45 +01:00
|
|
|
// 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
|
2024-02-07 21:45:40 +05:30
|
|
|
// @example
|
|
|
|
|
// ```javascript
|
|
|
|
|
// const ssh = require('nuclei/ssh');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// client.Connect('acme.com', 22, 'username', 'password');
|
|
|
|
|
// const output = client.Run('id');
|
|
|
|
|
// log(output);
|
|
|
|
|
// ```
|
2023-11-23 19:37:45 +01:00
|
|
|
func (c *SSHClient) Run(cmd string) (string, error) {
|
2024-02-07 21:45:40 +05:30
|
|
|
if c.connection == nil {
|
2025-08-20 05:28:23 +05:30
|
|
|
return "", errkit.New("no connection").Build()
|
2023-11-23 19:37:45 +01:00
|
|
|
}
|
2024-02-07 21:45:40 +05:30
|
|
|
session, err := c.connection.NewSession()
|
2023-11-23 19:37:45 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2025-07-01 00:40:44 +07:00
|
|
|
defer func() {
|
2025-07-09 14:47:26 -05:00
|
|
|
_ = session.Close()
|
|
|
|
|
}()
|
2023-11-23 19:37:45 +01:00
|
|
|
|
|
|
|
|
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
|
2024-02-07 21:45:40 +05:30
|
|
|
// @example
|
|
|
|
|
// ```javascript
|
|
|
|
|
// const ssh = require('nuclei/ssh');
|
2024-03-01 16:38:56 +05:30
|
|
|
// const client = new ssh.SSHClient();
|
2024-02-07 21:45:40 +05:30
|
|
|
// client.Connect('acme.com', 22, 'username', 'password');
|
|
|
|
|
// const closed = client.Close();
|
|
|
|
|
// ```
|
2023-11-23 19:37:45 +01:00
|
|
|
func (c *SSHClient) Close() (bool, error) {
|
2024-02-07 21:45:40 +05:30
|
|
|
if err := c.connection.Close(); err != nil {
|
2023-11-23 19:37:45 +01:00
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-07 19:38:32 +03:00
|
|
|
// unexported functions
|
|
|
|
|
type connectOptions struct {
|
2025-07-09 14:47:26 -05:00
|
|
|
Host string
|
|
|
|
|
Port int
|
|
|
|
|
User string
|
|
|
|
|
Password string
|
|
|
|
|
PrivateKey string
|
|
|
|
|
Timeout time.Duration // default 10s
|
|
|
|
|
ExecutionId string
|
2023-12-07 19:38:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *connectOptions) validate() error {
|
|
|
|
|
if c.Host == "" {
|
2025-08-20 05:28:23 +05:30
|
|
|
return errkit.New("host is required").Build()
|
2023-12-07 19:38:32 +03:00
|
|
|
}
|
|
|
|
|
if c.Port <= 0 {
|
2025-08-20 05:28:23 +05:30
|
|
|
return errkit.New("port is required").Build()
|
2023-12-07 19:38:32 +03:00
|
|
|
}
|
2025-07-09 14:47:26 -05:00
|
|
|
if !protocolstate.IsHostAllowed(c.ExecutionId, c.Host) {
|
2023-09-16 16:02:17 +05:30
|
|
|
// host is not valid according to network policy
|
2025-08-20 05:28:23 +05:30
|
|
|
return protocolstate.ErrHostDenied(c.Host)
|
2023-12-07 19:38:32 +03:00
|
|
|
}
|
|
|
|
|
if c.Timeout == 0 {
|
|
|
|
|
c.Timeout = 10 * time.Second
|
2023-09-16 16:02:17 +05:30
|
|
|
}
|
2023-12-07 19:38:32 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-01 16:10:18 +03:00
|
|
|
// @memo
|
2023-12-07 19:38:32 +03:00
|
|
|
func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
|
|
|
|
|
if err := opts.validate(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 16:02:17 +05:30
|
|
|
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
|
|
|
|
|
}
|
2023-12-07 19:38:32 +03:00
|
|
|
rhost := fmt.Sprintf("%s:%d", opts.Host, opts.Port)
|
2023-09-16 16:02:17 +05:30
|
|
|
client, err := ssh.Dial("tcp", rhost, sshConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2025-07-01 00:40:44 +07:00
|
|
|
defer func() {
|
2025-07-09 14:47:26 -05:00
|
|
|
_ = client.Close()
|
|
|
|
|
}()
|
2023-09-16 16:02:17 +05:30
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-07 19:38:32 +03:00
|
|
|
func connect(opts *connectOptions) (*ssh.Client, error) {
|
|
|
|
|
if err := opts.validate(); err != nil {
|
|
|
|
|
return nil, err
|
2023-09-16 16:02:17 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conf := &ssh.ClientConfig{
|
2023-12-07 19:38:32 +03:00
|
|
|
User: opts.User,
|
|
|
|
|
Auth: []ssh.AuthMethod{},
|
|
|
|
|
Timeout: opts.Timeout,
|
2023-09-16 16:02:17 +05:30
|
|
|
}
|
2023-12-07 19:38:32 +03:00
|
|
|
if len(opts.Password) > 0 {
|
|
|
|
|
conf.Auth = append(conf.Auth, ssh.Password(opts.Password))
|
2023-09-16 16:02:17 +05:30
|
|
|
}
|
2023-12-07 19:38:32 +03:00
|
|
|
if len(opts.PrivateKey) > 0 {
|
|
|
|
|
signer, err := ssh.ParsePrivateKey([]byte(opts.PrivateKey))
|
2023-09-16 16:02:17 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
conf.Auth = append(conf.Auth, ssh.PublicKeys(signer))
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-07 19:38:32 +03:00
|
|
|
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf)
|
2023-09-16 16:02:17 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return client, nil
|
|
|
|
|
}
|