Repository: projectdiscovery/nuclei

Issue:
VNC Enhancement
<!--
1. Please make sure to provide a detailed description with all the relevant information that might be required to start working on this feature.
2. In case you are not sure about your request or whether the particular feature is already supported or not, please start a discussion instead.
3. GitHub Discussion: https://github.com/projectdiscovery/nuclei/discussions/categories/ideas
4. Join our discord server at https://discord.gg/projectdiscovery to discuss the idea on the #nuclei channel.
-->

### Please describe your feature request:
<!-- A clear and concise description of feature to implement -->
[ ] VNC Authentication
### Describe the use case of this feature:
<!-- A clear and concise description of the feature request's motivation and the use-cases in which it could be useful. -->

https://nmap.org/nsedoc/scripts/vnc-brute.html


Hints from issue comments:

Solution patch:
diff --git a/go.mod b/go.mod
index 60d4ef09..6d0541da 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 59725b25..8c73280a 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/pkg/js/generated/go/libvnc/vnc.go b/pkg/js/generated/go/libvnc/vnc.go
index 625f3776..fa060ec1 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 870151e2..f74edb89 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 bd28ad69..63c3e6a3 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



Test patch:
diff --git a/cmd/integration-test/javascript.go b/cmd/integration-test/javascript.go
index 61516abd..6e99b7f8 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/integration_tests/protocols/javascript/vnc-pass-brute.yaml b/integration_tests/protocols/javascript/vnc-pass-brute.yaml
new file mode 100644
index 00000000..68cb9df6
--- /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"