Repository: projectdiscovery/nuclei

Issue:
Oracle  -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 -->

- [ ] [oracle-brute](https://nmap.org/nsedoc/scripts/oracle-brute.html)
- [ ] [oracle-brute-stealth](https://nmap.org/nsedoc/scripts/oracle-brute-stealth.html)
- [ ] [oracle-enum-users](https://nmap.org/nsedoc/scripts/oracle-enum-users.html)
- [ ] [oracle-sid-brute](https://nmap.org/nsedoc/scripts/oracle-sid-brute.html)

### 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. -->

Authentication required

Discussion:

Solution patch:
diff --git a/go.mod b/go.mod
index 1c104547..60d4ef09 100644
--- a/go.mod
+++ b/go.mod
@@ -113,6 +113,7 @@ require (
 	github.com/redis/go-redis/v9 v9.11.0
 	github.com/seh-msft/burpxml v1.0.1
 	github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
+	github.com/sijms/go-ora/v2 v2.9.0
 	github.com/stretchr/testify v1.11.1
 	github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
 	github.com/testcontainers/testcontainers-go v0.38.0
diff --git a/go.sum b/go.sum
index 11dadf02..59725b25 100644
--- a/go.sum
+++ b/go.sum
@@ -1381,6 +1381,8 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
 github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
+github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
+github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
diff --git a/pkg/js/generated/go/liboracle/oracle.go b/pkg/js/generated/go/liboracle/oracle.go
index 67110b4c..d579c347 100644
--- a/pkg/js/generated/go/liboracle/oracle.go
+++ b/pkg/js/generated/go/liboracle/oracle.go
@@ -15,12 +15,12 @@ func init() {
 	module.Set(
 		gojs.Objects{
 			// Functions
-			"IsOracle": lib_oracle.IsOracle,
 
 			// Var and consts
 
 			// Objects / Classes
 			"IsOracleResponse": gojs.GetClassConstructor[lib_oracle.IsOracleResponse](&lib_oracle.IsOracleResponse{}),
+			"OracleClient":     gojs.GetClassConstructor[lib_oracle.OracleClient](&lib_oracle.OracleClient{}),
 		},
 	).Register()
 }
diff --git a/pkg/js/generated/ts/oracle.ts b/pkg/js/generated/ts/oracle.ts
index 852e919e..5701a4c5 100755
--- a/pkg/js/generated/ts/oracle.ts
+++ b/pkg/js/generated/ts/oracle.ts
@@ -1,33 +1,106 @@
 
 
 /**
- * IsOracle checks if a host is running an Oracle server
+ * IsOracleResponse is the response from the IsOracle function.
+ * this is returned by IsOracle function.
  * @example
  * ```javascript
  * const oracle = require('nuclei/oracle');
- * const isOracle = oracle.IsOracle('acme.com', 1521);
- * log(toJSON(isOracle));
+ * const client = new oracle.OracleClient();
+ * const isOracle = client.IsOracle('acme.com', 1521);
  * ```
  */
-export function IsOracle(host: string, port: number): IsOracleResponse | null {
-    return null;
+export interface IsOracleResponse {
+    IsOracle?: boolean,
+    Banner?: string,
 }
 
-
-
 /**
- * IsOracleResponse is the response from the IsOracle function.
- * this is returned by IsOracle function.
+ * Client is a client for Oracle database.
+ * Internally client uses go-ora driver.
  * @example
  * ```javascript
  * const oracle = require('nuclei/oracle');
- * const isOracle = oracle.IsOracle('acme.com', 1521);
+ * const client = new oracle.OracleClient();
  * ```
  */
-export interface IsOracleResponse {
-    
-    IsOracle?: boolean,
-    
-    Banner?: string,
+export class OracleClient {
+    // Constructor of OracleClient
+    constructor() {}
+
+    /**
+     * Connect connects to an Oracle database
+     * @example
+     * ```javascript
+     * const oracle = require('nuclei/oracle');
+     * const client = new oracle.OracleClient();
+     * client.Connect('acme.com', 1521, 'XE', 'user', 'password');
+     * ```
+     */
+    public Connect(host: string, port: number, serviceName: string, username: string, password: string): boolean | null {
+        return null;
+    }
+
+    /**
+     * ConnectWithDSN connects to an Oracle database using a DSN string
+     * @example
+     * ```javascript
+     * const oracle = require('nuclei/oracle');
+     * const client = new oracle.OracleClient();
+     * client.ConnectWithDSN('oracle://user:password@host:port/service', 'SELECT @@version');
+     * ```
+     */
+    public ConnectWithDSN(dsn: string): boolean | null {
+        return null;
+    }
+
+    /**
+     * IsOracle checks if a host is running an Oracle server
+     * @example
+     * ```javascript
+     * const oracle = require('nuclei/oracle');
+     * const isOracle = oracle.IsOracle('acme.com', 1521);
+     * ```
+     */
+    public IsOracle(host: string, port: number): IsOracleResponse | null {
+        return null;
+    }
+
+    /**
+     * ExecuteQuery connects to Oracle database using given credentials and executes a query.
+     * It returns the results of the query or an error if something goes wrong.
+     * @example
+     * ```javascript
+     * const oracle = require('nuclei/oracle');
+     * const client = new oracle.OracleClient();
+     * const result = client.ExecuteQuery('acme.com', 1521, 'username', 'password', 'XE', 'SELECT * FROM dual');
+     * log(to_json(result));
+     * ```
+     */
+    public ExecuteQuery(host: string, port: number, username: string, password: string, dbName: string, query: string): SQLResult | null {
+        return null;
+    }
+
+    /**
+     * ExecuteQueryWithDSN executes a query on an Oracle database using a DSN
+     * @example
+     * ```javascript
+     * const oracle = require('nuclei/oracle');
+     * const client = new oracle.OracleClient();
+     * const result = client.ExecuteQueryWithDSN('oracle://user:password@host:port/service', 'SELECT * FROM dual');
+     * log(to_json(result));
+     * ```
+     */
+    public ExecuteQueryWithDSN(dsn: string, query: string): SQLResult | null {
+        return null;
+    }
 }
 
+/**
+ * SQLResult Interface
+ */
+export interface SQLResult {
+    Count?: number,
+    Columns?: string[],
+    Rows?: any[],
+}
diff --git a/pkg/js/libs/oracle/oracle.go b/pkg/js/libs/oracle/oracle.go
index 9d4117d8..5a0a3ad0 100644
--- a/pkg/js/libs/oracle/oracle.go
+++ b/pkg/js/libs/oracle/oracle.go
@@ -2,6 +2,7 @@ package oracle
 
 import (
 	"context"
+	"database/sql"
 	"fmt"
 	"net"
 	"strconv"
@@ -9,7 +10,9 @@ import (
 
 	"github.com/praetorian-inc/fingerprintx/pkg/plugins"
 	"github.com/praetorian-inc/fingerprintx/pkg/plugins/services/oracledb"
+	"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
 	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
+	goora "github.com/sijms/go-ora/v2"
 )
 
 type (
@@ -24,6 +27,16 @@ type (
 		IsOracle bool
 		Banner   string
 	}
+	// Client is a client for Oracle database.
+	// Internally client uses oracle/godror driver.
+	// @example
+	// ```javascript
+	// const oracle = require('nuclei/oracle');
+	// const client = new oracle.OracleClient();
+	// ```
+	OracleClient struct {
+		connector *goora.OracleConnector
+	}
 )
 
 // IsOracle checks if a host is running an Oracle server
@@ -33,7 +46,7 @@ type (
 // const isOracle = oracle.IsOracle('acme.com', 1521);
 // log(toJSON(isOracle));
 // ```
-func IsOracle(ctx context.Context, host string, port int) (IsOracleResponse, error) {
+func (c *OracleClient) IsOracle(ctx context.Context, host string, port int) (IsOracleResponse, error) {
 	executionId := ctx.Value("executionId").(string)
 	return memoizedisOracle(executionId, host, port)
 }
@@ -69,3 +82,129 @@ func isOracle(executionId string, host string, port int) (IsOracleResponse, erro
 	resp.IsOracle = true
 	return resp, nil
 }
+
+func (c *OracleClient) oracleDbInstance(connStr string, executionId string) (*goora.OracleConnector, error) {
+	if c.connector != nil {
+		return c.connector, nil
+	}
+
+	connector := goora.NewConnector(connStr)
+	oraConnector, ok := connector.(*goora.OracleConnector)
+	if !ok {
+		return nil, fmt.Errorf("failed to cast connector to OracleConnector")
+	}
+
+	// Create custom dialer wrapper
+	customDialer := &oracleCustomDialer{
+		executionId: executionId,
+	}
+
+	oraConnector.Dialer(customDialer)
+
+	c.connector = oraConnector
+
+	return oraConnector, nil
+}
+
+// Connect connects to an Oracle database
+// @example
+// ```javascript
+// const oracle = require('nuclei/oracle');
+// const client = new oracle.OracleClient;
+// 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.ConnectWithDSN(ctx, connStr)
+}
+
+func (c *OracleClient) ConnectWithDSN(ctx context.Context, dsn string) (bool, error) {
+	executionId := ctx.Value("executionId").(string)
+
+	connector, err := c.oracleDbInstance(dsn, executionId)
+	if err != nil {
+		return false, err
+	}
+
+	db := sql.OpenDB(connector)
+	defer func() {
+		_ = db.Close()
+	}()
+
+	db.SetMaxOpenConns(1)
+	db.SetMaxIdleConns(0)
+
+	// Test the connection
+	err = db.Ping()
+	if err != nil {
+		return false, err
+	}
+
+	return true, 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
+// ```javascript
+// const oracle = require('nuclei/oracle');
+// const client = new oracle.OracleClient;
+// const result = client.ExecuteQuery('acme.com', 1521, 'username', 'password', 'XE', 'SELECT @@version');
+// log(to_json(result));
+// ```
+func (c *OracleClient) ExecuteQuery(ctx context.Context, host string, port int, username, password, dbName, query string) (*utils.SQLResult, error) {
+	if host == "" || port <= 0 {
+		return nil, fmt.Errorf("invalid host or port")
+	}
+
+	isOracleResp, err := c.IsOracle(ctx, host, port)
+	if err != nil {
+		return nil, err
+	}
+	if !isOracleResp.IsOracle {
+		return nil, fmt.Errorf("not a oracle service")
+	}
+
+	connStr := goora.BuildUrl(host, port, dbName, username, password, nil)
+
+	return c.ExecuteQueryWithDSN(ctx, connStr, query)
+}
+
+// ExecuteQueryWithDSN executes a query on an Oracle database using a DSN
+// @example
+// ```javascript
+// const oracle = require('nuclei/oracle');
+// const client = new oracle.OracleClient;
+// const result = client.ExecuteQueryWithDSN('oracle://user:password@host:port/service', 'SELECT @@version');
+// log(to_json(result));
+// ```
+func (c *OracleClient) ExecuteQueryWithDSN(ctx context.Context, dsn string, query string) (*utils.SQLResult, error) {
+	executionId := ctx.Value("executionId").(string)
+
+	connector, err := c.oracleDbInstance(dsn, executionId)
+	if err != nil {
+		return nil, err
+	}
+	db := sql.OpenDB(connector)
+	defer func() {
+		_ = db.Close()
+	}()
+
+	db.SetMaxOpenConns(1)
+	db.SetMaxIdleConns(0)
+
+	rows, err := db.Query(query)
+	if err != nil {
+		return nil, err
+	}
+
+	data, err := utils.UnmarshalSQLRows(rows)
+	if err != nil {
+		if data != nil && len(data.Rows) > 0 {
+			return data, nil
+		}
+		return nil, err
+	}
+	return data, nil
+}
diff --git a/pkg/js/libs/oracle/oracledialer.go b/pkg/js/libs/oracle/oracledialer.go
new file mode 100644
index 00000000..47c62dc1
--- /dev/null
+++ b/pkg/js/libs/oracle/oracledialer.go
@@ -0,0 +1,42 @@
+package oracle
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
+)
+
+// oracleCustomDialer implements the dialer interface expected by go-ora
+type oracleCustomDialer struct {
+	executionId string
+}
+
+func (o *oracleCustomDialer) dialWithCtx(ctx context.Context, network, address string) (net.Conn, error) {
+	dialers := protocolstate.GetDialersWithId(o.executionId)
+	if dialers == nil {
+		return nil, fmt.Errorf("dialers not initialized for %s", o.executionId)
+	}
+	if !protocolstate.IsHostAllowed(o.executionId, address) {
+		// host is not valid according to network policy
+		return nil, protocolstate.ErrHostDenied.Msgf(address)
+	}
+	return dialers.Fastdialer.Dial(ctx, network, address)
+}
+
+func (o *oracleCustomDialer) Dial(network, address string) (net.Conn, error) {
+	return o.dialWithCtx(context.TODO(), network, address)
+}
+
+func (o *oracleCustomDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+
+	return o.dialWithCtx(ctx, network, address)
+}
+
+func (o *oracleCustomDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
+	return o.dialWithCtx(ctx, network, address)
+}
diff --git a/pkg/protocols/javascript/testcases/oracle-auth-test.yaml b/pkg/protocols/javascript/testcases/oracle-auth-test.yaml
new file mode 100644
index 00000000..527bfec6
--- /dev/null
+++ b/pkg/protocols/javascript/testcases/oracle-auth-test.yaml
@@ -0,0 +1,29 @@
+id: oracle-auth-test
+
+info:
+  name: Oracle - Authentication Test
+  author: pdteam
+  severity: info
+  tags: js,oracle,network,auth
+
+javascript:
+  - pre-condition: |
+      isPortOpen(Host,Port);
+    code: |
+      let o = require('nuclei/oracle');
+      let c = o.OracleClient();
+      c.Connect(Host, Port, ServiceName, User, Pass);
+      
+    args:
+      ServiceName: "XE"
+      Host: "{{Host}}"
+      Port: "1521"
+      User: "system"
+      Pass: "{{passwords}}"
+    payloads:
+      passwords:
+        - mysecret
+    matchers:
+      - type: dsl
+        dsl:
+          - "response == true"
\ No newline at end of file


Test patch:
diff --git a/cmd/integration-test/javascript.go b/cmd/integration-test/javascript.go
index e45f122c..61516abd 100644
--- a/cmd/integration-test/javascript.go
+++ b/cmd/integration-test/javascript.go
@@ -15,13 +15,15 @@ var jsTestcases = []TestCaseInfo{
 	{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
 	{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() }},
 }
 
 var (
-	redisResource *dockertest.Resource
-	sshResource   *dockertest.Resource
-	pool          *dockertest.Pool
-	defaultRetry  = 3
+	redisResource  *dockertest.Resource
+	sshResource    *dockertest.Resource
+	oracleResource *dockertest.Resource
+	pool           *dockertest.Pool
+	defaultRetry   = 3
 )
 
 type javascriptNetHttps struct{}
@@ -98,6 +100,38 @@ func (j *javascriptSSHServerFingerprint) Execute(filePath string) error {
 	return multierr.Combine(errs...)
 }
 
+type javascriptOracleAuthTest struct{}
+
+func (j *javascriptOracleAuthTest) Execute(filePath string) error {
+	if oracleResource == nil || pool == nil {
+		// skip test as oracle is not running
+		return nil
+	}
+	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...)
+}
+
 // purge any given resource if it is not nil
 func purge(resource *dockertest.Resource) {
 	if resource != nil && pool != nil {
@@ -163,4 +197,23 @@ func init() {
 	if err := sshResource.Expire(30); err != nil {
 		log.Printf("Could not expire resource: %s", err)
 	}
+
+	// setup a temporary oracle instance
+	oracleResource, err = pool.RunWithOptions(&dockertest.RunOptions{
+		Repository: "gvenzl/oracle-xe",
+		Tag:        "latest",
+		Env: []string{
+			"ORACLE_PASSWORD=mysecret",
+		},
+		Platform: "linux/amd64",
+	})
+	if err != nil {
+		log.Printf("Could not start Oracle resource: %s", err)
+		return
+	}
+
+	// by default expire after 30 sec
+	if err := oracleResource.Expire(30); err != nil {
+		log.Printf("Could not expire Oracle resource: %s", err)
+	}
 }