diff --git a/CHANGELOG.txt b/CHANGELOG.txt
@@ -5,6 +5,7 @@
- feat(scan): add nuclei module for vulnerability scanning (opt-in via --nuclei; installs templates up front)
- change(cli): gobuster is now opt-in via --gobuster (replaces the default-on --disable-dirbusting)
- feat(osint): add gau historical URL discovery (saved to urls.txt; discovered hostnames merged into domains.txt)
+- feat(osint): add alterx active subdomain permutation (generates + resolves via dnsx within a time budget; live finds merged into domains.txt)
- fix(scan): perform the real robots.txt fetch (was duplicating the sitemap scan)
- fix(net): add HTTP client timeouts and close response bodies (prevents hangs and fd leaks)
- fix(net): keep running when the public IP lookup fails instead of exiting the program
diff --git a/README.md b/README.md
@@ -66,7 +66,6 @@ scan:
osint:
-* `alterx` (projectdiscovery) subdomain permutations to feed dnsx
* `asnmap` (projectdiscovery) domain/org to ASN to netblocks for scope expansion
other:
@@ -144,6 +143,9 @@ Run various scans to retrieve more targets, using OSINT techniques.
osint also runs `gau` to pull historical URLs (Wayback, CommonCrawl, OTX, URLScan).
+osint also runs `alterx`: it generates permutations of the discovered subdomains, resolves
+them (time-bounded), and merges the live ones into `domains.txt` (and `alterx.txt`).
+
## 4.c Run active scans
Run scans against the target actively. (You might want to use a proxy for this !)
diff --git a/cli/parser.go b/cli/parser.go
@@ -143,7 +143,7 @@ func GetParser(opts *StandardOptions) *cobra.Command {
var osintCmd = &cobra.Command{
Use: "osint",
Short: "Run OSINT tools to retrieve IP addresses and interesting assets",
- Long: "Runs dorks, subfinder, assetfinder, dnsx & gau to find subdomains, assets & historical URLs",
+ Long: "Runs dorks, subfinder, assetfinder, dnsx, gau & alterx to find subdomains, assets & historical URLs",
Args: cobra.MinimumNArgs(0),
Run: func(cmd *cobra.Command, args []string) {
diff --git a/go.mod b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/deckarep/golang-set/v2 v2.3.0
github.com/lc/gau/v2 v2.2.4
github.com/praetorian-inc/nerva v1.18.0
+ github.com/projectdiscovery/alterx v0.1.0
github.com/projectdiscovery/dnsx v1.2.3
github.com/projectdiscovery/goflags v0.1.74
github.com/projectdiscovery/httpx v1.9.0
diff --git a/go.sum b/go.sum
@@ -875,6 +875,8 @@ github.com/praetorian-inc/fingerprintx v1.1.15 h1:CVIxrIQARbmdk5h8E9tIJZbvFoY2sG
github.com/praetorian-inc/fingerprintx v1.1.15/go.mod h1:hqRroITBwKpP8BOGF+n/A+qv9wSF7OSVinmu5NCyOUI=
github.com/praetorian-inc/nerva v1.18.0 h1:KwvgHtfqd69afnqKKtXbBmiKx2X5bLWZ4LfcK5KAUX8=
github.com/praetorian-inc/nerva v1.18.0/go.mod h1:FHEc0IZVvyCu8NjjErOsd7rY2e4Yo76apBeFdWpYSBE=
+github.com/projectdiscovery/alterx v0.1.0 h1:oMi/CV/RcaIvg21PbqBYqlGDtXodvEfPohQ8Y2BthO0=
+github.com/projectdiscovery/alterx v0.1.0/go.mod h1:PNilhIp5zjvgKD8A0sZ56RyZRBcOhCWlVp2O299nc4M=
github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kID2iwsDqI=
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
github.com/projectdiscovery/awesome-search-queries v0.0.0-20260411001519-47ff919ec921 h1:8LLtUicd7P3A+DrY2PUlq4DGkttViWnu/QbQ5ooo8Zc=
diff --git a/osint/alterx.go b/osint/alterx.go
@@ -0,0 +1,141 @@
+package osint
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "evil.djnn.sh/djnn/yellow/core"
+ "github.com/projectdiscovery/alterx"
+ "github.com/projectdiscovery/dnsx/libs/dnsx"
+)
+
+const (
+ alterxLimit = 5000
+ alterxWorkers = 40
+ alterxBudget = 5 * time.Minute
+)
+
+type Alterx struct{}
+
+func (*Alterx) Name() string { return "alterx" }
+
+func (*Alterx) Run(ctx *core.Context) error {
+ fmt.Println("[+] Running alterx on", ctx.Domain)
+ if ctx.DryRun {
+ return nil
+ }
+
+ var inputs []string
+ for _, d := range ctx.Domains {
+ if d != "" && net.ParseIP(d) == nil {
+ inputs = append(inputs, d)
+ }
+ }
+ if len(inputs) == 0 {
+ fmt.Println("[!] alterx: no subdomains to permute, skipping")
+ return nil
+ }
+
+ mutator, err := alterx.New(&alterx.Options{Domains: inputs, Enrich: true})
+ if err != nil {
+ fmt.Printf("[!] alterx: %v\n", err)
+ return nil
+ }
+
+ // Execute ignores Options.Limit; cap here, draining fully so the producer can't leak
+ var candidates []string
+ for c := range mutator.Execute(context.Background()) {
+ if len(candidates) < alterxLimit {
+ candidates = append(candidates, c)
+ }
+ }
+ if len(candidates) == 0 {
+ fmt.Println("[!] alterx: generated no candidates, skipping")
+ return nil
+ }
+
+ live := resolveLive(candidates)
+
+ existing := map[string]struct{}{}
+ for _, d := range ctx.Domains {
+ existing[d] = struct{}{}
+ }
+ var added []string
+ for _, h := range live {
+ if _, ok := existing[h]; ok {
+ continue
+ }
+ existing[h] = struct{}{}
+ added = append(added, h)
+ }
+ ctx.Domains = append(ctx.Domains, added...)
+
+ outfile := fmt.Sprintf("%s/alterx.txt", ctx.ScanPath)
+ content := strings.Join(added, "\n")
+ if content != "" {
+ content += "\n"
+ }
+ if err := os.WriteFile(outfile, []byte(content), 0644); err != nil {
+ fmt.Printf("[!] alterx: could not write %s: %v\n", outfile, err)
+ }
+
+ fmt.Printf("[OSINT %s] alterx: %d candidates, %d live, %d new in %s\n", ctx.Domain, len(candidates), len(live), len(added), outfile)
+ return nil
+}
+
+func resolveLive(candidates []string) []string {
+ opts := dnsx.DefaultOptions
+ opts.MaxRetries = 2
+ client, err := dnsx.New(opts)
+ if err != nil {
+ fmt.Printf("[!] alterx: dns resolver init failed: %v\n", err)
+ return nil
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), alterxBudget)
+ defer cancel()
+
+ jobs := make(chan string)
+ var (
+ mu sync.Mutex
+ live []string
+ wg sync.WaitGroup
+ )
+ for i := 0; i < alterxWorkers; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case name, ok := <-jobs:
+ if !ok {
+ return
+ }
+ if ips, err := client.Lookup(name); err == nil && len(ips) > 0 {
+ mu.Lock()
+ live = append(live, name)
+ mu.Unlock()
+ }
+ }
+ }
+ }()
+ }
+dispatch:
+ for _, c := range candidates {
+ select {
+ case <-ctx.Done():
+ break dispatch
+ case jobs <- c:
+ }
+ }
+ close(jobs)
+ wg.Wait()
+ return live
+}
diff --git a/osint/run.go b/osint/run.go
@@ -1,7 +1,6 @@
package osint
import (
- "bytes"
"fmt"
"net"
"net/url"
@@ -45,7 +44,6 @@ func (opts *OsintOpts) SetEmailsFile(data string) {
opts.emailsFile = data
}
-// context builds a Context from the current options.
func (opts *OsintOpts) context() *core.Context {
return &core.Context{
Domain: opts.domain,
@@ -85,29 +83,29 @@ func (opts *OsintOpts) Run() {
&Leaker{},
})
- // aggregate the assets the enumeration modules wrote, then let shodan
- // consume the unique list.
- domains, buf := opts.aggregateDomains()
- ctx.Domains = domains
+ ctx.Domains = opts.aggregateDomains()
- core.RunModules(ctx, []core.Module{&Shodan{}})
+ core.RunModules(ctx, []core.Module{&Alterx{}, &Shodan{}})
uniqueOutfile := fmt.Sprintf("%s/domains.txt", opts.scanPath)
- if err := os.WriteFile(uniqueOutfile, buf, 0644); err != nil {
+ content := strings.Join(ctx.Domains, "\n")
+ if content != "" {
+ content += "\n"
+ }
+ if err := os.WriteFile(uniqueOutfile, []byte(content), 0644); err != nil {
fmt.Printf("[!] osint: could not write %s: %v\n", uniqueOutfile, err)
return
}
- fmt.Printf("[OSINT %s] Registered %v IP addresses and assets.\n", opts.domain, len(domains))
+ fmt.Printf("[OSINT %s] Registered %v IP addresses and assets.\n", opts.domain, len(ctx.Domains))
fmt.Printf("[OSINT %s] Location of unique domain names: %s\n", opts.domain, uniqueOutfile)
fmt.Printf("[OSINT %s] done.\n", opts.domain)
opts.RunCleanup()
}
-func (opts *OsintOpts) aggregateDomains() ([]string, []byte) {
+func (opts *OsintOpts) aggregateDomains() []string {
var domains []string
- var domainBuffer bytes.Buffer
seen := map[string]struct{}{}
add := func(candidate string) {
@@ -118,12 +116,10 @@ func (opts *OsintOpts) aggregateDomains() ([]string, []byte) {
if _, ok := seen[candidate]; ok {
return
}
- // IPv4, or something that roughly looks like a domain; IPv6 carries colons that break downstream
ip := net.ParseIP(candidate)
if (ip != nil && ip.To4() != nil) || (ip == nil && !helper.StringHasUnwantedCharactersForDomainName(candidate)) {
seen[candidate] = struct{}{}
domains = append(domains, candidate)
- domainBuffer.WriteString(candidate + "\n")
}
}
@@ -151,5 +147,5 @@ func (opts *OsintOpts) aggregateDomains() ([]string, []byte) {
}
}
- return domains, domainBuffer.Bytes()
+ return domains
}
| | | | | | |