mirror of
https://github.com/projectdiscovery/nuclei
synced 2026-06-08 16:50:47 +00:00
3537030e1f
* fix(js): prevent pool slot starvation under load Zombie goroutines from timed-out JS executions held pool slots indefinitely: Add() blocked with context.Background(), and defer Done() only ran when the goroutine eventually completed. Under load, both pools (80 pooled + 20 non-pooled slots) filled with zombies, silently dropping all subsequent matches. Three changes fix slot lifecycle management: 1. Propagate the 20s deadline context into ExecuteProgram (compiler.go) so both execution paths can respect the deadline. 2. Replace Add() with AddWithContext(ctx) in both pool.go and non-pool.go so goroutines waiting for a slot fail fast when the deadline expires instead of blocking indefinitely. 3. Add a watchdog goroutine that releases the slot when the deadline expires, even if the zombie is still running. An atomic.Bool ensures exactly one Done() call between the watchdog and the normal defer path. * adding context * refactor(js): derived the ctx to remaining tractable deadline leaks (#7302) * refactor(js): use `context.Background` as default instead Signed-off-by: Dwi Siswanto <git@dw1.io> * refactor(js): derived the ctx to remaining tractable deadline leaks Signed-off-by: Dwi Siswanto <git@dw1.io> * test(js): add `NucleiJS.Context` tests Signed-off-by: Dwi Siswanto <git@dw1.io> * fix(cmd): context param exclusion in memoization hash The memoization template condition for excluding context parameters from hash keys was incorrect. The memoize package represents context.Context types as "&{context Context}" (AST string representation), not "context.Context". Signed-off-by: Dwi Siswanto <git@dw1.io> * chore(js): memogen'ed Signed-off-by: Dwi Siswanto <git@dw1.io> --------- Signed-off-by: Dwi Siswanto <git@dw1.io> * fix(js): hangs in checkRDPEncryption by bounding socket I/O Add `setConnDeadlineFromContext` helper to set deadlines on conns derived from context timeouts. Move conn cleanup out of loop-scoped defers to make sure immediate cleanup per probe attempt. Signed-off-by: Dwi Siswanto <git@dw1.io> --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Niek den Breeje <AuditeMarlow@users.noreply.github.com> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com> Co-authored-by: Dwi Siswanto <git@dw1.io>
226 lines
6.9 KiB
Go
226 lines
6.9 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-pg/pg/v10"
|
|
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
|
|
postgres "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/postgresql"
|
|
utils "github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils/pgwrap"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
|
)
|
|
|
|
type (
|
|
// PGClient is a client for Postgres database.
|
|
// Internally client uses go-pg/pg driver.
|
|
// @example
|
|
// ```javascript
|
|
// const postgres = require('nuclei/postgres');
|
|
// const client = new postgres.PGClient;
|
|
// ```
|
|
PGClient struct{}
|
|
)
|
|
|
|
// IsPostgres checks if the given host and port are running Postgres database.
|
|
// If connection is successful, it returns true.
|
|
// If connection is unsuccessful, it returns false and error.
|
|
// @example
|
|
// ```javascript
|
|
// const postgres = require('nuclei/postgres');
|
|
// const isPostgres = postgres.IsPostgres('acme.com', 5432);
|
|
// ```
|
|
func (c *PGClient) IsPostgres(ctx context.Context, host string, port int) (bool, error) {
|
|
executionId := ctx.Value("executionId").(string)
|
|
// todo: why this is exposed? Service fingerprint should be automatic
|
|
return memoizedisPostgres(ctx, executionId, host, port)
|
|
}
|
|
|
|
// @memo
|
|
func isPostgres(ctx context.Context, executionId string, host string, port int) (bool, error) {
|
|
timeout := 10 * time.Second
|
|
|
|
dialer := protocolstate.GetDialersWithId(executionId)
|
|
if dialer == nil {
|
|
return false, fmt.Errorf("dialers not initialized for %s", executionId)
|
|
}
|
|
|
|
conn, err := dialer.Fastdialer.Dial(ctx, "tcp", fmt.Sprintf("%s:%d", host, port))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() {
|
|
_ = conn.Close()
|
|
}()
|
|
|
|
_ = conn.SetDeadline(time.Now().Add(timeout))
|
|
|
|
plugin := &postgres.POSTGRESPlugin{}
|
|
service, err := plugin.Run(conn, timeout, plugins.Target{Host: host})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if service == nil {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Connect connects to Postgres database using given credentials.
|
|
// If connection is successful, it returns true.
|
|
// If connection is unsuccessful, it returns false and error.
|
|
// The connection is closed after the function returns.
|
|
// @example
|
|
// ```javascript
|
|
// const postgres = require('nuclei/postgres');
|
|
// const client = new postgres.PGClient;
|
|
// const connected = client.Connect('acme.com', 5432, 'username', 'password');
|
|
// ```
|
|
func (c *PGClient) Connect(ctx context.Context, host string, port int, username string, password string) (bool, error) {
|
|
ok, err := c.IsPostgres(ctx, host, port)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, fmt.Errorf("not a postgres service")
|
|
}
|
|
executionId := ctx.Value("executionId").(string)
|
|
return memoizedconnect(ctx, executionId, host, port, username, password, "postgres")
|
|
}
|
|
|
|
// 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.
|
|
// @example
|
|
// ```javascript
|
|
// const postgres = require('nuclei/postgres');
|
|
// const client = new postgres.PGClient;
|
|
// const result = client.ExecuteQuery('acme.com', 5432, 'username', 'password', 'dbname', 'select * from users');
|
|
// log(to_json(result));
|
|
// ```
|
|
func (c *PGClient) ExecuteQuery(ctx context.Context, host string, port int, username string, password string, dbName string, query string) (*utils.SQLResult, error) {
|
|
ok, err := c.IsPostgres(ctx, host, port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, fmt.Errorf("not a postgres service")
|
|
}
|
|
|
|
executionId := ctx.Value("executionId").(string)
|
|
|
|
return memoizedexecuteQuery(ctx, executionId, host, port, username, password, dbName, query)
|
|
}
|
|
|
|
// @memo
|
|
func executeQuery(ctx context.Context, executionId string, host string, port int, username string, password string, dbName string, query string) (*utils.SQLResult, error) {
|
|
if !protocolstate.IsHostAllowed(executionId, host) {
|
|
// host is not valid according to network policy
|
|
return nil, protocolstate.ErrHostDenied.Msgf(host)
|
|
}
|
|
|
|
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
|
|
|
connStr := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable&executionId=%s", username, password, target, dbName, executionId)
|
|
db, err := pgwrap.OpenDB(ctx, executionId, connStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = db.Close()
|
|
}()
|
|
|
|
rows, err := db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := utils.UnmarshalSQLRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// ConnectWithDB connects to Postgres database using given credentials and database name.
|
|
// If connection is successful, it returns true.
|
|
// If connection is unsuccessful, it returns false and error.
|
|
// The connection is closed after the function returns.
|
|
// @example
|
|
// ```javascript
|
|
// const postgres = require('nuclei/postgres');
|
|
// const client = new postgres.PGClient;
|
|
// const connected = client.ConnectWithDB('acme.com', 5432, 'username', 'password', 'dbname');
|
|
// ```
|
|
func (c *PGClient) ConnectWithDB(ctx context.Context, host string, port int, username string, password string, dbName string) (bool, error) {
|
|
ok, err := c.IsPostgres(ctx, host, port)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, fmt.Errorf("not a postgres service")
|
|
}
|
|
|
|
executionId := ctx.Value("executionId").(string)
|
|
|
|
return memoizedconnect(ctx, executionId, host, port, username, password, dbName)
|
|
}
|
|
|
|
// @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 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)
|
|
}
|
|
|
|
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
|
|
|
execCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
dialer := protocolstate.GetDialersWithId(executionId)
|
|
if dialer == nil {
|
|
return false, fmt.Errorf("dialers not initialized for %s", executionId)
|
|
}
|
|
|
|
db := pg.Connect(&pg.Options{
|
|
Addr: target,
|
|
User: username,
|
|
Password: password,
|
|
Database: dbName,
|
|
Dialer: func(dialCtx context.Context, network, addr string) (net.Conn, error) {
|
|
return dialer.Fastdialer.Dial(dialCtx, network, addr)
|
|
},
|
|
IdleCheckFrequency: -1,
|
|
}).WithTimeout(10 * time.Second)
|
|
|
|
defer func() {
|
|
_ = db.Close()
|
|
}()
|
|
|
|
_, err := db.ExecContext(execCtx, "select 1")
|
|
if err != nil {
|
|
switch true {
|
|
case strings.Contains(err.Error(), "connect: connection refused"):
|
|
fallthrough
|
|
case strings.Contains(err.Error(), "no pg_hba.conf entry for host"):
|
|
fallthrough
|
|
case strings.Contains(err.Error(), "network unreachable"):
|
|
fallthrough
|
|
case strings.Contains(err.Error(), "reset"):
|
|
fallthrough
|
|
case strings.Contains(err.Error(), "i/o timeout"):
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|