diff --git a/cmd/integration-test/javascript.go b/cmd/integration-test/javascript.go index e45f122c3..61516abd2 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) + } } diff --git a/go.mod b/go.mod index 1c1045470..60d4ef093 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 11dadf023..59725b255 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 67110b4c8..d579c3474 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 852e919e7..5701a4c51 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 - * @example - * ```javascript - * const oracle = require('nuclei/oracle'); - * const isOracle = oracle.IsOracle('acme.com', 1521); - * log(toJSON(isOracle)); - * ``` - */ -export function IsOracle(host: string, port: number): IsOracleResponse | null { - return null; -} - - - /** * 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); + * const client = new oracle.OracleClient(); + * const isOracle = client.IsOracle('acme.com', 1521); * ``` */ export interface IsOracleResponse { - IsOracle?: boolean, - Banner?: string, } +/** + * Client is a client for Oracle database. + * Internally client uses go-ora driver. + * @example + * ```javascript + * const oracle = require('nuclei/oracle'); + * const client = new oracle.OracleClient(); + * ``` + */ +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 9d4117d85..5a0a3ad05 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 000000000..47c62dc13 --- /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 000000000..527bfec6f --- /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