Files
2026-05-14 18:42:48 +02:00

593 lines
20 KiB
Go

//go:build integration
// +build integration
package integration_test
import (
"database/sql"
"fmt"
"io"
"net"
"time"
"github.com/go-pg/pg/v10"
_ "github.com/go-sql-driver/mysql"
"github.com/ory/dockertest/v3"
dc "github.com/ory/dockertest/v3/docker"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
osutils "github.com/projectdiscovery/utils/os"
"github.com/projectdiscovery/utils/reader"
"go.uber.org/multierr"
)
func javascriptDockerDisabled() bool {
return !osutils.IsLinux() || !hasAnyExecutable("docker", "podman")
}
var jsTestcases = []integrationCase{
{Path: "protocols/javascript/redis-pass-brute.yaml", TestCase: &javascriptRedisPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
{Path: "protocols/javascript/rsync-test.yaml", TestCase: &javascriptRsyncTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/mysql-connect.yaml", TestCase: &javascriptMySQLConnect{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/multi-ports.yaml", TestCase: &javascriptMultiPortsSSH{}},
{Path: "protocols/javascript/no-port-args.yaml", TestCase: &javascriptNoPortArgs{}},
{Path: "protocols/javascript/telnet-auth-test.yaml", TestCase: &javascriptTelnetAuthTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/asrep-roast.yaml", TestCase: &javascriptASRepRoast{}},
{Path: "protocols/javascript/wmi-command.yaml", TestCase: &javascriptWMICommand{}},
{Path: "protocols/javascript/goexec-redaction.yaml", TestCase: &javascriptGoExecRedaction{}},
{Path: "protocols/javascript/goexec-modules.yaml", TestCase: &javascriptGoExecModules{}},
{Path: "protocols/javascript/goexec-samba-ntlm.yaml", TestCase: &javascriptGoExecSambaNTLM{}, DisableOn: javascriptDockerDisabled, Serial: true},
}
var (
defaultRetry = 3
)
const (
javascriptContainerTTLSeconds = 300
javascriptDatabaseReadyTimeout = 3 * time.Minute
javascriptServiceReadyTimeout = 45 * time.Second
javascriptRetryDelay = 500 * time.Millisecond
javascriptDialTimeout = 500 * time.Millisecond
)
type javascriptDockerReadyCheck func(address string) error
type javascriptDockerSpec struct {
options *dockertest.RunOptions
port string
readyTimeout time.Duration
settleDelay time.Duration
readyCheck javascriptDockerReadyCheck
}
func newJavascriptDockerSpec(port string, options *dockertest.RunOptions, readyTimeout time.Duration, settleDelay time.Duration, readyCheck javascriptDockerReadyCheck) javascriptDockerSpec {
if options == nil {
options = &dockertest.RunOptions{}
}
if options.Platform == "" {
options.Platform = "linux/amd64"
}
if len(options.ExposedPorts) == 0 {
options.ExposedPorts = []string{port}
}
if readyTimeout <= 0 {
readyTimeout = javascriptServiceReadyTimeout
}
return javascriptDockerSpec{
options: options,
port: port,
readyTimeout: readyTimeout,
settleDelay: settleDelay,
readyCheck: readyCheck,
}
}
type javascriptNetHttps struct{}
func (j *javascriptNetHttps) Execute(filePath string) error {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptRedisPassBrute struct{}
func (j *javascriptRedisPassBrute) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("6379/tcp", &dockertest.RunOptions{
Repository: "redis",
Tag: "latest",
Cmd: []string{"redis-server", "--requirepass", "iamadmin"},
}, javascriptServiceReadyTimeout, 0, nil))
}
type javascriptSSHServerFingerprint struct{}
func (j *javascriptSSHServerFingerprint) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("22/tcp", &dockertest.RunOptions{
Repository: "alpine",
Tag: "latest",
Cmd: []string{
"sh",
"-c",
"apk add --no-cache openssh && ssh-keygen -A && mkdir -p /run/sshd && exec /usr/sbin/sshd -D -e",
},
}, javascriptServiceReadyTimeout, 2*time.Second, nil))
}
type javascriptVncPassBrute struct{}
func (j *javascriptVncPassBrute) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("5900/tcp", &dockertest.RunOptions{
Repository: "dorowu/ubuntu-desktop-lxde-vnc",
Tag: "latest",
Env: []string{
"VNC_PASSWORD=mysecret",
},
}, javascriptServiceReadyTimeout, 20*time.Second, nil))
}
type javascriptPostgresPassBrute struct{}
func (j *javascriptPostgresPassBrute) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("5432/tcp", &dockertest.RunOptions{
Repository: "postgres",
Tag: "13",
Env: []string{
"POSTGRES_INITDB_ARGS=--auth-host=md5 --auth-local=trust",
"POSTGRES_PASSWORD=postgres",
"POSTGRES_USER=postgres",
},
}, javascriptDatabaseReadyTimeout, 0, postgresReadyCheck("postgres", "postgres")), 1, 2)
}
type javascriptMySQLConnect struct{}
func (j *javascriptMySQLConnect) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("3306/tcp", &dockertest.RunOptions{
Repository: "mysql",
Tag: "5.7",
Env: []string{
"MYSQL_ROOT_PASSWORD=secret",
},
}, javascriptDatabaseReadyTimeout, 0, mysqlReadyCheck("root", "secret")))
}
type javascriptMultiPortsSSH struct{}
func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
// use scanme.sh as target to ensure we match on the 2nd default port 22
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptNoPortArgs struct{}
func (j *javascriptNoPortArgs) Execute(filePath string) error {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptWMICommand struct{}
func (j *javascriptWMICommand) Execute(filePath string) error {
// Exercises the protocolstate network-policy denial path: the helper sees
// 203.0.113.10 (RFC 5737 TEST-NET-3, hard-coded inside the template) on
// the exclude list and short-circuits before any dial, so the JSON result
// must contain "ok":false plus "network policy" in the error while not
// leaking the password.
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptGoExecRedaction struct{}
func (j *javascriptGoExecRedaction) Execute(filePath string) error {
listener := newGoExecCloseListener()
defer listener.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptGoExecModules struct{}
func (j *javascriptGoExecModules) Execute(filePath string) error {
listener := newGoExecCloseListener()
defer listener.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptGoExecSambaNTLM struct{}
// Execute spins up a Samba container with a known local user, binds it to
// host 127.0.0.1:445 (the goexec SMB dialer ignores any port carried in the
// target, so a randomised host port would never be reached), and runs the
// SCMR helper against it. NTLM authentication completes end-to-end against a
// real SMB server; the eventual failure happens at the SCMR layer because
// Samba does not implement remote service creation, which is exactly what
// the template asserts (structured Result + redaction).
func (j *javascriptGoExecSambaNTLM) Execute(filePath string) error {
pool, err := dockertest.NewPool("")
if err != nil {
return fmt.Errorf("could not create docker pool: %w", err)
}
if err := pool.Client.Ping(); err != nil {
return fmt.Errorf("could not connect to Docker: %w", err)
}
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "dperson/samba",
Tag: "latest",
Platform: "linux/amd64",
ExposedPorts: []string{"445/tcp"},
PortBindings: map[dc.Port][]dc.PortBinding{
"445/tcp": {{HostIP: "127.0.0.1", HostPort: "445"}},
},
Cmd: []string{
"-u", "auditor;samba-ntlm-secret",
"-s", "data;/share;yes;no;no;auditor;auditor;auditor",
"-w", "WORKGROUP",
"-p",
},
})
if err != nil {
return fmt.Errorf("could not start samba: %w", err)
}
defer purge(pool, resource)
if err := resource.Expire(javascriptContainerTTLSeconds); err != nil {
return fmt.Errorf("could not expire samba: %w", err)
}
targetAddress := "127.0.0.1:445"
if err := waitForTCPService(targetAddress, javascriptServiceReadyTimeout); err != nil {
return err
}
// SMB ready-check: the port binds before smbd is actually accepting
// negotiate requests (especially under qemu emulation on arm64). Send a
// minimal SMB2 NEGOTIATE and require the response header to start with
// 0xFE 'S' 'M' 'B'.
if err := waitForServiceCheck(targetAddress, javascriptServiceReadyTimeout, smbReadyCheck()); err != nil {
return err
}
errS := make([]error, 0, defaultRetry)
for attempt := 1; attempt <= defaultRetry; attempt++ {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
if err == nil {
if countErr := expectResultsCount(results, 1); countErr == nil {
return nil
} else {
err = countErr
}
}
errS = append(errS, err)
if attempt < defaultRetry {
time.Sleep(javascriptRetryDelay)
}
}
return multierr.Combine(errS...)
}
// goExecCloseListener is a loopback TCP listener that accepts incoming
// connections and immediately closes them. It is used to intercept the
// DCERPC dial issued by the GoExec helpers (via Options.Endpoint), so we
// exercise target normalization, host-allowed check, real dial, RPC bind
// failure, structured Result, and redaction without a Windows backend.
type goExecCloseListener struct {
listener net.Listener
host string
binding string
}
func newGoExecCloseListener() *goExecCloseListener {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(fmt.Errorf("goexec listener: %w", err))
}
host, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
_ = ln.Close()
panic(fmt.Errorf("goexec listener addr: %w", err))
}
g := &goExecCloseListener{
listener: ln,
host: host,
binding: fmt.Sprintf("ncacn_ip_tcp:%s[%s]", host, port),
}
go g.serve()
return g
}
func (g *goExecCloseListener) serve() {
for {
conn, err := g.listener.Accept()
if err != nil {
return
}
_ = conn.Close()
}
}
func (g *goExecCloseListener) Close() {
_ = g.listener.Close()
}
type javascriptRsyncTest struct{}
func (j *javascriptRsyncTest) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("873/tcp", &dockertest.RunOptions{
Repository: "alpine",
Tag: "latest",
Cmd: []string{"sh", "-c", "apk add --no-cache rsync shadow && useradd -m rsyncuser && echo 'rsyncuser:mysecret' | chpasswd && echo 'rsyncuser:MySecret123' > /etc/rsyncd.secrets && chmod 600 /etc/rsyncd.secrets && echo -e '[data]\\n path = /data\\n comment = Local Rsync Share\\n read only = false\\n auth users = rsyncuser\\n secrets file = /etc/rsyncd.secrets' > /etc/rsyncd.conf && mkdir -p /data && exec rsync --daemon --no-detach --config=/etc/rsyncd.conf"},
}, javascriptServiceReadyTimeout, 0, nil))
}
type javascriptTelnetAuthTest struct{}
func (j *javascriptTelnetAuthTest) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("23/tcp", &dockertest.RunOptions{
Repository: "alpine",
Tag: "latest",
Cmd: []string{"sh", "-c", "apk add --no-cache busybox-extras shadow && useradd -m dev && echo 'dev:mysecret' | chpasswd && exec /usr/sbin/telnetd -F -p 23 -l /bin/login"},
}, javascriptServiceReadyTimeout, 0, nil))
}
type networkMultiStep struct{}
func (j *networkMultiStep) Execute(filePath string) error {
errState := &capturedError{}
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 5, 5*time.Second)
if err != nil {
errState.Set(err)
return
}
if string(data) == "FIRST" {
_, _ = conn.Write([]byte("PING"))
}
data, err = reader.ConnReadNWithTimeout(conn, 6, 5*time.Second)
if err != nil {
errState.Set(err)
return
}
if string(data) == "SECOND" {
_, _ = conn.Write([]byte("PONG"))
}
_, _ = conn.Write([]byte("NUCLEI"))
})
defer server.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, server.URL, debug)
if err != nil {
return err
}
if err := errState.Err(); err != nil {
return err
}
if debug {
return expectResultsCount(results, 3)
}
return expectResultsCount(results, 1)
}
func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expectedNumbers ...int) error {
if len(expectedNumbers) == 0 {
expectedNumbers = []int{1}
}
if spec.options == nil {
return fmt.Errorf("missing docker options for %s", filePath)
}
pool, err := dockertest.NewPool("")
if err != nil {
return fmt.Errorf("could not create docker pool: %w", err)
}
if err := pool.Client.Ping(); err != nil {
return fmt.Errorf("could not connect to Docker: %w", err)
}
resource, err := pool.RunWithOptions(spec.options)
if err != nil {
return fmt.Errorf("could not start resource for %s: %w", filePath, err)
}
defer purge(pool, resource)
if err := resource.Expire(javascriptContainerTTLSeconds); err != nil {
return fmt.Errorf("could not expire resource for %s: %w", filePath, err)
}
mappedPort := resource.GetPort(spec.port)
if mappedPort == "" {
return fmt.Errorf("missing mapped port for %s", spec.port)
}
targetAddress := net.JoinHostPort("127.0.0.1", mappedPort)
if err := waitForTCPService(targetAddress, spec.readyTimeout); err != nil {
return err
}
if spec.readyCheck != nil {
if err := waitForServiceCheck(targetAddress, spec.readyTimeout, spec.readyCheck); err != nil {
return err
}
}
if spec.settleDelay > 0 {
time.Sleep(spec.settleDelay)
}
errS := make([]error, 0, defaultRetry)
for attempt := 1; attempt <= defaultRetry; attempt++ {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, targetAddress, debug)
if err == nil {
if countErr := expectResultsCount(results, expectedNumbers...); countErr == nil {
return nil
} else {
err = countErr
}
}
errS = append(errS, err)
if attempt < defaultRetry {
time.Sleep(javascriptRetryDelay)
}
}
return multierr.Combine(errS...)
}
func waitForTCPService(address string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
var lastErr error
for time.Now().Before(deadline) {
conn, err := net.DialTimeout("tcp", address, javascriptDialTimeout)
if err == nil {
_ = conn.Close()
return nil
}
lastErr = err
time.Sleep(javascriptRetryDelay)
}
if lastErr == nil {
lastErr = fmt.Errorf("timed out waiting for %s", address)
}
return fmt.Errorf("service %s did not become ready: %w", address, lastErr)
}
func waitForServiceCheck(address string, timeout time.Duration, check javascriptDockerReadyCheck) error {
deadline := time.Now().Add(timeout)
var lastErr error
for time.Now().Before(deadline) {
if err := check(address); err == nil {
return nil
} else {
lastErr = err
}
time.Sleep(javascriptRetryDelay)
}
if lastErr == nil {
lastErr = fmt.Errorf("timed out waiting for %s", address)
}
return fmt.Errorf("service %s did not become fully ready: %w", address, lastErr)
}
// smbReadyCheck sends a minimal SMB2 NEGOTIATE request and verifies the
// response carries the SMB2 protocol marker (0xFE 'S' 'M' 'B'). This lets us
// wait until samba's smbd is actually answering, not just until the kernel
// has bound port 445.
func smbReadyCheck() javascriptDockerReadyCheck {
return func(address string) error {
conn, err := net.DialTimeout("tcp", address, javascriptDialTimeout)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(2 * time.Second))
// minimal SMB2 NEGOTIATE: NetBIOS length-prefixed SMB2 header with
// command=0x0000 (NEGOTIATE) and a 36-byte structure that advertises
// support for dialect 0x0202 (SMB 2.0.2). Crafted byte-for-byte so we
// avoid pulling an SMB client library into the test binary.
req := []byte{
0x00, 0x00, 0x00, 0x66, // NetBIOS session length = 102 (64 header + 38 negotiate)
0xfe, 'S', 'M', 'B', // SMB2 protocol marker
0x40, 0x00, // Structure size
0x00, 0x00, // Credit charge
0x00, 0x00, 0x00, 0x00, // Status
0x00, 0x00, // Command = NEGOTIATE (0)
0x00, 0x00, // Credits requested
0x00, 0x00, 0x00, 0x00, // Flags
0x00, 0x00, 0x00, 0x00, // Next command
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Message ID
0x00, 0x00, 0x00, 0x00, // Reserved
0x00, 0x00, 0x00, 0x00, // Tree ID
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Session ID
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature (16 bytes)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x24, 0x00, // NEGOTIATE structure size = 36
0x01, 0x00, // DialectCount = 1
0x00, 0x00, // SecurityMode
0x00, 0x00, // Reserved
0x00, 0x00, 0x00, 0x00, // Capabilities
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ClientGuid (16)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ClientStartTime
0x02, 0x02, // Dialect = 0x0202 (SMB 2.0.2)
}
if _, err := conn.Write(req); err != nil {
return err
}
header := make([]byte, 8)
if _, err := io.ReadFull(conn, header); err != nil {
return fmt.Errorf("smb negotiate: read header: %w", err)
}
// header[4:8] should be the SMB2 protocol marker (skip the 4-byte NetBIOS prefix)
if header[4] != 0xFE || header[5] != 'S' || header[6] != 'M' || header[7] != 'B' {
return fmt.Errorf("smb negotiate: unexpected protocol marker %x", header[4:8])
}
return nil
}
}
func mysqlReadyCheck(username, password string) javascriptDockerReadyCheck {
return func(address string) error {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/information_schema?timeout=5s&readTimeout=5s&writeTimeout=5s", username, password, address)
db, err := sql.Open("mysql", dsn)
if err != nil {
return err
}
defer func() { _ = db.Close() }()
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(0)
return db.Ping()
}
}
func postgresReadyCheck(username, password string) javascriptDockerReadyCheck {
return func(address string) error {
db := pg.Connect(&pg.Options{
Addr: address,
User: username,
Password: password,
Database: "postgres",
}).WithTimeout(5 * time.Second)
defer func() { _ = db.Close() }()
_, err := db.Exec("select 1")
return err
}
}
func purge(pool *dockertest.Pool, resource *dockertest.Resource) {
if resource == nil || pool == nil {
return
}
containerName := resource.Container.Name
_ = pool.Client.StopContainer(resource.Container.ID, 0)
_ = pool.Purge(resource)
_ = pool.RemoveContainerByName(containerName)
}