mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 10:10:18 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e80959022 | ||
|
|
17e6a46cba | ||
|
|
a03e867be0 | ||
|
|
38133c1d7b | ||
|
|
c53042cfb3 | ||
|
|
b7fde879cf | ||
|
|
1e5dada8d5 | ||
|
|
8b3dbfd1c9 | ||
|
|
15b7ee0d6c | ||
|
|
f28691ad59 | ||
|
|
54e6279130 |
No files matched your search
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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".
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user