mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-09 17:43:08 -10:00
437 lines
11 KiB
Go
437 lines
11 KiB
Go
package engine
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-rod/rod"
|
|
"github.com/go-rod/rod/lib/launcher"
|
|
"github.com/go-rod/rod/lib/proto"
|
|
"github.com/projectdiscovery/networkpolicy"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHeadlessProxy_BlocksExcludedHosts(t *testing.T) {
|
|
// 1. Setup blocked TCP server
|
|
blockedLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = blockedLn.Close()
|
|
}()
|
|
|
|
var blockedHits atomic.Int32
|
|
go func() {
|
|
for {
|
|
conn, err := blockedLn.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
blockedHits.Add(1)
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
blockedPort := blockedLn.Addr().(*net.TCPAddr).Port
|
|
|
|
// 2. Setup allowed TCP server
|
|
allowedLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = allowedLn.Close()
|
|
}()
|
|
|
|
var allowedHits atomic.Int32
|
|
go func() {
|
|
for {
|
|
conn, err := allowedLn.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
allowedHits.Add(1)
|
|
reqBuf := make([]byte, 512)
|
|
_, _ = conn.Read(reqBuf)
|
|
_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"))
|
|
time.Sleep(50 * time.Millisecond)
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
allowedPort := allowedLn.Addr().(*net.TCPAddr).Port
|
|
|
|
// 3. Configure network policy with blocked target
|
|
execID := fmt.Sprintf("test-proxy-%d", time.Now().UnixNano())
|
|
opts := &types.Options{
|
|
ExecutionId: execID,
|
|
ExcludeTargets: []string{
|
|
fmt.Sprintf("127.0.0.1:%d", blockedPort),
|
|
},
|
|
}
|
|
|
|
np, err := networkpolicy.New(networkpolicy.Options{
|
|
DenyList: []string{
|
|
fmt.Sprintf("127.0.0.1:%d", blockedPort),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
d := &protocolstate.Dialers{
|
|
NetworkPolicy: np,
|
|
ExcludeTargets: opts.ExcludeTargets,
|
|
}
|
|
_ = protocolstate.Init(opts)
|
|
// Override with our strict policy for this test
|
|
if existing := protocolstate.GetDialersWithId(execID); existing != nil {
|
|
existing.NetworkPolicy = np
|
|
} else {
|
|
// Mock dialer in case Init didn't populate for test
|
|
_ = d
|
|
}
|
|
|
|
// 4. Start headless proxy
|
|
proxy, err := newHeadlessProxy(opts)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = proxy.Close()
|
|
}()
|
|
|
|
// 5. Test CONNECT to blocked destination
|
|
clientConn, err := net.Dial("tcp", proxy.Addr())
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = clientConn.Close()
|
|
}()
|
|
|
|
connectReq := fmt.Sprintf("CONNECT 127.0.0.1:%d HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n\r\n", blockedPort, blockedPort)
|
|
_, err = clientConn.Write([]byte(connectReq))
|
|
require.NoError(t, err)
|
|
|
|
reader := bufio.NewReader(clientConn)
|
|
resp, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
require.Zero(t, blockedHits.Load(), "blocked host must never receive connection")
|
|
|
|
// 6. Test GET to blocked destination
|
|
clientConn2, err := net.Dial("tcp", proxy.Addr())
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = clientConn2.Close()
|
|
}()
|
|
|
|
getReq := fmt.Sprintf("GET http://127.0.0.1:%d/ HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n\r\n", blockedPort, blockedPort)
|
|
_, err = clientConn2.Write([]byte(getReq))
|
|
require.NoError(t, err)
|
|
|
|
reader2 := bufio.NewReader(clientConn2)
|
|
resp2, err := http.ReadResponse(reader2, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusForbidden, resp2.StatusCode)
|
|
require.Zero(t, blockedHits.Load(), "blocked host must never receive connection")
|
|
|
|
// 7. Test CONNECT to allowed destination
|
|
clientConn3, err := net.Dial("tcp", proxy.Addr())
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = clientConn3.Close()
|
|
}()
|
|
|
|
allowedReq := fmt.Sprintf("CONNECT 127.0.0.1:%d HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n\r\n", allowedPort, allowedPort)
|
|
_, err = clientConn3.Write([]byte(allowedReq))
|
|
require.NoError(t, err)
|
|
|
|
reader3 := bufio.NewReader(clientConn3)
|
|
resp3, err := http.ReadResponse(reader3, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp3.StatusCode)
|
|
|
|
// Send data through tunnel
|
|
_, err = clientConn3.Write([]byte("GET / HTTP/1.1\r\nHost: test\r\n\r\n"))
|
|
require.NoError(t, err)
|
|
|
|
buf := make([]byte, 1024)
|
|
n, err := reader3.Read(buf)
|
|
// Windows can return the tunneled bytes and a connection-abort error
|
|
// once the target closes and the proxy tears the tunnel down.
|
|
require.Contains(t, string(buf[:n]), "200 OK")
|
|
if n == 0 {
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, int32(1), allowedHits.Load())
|
|
}
|
|
|
|
func TestHeadlessProxy_RealChromeWebSocketEndToEnd(t *testing.T) {
|
|
// 1. Setup blocked WebSocket server
|
|
blockedLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = blockedLn.Close()
|
|
}()
|
|
|
|
var blockedHits atomic.Int32
|
|
go func() {
|
|
for {
|
|
conn, err := blockedLn.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
blockedHits.Add(1)
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
blockedPort := blockedLn.Addr().(*net.TCPAddr).Port
|
|
|
|
// 2. Configure network policy with blocked target
|
|
execID := fmt.Sprintf("test-chrome-ws-%d", time.Now().UnixNano())
|
|
opts := &types.Options{
|
|
ExecutionId: execID,
|
|
ExcludeTargets: []string{
|
|
fmt.Sprintf("127.0.0.1:%d", blockedPort),
|
|
},
|
|
ShowBrowser: false,
|
|
}
|
|
|
|
np, err := networkpolicy.New(networkpolicy.Options{
|
|
DenyList: []string{
|
|
fmt.Sprintf("127.0.0.1:%d", blockedPort),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_ = protocolstate.Init(opts)
|
|
if existing := protocolstate.GetDialersWithId(execID); existing != nil {
|
|
existing.NetworkPolicy = np
|
|
}
|
|
|
|
// 3. Start headless proxy
|
|
proxy, err := newHeadlessProxy(opts)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = proxy.Close()
|
|
}()
|
|
|
|
// 4. Launch Chrome with proxy and <-loopback>
|
|
chromeLauncher := launcher.New().
|
|
Leakless(false).
|
|
Headless(true).
|
|
Proxy(proxy.Addr()).
|
|
Set("proxy-bypass-list", "<-loopback>")
|
|
|
|
if bin, hasChrome := launcher.LookPath(); hasChrome {
|
|
chromeLauncher.Bin(bin)
|
|
} else {
|
|
t.Skip("Chrome binary not found on system, skipping end-to-end browser test")
|
|
}
|
|
|
|
controlURL, err := chromeLauncher.Launch()
|
|
if err != nil {
|
|
t.Skipf("Chrome launch failed in test environment: %v", err)
|
|
}
|
|
|
|
browser := rod.New().ControlURL(controlURL).MustConnect()
|
|
defer func() {
|
|
_ = browser.Close()
|
|
}()
|
|
|
|
page, err := browser.Page(proto.TargetCreateTarget{URL: "about:blank"})
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = page.Close()
|
|
}()
|
|
|
|
script := fmt.Sprintf(`() => new Promise((resolve) => {
|
|
try {
|
|
const ws = new WebSocket("ws://127.0.0.1:%d/");
|
|
ws.onopen = () => resolve("OPEN");
|
|
ws.onerror = () => resolve("ERROR");
|
|
setTimeout(() => resolve("TIMEOUT"), 3000);
|
|
} catch (err) {
|
|
resolve("ERROR");
|
|
}
|
|
})`, blockedPort)
|
|
|
|
res, err := page.Eval(script)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ERROR", res.Value.String())
|
|
require.Zero(t, blockedHits.Load(), "blocked WebSocket server must never receive connections from Chrome")
|
|
}
|
|
|
|
func TestHeadlessProxy_IPv4MappedIPv6WithUpstreamProxy(t *testing.T) {
|
|
// 1. Setup mock upstream HTTP proxy
|
|
upstreamLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = upstreamLn.Close()
|
|
}()
|
|
|
|
var upstreamHits atomic.Int32
|
|
go func() {
|
|
for {
|
|
conn, err := upstreamLn.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
upstreamHits.Add(1)
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
// 2. Configure options with upstream proxy and restricted local network / excluded 127.0.0.1
|
|
execID := fmt.Sprintf("test-mapped-proxy-%d", time.Now().UnixNano())
|
|
opts := &types.Options{
|
|
ExecutionId: execID,
|
|
AliveHttpProxy: "http://" + upstreamLn.Addr().String(),
|
|
ExcludeTargets: []string{"127.0.0.1"},
|
|
RestrictLocalNetworkAccess: true,
|
|
}
|
|
|
|
err = protocolstate.Init(opts)
|
|
require.NoError(t, err)
|
|
|
|
// 3. Start headless proxy
|
|
proxy, err := newHeadlessProxy(opts)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = proxy.Close()
|
|
}()
|
|
|
|
// 4. Test mapped IPv6 variations
|
|
targets := []string{
|
|
"[::ffff:127.0.0.1]:9999",
|
|
"[0:0:0:0:0:ffff:127.0.0.1]:9999",
|
|
"[::ffff:169.254.169.254]:80",
|
|
}
|
|
|
|
for _, target := range targets {
|
|
t.Run(target, func(t *testing.T) {
|
|
conn, err := net.Dial("tcp", proxy.Addr())
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = conn.Close()
|
|
}()
|
|
|
|
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", target, target)
|
|
_, err = conn.Write([]byte(connectReq))
|
|
require.NoError(t, err)
|
|
|
|
reader := bufio.NewReader(conn)
|
|
resp, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// Assert upstream proxy never received any connection for blocked mapped literals
|
|
require.Zero(t, upstreamHits.Load(), "upstream proxy must not be reached for denied mapped literals")
|
|
}
|
|
|
|
func TestHeadlessProxy_ConnectKeepsBufferedBytes(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
got := make(chan string, 1)
|
|
go func() {
|
|
conn, acceptErr := ln.Accept()
|
|
if acceptErr != nil {
|
|
return
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
|
buf := make([]byte, 4)
|
|
n, _ := io.ReadFull(conn, buf)
|
|
got <- string(buf[:n])
|
|
_, _ = conn.Write([]byte("PONG"))
|
|
}()
|
|
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
proxy := startHeadlessProxy(t, nil)
|
|
conn, err := net.Dial("tcp", proxy.Addr())
|
|
require.NoError(t, err)
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
payload := fmt.Sprintf("CONNECT 127.0.0.1:%d HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n\r\nPING", port, port)
|
|
_, err = conn.Write([]byte(payload))
|
|
require.NoError(t, err)
|
|
|
|
reader := bufio.NewReader(conn)
|
|
resp, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
select {
|
|
case body := <-got:
|
|
require.Equal(t, "PING", body, "bytes sent with CONNECT must reach the target")
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("target did not receive the bytes sent with CONNECT")
|
|
}
|
|
|
|
reply := make([]byte, 4)
|
|
_, err = reader.Read(reply)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "PONG", string(reply))
|
|
}
|
|
|
|
func TestHeadlessProxy_RefusesNameResolvingToExcludedIP(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() { _ = ln.Close() }()
|
|
|
|
var hits atomic.Int32
|
|
go func() {
|
|
for {
|
|
conn, acceptErr := ln.Accept()
|
|
if acceptErr != nil {
|
|
return
|
|
}
|
|
hits.Add(1)
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
proxy := startHeadlessProxy(t, []string{"127.0.0.1"})
|
|
|
|
conn, err := net.Dial("tcp", proxy.Addr())
|
|
require.NoError(t, err)
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
req := fmt.Sprintf("CONNECT localhost:%d HTTP/1.1\r\nHost: localhost:%d\r\n\r\n", port, port)
|
|
_, err = conn.Write([]byte(req))
|
|
require.NoError(t, err)
|
|
|
|
reader := bufio.NewReader(conn)
|
|
resp, err := http.ReadResponse(reader, nil)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, http.StatusOK, resp.StatusCode)
|
|
require.Zero(t, hits.Load(), "a name that resolves to an excluded address must not be dialed")
|
|
}
|
|
|
|
func startHeadlessProxy(t *testing.T, deny []string) *headlessProxy {
|
|
t.Helper()
|
|
execID := fmt.Sprintf("test-proxy-%d", time.Now().UnixNano())
|
|
opts := &types.Options{
|
|
ExecutionId: execID,
|
|
ExcludeTargets: deny,
|
|
}
|
|
np, err := networkpolicy.New(networkpolicy.Options{DenyList: deny})
|
|
require.NoError(t, err)
|
|
_ = protocolstate.Init(opts)
|
|
if existing := protocolstate.GetDialersWithId(execID); existing != nil {
|
|
existing.NetworkPolicy = np
|
|
existing.ExcludeTargets = deny
|
|
}
|
|
proxy, err := newHeadlessProxy(opts)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = proxy.Close() })
|
|
return proxy
|
|
}
|