Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ echo target.com | httpx -path '/.well-known/security.txt,/.well-known/change-pas

- As default, `httpx` probe with **HTTPS** scheme and fall-back to **HTTP** only if **HTTPS** is not reachable.
- Burp Suite XML exports can be used as input with `-l burp-export.xml -im burp`
- A target can be probed through a given IP with a JSON input line, like `curl --resolve`. The hostname is still used for the Host header and SNI, and the connection goes to the IP, which `host_ip` reports. The same host can be listed with several IPs:
```
{"target": "https://app.example.com", "ip": "192.0.2.10"}
{"target": "https://app.example.com", "ip": "192.0.2.11"}
```
Only the `target` and `ip` keys are accepted. An invalid line stops the run, or is skipped with a warning with `-stream`. Read such lines from `-l` or stdin, as `-u` splits its value on commas. They cannot be combined with `-proxy`, `-unsafe`, `-pipeline`, `-http2`, `-jarm`, `-screenshot` or `-vhost-input`, which do not connect through the IP; a proxy set with `HTTP_PROXY`/`HTTPS_PROXY` resolves the host itself as well.
- The `-no-fallback` flag can be used to probe and display both **HTTP** and **HTTPS** result.
- Custom scheme for ports can be defined, for example `-ports http:443,http:80,https:8443`
- Custom resolver supports multiple protocol (**doh|tcp|udp**) in form of `protocol:resolver:port` (e.g. `udp:127.0.0.1:53`)
Expand Down
3 changes: 2 additions & 1 deletion common/httpx/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ func (h *HTTPX) ZTLSGrab(r *http.Response) *clients.Response {
}
// canonical net concatenation
host = net.JoinHostPort(hostname, fmt.Sprint(port))
ctx, cancel := context.WithTimeout(context.Background(), h.Options.Timeout)
// keeps the ip the request was pinned to, without its cancellation
ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Request.Context()), h.Options.Timeout)
defer cancel()
tlsConn, err := h.Dialer.DialTLS(ctx, "tcp", host)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ require (
github.com/projectdiscovery/gologger v1.1.73
github.com/projectdiscovery/hmap v0.0.102
github.com/projectdiscovery/mapcidr v1.1.97
github.com/projectdiscovery/networkpolicy v0.1.53
github.com/projectdiscovery/networkpolicy v0.1.54
github.com/projectdiscovery/ratelimit v0.0.92
github.com/projectdiscovery/rawhttp v0.1.92
github.com/projectdiscovery/retryablehttp-go v1.3.29
github.com/projectdiscovery/tlsx v1.4.0
github.com/projectdiscovery/useragent v0.0.109
github.com/projectdiscovery/utils v0.11.8
github.com/projectdiscovery/utils v0.11.9
github.com/projectdiscovery/wappalyzergo v0.3.4
github.com/rs/xid v1.6.0
github.com/spaolacci/murmur3 v1.1.0
Expand Down Expand Up @@ -169,3 +169,5 @@ require (
golang.org/x/time v0.15.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

replace github.com/projectdiscovery/fastdialer => github.com/mazzz3r/fastdialer v0.0.0-20261013195407-11ed52b6df6c
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mazzz3r/fastdialer v0.0.0-20261013195407-11ed52b6df6c h1:Bf6RjxuR/g7zniyJX7pJfh6y/FYx+KCPrxRtCVgaxQs=
github.com/mazzz3r/fastdialer v0.0.0-20261013195407-11ed52b6df6c/go.mod h1:CZZ0PwdTw2KCI/PrZTW9wyBY88MIjpT8WhsKWSw3bkw=
github.com/mfonda/simhash v0.0.0-20151007195837-79f94a1100d6 h1:bjfMeqxWEJ6IRUvGkiTkSwx0a6UdQJsbirRSoXogteY=
github.com/mfonda/simhash v0.0.0-20151007195837-79f94a1100d6/go.mod h1:WVJJvUw/pIOcwu2O8ZzHEhmigq2jzwRNfJVRMJB7bR8=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
Expand Down Expand Up @@ -256,8 +258,6 @@ github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9Mch
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
github.com/projectdiscovery/fastdialer v0.5.23 h1:uL0ILvwrOv52BK4XXFM7CyakR+0d3tmc0m1R+i8NelI=
github.com/projectdiscovery/fastdialer v0.5.23/go.mod h1:psWXmdbZhGxmVLNP2o1YI93R8d2YtOwK5XSNhlWxXJ8=
github.com/projectdiscovery/fdmax v0.0.4 h1:K9tIl5MUZrEMzjvwn/G4drsHms2aufTn1xUdeVcmhmc=
github.com/projectdiscovery/fdmax v0.0.4/go.mod h1:oZLqbhMuJ5FmcoaalOm31B1P4Vka/CqP50nWjgtSz+I=
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
Expand All @@ -280,8 +280,8 @@ github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 h1:eR+0
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582/go.mod h1:3G3BRKui7nMuDFAZKR/M2hiOLtaOmyukT20g88qRQjI=
github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8QuWs2puwy+2w=
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
github.com/projectdiscovery/networkpolicy v0.1.53 h1:Oduo0GXFm0aSWqkbkyVXRjwT+GitqnqEEV0KiEtR2aM=
github.com/projectdiscovery/networkpolicy v0.1.53/go.mod h1:+4ef+If67QABeDgSEoOxOgs5ZiM7ygaEeYQPkaZkLDQ=
github.com/projectdiscovery/networkpolicy v0.1.54 h1:+v7yyuRBu2S7dzeIJToDENy1i+HjRhNzqnQrDlm00z8=
github.com/projectdiscovery/networkpolicy v0.1.54/go.mod h1:tN+YAFVlJjHv/znPEhkcSAjmBMkMhslC9XpWKTrKEHw=
github.com/projectdiscovery/ratelimit v0.0.92 h1:2h2/AU81Lbn/xevb4KtZCMhaQej+u7W3uLXFUD163iU=
github.com/projectdiscovery/ratelimit v0.0.92/go.mod h1:Tqpx1QKdFI63L01T2gpSvV5Uro9CnZkZWr80ciSXSBY=
github.com/projectdiscovery/rawhttp v0.1.92 h1:38GRsVLNllES/dZcEcA6s6P+rYBp34H9mDphMJMDvvg=
Expand All @@ -296,8 +296,8 @@ github.com/projectdiscovery/tlsx v1.4.0 h1:OU2mNK6TOB/YiFX3jhmgwOepFWxPoj5rn8npW
github.com/projectdiscovery/tlsx v1.4.0/go.mod h1:WCrUXAHjdKbDNjsu5EbVBS7qvzap3fxi/E85v/Flyx8=
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
github.com/projectdiscovery/utils v0.11.8 h1:oBP3eRG/S/Pdid6CkL58IVyq+KZdWdXohDtEKIKvWag=
github.com/projectdiscovery/utils v0.11.8/go.mod h1:AcQ1n3ETcYj4cHiPXsSaAyFnvN4Bn8UgN5tCmh+7KWM=
github.com/projectdiscovery/utils v0.11.9 h1:aKLje3zPcZuOGmopN51PasWxPX/CypPwzP2ke/Qa2bA=
github.com/projectdiscovery/utils v0.11.9/go.mod h1:d3BcbsHhuxW9T3jpvrgzwshvqhaL9qPSkoi+3h/DY8E=
github.com/projectdiscovery/wappalyzergo v0.3.4 h1:yO+xel+C0d/vOFWcUKVZRpIDAfcEmsjpVDjQ0jifAUw=
github.com/projectdiscovery/wappalyzergo v0.3.4/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
Expand Down
80 changes: 49 additions & 31 deletions runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,10 @@ func (r *Runner) prepareInput() {
// check if input target host(s) have been provided
if len(r.options.InputTargetHost) > 0 {
for _, target := range r.options.InputTargetHost {
target, err := r.normalizeInput(target)
if err != nil {
gologger.Fatal().Msgf("Could not read input target: %s\n", err)
}
expandedTarget, err := r.countTargetFromRawTarget(target)
if err == nil && expandedTarget > 0 {
numHosts += expandedTarget
Expand Down Expand Up @@ -722,6 +726,20 @@ func (r *Runner) streamInput() (chan string, error) {
return true
}
}
// sendLine normalizes, deduplicates and sends an input line, returning
// false if interrupted. The scan has already started, so an invalid
// line is skipped with a warning.
sendLine := func(item string) bool {
item, err := r.normalizeInput(item)
if err != nil {
gologger.Error().Msgf("Skipping input: %s\n", err)
return true
}
if r.options.SkipDedupe || r.testAndSet(item) {
return trySend(item)
}
return true
}

if fileutil.FileExists(r.options.InputFile) {
// check if input mode is specified for special format handling
Expand All @@ -744,14 +762,9 @@ func (r *Runner) streamInput() (chan string, error) {
}
} else {
for item, err := range fileutil.Lines(r.options.InputFile) {
if err != nil {
if err != nil || !sendLine(item) {
return
}
if r.options.SkipDedupe || r.testAndSet(item) {
if !trySend(item) {
return
}
}
}
}
} else if r.options.InputFile != "" {
Expand All @@ -761,27 +774,17 @@ func (r *Runner) streamInput() (chan string, error) {
}
for _, file := range files {
for item, err := range fileutil.Lines(file) {
if err != nil {
if err != nil || !sendLine(item) {
return
}
if r.options.SkipDedupe || r.testAndSet(item) {
if !trySend(item) {
return
}
}
}
}
}
if fileutil.HasStdin() {
for item, err := range fileutil.LinesReader(os.Stdin) {
if err != nil {
if err != nil || !sendLine(item) {
return
}
if r.options.SkipDedupe || r.testAndSet(item) {
if !trySend(item) {
return
}
}
}
}
}()
Expand All @@ -791,7 +794,11 @@ func (r *Runner) streamInput() (chan string, error) {
func (r *Runner) loadAndCloseFile(finput *os.File) (numTargets int, err error) {
scanner := bufio.NewScanner(finput)
for scanner.Scan() {
target := strings.TrimSpace(scanner.Text())
target, err := r.normalizeInput(scanner.Text())
if err != nil {
_ = finput.Close()
return 0, err
}
// Used just to get the exact number of targets
expandedTarget, err := r.countTargetFromRawTarget(target)
if err == nil && expandedTarget > 0 {
Expand Down Expand Up @@ -1524,7 +1531,7 @@ func (r *Runner) RunEnumeration() {

protocol := r.options.protocol
// attempt to parse url as is
if u, err := r.parseURL(k); err == nil {
if u, err := r.parseURL(targetOf(k)); err == nil {
if r.options.NoFallbackScheme && u.Scheme == httpx.HTTP || u.Scheme == httpx.HTTPS {
protocol = u.Scheme
}
Expand Down Expand Up @@ -1776,6 +1783,13 @@ func (r *Runner) targets(hp *httpx.HTTPX, target string) chan httpx.Target {
target = strings.TrimSpace(target)

switch {
case isTargetLine(target):
line, err := parseTargetLine(target)
if err != nil {
gologger.Warning().Msgf("%s\n", err)
return
}
results <- httpx.Target{Host: line.Target, CustomIP: line.IP}
case stringsutil.HasPrefixAny(target, "*", "."):
// A valid target does not contain:
// trim * and/or . (prefix) from the target to return the domain instead of wildcard
Expand Down Expand Up @@ -1852,6 +1866,10 @@ retry:

// check if we have to skip the host:port as a result of a previous failure
hostPort := net.JoinHostPort(URL.Host, URL.Port())
if target.CustomIP != "" {
// the ips of a host fail independently
hostPort += "@" + target.CustomIP
}
if r.options.HostMaxErrors >= 0 && r.HostErrorsCache.Has(hostPort) {
numberOfErrors, err := r.HostErrorsCache.GetIFPresent(hostPort)
if err == nil && numberOfErrors >= r.options.HostMaxErrors {
Expand Down Expand Up @@ -1880,16 +1898,11 @@ retry:
req *retryablehttp.Request
ctx context.Context
)
ctx = context.Background()
if target.CustomIP != "" {
var requestIP string
if iputil.IsIPv6(target.CustomIP) {
requestIP = fmt.Sprintf("[%s]", target.CustomIP)
} else {
requestIP = target.CustomIP
}
ctx = context.WithValue(context.Background(), fastdialer.IP, requestIP)
} else {
ctx = context.Background()
// only dials to the target's host go to the ip, not those to a
// redirect target on another host
ctx = fastdialer.WithResolveOverride(ctx, URL.Hostname(), target.CustomIP)
}
req, err = hp.NewRequestWithContext(ctx, method, URL.String())
if err != nil {
Expand Down Expand Up @@ -2761,6 +2774,11 @@ func (r *Runner) skip(URL *urlutil.URL, target httpx.Target, origInput string) (
return true, Result{URL: target.Host, Input: origInput, Err: errors.New("target host is not allowed by network policy")}
}

if target.CustomIP != "" && !r.hp.NetworkPolicy.Validate(target.CustomIP) {
gologger.Debug().Msgf("Skipping target due to network policy: %s (%s)\n", URL.Hostname(), target.CustomIP)
return true, Result{URL: target.Host, Input: origInput, Err: errors.New("target ip is not allowed by network policy")}
}

return false, Result{}
}

Expand Down Expand Up @@ -2811,8 +2829,8 @@ func (r *Runner) HandleFaviconHash(hp *httpx.HTTPX, req *retryablehttp.Request,
}
}

// Clone original request (reuse headers/cookies)
clone := req.Clone(context.Background())
// Clone original request (reuse headers/cookies, and the pinned ip)
clone := req.Clone(req.Context())

var (
faviconMMH3 string
Expand Down
Loading