diff --git a/cmd/integration-test/javascript.go b/cmd/integration-test/javascript.go index 61516abd26..6e99b7f844 100644 --- a/cmd/integration-test/javascript.go +++ b/cmd/integration-test/javascript.go @@ -16,12 +16,14 @@ var jsTestcases = []TestCaseInfo{ {Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}}, {Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}}, {Path: "protocols/javascript/oracle-auth-test.yaml", TestCase: &javascriptOracleAuthTest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }}, + {Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}}, } var ( redisResource *dockertest.Resource sshResource *dockertest.Resource oracleResource *dockertest.Resource + vncResource *dockertest.Resource pool *dockertest.Pool defaultRetry = 3 ) @@ -110,6 +112,39 @@ func (j *javascriptOracleAuthTest) Execute(filePath string) error { tempPort := oracleResource.GetPort("1521/tcp") finalURL := "localhost:" + tempPort defer purge(oracleResource) + + errs := []error{} + for i := 0; i < defaultRetry; i++ { + results := []string{} + var err error + _ = pool.Retry(func() error { + //let ssh server start + time.Sleep(3 * time.Second) + results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug) + return nil + }) + if err != nil { + return err + } + if err := expectResultsCount(results, 1); err == nil { + return nil + } else { + errs = append(errs, err) + } + } + return multierr.Combine(errs...) +} + +type javascriptVncPassBrute struct{} + +func (j *javascriptVncPassBrute) Execute(filePath string) error { + if vncResource == nil || pool == nil { + // skip test as vnc is not running + return nil + } + tempPort := vncResource.GetPort("5900/tcp") + finalURL := "localhost:" + tempPort + defer purge(vncResource) errs := []error{} for i := 0; i < defaultRetry; i++ { results := []string{} @@ -216,4 +251,22 @@ func init() { if err := oracleResource.Expire(30); err != nil { log.Printf("Could not expire Oracle resource: %s", err) } + + // setup a temporary vnc server + vncResource, err = pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "dorowu/ubuntu-desktop-lxde-vnc", + Tag: "latest", + Env: []string{ + "VNC_PASSWORD=mysecret", + }, + Platform: "linux/amd64", + }) + if err != nil { + log.Printf("Could not start resource: %s", err) + return + } + // by default expire after 30 sec + if err := vncResource.Expire(30); err != nil { + log.Printf("Could not expire resource: %s", err) + } } diff --git a/go.mod b/go.mod index 60d4ef0936..6d0541dab8 100644 --- a/go.mod +++ b/go.mod @@ -149,6 +149,7 @@ require ( github.com/alecthomas/kingpin/v2 v2.4.0 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/alexsnet/go-vnc v0.1.0 // indirect github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect diff --git a/go.sum b/go.sum index 59725b255a..8c73280a20 100644 --- a/go.sum +++ b/go.sum @@ -378,6 +378,8 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAu github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexsnet/go-vnc v0.1.0 h1:vBCwPNy79WEL8V/Z5A0ngEFCvTWBAjmS048lkR2rdmY= +github.com/alexsnet/go-vnc v0.1.0/go.mod h1:bbRsg41Sh3zvrnWsw+REKJVGZd8Of2+S0V1G0ZaBhlU= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= diff --git a/integration_tests/protocols/javascript/vnc-pass-brute.yaml b/integration_tests/protocols/javascript/vnc-pass-brute.yaml new file mode 100644 index 0000000000..68cb9df625 --- /dev/null +++ b/integration_tests/protocols/javascript/vnc-pass-brute.yaml @@ -0,0 +1,38 @@ +id: vnc-password-test + +info: + name: VNC Password Authentication Test + author: pdteam + severity: high + description: | + Tests VNC authentication with correct and incorrect passwords. + metadata: + shodan-query: product:"vnc" + tags: js,network,vnc,authentication + +javascript: + - pre-condition: | + isPortOpen(Host,Port) + + code: | + let vnc = require('nuclei/vnc'); + let client = new vnc.VNCClient(); + client.Connect(Host, Port, Password); + + args: + Host: "{{Host}}" + Port: "5900" + Password: "{{passwords}}" + payloads: + passwords: + - "" + - root + - password + - admin + - mysecret + stop-at-first-match: true + + matchers: + - type: dsl + dsl: + - "success == true" diff --git a/pkg/js/generated/go/libvnc/vnc.go b/pkg/js/generated/go/libvnc/vnc.go index 625f3776d0..fa060ec144 100644 --- a/pkg/js/generated/go/libvnc/vnc.go +++ b/pkg/js/generated/go/libvnc/vnc.go @@ -21,6 +21,7 @@ func init() { // Objects / Classes "IsVNCResponse": gojs.GetClassConstructor[lib_vnc.IsVNCResponse](&lib_vnc.IsVNCResponse{}), + "VNCClient": gojs.GetClassConstructor[lib_vnc.VNCClient](&lib_vnc.VNCClient{}), }, ).Register() } diff --git a/pkg/js/generated/ts/vnc.ts b/pkg/js/generated/ts/vnc.ts index 870151e2bc..f74edb8946 100755 --- a/pkg/js/generated/ts/vnc.ts +++ b/pkg/js/generated/ts/vnc.ts @@ -33,3 +33,34 @@ export interface IsVNCResponse { Banner?: string, } +/** + * VNCClient is a client for VNC servers. + * @example + * ```javascript + * const vnc = require('nuclei/vnc'); + * const client = new vnc.VNCClient(); + * ``` + */ +export class VNCClient { + + + // Constructor of VNCClient + constructor() {} + + /** + * 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. + * @example + * ```javascript + * const vnc = require('nuclei/vnc'); + * const client = new vnc.VNCClient(); + * const connected = client.Connect('acme.com', 5900, 'password'); + * ``` + */ + public Connect(host: string, port: number, password: string): boolean | null { + return null; + } +} + diff --git a/pkg/js/libs/vnc/vnc.go b/pkg/js/libs/vnc/vnc.go index bd28ad692a..63c3e6a370 100644 --- a/pkg/js/libs/vnc/vnc.go +++ b/pkg/js/libs/vnc/vnc.go @@ -7,9 +7,11 @@ import ( "strconv" "time" + vnclib "github.com/alexsnet/go-vnc" "github.com/praetorian-inc/fingerprintx/pkg/plugins" - "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/vnc" + vncplugin "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/vnc" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + stringsutil "github.com/projectdiscovery/utils/strings" ) type ( @@ -24,8 +26,89 @@ type ( IsVNC bool Banner string } + + // VNCClient is a client for VNC servers. + // @example + // ```javascript + // const vnc = require('nuclei/vnc'); + // const client = new vnc.VNCClient(); + // const connected = client.Connect('acme.com', 5900, 'password'); + // log(toJSON(connected)); + // ``` + VNCClient struct{} ) +// 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. +// @example +// ```javascript +// const vnc = require('nuclei/vnc'); +// const client = new vnc.VNCClient(); +// 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(executionId, host, port, password) +} + +// connect attempts to authenticate with a VNC server using the given password +func connect(executionId string, host string, port int, password string) (bool, error) { + if host == "" || port <= 0 { + return false, fmt.Errorf("invalid host or port") + } + if !protocolstate.IsHostAllowed(executionId, host) { + // host is not valid according to network policy + return false, protocolstate.ErrHostDenied.Msgf(host) + } + + dialer := protocolstate.GetDialersWithId(executionId) + if dialer == nil { + return false, fmt.Errorf("dialers not initialized for %s", executionId) + } + + conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port))) + if err != nil { + return false, err + } + defer func() { + _ = conn.Close() + }() + + // Set connection timeout + _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) + + // Create VNC client config with password + vncConfig := vnclib.NewClientConfig(password) + + // Attempt to connect and authenticate + c, err := vnclib.Connect(context.TODO(), conn, vncConfig) + if err != nil { + // Check for specific authentication errors + if isAuthError(err) { + return false, nil // Authentication failed, but connection succeeded + } + return false, err // Connection or other error + } + if c != nil { + _ = c.Close() + } + + return true, nil +} + +// isAuthError checks if the error is an authentication failure +func isAuthError(err error) bool { + if err == nil { + return false + } + + // Check for common VNC authentication error messages + errStr := err.Error() + return stringsutil.ContainsAnyI(errStr, "authentication", "auth", "password", "invalid", "failed") +} + // IsVNC checks if a host is running a VNC server. // It returns a boolean indicating if the host is running a VNC server // and the banner of the VNC server. @@ -57,7 +140,7 @@ func isVNC(executionId string, host string, port int) (IsVNCResponse, error) { _ = conn.Close() }() - vncPlugin := vnc.VNCPlugin{} + vncPlugin := vncplugin.VNCPlugin{} service, err := vncPlugin.Run(conn, timeout, plugins.Target{Host: host}) if err != nil { return resp, err