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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ Usage:

Flags:
INPUT:
-l, -list string input file containing list of hosts to process
-rr, -request string file containing raw request
-u, -target string[] input target host(s) to probe
-l, -list string input file containing list of hosts to process
-rr, -request string file containing raw request
-u, -target string[] input target host(s) to probe
-im, -input-mode string mode of input file (burp)

PROBES:
-sc, -status-code display response status-code
Expand Down Expand Up @@ -279,6 +280,7 @@ For details about running httpx, see https://docs.projectdiscovery.io/tools/http
# Notes

- As default, `httpx` probe with **HTTPS** scheme and fall-back to **HTTP** only if **HTTPS** is not reachable.
- Burp Suite XML exports can be used as input with `-l burp-export.xml -im burp`
- The `-no-fallback` flag can be used to probe and display both **HTTP** and **HTTPS** result.
- Custom scheme for ports can be defined, for example `-ports http:443,http:80,https:8443`
- Custom resolver supports multiple protocol (**doh|tcp|udp**) in form of `protocol:resolver:port` (e.g. `udp:127.0.0.1:53`)
Expand Down
44 changes: 44 additions & 0 deletions common/inputformats/burp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package inputformats

import (
"io"

"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/seh-msft/burpxml"
)

// BurpFormat is a Burp Suite XML file parser
type BurpFormat struct{}

// NewBurpFormat creates a new Burp XML file parser
func NewBurpFormat() *BurpFormat {
return &BurpFormat{}
}

var _ Format = &BurpFormat{}

// Name returns the name of the format
func (b *BurpFormat) Name() string {
return "burp"
}

// Parse parses the Burp XML input and calls the provided callback
// function for each URL it discovers.
func (b *BurpFormat) Parse(input io.Reader, callback func(url string) bool) error {
items, err := burpxml.Parse(input, true)
if err != nil {
return errors.Wrap(err, "could not parse burp xml")
}

for i, item := range items.Items {
if item.Url == "" {
gologger.Debug().Msgf("Skipping burp item %d: empty URL", i)
continue
}
if !callback(item.Url) {
break
}
}
return nil
}
169 changes: 169 additions & 0 deletions common/inputformats/burp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package inputformats

import (
"strings"
"testing"
)

func TestBurpFormat_Name(t *testing.T) {
b := NewBurpFormat()
if b.Name() != "burp" {
t.Errorf("Expected name 'burp', got '%s'", b.Name())
}
}

func TestBurpFormat_Parse(t *testing.T) {
burpXML := `<?xml version="1.0"?>
<items burpVersion="2023.10.1.2" exportTime="Sat Sep 30 20:11:44 IST 2023">
<item>
<time>Sat Sep 30 20:11:32 IST 2023</time>
<url><![CDATA[http://example.com/path1]]></url>
<host ip="127.0.0.1">example.com</host>
<port>80</port>
<protocol>http</protocol>
<method><![CDATA[GET]]></method>
<path><![CDATA[/path1]]></path>
<extension>null</extension>
<request base64="true"><![CDATA[R0VUIC8gSFRUUC8xLjE=]]></request>
<status>200</status>
<responselength>100</responselength>
<mimetype>HTML</mimetype>
<response base64="true"><![CDATA[SFRUUC8xLjEgMjAwIE9L]]></response>
<comment></comment>
</item>
<item>
<time>Sat Sep 30 20:08:54 IST 2023</time>
<url><![CDATA[https://example.com/path2]]></url>
<host ip="127.0.0.1">example.com</host>
<port>443</port>
<protocol>https</protocol>
<method><![CDATA[POST]]></method>
<path><![CDATA[/path2]]></path>
<extension>null</extension>
<request base64="true"><![CDATA[UE9TVCAvIEhUVFAvMS4x]]></request>
<status>200</status>
<responselength>100</responselength>
<mimetype>JSON</mimetype>
<response base64="true"><![CDATA[SFRUUC8xLjEgMjAwIE9L]]></response>
<comment></comment>
</item>
</items>`

b := NewBurpFormat()
var urls []string

err := b.Parse(strings.NewReader(burpXML), func(url string) bool {
urls = append(urls, url)
return true
})

if err != nil {
t.Fatalf("Parse returned error: %v", err)
}

if len(urls) != 2 {
t.Errorf("Expected 2 URLs, got %d", len(urls))
}

expectedURLs := []string{"http://example.com/path1", "https://example.com/path2"}
if len(urls) != len(expectedURLs) {
t.Fatalf("Expected %d URLs, got %d: %v", len(expectedURLs), len(urls), urls)
}
for i, expected := range expectedURLs {
if urls[i] != expected {
t.Errorf("Expected URL %d to be '%s', got '%s'", i, expected, urls[i])
}
}
}

func TestBurpFormat_ParseEmpty(t *testing.T) {
burpXML := `<?xml version="1.0"?>
<items burpVersion="2023.10.1.2" exportTime="Sat Sep 30 20:11:44 IST 2023">
</items>`

b := NewBurpFormat()
var urls []string

err := b.Parse(strings.NewReader(burpXML), func(url string) bool {
urls = append(urls, url)
return true
})

if err != nil {
t.Fatalf("Parse returned error: %v", err)
}

if len(urls) != 0 {
t.Errorf("Expected 0 URLs, got %d", len(urls))
}
}

func TestBurpFormat_ParseStopEarly(t *testing.T) {
burpXML := `<?xml version="1.0"?>
<items burpVersion="2023.10.1.2" exportTime="Sat Sep 30 20:11:44 IST 2023">
<item>
<url><![CDATA[http://example.com/1]]></url>
<host>example.com</host>
<port>80</port>
<protocol>http</protocol>
<method><![CDATA[GET]]></method>
<path><![CDATA[/1]]></path>
<extension>null</extension>
<request base64="true"><![CDATA[R0VUIC8=]]></request>
<status>200</status>
<responselength>100</responselength>
<mimetype>HTML</mimetype>
<response base64="true"><![CDATA[T0s=]]></response>
<comment></comment>
</item>
<item>
<url><![CDATA[http://example.com/2]]></url>
<host>example.com</host>
<port>80</port>
<protocol>http</protocol>
<method><![CDATA[GET]]></method>
<path><![CDATA[/2]]></path>
<extension>null</extension>
<request base64="true"><![CDATA[R0VUIC8=]]></request>
<status>200</status>
<responselength>100</responselength>
<mimetype>HTML</mimetype>
<response base64="true"><![CDATA[T0s=]]></response>
<comment></comment>
</item>
</items>`

b := NewBurpFormat()
var urls []string

err := b.Parse(strings.NewReader(burpXML), func(url string) bool {
urls = append(urls, url)
return false // stop after first
})

if err != nil {
t.Fatalf("Parse returned error: %v", err)
}

if len(urls) != 1 {
t.Errorf("Expected 1 URL (stopped early), got %d", len(urls))
}
}

func TestBurpFormat_ParseMalformed(t *testing.T) {
malformedXML := `<?xml version="1.0"?>
<items burpVersion="2023.10.1.2">
<item>
<url><![CDATA[http://example.com/path1]]></url>
<!-- missing closing tags -->
</items>`

b := NewBurpFormat()
err := b.Parse(strings.NewReader(malformedXML), func(url string) bool {
return true
})

if err == nil {
t.Error("Expected error for malformed XML, got nil")
}
}
41 changes: 41 additions & 0 deletions common/inputformats/formats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// TODO: This package should be abstracted out to projectdiscovery/utils
// so it can be shared between httpx, nuclei, and other tools.
package inputformats

import (
"io"
"strings"
)

// Format is an interface implemented by all input formats
type Format interface {
// Name returns the name of the format
Name() string
// Parse parses the input and calls the provided callback
// function for each URL it discovers.
Parse(input io.Reader, callback func(url string) bool) error
}

// Supported formats
var formats = []Format{
NewBurpFormat(),
}

// GetFormat returns the format by name
func GetFormat(name string) Format {
for _, f := range formats {
if strings.EqualFold(f.Name(), name) {
return f
}
}
return nil
}

// SupportedFormats returns a comma-separated list of supported format names
func SupportedFormats() string {
var names []string
for _, f := range formats {
names = append(names, f.Name())
}
return strings.Join(names, ", ")
}
43 changes: 43 additions & 0 deletions common/inputformats/formats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package inputformats

import (
"strings"
"testing"
)

func TestGetFormat(t *testing.T) {
tests := []struct {
name string
input string
wantNil bool
wantName string
}{
{"burp lowercase", "burp", false, "burp"},
{"burp uppercase", "BURP", false, "burp"},
{"burp mixed case", "Burp", false, "burp"},
{"invalid format", "invalid", true, ""},
{"empty string", "", true, ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetFormat(tt.input)
if tt.wantNil && got != nil {
t.Errorf("GetFormat(%q) = %v, want nil", tt.input, got)
}
if !tt.wantNil && got == nil {
t.Errorf("GetFormat(%q) = nil, want non-nil", tt.input)
}
if !tt.wantNil && got != nil && got.Name() != tt.wantName {
t.Errorf("GetFormat(%q).Name() = %q, want %q", tt.input, got.Name(), tt.wantName)
}
})
}
}

func TestSupportedFormats(t *testing.T) {
supported := SupportedFormats()
if !strings.Contains(supported, "burp") {
t.Errorf("SupportedFormats() = %q, expected to contain 'burp'", supported)
}
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ require (
github.com/dustin/go-humanize v1.0.1
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/projectdiscovery/awesome-search-queries v0.0.0-20260104120501-961ef30f7193
github.com/seh-msft/burpxml v1.0.1
github.com/weppos/publicsuffix-go v0.50.2
)

Expand Down Expand Up @@ -128,7 +130,6 @@ require (
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/projectdiscovery/awesome-search-queries v0.0.0-20260104120501-961ef30f7193 // indirect
github.com/projectdiscovery/blackrock v0.0.1 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/gostruct v0.0.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ 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/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
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.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
Expand Down
11 changes: 11 additions & 0 deletions runner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
customport "github.com/projectdiscovery/httpx/common/customports"
fileutilz "github.com/projectdiscovery/httpx/common/fileutil"
httpxcommon "github.com/projectdiscovery/httpx/common/httpx"
"github.com/projectdiscovery/httpx/common/inputformats"
"github.com/projectdiscovery/httpx/common/stringz"
"github.com/projectdiscovery/networkpolicy"
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
Expand Down Expand Up @@ -191,6 +192,7 @@ type Options struct {
SocksProxy string
Proxy string
InputFile string
InputMode string
InputTargetHost goflags.StringSlice
Methods string
RequestURI string
Expand Down Expand Up @@ -375,6 +377,7 @@ func ParseOptions() *Options {
flagSet.StringVarP(&options.InputFile, "list", "l", "", "input file containing list of hosts to process"),
flagSet.StringVarP(&options.InputRawRequest, "request", "rr", "", "file containing raw request"),
flagSet.StringSliceVarP(&options.InputTargetHost, "target", "u", nil, "input target host(s) to probe", goflags.CommaSeparatedStringSliceOptions),
flagSet.StringVarP(&options.InputMode, "input-mode", "im", "", fmt.Sprintf("mode of input file (%s)", inputformats.SupportedFormats())),
)

flagSet.CreateGroup("Probes", "Probes",
Expand Down Expand Up @@ -677,6 +680,14 @@ func (options *Options) ValidateOptions() error {
return fmt.Errorf("file '%s' does not exist", options.InputRawRequest)
}

if options.InputMode != "" && inputformats.GetFormat(options.InputMode) == nil {
return fmt.Errorf("invalid input mode '%s', supported formats: %s", options.InputMode, inputformats.SupportedFormats())
}

if options.InputMode != "" && options.InputFile == "" {
return errors.New("-im/-input-mode requires -l/-list to specify an input file")
}

if options.Silent {
incompatibleFlagsList := flagsIncompatibleWithSilent(options)
if len(incompatibleFlagsList) > 0 {
Expand Down
Loading
Loading