diff --git a/.github/workflows/govulncheck.yaml b/.github/workflows/govulncheck.yaml
index 24fc0919f2..5d22f7fe52 100644
--- a/.github/workflows/govulncheck.yaml
+++ b/.github/workflows/govulncheck.yaml
@@ -19,7 +19,9 @@ jobs:
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go@v1
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- - run: govulncheck -scan package -format sarif ./... > $OUTPUT
+ - run: |
+ govulncheck -scan package -format sarif ./... | \
+ jq '(.runs[].tool.driver.rules[]?.properties.tags)? |= unique' > $OUTPUT
- uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: "${{ env.OUTPUT }}"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ad05027e86..633f8076b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,7 +20,26 @@ When creating a PR, please follow these guidelines:
## Code Style
-Please adhere to the existing coding style for consistency.
+Please adhere to the existing coding style for consistency.
+
+## Development
+
+To ensure your changes work as expected and strictly adhere to the project's standards, please run the following commands before submitting a PR:
+
+- **Run tests**:
+ ```sh
+ make test
+ ```
+
+- **Run linters/vet**:
+ ```sh
+ make vet
+ ```
+
+- **Build the project**:
+ ```sh
+ make build
+ ```
## Questions
diff --git a/README.md b/README.md
index 4b966d5a44..5dabc0e1e8 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,8 @@
`Indonesia` •
`Spanish` •
`日本語` •
- `Portuguese`
+ `Portuguese` •
+ `Türkçe`
@@ -308,6 +309,7 @@ HEADLESS:
-sb, -show-browser show the browser on the screen when running templates with headless mode
-ho, -headless-options string[] start headless chrome with additional options
-sc, -system-chrome use local installed Chrome browser instead of nuclei installed
+ -cdpe, -cdp-endpoint string use remote browser via Chrome DevTools Protocol (CDP) endpoint
-lha, -list-headless-action list available headless actions
DEBUG:
diff --git a/README_CN.md b/README_CN.md
index b9f3251aeb..5134bd57ed 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -263,6 +263,7 @@ UNCOVER引擎:
-sb, -show-brower 在无界面浏览器运行模板时,显示浏览器
-ho, -headless-options string[] 使用附加选项启动无界面浏览器
-sc, -system-chrome 不使用Nuclei自带的浏览器,使用本地浏览器
+ -cdpe, -cdp-endpoint string 通过Chrome DevTools Protocol (CDP)端点使用远程浏览器
-lha, -list-headless-action 列出可用的无界面操作
调试:
diff --git a/README_ES.md b/README_ES.md
index 3ad168c76c..ed80afe953 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -268,6 +268,7 @@ HEADLESS:
-sb, -show-browser muestra el navegador en la pantalla al ejecutar plantillas con modo sin interfaz
-ho, -headless-options string[] inicia Chrome en modo sin interfaz con opciones adicionales
-sc, -system-chrome utiliza el navegador Chrome instalado localmente en lugar del instalado por nuclei
+ -cdpe, -cdp-endpoint string usar navegador remoto a través del endpoint del Protocolo de Herramientas de Desarrollador de Chrome (CDP)
-lha, -list-headless-action lista de acciones sin interfaz disponibles
DEBUG:
diff --git a/README_ID.md b/README_ID.md
index feaca8fe3b..879e6ae7cc 100644
--- a/README_ID.md
+++ b/README_ID.md
@@ -234,6 +234,7 @@ HEADLESS:
-page-timeout int seconds to wait for each page in headless mode (default 20)
-sb, -show-browser show the browser on the screen when running templates with headless mode
-sc, -system-chrome use local installed Chrome browser instead of nuclei installed
+ -cdpe, -cdp-endpoint string use remote browser via Chrome DevTools Protocol (CDP) endpoint
-lha, -list-headless-action list available headless actions
DEBUG:
diff --git a/README_KR.md b/README_KR.md
index 204918cae7..fd375b7bdf 100644
--- a/README_KR.md
+++ b/README_KR.md
@@ -233,6 +233,7 @@ HEADLESS:
-sb, -show-browser headless 모드로 실행하는 템플릿에서 브라우저 화면 표시
-ho, -headless-options string[] 추가 옵션으로 headless chrome 시작
-sc, -system-chrome nuclei가 설치한 Chrome 대신 로컬에 설치된 Chrome 브라우저 사용
+ -cdpe, -cdp-endpoint string Chrome DevTools Protocol (CDP) 엔드포인트를 통한 원격 브라우저 사용
-lha, -list-headless-action 사용 가능한 headless 액션 목록 표시
DEBUG:
diff --git a/README_PT-BR.md b/README_PT-BR.md
index 46b1749dd9..344dd379be 100644
--- a/README_PT-BR.md
+++ b/README_PT-BR.md
@@ -268,6 +268,7 @@ HEADLESS:
-sb, -show-browser exibe o navegador na tela ao executar templates no modo headless
-ho, -headless-options string[] inicia o Chrome no modo headless com opções adicionais
-sc, -system-chrome utiliza o navegador Chrome instalado localmente em vez do instalado pelo nuclei
+ -cdpe, -cdp-endpoint string usar navegador remoto via endpoint do Protocolo de Ferramentas de Desenvolvedor do Chrome (CDP)
-lha, -list-headless-action lista ações disponíveis para o modo headless
DEBUG:
diff --git a/README_TR.md b/README_TR.md
new file mode 100644
index 0000000000..24fe839876
--- /dev/null
+++ b/README_TR.md
@@ -0,0 +1,294 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+**Nuclei, basit YAML tabanlı şablonlardan yararlanan modern, yüksek performanslı bir zafiyet tarayıcısıdır. Gerçek dünya koşullarını taklit eden özel zafiyet tespit senaryoları tasarlamanıza olanak tanıyarak sıfır hatalı pozitif sonuç sağlar.**
+
+- Güvenlik açığı şablonları oluşturmak ve özelleştirmek için basit YAML formatı.
+- Trend olan güvenlik açıklarını ele almak için binlerce güvenlik uzmanı tarafından katkıda bulunulmuştur.
+- Bir güvenlik açığını doğrulamak için gerçek dünya adımlarını simüle ederek hatalı pozitifleri azaltır.
+- Ultra hızlı paralel tarama işleme ve istek kümeleme.
+- Zafiyet tespiti ve regresyon testi için CI/CD hatlarına entegre edilebilir.
+- TCP, DNS, HTTP, SSL, WHOIS, JavaScript, Code ve daha fazlası gibi birçok protokolü destekler.
+- Jira, Splunk, GitHub, Elastic, GitLab ile entegre olur.
+
+
+
+
+## İçindekiler
+
+- [**`Başlarken`**](#başlarken)
+ - [_`1. Nuclei CLI`_](#1-nuclei-cli)
+ - [_`2. Pro ve Kurumsal Sürümler`_](#2-pro-ve-kurumsal-sürümler)
+- [**`Dokümantasyon`**](#dokümantasyon)
+ - [_`Komut Satırı Bayrakları`_](#komut-satırı-bayrakları)
+ - [_`Tek hedef tarama`_](#tek-hedef-tarama)
+ - [_`Çoklu hedef tarama`_](#çoklu-hedef-tarama)
+ - [_`Ağ taraması`_](#ağ-taraması)
+ - [_`Özel şablonunuzla tarama`_](#özel-şablonunuzla-tarama)
+ - [_`Nuclei'yi ProjectDiscovery'ye Bağlayın`_](#nucleiyi-projectdiscoveryye-bağlayın)
+- [**`Nuclei Şablonları, Topluluk ve Ödüller`**](#nuclei-şablonları-topluluk-ve-ödüller-) 💎
+- [**`Misyonumuz`**](#misyonumuz)
+- [**`Katkıda Bulunanlar`**](#katkıda-bulunanlar) ❤
+- [**`Lisans`**](#lisans)
+
+
+
+
+## Başlarken
+
+### **1. Nuclei CLI**
+
+_Nuclei'yi makinenize kurun. [**`Buradaki`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) kurulum kılavuzunu takip ederek başlayın. Ayrıca, [**`ücretsiz bir bulut katmanı`**](https://cloud.projectdiscovery.io/sign-up) sağlıyoruz ve cömert aylık ücretsiz limitlerle birlikte geliyor:_
+
+- Zafiyet bulgularınızı saklayın ve görselleştirin
+- nuclei şablonlarınızı yazın ve yönetin
+- En son nuclei şablonlarına erişin
+- Hedeflerinizi keşfedin ve saklayın
+
+> [!Important]
+> |**Bu proje aktif geliştirme aşamasındadır**. Sürümlerle birlikte kırılma değişiklikleri bekleyin. Güncellemeden önce sürüm değişiklik günlüğünü inceleyin.|
+> |:--------------------------------|
+> | Bu proje öncelikle bağımsız bir CLI aracı olarak kullanılmak üzere oluşturulmuştur. **Nuclei'yi bir servis olarak çalıştırmak güvenlik riskleri oluşturabilir.** Dikkatli kullanılması ve ek güvenlik önlemleri alınması önerilir. |
+
+
+
+### **2. Pro ve Kurumsal Sürümler**
+
+_Güvenlik ekipleri ve kuruluşlar için, ekibiniz ve mevcut iş akışlarınızla ölçekli olarak sürekli zafiyet taramaları yapmanıza yardımcı olmak üzere ince ayarlanmış, Nuclei OSS üzerine inşa edilmiş bulut tabanlı bir hizmet sunuyoruz:_
+
+- 50x daha hızlı taramalar
+- Yüksek doğrulukla büyük ölçekli tarama
+- Bulut hizmetleri ile entegrasyonlar (AWS, GCP, Azure, Cloudflare, Fastly, Terraform, Kubernetes)
+- Jira, Slack, Linear, API'ler ve Webhook'lar
+- Yönetici ve uyumluluk raporlaması
+- Artı: Gerçek zamanlı tarama, SAML SSO, SOC 2 uyumlu platform (AB ve ABD barındırma seçenekleri ile), paylaşılan ekip çalışma alanları ve daha fazlası
+- Sürekli olarak [**`yeni özellikler ekliyoruz`**](https://feedback.projectdiscovery.io/changelog)!
+- **Şunlar için ideal:** Sızma testi yapanlar, güvenlik ekipleri ve kuruluşlar
+
+Büyük bir organizasyonunuz ve karmaşık gereksinimleriniz varsa [**`Pro'ya kaydolun`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) veya [**`ekibimizle konuşun`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme).
+
+
+
+
+## Dokümantasyon
+
+Nuclei'nin tam [**`dokümantasyonuna buradan`**](https://docs.projectdiscovery.io/tools/nuclei/running) göz atın. Nuclei'de yeniyseniz, [**`temel YouTube serimize`**](https://www.youtube.com/playlist?list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl) göz atın.
+
+
+
+

+
+
+
+
+
+### Kurulum
+
+`nuclei` yüklemek için **go >= 1.24.2** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
+
+```sh
+go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
+```
+
+Nuclei kurulumu hakkında daha fazla bilgi edinmek için `https://docs.projectdiscovery.io/tools/nuclei/install` adresine bakın.
+
+### Komut Satırı Bayrakları
+
+Aracın tüm bayraklarını görüntülemek için:
+
+```sh
+nuclei -h
+```
+
+
+ Tüm yardım bayraklarını genişlet
+
+```yaml
+Nuclei, kapsamlı yapılandırılabilirlik, devasa genişletilebilirlik ve kullanım kolaylığına odaklanan hızlı, şablon tabanlı bir zafiyet tarayıcısıdır.
+
+Kullanım:
+ ./nuclei [bayraklar]
+
+Bayraklar:
+TARGET:
+ -u, -target string[] taranacak hedef URL'ler/hostlar
+ -l, -list string taranacak hedef URL'leri/hostları içeren dosya yolu (her satırda bir tane)
+ -eh, -exclude-hosts string[] girilen listeden tarama dışında tutulacak hostlar (ip, cidr, hostname)
+ -resume string taramayı belirtilen dosyadan devam ettir ve kaydet (kümeleme devre dışı bırakılır)
+ -sa, -scan-all-ips dns kaydı ile ilişkili tüm IP'leri tara
+ -iv, -ip-version string[] taranacak hostun IP versiyonu (4,6) - (varsayılan 4)
+
+TARGET-FORMAT:
+ -im, -input-mode string girdi dosyasının modu (list, burp, jsonl, yaml, openapi, swagger) (varsayılan "list")
+ -ro, -required-only istekler oluşturulurken girdi formatındaki sadece zorunlu alanları kullan
+ -sfv, -skip-format-validation girdi dosyasını ayrıştırırken format doğrulamasını atla (eksik değişkenler gibi)
+
+TEMPLATES:
+ -nt, -new-templates sadece en son nuclei-templates sürümünde eklenen yeni şablonları çalıştır
+ -ntv, -new-templates-version string[] belirli bir sürümde eklenen yeni şablonları çalıştır
+ -as, -automatic-scan wappalyzer teknoloji tespiti ile etiket eşlemesini kullanarak otomatik web taraması
+ -t, -templates string[] çalıştırılacak şablon veya şablon dizini listesi (virgülle ayrılmış, dosya)
+ -turl, -template-url string[] çalıştırılacak şablon url'si veya şablon url'lerini içeren liste (virgülle ayrılmış, dosya)
+ -ai, -prompt string yapay zeka istemi kullanarak şablon oluştur ve çalıştır
+ -w, -workflows string[] çalıştırılacak iş akışı veya iş akışı dizini listesi (virgülle ayrılmış, dosya)
+ -wurl, -workflow-url string[] çalıştırılacak iş akışı url'si veya iş akışı url'lerini içeren liste (virgülle ayrılmış, dosya)
+ -validate nuclei'ye iletilen şablonları doğrula
+ -nss, -no-strict-syntax şablonlarda katı sözdizimi kontrolünü devre dışı bırak
+ -td, -template-display şablon içeriğini görüntüler
+ -tl mevcut filtrelerle eşleşen tüm şablonları listele
+ -tgl tüm mevcut etiketleri listele
+ -sign şablonları NUCLEI_SIGNATURE_PRIVATE_KEY ortam değişkeninde tanımlanan özel anahtarla imzala
+ -code kod protokolü tabanlı şablonların yüklenmesini etkinleştir
+ -dut, -disable-unsigned-templates imzasız şablonların veya imzası eşleşmeyen şablonların çalıştırılmasını devre dışı bırak
+ -esc, -enable-self-contained kendi kendine yeten (self-contained) şablonların yüklenmesini etkinleştir
+ -egm, -enable-global-matchers global eşleştirici şablonların yüklenmesini etkinleştir
+ -file dosya şablonlarının yüklenmesini etkinleştir
+
+... (Diğer bayraklar orijinalindeki gibi, tam çeviri için çok uzun olabilir, ancak bağlam için yeterli)
+```
+
+Ek dokümantasyon şu adreste mevcuttur: [**`docs.nuclei.sh/getting-started/running`**](https://docs.nuclei.sh/getting-started/running?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme)
+
+
+
+### Tek hedef tarama
+
+Web uygulamasında hızlı bir tarama yapmak için:
+
+```sh
+nuclei -target https://example.com
+```
+
+### Çoklu hedef tarama
+
+Nuclei, bir hedef listesi sağlayarak toplu taramayı gerçekleştirebilir. Birden fazla URL içeren bir dosya kullanabilirsiniz.
+
+```sh
+nuclei -list urls.txt
+```
+
+### Ağ taraması
+
+Bu, açık portlar veya yanlış yapılandırılmış servisler gibi ağla ilgili sorunlar için tüm alt ağı tarayacaktır.
+
+```sh
+nuclei -target 192.168.1.0/24
+```
+
+### Özel şablonunuzla tarama
+
+Kendi şablonunuzu yazmak ve kullanmak için, belirli kurallara sahip bir `.yaml` dosyası oluşturun ve ardından aşağıdaki gibi kullanın.
+
+```sh
+nuclei -u https://example.com -t /path/to/your-template.yaml
+```
+
+### Nuclei'yi ProjectDiscovery'ye Bağlayın
+
+Taramaları makinenizde çalıştırabilir ve sonuçları daha fazla analiz ve düzeltme için bulut platformuna yükleyebilirsiniz.
+
+```sh
+nuclei -target https://example.com -dashboard
+```
+
+> [!NOTE]
+> Bu özellik tamamen ücretsizdir ve herhangi bir abonelik gerektirmez. Ayrıntılı bir kılavuz için [**`dokümantasyona`**](https://docs.projectdiscovery.io/cloud/scanning/nuclei-scan?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) bakın.
+
+
+
+
+## Nuclei Şablonları, Topluluk ve Ödüller 💎
+[**Nuclei şablonları**](https://github.com/projectdiscovery/nuclei-templates), isteklerin nasıl gönderileceğini ve işleneceğini tanımlayan YAML tabanlı şablon dosyaları kavramına dayanır. Bu, nuclei'ye kolay genişletilebilirlik yetenekleri sağlar. Şablonlar, yürütme sürecini hızlı bir şekilde tanımlamak için insan tarafından okunabilir basit bir format belirten YAML ile yazılmıştır.
+
+**[**`Buraya tıklayarak`**](https://cloud.projectdiscovery.io/templates) ücretsiz yapay zeka destekli Nuclei Şablon Editörü ile çevrimiçi deneyin.**
+
+Nuclei Şablonları, önem dereceleri ve tespit yöntemleri gibi temel ayrıntıları birleştirerek güvenlik açıklarını tanımlamak ve iletmek için akıcı bir yol sunar. Bu açık kaynaklı, topluluk tarafından geliştirilen araç, tehdit yanıtını hızlandırır ve siber güvenlik dünyasında geniş çapta tanınmaktadır. Nuclei şablonları, dünya çapında binlerce güvenlik araştırmacısı tarafından aktif olarak katkıda bulunulmaktadır. Katılımcılarımız için iki program yürütüyoruz: [**`Öncüler (Pioneers)`**](https://projectdiscovery.io/pioneers) ve [**`💎 ödüller`**](https://github.com/projectdiscovery/nuclei-templates/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%92%8E%20Bounty%22).
+
+
+
+
+
+
+#### Örnekler
+
+Kullanım durumları ve fikirler için [**dokümantasyonumuzu**](https://docs.projectdiscovery.io/templates/introduction) ziyaret edin.
+
+| Kullanım durumu | Nuclei şablonu |
+| :----------------------------------- | :------------------------------------------------- |
+| Bilinen CVE'leri tespit et | **[CVE-2021-44228 (Log4Shell)](https://cloud.projectdiscovery.io/public/CVE-2021-45046)** |
+| Bant Dışı (Out-of-Band) zafiyetlerini belirle | **[Blind SQL Injection via OOB](https://cloud.projectdiscovery.io/public/CVE-2024-22120)** |
+| SQL Injection tespiti | **[Generic SQL Injection](https://cloud.projectdiscovery.io/public/CVE-2022-34265)** |
+| Siteler Arası Komut Dosyası Çalıştırma (XSS) | **[Reflected XSS Detection](https://cloud.projectdiscovery.io/public/CVE-2023-4173)** |
+| Varsayılan veya zayıf şifreler | **[Default Credentials Check](https://cloud.projectdiscovery.io/public/airflow-default-login)** |
+| Gizli dosyalar veya veri ifşası | **[Sensitive File Disclosure](https://cloud.projectdiscovery.io/public/airflow-configuration-exposure)** |
+| Açık yönlendirmeleri (open redirects) belirle | **[Open Redirect Detection](https://cloud.projectdiscovery.io/public/open-redirect)** |
+| Alt alan adı devralmalarını (takeover) tespit et | **[Subdomain Takeover Templates](https://cloud.projectdiscovery.io/public/azure-takeover-detection)** |
+| Güvenlik yanlış yapılandırmaları | **[Unprotected Jenkins Console](https://cloud.projectdiscovery.io/public/unauthenticated-jenkins)** |
+| Zayıf SSL/TLS yapılandırmaları | **[SSL Certificate Expiry](https://cloud.projectdiscovery.io/public/expired-ssl)** |
+| Yanlış yapılandırılmış bulut hizmetleri | **[Open S3 Bucket Detection](https://cloud.projectdiscovery.io/public/s3-public-read-acp)** |
+| Uzaktan kod yürütme zafiyetleri | **[RCE Detection Templates](https://cloud.projectdiscovery.io/public/CVE-2024-29824)** |
+| Dizin geçiş (path traversal) saldırıları | **[Path Traversal Detection](https://cloud.projectdiscovery.io/public/oracle-fatwire-lfi)** |
+| Dosya dahil etme (file inclusion) zafiyetleri | **[Local/Remote File Inclusion](https://cloud.projectdiscovery.io/public/CVE-2023-6977)** |
+
+
+
+
+
+## Misyonumuz
+
+Geleneksel zafiyet tarayıcıları on yıllar önce inşa edildi. Kapalı kaynaklıdırlar, inanılmaz derecede yavaştırlar ve satıcı odaklıdırlar. Günümüzün saldırganları, eskiden yıllar süren süreçlerin aksine, yeni yayınlanan CVE'leri günler içinde internet genelinde kitlesel olarak istismar ediyor. Bu değişim, internetteki trend olan istismarlarla mücadele etmek için tamamen farklı bir yaklaşım gerektiriyor.
+
+Bu zorluğu çözmek için Nuclei'yi inşa ettik. Tüm tarama motoru çerçevesini açık ve özelleştirilebilir hale getirdik; bu sayede küresel güvenlik topluluğunun işbirliği yapmasına ve internet üzerindeki trend saldırı vektörlerini ve zafiyetlerini ele almasına olanak tanıdık. Nuclei artık Fortune 500 şirketleri, devlet kurumları ve üniversiteler tarafından kullanılmakta ve katkıda bulunulmaktadır.
+
+Kodumuza, [**`şablon kitaplığımıza`**](https://github.com/projectdiscovery/nuclei-templates) katkıda bulunarak veya [**`ekibimize katılarak`**](https://projectdiscovery.io/) siz de yer alabilirsiniz.
+
+
+
+
+## Katkıda Bulunanlar :heart:
+
+Projeyi güncel tuttukları ve [**`PR gönderdikleri için harika topluluk katkıda bulunanlara`**](https://github.com/projectdiscovery/nuclei/graphs/contributors) teşekkür ederiz. :heart:
+
+(Katkıda bulunanların listesi orijinalindeki gibi korunmuştur)
+
+
+
+
+...
+
+
+
+
+
+
+
+
+ **`nuclei`** [**MIT Lisansı**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md) altında dağıtılmaktadır.
+
+
diff --git a/cmd/integration-test/http.go b/cmd/integration-test/http.go
index 17effbd219..b11e48ccca 100644
--- a/cmd/integration-test/http.go
+++ b/cmd/integration-test/http.go
@@ -11,6 +11,7 @@ import (
"regexp"
"strconv"
"strings"
+ "sync"
"time"
"github.com/julienschmidt/httprouter"
@@ -62,6 +63,7 @@ var httpTestcases = []TestCaseInfo{
{Path: "protocols/http/dsl-functions.yaml", TestCase: &httpDSLFunctions{}},
{Path: "protocols/http/race-simple.yaml", TestCase: &httpRaceSimple{}},
{Path: "protocols/http/race-multiple.yaml", TestCase: &httpRaceMultiple{}},
+ {Path: "protocols/http/race-condition-with-delay.yaml", TestCase: &httpRaceWithDelay{}},
{Path: "protocols/http/race-with-variables.yaml", TestCase: &httpRaceWithVariables{}},
{Path: "protocols/http/stop-at-first-match.yaml", TestCase: &httpStopAtFirstMatch{}},
{Path: "protocols/http/stop-at-first-match-with-extractors.yaml", TestCase: &httpStopAtFirstMatchWithExtractors{}},
@@ -161,6 +163,30 @@ func (h *httpInteractshRequest) Execute(filePath string) error {
return expectResultsCount(results, 1, 2)
}
+type httpInteractshWithPayloadsRequest struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *httpInteractshWithPayloadsRequest) Execute(filePath string) error {
+ router := httprouter.New()
+ router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ value := r.Header.Get("url")
+ if value != "" {
+ if resp, _ := retryablehttp.DefaultClient().Get(value); resp != nil {
+ _ = resp.Body.Close()
+ }
+ }
+ })
+ ts := httptest.NewServer(router)
+ defer ts.Close()
+
+ results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
+ if err != nil {
+ return err
+ }
+
+ return expectResultsCount(results, 1, 3)
+}
+
type httpDefaultMatcherCondition struct{}
// Execute executes a test case and returns an error if occurred
@@ -1156,6 +1182,51 @@ func (h *httpRaceMultiple) Execute(filePath string) error {
return expectResultsCount(results, 5)
}
+type httpRaceWithDelay struct{}
+
+// Execute executes a test case and returns an error if occurred
+func (h *httpRaceWithDelay) Execute(filePath string) error {
+ var requestTimes []time.Time
+ var mu sync.Mutex
+
+ router := httprouter.New()
+ router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ mu.Lock()
+ requestTimes = append(requestTimes, time.Now())
+ mu.Unlock()
+ time.Sleep(2 * time.Second)
+ w.WriteHeader(http.StatusOK)
+ })
+ ts := httptest.NewServer(router)
+ defer ts.Close()
+
+ results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
+ if err != nil {
+ return err
+ }
+ if err := expectResultsCount(results, 3); err != nil {
+ return err
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+ if len(requestTimes) != 3 {
+ return fmt.Errorf("expected 3 requests, got %d", len(requestTimes))
+ }
+
+ // Check concurrency of first two requests (should be very close)
+ if diff := requestTimes[1].Sub(requestTimes[0]); diff > 500*time.Millisecond {
+ return fmt.Errorf("expected first 2 requests to be concurrent, diff: %v", diff)
+ }
+
+ // Check delay of third request (should be after ~2s)
+ if diff := requestTimes[2].Sub(requestTimes[0]); diff < 1500*time.Millisecond {
+ return fmt.Errorf("expected 3rd request to be delayed, diff: %v", diff)
+ }
+
+ return nil
+}
+
type httpRaceWithVariables struct{}
// Execute tests that variables and constants are properly resolved in race mode.
diff --git a/cmd/integration-test/interactsh.go b/cmd/integration-test/interactsh.go
index 8d737909d9..03df594cf1 100644
--- a/cmd/integration-test/interactsh.go
+++ b/cmd/integration-test/interactsh.go
@@ -5,6 +5,7 @@ import osutils "github.com/projectdiscovery/utils/os"
// All Interactsh related testcases
var interactshTestCases = []TestCaseInfo{
{Path: "protocols/http/interactsh.yaml", TestCase: &httpInteractshRequest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
+ {Path: "protocols/http/interactsh-with-payloads.yaml", TestCase: &httpInteractshWithPayloadsRequest{}, DisableOn: func() bool { return true }},
{Path: "protocols/http/interactsh-stop-at-first-match.yaml", TestCase: &httpInteractshStopAtFirstMatchRequest{}, DisableOn: func() bool { return true }}, // disable this test for now
{Path: "protocols/http/default-matcher-condition.yaml", TestCase: &httpDefaultMatcherCondition{}, DisableOn: func() bool { return true }},
{Path: "protocols/http/interactsh-requests-mc-and.yaml", TestCase: &httpInteractshRequestsWithMCAnd{}},
diff --git a/cmd/integration-test/javascript.go b/cmd/integration-test/javascript.go
index 63b2d59e85..6cc71e91a8 100644
--- a/cmd/integration-test/javascript.go
+++ b/cmd/integration-test/javascript.go
@@ -15,12 +15,14 @@ var jsTestcases = []TestCaseInfo{
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{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: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/oracle-auth-test.yaml", TestCase: &javascriptOracleAuthTest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}},
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/mysql-connect.yaml", TestCase: &javascriptMySQLConnect{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{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: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
}
var (
@@ -28,8 +30,10 @@ var (
sshResource *dockertest.Resource
oracleResource *dockertest.Resource
vncResource *dockertest.Resource
+ telnetResource *dockertest.Resource
postgresResource *dockertest.Resource
mysqlResource *dockertest.Resource
+ rsyncResource *dockertest.Resource
pool *dockertest.Pool
defaultRetry = 3
)
@@ -124,7 +128,7 @@ func (j *javascriptOracleAuthTest) Execute(filePath string) error {
results := []string{}
var err error
_ = pool.Retry(func() error {
- //let ssh server start
+ // let oracle server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
@@ -258,6 +262,70 @@ func (j *javascriptNoPortArgs) Execute(filePath string) error {
return expectResultsCount(results, 1)
}
+type javascriptRsyncTest struct{}
+
+func (j *javascriptRsyncTest) Execute(filePath string) error {
+ if rsyncResource == nil || pool == nil {
+ // skip test as rsync is not running
+ return nil
+ }
+ tempPort := rsyncResource.GetPort("873/tcp")
+ finalURL := "localhost:" + tempPort
+ defer purge(rsyncResource)
+ errs := []error{}
+ for i := 0; i < defaultRetry; i++ {
+ results := []string{}
+ var err error
+ _ = pool.Retry(func() error {
+ //let rsync server start
+ time.Sleep(3 * time.Second)
+ results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ if err := expectResultsCount(results, 1); err == nil {
+ return nil
+ } else {
+ errs = append(errs, err)
+ }
+ }
+ return multierr.Combine(errs...)
+}
+
+type javascriptTelnetAuthTest struct{}
+
+func (j *javascriptTelnetAuthTest) Execute(filePath string) error {
+ if telnetResource == nil || pool == nil {
+ // skip test as telnet is not running
+ return nil
+ }
+ tempPort := telnetResource.GetPort("23/tcp")
+ finalURL := "localhost:" + tempPort
+ defer purge(telnetResource)
+ errs := []error{}
+ for i := 0; i < defaultRetry; i++ {
+ results := []string{}
+ var err error
+ _ = pool.Retry(func() error {
+ //let telnet server start
+ time.Sleep(3 * time.Second)
+ results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ if err := expectResultsCount(results, 1); err == nil {
+ return nil
+ } else {
+ errs = append(errs, err)
+ }
+ }
+ return multierr.Combine(errs...)
+}
+
// purge any given resource if it is not nil
func purge(resource *dockertest.Resource) {
if resource != nil && pool != nil {
@@ -397,4 +465,38 @@ func init() {
if err := mysqlResource.Expire(30); err != nil {
log.Printf("Could not expire mysql resource: %s", err)
}
+
+ // setup a temporary rsync server
+ rsyncResource, err = pool.RunWithOptions(&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"},
+ Platform: "linux/amd64",
+ })
+ if err != nil {
+ log.Printf("Could not start Rsync resource: %s", err)
+ return
+ }
+ // by default expire after 30 sec
+ if err := rsyncResource.Expire(30); err != nil {
+ log.Printf("Could not expire Rsync resource: %s", err)
+ }
+
+ // setup a temporary telnet server
+ // username: dev
+ // password: mysecret
+ telnetResource, err = pool.RunWithOptions(&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"},
+ Platform: "linux/amd64",
+ })
+ if err != nil {
+ log.Printf("Could not start Telnet resource: %s", err)
+ return
+ }
+ // by default expire after 30 sec
+ if err := telnetResource.Expire(30); err != nil {
+ log.Printf("Could not expire Telnet resource: %s", err)
+ }
}
diff --git a/cmd/nuclei/main.go b/cmd/nuclei/main.go
index 1a51c9f010..8421d1dc69 100644
--- a/cmd/nuclei/main.go
+++ b/cmd/nuclei/main.go
@@ -196,7 +196,7 @@ func main() {
// Setup filename for graceful exits
resumeFileName := types.DefaultResumeFilePath()
- if options.Resume == "" {
+ if options.Resume != "" {
resumeFileName = options.Resume
}
c := make(chan os.Signal, 1)
@@ -443,6 +443,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.ShowBrowser, "show-browser", "sb", false, "show the browser on the screen when running templates with headless mode"),
flagSet.StringSliceVarP(&options.HeadlessOptionalArguments, "headless-options", "ho", nil, "start headless chrome with additional options", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.BoolVarP(&options.UseInstalledChrome, "system-chrome", "sc", false, "use local installed Chrome browser instead of nuclei installed"),
+ flagSet.StringVarP(&options.CDPEndpoint, "cdp-endpoint", "cdpe", "", "use remote browser via Chrome DevTools Protocol (CDP) endpoint"),
flagSet.BoolVarP(&options.ShowActions, "list-headless-action", "lha", false, "list available headless actions"),
)
diff --git a/go.mod b/go.mod
index c27befc3cf..02c33de34f 100644
--- a/go.mod
+++ b/go.mod
@@ -22,12 +22,12 @@ require (
github.com/olekukonko/tablewriter v1.0.8
github.com/pkg/errors v0.9.1
github.com/projectdiscovery/clistats v0.1.1
- github.com/projectdiscovery/fastdialer v0.5.1
+ github.com/projectdiscovery/fastdialer v0.5.3
github.com/projectdiscovery/hmap v0.0.99
github.com/projectdiscovery/interactsh v1.2.4
github.com/projectdiscovery/rawhttp v0.1.90
- github.com/projectdiscovery/retryabledns v1.0.112
- github.com/projectdiscovery/retryablehttp-go v1.3.1
+ github.com/projectdiscovery/retryabledns v1.0.113
+ github.com/projectdiscovery/retryablehttp-go v1.3.5
github.com/projectdiscovery/yamldoc-go v1.0.6
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.6.0
@@ -37,11 +37,11 @@ require (
github.com/spf13/cast v1.9.2
github.com/syndtr/goleveldb v1.0.0
github.com/valyala/fasttemplate v1.2.2
- github.com/weppos/publicsuffix-go v0.50.1
+ github.com/weppos/publicsuffix-go v0.50.3-0.20260104170930-90713dec78f2
go.uber.org/multierr v1.11.0
- golang.org/x/net v0.48.0
+ golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.30.0
- golang.org/x/text v0.32.0
+ golang.org/x/text v0.33.0
gopkg.in/yaml.v2 v2.4.0
)
@@ -50,9 +50,11 @@ require (
code.gitea.io/sdk/gitea v0.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
+ github.com/Azure/go-ntlmssp v0.1.0
github.com/DataDog/gostackparse v0.7.0
github.com/Masterminds/semver/v3 v3.2.1
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057
+ github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883
github.com/alexsnet/go-vnc v0.1.0
@@ -92,28 +94,27 @@ require (
github.com/microsoft/go-mssqldb v1.9.2
github.com/ory/dockertest/v3 v3.12.0
github.com/praetorian-inc/fingerprintx v1.1.15
- github.com/projectdiscovery/dsl v0.8.10
+ github.com/projectdiscovery/dsl v0.8.12
github.com/projectdiscovery/fasttemplate v0.0.2
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb
github.com/projectdiscovery/goflags v0.1.74
- github.com/projectdiscovery/gologger v1.1.66
+ github.com/projectdiscovery/gologger v1.1.67
github.com/projectdiscovery/gostruct v0.0.2
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81
- github.com/projectdiscovery/httpx v1.7.4
+ github.com/projectdiscovery/httpx v1.8.1
github.com/projectdiscovery/mapcidr v1.1.97
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
- github.com/projectdiscovery/networkpolicy v0.1.33
- github.com/projectdiscovery/ratelimit v0.0.82
+ github.com/projectdiscovery/networkpolicy v0.1.34
+ github.com/projectdiscovery/ratelimit v0.0.83
github.com/projectdiscovery/rdap v0.9.0
github.com/projectdiscovery/sarif v0.0.1
github.com/projectdiscovery/tlsx v1.2.2
github.com/projectdiscovery/uncover v1.2.0
- github.com/projectdiscovery/useragent v0.0.106
- github.com/projectdiscovery/utils v0.8.0
- github.com/projectdiscovery/wappalyzergo v0.2.61
+ github.com/projectdiscovery/useragent v0.0.107
+ github.com/projectdiscovery/utils v0.9.0
+ github.com/projectdiscovery/wappalyzergo v0.2.65
github.com/redis/go-redis/v9 v9.11.0
- github.com/seh-msft/burpxml v1.0.1
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/sijms/go-ora/v2 v2.9.0
github.com/stretchr/testify v1.11.1
@@ -124,7 +125,7 @@ require (
github.com/zmap/zgrab2 v0.1.8
gitlab.com/gitlab-org/api/client-go v0.130.1
go.mongodb.org/mongo-driver v1.17.4
- golang.org/x/term v0.38.0
+ golang.org/x/term v0.39.0
gopkg.in/yaml.v3 v3.0.1
moul.io/http2curl v1.0.0
)
@@ -137,7 +138,6 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
- github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -212,6 +212,7 @@ require (
github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
@@ -237,7 +238,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
- github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
@@ -269,6 +270,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
+ github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
github.com/kataras/jwt v0.1.10 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
@@ -308,7 +310,7 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
- github.com/nwaples/rardecode/v2 v2.2.1 // indirect
+ github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
@@ -319,14 +321,14 @@ require (
github.com/openrdap/rdap v0.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
- github.com/pierrec/lz4/v4 v4.1.22 // indirect
+ github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/projectdiscovery/asnmap v1.1.1 // indirect
github.com/projectdiscovery/blackrock v0.0.1 // indirect
- github.com/projectdiscovery/cdncheck v1.2.16 // indirect
+ github.com/projectdiscovery/cdncheck v1.2.20 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
@@ -403,16 +405,16 @@ require (
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
- github.com/zmap/zcrypto v0.0.0-20240512203510-0fef58d9a9db // indirect
+ github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.uber.org/zap v1.27.0 // indirect
goftp.io/server/v2 v2.0.1 // indirect
- golang.org/x/crypto v0.46.0 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621
- golang.org/x/mod v0.30.0 // indirect
- golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/mod v0.31.0 // indirect
+ golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
- golang.org/x/tools v0.39.0
+ golang.org/x/tools v0.40.0
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 76e4463fee..13947890e3 100644
--- a/go.sum
+++ b/go.sum
@@ -64,8 +64,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613E
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
-github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
+github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
@@ -87,6 +87,8 @@ github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4=
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8=
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
+github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
+github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 h1:54I+OF5vS4a/rxnUrN5J3hi0VEYKcrTlpc8JosDyP+c=
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697/go.mod h1:yNqYRqxYkSROY1J+LX+A0tOSA/6soXQs5m8hZSqYBac=
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 h1:+Is1AS20q3naP+qJophNpxuvx1daFOx9C0kLIuI0GVk=
@@ -331,6 +333,8 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NE
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
@@ -435,8 +439,8 @@ github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI6
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
-github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goburrow/cache v0.1.4 h1:As4KzO3hgmzPlnaMniZU9+VmoNYseUhuELbxy9mRBfw=
github.com/goburrow/cache v0.1.4/go.mod h1:cDFesZDnIlrHoNlMYqqMpCRawuXulgx+y7mXU8HZ+/c=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -634,6 +638,8 @@ github.com/k14s/difflib v0.0.0-20201117154628-0c031775bf57 h1:CwBRArr+BWBopnUJhD
github.com/k14s/difflib v0.0.0-20201117154628-0c031775bf57/go.mod h1:B0xN2MiNBGWOWi9CcfAo9LBI8IU4J1utlbOIJCsmKr4=
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 h1:4bcRTTSx+LKSxMWibIwzHnDNmaN1x52oEpvnjCy+8vk=
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368/go.mod h1:lKGj1op99m4GtQISxoD2t+K+WO/q2NzEPKvfXFQfbCA=
+github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 h1:IAukUBAVLUWBcexOYgkTD/EjMkfnNos7g7LFpyIdHJI=
+github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166/go.mod h1:T4xUEny5PVedYIbkMAKYEBjMyDsOvvP0qK4s324AKA8=
github.com/kataras/jwt v0.1.10 h1:GBXOF9RVInDPhCFBiDumRG9Tt27l7ugLeLo8HL5SeKQ=
github.com/kataras/jwt v0.1.10/go.mod h1:xkimAtDhU/aGlQqjwvgtg+VyuPwMiyZHaY8LJRh0mYo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
@@ -769,8 +775,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nwaples/rardecode/v2 v2.2.1 h1:DgHK/O/fkTQEKBJxBMC5d9IU8IgauifbpG78+rZJMnI=
-github.com/nwaples/rardecode/v2 v2.2.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
+github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
+github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
@@ -806,8 +812,8 @@ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZ
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
-github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
-github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
+github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -827,14 +833,14 @@ github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kI
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ=
github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
-github.com/projectdiscovery/cdncheck v1.2.16 h1:m6sQh5VAWN2sVp3o8LSFtAfnv3V0Dpoqqw83ZwnrA2c=
-github.com/projectdiscovery/cdncheck v1.2.16/go.mod h1:2OsjLn4x7VsM+ZNccGU185pd43U+0h5gDDStU0GtNl8=
+github.com/projectdiscovery/cdncheck v1.2.20 h1:sMzoCi5TR7qQsH4LW0NF219PmX/lYjWUeoB2Iiddwcs=
+github.com/projectdiscovery/cdncheck v1.2.20/go.mod h1:gpeX5OrzaC4DmeUGDcKrC7cPUXQvRGTY/Ui0XrVfdzU=
github.com/projectdiscovery/clistats v0.1.1 h1:8mwbdbwTU4aT88TJvwIzTpiNeow3XnAB72JIg66c8wE=
github.com/projectdiscovery/clistats v0.1.1/go.mod h1:4LtTC9Oy//RiuT1+76MfTg8Hqs7FQp1JIGBM3nHK6a0=
-github.com/projectdiscovery/dsl v0.8.10 h1:7t8KMukLlNGiOPdESGCkpOxu9RAT3RLRBY0Z5CoZIgs=
-github.com/projectdiscovery/dsl v0.8.10/go.mod h1:66WXaiVEOA2LlZuH81/izzybA3s++zX/nrKwgQV/2S0=
-github.com/projectdiscovery/fastdialer v0.5.1 h1:/4PX1u80QfZ8+DdF9jdtz1in5eajJhw89/xouVc9/+c=
-github.com/projectdiscovery/fastdialer v0.5.1/go.mod h1:34SS9VxrrmUhO67LYioxjrAWOQ5/kwgJNj9CiFgRJso=
+github.com/projectdiscovery/dsl v0.8.12 h1:gQL8k5zPok+5JGc7poiXzHCElNY/WnaTKoRB2wI3CYA=
+github.com/projectdiscovery/dsl v0.8.12/go.mod h1:pdMfUTNHMxlt6M94CSrCpZ1QObTP44rLqWifMMWW+IA=
+github.com/projectdiscovery/fastdialer v0.5.3 h1:Io57Q37ouFzrPK53ZdzK6jsELgqjIMCWcoDs+lRDGMA=
+github.com/projectdiscovery/fastdialer v0.5.3/go.mod h1:euoxS1E93LDnl0OnNN0UALedAFF+EehBxyU3z+79l0g=
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
@@ -845,16 +851,16 @@ github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb h1:rutG90
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb/go.mod h1:FLjF1DmZ+POoGEiIQdWuYVwS++C/GwpX8YaCsTSm1RY=
github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c=
github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4=
-github.com/projectdiscovery/gologger v1.1.66 h1:dYXfCWWNNiC7AgIDBkIF+3sTGyQbrpncsQEFQwyKCOg=
-github.com/projectdiscovery/gologger v1.1.66/go.mod h1:lm3vFKt52lXgo799dBtySe/1zRN1lMGUQuNb5YtwkYQ=
+github.com/projectdiscovery/gologger v1.1.67 h1:GZU3AjYiJvcwJT5TlfIv+152/TVmaz62Zyn3/wWXlig=
+github.com/projectdiscovery/gologger v1.1.67/go.mod h1:35oeQP6wvj58S+o+Km6boED/t786FXQkI0exhFHJbNE=
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81 h1:yHh46pJovYbyiaHCV7oIDinFmy+Fyq36H1BowJgb0M0=
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81/go.mod h1:9lmGPBDGZVANzCGjQg+V32n8Y3Cgjo/4kT0E88lsVTI=
github.com/projectdiscovery/hmap v0.0.99 h1:XPfLnD3CUrMqVCIdpK9ozD7Xmp3simx3T+2j4WWhHnU=
github.com/projectdiscovery/hmap v0.0.99/go.mod h1:koyUJi83K5G3w35ZLFXOYZIyYJsO+6hQrgDDN1RBrVE=
-github.com/projectdiscovery/httpx v1.7.4 h1:GUEklAZ71VKM0krmgck2Km5odJi5dwfo0euc2r88tj0=
-github.com/projectdiscovery/httpx v1.7.4/go.mod h1:sPnFYIXh0RAdjb02vpUGOsP5j28VDZs5khjMbXwEUGQ=
+github.com/projectdiscovery/httpx v1.8.1 h1:50NTzbgnqCgTJ1uawvusJq8Q6g0HM8TwEcxZgWdq5d4=
+github.com/projectdiscovery/httpx v1.8.1/go.mod h1:ws3cY6c7guy99M1eCYRbyaN57K0pEOguTZymMdxRZzc=
github.com/projectdiscovery/interactsh v1.2.4 h1:WUSj+fxbcV53J64oIAhbYzCKD1w/IyenyRBhkI5jiqI=
github.com/projectdiscovery/interactsh v1.2.4/go.mod h1:E/IVNZ80/WKz8zTwGJWQygxIbhlRmuzZFsZwcGSZTdc=
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb h1:MGtI4oE12ruWv11ZlPXXd7hl/uAaQZrFvrIDYDeVMd8=
@@ -865,18 +871,18 @@ github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8Qu
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 h1:L/e8z8yw1pfT6bg35NiN7yd1XKtJap5Nk6lMwQ0RNi8=
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5/go.mod h1:pGW2ncnTxTxHtP9wzcIJAB+3/NMp6IiuQWd2NK7K+oc=
-github.com/projectdiscovery/networkpolicy v0.1.33 h1:bVgp+XpLEsQ7ZEJt3UaUqIwhI01MMdt7F2dfIKFQg/w=
-github.com/projectdiscovery/networkpolicy v0.1.33/go.mod h1:YAPddAXUc/lhoU85AFdvgOQKx8Qh8r0vzSjexRWk6Yk=
-github.com/projectdiscovery/ratelimit v0.0.82 h1:rtO5SQf5uQFu5zTahTaTcO06OxmG8EIF1qhdFPIyTak=
-github.com/projectdiscovery/ratelimit v0.0.82/go.mod h1:z076BrLkBb5yS7uhHNoCTf8X/BvFSGRxwQ8EzEL9afM=
+github.com/projectdiscovery/networkpolicy v0.1.34 h1:TRwNbgMwdx3NC190TKSLwtTvr0JAIZAlnWkOhW0yBME=
+github.com/projectdiscovery/networkpolicy v0.1.34/go.mod h1:GJ20E7fJoA2vk8ZBSa1Cvc5WyP8RxglF5bZmYgK8jag=
+github.com/projectdiscovery/ratelimit v0.0.83 h1:hfb36QvznBrjA4FNfpFE8AYRVBYrfJh8qHVROLQgl54=
+github.com/projectdiscovery/ratelimit v0.0.83/go.mod h1:z076BrLkBb5yS7uhHNoCTf8X/BvFSGRxwQ8EzEL9afM=
github.com/projectdiscovery/rawhttp v0.1.90 h1:LOSZ6PUH08tnKmWsIwvwv1Z/4zkiYKYOSZ6n+8RFKtw=
github.com/projectdiscovery/rawhttp v0.1.90/go.mod h1:VZYAM25UI/wVB3URZ95ZaftgOnsbphxyAw/XnQRRz4Y=
github.com/projectdiscovery/rdap v0.9.0 h1:wPhHx5pQ2QI+WGhyNb2PjhTl0NtB39Nk7YFZ9cp8ZGA=
github.com/projectdiscovery/rdap v0.9.0/go.mod h1:zk4yrJFQ2Hy36Aqk+DvotYQxYAeALaCJ5ORySkff36Q=
-github.com/projectdiscovery/retryabledns v1.0.112 h1:4iCiuo6jMnw/pdOZRzBQrbUOUu5tOeuvGupxVV8RDLw=
-github.com/projectdiscovery/retryabledns v1.0.112/go.mod h1:xsJTKbo+KGqd7+88z1naEUFJybLH2yjB/zUyOweA7k0=
-github.com/projectdiscovery/retryablehttp-go v1.3.1 h1:ds0dcKa3565pYdIJrLcwcbrb9cH6MojY2uHwGPoXubg=
-github.com/projectdiscovery/retryablehttp-go v1.3.1/go.mod h1:ISMHtd5DrSSDenQ23Vz9cIyIj3XlccZzZhuWHUJTa60=
+github.com/projectdiscovery/retryabledns v1.0.113 h1:s+DAzdJ8XhLxRgt5636H0HG9OqHsGRjX9wTrLSTMqlQ=
+github.com/projectdiscovery/retryabledns v1.0.113/go.mod h1:+DyanDr8naxQ2dRO9c4Ezo3NHHXhz8L0tTSRYWhiwyA=
+github.com/projectdiscovery/retryablehttp-go v1.3.5 h1:6UXSJOEeeSE/IpI4xPrKRhSLkk3itNajfbgH91WtPPc=
+github.com/projectdiscovery/retryablehttp-go v1.3.5/go.mod h1:2ma5Itx44tgfZCtHqnI7xbWEmsLXt1qXh+oOaJfmA+g=
github.com/projectdiscovery/sarif v0.0.1 h1:C2Tyj0SGOKbCLgHrx83vaE6YkzXEVrMXYRGLkKCr/us=
github.com/projectdiscovery/sarif v0.0.1/go.mod h1:cEYlDu8amcPf6b9dSakcz2nNnJsoz4aR6peERwV+wuQ=
github.com/projectdiscovery/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
@@ -885,12 +891,12 @@ github.com/projectdiscovery/tlsx v1.2.2 h1:Y96QBqeD2anpzEtBl4kqNbwzXh2TrzJuXfgiB
github.com/projectdiscovery/tlsx v1.2.2/go.mod h1:ZJl9F1sSl0sdwE+lR0yuNHVX4Zx6tCSTqnNxnHCFZB4=
github.com/projectdiscovery/uncover v1.2.0 h1:31tjYa0v8FB8Ch8hJTxb+2t63vsljdOo0OSFylJcX4M=
github.com/projectdiscovery/uncover v1.2.0/go.mod h1:ozqKb++p39Kmh1SmwIpbQ9p0aVGPXuwsb4/X2Kvx6ms=
-github.com/projectdiscovery/useragent v0.0.106 h1:9fS08MRUUJvfBskTxcXY9TA4X1TwpH6iJ3P3YNaXNlo=
-github.com/projectdiscovery/useragent v0.0.106/go.mod h1:9oVMjgd7CchIsyeweyigIPtW83gpiGf2NtR6UM5XK+o=
-github.com/projectdiscovery/utils v0.8.0 h1:8d79OCs5xGDNXdKxMUKMY/lgQSUWJMYB1B2Sx+oiqkQ=
-github.com/projectdiscovery/utils v0.8.0/go.mod h1:CU6tjtyTRxBrnNek+GPJplw4IIHcXNZNKO09kWgqTdg=
-github.com/projectdiscovery/wappalyzergo v0.2.61 h1:TxiYJvXqReiscuWKtGKhFx3VxbVVjHOgECNX709AEX4=
-github.com/projectdiscovery/wappalyzergo v0.2.61/go.mod h1:8FtSVcmPRZU0g1euBpdSYEBHIvB7Zz9MOb754ZqZmfU=
+github.com/projectdiscovery/useragent v0.0.107 h1:45gSBda052fv2Gtxtnpx7cu2rWtUpZEQRGAoYGP6F5M=
+github.com/projectdiscovery/useragent v0.0.107/go.mod h1:yv5ZZLDT/kq6P+NvBcCPq6sjEVQtZGgO+OvvHzZ+WtY=
+github.com/projectdiscovery/utils v0.9.0 h1:eu9vdbP0VYXI9nGSLfnOpUqBeW9/B/iSli7U8gPKZw8=
+github.com/projectdiscovery/utils v0.9.0/go.mod h1:zcVu1QTlMi5763qCol/L3ROnbd/UPSBP8fI5PmcnF6s=
+github.com/projectdiscovery/wappalyzergo v0.2.65 h1:5hWGkuortLiq0whmVIfxbbE9pDl7Zd5e1rVRIEimOyk=
+github.com/projectdiscovery/wappalyzergo v0.2.65/go.mod h1:Oc+U2RPJObmpi6LW5lTMEDiKagcKZNkEfZfwrVMURa0=
github.com/projectdiscovery/yamldoc-go v1.0.6 h1:GCEdIRlQjDux28xTXKszM7n3jlMf152d5nqVpVoetas=
github.com/projectdiscovery/yamldoc-go v1.0.6/go.mod h1:R5lWrNzP+7Oyn77NDVPnBsxx2/FyQZBBkIAaSaCQFxw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -939,8 +945,6 @@ github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uR
github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
-github.com/seh-msft/burpxml v1.0.1 h1:5G3QPSzvfA1WcX7LkxmKBmK2RnNyGviGWnJPumE0nwg=
-github.com/seh-msft/burpxml v1.0.1/go.mod h1:lTViCHPtGGS0scK0B4krm6Ld1kVZLWzQccwUomRc58I=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
@@ -1066,9 +1070,9 @@ github.com/vulncheck-oss/go-exploit v1.51.0/go.mod h1:J28w0dLnA6DnCrnBm9Sbt6smX8
github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
-github.com/weppos/publicsuffix-go v0.30.2/go.mod h1:/hGscit36Yt+wammfBBwdMdxBT8btsTt6KvwO9OvMyM=
-github.com/weppos/publicsuffix-go v0.50.1 h1:elrBHeSkS/eIb169+DnLrknqmdP4AjT0Q0tEdytz1Og=
-github.com/weppos/publicsuffix-go v0.50.1/go.mod h1:znn0JVXjcR5hpUl9pbEogwH6I710rA1AX0QQPT0bf+k=
+github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko=
+github.com/weppos/publicsuffix-go v0.50.3-0.20260104170930-90713dec78f2 h1:LiQSn5u8Nc6V/GixI+SWxt+YkNIyfKIlkVRULSw2Zt0=
+github.com/weppos/publicsuffix-go v0.50.3-0.20260104170930-90713dec78f2/go.mod h1:CbQCKDtXF8UcT7hrxeMa0MDjwhpOI9iYOU7cfq+yo8k=
github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
@@ -1143,8 +1147,8 @@ github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+T
github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w=
-github.com/zmap/zcrypto v0.0.0-20240512203510-0fef58d9a9db h1:IfONOhyZlf4qPt3ENPU+27mBbPjzTQ+swKpj7MJva9I=
-github.com/zmap/zcrypto v0.0.0-20240512203510-0fef58d9a9db/go.mod h1:mo/07mo6reDaiz6BzveCuYBWb1d+aX8Pf8Nh+Q57y2g=
+github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 h1:DCz0McWRVJNICkHdu2XpETqeLvPtZXs315OZyUs1BDk=
+github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77/go.mod h1:aSvf+uTU222mUYq/KQj3oiEU7ajhCZe8RRSLHIoM4EM=
github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXDUD+uue8yeLHr0eXx1lvY6CvMiHbTKw5nGmA9OUoo=
github.com/zmap/zgrab2 v0.1.8 h1:PFnXrIBcGjYFec1JNbxMKQuSXXzS+SbqE89luuF4ORY=
github.com/zmap/zgrab2 v0.1.8/go.mod h1:5d8HSmUwvllx4q1qG50v/KXphkg45ZzWdaQtgTFnegE=
@@ -1212,12 +1216,12 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
-golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1255,8 +1259,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
-golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1303,12 +1307,12 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1400,12 +1404,12 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1415,12 +1419,12 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
-golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
-golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
-golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1436,9 +1440,10 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
-golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1492,8 +1497,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
-golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/integration_tests/protocols/http/interactsh-with-payloads.yaml b/integration_tests/protocols/http/interactsh-with-payloads.yaml
new file mode 100644
index 0000000000..3c4fa0cd31
--- /dev/null
+++ b/integration_tests/protocols/http/interactsh-with-payloads.yaml
@@ -0,0 +1,24 @@
+id: interactsh-with-payloads
+
+info:
+ name: Interactsh With Payloads Integration Test
+ author: dwisiswant0
+ severity: info
+ tags: test
+
+http:
+ - method: GET
+ path:
+ - "{{BaseURL}}/?p={{p}}"
+ headers:
+ url: 'http://{{interactsh-url}}'
+ payloads:
+ p:
+ - a
+ - b
+ - c
+ matchers:
+ - type: word
+ part: interactsh_protocol
+ words:
+ - "dns"
diff --git a/integration_tests/protocols/http/race-condition-with-delay.yaml b/integration_tests/protocols/http/race-condition-with-delay.yaml
new file mode 100644
index 0000000000..fd54941c6c
--- /dev/null
+++ b/integration_tests/protocols/http/race-condition-with-delay.yaml
@@ -0,0 +1,28 @@
+id: race-condition-with-delay
+
+info:
+ name: Race Condition Testing with Delay
+ author: pdteam
+ severity: info
+ description: |
+ Test race condition handling with induced server delay.
+ tags: test
+
+http:
+ - raw:
+ - |
+ GET / HTTP/1.1
+ Host: {{Hostname}}
+ - |
+ GET / HTTP/1.1
+ Host: {{Hostname}}
+ - |
+ GET / HTTP/1.1
+ Host: {{Hostname}}
+
+ threads: 2
+ race: true
+ matchers:
+ - type: status
+ status:
+ - 200
diff --git a/integration_tests/protocols/javascript/rsync-test.yaml b/integration_tests/protocols/javascript/rsync-test.yaml
new file mode 100644
index 0000000000..ce4ae4895e
--- /dev/null
+++ b/integration_tests/protocols/javascript/rsync-test.yaml
@@ -0,0 +1,21 @@
+id: rsync-test
+
+info:
+ name: Rsync Test
+ author: pdteam
+ severity: info
+
+javascript:
+ - code: |
+ const rsync = require('nuclei/rsync');
+ rsync.IsRsync(Host, Port);
+
+ args:
+ Host: "{{Host}}"
+ Port: "873"
+
+ matchers:
+ - type: dsl
+ dsl:
+ - "success == true"
+
\ No newline at end of file
diff --git a/integration_tests/protocols/javascript/telnet-auth-test.yaml b/integration_tests/protocols/javascript/telnet-auth-test.yaml
new file mode 100644
index 0000000000..79d1c844c1
--- /dev/null
+++ b/integration_tests/protocols/javascript/telnet-auth-test.yaml
@@ -0,0 +1,28 @@
+id: telnet-auth-test
+
+info:
+ name: Telnet Authentication Test
+ author: pdteam
+ severity: info
+ metadata:
+ shodan-query: port:23
+
+
+javascript:
+ - code: |
+ var m = require("nuclei/telnet");
+ var c = m.TelnetClient();
+ c.Connect(Host, Port, User, Password);
+
+ args:
+ Host: "{{Host}}"
+ Port: "23"
+ User: "dev"
+ Password: "mysecret"
+
+ matchers:
+ - type: dsl
+ dsl:
+ - "response == true"
+ - "success == true"
+ condition: and
diff --git a/lib/config.go b/lib/config.go
index c9fc6e74a4..e79b3310bb 100644
--- a/lib/config.go
+++ b/lib/config.go
@@ -102,7 +102,8 @@ type InteractshOpts interactsh.Options
// WithInteractshOptions sets interactsh options
func WithInteractshOptions(opts InteractshOpts) NucleiSDKOptions {
return func(e *NucleiEngine) error {
- if e.mode == threadSafe {
+ // WithInteractshOptions can be used when creating ThreadSafeNucleiEngine but not after it's initialized
+ if e.mode == threadSafe && e.interactshOpts != nil {
return errkit.Wrap(ErrOptionsNotSupported, "WithInteractshOptions")
}
optsPtr := &opts
@@ -284,7 +285,8 @@ type NetworkConfig struct {
// WithNetworkConfig allows setting network config options
func WithNetworkConfig(opts NetworkConfig) NucleiSDKOptions {
return func(e *NucleiEngine) error {
- if e.mode == threadSafe {
+ // WithNetworkConfig can be used when creating ThreadSafeNucleiEngine but not after it's initialized
+ if e.mode == threadSafe && e.hostErrCache != nil {
return errkit.Wrap(ErrOptionsNotSupported, "WithNetworkConfig")
}
e.opts.NoHostErrors = opts.DisableMaxHostErr
diff --git a/lib/multi.go b/lib/multi.go
index 5b84a2e9e1..a20403cb6a 100644
--- a/lib/multi.go
+++ b/lib/multi.go
@@ -14,7 +14,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/utils/errkit"
- "github.com/rs/xid"
)
// unsafeOptions are those nuclei objects/instances/types
@@ -86,8 +85,6 @@ type ThreadSafeNucleiEngine struct {
// Note: Non-thread-safe methods start with Global prefix
func NewThreadSafeNucleiEngineCtx(ctx context.Context, opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) {
defaultOptions := types.DefaultOptions()
- defaultOptions.ExecutionId = xid.New().String()
-
e := &NucleiEngine{
opts: defaultOptions,
mode: threadSafe,
diff --git a/lib/sdk.go b/lib/sdk.go
index a70c02d61c..e98d14adf6 100644
--- a/lib/sdk.go
+++ b/lib/sdk.go
@@ -31,7 +31,6 @@ import (
"github.com/projectdiscovery/ratelimit"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/projectdiscovery/utils/errkit"
- "github.com/rs/xid"
)
// NucleiSDKOptions contains options for nuclei SDK
@@ -323,8 +322,6 @@ func (e *NucleiEngine) Store() *loader.Store {
func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*NucleiEngine, error) {
// default options
defaultOptions := types.DefaultOptions()
- defaultOptions.ExecutionId = xid.New().String()
-
e := &NucleiEngine{
opts: defaultOptions,
mode: singleInstance,
diff --git a/pkg/catalog/config/constants.go b/pkg/catalog/config/constants.go
index ebd49dda7d..480934c02c 100644
--- a/pkg/catalog/config/constants.go
+++ b/pkg/catalog/config/constants.go
@@ -31,7 +31,7 @@ const (
CLIConfigFileName = "config.yaml"
ReportingConfigFilename = "reporting-config.yaml"
// Version is the current version of nuclei
- Version = `v3.6.2`
+ Version = `v3.7.0`
// Directory Names of custom templates
CustomS3TemplatesDirName = "s3"
CustomGitHubTemplatesDirName = "github"
diff --git a/pkg/catalog/loader/loader.go b/pkg/catalog/loader/loader.go
index d7648073a1..19aa590970 100644
--- a/pkg/catalog/loader/loader.go
+++ b/pkg/catalog/loader/loader.go
@@ -172,6 +172,9 @@ func New(cfg *Config) (*Store, error) {
// Initialize metadata index and filter (load from disk & cache for reuse)
store.metadataIndex = store.loadTemplatesIndex()
store.indexFilter = store.buildIndexFilter()
+ if cfg.ExecutorOptions != nil {
+ cfg.ExecutorOptions.TemplateVerificationCallback = store.getTemplateVerification
+ }
store.saveMetadataIndexOnce = sync.OnceFunc(func() {
if store.metadataIndex == nil {
return
@@ -246,6 +249,22 @@ func New(cfg *Config) (*Store, error) {
return store, nil
}
+func (store *Store) getTemplateVerification(templatePath string) *protocols.TemplateVerification {
+ if store.metadataIndex == nil {
+ return nil
+ }
+
+ metadata, found := store.metadataIndex.Get(templatePath)
+ if !found {
+ return nil
+ }
+
+ return &protocols.TemplateVerification{
+ Verified: metadata.Verified,
+ Verifier: metadata.TemplateVerifier,
+ }
+}
+
func handleTemplatesEditorURLs(input string) string {
parsed, err := url.Parse(input)
if err != nil {
diff --git a/pkg/core/executors.go b/pkg/core/executors.go
index aeb85ddfe6..22c2b090c6 100644
--- a/pkg/core/executors.go
+++ b/pkg/core/executors.go
@@ -112,7 +112,7 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
match, err := e.executeTemplateOnInput(ctx, template, t.value)
if err != nil {
- e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), t.value.Input, err)
+ e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", template.ID, t.value.Input, err)
}
results.CompareAndSwap(false, match)
}()
@@ -216,7 +216,7 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
match, err := e.executeTemplateOnInput(ctx, template, value)
if err != nil {
- e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), value.Input, err)
+ e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", template.ID, value.Input, err)
}
results.CompareAndSwap(false, match)
}(tpl, target, sg)
diff --git a/pkg/input/formats/burp/burp.go b/pkg/input/formats/burp/burp.go
index 459c6d8a54..0848005537 100644
--- a/pkg/input/formats/burp/burp.go
+++ b/pkg/input/formats/burp/burp.go
@@ -9,7 +9,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/utils/conversion"
- "github.com/seh-msft/burpxml"
+ burpxml "github.com/projectdiscovery/utils/parsers/burp/xml"
)
// BurpFormat is a Burp XML File parser
@@ -36,12 +36,11 @@ func (j *BurpFormat) SetOptions(options formats.InputFormatOptions) {
// Parse parses the input and calls the provided callback
// function for each RawRequest it discovers.
func (j *BurpFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallback, filePath string) error {
- items, err := burpxml.Parse(input, true)
+ items, err := burpxml.ParseXML(input, burpxml.XMLParseOptions{DecodeBase64: true})
if err != nil {
return errors.Wrap(err, "could not decode burp xml schema")
}
- // Print the parsed data for verification
for _, item := range items.Items {
binx, err := base64.StdEncoding.DecodeString(item.Request.Raw)
if err != nil {
@@ -50,11 +49,11 @@ func (j *BurpFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallba
if strings.TrimSpace(conversion.String(binx)) == "" {
continue
}
- rawRequest, err := types.ParseRawRequestWithURL(conversion.String(binx), item.Url)
+ rawRequest, err := types.ParseRawRequestWithURL(conversion.String(binx), item.URL)
if err != nil {
return errors.Wrap(err, "could not parse raw request")
}
- resultsCb(rawRequest) // TODO: Handle false and true from callback
+ resultsCb(rawRequest)
}
return nil
}
diff --git a/pkg/input/provider/list/hmap_test.go b/pkg/input/provider/list/hmap_test.go
index d2a409352f..1c99031706 100644
--- a/pkg/input/provider/list/hmap_test.go
+++ b/pkg/input/provider/list/hmap_test.go
@@ -137,7 +137,7 @@ func Test_scanallips_normalizeStoreInputValue(t *testing.T) {
},
}
- input.Set("", tt.hostname)
+ input.Set(defaultOpts.ExecutionId, tt.hostname)
// scan
got := []string{}
input.hostMap.Scan(func(k, v []byte) error {
diff --git a/pkg/installer/template.go b/pkg/installer/template.go
index 00a9982da2..8987b305f8 100644
--- a/pkg/installer/template.go
+++ b/pkg/installer/template.go
@@ -214,7 +214,7 @@ func (t *TemplateManager) updateTemplatesAt(dir string) error {
if !HideUpdateChangesTable {
// print summary table
gologger.Print().Msgf("\nNuclei Templates %s Changelog\n", ghrd.Latest.GetTagName())
- gologger.DefaultLogger.Print().Msg(results.String())
+ gologger.Print().Msg(results.String())
}
} else {
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", ghrd.Latest.GetTagName(), dir)
diff --git a/pkg/js/generated/go/librsync/rsync.go b/pkg/js/generated/go/librsync/rsync.go
index 6c269fcb00..ffc6f0a616 100644
--- a/pkg/js/generated/go/librsync/rsync.go
+++ b/pkg/js/generated/go/librsync/rsync.go
@@ -21,6 +21,7 @@ func init() {
// Objects / Classes
"IsRsyncResponse": gojs.GetClassConstructor[lib_rsync.IsRsyncResponse](&lib_rsync.IsRsyncResponse{}),
+ "RsyncClient": gojs.GetClassConstructor[lib_rsync.RsyncClient](&lib_rsync.RsyncClient{}),
},
).Register()
}
diff --git a/pkg/js/generated/go/libtelnet/telnet.go b/pkg/js/generated/go/libtelnet/telnet.go
index a9b50a5fb0..a51a54b9c8 100644
--- a/pkg/js/generated/go/libtelnet/telnet.go
+++ b/pkg/js/generated/go/libtelnet/telnet.go
@@ -2,6 +2,7 @@ package telnet
import (
lib_telnet "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/telnet"
+ telnetmini "github.com/projectdiscovery/nuclei/v3/pkg/utils/telnetmini"
"github.com/Mzack9999/goja"
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
@@ -20,7 +21,10 @@ func init() {
// Var and consts
// Objects / Classes
- "IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}),
+ "TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}),
+ "IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}),
+ "TelnetInfoResponse": gojs.GetClassConstructor[lib_telnet.TelnetInfoResponse](&lib_telnet.TelnetInfoResponse{}),
+ "NTLMInfoResponse": gojs.GetClassConstructor[telnetmini.NTLMInfoResponse](&telnetmini.NTLMInfoResponse{}),
},
).Register()
}
diff --git a/pkg/js/generated/ts/rsync.ts b/pkg/js/generated/ts/rsync.ts
index afe2146803..6cb675b0d6 100755
--- a/pkg/js/generated/ts/rsync.ts
+++ b/pkg/js/generated/ts/rsync.ts
@@ -13,7 +13,61 @@ export function IsRsync(host: string, port: number): IsRsyncResponse | null {
return null;
}
-
+/**
+ * RsyncClient is a client for RSYNC servers.
+ * Internally client uses https://github.com/gokrazy/rsync driver.
+ * @example
+ * ```javascript
+ * const rsync = require('nuclei/rsync');
+ * const client = new rsync.RsyncClient();
+ * ```
+ */
+export class RsyncClient {
+
+ // Constructor of RsyncClient
+ constructor() {}
+
+ /**
+ * Connect establishes a connection to the rsync server with authentication.
+ * @example
+ * ```javascript
+ * const rsync = require('nuclei/rsync');
+ * const client = new rsync.RsyncClient();
+ * const connected = client.Connect('acme.com', 873, 'username', 'password', 'backup');
+ * ```
+ */
+ public Connect(host: string, port: number, username: string, password: string, module: string): boolean | null {
+ return null;
+ }
+
+ /**
+ * ListModules lists available modules on the rsync server.
+ * @example
+ * ```javascript
+ * const rsync = require('nuclei/rsync');
+ * const client = new rsync.RsyncClient();
+ * const modules = client.ListModules('acme.com', 873, 'username', 'password');
+ * log(toJSON(modules));
+ * ```
+ */
+ public ListModules(host: string, port: number, username: string, password: string): string[] | null {
+ return null;
+ }
+
+ /**
+ * ListFilesInModule lists files in a specific module on the rsync server.
+ * @example
+ * ```javascript
+ * const rsync = require('nuclei/rsync');
+ * const client = new rsync.RsyncClient();
+ * const files = client.ListFilesInModule('acme.com', 873, 'username', 'password', 'backup');
+ * log(toJSON(files));
+ * ```
+ */
+ public ListFilesInModule(host: string, port: number, username: string, password: string, module: string): string[] | null {
+ return null;
+ }
+}
/**
* IsRsyncResponse is the response from the IsRsync function.
diff --git a/pkg/js/generated/ts/telnet.ts b/pkg/js/generated/ts/telnet.ts
index cd49c2078e..39513bd91b 100755
--- a/pkg/js/generated/ts/telnet.ts
+++ b/pkg/js/generated/ts/telnet.ts
@@ -13,7 +13,65 @@ export function IsTelnet(host: string, port: number): IsTelnetResponse | null {
return null;
}
+/**
+ * TelnetClient is a client for Telnet servers.
+ * @example
+ * ```javascript
+ * const telnet = require('nuclei/telnet');
+ * const client = new telnet.TelnetClient();
+ * ```
+ */
+export class TelnetClient {
+
+ /**
+ * Connect tries to connect to provided host and port with telnet.
+ * Optionally provides username and password for authentication.
+ * Returns state of connection. If the connection is successful,
+ * the function will return true, otherwise false.
+ * @example
+ * ```javascript
+ * const telnet = require('nuclei/telnet');
+ * const client = new telnet.TelnetClient();
+ * const connected = client.Connect('acme.com', 23, 'username', 'password');
+ * ```
+ */
+ public Connect(host: string, port: number, username: string, password: string): boolean {
+ return false;
+ }
+ /**
+ * Info gathers information about the telnet server including encryption support.
+ * Uses the telnetmini library's DetectEncryption helper function.
+ * WARNING: The connection used for detection becomes unusable after this call.
+ * @example
+ * ```javascript
+ * const telnet = require('nuclei/telnet');
+ * const client = new telnet.TelnetClient();
+ * const info = client.Info('acme.com', 23);
+ * log(toJSON(info));
+ * ```
+ */
+ public Info(host: string, port: number): TelnetInfoResponse | null {
+ return null;
+ }
+
+ /**
+ * GetTelnetNTLMInfo implements the Nmap telnet-ntlm-info.nse script functionality.
+ * This function uses the telnetmini library and SMB packet crafting functions to send
+ * MS-TNAP NTLM authentication requests with null credentials. It might work only on
+ * Microsoft Telnet servers.
+ * @example
+ * ```javascript
+ * const telnet = require('nuclei/telnet');
+ * const client = new telnet.TelnetClient();
+ * const ntlmInfo = client.GetTelnetNTLMInfo('acme.com', 23);
+ * log(toJSON(ntlmInfo));
+ * ```
+ */
+ public GetTelnetNTLMInfo(host: string, port: number): NTLMInfoResponse | null {
+ return null;
+ }
+}
/**
* IsTelnetResponse is the response from the IsTelnet function.
@@ -32,3 +90,76 @@ export interface IsTelnetResponse {
Banner?: string,
}
+/**
+ * TelnetInfoResponse is the response from the Info function.
+ * @example
+ * ```javascript
+ * const telnet = require('nuclei/telnet');
+ * const client = new telnet.TelnetClient();
+ * const info = client.Info('acme.com', 23);
+ * log(toJSON(info));
+ * ```
+ */
+export interface TelnetInfoResponse {
+
+ SupportsEncryption?: boolean,
+
+ Banner?: string,
+
+ Options?: { [key: number]: number[] },
+}
+
+/**
+ * NTLMInfoResponse represents the response from NTLM information gathering.
+ * This matches exactly the output structure from the Nmap telnet-ntlm-info.nse script.
+ * @example
+ * ```javascript
+ * const telnet = require('nuclei/telnet');
+ * const client = new telnet.TelnetClient();
+ * const ntlmInfo = client.GetTelnetNTLMInfo('acme.com', 23);
+ * log(toJSON(ntlmInfo));
+ * ```
+ */
+export interface NTLMInfoResponse {
+
+ /**
+ * Target_Name from script (target_realm in script)
+ */
+ TargetName?: string,
+
+ /**
+ * NetBIOS_Domain_Name from script
+ */
+ NetBIOSDomainName?: string,
+
+ /**
+ * NetBIOS_Computer_Name from script
+ */
+ NetBIOSComputerName?: string,
+
+ /**
+ * DNS_Domain_Name from script
+ */
+ DNSDomainName?: string,
+
+ /**
+ * DNS_Computer_Name from script (fqdn in script)
+ */
+ DNSComputerName?: string,
+
+ /**
+ * DNS_Tree_Name from script (dns_forest_name in script)
+ */
+ DNSTreeName?: string,
+
+ /**
+ * Product_Version from script
+ */
+ ProductVersion?: string,
+
+ /**
+ * Raw timestamp for skew calculation
+ */
+ Timestamp?: number,
+}
+
diff --git a/pkg/js/libs/rsync/rsync.go b/pkg/js/libs/rsync/rsync.go
index a1b4073959..1dddd80333 100644
--- a/pkg/js/libs/rsync/rsync.go
+++ b/pkg/js/libs/rsync/rsync.go
@@ -1,18 +1,31 @@
package rsync
import (
+ "bytes"
"context"
"fmt"
+ "log/slog"
"net"
"strconv"
"time"
+ rsynclib "github.com/Mzack9999/go-rsync/rsync"
+
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
"github.com/praetorian-inc/fingerprintx/pkg/plugins/services/rsync"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)
type (
+ // RsyncClient is a client for RSYNC servers.
+ // Internally client uses https://github.com/gokrazy/rsync driver.
+ // @example
+ // ```javascript
+ // const rsync = require('nuclei/rsync');
+ // const client = new rsync.RsyncClient();
+ // ```
+ RsyncClient struct{}
+
// IsRsyncResponse is the response from the IsRsync function.
// this is returned by IsRsync function.
// @example
@@ -25,8 +38,30 @@ type (
IsRsync bool
Banner string
}
+
+ // ListSharesResponse is the response from the ListShares function.
+ // this is returned by ListShares function.
+ // @example
+ // ```javascript
+ // const rsync = require('nuclei/rsync');
+ // const client = new rsync.RsyncClient();
+ // const listShares = client.ListShares('acme.com', 873);
+ // log(toJSON(listShares));
+ RsyncListResponse struct {
+ Modules []string
+ Files []string
+ Output string
+ }
)
+func connectWithFastDialer(executionId string, host string, port int) (net.Conn, error) {
+ dialer := protocolstate.GetDialersWithId(executionId)
+ if dialer == nil {
+ return nil, fmt.Errorf("dialers not initialized for %s", executionId)
+ }
+ return dialer.Fastdialer.Dial(context.Background(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
+}
+
// IsRsync checks if a host is running a Rsync server.
// @example
// ```javascript
@@ -44,11 +79,7 @@ func isRsync(executionId string, host string, port int) (IsRsyncResponse, error)
resp := IsRsyncResponse{}
timeout := 5 * time.Second
- dialer := protocolstate.GetDialersWithId(executionId)
- if dialer == nil {
- return IsRsyncResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
- }
- conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
+ conn, err := connectWithFastDialer(executionId, host, port)
if err != nil {
return resp, err
}
@@ -59,7 +90,7 @@ func isRsync(executionId string, host string, port int) (IsRsyncResponse, error)
rsyncPlugin := rsync.RSYNCPlugin{}
service, err := rsyncPlugin.Run(conn, timeout, plugins.Target{Host: host})
if err != nil {
- return resp, err
+ return resp, nil
}
if service == nil {
return resp, nil
@@ -68,3 +99,115 @@ func isRsync(executionId string, host string, port int) (IsRsyncResponse, error)
resp.IsRsync = true
return resp, nil
}
+
+// ListModules lists the modules of a Rsync server.
+// @example
+// ```javascript
+// const rsync = require('nuclei/rsync');
+// const client = new rsync.RsyncClient();
+// const listModules = client.ListModules('acme.com', 873, 'username', 'password');
+// log(toJSON(listModules));
+// ```
+func (c *RsyncClient) ListModules(ctx context.Context, host string, port int, username string, password string) (RsyncListResponse, error) {
+ executionId := ctx.Value("executionId").(string)
+ return listModules(executionId, host, port, username, password)
+}
+
+// ListShares lists the shares of a Rsync server.
+// @example
+// ```javascript
+// const rsync = require('nuclei/rsync');
+// const client = new rsync.RsyncClient();
+// const listShares = client.ListFilesInModule('acme.com', 873, 'username', 'password', '/');
+// log(toJSON(listShares));
+// ```
+func (c *RsyncClient) ListFilesInModule(ctx context.Context, host string, port int, username string, password string, module string) (RsyncListResponse, error) {
+ executionId := ctx.Value("executionId").(string)
+ return listFilesInModule(executionId, host, port, username, password, module)
+}
+
+func listModules(executionId string, host string, port int, username string, password string) (RsyncListResponse, error) {
+ fastDialer := protocolstate.GetDialersWithId(executionId)
+ if fastDialer == nil {
+ return RsyncListResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
+ }
+
+ address := net.JoinHostPort(host, strconv.Itoa(port))
+
+ // Create a bytes buffer for logging
+ var logBuffer bytes.Buffer
+
+ // Create a custom slog handler that writes to the buffer
+ logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
+ Level: slog.LevelDebug,
+ })
+
+ // Create a logger that writes to our buffer
+ logger := slog.New(logHandler)
+
+ sr, err := rsynclib.ListModules(address,
+ rsynclib.WithClientAuth(username, password),
+ rsynclib.WithLogger(logger),
+ rsynclib.WithFastDialer(fastDialer.Fastdialer),
+ )
+ if err != nil {
+ return RsyncListResponse{}, fmt.Errorf("connect failed: %v", err)
+ }
+
+ result := RsyncListResponse{
+ Modules: make([]string, len(sr)),
+ Output: logBuffer.String(),
+ }
+
+ for i, item := range sr {
+ result.Modules[i] = string(item.Name)
+ }
+
+ return result, nil
+}
+
+func listFilesInModule(executionId string, host string, port int, username string, password string, module string) (RsyncListResponse, error) {
+ fastDialer := protocolstate.GetDialersWithId(executionId)
+ if fastDialer == nil {
+ return RsyncListResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
+ }
+
+ address := net.JoinHostPort(host, strconv.Itoa(port))
+
+ // Create a bytes buffer for logging
+ var logBuffer bytes.Buffer
+
+ // Create a custom slog handler that writes to the buffer
+ logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
+ Level: slog.LevelDebug,
+ })
+
+ // Create a logger that writes to our buffer
+ logger := slog.New(logHandler)
+
+ sr, err := rsynclib.SocketClient(nil, address, module, ".",
+ rsynclib.WithClientAuth(username, password),
+ rsynclib.WithLogger(logger),
+ rsynclib.WithFastDialer(fastDialer.Fastdialer),
+ )
+ if err != nil {
+ return RsyncListResponse{}, fmt.Errorf("connect failed: %v", err)
+ }
+
+ // Try to list files to test authentication
+ list, err := sr.List()
+ if err != nil {
+ return RsyncListResponse{}, fmt.Errorf("authentication failed: %v", err)
+ }
+
+ result := RsyncListResponse{
+ Files: make([]string, len(list)),
+ Output: logBuffer.String(),
+ }
+
+ for i, item := range list {
+ result.Files[i] = string(item.Path)
+ }
+
+ return result, nil
+}
diff --git a/pkg/js/libs/telnet/telnet.go b/pkg/js/libs/telnet/telnet.go
index db220309fc..86006bcb06 100644
--- a/pkg/js/libs/telnet/telnet.go
+++ b/pkg/js/libs/telnet/telnet.go
@@ -10,6 +10,23 @@ import (
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
"github.com/praetorian-inc/fingerprintx/pkg/plugins/services/telnet"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
+ "github.com/projectdiscovery/nuclei/v3/pkg/utils/telnetmini"
+)
+
+// Telnet protocol constants
+const (
+ IAC = 255 // Interpret As Command
+ WILL = 251 // Will
+ WONT = 252 // Won't
+ DO = 253 // Do
+ DONT = 254 // Don't
+ SB = 250 // Subnegotiation Begin
+ SE = 240 // Subnegotiation End
+ ECHO = 1 // Echo
+ SUPPRESS_GO_AHEAD = 3 // Suppress Go Ahead
+ TERMINAL_TYPE = 24 // Terminal Type
+ NAWS = 31 // Negotiate About Window Size
+ ENCRYPT = 38 // Encryption option (0x26)
)
type (
@@ -25,6 +42,28 @@ type (
IsTelnet bool
Banner string
}
+
+ // TelnetInfoResponse is the response from the Info function.
+ // @example
+ // ```javascript
+ // const telnet = require('nuclei/telnet');
+ // const client = new telnet.TelnetClient();
+ // const info = client.Info('acme.com', 23);
+ // log(toJSON(info));
+ // ```
+ TelnetInfoResponse struct {
+ SupportsEncryption bool
+ Banner string
+ Options map[int][]int
+ }
+
+ // TelnetClient is a client for Telnet servers.
+ // @example
+ // ```javascript
+ // const telnet = require('nuclei/telnet');
+ // const client = new telnet.TelnetClient();
+ // ```
+ TelnetClient struct{}
)
// IsTelnet checks if a host is running a Telnet server.
@@ -69,3 +108,171 @@ func isTelnet(executionId string, host string, port int) (IsTelnetResponse, erro
resp.IsTelnet = true
return resp, nil
}
+
+// Connect tries to connect to provided host and port with telnet.
+// Optionally provides username and password for authentication.
+// Returns state of connection. If the connection is successful,
+// the function will return true, otherwise false.
+// @example
+// ```javascript
+// const telnet = require('nuclei/telnet');
+// const client = new telnet.TelnetClient();
+// const connected = client.Connect('acme.com', 23, 'username', 'password');
+// ```
+func (c *TelnetClient) Connect(ctx context.Context, host string, port int, username string, password string) (bool, error) {
+ executionId := ctx.Value("executionId").(string)
+
+ dialer := protocolstate.GetDialersWithId(executionId)
+ if dialer == nil {
+ return false, fmt.Errorf("dialers not initialized for %s", executionId)
+ }
+
+ if !protocolstate.IsHostAllowed(executionId, host) {
+ return false, protocolstate.ErrHostDenied.Msgf(host)
+ }
+
+ // Create TCP connection
+ conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
+ if err != nil {
+ return false, err
+ }
+
+ // Create telnet client using the telnetmini library
+ client := telnetmini.New(conn)
+ defer func() {
+ _ = client.Close()
+ }()
+
+ // Handle authentication if credentials provided
+ if username != "" && password != "" {
+ // Set a timeout context for authentication
+ authCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ if err := client.Auth(authCtx, username, password); err != nil {
+ return false, err
+ }
+ }
+
+ return true, nil
+}
+
+// Info gathers information about the telnet server including encryption support.
+// Uses the telnetmini library's DetectEncryption helper function.
+// WARNING: The connection used for detection becomes unusable after this call.
+// @example
+// ```javascript
+// const telnet = require('nuclei/telnet');
+// const client = new telnet.TelnetClient();
+// const info = client.Info('acme.com', 23);
+// log(toJSON(info));
+// ```
+func (c *TelnetClient) Info(ctx context.Context, host string, port int) (TelnetInfoResponse, error) {
+ executionId := ctx.Value("executionId").(string)
+
+ if !protocolstate.IsHostAllowed(executionId, host) {
+ return TelnetInfoResponse{}, protocolstate.ErrHostDenied.Msgf(host)
+ }
+
+ // Create TCP connection for encryption detection
+ dialer := protocolstate.GetDialersWithId(executionId)
+ if dialer == nil {
+ return TelnetInfoResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
+ }
+
+ conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
+ if err != nil {
+ return TelnetInfoResponse{}, err
+ }
+ defer func() {
+ _ = conn.Close()
+ }()
+
+ // Use the telnetmini library's DetectEncryption helper function
+ // Note: The connection becomes unusable after this call
+ encryptionInfo, err := telnetmini.DetectEncryption(conn, 7*time.Second)
+ if err != nil {
+ return TelnetInfoResponse{}, err
+ }
+
+ return TelnetInfoResponse{
+ SupportsEncryption: encryptionInfo.SupportsEncryption,
+ Banner: encryptionInfo.Banner,
+ Options: encryptionInfo.Options,
+ }, nil
+}
+
+// GetTelnetNTLMInfo implements the Nmap telnet-ntlm-info.nse script functionality.
+// This function uses the telnetmini library and SMB packet crafting functions to send
+// MS-TNAP NTLM authentication requests with null credentials. It might work only on
+// Microsoft Telnet servers.
+// @example
+// ```javascript
+// const telnet = require('nuclei/telnet');
+// const client = new telnet.TelnetClient();
+// const ntlmInfo = client.GetTelnetNTLMInfo('acme.com', 23);
+// log(toJSON(ntlmInfo));
+// ```
+func (c *TelnetClient) GetTelnetNTLMInfo(ctx context.Context, host string, port int) (*telnetmini.NTLMInfoResponse, error) {
+ executionId := ctx.Value("executionId").(string)
+
+ if !protocolstate.IsHostAllowed(executionId, host) {
+ return nil, protocolstate.ErrHostDenied.Msgf(host)
+ }
+
+ dialer := protocolstate.GetDialersWithId(executionId)
+ if dialer == nil {
+ return nil, fmt.Errorf("dialers not initialized for %s", executionId)
+ }
+
+ // Create TCP connection
+ conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ _ = conn.Close()
+ }()
+
+ // Create telnet client using the telnetmini library
+ client := telnetmini.New(conn)
+ defer func() {
+ _ = client.Close()
+ }()
+
+ // Set timeout
+ _ = conn.SetDeadline(time.Now().Add(10 * time.Second))
+
+ // Use the MS-TNAP packet crafting functions from our telnetmini library
+ // Create MS-TNAP Login Packet (Option Command IS) as per Nmap script
+ tnapLoginPacket := telnetmini.CreateTNAPLoginPacket()
+
+ // Send the MS-TNAP login packet
+ _, err = conn.Write(tnapLoginPacket)
+ if err != nil {
+ return nil, fmt.Errorf("failed to send MS-TNAP login packet: %w", err)
+ }
+
+ // Read response data
+ buffer := make([]byte, 4096)
+ n, err := conn.Read(buffer)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ }
+
+ if n == 0 {
+ return nil, fmt.Errorf("no response received")
+ }
+
+ // Parse NTLM response using our telnetmini library functions
+ response := buffer[:n]
+
+ // Use the parsing functions from our library instead of reimplementing
+ // This should use the NTLM parsing functions we added to telnetmini
+ ntlmInfo, err := telnetmini.ParseNTLMResponse(response)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse NTLM response: %w", err)
+ }
+
+ return ntlmInfo, nil
+}
diff --git a/pkg/protocols/common/hosterrorscache/hosterrorscache.go b/pkg/protocols/common/hosterrorscache/hosterrorscache.go
index 3053b8efb1..571e2af92f 100644
--- a/pkg/protocols/common/hosterrorscache/hosterrorscache.go
+++ b/pkg/protocols/common/hosterrorscache/hosterrorscache.go
@@ -27,6 +27,7 @@ type CacheInterface interface {
Remove(ctx *contextargs.Context) // remove a host from the cache
MarkFailed(protoType string, ctx *contextargs.Context, err error) // record a failure (and cause) for the host
MarkFailedOrRemove(protoType string, ctx *contextargs.Context, err error) // record a failure (and cause) for the host or remove it
+ IsPermanentErr(ctx *contextargs.Context, err error) bool // return true if the error is permanent for the host
}
var (
@@ -137,8 +138,9 @@ func (c *Cache) Check(protoType string, ctx *contextargs.Context) bool {
defer cache.mu.Unlock()
if cache.isPermanentErr {
- // skipping permanent errors is expected so verbose instead of info
- gologger.Verbose().Msgf("Skipped %s from target list as found unresponsive permanently: %s", finalValue, cache.cause)
+ cache.Do(func() {
+ gologger.Info().Msgf("Skipped %s from target list as found unresponsive permanently: %s", finalValue, cache.cause)
+ })
return true
}
@@ -232,6 +234,28 @@ func (c *Cache) MarkFailedOrRemove(protoType string, ctx *contextargs.Context, e
_ = c.failedTargets.Set(cacheKey, cache)
}
+// IsPermanentErr returns true if the error is permanent for the host.
+func (c *Cache) IsPermanentErr(ctx *contextargs.Context, err error) bool {
+ if err == nil {
+ return false
+ }
+
+ if errkit.IsKind(err, errkit.ErrKindNetworkPermanent) {
+ return true
+ }
+
+ cacheKey := c.GetKeyFromContext(ctx, err)
+ cache, cacheErr := c.failedTargets.GetIFPresent(cacheKey)
+ if cacheErr != nil {
+ return false
+ }
+
+ cache.mu.Lock()
+ defer cache.mu.Unlock()
+
+ return cache.isPermanentErr
+}
+
// GetKeyFromContext returns the key for the cache from the context
func (c *Cache) GetKeyFromContext(ctx *contextargs.Context, err error) string {
// Note:
diff --git a/pkg/protocols/common/interactsh/interactsh.go b/pkg/protocols/common/interactsh/interactsh.go
index e92cfaecd9..f9d3e10af6 100644
--- a/pkg/protocols/common/interactsh/interactsh.go
+++ b/pkg/protocols/common/interactsh/interactsh.go
@@ -200,6 +200,14 @@ func (c *Client) processInteractionForRequest(interaction *server.Interaction, d
} else {
data.Event.SetOperatorResult(result)
}
+ // ensure payload values are preserved for interactsh-only matches
+ data.Event.Lock()
+ if data.Event.OperatorsResult != nil && len(data.Event.OperatorsResult.PayloadValues) == 0 {
+ if payloads, ok := data.Event.InternalEvent["payloads"].(map[string]interface{}); ok {
+ data.Event.OperatorsResult.PayloadValues = payloads
+ }
+ }
+ data.Event.Unlock()
data.Event.Lock()
data.Event.Results = data.MakeResultFunc(data.Event)
diff --git a/pkg/protocols/headless/engine/engine.go b/pkg/protocols/headless/engine/engine.go
index c045580d64..0d2a75786c 100644
--- a/pkg/protocols/headless/engine/engine.go
+++ b/pkg/protocols/headless/engine/engine.go
@@ -35,61 +35,72 @@ type Browser struct {
// New creates a new nuclei headless browser module
func New(options *types.Options) (*Browser, error) {
- dataStore, err := os.MkdirTemp("", "nuclei-*")
- if err != nil {
- return nil, errors.Wrap(err, "could not create temporary directory")
- }
- previousPIDs := processutil.FindProcesses(processutil.IsChromeProcess)
-
- chromeLauncher := launcher.New().
- Leakless(false).
- Set("disable-gpu", "true").
- Set("ignore-certificate-errors", "true").
- Set("ignore-certificate-errors", "1").
- Set("disable-crash-reporter", "true").
- Set("disable-notifications", "true").
- Set("hide-scrollbars", "true").
- Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)).
- Set("mute-audio", "true").
- Set("incognito", "true").
- Delete("use-mock-keychain").
- UserDataDir(dataStore)
-
- if MustDisableSandbox() {
- chromeLauncher = chromeLauncher.NoSandbox(true)
- }
+ var launcherURL, dataStore string
+ var previousPIDs map[int32]struct{}
+ var err error
- executablePath, err := os.Executable()
- if err != nil {
- return nil, err
- }
+ chromeLauncher := launcher.New()
- // if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome
- useMusl, _ := fileutil.UseMusl(executablePath)
- if options.UseInstalledChrome || useMusl {
- if chromePath, hasChrome := launcher.LookPath(); hasChrome {
- chromeLauncher.Bin(chromePath)
- } else {
- return nil, errors.New("the chrome browser is not installed")
+ if options.CDPEndpoint == "" {
+ previousPIDs = processutil.FindProcesses(processutil.IsChromeProcess)
+
+ dataStore, err = os.MkdirTemp("", "nuclei-*")
+ if err != nil {
+ return nil, errors.Wrap(err, "could not create temporary directory")
}
- }
- if options.ShowBrowser {
- chromeLauncher = chromeLauncher.Headless(false)
- } else {
- chromeLauncher = chromeLauncher.Headless(true)
- }
- if options.AliveHttpProxy != "" {
- chromeLauncher = chromeLauncher.Proxy(options.AliveHttpProxy)
- }
+ chromeLauncher = chromeLauncher.
+ Leakless(false).
+ Set("disable-crash-reporter").
+ Set("disable-gpu").
+ Set("disable-notifications").
+ Set("hide-scrollbars").
+ Set("ignore-certificate-errors").
+ Set("ignore-ssl-errors").
+ Set("incognito").
+ Set("mute-audio").
+ Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)).
+ Delete("use-mock-keychain").
+ UserDataDir(dataStore)
+
+ if MustDisableSandbox() {
+ chromeLauncher = chromeLauncher.NoSandbox(true)
+ }
- for k, v := range options.ParseHeadlessOptionalArguments() {
- chromeLauncher.Set(flags.Flag(k), v)
- }
+ executablePath, err := os.Executable()
+ if err != nil {
+ return nil, err
+ }
- launcherURL, err := chromeLauncher.Launch()
- if err != nil {
- return nil, err
+ // if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome
+ useMusl, _ := fileutil.UseMusl(executablePath)
+ if options.UseInstalledChrome || useMusl {
+ if chromePath, hasChrome := launcher.LookPath(); hasChrome {
+ chromeLauncher.Bin(chromePath)
+ } else {
+ return nil, errors.New("the chrome browser is not installed")
+ }
+ }
+
+ if options.ShowBrowser {
+ chromeLauncher = chromeLauncher.Headless(false)
+ } else {
+ chromeLauncher = chromeLauncher.Headless(true)
+ }
+ if options.AliveHttpProxy != "" {
+ chromeLauncher = chromeLauncher.Proxy(options.AliveHttpProxy)
+ }
+
+ for k, v := range options.ParseHeadlessOptionalArguments() {
+ chromeLauncher.Set(flags.Flag(k), v)
+ }
+
+ launcherURL, err = chromeLauncher.Launch()
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ launcherURL = options.CDPEndpoint
}
browser := rod.New().ControlURL(launcherURL)
@@ -178,7 +189,13 @@ func (b *Browser) getHTTPClient() (*http.Client, error) {
}
// Close closes the browser engine
+//
+// When connected over CDP, it does NOT close the browsers.
func (b *Browser) Close() {
+ if b.options.CDPEndpoint != "" {
+ return
+ }
+
_ = b.engine.Close()
b.launcher.Kill()
_ = os.RemoveAll(b.tempDir)
diff --git a/pkg/protocols/headless/request.go b/pkg/protocols/headless/request.go
index 7518cbc6c0..406deba982 100644
--- a/pkg/protocols/headless/request.go
+++ b/pkg/protocols/headless/request.go
@@ -275,7 +275,7 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, payloads
return nil
}
-// getLastNavigationURL returns last successfully navigated URL
+// getLastNavigationURLWithLog returns last successfully navigated URL
func (request *Request) getLastNavigationURLWithLog(reqLog map[string]string) string {
for i := len(request.Steps) - 1; i >= 0; i-- {
if request.Steps[i].ActionType.ActionType == engine.ActionNavigate {
diff --git a/pkg/protocols/http/raw/raw.go b/pkg/protocols/http/raw/raw.go
index 7b1457afa5..dce6ca02f6 100644
--- a/pkg/protocols/http/raw/raw.go
+++ b/pkg/protocols/http/raw/raw.go
@@ -38,6 +38,25 @@ func Parse(request string, inputURL *urlutil.URL, unsafe, disablePathAutomerge b
return nil, err
}
+ // handle full URLs first (before checking unsafe flag) to extract relative path
+ if strings.HasPrefix(rawrequest.Path, "http://") || strings.HasPrefix(rawrequest.Path, "https://") {
+ urlx, err := urlutil.ParseURL(rawrequest.Path, true)
+ if err != nil {
+ return nil, errkit.Wrapf(err, "failed to parse url %v from template", rawrequest.Path)
+ }
+ prevPath := rawrequest.Path
+ relPath := urlx.GetRelativePath()
+
+ // NOTE(dwisiswant0): Use rel path instead if unsafe.
+ // See https://github.com/projectdiscovery/nuclei/issues/6558.
+ if unsafe {
+ rawrequest.UnsafeRawBytes = bytes.Replace(rawrequest.UnsafeRawBytes, []byte(prevPath), []byte(relPath), 1)
+ }
+
+ // rotate full URL with rel path
+ rawrequest.Path = relPath
+ }
+
switch {
// If path is empty do not tamper input url (see doc)
// can be omitted but makes things clear
@@ -47,22 +66,6 @@ func Parse(request string, inputURL *urlutil.URL, unsafe, disablePathAutomerge b
rawrequest.Path = inputURL.GetRelativePath()
}
- // full url provided instead of rel path
- case strings.HasPrefix(rawrequest.Path, "http") && !unsafe:
- urlx, err := urlutil.ParseURL(rawrequest.Path, true)
- if err != nil {
- return nil, errkit.Wrapf(err, "failed to parse url %v from template", rawrequest.Path)
- }
- cloned := inputURL.Clone()
- cloned.Params.IncludeEquals = true
- if disablePathAutomerge {
- cloned.Path = ""
- }
- parseErr := cloned.MergePath(urlx.GetRelativePath(), true)
- if parseErr != nil {
- return nil, errkit.Wrapf(parseErr, "could not automergepath for template path %v", urlx.GetRelativePath())
- }
- rawrequest.Path = cloned.GetRelativePath()
// If unsafe changes must be made in raw request string itself
case unsafe:
prevPath := rawrequest.Path
diff --git a/pkg/protocols/http/raw/raw_test.go b/pkg/protocols/http/raw/raw_test.go
index 80fefff7fa..c514966835 100644
--- a/pkg/protocols/http/raw/raw_test.go
+++ b/pkg/protocols/http/raw/raw_test.go
@@ -122,6 +122,82 @@ func TestDisableMergePath(t *testing.T) {
}
+func TestUnsafeWithFullURL(t *testing.T) {
+ // Test unsafe mode with full URL - should extract relative path
+ request, err := Parse(`GET http://127.0.0.1/foo HTTP/1.1
+Host: {{Hostname}}
+User-Agent: Mozilla/5.0
+Connection: close`, parseURL(t, "http://httpbin.org/bar"), true, true)
+ require.Nil(t, err, "could not parse unsafe request with full URL")
+ require.Equal(t, "/foo", request.Path, "Could not extract relative path from full URL in unsafe mode")
+ require.Contains(t, string(request.UnsafeRawBytes), "GET /foo HTTP/1.1", "UnsafeRawBytes should contain relative path, not full URL")
+ require.NotContains(t, string(request.UnsafeRawBytes), "http://127.0.0.1", "UnsafeRawBytes should not contain full URL")
+}
+
+func TestUnsafeWithFullURLAndPath(t *testing.T) {
+ // Test unsafe mode with full URL and target URL that has a path
+ request, err := Parse(`GET http://127.0.0.1/foo HTTP/1.1
+Host: {{Hostname}}
+User-Agent: Mozilla/5.0
+Connection: close`, parseURL(t, "http://httpbin.org/bar"), true, false)
+ require.Nil(t, err, "could not parse unsafe request with full URL and path merge")
+ require.Equal(t, "/bar/foo", request.Path, "Could not merge path correctly from full URL in unsafe mode")
+ require.Contains(t, string(request.UnsafeRawBytes), "GET /bar/foo HTTP/1.1", "UnsafeRawBytes should contain merged relative path")
+ require.NotContains(t, string(request.UnsafeRawBytes), "http://127.0.0.1", "UnsafeRawBytes should not contain full URL")
+}
+
+func TestUnsafeWithFullURLAndQueryParams(t *testing.T) {
+ // Test unsafe mode with full URL containing query parameters
+ request, err := Parse(`GET http://127.0.0.1/foo?id=123&name=test HTTP/1.1
+Host: {{Hostname}}
+User-Agent: Mozilla/5.0
+Connection: close`, parseURL(t, "http://httpbin.org/bar"), true, true)
+ require.Nil(t, err, "could not parse unsafe request with full URL and query params")
+ require.Equal(t, "/foo?id=123&name=test", request.Path, "Could not extract relative path with query params from full URL in unsafe mode")
+ require.Contains(t, string(request.UnsafeRawBytes), "GET /foo?id=123&name=test HTTP/1.1", "UnsafeRawBytes should contain relative path with query params")
+ require.NotContains(t, string(request.UnsafeRawBytes), "http://127.0.0.1", "UnsafeRawBytes should not contain full URL")
+}
+
+func TestUnsafeWithHTTPSFullURL(t *testing.T) {
+ // Test unsafe mode with HTTPS full URL
+ request, err := Parse(`GET https://secure.example.com/api/v1/users HTTP/1.1
+Host: {{Hostname}}
+Authorization: Bearer token123
+Connection: close`, parseURL(t, "https://target.com/test"), true, true)
+ require.Nil(t, err, "could not parse unsafe request with HTTPS full URL")
+ require.Equal(t, "/api/v1/users", request.Path, "Could not extract relative path from HTTPS full URL in unsafe mode")
+ require.Contains(t, string(request.UnsafeRawBytes), "GET /api/v1/users HTTP/1.1", "UnsafeRawBytes should contain relative path")
+ require.NotContains(t, string(request.UnsafeRawBytes), "https://secure.example.com", "UnsafeRawBytes should not contain full URL")
+}
+
+func TestUnsafeWithFullURLRootPath(t *testing.T) {
+ // Test unsafe mode with full URL pointing to root path
+ // When disable-path-automerge is true and path is /, it becomes empty string (expected behavior)
+ request, err := Parse(`GET http://example.com/ HTTP/1.1
+Host: {{Hostname}}
+Connection: close`, parseURL(t, "http://target.com/api"), true, true)
+ require.Nil(t, err, "could not parse unsafe request with full URL root path")
+ // With disable-path-automerge=true and root path, it becomes empty per existing logic
+ require.Equal(t, "", request.Path, "Root path with disable-path-automerge should be empty")
+
+ // Test with disable-path-automerge=false
+ request, err = Parse(`GET http://example.com/ HTTP/1.1
+Host: {{Hostname}}
+Connection: close`, parseURL(t, "http://target.com/api"), true, false)
+ require.Nil(t, err, "could not parse unsafe request with full URL root path and merge")
+ require.Equal(t, "/api", request.Path, "Should merge with target path when automerge enabled")
+}
+
+func TestSafeWithFullURL(t *testing.T) {
+ // Verify that safe mode still works correctly with full URLs (existing behavior)
+ request, err := Parse(`GET http://example.com/api/users HTTP/1.1
+Host: {{Hostname}}
+Connection: close`, parseURL(t, "http://target.com/v1"), false, true)
+ require.Nil(t, err, "could not parse safe request with full URL")
+ require.Equal(t, "/api/users", request.Path, "Could not extract path from full URL in safe mode")
+ require.Equal(t, "http://target.com/api/users", request.FullURL, "Could not build correct FullURL in safe mode")
+}
+
func parseURL(t *testing.T, inputurl string) *urlutil.URL {
urlx, err := urlutil.Parse(inputurl)
if err != nil {
diff --git a/pkg/protocols/http/request.go b/pkg/protocols/http/request.go
index 0b7a35bc4f..7fb3218e7d 100644
--- a/pkg/protocols/http/request.go
+++ b/pkg/protocols/http/request.go
@@ -190,7 +190,7 @@ func (request *Request) executeRaceRequest(input *contextargs.Context, previous
return multierr.Combine(spmHandler.CombinedResults()...)
}
-// executeRaceRequest executes parallel requests for a template
+// executeParallelHTTP executes parallel requests for a template
func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicValues output.InternalEvent, callback protocols.OutputEventCallback) error {
// Workers that keeps enqueuing new requests
maxWorkers := request.Threads
@@ -242,8 +242,9 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
// bounded worker-pool to avoid spawning one goroutine per payload
type task struct {
- req *generatedRequest
- updatedInput *contextargs.Context
+ req *generatedRequest
+ updatedInput *contextargs.Context
+ hasInteractMarkers bool
}
var workersWg sync.WaitGroup
@@ -268,11 +269,27 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
continue
}
request.options.RateLimitTake()
+ hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
+ needsRequestEvent := hasInteractMatchers && request.NeedsRequestCondition()
select {
case <-spmHandler.Done():
spmHandler.Release()
continue
- case spmHandler.ResultChan <- request.executeRequest(t.updatedInput, t.req, make(map[string]interface{}), false, wrappedCallback, 0):
+ case spmHandler.ResultChan <- request.executeRequest(t.updatedInput, t.req, make(map[string]interface{}), hasInteractMatchers, func(event *output.InternalWrappedEvent) {
+ if (t.hasInteractMarkers || needsRequestEvent) && request.options.Interactsh != nil {
+ requestData := &interactsh.RequestData{
+ MakeResultFunc: request.MakeResultEvent,
+ Event: event,
+ Operators: request.CompiledOperators,
+ MatchFunc: request.Match,
+ ExtractFunc: request.Extract,
+ }
+ allOASTUrls := httputils.GetInteractshURLSFromEvent(event.InternalEvent)
+ allOASTUrls = append(allOASTUrls, t.req.interactshURLs...)
+ request.options.Interactsh.RequestEvent(sliceutil.Dedupe(allOASTUrls), requestData)
+ }
+ wrappedCallback(event)
+ }, 0):
spmHandler.Release()
}
}
@@ -330,6 +347,7 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
workersWg.Wait()
return err
}
+ hasInteractMarkers := interactsh.HasMarkers(inputData) || len(generatedHttpRequest.interactshURLs) > 0
if input.MetaInput.Input == "" {
input.MetaInput.Input = generatedHttpRequest.URL()
}
@@ -350,7 +368,7 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
return nil
}
return multierr.Combine(spmHandler.CombinedResults()...)
- case tasks <- task{req: generatedHttpRequest, updatedInput: updatedInput}:
+ case tasks <- task{req: generatedHttpRequest, updatedInput: updatedInput, hasInteractMarkers: hasInteractMarkers}:
}
request.options.Progress.IncrementRequests()
}
@@ -501,7 +519,7 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicVa
}
// verify if parallel elaboration was requested
- if request.Threads > 0 && len(request.Payloads) > 0 {
+ if request.Threads > 0 && (len(request.Payloads) > 0 || request.Race) {
return request.executeParallelHTTP(input, dynamicValues, callback)
}
@@ -1047,6 +1065,7 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
request.pruneSignatureInternalValues(generatedRequest.meta)
interimEvent := generators.MergeMaps(generatedRequest.dynamicValues, finalEvent)
+ interimEvent["payloads"] = generatedRequest.meta
// add the request URL pattern to the event BEFORE operators execute
// so that interactsh events etc can also access it
if request.options.ExportReqURLPattern {
diff --git a/pkg/protocols/http/request_test.go b/pkg/protocols/http/request_test.go
index 9eb7b100e4..dfc5041a23 100644
--- a/pkg/protocols/http/request_test.go
+++ b/pkg/protocols/http/request_test.go
@@ -274,6 +274,9 @@ func (f *fakeHostErrorsCache) MarkFailedOrRemove(string, *contextargs.Context, e
// Check always returns true to simulate an already unresponsive host
func (f *fakeHostErrorsCache) Check(string, *contextargs.Context) bool { return true }
+// IsPermanentErr returns false for tests
+func (f *fakeHostErrorsCache) IsPermanentErr(*contextargs.Context, error) bool { return false }
+
func TestExecuteParallelHTTP_StopAtFirstMatch(t *testing.T) {
options := testutils.DefaultOptions
testutils.Init(options)
diff --git a/pkg/protocols/network/network.go b/pkg/protocols/network/network.go
index 16af0362dd..e305fbf648 100644
--- a/pkg/protocols/network/network.go
+++ b/pkg/protocols/network/network.go
@@ -179,11 +179,20 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
request.addresses = append(request.addresses, addressKV{address: address, tls: shouldUseTLS})
}
// Pre-compile any input dsl functions before executing the request.
+ // Build a map with template variables and -var flag values for pre-compilation
+ preCompileVars := request.options.Variables.GetAll()
+ // Merge in -var flag values
+ if request.options.Options != nil {
+ generators.MergeMapsInto(preCompileVars, request.options.Options.Vars.AsMap())
+ }
+ // Also merge in constants
+ generators.MergeMapsInto(preCompileVars, request.options.Constants)
+
for _, input := range request.Inputs {
if input.Type.String() != "" {
continue
}
- if compiled, evalErr := expressions.Evaluate(input.Data, map[string]interface{}{}); evalErr == nil {
+ if compiled, evalErr := expressions.Evaluate(input.Data, preCompileVars); evalErr == nil {
input.Data = compiled
}
}
diff --git a/pkg/protocols/protocols.go b/pkg/protocols/protocols.go
index cb29f1258f..bf2d8ca00c 100644
--- a/pkg/protocols/protocols.go
+++ b/pkg/protocols/protocols.go
@@ -57,6 +57,12 @@ type Executer interface {
ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error)
}
+// TemplateVerification holds cached verification information for a template.
+type TemplateVerification struct {
+ Verified bool
+ Verifier string
+}
+
// ExecutorOptions contains the configuration options for executer clients
type ExecutorOptions struct {
// TemplateID is the ID of the template for the request
@@ -67,6 +73,9 @@ type ExecutorOptions struct {
TemplateInfo model.Info
// TemplateVerifier is the verifier for the template
TemplateVerifier string
+ // TemplateVerificationCallback returns cached verification info for a template path.
+ // If it returns nil, verification should be computed normally.
+ TemplateVerificationCallback func(templatePath string) *TemplateVerification
// RawTemplate is the raw template for the request
RawTemplate []byte
// Output is a writer interface for writing output events from executer.
@@ -266,6 +275,7 @@ func (e *ExecutorOptions) Copy() *ExecutorOptions {
TemplatePath: e.TemplatePath,
TemplateInfo: e.TemplateInfo,
TemplateVerifier: e.TemplateVerifier,
+ TemplateVerificationCallback: e.TemplateVerificationCallback,
RawTemplate: e.RawTemplate,
Output: e.Output,
Options: e.Options,
diff --git a/pkg/protocols/utils/fields.go b/pkg/protocols/utils/fields.go
index a2b1a80d71..4cd90f0c6c 100644
--- a/pkg/protocols/utils/fields.go
+++ b/pkg/protocols/utils/fields.go
@@ -1,6 +1,9 @@
package utils
import (
+ "net"
+ "strings"
+
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
iputil "github.com/projectdiscovery/utils/ip"
urlutil "github.com/projectdiscovery/utils/url"
@@ -28,17 +31,21 @@ func GetJsonFieldsFromURL(URL string) JsonFields {
URL: parsed.String(),
Path: parsed.Path,
}
+
+ host := parsed.Host
+ host, fields.Port = extractHostPort(host, fields.Port)
+
if fields.Port == "" {
fields.Port = "80"
if fields.Scheme == "https" {
fields.Port = "443"
}
}
- if iputil.IsIP(parsed.Host) {
- fields.Ip = parsed.Host
+ if iputil.IsIP(host) {
+ fields.Ip = host
}
- fields.Host = parsed.Host
+ fields.Host = host
return fields
}
@@ -56,16 +63,45 @@ func GetJsonFieldsFromMetaInput(ctx *contextargs.MetaInput) JsonFields {
fields.Scheme = parsed.Scheme
fields.URL = parsed.String()
fields.Path = parsed.Path
+
+ host := parsed.Host
+ host, fields.Port = extractHostPort(host, fields.Port)
+
if fields.Port == "" {
fields.Port = "80"
if fields.Scheme == "https" {
fields.Port = "443"
}
}
- if iputil.IsIP(parsed.Host) {
- fields.Ip = parsed.Host
+ if iputil.IsIP(host) {
+ fields.Ip = host
}
- fields.Host = parsed.Host
+ fields.Host = host
return fields
}
+
+func extractHostPort(host, port string) (string, string) {
+ if !strings.Contains(host, ":") {
+ return host, port
+ }
+ if strings.HasPrefix(host, "[") {
+ if idx := strings.Index(host, "]:"); idx != -1 {
+ if port == "" {
+ port = host[idx+2:]
+ }
+ return host[1:idx], port
+ }
+ if strings.HasSuffix(host, "]") {
+ return host[1 : len(host)-1], port
+ }
+ return host, port
+ }
+ if h, p, err := net.SplitHostPort(host); err == nil {
+ if port == "" {
+ port = p
+ }
+ return h, port
+ }
+ return host, port
+}
diff --git a/pkg/protocols/utils/fields_test.go b/pkg/protocols/utils/fields_test.go
new file mode 100644
index 0000000000..17cd96a2e7
--- /dev/null
+++ b/pkg/protocols/utils/fields_test.go
@@ -0,0 +1,138 @@
+package utils
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetJsonFieldsFromURL_HostPortExtraction(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ input string
+ expectedHost string
+ expectedPort string
+ }{
+ {
+ name: "URL with scheme and port",
+ input: "http://example.com:8080/path",
+ expectedHost: "example.com",
+ expectedPort: "8080",
+ },
+ {
+ name: "URL with scheme no port",
+ input: "https://example.com/path",
+ expectedHost: "example.com",
+ expectedPort: "443",
+ },
+ {
+ name: "host:port without scheme",
+ input: "example.com:8080",
+ expectedHost: "example.com",
+ expectedPort: "8080",
+ },
+ {
+ name: "host:port with standard HTTPS port",
+ input: "example.com:443",
+ expectedHost: "example.com",
+ expectedPort: "443",
+ },
+ {
+ name: "IPv4 with port",
+ input: "192.168.1.1:8080",
+ expectedHost: "192.168.1.1",
+ expectedPort: "8080",
+ },
+ {
+ name: "IPv6 with port",
+ input: "[2001:db8::1]:8080",
+ expectedHost: "2001:db8::1",
+ expectedPort: "8080",
+ },
+ {
+ name: "localhost with port",
+ input: "localhost:3000",
+ expectedHost: "localhost",
+ expectedPort: "3000",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ fields := GetJsonFieldsFromURL(tt.input)
+
+ assert.Equal(t, tt.expectedHost, fields.Host)
+ assert.Equal(t, tt.expectedPort, fields.Port)
+ })
+ }
+}
+
+func TestExtractHostPort(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ host string
+ port string
+ expectedHost string
+ expectedPort string
+ }{
+ {
+ name: "host without port",
+ host: "example.com",
+ port: "",
+ expectedHost: "example.com",
+ expectedPort: "",
+ },
+ {
+ name: "host with port",
+ host: "example.com:8080",
+ port: "",
+ expectedHost: "example.com",
+ expectedPort: "8080",
+ },
+ {
+ name: "port already set",
+ host: "example.com:8080",
+ port: "443",
+ expectedHost: "example.com",
+ expectedPort: "443",
+ },
+ {
+ name: "IPv6 with port",
+ host: "[::1]:8080",
+ port: "",
+ expectedHost: "::1",
+ expectedPort: "8080",
+ },
+ {
+ name: "IPv6 without port",
+ host: "[::1]",
+ port: "",
+ expectedHost: "::1",
+ expectedPort: "",
+ },
+ {
+ name: "IPv4 with port",
+ host: "192.168.1.1:8080",
+ port: "",
+ expectedHost: "192.168.1.1",
+ expectedPort: "8080",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ host, port := extractHostPort(tt.host, tt.port)
+
+ assert.Equal(t, tt.expectedHost, host)
+ assert.Equal(t, tt.expectedPort, port)
+ })
+ }
+}
diff --git a/pkg/templates/compile.go b/pkg/templates/compile.go
index 7060023161..fcbf2eb54a 100644
--- a/pkg/templates/compile.go
+++ b/pkg/templates/compile.go
@@ -580,6 +580,19 @@ func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Templat
// check if the template is verified
// only valid templates can be verified or signed
+ if options.TemplateVerificationCallback != nil && options.TemplatePath != "" {
+ if cached := options.TemplateVerificationCallback(options.TemplatePath); cached != nil {
+ template.Verified = cached.Verified
+ template.TemplateVerifier = cached.Verifier
+ options.TemplateVerifier = cached.Verifier
+ //nolint
+ if !(template.Verified && template.TemplateVerifier == "projectdiscovery/nuclei-templates") {
+ template.Options.RawTemplate = data
+ }
+ return template, nil
+ }
+ }
+
var verifier *signer.TemplateSigner
for _, verifier = range signer.DefaultTemplateVerifiers {
template.Verified, _ = verifier.Verify(data, template)
@@ -592,10 +605,12 @@ func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Templat
}
}
options.TemplateVerifier = template.TemplateVerifier
+
//nolint
if !(template.Verified && verifier.Identifier() == "projectdiscovery/nuclei-templates") {
template.Options.RawTemplate = data
}
+
return template, nil
}
diff --git a/pkg/templates/compile_test.go b/pkg/templates/compile_test.go
index 34c22b0f2d..81728d9c27 100644
--- a/pkg/templates/compile_test.go
+++ b/pkg/templates/compile_test.go
@@ -22,6 +22,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
+ "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/variables"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
@@ -184,6 +185,26 @@ func Test_ParseWorkflow(t *testing.T) {
require.Equal(t, len(expectedTemplate.Workflows), len(got.Workflows))
}
+func Test_ParseWorkflowWithGlobalMatchers(t *testing.T) {
+ setup()
+ previousGlobalMatchers := executerOpts.Options.EnableGlobalMatchersTemplates
+ executerOpts.Options.EnableGlobalMatchersTemplates = true
+ defer func() {
+ executerOpts.Options.EnableGlobalMatchersTemplates = previousGlobalMatchers
+ executerOpts.GlobalMatchers = nil
+ }()
+ executerOpts.GlobalMatchers = globalmatchers.New()
+
+ filePath := "tests/workflow-global-matchers.yaml"
+ got, err := templates.Parse(filePath, nil, executerOpts)
+ require.NoError(t, err, "could not parse workflow template")
+ require.NotNil(t, got, "workflow template should not be nil")
+ require.NotNil(t, got.CompiledWorkflow, "compiled workflow should not be nil")
+ require.Len(t, got.CompiledWorkflow.Workflows, 2)
+ require.Len(t, got.CompiledWorkflow.Workflows[0].Executers, 1)
+ require.Len(t, got.CompiledWorkflow.Workflows[1].Executers, 0)
+}
+
func Test_WrongTemplate(t *testing.T) {
setup()
diff --git a/pkg/templates/tests/global-matcher.yaml b/pkg/templates/tests/global-matcher.yaml
new file mode 100644
index 0000000000..34c6f30d00
--- /dev/null
+++ b/pkg/templates/tests/global-matcher.yaml
@@ -0,0 +1,17 @@
+id: global-matcher-test
+
+info:
+ name: Global Matcher Test Template
+ author: pdteam
+ severity: info
+
+http:
+ - method: GET
+ path:
+ - "{{BaseURL}}"
+ global-matchers: true
+ matchers:
+ - type: word
+ part: body
+ words:
+ - "test"
diff --git a/pkg/templates/tests/workflow-global-matchers.yaml b/pkg/templates/tests/workflow-global-matchers.yaml
new file mode 100644
index 0000000000..7ac7a04983
--- /dev/null
+++ b/pkg/templates/tests/workflow-global-matchers.yaml
@@ -0,0 +1,10 @@
+id: workflow-global-matchers
+
+info:
+ name: Workflow With Global Matchers
+ author: pdteam
+ severity: info
+
+workflows:
+ - template: tests/match-1.yaml
+ - template: tests/global-matcher.yaml
diff --git a/pkg/templates/workflows.go b/pkg/templates/workflows.go
index 771cdf4d9c..d50222a8a9 100644
--- a/pkg/templates/workflows.go
+++ b/pkg/templates/workflows.go
@@ -77,6 +77,9 @@ func parseWorkflowTemplate(workflow *workflows.WorkflowTemplate, preprocessor Pr
gologger.Warning().Msgf("Could not parse workflow template %s: %v\n", path, err)
continue
}
+ if template == nil {
+ continue
+ }
if template.Executer == nil {
gologger.Warning().Msgf("Could not parse workflow template %s: no executer found\n", path)
continue
diff --git a/pkg/types/types.go b/pkg/types/types.go
index 0f6663f384..43d818fb43 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -18,6 +18,7 @@ import (
fileutil "github.com/projectdiscovery/utils/file"
folderutil "github.com/projectdiscovery/utils/folder"
unitutils "github.com/projectdiscovery/utils/unit"
+ "github.com/rs/xid"
)
const DefaultTemplateLoadingConcurrency = 50
@@ -177,6 +178,8 @@ type Options struct {
ForceAttemptHTTP2 bool
// StatsJSON writes stats output in JSON format
StatsJSON bool
+ // CDPEndpoint specifies the endpoint for Chrome DevTools Protocol (CDP)
+ CDPEndpoint string
// Headless specifies whether to allow headless mode templates
Headless bool
// ShowBrowser specifies whether the show the browser in headless mode
@@ -793,6 +796,7 @@ func DefaultOptions() *Options {
MaxHostError: 30,
ResponseReadSize: 10 * unitutils.Mega,
ResponseSaveSize: unitutils.Mega,
+ ExecutionId: xid.New().String(),
Logger: &gologger.Logger{},
}
}
diff --git a/pkg/utils/telnetmini/doc.go b/pkg/utils/telnetmini/doc.go
new file mode 100644
index 0000000000..c63a495c8f
--- /dev/null
+++ b/pkg/utils/telnetmini/doc.go
@@ -0,0 +1,7 @@
+// Package telnetmini is a library for interacting with Telnet servers.
+// it supports
+// - Basic Authentication phase (username/password)
+// - Encryption detection via encryption negotiation packet
+// - Minimal porting of https://github.com/nmap/nmap/blob/master/nselib/smbauth.lua SMB via NTLM negotiations
+// (TNAP Login Packet + Raw NTLM response parsing)
+package telnetmini
diff --git a/pkg/utils/telnetmini/ntlm.go b/pkg/utils/telnetmini/ntlm.go
new file mode 100644
index 0000000000..b4105b52bc
--- /dev/null
+++ b/pkg/utils/telnetmini/ntlm.go
@@ -0,0 +1,247 @@
+package telnetmini
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+)
+
+// NTLMInfoResponse represents the response from NTLM information gathering
+// This matches exactly the output structure from the Nmap telnet-ntlm-info.nse script
+type NTLMInfoResponse struct {
+ TargetName string // Target_Name from script
+ NetBIOSDomainName string // NetBIOS_Domain_Name from script
+ NetBIOSComputerName string // NetBIOS_Computer_Name from script
+ DNSDomainName string // DNS_Domain_Name from script
+ DNSComputerName string // DNS_Computer_Name from script
+ DNSTreeName string // DNS_Tree_Name from script
+ ProductVersion string // Product_Version from script
+ Timestamp uint64 // Raw timestamp for skew calculation
+}
+
+// ParseNTLMResponse parses the NTLM response to extract system information
+// This implements the exact parsing logic from the Nmap telnet-ntlm-info.nse script
+func ParseNTLMResponse(data []byte) (*NTLMInfoResponse, error) {
+ // Continue only if NTLMSSP response is returned.
+ // Verify that the response is terminated with Sub-option End values as various
+ // non Microsoft telnet implementations support NTLM but do not return valid data.
+ // This matches the script's: local data = string.match(response, "(NTLMSSP.*)\xff\xf0")
+ ntlmStart := bytes.Index(data, []byte("NTLMSSP"))
+ if ntlmStart == -1 {
+ return nil, fmt.Errorf("NTLMSSP signature not found in response")
+ }
+
+ // Find the end of NTLM data (Sub-option End: 0xFF 0xF0)
+ ntlmEnd := bytes.Index(data[ntlmStart:], []byte{0xFF, 0xF0})
+ if ntlmEnd == -1 {
+ return nil, fmt.Errorf("NTLM response not properly terminated with Sub-option End")
+ }
+
+ // Extract NTLM data (NTLMSSP.*\xff\xf0)
+ ntlmData := data[ntlmStart : ntlmStart+ntlmEnd]
+
+ // Check message type (should be 2 for Challenge)
+ if len(ntlmData) < 12 {
+ return nil, fmt.Errorf("NTLM response too short")
+ }
+
+ messageType := binary.LittleEndian.Uint32(ntlmData[8:12])
+ if messageType != 2 {
+ return nil, fmt.Errorf("expected NTLM challenge message, got type %d", messageType)
+ }
+
+ // Parse target name fields
+ targetNameLen := binary.LittleEndian.Uint16(ntlmData[12:14])
+ targetNameOffset := binary.LittleEndian.Uint32(ntlmData[16:20])
+
+ // Parse target info fields
+ targetInfoLen := binary.LittleEndian.Uint16(ntlmData[40:42])
+ targetInfoOffset := binary.LittleEndian.Uint32(ntlmData[44:48])
+
+ // Extract target name (Target Name will always be returned under any implementation)
+ var targetName string
+ if targetNameLen > 0 && int(targetNameOffset) < len(ntlmData) {
+ end := int(targetNameOffset) + int(targetNameLen)
+ if end <= len(ntlmData) {
+ targetName = string(ntlmData[targetNameOffset:end])
+ }
+ }
+
+ // Extract target info (contains detailed system information)
+ var ntlmInfo NTLMInfoResponse
+ ntlmInfo.TargetName = targetName
+
+ // Parse target info structure if available
+ if targetInfoLen > 0 && int(targetInfoOffset) < len(ntlmData) {
+ end := int(targetInfoOffset) + int(targetInfoLen)
+ if end <= len(ntlmData) {
+ parseTargetInfo(ntlmData[targetInfoOffset:end], &ntlmInfo)
+ }
+ }
+
+ return &ntlmInfo, nil
+}
+
+// CalculateTimestampSkew calculates the time skew from NTLM timestamp
+// This implements the timestamp calculation from the Nmap script:
+// local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+func CalculateTimestampSkew(ntlmTimestamp uint64) int64 {
+ if ntlmTimestamp == 0 {
+ return 0
+ }
+
+ // Convert 100ns clicks since 1/1/1601 to Unix timestamp
+ // Formula: (ntlmTimestamp / 10000000) - 11644473600
+ unixTimestamp := int64(ntlmTimestamp/10000000) - 11644473600
+ return unixTimestamp
+}
+
+// parseTargetInfo parses the NTLM target info structure to extract system details
+func parseTargetInfo(data []byte, info *NTLMInfoResponse) {
+ // Target info is a series of type-length-value pairs
+ // Each entry starts with a 2-byte type and 2-byte length
+ for i := 0; i < len(data)-4; {
+ if i+4 > len(data) {
+ break
+ }
+
+ infoType := binary.LittleEndian.Uint16(data[i : i+2])
+ infoLen := binary.LittleEndian.Uint16(data[i+2 : i+4])
+
+ if i+4+int(infoLen) > len(data) {
+ break
+ }
+
+ infoData := data[i+4 : i+4+int(infoLen)]
+
+ switch infoType {
+ case 1: // NetBIOS Computer Name
+ // Display information returned & ignore responses with null values
+ if len(infoData) > 0 {
+ info.NetBIOSComputerName = string(infoData)
+ }
+ case 2: // NetBIOS Domain Name
+ if len(infoData) > 0 {
+ info.NetBIOSDomainName = string(infoData)
+ }
+ case 3: // DNS Computer Name (fqdn in script)
+ if len(infoData) > 0 {
+ info.DNSComputerName = string(infoData)
+ }
+ case 4: // DNS Domain Name
+ if len(infoData) > 0 {
+ info.DNSDomainName = string(infoData)
+ }
+ case 5: // DNS Tree Name (dns_forest_name in script)
+ if len(infoData) > 0 {
+ info.DNSTreeName = string(infoData)
+ }
+ case 6: // Timestamp - 64-bit number of 100ns clicks since 1/1/1601
+ if len(infoData) >= 8 {
+ info.Timestamp = binary.LittleEndian.Uint64(infoData)
+ }
+ case 7: // Single Host
+ // Skip single host
+ case 8: // Target Name (target_realm in script)
+ if len(infoData) > 0 {
+ info.TargetName = string(infoData)
+ }
+ case 9: // Channel Bindings
+ // Skip channel bindings
+ case 10: // Target Information
+ // Skip target information
+ case 11: // OS Version
+ if len(infoData) >= 8 {
+ major := uint8(infoData[0])
+ minor := uint8(infoData[1])
+ build := binary.LittleEndian.Uint16(infoData[2:4])
+ info.ProductVersion = fmt.Sprintf("%d.%d.%d", major, minor, build)
+ }
+ }
+
+ i += 4 + int(infoLen)
+ }
+}
+
+// CreateNTLMNegotiateBlob creates the NTLM negotiate blob with specific flags
+// This matches the flags used in the Nmap script
+func CreateNTLMNegotiateBlob() []byte {
+ var buf bytes.Buffer
+
+ // NTLMSSP signature
+ buf.WriteString("NTLMSSP")
+ buf.WriteByte(0x00)
+
+ // Message type (1 = Negotiate)
+ messageTypeBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(messageTypeBytes, 1)
+ buf.Write(messageTypeBytes)
+
+ // Negotiate flags (matching Nmap script exactly)
+ flags := uint32(0x00000001 + // Negotiate Unicode
+ 0x00000002 + // Negotiate OEM strings
+ 0x00000004 + // Request Target
+ 0x00000200 + // Negotiate NTLM
+ 0x00008000 + // Negotiate Always Sign
+ 0x00080000 + // Negotiate NTLM2 Key
+ 0x20000000 + // Negotiate 128
+ 0x80000000) // Negotiate 56
+ flagsBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(flagsBytes, flags)
+ buf.Write(flagsBytes)
+
+ // Domain name fields (empty for negotiate)
+ domainNameLenBytes := make([]byte, 2)
+ binary.LittleEndian.PutUint16(domainNameLenBytes, 0)
+ buf.Write(domainNameLenBytes)
+
+ domainNameMaxLenBytes := make([]byte, 2)
+ binary.LittleEndian.PutUint16(domainNameMaxLenBytes, 0)
+ buf.Write(domainNameMaxLenBytes)
+
+ domainNameOffsetBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(domainNameOffsetBytes, 0)
+ buf.Write(domainNameOffsetBytes)
+
+ // Workstation name fields (empty for negotiate)
+ workstationNameLenBytes := make([]byte, 2)
+ binary.LittleEndian.PutUint16(workstationNameLenBytes, 0)
+ buf.Write(workstationNameLenBytes)
+
+ workstationNameMaxLenBytes := make([]byte, 2)
+ binary.LittleEndian.PutUint16(workstationNameMaxLenBytes, 0)
+ buf.Write(workstationNameMaxLenBytes)
+
+ workstationNameOffsetBytes := make([]byte, 4)
+ binary.LittleEndian.PutUint32(workstationNameOffsetBytes, 0)
+ buf.Write(workstationNameOffsetBytes)
+
+ // Version (empty for negotiate)
+ buf.Write(make([]byte, 8))
+
+ return buf.Bytes()
+}
+
+// CreateTNAPLoginPacket creates the MS-TNAP Login Packet (Option Command IS)
+// This implements the exact packet structure from the Nmap script
+func CreateTNAPLoginPacket() []byte {
+ var buf bytes.Buffer
+
+ // TNAP Option Command IS (0x01)
+ buf.WriteByte(0x01)
+
+ // Length (will be updated later)
+ buf.WriteByte(0x00)
+
+ // NTLM authentication blob
+ ntlmBlob := CreateNTLMNegotiateBlob()
+
+ // Update length
+ data := buf.Bytes()
+ data[1] = byte(len(ntlmBlob)) // Length of the NTLM blob
+
+ // Append NTLM blob
+ buf.Write(ntlmBlob)
+
+ return buf.Bytes()
+}
diff --git a/pkg/utils/telnetmini/smb.go b/pkg/utils/telnetmini/smb.go
new file mode 100644
index 0000000000..12d150564f
--- /dev/null
+++ b/pkg/utils/telnetmini/smb.go
@@ -0,0 +1,781 @@
+package telnetmini
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+
+ "github.com/Azure/go-ntlmssp"
+)
+
+// SMB constants for packet crafting
+const (
+ // SMB Commands
+ SMB_COM_NEGOTIATE_PROTOCOL = 0x72
+ SMB_COM_SESSION_SETUP_ANDX = 0x73
+ SMB_COM_TREE_CONNECT_ANDX = 0x75
+ SMB_COM_NT_CREATE_ANDX = 0xA2
+ SMB_COM_READ_ANDX = 0x2E
+ SMB_COM_WRITE_ANDX = 0x2F
+ SMB_COM_CLOSE = 0x04
+ SMB_COM_TREE_DISCONNECT = 0x71
+ SMB_COM_LOGOFF_ANDX = 0x74
+
+ // SMB Flags
+ SMB_FLAGS_CANONICAL_PATHNAMES = 0x10
+ SMB_FLAGS_CASELESS_PATHNAMES = 0x08
+ SMB_FLAGS2_UNICODE_STRINGS = 0x8000
+ SMB_FLAGS2_ERRSTATUS = 0x4000
+ SMB_FLAGS2_READ_IF_EXECUTE = 0x2000
+ SMB_FLAGS2_32_BIT_ERRORS = 0x1000
+ SMB_FLAGS2_DFS = 0x0800
+ SMB_FLAGS2_EXTENDED_SECURITY = 0x0400
+ SMB_FLAGS2_REPARSE_PATH = 0x0200
+ SMB_FLAGS2_SMB_SECURITY_SIGNATURE = 0x0100
+ SMB_FLAGS2_SMB_SECURITY_SIGNATURE_REQUIRED = 0x0080
+
+ // SMB Security modes
+ SMB_SECURITY_SHARE = 0x00
+ SMB_SECURITY_USER = 0x01
+ SMB_SECURITY_DOMAIN = 0x02
+
+ // SMB Capabilities
+ SMB_CAP_EXTENDED_SECURITY = 0x80000000
+ SMB_CAP_COMPRESSED_DATA = 0x40000000
+ SMB_CAP_BULK_TRANSFER = 0x20000000
+ SMB_CAP_UNIX = 0x00800000
+ SMB_CAP_LARGE_READX = 0x00400000
+ SMB_CAP_LARGE_WRITEX = 0x00200000
+ SMB_CAP_INFOLEVEL_PASSTHRU = 0x00100000
+ SMB_CAP_DFS = 0x00080000
+ SMB_CAP_NT_FIND = 0x00040000
+ SMB_CAP_LOCK_AND_READ = 0x00020000
+ SMB_CAP_LEVEL_II_OPLOCKS = 0x00010000
+ SMB_CAP_STATUS32 = 0x00008000
+ SMB_CAP_RPC_REMOTE_APIS = 0x00004000
+ SMB_CAP_NT_SMBS = 0x00002000
+
+ // NTLM constants
+ NTLMSSP_NEGOTIATE_56 = 0x80000000
+ NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000
+ NTLMSSP_NEGOTIATE_128 = 0x20000000
+ NTLMSSP_NEGOTIATE_VERSION = 0x02000000
+ NTLMSSP_NEGOTIATE_TARGET_INFO = 0x00800000
+ NTLMSSP_REQUEST_NON_NT_SESSION_KEY = 0x00400000
+ NTLMSSP_NEGOTIATE_IDENTIFY = 0x00100000
+ NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY = 0x00080000
+ NTLMSSP_TARGET_TYPE_SERVER = 0x00020000
+ NTLMSSP_NEGOTIATE_ALWAYS_SIGN = 0x00008000
+ NTLMSSP_NEGOTIATE_NTLM = 0x00000200
+ NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080
+ NTLMSSP_NEGOTIATE_DATAGRAM = 0x00000040
+ NTLMSSP_NEGOTIATE_SEAL = 0x00000020
+ NTLMSSP_NEGOTIATE_SIGN = 0x00000010
+ NTLMSSP_REQUEST_TARGET = 0x00000004
+ NTLMSSP_NEGOTIATE_UNICODE = 0x00000001
+)
+
+// SMBPacket represents a complete SMB packet
+type SMBPacket struct {
+ NetBIOSHeader []byte
+ SMBHeader []byte
+ SMBData []byte
+}
+
+// SMBHeader represents the SMB header structure
+type SMBHeader struct {
+ ProtocolID [4]byte // 0xFF, 'S', 'M', 'B'
+ Command byte
+ Status uint32
+ Flags byte
+ Flags2 uint16
+ PIDHigh uint16
+ Signature [8]byte
+ Reserved uint16
+ TreeID uint16
+ ProcessID uint16
+ UserID uint16
+ MultiplexID uint16
+}
+
+// CreateSMBPacket creates a complete SMB packet with NetBIOS header
+func CreateSMBPacket(smbData []byte) *SMBPacket {
+ // Create NetBIOS header
+ netbiosHeader := make([]byte, 4)
+ netbiosHeader[0] = 0x00 // Message type (Session message)
+ netbiosHeader[1] = 0x00 // Padding
+ netbiosHeader[2] = 0x00 // Padding
+
+ // Calculate NetBIOS length (big-endian)
+ length := len(smbData)
+ netbiosHeader[3] = byte(length & 0xFF)
+
+ return &SMBPacket{
+ NetBIOSHeader: netbiosHeader,
+ SMBHeader: createSMBHeader(),
+ SMBData: smbData,
+ }
+}
+
+// createSMBHeader creates a standard SMB header
+func createSMBHeader() []byte {
+ header := make([]byte, 32)
+
+ // Protocol ID: 0xFF, 'S', 'M', 'B'
+ header[0] = 0xFF
+ header[1] = 'S'
+ header[2] = 'M'
+ header[3] = 'B'
+
+ // Command (will be set by caller)
+ header[4] = 0x00
+
+ // Status (0 for requests)
+ binary.LittleEndian.PutUint32(header[5:9], 0)
+
+ // Flags
+ header[9] = SMB_FLAGS_CANONICAL_PATHNAMES
+
+ // Flags2
+ binary.LittleEndian.PutUint16(header[10:12], SMB_FLAGS2_UNICODE_STRINGS|SMB_FLAGS2_EXTENDED_SECURITY)
+
+ // PIDHigh, Signature, Reserved, TreeID, ProcessID, UserID, MultiplexID
+ // All set to 0 for new connections
+ binary.LittleEndian.PutUint16(header[12:14], 0) // PIDHigh
+ // Signature is 8 bytes of zeros
+ binary.LittleEndian.PutUint16(header[20:22], 0) // Reserved
+ binary.LittleEndian.PutUint16(header[22:24], 0) // TreeID
+ binary.LittleEndian.PutUint16(header[24:26], 0) // ProcessID
+ binary.LittleEndian.PutUint16(header[26:28], 0) // UserID
+ binary.LittleEndian.PutUint16(header[28:30], 0) // MultiplexID
+
+ return header
+}
+
+// CreateNegotiateProtocolPacket creates an SMB negotiate protocol packet
+func CreateNegotiateProtocolPacket() []byte {
+ // Create SMB header
+ header := createSMBHeader()
+ header[4] = SMB_COM_NEGOTIATE_PROTOCOL
+
+ // Create negotiate protocol data
+ data := createNegotiateProtocolData()
+
+ // Combine header and data
+ packet := append(header, data...)
+
+ // Create complete packet with NetBIOS header
+ smbPacket := CreateSMBPacket(packet)
+
+ return smbPacket.Bytes()
+}
+
+// createNegotiateProtocolData creates the data portion of negotiate protocol packet
+func createNegotiateProtocolData() []byte {
+ var buf bytes.Buffer
+
+ // Word count
+ _ = buf.WriteByte(0x00)
+
+ // Byte count
+ _ = buf.WriteByte(0x00)
+
+ // Dialect strings
+ dialects := []string{
+ "NT LM 0.12",
+ "SMB 2.002",
+ "SMB 2.???",
+ }
+
+ for _, dialect := range dialects {
+ _ = buf.WriteByte(byte(len(dialect)))
+ _, _ = buf.WriteString(dialect)
+ _ = buf.WriteByte(0x00)
+ }
+
+ // Update byte count
+ data := buf.Bytes()
+ data[1] = byte(len(data) - 2)
+
+ return data
+}
+
+// CreateSessionSetupPacket creates an SMB session setup packet
+func CreateSessionSetupPacket(username, password, domain string, sessionKey uint64) []byte {
+ // Create SMB header
+ header := createSMBHeader()
+ header[4] = SMB_COM_SESSION_SETUP_ANDX
+
+ // Create session setup data
+ data := createSessionSetupData(username, password, domain, sessionKey)
+
+ // Combine header and data
+ packet := append(header, data...)
+
+ // Create complete packet with NetBIOS header
+ smbPacket := CreateSMBPacket(packet)
+
+ return smbPacket.Bytes()
+}
+
+// createSessionSetupData creates the data portion of session setup packet
+func createSessionSetupData(username, password, domain string, sessionKey uint64) []byte {
+ var buf bytes.Buffer
+
+ // Word count
+ _ = buf.WriteByte(0x0D)
+
+ // AndXCommand (no chained command)
+ _ = buf.WriteByte(0xFF)
+
+ // AndXReserved
+ _ = buf.WriteByte(0x00)
+
+ // AndXOffset
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+
+ // MaxBufferSize
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0xFFFF))
+
+ // MaxMpxCount
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x01))
+
+ // VcNumber
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x00))
+
+ // SessionKey
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(sessionKey))
+
+ // CaseInsensitivePasswordLength
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(password)))
+
+ // CaseSensitivePasswordLength
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(password)))
+
+ // Reserved
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00))
+
+ // Capabilities
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(SMB_CAP_EXTENDED_SECURITY|SMB_CAP_NT_SMBS))
+
+ // Byte count
+ _ = buf.WriteByte(0x00)
+
+ // CaseInsensitivePassword
+ _, _ = buf.WriteString(password)
+
+ // CaseSensitivePassword
+ _, _ = buf.WriteString(password)
+
+ // Account name
+ _, _ = buf.WriteString(username)
+ _ = buf.WriteByte(0x00)
+
+ // Primary domain
+ _, _ = buf.WriteString(domain)
+ _ = buf.WriteByte(0x00)
+
+ // Native OS
+ _, _ = buf.WriteString("Windows 2000 2195")
+ _ = buf.WriteByte(0x00)
+
+ // Native LAN Manager
+ _, _ = buf.WriteString("Windows 2000 5.0")
+ _ = buf.WriteByte(0x00)
+
+ // Update byte count
+ data := buf.Bytes()
+ data[len(data)-1] = byte(len(data) - 0x21) // 0x21 is the offset to the start of variable data
+
+ return data
+}
+
+// CreateTreeConnectPacket creates an SMB tree connect packet
+func CreateTreeConnectPacket(shareName string, password string) []byte {
+ // Create SMB header
+ header := createSMBHeader()
+ header[4] = SMB_COM_TREE_CONNECT_ANDX
+
+ // Create tree connect data
+ data := createTreeConnectData(shareName, password)
+
+ // Combine header and data
+ packet := append(header, data...)
+
+ // Create complete packet with NetBIOS header
+ smbPacket := CreateSMBPacket(packet)
+
+ return smbPacket.Bytes()
+}
+
+// createTreeConnectData creates the data portion of tree connect packet
+func createTreeConnectData(shareName, password string) []byte {
+ var buf bytes.Buffer
+
+ // Word count
+ _ = buf.WriteByte(0x04)
+
+ // AndXCommand (no chained command)
+ _ = buf.WriteByte(0xFF)
+
+ // AndXReserved
+ _ = buf.WriteByte(0x00)
+
+ // AndXOffset
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+
+ // Flags
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x00))
+
+ // Password length
+ _ = buf.WriteByte(byte(len(password)))
+
+ // Byte count
+ _ = buf.WriteByte(0x00)
+
+ // Password
+ _, _ = buf.WriteString(password)
+ _ = buf.WriteByte(0x00)
+
+ // Tree
+ _, _ = buf.WriteString(shareName)
+ _ = buf.WriteByte(0x00)
+
+ // Service
+ _, _ = buf.WriteString("?????")
+ _ = buf.WriteByte(0x00)
+
+ // Update byte count
+ data := buf.Bytes()
+ data[7] = byte(len(data) - 0x0B) // 0x0B is the offset to the start of variable data
+
+ return data
+}
+
+// CreateNTCreatePacket creates an SMB NT create packet
+func CreateNTCreatePacket(fileName string) []byte {
+ // Create SMB header
+ header := createSMBHeader()
+ header[4] = SMB_COM_NT_CREATE_ANDX
+
+ // Create NT create data
+ data := createNTCreateData(fileName)
+
+ // Combine header and data
+ packet := append(header, data...)
+
+ // Create complete packet with NetBIOS header
+ smbPacket := CreateSMBPacket(packet)
+
+ return smbPacket.Bytes()
+}
+
+// createNTCreateData creates the data portion of NT create packet
+func createNTCreateData(fileName string) []byte {
+ var buf bytes.Buffer
+
+ // Word count
+ _ = buf.WriteByte(0x18)
+
+ // AndXCommand (no chained command)
+ _ = buf.WriteByte(0xFF)
+
+ // AndXReserved
+ _ = buf.WriteByte(0x00)
+
+ // AndXOffset
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+
+ // Reserved
+ _ = buf.WriteByte(0x00)
+
+ // NameLength
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(fileName)))
+
+ // Flags
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // RootDirectoryFID
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // DesiredAccess
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // AllocationSize
+ _ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000000))
+
+ // FileAttributes
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // ShareAccess
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // CreateDisposition
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000001)) // FILE_OPEN
+
+ // CreateOptions
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // ImpersonationLevel
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000002)) // SecurityImpersonation
+
+ // SecurityFlags
+ _ = buf.WriteByte(0x00)
+
+ // Byte count
+ _ = buf.WriteByte(0x00)
+
+ // SecurityDescriptor
+ _ = buf.WriteByte(0x00)
+
+ // FileName
+ _, _ = buf.WriteString(fileName)
+ _ = buf.WriteByte(0x00)
+
+ // Update byte count
+ data := buf.Bytes()
+ data[0x3B] = byte(len(data) - 0x3C) // 0x3C is the offset to the start of variable data
+
+ return data
+}
+
+// Bytes returns the complete SMB packet as bytes
+func (p *SMBPacket) Bytes() []byte {
+ var result bytes.Buffer
+ result.Write(p.NetBIOSHeader)
+ result.Write(p.SMBHeader)
+ result.Write(p.SMBData)
+ return result.Bytes()
+}
+
+// CreateNTLMNegotiatePacket creates an NTLM negotiate packet for SMB authentication
+func CreateNTLMNegotiatePacket() []byte {
+ var buf bytes.Buffer
+
+ // NTLMSSP signature
+ _, _ = buf.WriteString("NTLMSSP")
+ _ = buf.WriteByte(0x00)
+
+ // Message type (1 = Negotiate)
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(1))
+
+ // Negotiate flags
+ flags := uint32(NTLMSSP_NEGOTIATE_56 |
+ NTLMSSP_NEGOTIATE_128 |
+ NTLMSSP_NEGOTIATE_VERSION |
+ NTLMSSP_NEGOTIATE_TARGET_INFO |
+ NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY |
+ NTLMSSP_TARGET_TYPE_SERVER |
+ NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
+ NTLMSSP_NEGOTIATE_NTLM |
+ NTLMSSP_NEGOTIATE_UNICODE)
+ _ = binary.Write(&buf, binary.LittleEndian, flags)
+
+ // Domain name fields (empty for negotiate)
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // DomainNameLen
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // DomainNameMaxLen
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0)) // DomainNameBufferOffset
+
+ // Workstation name fields (empty for negotiate)
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // WorkstationNameLen
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0)) // WorkstationNameMaxLen
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0)) // WorkstationNameBufferOffset
+
+ // Version
+ _ = buf.WriteByte(0x05) // Major version
+ _ = buf.WriteByte(0x02) // Minor version
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0A28)) // Build number
+ _, _ = buf.Write(make([]byte, 3)) // Reserved
+ _ = buf.WriteByte(0x0F) // NTLM revision
+
+ return buf.Bytes()
+}
+
+// CreateNTLMChallengePacket creates an NTLM challenge packet (for testing)
+func CreateNTLMChallengePacket(challenge []byte, targetInfo []byte) []byte {
+ var buf bytes.Buffer
+
+ // NTLMSSP signature
+ _, _ = buf.WriteString("NTLMSSP")
+ _ = buf.WriteByte(0x00)
+
+ // Message type (2 = Challenge)
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(2))
+
+ // Target name fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(56)) // TargetNameBufferOffset
+
+ // Negotiate flags
+ flags := uint32(NTLMSSP_NEGOTIATE_56 |
+ NTLMSSP_NEGOTIATE_128 |
+ NTLMSSP_NEGOTIATE_VERSION |
+ NTLMSSP_NEGOTIATE_TARGET_INFO |
+ NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY |
+ NTLMSSP_TARGET_TYPE_SERVER |
+ NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
+ NTLMSSP_NEGOTIATE_NTLM |
+ NTLMSSP_NEGOTIATE_UNICODE)
+ _ = binary.Write(&buf, binary.LittleEndian, flags)
+
+ // Challenge
+ _, _ = buf.Write(challenge)
+
+ // Reserved
+ _, _ = buf.Write(make([]byte, 8))
+
+ // Target info fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(targetInfo)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(targetInfo)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(56)) // TargetInfoBufferOffset
+
+ // Version
+ _ = buf.WriteByte(0x05) // Major version
+ _ = buf.WriteByte(0x02) // Minor version
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0A28)) // Build number
+ _, _ = buf.Write(make([]byte, 3)) // Reserved
+ _ = buf.WriteByte(0x0F) // NTLM revision
+
+ // Target info
+ _, _ = buf.Write(targetInfo)
+
+ return buf.Bytes()
+}
+
+// CreateNTLMAuthPacket creates an NTLM authenticate packet
+func CreateNTLMAuthPacket(username, password, domain, workstation string, challenge []byte, lmResponse, ntResponse []byte) []byte {
+ var buf bytes.Buffer
+
+ // NTLMSSP signature
+ _, _ = buf.WriteString("NTLMSSP")
+ _ = buf.WriteByte(0x00)
+
+ // Message type (3 = Authenticate)
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(3))
+
+ // Calculate offsets
+ baseOffset := uint32(72) // Header size (assuming Version is present)
+
+ lmOffset := baseOffset
+ ntOffset := lmOffset + uint32(len(lmResponse))
+ domainOffset := ntOffset + uint32(len(ntResponse))
+ userOffset := domainOffset + uint32(len(domain))
+ workOffset := userOffset + uint32(len(username))
+ sessionKeyOffset := workOffset + uint32(len(workstation))
+
+ // LM response fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(lmResponse)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(lmResponse)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(lmOffset))
+
+ // NT response fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(ntResponse)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(ntResponse)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(ntOffset))
+
+ // Domain name fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(domain)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(domain)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(domainOffset))
+
+ // Username fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(username)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(username)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(userOffset))
+
+ // Workstation name fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(workstation)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(len(workstation)))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(workOffset))
+
+ // Encrypted random session key fields
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0))
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(sessionKeyOffset))
+
+ // Negotiate flags
+ flags := uint32(NTLMSSP_NEGOTIATE_56 |
+ NTLMSSP_NEGOTIATE_128 |
+ NTLMSSP_NEGOTIATE_VERSION |
+ NTLMSSP_NEGOTIATE_TARGET_INFO |
+ NTLMSSP_NEGOTIATE_EXTENDED_SESSION_SECURITY |
+ NTLMSSP_TARGET_TYPE_SERVER |
+ NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
+ NTLMSSP_NEGOTIATE_NTLM |
+ NTLMSSP_NEGOTIATE_UNICODE)
+ _ = binary.Write(&buf, binary.LittleEndian, flags)
+
+ // Version
+ _ = buf.WriteByte(0x05) // Major version
+ _ = buf.WriteByte(0x02) // Minor version
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0A28)) // Build number
+ _, _ = buf.Write(make([]byte, 3)) // Reserved
+ _ = buf.WriteByte(0x0F) // NTLM revision
+
+ // LM response
+ _, _ = buf.Write(lmResponse)
+
+ // NT response
+ _, _ = buf.Write(ntResponse)
+
+ // Domain name
+ _, _ = buf.WriteString(domain)
+
+ // Username
+ _, _ = buf.WriteString(username)
+
+ // Workstation name
+ _, _ = buf.WriteString(workstation)
+
+ return buf.Bytes()
+}
+
+// Helper function to create LM hash response
+func CreateLMResponse(challenge []byte, password string) []byte {
+ // Create a minimal Type 2 challenge packet to satisfy ntlmssp
+ type2 := CreateNTLMChallengePacket(challenge, []byte{})
+
+ // Generate Type 3 authenticate packet
+ // We use empty username/domain as we only need the hash response
+ authMsg, err := ntlmssp.NewAuthenticateMessage(type2, "", password, nil)
+ if err != nil {
+ return nil
+ }
+
+ // Parse the response to extract LM response
+ // LM Response Len is at offset 12 (2 bytes)
+ // LM Response Offset is at offset 16 (4 bytes)
+ if len(authMsg) < 20 {
+ return nil
+ }
+
+ lmLen := binary.LittleEndian.Uint16(authMsg[12:14])
+ lmOffset := binary.LittleEndian.Uint32(authMsg[16:20])
+
+ if int(lmOffset)+int(lmLen) > len(authMsg) {
+ return nil
+ }
+
+ return authMsg[lmOffset : lmOffset+uint32(lmLen)]
+}
+
+// Helper function to create NT hash response
+func CreateNTResponse(challenge []byte, password string) []byte {
+ // Create a minimal Type 2 challenge packet to satisfy ntlmssp
+ type2 := CreateNTLMChallengePacket(challenge, []byte{})
+
+ // Generate Type 3 authenticate packet
+ authMsg, err := ntlmssp.NewAuthenticateMessage(type2, "", password, nil)
+ if err != nil {
+ return nil
+ }
+
+ // Parse the response to extract NT response
+ // NT Response Len is at offset 20 (2 bytes)
+ // NT Response Offset is at offset 24 (4 bytes)
+ if len(authMsg) < 28 {
+ return nil
+ }
+
+ ntLen := binary.LittleEndian.Uint16(authMsg[20:22])
+ ntOffset := binary.LittleEndian.Uint32(authMsg[24:28])
+
+ if int(ntOffset)+int(ntLen) > len(authMsg) {
+ return nil
+ }
+
+ return authMsg[ntOffset : ntOffset+uint32(ntLen)]
+}
+
+// CreateSMBv2NegotiatePacket creates an SMBv2 negotiate protocol packet
+func CreateSMBv2NegotiatePacket() []byte {
+ var buf bytes.Buffer
+
+ // SMB2 header
+ _ = buf.WriteByte(0xFE) // Protocol ID
+ _, _ = buf.WriteString("SMB")
+ _ = buf.WriteByte(0x00) // Protocol ID
+
+ // Command (Negotiate Protocol)
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0000))
+
+ // Status
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // Flags
+ _ = buf.WriteByte(0x00)
+
+ // Next command
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0000))
+
+ // Message ID
+ _ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000001))
+
+ // Reserved
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // Tree ID
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000000))
+
+ // Session ID
+ _ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000000))
+
+ // Signature
+ _, _ = buf.Write(make([]byte, 16))
+
+ // Structure size
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x24))
+
+ // Dialect count
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0001))
+
+ // Security mode
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0001))
+
+ // Reserved
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0000))
+
+ // Capabilities
+ _ = binary.Write(&buf, binary.LittleEndian, uint32(0x00000001))
+
+ // Client GUID
+ _, _ = buf.Write(make([]byte, 16))
+
+ // Client start time
+ _ = binary.Write(&buf, binary.LittleEndian, uint64(0x0000000000000000))
+
+ // Dialects
+ _ = binary.Write(&buf, binary.LittleEndian, uint16(0x0311)) // SMB 3.1.1
+
+ return buf.Bytes()
+}
+
+// ParseSMBResponse parses an SMB response packet
+func ParseSMBResponse(data []byte) (*SMBHeader, error) {
+ if len(data) < 32 {
+ return nil, fmt.Errorf("SMB response too short: %d bytes", len(data))
+ }
+
+ header := &SMBHeader{}
+
+ // Check protocol ID
+ if data[0] != 0xFF || data[1] != 'S' || data[2] != 'M' || data[3] != 'B' {
+ return nil, fmt.Errorf("invalid SMB protocol ID")
+ }
+
+ // Parse header fields
+ header.Command = data[4]
+ header.Status = binary.LittleEndian.Uint32(data[5:9])
+ header.Flags = data[9]
+ header.Flags2 = binary.LittleEndian.Uint16(data[10:12])
+ header.PIDHigh = binary.LittleEndian.Uint16(data[12:14])
+ copy(header.Signature[:], data[14:22])
+ header.Reserved = binary.LittleEndian.Uint16(data[22:24])
+ header.TreeID = binary.LittleEndian.Uint16(data[24:26])
+ header.ProcessID = binary.LittleEndian.Uint16(data[26:28])
+ header.UserID = binary.LittleEndian.Uint16(data[28:30])
+ header.MultiplexID = binary.LittleEndian.Uint16(data[30:32])
+
+ return header, nil
+}
diff --git a/pkg/utils/telnetmini/telnet.go b/pkg/utils/telnetmini/telnet.go
new file mode 100644
index 0000000000..6c65cdb610
--- /dev/null
+++ b/pkg/utils/telnetmini/telnet.go
@@ -0,0 +1,372 @@
+// telnetmini.go
+// Minimal Telnet helper for authentication + simple I/O over an existing or new connection.
+
+package telnetmini
+
+import (
+ "bufio"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "strings"
+ "time"
+)
+
+// Telnet protocol constants
+const (
+ IAC = 255 // Interpret As Command
+ WILL = 251 // Will
+ WONT = 252 // Won't
+ DO = 253 // Do
+ DONT = 254 // Don't
+ SB = 250 // Subnegotiation Begin
+ SE = 240 // Subnegotiation End
+ ENCRYPT = 38 // Encryption option (0x26)
+)
+
+// EncryptionInfo contains information about telnet encryption support
+type EncryptionInfo struct {
+ SupportsEncryption bool
+ Banner string
+ Options map[int][]int
+}
+
+// Client wraps a Telnet connection with tiny helpers.
+type Client struct {
+ Conn net.Conn
+ rd *bufio.Reader
+ wr *bufio.Writer
+ LoginPrompts []string // matched case-insensitively
+ UserPrompts []string // alternative to LoginPrompts; if empty, LoginPrompts used for username step
+ PasswordPrompts []string
+ FailBanners []string // e.g., "login incorrect", "authentication failed"
+ ShellPrompts []string // e.g., "$ ", "# ", "> "
+ ReadCapBytes int // safety cap while scanning (default 64 KiB)
+}
+
+// Defaults sets reasonable prompt patterns if none provided.
+func (c *Client) Defaults() {
+ if c.ReadCapBytes == 0 {
+ c.ReadCapBytes = 64 * 1024
+ }
+ if len(c.LoginPrompts) == 0 {
+ c.LoginPrompts = []string{"login:", "username:"}
+ }
+ if len(c.PasswordPrompts) == 0 {
+ c.PasswordPrompts = []string{"password:"}
+ }
+ if len(c.FailBanners) == 0 {
+ c.FailBanners = []string{"login incorrect", "authentication failed", "login failed"}
+ }
+ if len(c.ShellPrompts) == 0 {
+ c.ShellPrompts = []string{"$ ", "# ", "> "}
+ }
+ if len(c.UserPrompts) == 0 {
+ c.UserPrompts = c.LoginPrompts
+ }
+}
+
+// New wraps an existing net.Conn.
+func New(conn net.Conn) *Client {
+ c := &Client{
+ Conn: conn,
+ rd: bufio.NewReader(conn),
+ wr: bufio.NewWriter(conn),
+ }
+ c.Defaults()
+ return c
+}
+
+// Close closes the underlying connection.
+func (c *Client) Close() error {
+ return c.Conn.Close()
+}
+
+// DetectEncryption detects if a telnet server supports encryption.
+// Based on Nmap's telnet-encryption.nse script functionality.
+// WARNING: The connection becomes unusable after calling this function
+// due to the encryption negotiation packets sent.
+func DetectEncryption(conn net.Conn, timeout time.Duration) (*EncryptionInfo, error) {
+ if timeout == 0 {
+ timeout = 7 * time.Second
+ }
+
+ // Set connection timeout
+ _ = conn.SetDeadline(time.Now().Add(timeout))
+
+ // Send encryption negotiation packet (based on Nmap script)
+ // FF FD 26 FF FB 26 = IAC DO ENCRYPT IAC WILL ENCRYPT
+ encryptionPacket := []byte{IAC, DO, ENCRYPT, IAC, WILL, ENCRYPT}
+ _, err := conn.Write(encryptionPacket)
+ if err != nil {
+ return nil, fmt.Errorf("failed to send encryption packet: %w", err)
+ }
+
+ // Process server responses
+ options := make(map[int][]int)
+ supportsEncryption := false
+ banner := ""
+
+ // Read responses until we get encryption info or timeout
+ for {
+ _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
+ buffer := make([]byte, 1024)
+ n, err := conn.Read(buffer)
+ if err != nil {
+ // Timeout or connection closed, break
+ break
+ }
+
+ if n > 0 {
+ data := buffer[:n]
+ // Check if this contains banner text (non-IAC bytes)
+ for _, b := range data {
+ if b != IAC {
+ banner += string(b)
+ }
+ }
+
+ // Process telnet options
+ encrypted, opts := processTelnetOptions(data)
+ if encrypted {
+ supportsEncryption = true
+ }
+
+ // Merge options
+ for opt, cmds := range opts {
+ if options[opt] == nil {
+ options[opt] = make([]int, 0)
+ }
+ options[opt] = append(options[opt], cmds...)
+ }
+
+ // Check if we have encryption info
+ if cmds, exists := options[ENCRYPT]; exists {
+ for _, cmd := range cmds {
+ if cmd == WILL || cmd == DO {
+ supportsEncryption = true
+ break
+ }
+ }
+ }
+ }
+ }
+
+ return &EncryptionInfo{
+ SupportsEncryption: supportsEncryption,
+ Banner: banner,
+ Options: options,
+ }, nil
+}
+
+// processTelnetOptions processes telnet protocol options and returns encryption support status
+func processTelnetOptions(data []byte) (bool, map[int][]int) {
+ options := make(map[int][]int)
+ supportsEncryption := false
+
+ for i := 0; i < len(data); i++ {
+ if data[i] == IAC && i+2 < len(data) {
+ cmd := data[i+1]
+ option := data[i+2]
+
+ // Initialize option slice if not exists
+ optInt := int(option)
+ if options[optInt] == nil {
+ options[optInt] = make([]int, 0)
+ }
+ options[optInt] = append(options[optInt], int(cmd))
+
+ // Check for encryption support
+ if option == ENCRYPT && (cmd == WILL || cmd == DO) {
+ supportsEncryption = true
+ }
+
+ // Handle subnegotiation
+ if cmd == SB {
+ // Skip until SE
+ for j := i + 3; j < len(data); j++ {
+ if data[j] == IAC && j+1 < len(data) && data[j+1] == SE {
+ i = j + 1
+ break
+ }
+ }
+ } else {
+ i += 2 // Skip command and option
+ }
+ }
+ }
+
+ return supportsEncryption, options
+}
+
+// Auth performs a minimal Telnet username/password interaction.
+// It waits for a username/login prompt, sends username, waits for a password prompt,
+// sends password, and then looks for fail banners or shell prompts.
+// A timeout should be enforced via ctx.
+func (c *Client) Auth(ctx context.Context, username, password string) error {
+ // Wait for username/login prompt
+ if _, _, err := c.readUntil(ctx, c.UserPrompts...); err != nil {
+ return fmt.Errorf("waiting for login/username prompt: %w", err)
+ }
+ if err := c.writeLine(ctx, username); err != nil {
+ return fmt.Errorf("sending username: %w", err)
+ }
+
+ // Wait for password prompt
+ if _, _, err := c.readUntil(ctx, c.PasswordPrompts...); err != nil {
+ return fmt.Errorf("waiting for password prompt: %w", err)
+ }
+ if err := c.writeLine(ctx, password); err != nil {
+ return fmt.Errorf("sending password: %w", err)
+ }
+
+ // Post-auth: look quickly for explicit failure, else accept shell prompt / silence.
+ match, got, err := c.readUntil(ctx,
+ append(append([]string{}, c.FailBanners...), c.ShellPrompts...)...,
+ )
+ if err != nil && !errors.Is(err, context.DeadlineExceeded) {
+ return fmt.Errorf("post-auth read: %s (got: %s)", preview(got, 200), err)
+ }
+ low := strings.ToLower(match)
+ for _, fb := range c.FailBanners {
+ if low == strings.ToLower(fb) {
+ return errors.New("authentication failed")
+ }
+ }
+ // success (matched a shell prompt or timed out without explicit failure)
+ return nil
+}
+
+// Exec sends a command followed by CRLF and returns text captured until one of
+// the provided prompts appears (typically your shell prompt). Provide a deadline via ctx.
+func (c *Client) Exec(ctx context.Context, command string, until ...string) (string, error) {
+ if err := c.writeLine(ctx, command); err != nil {
+ return "", err
+ }
+ _, out, err := c.readUntil(ctx, until...)
+ return out, err
+}
+
+// --- internals ---
+
+// writeLine writes s + CRLF and flushes.
+func (c *Client) writeLine(ctx context.Context, s string) error {
+ c.setDeadlineFromCtx(ctx, true)
+ if _, err := io.WriteString(c.wr, s+"\r\n"); err != nil {
+ return err
+ }
+ return c.wr.Flush()
+}
+
+// readUntil scans bytes, handles minimal Telnet IAC negotiation, and returns when any needle appears.
+func (c *Client) readUntil(ctx context.Context, needles ...string) (matched string, bufStr string, err error) {
+ if len(needles) == 0 {
+ return "", "", errors.New("readUntil: no needles provided")
+ }
+ c.setDeadlineFromCtx(ctx, false)
+
+ lowNeedles := make([]string, len(needles))
+ for i, n := range needles {
+ lowNeedles[i] = strings.ToLower(n)
+ }
+
+ var b strings.Builder
+ tmp := make([]byte, 1)
+
+ // Maximum iteration counter to prevent infinite loops
+ maxIterations := 20
+ iterationCount := 0
+
+ for {
+ iterationCount++
+ // if we have iterated more than maxIterations, return
+ if iterationCount > maxIterations {
+ return "", b.String(), nil
+ }
+ // honor context deadline on every read
+ c.setDeadlineFromCtx(ctx, false)
+ _, err := c.rd.Read(tmp)
+ if err != nil {
+ if ne, ok := err.(net.Error); ok && ne.Timeout() {
+ return "", b.String(), context.DeadlineExceeded
+ }
+ return "", b.String(), err
+ }
+
+ // Telnet IAC (Interpret As Command)
+ if tmp[0] == 255 { // IAC
+ cmd, err := c.rd.ReadByte()
+ if err != nil {
+ return "", b.String(), err
+ }
+ switch cmd {
+ case 251, 252, 253, 254: // WILL, WONT, DO, DONT
+ opt, err := c.rd.ReadByte()
+ if err != nil {
+ return "", b.String(), err
+ }
+ // Politely refuse everything: DONT to WILL; WONT to DO.
+ var reply []byte
+ if cmd == 251 { // WILL
+ reply = []byte{255, 254, opt} // DONT
+ }
+ if cmd == 253 { // DO
+ reply = []byte{255, 252, opt} // WONT
+ }
+ if len(reply) > 0 {
+ c.setDeadlineFromCtx(ctx, true)
+ _, _ = c.wr.Write(reply)
+ _ = c.wr.Flush()
+ }
+ case 250: // SB (subnegotiation): skip until SE
+ for {
+ bb, err := c.rd.ReadByte()
+ if err != nil {
+ return "", b.String(), err
+ }
+ if bb == 255 {
+ if se, err := c.rd.ReadByte(); err == nil && se == 240 { // SE
+ break
+ }
+ }
+ }
+ default:
+ // NOP for other commands (IAC NOP, GA, etc.)
+ }
+ continue
+ }
+
+ // regular data byte
+ b.WriteByte(tmp[0])
+ lower := strings.ToLower(b.String())
+ for i, n := range lowNeedles {
+ if strings.Contains(lower, n) {
+ return needles[i], b.String(), nil
+ }
+ }
+ if b.Len() > c.ReadCapBytes {
+ return "", b.String(), errors.New("prompt not found (read cap reached)")
+ }
+ }
+}
+
+func (c *Client) setDeadlineFromCtx(ctx context.Context, write bool) {
+ if ctx == nil {
+ return
+ }
+ if dl, ok := ctx.Deadline(); ok {
+ _ = c.Conn.SetReadDeadline(dl)
+ if write {
+ _ = c.Conn.SetWriteDeadline(dl)
+ }
+ }
+}
+
+func preview(s string, n int) string {
+ if len(s) <= n {
+ return s
+ }
+ return s[:n] + "..."
+}