Compare commits

...
Author SHA1 Message Date
Mzack9999 4e80959022 Merge dev 2026-10-26 13:48:11 +01:00
Mzack9999 17e6a46cba Bound timeouts 2026-10-26 07:54:40 +01:00
Mzack9999 a03e867be0 Fix review 2026-10-26 07:50:51 +01:00
Mzack9999 38133c1d7b Merge dev 2026-10-26 07:46:56 +01:00
Mzack9999 c53042cfb3 fix options 2026-08-26 12:56:54 +04:00
Mzack9999 b7fde879cf Merge remote-tracking branch 'origin/dev' into 6447-js-connect-options 2026-08-26 12:55:13 +04:00
Mzack9999 1e5dada8d5 fix timeout 2026-08-17 13:46:39 +04:00
Mzack9999 8b3dbfd1c9 address review 2026-08-17 13:46:05 +04:00
Mzack9999 15b7ee0d6c merge dev 2026-08-17 13:43:00 +04:00
Mzack9999 f28691ad59 lint review 2026-07-26 13:23:23 +04:00
Mzack9999 54e6279130 js options 2026-07-26 13:23:12 +04:00
46 changed files with 1655 additions and 180 deletions

No files matched your search

+7 -6
View File
@@ -53,12 +53,13 @@ func init() {
"FilterWorkstationTrustAccount": lib_ldap.FilterWorkstationTrustAccount,
// Objects / Classes
"Client": lib_ldap.NewClient,
"Config": gojs.GetClassConstructor[lib_ldap.Config](&lib_ldap.Config{}),
"LdapAttributes": gojs.GetClassConstructor[lib_ldap.LdapAttributes](&lib_ldap.LdapAttributes{}),
"LdapEntry": gojs.GetClassConstructor[lib_ldap.LdapEntry](&lib_ldap.LdapEntry{}),
"Metadata": gojs.GetClassConstructor[lib_ldap.Metadata](&lib_ldap.Metadata{}),
"SearchResult": gojs.GetClassConstructor[lib_ldap.SearchResult](&lib_ldap.SearchResult{}),
"AuthenticateOptions": gojs.GetClassConstructor[lib_ldap.AuthenticateOptions](&lib_ldap.AuthenticateOptions{}),
"Client": lib_ldap.NewClient,
"Config": gojs.GetClassConstructor[lib_ldap.Config](&lib_ldap.Config{}),
"LdapAttributes": gojs.GetClassConstructor[lib_ldap.LdapAttributes](&lib_ldap.LdapAttributes{}),
"LdapEntry": gojs.GetClassConstructor[lib_ldap.LdapEntry](&lib_ldap.LdapEntry{}),
"Metadata": gojs.GetClassConstructor[lib_ldap.Metadata](&lib_ldap.Metadata{}),
"SearchResult": gojs.GetClassConstructor[lib_ldap.SearchResult](&lib_ldap.SearchResult{}),
},
).Register()
}
+3 -2
View File
@@ -19,8 +19,9 @@ func init() {
// Var and consts
// Objects / Classes
"MSSQLClient": gojs.GetClassConstructor[lib_mssql.MSSQLClient](&lib_mssql.MSSQLClient{}),
"MSSQLInfo": gojs.GetClassConstructor[lib_mssql.MSSQLInfo](&lib_mssql.MSSQLInfo{}),
"MSSQLClient": gojs.GetClassConstructor[lib_mssql.MSSQLClient](&lib_mssql.MSSQLClient{}),
"MSSQLInfo": gojs.GetClassConstructor[lib_mssql.MSSQLInfo](&lib_mssql.MSSQLInfo{}),
"MSSQLOptions": gojs.GetClassConstructor[lib_mssql.MSSQLOptions](&lib_mssql.MSSQLOptions{}),
},
).Register()
}
+1
View File
@@ -21,6 +21,7 @@ func init() {
// Objects / Classes
"IsOracleResponse": gojs.GetClassConstructor[lib_oracle.IsOracleResponse](&lib_oracle.IsOracleResponse{}),
"OracleClient": gojs.GetClassConstructor[lib_oracle.OracleClient](&lib_oracle.OracleClient{}),
"OracleOptions": gojs.GetClassConstructor[lib_oracle.OracleOptions](&lib_oracle.OracleOptions{}),
},
).Register()
}
+2 -1
View File
@@ -19,7 +19,8 @@ func init() {
// Var and consts
// Objects / Classes
"PGClient": gojs.GetClassConstructor[lib_postgres.PGClient](&lib_postgres.PGClient{}),
"PGClient": gojs.GetClassConstructor[lib_postgres.PGClient](&lib_postgres.PGClient{}),
"PostgresOptions": gojs.GetClassConstructor[lib_postgres.PostgresOptions](&lib_postgres.PostgresOptions{}),
},
).Register()
}
+7 -6
View File
@@ -15,16 +15,17 @@ func init() {
module.Set(
gojs.Objects{
// Functions
"Connect": lib_redis.Connect,
"GetServerInfo": lib_redis.GetServerInfo,
"GetServerInfoAuth": lib_redis.GetServerInfoAuth,
"IsAuthenticated": lib_redis.IsAuthenticated,
"RunLuaScript": lib_redis.RunLuaScript,
"Connect": lib_redis.Connect, //nolint:staticcheck // SA1019: retained for JS API compatibility
"ConnectWithOptions": lib_redis.ConnectWithOptions,
"GetServerInfo": lib_redis.GetServerInfo,
"GetServerInfoAuth": lib_redis.GetServerInfoAuth,
"IsAuthenticated": lib_redis.IsAuthenticated,
"RunLuaScript": lib_redis.RunLuaScript,
// Var and consts
// Objects / Classes
"RedisOptions": gojs.GetClassConstructor[lib_redis.RedisOptions](&lib_redis.RedisOptions{}),
},
).Register()
}
+2 -1
View File
@@ -19,7 +19,8 @@ func init() {
// Var and consts
// Objects / Classes
"SMBClient": gojs.GetClassConstructor[lib_smb.SMBClient](&lib_smb.SMBClient{}),
"SMBClient": gojs.GetClassConstructor[lib_smb.SMBClient](&lib_smb.SMBClient{}),
"SMBOptions": gojs.GetClassConstructor[lib_smb.SMBOptions](&lib_smb.SMBOptions{}),
},
).Register()
}
+2 -1
View File
@@ -19,7 +19,8 @@ func init() {
// Var and consts
// Objects / Classes
"SSHClient": gojs.GetClassConstructor[lib_ssh.SSHClient](&lib_ssh.SSHClient{}),
"SSHClient": gojs.GetClassConstructor[lib_ssh.SSHClient](&lib_ssh.SSHClient{}),
"SSHOptions": gojs.GetClassConstructor[lib_ssh.SSHOptions](&lib_ssh.SSHOptions{}),
},
).Register()
}
+1
View File
@@ -35,6 +35,7 @@ func init() {
"IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}),
"TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}),
"TelnetInfoResponse": gojs.GetClassConstructor[lib_telnet.TelnetInfoResponse](&lib_telnet.TelnetInfoResponse{}),
"TelnetOptions": gojs.GetClassConstructor[lib_telnet.TelnetOptions](&lib_telnet.TelnetOptions{}),
},
).Register()
}
+1
View File
@@ -22,6 +22,7 @@ func init() {
// Objects / Classes
"IsVNCResponse": gojs.GetClassConstructor[lib_vnc.IsVNCResponse](&lib_vnc.IsVNCResponse{}),
"VNCClient": gojs.GetClassConstructor[lib_vnc.VNCClient](&lib_vnc.VNCClient{}),
"VNCOptions": gojs.GetClassConstructor[lib_vnc.VNCOptions](&lib_vnc.VNCOptions{}),
},
).Register()
}
+23
View File
@@ -404,6 +404,14 @@ export class Client {
}
/**
* AuthenticateWithOptions authenticates using a password or an NTLM hash.
*/
public AuthenticateWithOptions(opts: AuthenticateOptions): boolean {
return false;
}
/**
* AuthenticateWithNTLMHash authenticates with the ldap server using the given username and NTLM hash
* @example
@@ -507,6 +515,21 @@ export interface Config {
/**
* AuthenticateOptions defines credentials for LDAP authentication. Hash takes
* precedence over Password when both are provided.
*/
export interface AuthenticateOptions {
Username?: string,
Password?: string,
Hash?: string,
}
/**
* LdapAttributes represents all LDAP attributes of a particular
* ldap entry
+28
View File
@@ -46,6 +46,14 @@ export class MSSQLClient {
}
/**
* ConnectWithOptions connects to MS SQL using the supplied connection options.
*/
public ConnectWithOptions(opts: MSSQLOptions): boolean | null {
return null;
}
/**
* IsMssql checks if the given host is running MS SQL database.
* If the host is running MS SQL database, it returns true.
@@ -95,6 +103,26 @@ export class MSSQLClient {
/**
* MSSQLOptions defines the connection options for an MS SQL database.
*/
export interface MSSQLOptions {
Host?: string,
Port?: number,
Username?: string,
Password?: string,
DbName?: string,
Timeout?: number,
}
/**
* SQLResult Interface
*/
+8
View File
@@ -63,6 +63,14 @@ export class MySQLClient {
}
/**
* ConnectWithOptions connects to MySQL using the supplied connection options.
*/
public ConnectWithOptions(opts: MySQLOptions): boolean | null {
return null;
}
/**
* returns MySQLInfo when fingerprint is successful
* @example
+18
View File
@@ -41,6 +41,13 @@ export class OracleClient {
return null;
}
/**
* ConnectWithOptions connects to Oracle using the supplied connection options.
*/
public ConnectWithOptions(opts: OracleOptions): boolean | null {
return null;
}
/**
* ConnectWithDSN connects to an Oracle database using a DSN string
* @example
@@ -96,6 +103,17 @@ export class OracleClient {
}
}
/**
* OracleOptions defines the connection options for an Oracle database.
*/
export interface OracleOptions {
Host?: string,
Port?: number,
ServiceName?: string,
Username?: string,
Password?: string,
}
/**
* SQLResult Interface
*/
+30
View File
@@ -46,6 +46,14 @@ export class PGClient {
}
/**
* ConnectWithOptions connects to Postgres using the supplied connection options.
*/
public ConnectWithOptions(opts: PostgresOptions): boolean | null {
return null;
}
/**
* ExecuteQuery connects to Postgres database using given credentials and database name.
* and executes a query on the db.
@@ -84,6 +92,28 @@ export class PGClient {
/**
* PostgresOptions defines the connection options for a Postgres database.
*/
export interface PostgresOptions {
Host?: string,
Port?: number,
Username?: string,
Password?: string,
DbName?: string,
Timeout?: number,
SSLMode?: string,
}
/**
* SQLResult Interface
*/
+37
View File
@@ -14,6 +14,43 @@ export function Connect(host: string, port: number, password: string): boolean |
/**
* ConnectWithOptions tries to connect to Redis using the supplied options.
* @example
* ```javascript
* const redis = require('nuclei/redis');
* const opts = new redis.RedisOptions();
* opts.Host = 'acme.com';
* opts.Port = 6379;
* opts.Password = 'password';
* opts.DB = 1;
* const connected = redis.ConnectWithOptions(opts);
* ```
*/
export function ConnectWithOptions(opts: RedisOptions): boolean | null {
return null;
}
/**
* RedisOptions defines the connection options for a Redis server.
*/
export interface RedisOptions {
Host?: string,
Port?: number,
Password?: string,
DB?: number,
Timeout?: number,
}
/**
* GetServerInfo returns the server info for a redis server
* @example
+52
View File
@@ -149,6 +149,26 @@ export class SMBClient {
return null;
}
/**
* ListSharesWithOptions lists SMB share names using SMBOptions.
* Supports domain, pass-the-hash, workstation, and SPN overrides.
* @example
* ```javascript
* const smb = require('nuclei/smb');
* const client = new smb.SMBClient();
* const opts = new smb.SMBOptions();
* opts.Host = 'acme.com';
* opts.Port = 445;
* opts.User = 'Administrator';
* opts.Domain = 'ACME';
* const shares = client.ListSharesWithOptions(opts);
* ```
*/
public ListSharesWithOptions(opts: SMBOptions): string[] | null {
return null;
}
/**
* DetectSMBGhost tries to detect SMBGhost vulnerability
* by using SMBv3 compression feature.
@@ -204,6 +224,38 @@ export interface ProtocolInfo {
/**
* SMBOptions represents configuration for authenticated SMB operations.
* @example
* ```javascript
* const smb = require('nuclei/smb');
* const opts = new smb.SMBOptions();
* opts.Host = 'acme.com';
* opts.Port = 445;
* opts.User = 'Administrator';
* opts.Password = 'password';
* opts.Domain = 'ACME';
* ```
*/
export interface SMBOptions {
Host?: string,
Port?: number,
User?: string,
Password?: string,
Hash?: string,
Domain?: string,
Timeout?: number,
}
/**
* HeaderLog Interface
*/
+56
View File
@@ -28,6 +28,27 @@ export class SSHClient {
}
/**
* ConnectWithOptions tries to connect using the provided SSHOptions.
* Prefer this over Connect/ConnectWithKey when setting timeout, client version,
* or combining password and private key auth.
* @example
* ```javascript
* const ssh = require('nuclei/ssh');
* const client = new ssh.SSHClient();
* const opts = new ssh.SSHOptions();
* opts.Host = 'acme.com';
* opts.Port = 22;
* opts.User = 'username';
* opts.Password = 'password';
* const connected = client.ConnectWithOptions(opts);
* ```
*/
public ConnectWithOptions(opts: SSHOptions): boolean | null {
return null;
}
/**
* Connect tries to connect to provided host and port
* with provided username and password with ssh.
@@ -124,6 +145,41 @@ export class SSHClient {
/**
* SSHOptions represents configuration options for an SSH connection.
* Use ConnectWithOptions when you need protocol-specific knobs beyond
* username/password (timeout, client version, private key).
* @example
* ```javascript
* const ssh = require('nuclei/ssh');
* const opts = new ssh.SSHOptions();
* opts.Host = 'acme.com';
* opts.Port = 22;
* opts.User = 'username';
* opts.Password = 'password';
* opts.Timeout = 15;
* opts.ClientVersion = 'SSH-2.0-OpenSSH_8.9';
* ```
*/
export interface SSHOptions {
Host?: string,
Port?: number,
User?: string,
Password?: string,
PrivateKey?: string,
Timeout?: number,
ClientVersion?: string,
}
/**
* Algorithms Interface
*/
+23
View File
@@ -39,6 +39,13 @@ export class TelnetClient {
return false;
}
/**
* ConnectWithOptions connects to Telnet using the supplied connection options.
*/
public ConnectWithOptions(opts: TelnetOptions): boolean {
return false;
}
/**
* Info gathers information about the telnet server including encryption support.
* Uses the telnetmini library's DetectEncryption helper function.
@@ -90,6 +97,22 @@ export interface IsTelnetResponse {
Banner?: string,
}
/**
* TelnetOptions defines the connection options for a Telnet server.
*/
export interface TelnetOptions {
Host?: string,
Port?: number,
Username?: string,
Password?: string,
Timeout?: number,
}
/**
* TelnetInfoResponse is the response from the Info function.
* @example
+21
View File
@@ -62,5 +62,26 @@ export class VNCClient {
public Connect(host: string, port: number, password: string): boolean | null {
return null;
}
/**
* ConnectWithOptions connects to VNC using the supplied connection options.
*/
public ConnectWithOptions(opts: VNCOptions): boolean | null {
return null;
}
}
/**
* VNCOptions defines the connection options for a VNC server.
*/
export interface VNCOptions {
Host?: string,
Port?: number,
Password?: string,
Timeout?: number,
}
+30 -13
View File
@@ -61,6 +61,14 @@ type (
ServerName string // default to host (when using tls)
Upgrade bool // when true first connects to non-tls and then upgrades to tls
}
// AuthenticateOptions defines credentials for LDAP authentication. Hash takes
// precedence over Password when both are provided.
AuthenticateOptions struct {
Username string
Password string
Hash string
}
)
// Constructor for creating a new ldap client
@@ -168,6 +176,8 @@ func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object {
// Authenticate authenticates with the ldap server using the given username and password
// performs NTLMBind first and then Bind/UnauthenticatedBind if NTLMBind fails
//
// Deprecated: prefer AuthenticateWithOptions for new templates.
// @example
// ```javascript
// const ldap = require('nuclei/ldap');
@@ -175,24 +185,35 @@ func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object {
// client.Authenticate('user', 'password');
// ```
func (c *Client) Authenticate(username, password string) bool {
return c.AuthenticateWithOptions(AuthenticateOptions{Username: username, Password: password})
}
// AuthenticateWithOptions authenticates using a password or an NTLM hash.
func (c *Client) AuthenticateWithOptions(opts AuthenticateOptions) bool {
c.nj.Require(c.conn != nil, "no existing connection")
if c.BaseDN == "" {
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
}
if err := c.conn.NTLMBind(c.Realm, username, password); err == nil {
if authenticationUsesHash(opts) {
if err := c.conn.NTLMBindWithHash(c.Realm, opts.Username, opts.Hash); err != nil {
c.nj.ThrowError(err)
}
return true
}
if err := c.conn.NTLMBind(c.Realm, opts.Username, opts.Password); err == nil {
// if bind with NTLMBind(), there is nothing
// else to do, you are authenticated
return true
}
var err error
switch password {
switch opts.Password {
case "":
if err = c.conn.UnauthenticatedBind(username); err != nil {
if err = c.conn.UnauthenticatedBind(opts.Username); err != nil {
c.nj.ThrowError(err)
}
default:
if err = c.conn.Bind(username, password); err != nil {
if err = c.conn.Bind(opts.Username, opts.Password); err != nil {
c.nj.ThrowError(err)
}
}
@@ -207,15 +228,11 @@ func (c *Client) Authenticate(username, password string) bool {
// client.AuthenticateWithNTLMHash('pdtm', 'hash');
// ```
func (c *Client) AuthenticateWithNTLMHash(username, hash string) bool {
c.nj.Require(c.conn != nil, "no existing connection")
if c.BaseDN == "" {
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
}
var err error
if err = c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil {
c.nj.ThrowError(err)
}
return err == nil
return c.AuthenticateWithOptions(AuthenticateOptions{Username: username, Hash: hash})
}
func authenticationUsesHash(opts AuthenticateOptions) bool {
return opts.Hash != ""
}
// Search accepts whatever filter and returns a list of maps having provided attributes
+12
View File
@@ -173,3 +173,15 @@ func requireNetworkPolicyError(t *testing.T, err error, target string) {
t.Fatalf("expected network-policy denial for %q, got %q", target, err)
}
}
func TestAuthenticateOptionsPreferHash(t *testing.T) {
if !authenticationUsesHash(AuthenticateOptions{Username: "user", Hash: "ntlm-hash"}) {
t.Fatal("expected hash authentication mode")
}
if !authenticationUsesHash(AuthenticateOptions{Username: "user", Password: "password", Hash: "ntlm-hash"}) {
t.Fatal("expected hash to take precedence over password")
}
if authenticationUsesHash(AuthenticateOptions{Username: "user", Password: "password"}) {
t.Fatal("expected password authentication mode")
}
}
+98 -14
View File
@@ -9,7 +9,7 @@ import (
"net/url"
"strings"
_ "github.com/microsoft/go-mssqldb"
mssqldb "github.com/microsoft/go-mssqldb"
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)
@@ -23,9 +23,33 @@ type (
// const client = new mssql.MSSQLClient;
// ```
MSSQLClient struct{}
// MSSQLOptions defines the connection options for an MS SQL database.
// @example
// ```javascript
// const mssql = require('nuclei/mssql');
// const opts = new mssql.MSSQLOptions();
// opts.Host = 'acme.com';
// opts.Port = 1433;
// opts.Username = 'sa';
// opts.Password = 'password';
// opts.DbName = 'master';
// opts.Timeout = 15;
// const connected = client.ConnectWithOptions(opts);
// ```
MSSQLOptions struct {
Host string // Host is the hostname or IP of the MS SQL server.
Port int // Port is the MS SQL port (usually 1433).
Username string // Username is the login name.
Password string // Password is the login password.
DbName string // DbName is the database name (default master when using Connect).
Timeout int // Timeout is the connection timeout in seconds (default 30).
}
)
// Connect connects to MS SQL database using given credentials.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// If connection is successful, it returns true.
// If connection is unsuccessful, it returns false and error.
// The connection is closed after the function returns.
@@ -41,6 +65,8 @@ func (c *MSSQLClient) Connect(ctx context.Context, host string, port int, userna
}
// ConnectWithDB connects to MS SQL database using given credentials and database name.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// If connection is successful, it returns true.
// If connection is unsuccessful, it returns false and error.
// The connection is closed after the function returns.
@@ -55,21 +81,47 @@ func (c *MSSQLClient) ConnectWithDB(ctx context.Context, host string, port int,
return memoizedconnect(ctx, executionId, host, port, username, password, dbName)
}
// ConnectWithOptions connects to MS SQL using the supplied connection options.
// Prefer this over Connect/ConnectWithDB when setting timeout or database name together.
// @example
// ```javascript
// const mssql = require('nuclei/mssql');
// const client = new mssql.MSSQLClient;
// const opts = new mssql.MSSQLOptions();
// opts.Host = 'acme.com';
// opts.Port = 1433;
// opts.Username = 'sa';
// opts.Password = 'password';
// opts.DbName = 'master';
// opts.Timeout = 15;
// const connected = client.ConnectWithOptions(opts);
// ```
func (c *MSSQLClient) ConnectWithOptions(ctx context.Context, opts MSSQLOptions) (bool, error) {
executionId := ctx.Value("executionId").(string)
return connectWithOptions(ctx, executionId, opts)
}
// @memo
func connect(ctx context.Context, executionId string, host string, port int, username string, password string, dbName string) (bool, error) {
if host == "" || port <= 0 {
return connectWithOptions(ctx, executionId, MSSQLOptions{
Host: host, Port: port, Username: username, Password: password, DbName: dbName,
})
}
func connectWithOptions(ctx context.Context, executionId string, opts MSSQLOptions) (bool, error) {
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if !protocolstate.IsHostAllowed(executionId, host) {
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(host)
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
target := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port))
connString := mssqlConnString(target, username, password, dbName)
connString := mssqlConnString(target, opts)
db, err := sql.Open("sqlserver", connString)
db, err := openDB(executionId, connString)
if err != nil {
return false, err
}
@@ -156,9 +208,11 @@ func (c *MSSQLClient) ExecuteQuery(ctx context.Context, host string, port int, u
}
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
connString := mssqlConnString(target, username, password, dbName)
connString := mssqlConnString(target, MSSQLOptions{
Username: username, Password: password, DbName: dbName,
})
db, err := sql.Open("sqlserver", connString)
db, err := openDB(executionId, connString)
if err != nil {
return nil, err
}
@@ -184,10 +238,40 @@ func (c *MSSQLClient) ExecuteQuery(ctx context.Context, host string, port int, u
return data, nil
}
func mssqlConnString(target, username, password, dbName string) string {
return fmt.Sprintf("sqlserver://%s:%s@%s?database=%s&connection+timeout=30",
url.PathEscape(username),
url.PathEscape(password),
func mssqlConnString(target string, opts MSSQLOptions) string {
timeout := opts.Timeout
if timeout <= 0 {
timeout = 30
}
return fmt.Sprintf("sqlserver://%s:%s@%s?database=%s&connection+timeout=%d",
url.PathEscape(opts.Username),
url.PathEscape(opts.Password),
target,
url.QueryEscape(dbName))
url.QueryEscape(opts.DbName),
timeout)
}
// openDB opens an MSSQL database using nuclei's policy-aware fastdialer.
func openDB(executionId, connString string) (*sql.DB, error) {
connector, err := mssqldb.NewConnector(connString)
if err != nil {
return nil, err
}
connector.Dialer = mssqlDialer{executionId: executionId}
return sql.OpenDB(connector), nil
}
type mssqlDialer struct {
executionId string
}
func (d mssqlDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
if !protocolstate.IsHostAllowed(d.executionId, address) {
return nil, protocolstate.ErrHostDenied.Msgf(address)
}
dialers := protocolstate.GetDialersWithId(d.executionId)
if dialers == nil {
return nil, fmt.Errorf("dialers not initialized for %s", d.executionId)
}
return dialers.Fastdialer.Dial(ctx, network, address)
}
+33 -2
View File
@@ -1,9 +1,13 @@
package mssql
import (
"context"
"strings"
"testing"
"github.com/microsoft/go-mssqldb/msdsn"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
@@ -14,7 +18,9 @@ func TestConnectionStringDoesNotTreatDatabaseNameAsDriverOptions(t *testing.T) {
"&krb5-keytabfile=/tmp/krb5.keytab" +
"&krb5-credcachefile=/tmp/krb5.ccache"
cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", "user", "password", dbName))
cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", MSSQLOptions{
Username: "user", Password: "password", DbName: dbName,
}))
require.NoError(t, err)
require.Equal(t, dbName, cfg.Database)
@@ -27,9 +33,34 @@ func TestConnectionStringDoesNotTreatDatabaseNameAsDriverOptions(t *testing.T) {
require.NotContains(t, cfg.Parameters, "krb5-credcachefile")
}
func TestConnectWithOptionsValidatesTarget(t *testing.T) {
connected, err := connectWithOptions(context.Background(), "test", MSSQLOptions{Port: 1433})
require.False(t, connected)
require.EqualError(t, err, "invalid host or port")
}
func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
require.NoError(t, protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}))
t.Cleanup(func() { protocolstate.Close(executionID) })
connected, err := connectWithOptions(context.Background(), executionID, MSSQLOptions{
Host: "127.0.0.1", Port: 1433,
})
require.False(t, connected)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "network policy") && strings.Contains(err.Error(), "127.0.0.1"))
}
func TestConnectionStringKeepsPlainDatabaseName(t *testing.T) {
cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", "user", "password", "master"))
cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", MSSQLOptions{
Username: "user", Password: "password", DbName: "master", Timeout: 12,
}))
require.NoError(t, err)
require.Equal(t, "master", cfg.Database)
require.Equal(t, "12", cfg.Parameters["connection timeout"])
}
+26 -16
View File
@@ -48,6 +48,8 @@ func isMySQL(ctx context.Context, executionId string, host string, port int) (bo
}
// Connect connects to MySQL database using given credentials.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// If connection is successful, it returns true.
// If connection is unsuccessful, it returns false and error.
// The connection is closed after the function returns.
@@ -58,22 +60,7 @@ func isMySQL(ctx context.Context, executionId string, host string, port int) (bo
// const connected = client.Connect('acme.com', 3306, 'username', 'password');
// ```
func (c *MySQLClient) Connect(ctx context.Context, host string, port int, username, password string) (bool, error) {
executionId := ctx.Value("executionId").(string)
if !protocolstate.IsHostAllowed(executionId, host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(host)
}
// executing queries implies the remote mysql service
ok, err := c.IsMySQL(ctx, host, port)
if err != nil {
return false, err
}
if !ok {
return false, fmt.Errorf("not a mysql service")
}
dsn, err := BuildDSN(MySQLOptions{
return c.ConnectWithOptions(ctx, MySQLOptions{
Host: host,
Port: port,
DbName: "INFORMATION_SCHEMA",
@@ -81,6 +68,29 @@ func (c *MySQLClient) Connect(ctx context.Context, host string, port int, userna
Username: username,
Password: password,
})
}
// ConnectWithOptions connects to MySQL using the supplied connection options.
func (c *MySQLClient) ConnectWithOptions(ctx context.Context, opts MySQLOptions) (bool, error) {
executionId := ctx.Value("executionId").(string)
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
// executing queries implies the remote mysql service
ok, err := c.IsMySQL(ctx, opts.Host, opts.Port)
if err != nil {
return false, err
}
if !ok {
return false, fmt.Errorf("not a mysql service")
}
dsn, err := BuildDSN(opts)
if err != nil {
return false, err
}
+31 -5
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/url"
"strings"
"time"
"github.com/go-sql-driver/mysql"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
@@ -51,11 +52,14 @@ func BuildDSN(opts MySQLOptions) (string, error) {
if opts.Protocol == "" {
opts.Protocol = "tcp"
}
// We're going to use a custom dialer when creating MySQL connections, so if we've been
// given "tcp" as the protocol, then quietly switch it to "nucleitcp", which we have
// already registered.
if opts.Protocol == "tcp" {
switch opts.Protocol {
case "tcp", "nucleitcp":
// We're going to use a custom dialer when creating MySQL connections, so if we've been
// given "tcp" as the protocol, then quietly switch it to "nucleitcp", which we have
// already registered with fastdialer + network policy.
opts.Protocol = "nucleitcp"
default:
return "", fmt.Errorf("unsupported mysql protocol %q (only tcp is allowed)", opts.Protocol)
}
if opts.DbName == "" {
opts.DbName = "/"
@@ -73,13 +77,22 @@ func BuildDSN(opts MySQLOptions) (string, error) {
if opts.RawQuery != "" {
dsn.WriteString(opts.RawQuery)
}
return dsn.String(), nil
config, err := mysql.ParseDSN(dsn.String())
if err != nil {
return "", err
}
if opts.Timeout > 0 {
config.Timeout = time.Duration(opts.Timeout) * time.Second
}
return config.FormatDSN(), nil
}
// sandboxDSN enforces the local file access sandbox on a MySQL DSN. The
// driver's allowAllFiles option lets a malicious server read any local file
// off the host via LOAD DATA LOCAL INFILE, so it is only honored when -lfa is
// enabled, mirroring the fs.ReadFile restriction.
// It also rewrites the network to nucleitcp so ConnectWithDSN cannot bypass
// fastdialer / network policy via a raw tcp or unix DSN.
func sandboxDSN(dsn string, lfaAllowed bool) (string, error) {
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
@@ -88,6 +101,12 @@ func sandboxDSN(dsn string, lfaAllowed bool) (string, error) {
if cfg.AllowAllFiles && !lfaAllowed {
cfg.AllowAllFiles = false
}
switch cfg.Net {
case "", "tcp", "nucleitcp":
cfg.Net = "nucleitcp"
default:
return "", fmt.Errorf("unsupported mysql protocol %q (only tcp is allowed)", cfg.Net)
}
return cfg.FormatDSN(), nil
}
@@ -97,6 +116,13 @@ func openDB(executionId, dsn string) (*sql.DB, error) {
if err != nil {
return nil, err
}
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
return nil, err
}
if !protocolstate.IsHostAllowed(executionId, cfg.Addr) {
return nil, protocolstate.ErrHostDenied.Msgf(cfg.Addr)
}
return sql.Open("mysql", dsn)
}
+61
View File
@@ -2,11 +2,72 @@ package mysql
import (
"testing"
"time"
"github.com/go-sql-driver/mysql"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestBuildDSNMapsOptions(t *testing.T) {
dsn, err := BuildDSN(MySQLOptions{
Host: "127.0.0.1", Port: 3306, Username: "user", Password: "password",
DbName: "app", Timeout: 7, RawQuery: "?tls=skip-verify",
})
require.NoError(t, err)
cfg, err := mysql.ParseDSN(dsn)
require.NoError(t, err)
require.Equal(t, "nucleitcp", cfg.Net)
require.Equal(t, "127.0.0.1:3306", cfg.Addr)
require.Equal(t, "app", cfg.DBName)
require.Equal(t, 7*time.Second, cfg.Timeout)
require.Equal(t, "skip-verify", cfg.TLSConfig)
}
func TestBuildDSNRejectsMissingHostOrPort(t *testing.T) {
_, err := BuildDSN(MySQLOptions{Port: 3306})
require.EqualError(t, err, "invalid host or port")
_, err = BuildDSN(MySQLOptions{Host: "127.0.0.1"})
require.EqualError(t, err, "invalid host or port")
}
func TestBuildDSNRejectsNonTCPProtocol(t *testing.T) {
_, err := BuildDSN(MySQLOptions{Host: "127.0.0.1", Port: 3306, Protocol: "unix"})
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported mysql protocol")
}
func TestSandboxDSNForcesNucleiTCP(t *testing.T) {
got, err := sandboxDSN("root:x@tcp(127.0.0.1:3306)/", false)
require.NoError(t, err)
cfg, err := mysql.ParseDSN(got)
require.NoError(t, err)
require.Equal(t, "nucleitcp", cfg.Net)
}
func TestSandboxDSNRejectsUnixSocket(t *testing.T) {
_, err := sandboxDSN("root:x@unix(/var/run/mysqld.sock)/", false)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported mysql protocol")
}
func TestOpenDBDeniesRestrictedLocalHost(t *testing.T) {
executionID := "mysql-open-deny-" + t.Name()
require.NoError(t, protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}))
t.Cleanup(func() { protocolstate.Close(executionID) })
_, err := openDB(executionID, "root:x@tcp(127.0.0.1:3306)/")
require.Error(t, err)
require.Contains(t, err.Error(), "network policy")
require.Contains(t, err.Error(), "127.0.0.1")
}
func TestSandboxDSN(t *testing.T) {
t.Run("strips allowAllFiles when lfa disabled", func(t *testing.T) {
got, err := sandboxDSN("root:x@nucleitcp(127.0.0.1:3306)/?allowAllFiles=true", false)
+59 -10
View File
@@ -39,6 +39,15 @@ type (
OracleClient struct {
connector *goora.OracleConnector
}
// OracleOptions defines the connection options for an Oracle database.
OracleOptions struct {
Host string
Port int
ServiceName string
Username string
Password string
}
)
// IsOracle checks if a host is running an Oracle server
@@ -56,6 +65,9 @@ func (c *OracleClient) IsOracle(ctx context.Context, host string, port int) (IsO
// @memo
func isOracle(ctx context.Context, executionId string, host string, port int) (IsOracleResponse, error) {
resp := IsOracleResponse{}
if !protocolstate.IsHostAllowed(executionId, host) {
return resp, protocolstate.ErrHostDenied.Msgf(host)
}
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
@@ -90,15 +102,24 @@ func (c *OracleClient) oracleDbInstance(ctx context.Context, connStr string, exe
if err != nil {
return nil, err
}
if c.connector == nil {
connector := goora.NewConnector(connStr)
oraConnector, ok := connector.(*goora.OracleConnector)
if !ok {
return nil, fmt.Errorf("failed to cast connector to OracleConnector")
}
c.connector = oraConnector
config, err := goora.ParseConfig(connStr)
if err != nil {
return nil, err
}
for _, server := range config.Servers {
if !protocolstate.IsHostAllowed(executionId, server.Addr) {
return nil, protocolstate.ErrHostDenied.Msgf(server.Addr)
}
}
connector := goora.NewConnector(connStr)
oraConnector, ok := connector.(*goora.OracleConnector)
if !ok {
return nil, fmt.Errorf("failed to cast connector to OracleConnector")
}
// A connector embeds its DSN, so replace it for each invocation to ensure
// option changes and the sandboxed DSN are applied to the actual dial.
c.connector = oraConnector
// Refresh the dialer on every call so the connector uses the current
// execution context instead of a stale or already-canceled one.
@@ -149,7 +170,9 @@ func isOracleTracePathOption(key string) bool {
}
}
// Connect connects to an Oracle database
// Connect connects to an Oracle database.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// @example
// ```javascript
// const oracle = require('nuclei/oracle');
@@ -157,8 +180,24 @@ func isOracleTracePathOption(key string) bool {
// client.Connect('acme.com', 1521, 'XE', 'user', 'password');
// ```
func (c *OracleClient) Connect(ctx context.Context, host string, port int, serviceName string, username string, password string) (bool, error) {
connStr := goora.BuildUrl(host, port, serviceName, username, password, nil)
return c.ConnectWithOptions(ctx, OracleOptions{
Host: host, Port: port, ServiceName: serviceName, Username: username, Password: password,
})
}
// ConnectWithOptions connects to Oracle using the supplied connection options.
func (c *OracleClient) ConnectWithOptions(ctx context.Context, opts OracleOptions) (bool, error) {
executionId := ctx.Value("executionId").(string)
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
connStr, err := buildOracleDSN(opts)
if err != nil {
return false, err
}
return c.ConnectWithDSN(ctx, connStr)
}
@@ -187,6 +226,16 @@ func (c *OracleClient) ConnectWithDSN(ctx context.Context, dsn string) (bool, er
return true, nil
}
func buildOracleDSN(opts OracleOptions) (string, error) {
if opts.Host == "" || opts.Port <= 0 {
return "", fmt.Errorf("invalid host or port")
}
if opts.ServiceName == "" {
return "", fmt.Errorf("service name cannot be empty")
}
return goora.BuildUrl(opts.Host, opts.Port, opts.ServiceName, opts.Username, opts.Password, nil), nil
}
// ExecuteQuery connects to MS SQL database using given credentials and executes a query.
// It returns the results of the query or an error if something goes wrong.
// @example
+39
View File
@@ -100,3 +100,42 @@ func restoreOracleTemplatesDir(t *testing.T, templatesDir string) {
config.DefaultConfig.SetTemplatesDir(oldTemplatesDir)
})
}
func TestBuildOracleDSNMapsOptions(t *testing.T) {
dsn, err := buildOracleDSN(OracleOptions{
Host: "127.0.0.1", Port: 1521, ServiceName: "XE", Username: "user", Password: "pass",
})
require.NoError(t, err)
cfg, err := go_ora.ParseConfig(dsn)
require.NoError(t, err)
require.Len(t, cfg.Servers, 1)
require.Equal(t, "127.0.0.1", cfg.Servers[0].Addr)
require.Equal(t, 1521, cfg.Servers[0].Port)
require.Equal(t, "XE", cfg.ServiceName)
}
func TestBuildOracleDSNValidatesTarget(t *testing.T) {
_, err := buildOracleDSN(OracleOptions{Port: 1521, ServiceName: "XE"})
require.EqualError(t, err, "invalid host or port")
_, err = buildOracleDSN(OracleOptions{Host: "127.0.0.1", Port: 1521})
require.EqualError(t, err, "service name cannot be empty")
}
func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
require.NoError(t, protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}))
t.Cleanup(func() { protocolstate.Close(executionID) })
ctx := context.WithValue(context.Background(), "executionId", executionID) // nolint:staticcheck
connected, err := (&OracleClient{}).ConnectWithOptions(ctx, OracleOptions{
Host: "127.0.0.1", Port: 1521, ServiceName: "XE", Username: "user", Password: "pass",
})
require.False(t, connected)
require.Error(t, err)
require.Contains(t, err.Error(), "network policy")
require.Contains(t, err.Error(), "127.0.0.1")
}
+102 -13
View File
@@ -2,6 +2,7 @@ package postgres
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
@@ -25,6 +26,17 @@ type (
// const client = new postgres.PGClient;
// ```
PGClient struct{}
// PostgresOptions defines the connection options for a Postgres database.
PostgresOptions struct {
Host string
Port int
Username string
Password string
DbName string
Timeout int // Timeout is in seconds.
SSLMode string
}
)
// IsPostgres checks if the given host and port are running Postgres database.
@@ -45,6 +57,9 @@ func (c *PGClient) IsPostgres(ctx context.Context, host string, port int) (bool,
func isPostgres(ctx context.Context, executionId string, host string, port int) (bool, error) {
timeout := 10 * time.Second
if !protocolstate.IsHostAllowed(executionId, host) {
return false, protocolstate.ErrHostDenied.Msgf(host)
}
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return false, fmt.Errorf("dialers not initialized for %s", executionId)
@@ -72,6 +87,8 @@ func isPostgres(ctx context.Context, executionId string, host string, port int)
}
// Connect connects to Postgres database using given credentials.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// If connection is successful, it returns true.
// If connection is unsuccessful, it returns false and error.
// The connection is closed after the function returns.
@@ -93,6 +110,25 @@ func (c *PGClient) Connect(ctx context.Context, host string, port int, username
return memoizedconnect(ctx, executionId, host, port, username, password, "postgres")
}
// ConnectWithOptions connects to Postgres using the supplied connection options.
func (c *PGClient) ConnectWithOptions(ctx context.Context, opts PostgresOptions) (bool, error) {
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if _, err := postgresTLSConfig(opts.SSLMode, opts.Host); err != nil {
return false, err
}
ok, err := c.IsPostgres(ctx, opts.Host, opts.Port)
if err != nil {
return false, err
}
if !ok {
return false, fmt.Errorf("not a postgres service")
}
executionId := ctx.Value("executionId").(string)
return connectWithOptions(ctx, executionId, opts)
}
// ExecuteQuery connects to Postgres database using given credentials and database name.
// and executes a query on the db.
// If connection is successful, it returns the result of the query.
@@ -147,19 +183,31 @@ func executeQuery(ctx context.Context, executionId string, host string, port int
}
func buildPostgresConnURL(username, password, target, dbName, executionId string) string {
return buildPostgresConnURLWithOptions(PostgresOptions{
Username: username, Password: password, DbName: dbName,
}, target, executionId)
}
func buildPostgresConnURLWithOptions(opts PostgresOptions, target, executionId string) string {
values := url.Values{}
values.Set("sslmode", "disable")
sslMode := opts.SSLMode
if sslMode == "" {
sslMode = "disable"
}
values.Set("sslmode", sslMode)
values.Set("executionId", executionId)
return fmt.Sprintf("postgres://%s@%s/%s?%s",
url.UserPassword(username, password).String(),
url.UserPassword(opts.Username, opts.Password).String(),
target,
url.PathEscape(dbName),
url.PathEscape(opts.DbName),
values.Encode(),
)
}
// ConnectWithDB connects to Postgres database using given credentials and database name.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// If connection is successful, it returns true.
// If connection is unsuccessful, it returns false and error.
// The connection is closed after the function returns.
@@ -185,16 +233,26 @@ func (c *PGClient) ConnectWithDB(ctx context.Context, host string, port int, use
// @memo
func connect(ctx context.Context, executionId string, host string, port int, username string, password string, dbName string) (bool, error) {
if host == "" || port <= 0 {
return connectWithOptions(ctx, executionId, PostgresOptions{
Host: host, Port: port, Username: username, Password: password, DbName: dbName,
})
}
func connectWithOptions(ctx context.Context, executionId string, opts PostgresOptions) (bool, error) {
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if !protocolstate.IsHostAllowed(executionId, host) {
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(host)
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
target := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port))
tlsConfig, err := postgresTLSConfig(opts.SSLMode, opts.Host)
if err != nil {
return false, err
}
execCtx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -205,21 +263,25 @@ func connect(ctx context.Context, executionId string, host string, port int, use
}
db := pg.Connect(&pg.Options{
Addr: target,
User: username,
Password: password,
Database: dbName,
Addr: target,
User: opts.Username,
Password: opts.Password,
Database: opts.DbName,
TLSConfig: tlsConfig,
DialTimeout: postgresTimeout(opts.Timeout),
ReadTimeout: postgresTimeout(opts.Timeout),
WriteTimeout: postgresTimeout(opts.Timeout),
Dialer: func(dialCtx context.Context, network, addr string) (net.Conn, error) {
return dialer.Fastdialer.Dial(dialCtx, network, addr)
},
IdleCheckFrequency: -1,
}).WithTimeout(10 * time.Second)
}).WithTimeout(postgresTimeout(opts.Timeout))
defer func() {
_ = db.Close()
}()
_, err := db.ExecContext(execCtx, "select 1")
_, err = db.ExecContext(execCtx, "select 1")
if err != nil {
switch true {
case strings.Contains(err.Error(), "connect: connection refused"):
@@ -237,3 +299,30 @@ func connect(ctx context.Context, executionId string, host string, port int, use
}
return true, nil
}
func postgresTimeout(timeout int) time.Duration {
if timeout > 0 {
return time.Duration(timeout) * time.Second
}
return 10 * time.Second
}
func postgresTLSConfig(sslMode, host string) (*tls.Config, error) {
switch sslMode {
case "", "disable":
return nil, nil
case "allow", "prefer":
// go-pg cannot implement libpq's ordered plaintext/TLS fallback.
return nil, fmt.Errorf("postgres sslmode %q is unsupported: TLS fallback is unavailable", sslMode)
case "require", "verify-ca", "verify-full":
// Always verify both the certificate chain and hostname. This intentionally
// hardens require and verify-ca because the options API has no safe way to
// configure a custom CA-only verifier without disabling the standard check.
return &tls.Config{
ServerName: host,
MinVersion: tls.VersionTLS12,
}, nil
default:
return nil, fmt.Errorf("unsupported postgres sslmode %q", sslMode)
}
}
+82
View File
@@ -1,11 +1,14 @@
package postgres
import (
"context"
"net/url"
"strings"
"testing"
"github.com/lib/pq"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestBuildPostgresConnectionURLDoesNotAllowDBNameQueryInjection(t *testing.T) {
@@ -72,3 +75,82 @@ func TestBuildPostgresConnectionURLEscapesCredentials(t *testing.T) {
t.Fatalf("database name = %q, want postgres", got)
}
}
func TestBuildPostgresConnectionURLMapsOptions(t *testing.T) {
connStr := buildPostgresConnURLWithOptions(PostgresOptions{
Username: "user", Password: "password", DbName: "app", SSLMode: "require",
}, "127.0.0.1:5432", "exec-1")
u, err := url.Parse(connStr)
if err != nil {
t.Fatalf("parse connection URL: %v", err)
}
if got := u.Query().Get("sslmode"); got != "require" {
t.Fatalf("sslmode = %q, want require", got)
}
if got := strings.TrimPrefix(u.Path, "/"); got != "app" {
t.Fatalf("database name = %q, want app", got)
}
}
func TestPostgresTimeout(t *testing.T) {
if got := postgresTimeout(3); got.String() != "3s" {
t.Fatalf("timeout = %s, want 3s", got)
}
if got := postgresTimeout(0); got.String() != "10s" {
t.Fatalf("default timeout = %s, want 10s", got)
}
}
func TestPostgresTLSConfigMapsSSLMode(t *testing.T) {
tlsConfig, err := postgresTLSConfig("require", "db.example.com")
if err != nil {
t.Fatalf("map require SSL mode: %v", err)
}
if tlsConfig == nil || tlsConfig.ServerName != "db.example.com" || tlsConfig.InsecureSkipVerify {
t.Fatalf("require SSL mode should enable verified TLS, got %+v", tlsConfig)
}
tlsConfig, err = postgresTLSConfig("disable", "db.example.com")
if err != nil {
t.Fatalf("map disable SSL mode: %v", err)
}
if tlsConfig != nil {
t.Fatal("disable SSL mode should not configure TLS")
}
if _, err := postgresTLSConfig("invalid", "db.example.com"); err == nil {
t.Fatal("expected unsupported SSL mode error")
}
for _, mode := range []string{"allow", "prefer"} {
if _, err := postgresTLSConfig(mode, "db.example.com"); err == nil {
t.Fatalf("expected unsupported %s SSL mode error", mode)
}
}
tlsConfig, err = postgresTLSConfig("verify-full", "db.example.com")
if err != nil {
t.Fatalf("map verify-full SSL mode: %v", err)
}
if tlsConfig == nil || tlsConfig.ServerName != "db.example.com" || tlsConfig.InsecureSkipVerify {
t.Fatalf("verify-full should set ServerName and verify certs, got %+v", tlsConfig)
}
}
func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
if err := protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { protocolstate.Close(executionID) })
ctx := context.WithValue(context.Background(), "executionId", executionID) // nolint:staticcheck
connected, err := (&PGClient{}).ConnectWithOptions(ctx, PostgresOptions{
Host: "127.0.0.1", Port: 5432, SSLMode: "require",
})
if connected || err == nil || !strings.Contains(err.Error(), "network policy") {
t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err)
}
}
+87 -14
View File
@@ -17,6 +17,26 @@ import (
pluginsredis "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/redis"
)
// RedisOptions defines the connection options for a Redis server.
// @example
// ```javascript
// const redis = require('nuclei/redis');
// const opts = new redis.RedisOptions();
// opts.Host = 'acme.com';
// opts.Port = 6379;
// opts.Password = 'password';
// opts.DB = 0;
// opts.Timeout = 10;
// const connected = redis.ConnectWithOptions(opts);
// ```
type RedisOptions struct {
Host string // Host is the hostname or IP of the Redis server.
Port int // Port is the Redis port (usually 6379).
Password string // Password is the Redis AUTH password.
DB int // DB is the Redis database index.
Timeout int // Timeout is the dial/read/write timeout in seconds.
}
// GetServerInfo returns the server info for a redis server
// @example
// ```javascript
@@ -35,7 +55,7 @@ func getServerInfo(ctx context.Context, executionId string, host string, port in
return "", protocolstate.ErrHostDenied.Msgf(host)
}
// create a new client
client := newClient(fmt.Sprintf("%s:%d", host, port), "")
client := newClient(executionId, RedisOptions{Host: host, Port: port})
defer func() {
_ = client.Close()
}()
@@ -55,7 +75,9 @@ func getServerInfo(ctx context.Context, executionId string, host string, port in
return infoCmd.Val(), nil
}
// Connect tries to connect redis server with password
// Connect tries to connect redis server with password.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// @example
// ```javascript
// const redis = require('nuclei/redis');
@@ -66,14 +88,37 @@ func Connect(ctx context.Context, host string, port int, password string) (bool,
return memoizedconnect(ctx, executionId, host, port, password)
}
// ConnectWithOptions tries to connect to Redis using the supplied options.
// @example
// ```javascript
// const redis = require('nuclei/redis');
// const opts = new redis.RedisOptions();
// opts.Host = 'acme.com';
// opts.Port = 6379;
// opts.Password = 'password';
// opts.DB = 1;
// const connected = redis.ConnectWithOptions(opts);
// ```
func ConnectWithOptions(ctx context.Context, opts RedisOptions) (bool, error) {
executionId := ctx.Value("executionId").(string)
return connectWithOptions(ctx, executionId, opts)
}
// @memo
func connect(ctx context.Context, executionId string, host string, port int, password string) (bool, error) {
if !protocolstate.IsHostAllowed(executionId, host) {
return connectWithOptions(ctx, executionId, RedisOptions{Host: host, Port: port, Password: password})
}
func connectWithOptions(ctx context.Context, executionId string, opts RedisOptions) (bool, error) {
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(host)
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
// create a new client
client := newClient(fmt.Sprintf("%s:%d", host, port), password)
client := newClient(executionId, opts)
defer func() {
_ = client.Close()
}()
@@ -91,6 +136,35 @@ func connect(ctx context.Context, executionId string, host string, port int, pas
return true, nil
}
func redisClientOptions(executionId string, opts RedisOptions) *redis.Options {
clientOpts := &redis.Options{
Addr: fmt.Sprintf("%s:%d", opts.Host, opts.Port),
Password: opts.Password,
DB: opts.DB,
Dialer: redisDialer(executionId),
}
if opts.Timeout > 0 {
timeout := time.Duration(opts.Timeout) * time.Second
clientOpts.DialTimeout = timeout
clientOpts.ReadTimeout = timeout
clientOpts.WriteTimeout = timeout
}
return clientOpts
}
func redisDialer(executionId string) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
if !protocolstate.IsHostAllowed(executionId, address) {
return nil, protocolstate.ErrHostDenied.Msgf(address)
}
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
return nil, fmt.Errorf("dialers not initialized for %s", executionId)
}
return dialers.Fastdialer.Dial(ctx, network, address)
}
}
// GetServerInfoAuth returns the server info for a redis server
// @example
// ```javascript
@@ -109,7 +183,7 @@ func getServerInfoAuth(ctx context.Context, executionId string, host string, por
return "", protocolstate.ErrHostDenied.Msgf(host)
}
// create a new client
client := newClient(fmt.Sprintf("%s:%d", host, port), password)
client := newClient(executionId, RedisOptions{Host: host, Port: port, Password: password})
defer func() {
_ = client.Close()
}()
@@ -142,6 +216,9 @@ func IsAuthenticated(ctx context.Context, host string, port int) (bool, error) {
// @memo
func isAuthenticated(ctx context.Context, executionId string, host string, port int) (bool, error) {
if !protocolstate.IsHostAllowed(executionId, host) {
return false, protocolstate.ErrHostDenied.Msgf(host)
}
plugin := pluginsredis.REDISPlugin{}
timeout := 5 * time.Second
dialer := protocolstate.GetDialersWithId(executionId)
@@ -180,7 +257,7 @@ func RunLuaScript(ctx context.Context, host string, port int, password string, s
return false, protocolstate.ErrHostDenied.Msgf(host)
}
// create a new client
client := newClient(fmt.Sprintf("%s:%d", host, port), password)
client := newClient(executionId, RedisOptions{Host: host, Port: port, Password: password})
defer func() {
_ = client.Close()
}()
@@ -267,13 +344,9 @@ var errReadLimit = fmt.Errorf("redis: reply exceeds %d bytes", maxReadBytes)
// newClient is redis.NewClient on the default dialer, with each connection's reads capped at
// maxReadBytes.
func newClient(addr, password string) *redis.Client {
opts := &redis.Options{
Addr: addr,
Password: password,
DB: 0,
}
dial := redis.NewDialer(opts)
func newClient(executionId string, redisOpts RedisOptions) *redis.Client {
opts := redisClientOptions(executionId, redisOpts)
dial := opts.Dialer
opts.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := dial(ctx, network, addr)
if err != nil {
+49
View File
@@ -0,0 +1,49 @@
package redis
import (
"context"
"strings"
"testing"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestRedisClientOptionsMapsConnectionOptions(t *testing.T) {
opts := redisClientOptions("test", RedisOptions{
Host: "127.0.0.1", Port: 6379, Password: "secret", DB: 3, Timeout: 4,
})
if opts.Addr != "127.0.0.1:6379" || opts.Password != "secret" || opts.DB != 3 {
t.Fatalf("unexpected Redis client options: %#v", opts)
}
if opts.DialTimeout != 4*time.Second || opts.ReadTimeout != 4*time.Second || opts.WriteTimeout != 4*time.Second {
t.Fatalf("timeout mapping failed: %#v", opts)
}
}
func TestConnectWithOptionsValidatesTarget(t *testing.T) {
connected, err := connectWithOptions(context.Background(), "test", RedisOptions{Port: 6379})
if connected || err == nil || err.Error() != "invalid host or port" {
t.Fatalf("expected invalid target error, got connected=%t err=%v", connected, err)
}
}
func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
if err := protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { protocolstate.Close(executionID) })
connected, err := connectWithOptions(context.Background(), executionID, RedisOptions{
Host: "127.0.0.1", Port: 6379, Timeout: 1,
})
if connected || err == nil || !strings.Contains(err.Error(), "network policy") {
t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err)
}
}
+70
View File
@@ -0,0 +1,70 @@
package smb
import (
"context"
"strings"
"testing"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestListSharesWithOptionsRejectsInvalidHostPort(t *testing.T) {
_, err := listSharesWithOptions(t.Context(), "exec", SMBOptions{Host: "", Port: 445})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid host or port")
_, err = listSharesWithOptions(t.Context(), "exec", SMBOptions{Host: "acme.com", Port: 0})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid host or port")
}
func TestListSharesWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
require.NoError(t, protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}))
t.Cleanup(func() { protocolstate.Close(executionID) })
_, err := listSharesWithOptions(context.Background(), executionID, SMBOptions{
Host: "127.0.0.1", Port: 445, Domain: "ACME", Timeout: 1,
})
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "network policy") && strings.Contains(err.Error(), "127.0.0.1"))
}
func TestSMBOptionsFields(t *testing.T) {
opts := SMBOptions{
Host: "dc.acme.com",
Port: 445,
User: "Administrator",
Password: "secret",
Hash: "31d6cfe0d16ae931b73c59d7e0c089c0",
Domain: "ACME",
Timeout: 15,
}
require.Equal(t, "ACME", opts.Domain)
require.Equal(t, 15, opts.Timeout)
require.Equal(t, "31d6cfe0d16ae931b73c59d7e0c089c0", opts.Hash)
}
func TestSMBDialContextUsesShorterConfiguredTimeout(t *testing.T) {
parent, cancelParent := context.WithTimeout(t.Context(), time.Minute)
defer cancelParent()
start := time.Now()
dialCtx, cancelDial := smbDialContext(parent, 20*time.Millisecond)
end := time.Now()
defer cancelDial()
parentDeadline, ok := parent.Deadline()
require.True(t, ok)
dialDeadline, ok := dialCtx.Deadline()
require.True(t, ok)
require.True(t, dialDeadline.Before(parentDeadline))
require.False(t, dialDeadline.Before(start.Add(20*time.Millisecond)))
require.False(t, dialDeadline.After(end.Add(20*time.Millisecond)))
}
+46
View File
@@ -191,6 +191,8 @@ func (c *SMBClient) Authenticate(ctx context.Context, host string, port int, use
// and list shares by using given credentials.
// Credentials cannot be blank. guest or anonymous credentials
// can be used by providing empty password.
//
// Deprecated: prefer ListSharesWithOptions for new templates.
// @example
// ```javascript
// const smb = require('nuclei/smb');
@@ -207,6 +209,50 @@ func (c *SMBClient) ListShares(ctx context.Context, host string, port int, user,
return memoizedlistShares(ctx, executionId, host, port, user, password)
}
type (
// SMBOptions represents configuration for authenticated SMB operations.
// @example
// ```javascript
// const smb = require('nuclei/smb');
// const client = new smb.SMBClient();
// const opts = new smb.SMBOptions();
// opts.Host = 'acme.com';
// opts.Port = 445;
// opts.User = 'Administrator';
// opts.Password = 'password';
// opts.Domain = 'ACME';
// const shares = client.ListSharesWithOptions(opts);
// ```
SMBOptions struct {
Host string // Host is the hostname or IP of the SMB server.
Port int // Port is the SMB port (usually 445).
User string // User is the username for NTLM authentication.
Password string // Password is the password for NTLM authentication.
Hash string // Hash is an optional hex-encoded NTLM hash (pass-the-hash).
Domain string // Domain is the NTLM domain / workgroup.
Timeout int // Timeout is the dial timeout in seconds (default 10).
}
)
// ListSharesWithOptions lists SMB share names using SMBOptions.
// Supports domain and pass-the-hash via smbsession.
// @example
// ```javascript
// const smb = require('nuclei/smb');
// const client = new smb.SMBClient();
// const opts = new smb.SMBOptions();
// opts.Host = 'acme.com';
// opts.Port = 445;
// opts.User = 'Administrator';
// opts.Hash = 'aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0';
// opts.Domain = 'ACME';
// const shares = client.ListSharesWithOptions(opts);
// ```
func (c *SMBClient) ListSharesWithOptions(ctx context.Context, opts SMBOptions) ([]string, error) {
executionId := ctx.Value("executionId").(string)
return listSharesWithOptions(ctx, executionId, opts)
}
// ListDir lists files and directories under path on the given share
// (nmap smb-ls). path may be empty or "." for the share root.
// user may be "DOMAIN\\user" or "user@domain".
+39 -2
View File
@@ -2,6 +2,9 @@ package smb
import (
"context"
"fmt"
"strings"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/js/libs/smbsession"
)
@@ -11,12 +14,46 @@ type ShareEntry = smbsession.Entry
// @memo
func listShares(ctx context.Context, executionId string, host string, port int, user string, password string) ([]string, error) {
sess, err := smbsession.Dial(ctx, executionId, host, port, smbsession.Creds{User: user, Password: password})
return listSharesWithOptions(ctx, executionId, SMBOptions{
Host: host,
Port: port,
User: user,
Password: password,
})
}
func listSharesWithOptions(ctx context.Context, executionId string, opts SMBOptions) ([]string, error) {
if opts.Host == "" || opts.Port <= 0 {
return nil, fmt.Errorf("invalid host or port")
}
timeout := 10 * time.Second
if opts.Timeout > 0 {
timeout = time.Duration(opts.Timeout) * time.Second
}
dialCtx, cancel := smbDialContext(ctx, timeout)
defer cancel()
hash := opts.Hash
if i := strings.LastIndex(hash, ":"); i >= 0 {
hash = hash[i+1:]
}
sess, err := smbsession.Dial(dialCtx, executionId, opts.Host, opts.Port, smbsession.Creds{
User: opts.User,
Password: opts.Password,
Domain: opts.Domain,
Hash: hash,
})
if err != nil {
return nil, err
}
defer sess.Close()
return sess.ListShares()
return sess.ListSharesContext(dialCtx)
}
func smbDialContext(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
}
// @memo
+25 -10
View File
@@ -154,6 +154,18 @@ func (s *Session) ListShares() ([]string, error) {
return ops.ListShares()
}
// ListSharesContext enumerates share names and stops waiting when ctx is
// cancelled by closing the session, matching Dial.
func (s *Session) ListSharesContext(ctx context.Context) ([]string, error) {
ops := s.ops()
if ops == nil {
return nil, fmt.Errorf("smb session not connected")
}
return await(ctx, s, func(context.Context) ([]string, error) {
return ops.ListShares()
})
}
// ListDir lists one directory on share (share-relative path).
func (s *Session) ListDir(share, dir string) ([]Entry, error) {
return s.ListDirContext(context.Background(), share, dir)
@@ -166,7 +178,7 @@ func (s *Session) ListDirContext(ctx context.Context, share, dir string) ([]Entr
if ops == nil {
return nil, fmt.Errorf("smb session not connected")
}
return s.awaitEntries(ctx, func(ctx context.Context) ([]Entry, error) {
return await(ctx, s, func(ctx context.Context) ([]Entry, error) {
return listDir(ctx, ops, share, dir)
})
}
@@ -192,34 +204,37 @@ func (s *Session) ListTreeContext(ctx context.Context, share, root string, maxDe
if ops == nil {
return nil, fmt.Errorf("smb session not connected")
}
return s.awaitEntries(ctx, func(ctx context.Context) ([]Entry, error) {
return await(ctx, s, func(ctx context.Context) ([]Entry, error) {
return listTree(ctx, ops, share, root, maxDepth, maxEntries)
})
}
func (s *Session) awaitEntries(ctx context.Context, fn func(context.Context) ([]Entry, error)) ([]Entry, error) {
// await runs fn and closes the session when ctx is cancelled first, which
// unblocks the pending SMB call.
func await[T any](ctx context.Context, s *Session, fn func(context.Context) (T, error)) (T, error) {
var zero T
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
return zero, err
}
type rec struct {
entries []Entry
err error
value T
err error
}
ch := make(chan rec, 1)
go func() {
entries, err := fn(ctx)
ch <- rec{entries, err}
value, err := fn(ctx)
ch <- rec{value, err}
}()
select {
case <-ctx.Done():
s.Close()
<-ch
return nil, ctx.Err()
return zero, ctx.Err()
case r := <-ch:
return r.entries, r.err
return r.value, r.err
}
}
+43
View File
@@ -103,6 +103,43 @@ func TestListDirContextUnblocksCancelledLs(t *testing.T) {
}
}
func TestListSharesContextUnblocksCancelledCall(t *testing.T) {
started := make(chan struct{})
closed := make(chan struct{})
hang := &hangBackend{fakeBackend: *newFakeBackend(nil), started: started, wait: closed}
sess := &Session{backend: hang, closed: closed}
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
_, err := sess.ListSharesContext(ctx)
errCh <- err
}()
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("ListShares did not start")
}
cancel()
select {
case err := <-errCh:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(2 * time.Second):
t.Fatal("share listing stayed blocked after cancel")
}
}
func TestListSharesContextReturnsShares(t *testing.T) {
fake := newFakeBackend(nil)
fake.shares = []string{"ADMIN$", "backup"}
sess := &Session{backend: fake, closed: make(chan struct{})}
shares, err := sess.ListSharesContext(context.Background())
require.NoError(t, err)
require.Equal(t, []string{"ADMIN$", "backup"}, shares)
}
func TestListTreeContextUnblocksCancelledLs(t *testing.T) {
started := make(chan struct{})
closed := make(chan struct{})
@@ -282,6 +319,12 @@ type hangBackend struct {
wait <-chan struct{}
}
func (h *hangBackend) ListShares() ([]string, error) {
h.startOnce.Do(func() { close(h.started) })
<-h.wait
return nil, context.Canceled
}
func (h *hangBackend) Ls(string) ([]os.FileInfo, error) {
h.startOnce.Do(func() { close(h.started) })
<-h.wait
+3 -2
View File
@@ -2,6 +2,7 @@
package ssh
import (
"context"
"errors"
"fmt"
@@ -10,11 +11,11 @@ import (
"github.com/zmap/zgrab2/lib/ssh"
)
func memoizedconnectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
func memoizedconnectSSHInfoMode(ctx context.Context, opts *connectOptions) (*ssh.HandshakeLog, error) {
hash := "connectSSHInfoMode" + ":" + fmt.Sprint(opts)
v, err, _ := protocolstate.Memoizer.Do(hash, func() (interface{}, error) {
return connectSSHInfoMode(opts)
return connectSSHInfoMode(ctx, opts)
})
if err != nil {
return nil, err
+84
View File
@@ -0,0 +1,84 @@
package ssh
import (
"strings"
"testing"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestOptionsToConnectMapsFields(t *testing.T) {
opts := SSHOptions{
Host: "ssh.example.com",
Port: 2222,
User: "alice",
Password: "secret",
PrivateKey: "-----BEGIN PRIVATE KEY-----\n...",
Timeout: 15,
ClientVersion: "SSH-2.0-OpenSSH_8.9",
}
got := optionsToConnect(opts, "exec-1")
require.Equal(t, "ssh.example.com", got.Host)
require.Equal(t, 2222, got.Port)
require.Equal(t, "alice", got.User)
require.Equal(t, "secret", got.Password)
require.Equal(t, "-----BEGIN PRIVATE KEY-----\n...", got.PrivateKey)
require.Equal(t, 15*time.Second, got.Timeout)
require.Equal(t, "SSH-2.0-OpenSSH_8.9", got.ClientVersion)
require.Equal(t, "exec-1", got.ExecutionId)
}
func TestOptionsToConnectLeavesDefaultTimeoutWhenUnset(t *testing.T) {
got := optionsToConnect(SSHOptions{Host: "h", Port: 22}, "exec")
require.Equal(t, time.Duration(0), got.Timeout)
}
func TestConnectOptionsValidate(t *testing.T) {
t.Run("missing host", func(t *testing.T) {
err := (&connectOptions{Port: 22, ExecutionId: "x"}).validate()
require.Error(t, err)
require.Contains(t, err.Error(), "host is required")
})
t.Run("missing port", func(t *testing.T) {
err := (&connectOptions{Host: "h", ExecutionId: "x"}).validate()
require.Error(t, err)
require.Contains(t, err.Error(), "port is required")
})
t.Run("default timeout", func(t *testing.T) {
// Unknown execution id → IsHostAllowed returns true (no dialers),
// so validate reaches the timeout default without network-policy noise.
opts := &connectOptions{Host: "scan.example", Port: 22, Timeout: 0, ExecutionId: "ssh-options-timeout-test"}
require.NoError(t, opts.validate())
require.Equal(t, 10*time.Second, opts.Timeout)
})
}
func TestSSHOptionsPasswordAndKeyCanBothBeSet(t *testing.T) {
opts := SSHOptions{
Host: "h",
Port: 22,
User: "u",
Password: "p",
PrivateKey: "k",
}
got := optionsToConnect(opts, "e")
require.Equal(t, "p", got.Password)
require.Equal(t, "k", got.PrivateKey)
}
func TestConnectOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
require.NoError(t, protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}))
t.Cleanup(func() { protocolstate.Close(executionID) })
err := optionsToConnect(SSHOptions{Host: "127.0.0.1", Port: 22}, executionID).validate()
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "network policy") && strings.Contains(err.Error(), "127.0.0.1"))
}
+132 -42
View File
@@ -45,10 +45,84 @@ func (c *SSHClient) SetTimeout(sec int) {
c.timeout = time.Duration(sec) * time.Second
}
type (
// SSHOptions represents configuration options for an SSH connection.
// Use ConnectWithOptions when you need protocol-specific knobs beyond
// username/password (timeout, client version, private key).
// @example
// ```javascript
// const ssh = require('nuclei/ssh');
// const client = new ssh.SSHClient();
// const opts = new ssh.SSHOptions();
// opts.Host = 'acme.com';
// opts.Port = 22;
// opts.User = 'username';
// opts.Password = 'password';
// opts.Timeout = 15;
// opts.ClientVersion = 'SSH-2.0-OpenSSH_8.9';
// const connected = client.ConnectWithOptions(opts);
// ```
SSHOptions struct {
Host string // Host is the hostname or IP of the SSH server.
Port int // Port is the port number of the SSH server.
User string // User is the username for authentication.
Password string // Password is the password for authentication.
PrivateKey string // PrivateKey is an optional PEM-encoded private key.
Timeout int // Timeout is the connection timeout in seconds (default 10).
ClientVersion string // ClientVersion overrides the SSH client version string (e.g. "SSH-2.0-OpenSSH_8.9").
}
)
// ConnectWithOptions tries to connect using the provided SSHOptions.
// Prefer this over Connect/ConnectWithKey when setting timeout, client version,
// or combining password and private key auth.
// @example
// ```javascript
// const ssh = require('nuclei/ssh');
// const client = new ssh.SSHClient();
// const opts = new ssh.SSHOptions();
// opts.Host = 'acme.com';
// opts.Port = 22;
// opts.User = 'username';
// opts.Password = 'password';
// const connected = client.ConnectWithOptions(opts);
// ```
func (c *SSHClient) ConnectWithOptions(ctx context.Context, opts SSHOptions) (bool, error) {
executionId := ctx.Value("executionId").(string)
copts := optionsToConnect(opts, executionId)
if c.timeout > 0 && copts.Timeout == 0 {
copts.Timeout = c.timeout
}
conn, err := connect(ctx, copts)
if err != nil {
return false, err
}
c.connection = conn
return true, nil
}
func optionsToConnect(opts SSHOptions, executionId string) *connectOptions {
copts := &connectOptions{
Host: opts.Host,
Port: opts.Port,
User: opts.User,
Password: opts.Password,
PrivateKey: opts.PrivateKey,
ClientVersion: opts.ClientVersion,
ExecutionId: executionId,
}
if opts.Timeout > 0 {
copts.Timeout = time.Duration(opts.Timeout) * time.Second
}
return copts
}
// 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
//
// Deprecated: prefer ConnectWithOptions for new templates.
// @example
// ```javascript
// const ssh = require('nuclei/ssh');
@@ -56,26 +130,20 @@ func (c *SSHClient) SetTimeout(sec int) {
// 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,
return c.ConnectWithOptions(ctx, SSHOptions{
Host: host,
Port: port,
User: username,
Password: password,
})
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
//
// Deprecated: prefer ConnectWithOptions (set PrivateKey) for new templates.
// @example
// ```javascript
// const ssh = require('nuclei/ssh');
@@ -84,21 +152,12 @@ func (c *SSHClient) Connect(ctx context.Context, host string, port int, username
// 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,
return c.ConnectWithOptions(ctx, SSHOptions{
Host: host,
Port: port,
User: username,
PrivateKey: key,
})
if err != nil {
return false, err
}
c.connection = conn
return true, nil
}
// ConnectSSHInfoMode tries to connect to provided host and port
@@ -116,7 +175,9 @@ func (c *SSHClient) ConnectWithKey(ctx context.Context, host string, port int, u
// ```
func (c *SSHClient) ConnectSSHInfoMode(ctx context.Context, host string, port int) (*ssh.HandshakeLog, error) {
executionId := ctx.Value("executionId").(string)
return memoizedconnectSSHInfoMode(&connectOptions{
// Concurrent callers share one memoized handshake, so one caller's cancel
// must not abort it for the others.
return memoizedconnectSSHInfoMode(context.WithoutCancel(ctx), &connectOptions{
Host: host,
Port: port,
ExecutionId: executionId,
@@ -175,13 +236,14 @@ func (c *SSHClient) Close() (bool, error) {
// unexported functions
type connectOptions struct {
Host string
Port int
User string
Password string
PrivateKey string
Timeout time.Duration // default 10s
ExecutionId string
Host string
Port int
User string
Password string
PrivateKey string
Timeout time.Duration // default 10s
ClientVersion string
ExecutionId string
}
func (c *connectOptions) validate() error {
@@ -202,7 +264,7 @@ func (c *connectOptions) validate() error {
}
// @memo
func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
func connectSSHInfoMode(ctx context.Context, opts *connectOptions) (*ssh.HandshakeLog, error) {
if err := opts.validate(); err != nil {
return nil, err
}
@@ -218,7 +280,7 @@ func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
return nil
}
rhost := fmt.Sprintf("%s:%d", opts.Host, opts.Port)
client, err := ssh.Dial("tcp", rhost, sshConfig)
client, err := dialSSH(ctx, opts.ExecutionId, rhost, sshConfig)
if err != nil {
return nil, err
}
@@ -229,15 +291,16 @@ func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) {
return data, nil
}
func connect(opts *connectOptions) (*ssh.Client, error) {
func connect(ctx context.Context, 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,
User: opts.User,
Auth: []ssh.AuthMethod{},
Timeout: opts.Timeout,
ClientVersion: opts.ClientVersion,
}
if len(opts.Password) > 0 {
@@ -271,9 +334,36 @@ func connect(opts *connectOptions) (*ssh.Client, error) {
conf.Auth = append(conf.Auth, ssh.PublicKeys(signer))
}
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf)
client, err := dialSSH(ctx, opts.ExecutionId, fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf)
if err != nil {
return nil, err
}
return client, nil
}
// dialSSH creates an SSH client over nuclei's policy-aware fastdialer.
func dialSSH(ctx context.Context, executionId, address string, config *ssh.ClientConfig) (*ssh.Client, error) {
if !protocolstate.IsHostAllowed(executionId, address) {
return nil, protocolstate.ErrHostDenied.Msgf(address)
}
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
return nil, fmt.Errorf("dialers not initialized for %s", executionId)
}
conn, err := dialers.Fastdialer.Dial(ctx, "tcp", address)
if err != nil {
return nil, err
}
if config.Timeout != 0 {
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
}
clientConn, chans, reqs, err := ssh.NewClientConn(conn, address, config)
if err != nil {
_ = conn.Close()
return nil, err
}
// Clear the dial/handshake deadline so it doesn't leak into later
// reads/writes on the established connection.
_ = conn.SetDeadline(time.Time{})
return ssh.NewClient(clientConn, chans, reqs), nil
}
+40 -9
View File
@@ -64,6 +64,15 @@ type (
// const client = new telnet.TelnetClient();
// ```
TelnetClient struct{}
// TelnetOptions defines the connection options for a Telnet server.
TelnetOptions struct {
Host string
Port int
Username string
Password string
Timeout int // Timeout is in seconds.
}
)
// IsTelnet checks if a host is running a Telnet server.
@@ -83,6 +92,9 @@ func isTelnet(ctx context.Context, executionId string, host string, port int) (I
resp := IsTelnetResponse{}
timeout := 5 * time.Second
if !protocolstate.IsHostAllowed(executionId, host) {
return resp, protocolstate.ErrHostDenied.Msgf(host)
}
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return IsTelnetResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
@@ -113,6 +125,8 @@ func isTelnet(ctx context.Context, executionId string, host string, port int) (I
// Optionally provides username and password for authentication.
// Returns state of connection. If the connection is successful,
// the function will return true, otherwise false.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// @example
// ```javascript
// const telnet = require('nuclei/telnet');
@@ -120,6 +134,16 @@ func isTelnet(ctx context.Context, executionId string, host string, port int) (I
// const connected = client.Connect('acme.com', 23, 'username', 'password');
// ```
func (c *TelnetClient) Connect(ctx context.Context, host string, port int, username string, password string) (bool, error) {
return c.ConnectWithOptions(ctx, TelnetOptions{
Host: host, Port: port, Username: username, Password: password,
})
}
// ConnectWithOptions connects to Telnet using the supplied connection options.
func (c *TelnetClient) ConnectWithOptions(ctx context.Context, opts TelnetOptions) (bool, error) {
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
executionId := ctx.Value("executionId").(string)
dialer := protocolstate.GetDialersWithId(executionId)
@@ -127,12 +151,16 @@ func (c *TelnetClient) Connect(ctx context.Context, host string, port int, usern
return false, fmt.Errorf("dialers not initialized for %s", executionId)
}
if !protocolstate.IsHostAllowed(executionId, host) {
return false, protocolstate.ErrHostDenied.Msgf(host)
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
// The timeout bounds both the dial and the authentication.
ctx, cancel := context.WithTimeout(ctx, telnetTimeout(opts.Timeout))
defer cancel()
// Create TCP connection
conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port)))
if err != nil {
return false, err
}
@@ -144,12 +172,8 @@ func (c *TelnetClient) Connect(ctx context.Context, host string, port int, usern
}()
// Handle authentication if credentials provided
if username != "" && password != "" {
// Set a timeout context for authentication
authCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := client.Auth(authCtx, username, password); err != nil {
if opts.Username != "" && opts.Password != "" {
if err := client.Auth(ctx, opts.Username, opts.Password); err != nil {
return false, err
}
}
@@ -157,6 +181,13 @@ func (c *TelnetClient) Connect(ctx context.Context, host string, port int, usern
return true, nil
}
func telnetTimeout(timeout int) time.Duration {
if timeout > 0 {
return time.Duration(timeout) * time.Second
}
return 10 * time.Second
}
// Info gathers information about the telnet server including encryption support.
// Uses the telnetmini library's DetectEncryption helper function.
// WARNING: The connection used for detection becomes unusable after this call.
+46
View File
@@ -0,0 +1,46 @@
package telnet
import (
"context"
"strings"
"testing"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestTelnetTimeout(t *testing.T) {
if got := telnetTimeout(2); got != 2*time.Second {
t.Fatalf("timeout = %s, want 2s", got)
}
if got := telnetTimeout(0); got != 10*time.Second {
t.Fatalf("default timeout = %s, want 10s", got)
}
}
func TestConnectWithOptionsValidatesTarget(t *testing.T) {
connected, err := (&TelnetClient{}).ConnectWithOptions(context.Background(), TelnetOptions{Port: 23})
if connected || err == nil || err.Error() != "invalid host or port" {
t.Fatalf("expected invalid target error, got connected=%t err=%v", connected, err)
}
}
func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
if err := protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { protocolstate.Close(executionID) })
ctx := context.WithValue(context.Background(), "executionId", executionID) // nolint:staticcheck
connected, err := (&TelnetClient{}).ConnectWithOptions(ctx, TelnetOptions{
Host: "127.0.0.1", Port: 23, Timeout: 1,
})
if connected || err == nil || !strings.Contains(err.Error(), "network policy") {
t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err)
}
}
+39 -11
View File
@@ -36,12 +36,22 @@ type (
// log(toJSON(connected));
// ```
VNCClient struct{}
// VNCOptions defines the connection options for a VNC server.
VNCOptions struct {
Host string
Port int
Password string
Timeout int // Timeout is in seconds.
}
)
// Connect connects to VNC server using given password.
// If connection and authentication is successful, it returns true.
// If connection or authentication is unsuccessful, it returns false and error.
// The connection is closed after the function returns.
//
// Deprecated: prefer ConnectWithOptions for new templates.
// @example
// ```javascript
// const vnc = require('nuclei/vnc');
@@ -49,18 +59,22 @@ type (
// const connected = client.Connect('acme.com', 5900, 'password');
// ```
func (c *VNCClient) Connect(ctx context.Context, host string, port int, password string) (bool, error) {
executionId := ctx.Value("executionId").(string)
return connect(ctx, executionId, host, port, password)
return c.ConnectWithOptions(ctx, VNCOptions{Host: host, Port: port, Password: password})
}
// connect attempts to authenticate with a VNC server using the given password
func connect(ctx context.Context, executionId string, host string, port int, password string) (bool, error) {
if host == "" || port <= 0 {
// ConnectWithOptions connects to VNC using the supplied connection options.
func (c *VNCClient) ConnectWithOptions(ctx context.Context, opts VNCOptions) (bool, error) {
executionId := ctx.Value("executionId").(string)
return connectWithOptions(ctx, executionId, opts)
}
func connectWithOptions(ctx context.Context, executionId string, opts VNCOptions) (bool, error) {
if opts.Host == "" || opts.Port <= 0 {
return false, fmt.Errorf("invalid host or port")
}
if !protocolstate.IsHostAllowed(executionId, host) {
if !protocolstate.IsHostAllowed(executionId, opts.Host) {
// host is not valid according to network policy
return false, protocolstate.ErrHostDenied.Msgf(host)
return false, protocolstate.ErrHostDenied.Msgf(opts.Host)
}
dialer := protocolstate.GetDialersWithId(executionId)
@@ -68,7 +82,12 @@ func connect(ctx context.Context, executionId string, host string, port int, pas
return false, fmt.Errorf("dialers not initialized for %s", executionId)
}
conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
// The timeout bounds the dial and, through the conn deadline, the handshake.
timeout := vncTimeout(opts.Timeout)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port)))
if err != nil {
return false, err
}
@@ -76,11 +95,10 @@ func connect(ctx context.Context, executionId string, host string, port int, pas
_ = conn.Close()
}()
// Set connection timeout
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
_ = conn.SetDeadline(time.Now().Add(timeout))
// Create VNC client config with password
vncConfig := vnclib.NewClientConfig(password)
vncConfig := vnclib.NewClientConfig(opts.Password)
// Attempt to connect and authenticate
c, err := vnclib.Connect(ctx, conn, vncConfig)
@@ -98,6 +116,13 @@ func connect(ctx context.Context, executionId string, host string, port int, pas
return true, nil
}
func vncTimeout(timeout int) time.Duration {
if timeout > 0 {
return time.Duration(timeout) * time.Second
}
return 10 * time.Second
}
// isAuthError checks if the error is an authentication failure
func isAuthError(err error) bool {
if err == nil {
@@ -128,6 +153,9 @@ func isVNC(ctx context.Context, executionId string, host string, port int) (IsVN
resp := IsVNCResponse{}
timeout := 5 * time.Second
if !protocolstate.IsHostAllowed(executionId, host) {
return resp, protocolstate.ErrHostDenied.Msgf(host)
}
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return IsVNCResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
+45
View File
@@ -0,0 +1,45 @@
package vnc
import (
"context"
"strings"
"testing"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestVNCTimeout(t *testing.T) {
if got := vncTimeout(2); got != 2*time.Second {
t.Fatalf("timeout = %s, want 2s", got)
}
if got := vncTimeout(0); got != 10*time.Second {
t.Fatalf("default timeout = %s, want 10s", got)
}
}
func TestConnectWithOptionsValidatesTarget(t *testing.T) {
connected, err := connectWithOptions(context.Background(), "test", VNCOptions{Port: 5900})
if connected || err == nil || err.Error() != "invalid host or port" {
t.Fatalf("expected invalid target error, got connected=%t err=%v", connected, err)
}
}
func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) {
executionID := t.Name()
if err := protocolstate.Init(&types.Options{
ExecutionId: executionID,
RestrictLocalNetworkAccess: true,
}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { protocolstate.Close(executionID) })
connected, err := connectWithOptions(context.Background(), executionID, VNCOptions{
Host: "127.0.0.1", Port: 5900, Timeout: 1,
})
if connected || err == nil || !strings.Contains(err.Error(), "network policy") {
t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err)
}
}
+9
View File
@@ -24,6 +24,9 @@ type pgDial struct {
}
func (p *pgDial) Dial(network, address string) (net.Conn, error) {
if !protocolstate.IsHostAllowed(p.executionId, address) {
return nil, protocolstate.ErrHostDenied.Msgf(address)
}
dialers := protocolstate.GetDialersWithId(p.executionId)
if dialers == nil {
return nil, fmt.Errorf("dialers not initialized for %s", p.executionId)
@@ -36,6 +39,9 @@ func (p *pgDial) Dial(network, address string) (net.Conn, error) {
}
func (p *pgDial) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
if !protocolstate.IsHostAllowed(p.executionId, address) {
return nil, protocolstate.ErrHostDenied.Msgf(address)
}
dialers := protocolstate.GetDialersWithId(p.executionId)
if dialers == nil {
return nil, fmt.Errorf("dialers not initialized for %s", p.executionId)
@@ -50,6 +56,9 @@ func (p *pgDial) DialTimeout(network, address string, timeout time.Duration) (ne
}
func (p *pgDial) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
if !protocolstate.IsHostAllowed(p.executionId, address) {
return nil, protocolstate.ErrHostDenied.Msgf(address)
}
if ctx == nil {
ctx = p.ctx
}
@@ -239,6 +239,9 @@ func initDialers(options *types.Options) error {
if val := ctx.Value("executionId"); val != nil {
executionId = val.(string)
}
if !IsHostAllowed(executionId, addr) {
return nil, ErrHostDenied.Msgf(addr)
}
dialer := GetDialersWithId(executionId)
if dialer == nil {
return nil, fmt.Errorf("dialers not initialized for %s", executionId)