Initial commit

This commit is contained in:
bauthard 2020-05-29 09:37:55 +00:00
commit e974bb52e5
23 changed files with 1416 additions and 0 deletions

246
README.md Normal file
View File

@ -0,0 +1,246 @@
<h1 align="left">
<img src="static/httpx-logo.png" alt="httpx" width="200px"></a>
<br>
</h1>
[![License](https://img.shields.io/badge/license-MIT-_red.svg)](https://opensource.org/licenses/MIT)
[![Go Report Card](https://goreportcard.com/badge/github.com/projectdiscovery/httpx)](https://goreportcard.com/report/github.com/projectdiscovery/httpx)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/projectdiscovery/httpx/issues)
httpx is a fast and multi-purpose HTTP toolkit allow to run multiple probers using [retryablehttp-go library](https://github.com/projectdiscovery/retryablehttp-go) library.
# Resources
- [Resources](#resources)
- [Features](#features)
- [Usage](#usage)
- [Installation Instructions](#installation-instructions)
- [From Binary](#from-binary)
- [From Source](#from-source)
- [Running httpx](#running-httpx)
- [Running httpx with a single template.](#running-httpx)
- [Thanks](#thanks)
# Features
<h1 align="left">
<img src="static/httpx-run.png" alt="httpx" width="700px"></a>
<br>
</h1>
- Simple and modular code base making it easy to contribute.
- Fast And fully configurable flags to probe mutiple elements
- Supports vhost, urls, ports, title, content-length, status-code, response-body probbing.
- Handles edge cases doing retries, backoffs etc for handling WAFs.
# Usage
```bash
httpx -h
```
This will display help for the tool. Here are all the switches it supports.
| Flag | Description | Example |
|------------------- |-------------------------------------------------------|----------------------------------------------------|
| -H | Custom Header input | httpx -H 'x-bug-bounty: hacker' |
| -follow-redirects | Follow URL redirects (default false) | httpx -follow-redirects |
| -http-proxy | URL of the proxy server | httpx -http-proxy hxxp://proxy-host:80 |
| -l | File to save output result (optional) | httpx -o output.txt |
| -no-color | Disable colors in the output. | httpx -no-color |
| -o | File to save output result (optional) | httpx -o output.txt |
| -json | Prints all the probes in JSON format (default false) | httpx -json |
| -vhost | Probes to detect vhost from list of subdomains | httpx -vhost |
| -threads | Number of threads (default 50) | httpx - threads 100 |
| -ports | Ports ranges to probe (nmap syntax: eg 1,2-10,11) | httpx -ports 80,443,100-200 |
| -title | Prints title of page if avaiable | httpx -title |
| -content-length | Prints content length in the output | httpx -content-length |
| -status-code | Prints status code in the output | httpx -status-code |
| -store-response | Store response as domain.txt | httpx -store-response |
| -store-response-dir| Directory to store response (default current path) | httpx -store-response-dir output |
| -retries | Number of retries | httpx -retries |
| -silent | Prints only results in the output | httpx -silent |
| -timeout | Timeout in seconds (default 5) | httpx -timeout 10 |
| -verbose | Verbose Mode | httpx -verbose |
| -version | Prints current version of the httpx | httpx -version |
| -x | Request Method (default 'GET') | httpx -x HEAD |
# Installation Instructions
### From Binary
The installation is easy. You can download the pre-built binaries for your platform from the [Releases](https://github.com/projectdiscovery/httpx/releases/) page. Extract them using tar, move it to your `$PATH`and you're ready to go.
```bash
> tar -xzvf httpx-linux-amd64.tar.gz
> mv httpx-linux-amd64 /usr/bin/httpx
> httpx -h
```
### From Source
httpx requires go1.13+ to install successfully. Run the following command to get the repo -
```bash
> GO111MODULE=on go get -u -v github.com/projectdiscovery/httpx/cmd/httpx
```
In order to update the tool, you can use -u flag with `go get` command.
# Running httpX to probe `2967` hosts
```bash
> chaos -d oath.cloud -count -silent
2967
> time chaos -d oath.cloud -silent | httpx -status-code -content-length -title -store-response -threads 100 -json | wc
__ __ __ _ __
/ /_ / /_/ /_____ | |/ /
/ __ \/ __/ __/ __ \| /
/ / / / /_/ /_/ /_/ / |
/_/ /_/\__/\__/ .___/_/|_|
/_/
projectdiscovery.io
[WRN] Use with caution. You are responsible for your actions
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
196
real 0m52.159s
user 0m4.084s
sys 0m3.880s
```
### Running httpx with stnin
This will run the tool against all the hosts in `urls.txt` and returns the matched results.
```bash
> cat hosts.txt | httpx
__ __ __ _ __
/ /_ / /_/ /_____ | |/ /
/ __ \/ __/ __/ __ \| /
/ / / / /_/ /_/ /_/ / |
/_/ /_/\__/\__/ .___/_/|_|
/_/
projectdiscovery.io
[WRN] Use with caution. You are responsible for your actions
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
https://mta-sts.managed.hackerone.com
https://mta-sts.hackerone.com
https://mta-sts.forwarding.hackerone.com
https://docs.hackerone.com
https://www.hackerone.com
https://resources.hackerone.com
https://api.hackerone.com
https://support.hackerone.com
```
### Running httpx with file input
This will run the tool against all the hosts in `urls.txt` and returns the matched results.
```bash
> httpx -l hosts.txt
root@b0x:~/httpx# httpx -l hosts.txt
__ __ __ _ __
/ /_ / /_/ /_____ | |/ /
/ __ \/ __/ __/ __ \| /
/ / / / /_/ /_/ /_/ / |
/_/ /_/\__/\__/ .___/_/|_|
/_/
projectdiscovery.io
[WRN] Use with caution. You are responsible for your actions
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
https://docs.hackerone.com
https://mta-sts.hackerone.com
https://mta-sts.managed.hackerone.com
https://mta-sts.forwarding.hackerone.com
https://www.hackerone.com
https://resources.hackerone.com
https://api.hackerone.com
https://support.hackerone.com
```
### Using httpX with subfinder/chaos and any other similar tool.
```bash
> subfinder -d hackerone.com -silent | httpx httpx -title -content-length -status-code
__ __ __ _ __
/ /_ / /_/ /_____ | |/ /
/ __ \/ __/ __/ __ \| /
/ / / / /_/ /_/ /_/ / |
/_/ /_/\__/\__/ .___/_/|_|
/_/
projectdiscovery.io
[WRN] Use with caution. You are responsible for your actions
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
https://mta-sts.forwarding.hackerone.com [404] [9339] [Page not found · GitHub Pages]
https://mta-sts.hackerone.com [404] [9339] [Page not found · GitHub Pages]
https://mta-sts.managed.hackerone.com [404] [9339] [Page not found · GitHub Pages]
https://docs.hackerone.com [200] [65444] [HackerOne Platform Documentation]
https://www.hackerone.com [200] [54166] [Bug Bounty - Hacker Powered Security Testing | HackerOne]
https://support.hackerone.com [301] [489] []
https://api.hackerone.com [200] [7791] [HackerOne API]
https://hackerone.com [301] [92] []
https://resources.hackerone.com [301] [0] []
```
### Running httpX with json output
```bash
> chaos -d hackerone.com -silent | httpx -json
__ __ __ _ __
/ /_ / /_/ /_____ | |/ /
/ __ \/ __/ __/ __ \| /
/ / / / /_/ /_/ /_/ / |
/_/ /_/\__/\__/ .___/_/|_|
/_/
projectdiscovery.io
[WRN] Use with caution. You are responsible for your actions
[WRN] Developers assume no liability and are not responsible for any misuse or damage.
{"url":"https://mta-sts.forwarding.hackerone.com","content-length":9339,"status-code":404,"title":"","error":null,"vhost":false}
{"url":"https://mta-sts.hackerone.com","content-length":9339,"status-code":404,"title":"","error":null,"vhost":false}
{"url":"https://docs.hackerone.com","content-length":65444,"status-code":200,"title":"","error":null,"vhost":false}
{"url":"https://mta-sts.managed.hackerone.com","content-length":9339,"status-code":404,"title":"","error":null,"vhost":false}
{"url":"https://support.hackerone.com","content-length":489,"status-code":301,"title":"","error":null,"vhost":false}
{"url":"https://resources.hackerone.com","content-length":0,"status-code":301,"title":"","error":null,"vhost":false}
{"url":"https://api.hackerone.com","content-length":7791,"status-code":200,"title":"","error":null,"vhost":false}
{"url":"https://www.hackerone.com","content-length":54166,"status-code":200,"title":"","error":null,"vhost":false}
```
You can simply use `jq` to filter out the json results as per your interest.
## Todo
- [ ] Adding support to probe [http smuggling](https://portswigger.net/web-security/request-smuggling)
# Thanks
httpX is made with 🖤 by the [projectdiscovery](https://projectdiscovery.io) team. Community contributions have made the project what it is. See the **[Thanks.md](https://github.com/projectdiscovery/httpx/blob/master/THANKS.md)** file for more details. Do also check out these similar awesome projects that may fit in your workflow:
[https://github.com/tomnomnom/httprobe](https://github.com/tomnomnom/httprobe)</br>

24
cmd/httpx/banner.go Normal file
View File

@ -0,0 +1,24 @@
package main
import "github.com/projectdiscovery/gologger"
const banner = `
__ __ __ _ __
/ /_ / /_/ /_____ | |/ /
/ __ \/ __/ __/ __ \| /
/ / / / /_/ /_/ /_/ / |
/_/ /_/\__/\__/ .___/_/|_|
/_/
`
// Version is the current version of httpx
const Version = `0.0.1`
// showBanner is used to show the banner to the user
func showBanner() {
gologger.Printf("%s\n", banner)
gologger.Printf("\t\tprojectdiscovery.io\n\n")
gologger.Labelf("Use with caution. You are responsible for your actions\n")
gologger.Labelf("Developers assume no liability and are not responsible for any misuse or damage.\n")
}

252
cmd/httpx/httpx.go Normal file
View File

@ -0,0 +1,252 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"time"
"github.com/projectdiscovery/gologger"
customport "github.com/projectdiscovery/httpx/common/customports"
"github.com/projectdiscovery/httpx/common/fileutil"
"github.com/projectdiscovery/httpx/common/httpx"
"github.com/projectdiscovery/httpx/common/stringz"
"github.com/remeh/sizedwaitgroup"
)
func main() {
options := ParseOptions()
options.validateOptions()
httpxOptions := httpx.DefaultOptions
httpxOptions.Timeout = time.Duration(options.Timeout) * time.Second
httpxOptions.RetryMax = options.Retries
httpxOptions.FollowRedirects = options.FollowRedirects
httpxOptions.CustomHeaders = make(map[string]string)
for _, customHeader := range options.CustomHeaders {
tokens := strings.Split(customHeader, ":")
// if it's an invalid header skip it
if len(tokens) < 2 {
continue
}
httpxOptions.CustomHeaders[tokens[0]] = tokens[1]
}
hp, err := httpx.New(&httpxOptions)
if err != nil {
gologger.Fatalf("Could not create httpx instance: %s\n", err)
}
var scanopts scanOptions
scanopts.Method = options.Method
protocol := "https"
scanopts.VHost = options.VHost
scanopts.OutputTitle = options.ExtractTitle
scanopts.OutputStatusCode = options.StatusCode
scanopts.OutputContentLength = options.ContentLength
scanopts.StoreResponse = options.StoreResponse
scanopts.StoreResponseDirectory = options.StoreResponseDir
scanopts.Method = options.Method
// Try to create output folder if it doesnt exist
if options.StoreResponse && options.StoreResponseDir != "" && options.StoreResponseDir != "." {
if err := os.MkdirAll(options.StoreResponseDir, os.ModePerm); err != nil {
gologger.Fatalf("Could not create output directory '%s': %s\n", options.StoreResponseDir, err)
}
}
// output routine
wgoutput := sizedwaitgroup.New(1)
wgoutput.Add()
output := make(chan Result)
go func(output chan Result) {
defer wgoutput.Done()
var f *os.File
if options.Output != "" {
var err error
f, err = os.Create(options.Output)
if err != nil {
gologger.Fatalf("Could not create output file '%s': %s\n", options.Output, err)
}
defer f.Close()
}
for r := range output {
if r.Error != nil {
continue
}
row := r.str
if options.JSONOutput {
row = r.JSON()
}
fmt.Println(row)
if f != nil {
f.WriteString(row + "\n")
}
}
}(output)
wg := sizedwaitgroup.New(options.Threads)
var sc *bufio.Scanner
// check if file has been provided
if fileutil.FileExists(options.InputFile) {
finput, err := os.Open(options.InputFile)
if err != nil {
gologger.Fatalf("Could read input file '%s': %s\n", options.InputFile, err)
}
defer finput.Close()
sc = bufio.NewScanner(finput)
} else if fileutil.HasStdin() {
sc = bufio.NewScanner(os.Stdin)
} else {
gologger.Fatalf("No input provided")
}
for sc.Scan() {
target := stringz.TrimProtocol(sc.Text())
// if no custom ports specified then test the default ones
if len(customport.Ports) == 0 {
wg.Add()
go func(target string) {
defer wg.Done()
analyze(hp, protocol, target, 0, &scanopts, output)
}(target)
}
// the host name shouldn't have any semicolon - in case remove the port
semicolonPosition := strings.LastIndex(target, ":")
if semicolonPosition > 0 {
target = target[:semicolonPosition]
}
for port := range customport.Ports {
wg.Add()
go func(port int) {
defer wg.Done()
analyze(hp, protocol, target, port, &scanopts, output)
}(port)
}
}
wg.Wait()
close(output)
wgoutput.Wait()
}
type scanOptions struct {
Method string
VHost bool
OutputTitle bool
OutputStatusCode bool
OutputContentLength bool
StoreResponse bool
StoreResponseDirectory string
}
func analyze(hp *httpx.HTTPX, protocol string, domain string, port int, scanopts *scanOptions, output chan Result) {
retried := false
retry:
URL := fmt.Sprintf("%s://%s", protocol, domain)
if port > 0 {
URL = fmt.Sprintf("%s:%d", URL, port)
}
req, err := hp.NewRequest(scanopts.Method, URL)
if err != nil {
output <- Result{URL: URL, Error: err}
return
}
hp.SetCustomHeaders(req, hp.CustomHeaders)
resp, err := hp.Do(req)
if err != nil {
output <- Result{URL: URL, Error: err}
if !retried {
if protocol == "https" {
protocol = "http"
} else {
protocol = "https"
}
retried = true
goto retry
}
return
}
var fullURL string
if resp.StatusCode >= 0 {
if port > 0 {
fullURL = fmt.Sprintf("%s://%s:%d", protocol, domain, port)
} else {
fullURL = fmt.Sprintf("%s://%s", protocol, domain)
}
}
builder := &strings.Builder{}
builder.WriteString(fullURL)
if scanopts.OutputStatusCode {
builder.WriteString(fmt.Sprintf(" [%d]", resp.StatusCode))
}
if scanopts.OutputContentLength {
builder.WriteString(fmt.Sprintf(" [%d]", resp.ContentLength))
}
title := ""
if scanopts.OutputTitle {
title = httpx.ExtractTitle(resp)
builder.WriteString(fmt.Sprintf(" [%s]", title))
}
// check for virtual host
isvhost := false
if scanopts.VHost {
isvhost, _ = hp.IsVirtualHost(req)
if isvhost {
builder.WriteString(" [vhost]")
}
}
// store responses in directory
if scanopts.StoreResponse {
responsePath := path.Join(scanopts.StoreResponseDirectory, domain+".txt")
ioutil.WriteFile(responsePath, []byte(resp.Raw), 0644)
}
output <- Result{URL: fullURL, ContentLength: resp.ContentLength, StatusCode: resp.StatusCode, Title: title, str: builder.String(), VHost: isvhost}
}
// Result of a scan
type Result struct {
URL string `json:"url"`
ContentLength int `json:"content-length"`
StatusCode int `json:"status-code"`
Title string `json:"title"`
str string
Error error `json:"error"`
VHost bool `json:"vhost"`
}
// JSON the result
func (r *Result) JSON() string {
if js, err := json.Marshal(r); err == nil {
return string(js)
}
return ""
}

101
cmd/httpx/options.go Normal file
View File

@ -0,0 +1,101 @@
package main
import (
"flag"
"os"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/httpx/common/customheader"
customport "github.com/projectdiscovery/httpx/common/customports"
"github.com/projectdiscovery/httpx/common/fileutil"
)
// Options contains configuration options for chaos client.
type Options struct {
RawRequestFile string
VHost bool
Smuggling bool
ExtractTitle bool
StatusCode bool
ContentLength bool
Retries int
Threads int
Timeout int
CustomHeaders customheader.CustomHeaders
CustomPorts customport.CustomPorts
Output string
FollowRedirects bool
StoreResponse bool
StoreResponseDir string
HttpProxy string
SocksProxy string
JSONOutput bool
InputFile string
Method string
Silent bool
Version bool
Verbose bool
NoColor bool
}
// ParseOptions parses the command line options for application
func ParseOptions() *Options {
options := &Options{}
flag.IntVar(&options.Threads, "threads", 50, "Number of threads")
flag.IntVar(&options.Retries, "retries", 0, "Number of retries")
flag.IntVar(&options.Timeout, "timeout", 5, "Timeout in seconds")
flag.StringVar(&options.Output, "o", "", "File to write output to (optional)")
flag.BoolVar(&options.VHost, "vhost", false, "Check for VHOSTs")
flag.BoolVar(&options.ExtractTitle, "title", false, "Extracts title")
flag.BoolVar(&options.StatusCode, "status-code", false, "Extracts Status Code")
flag.Var(&options.CustomHeaders, "H", "Custom Header")
flag.Var(&options.CustomPorts, "ports", "ports range (nmap syntax: eg 1,2-10,11)")
flag.BoolVar(&options.ContentLength, "content-length", false, "Content Length")
flag.BoolVar(&options.StoreResponse, "store-response", false, "Store Response as domain.txt")
flag.StringVar(&options.StoreResponseDir, "store-response-dir", ".", "Store Response Directory (default current directory)")
flag.BoolVar(&options.FollowRedirects, "follow-redirects", false, "Follow Redirects")
flag.StringVar(&options.HttpProxy, "http-proxy", "", "Http Proxy")
flag.BoolVar(&options.JSONOutput, "json", false, "JSON Output")
flag.StringVar(&options.InputFile, "l", "", "File containing domains")
flag.StringVar(&options.Method, "x", "GET", "Request Method")
flag.BoolVar(&options.Silent, "silent", false, "Silent mode")
flag.BoolVar(&options.Version, "version", false, "Show version of httpx")
flag.BoolVar(&options.Verbose, "verbose", false, "Verbose Mode")
flag.BoolVar(&options.NoColor, "no-color", true, "No Color")
flag.Parse()
// Read the inputs and configure the logging
options.configureOutput()
showBanner()
if options.Version {
gologger.Infof("Current Version: %s\n", Version)
os.Exit(0)
}
options.validateOptions()
return options
}
func (options *Options) validateOptions() {
if options.InputFile != "" && !fileutil.FileExists(options.InputFile) {
gologger.Fatalf("File %s does not exist!\n", options.InputFile)
}
}
// configureOutput configures the output on the screen
func (options *Options) configureOutput() {
// If the user desires verbose output, show verbose output
if options.Verbose {
gologger.MaxLevel = gologger.Verbose
}
if options.NoColor {
gologger.UseColors = false
}
if options.Silent {
gologger.MaxLevel = gologger.Silent
}
}

80
common/cache/cache.go vendored Normal file
View File

@ -0,0 +1,80 @@
package cache
import (
"net"
"github.com/coocood/freecache"
dns "github.com/projectdiscovery/httpx/common/resolver"
)
// Cache is a strcture for caching DNS lookups
type Cache struct {
dnsClient Resolver
cache *freecache.Cache
defaultExpirationTime int
}
// Resolver interface
type Resolver interface {
Resolve(string) (dns.Result, error)
}
// Options of the cache
type Options struct {
BaseResolvers []string
CacheSize int
ExpirationTime int
MaxRetries int
}
// DefaultOptions of the cache
var DefaultOptions = Options{
BaseResolvers: DefaultResolvers,
CacheSize: 10,
ExpirationTime: 5 * 60,
MaxRetries: 5,
}
// DefaultResolvers trusted
var DefaultResolvers = []string{
"1.1.1.1:53",
"1.0.0.1:53",
"8.8.8.8:53",
"8.8.4.4:53",
}
// New creates a new caching dns resolver
func New(options Options) (*Cache, error) {
dnsClient, err := dns.New(options.BaseResolvers, options.MaxRetries)
if err != nil {
return nil, err
}
cache := freecache.NewCache(options.CacheSize * 1024 * 1024)
return &Cache{dnsClient: dnsClient, cache: cache, defaultExpirationTime: options.ExpirationTime}, nil
}
// Lookup a hostname
func (c *Cache) Lookup(hostname string) ([]string, error) {
if ip := net.ParseIP(hostname); ip != nil {
return []string{hostname}, nil
}
hostnameBytes := []byte(hostname)
value, err := c.cache.Get(hostnameBytes)
if err != nil {
if len(err.Error()) != 15 {
return nil, err
}
results, err := c.dnsClient.Resolve(hostname)
if err != nil {
return nil, err
}
if results.TTL == 0 {
results.TTL = c.defaultExpirationTime
}
c.cache.Set(hostnameBytes, MarshalAddresses(results.IPs), results.TTL)
return results.IPs, nil
}
return UnmarshalAddresses(value), nil
}

53
common/cache/dialer.go vendored Normal file
View File

@ -0,0 +1,53 @@
package cache
import (
"context"
"net"
"strings"
"time"
)
// NoAddressFoundError occurs when no addresses are found for the host
type NoAddressFoundError struct{}
func (m *NoAddressFoundError) Error() string {
return "no address found for host"
}
// DialerFunc with signature matching of go net/dial
type DialerFunc func(context.Context, string, string) (net.Conn, error)
// NewDialer gets a new Dialer instance using a resolver cache
func NewDialer(options Options) (DialerFunc, error) {
cache, err := New(options)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 10 * time.Second,
DualStack: true,
}
return func(ctx context.Context, network, address string) (conn net.Conn, err error) {
separator := strings.LastIndex(address, ":")
// we need to filter out empty records
ips, err := cache.Lookup(address[:separator])
var finalIps []string
for _, ip := range ips {
if ip != "" {
finalIps = append(finalIps, ip)
}
}
if err != nil || len(finalIps) <= 0 {
return nil, &NoAddressFoundError{}
} // Dial to the IPs finally.
for _, ip := range ips {
conn, err = dialer.DialContext(ctx, network, ip+address[separator:])
if err == nil {
break
}
}
return
}, nil
}

13
common/cache/utils.go vendored Normal file
View File

@ -0,0 +1,13 @@
package cache
import "strings"
var Separator = "-"
func MarshalAddresses(ips []string) []byte {
return []byte(strings.Join(ips, Separator))
}
func UnmarshalAddresses(data []byte) []string {
return strings.Split(string(data), Separator)
}

View File

@ -0,0 +1,15 @@
package customheader
// CustomHeaders valid for all requests
type CustomHeaders []string
// String returns just a label
func (c *CustomHeaders) String() string {
return "Custom Global Headers"
}
// Set a new global header
func (c *CustomHeaders) Set(value string) error {
*c = append(*c, value)
return nil
}

View File

@ -0,0 +1,56 @@
package customport
import (
"strconv"
"strings"
)
func init() {
Ports = make(map[int]struct{})
}
// Ports to scan
var Ports map[int]struct{}
// CustomPorts definition
type CustomPorts []string
// String returns just a label
func (c *CustomPorts) String() string {
return "Custom Ports"
}
// Set a port range
func (c *CustomPorts) Set(value string) error {
// ports can be like nmap -p start-end,port1,port2,port3
// splits on comma
potentialPorts := strings.Split(value, ",")
// check if port is a single integer value or needs to be expanded futher
for _, potentialPort := range potentialPorts {
potentialRange := strings.Split(strings.TrimSpace(potentialPort), "-")
// it's a single port?
if len(potentialRange) < 2 {
if p, err := strconv.Atoi(potentialPort); err == nil {
Ports[p] = struct{}{}
}
} else {
// expand range
var lowP, highP int
lowP, err := strconv.Atoi(potentialRange[0])
if err != nil {
continue
}
highP, err = strconv.Atoi(potentialRange[1])
if err != nil {
continue
}
for i := lowP; i <= highP; i++ {
Ports[i] = struct{}{}
}
}
}
*c = append(*c, value)
return nil
}

View File

@ -0,0 +1,24 @@
package fileutil
import "os"
// FileExists checks if a file exists and is not a directory
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// HasStdin determines if the user has piped input
func HasStdin() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
if fi.Mode()&os.ModeNamedPipe == 0 {
return false
}
return true
}

View File

@ -0,0 +1,14 @@
package httputilz
import (
"net/http/httputil"
"github.com/projectdiscovery/retryablehttp-go"
)
// DumpRequest to string
func DumpRequest(req *retryablehttp.Request) (string, error) {
dump, err := httputil.DumpRequestOut(req.Request, true)
return string(dump), err
}

67
common/httpx/filter.go Normal file
View File

@ -0,0 +1,67 @@
package httpx
import (
"regexp"
"strings"
)
// Filter defines a generic filter interface to apply to responses
type Filter interface {
Filter(response *Response) (bool, error)
}
// FilterString defines a filter of type string
type FilterString struct {
Keywords []string
}
// Filter a response with strings filtering
func (f FilterString) Filter(response *Response) (bool, error) {
for _, keyword := range f.Keywords {
if strings.Contains(response.Raw, keyword) {
return true, nil
}
}
return false, nil
}
// FilterRegex defines a filter of type regex
type FilterRegex struct {
Regexs []string
}
// Filter a response with regexes
func (f FilterRegex) Filter(response *Response) (bool, error) {
for _, regex := range f.Regexs {
matched, err := regexp.MatchString(regex, response.Raw)
if err != nil {
return false, err
}
if matched {
return true, nil
}
}
return false, nil
}
// CustomCallback used in custom filters
type CustomCallback func(response *Response) (bool, error)
// FilterCustom defines a filter with callback functions applied
type FilterCustom struct {
CallBacks []CustomCallback
}
// Filter a response with custom callbacks
func (f FilterCustom) Filter(response *Response) (bool, error) {
for _, callback := range f.CallBacks {
ok, err := callback(response)
if ok && err == nil {
return true, err
}
}
return false, nil
}

161
common/httpx/httpx.go Normal file
View File

@ -0,0 +1,161 @@
package httpx
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"unicode/utf8"
"github.com/microcosm-cc/bluemonday"
"github.com/projectdiscovery/httpx/common/cache"
retryablehttp "github.com/projectdiscovery/retryablehttp-go"
)
// HTTPX represent an instance of the library client
type HTTPX struct {
client *retryablehttp.Client
Filters []Filter
Options *Options
htmlPolicy *bluemonday.Policy
CustomHeaders map[string]string
}
// New httpx instance
func New(options *Options) (*HTTPX, error) {
httpx := &HTTPX{}
dialer, err := cache.NewDialer(cache.DefaultOptions)
if err != nil {
return nil, fmt.Errorf("Could not create resolver cache: %s", err)
}
httpx.Options = options
var retryablehttpOptions = retryablehttp.DefaultOptionsSpraying
retryablehttpOptions.Timeout = httpx.Options.Timeout
retryablehttpOptions.RetryMax = httpx.Options.RetryMax
var redirectFunc = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}
if httpx.Options.FollowRedirects {
redirectFunc = nil
}
transport := &http.Transport{
DialContext: dialer,
MaxIdleConnsPerHost: -1,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
}
if httpx.Options.HttpProxy != "" {
proxyURL, err := url.Parse(httpx.Options.HttpProxy)
if err != nil {
return nil, err
}
transport.Proxy = http.ProxyURL(proxyURL)
}
httpx.client = retryablehttp.NewWithHTTPClient(&http.Client{
Transport: transport,
Timeout: httpx.Options.Timeout,
CheckRedirect: redirectFunc,
}, retryablehttpOptions)
httpx.htmlPolicy = bluemonday.NewPolicy()
httpx.CustomHeaders = httpx.Options.CustomHeaders
return httpx, nil
}
// Do http request
func (h *HTTPX) Do(req *retryablehttp.Request) (*Response, error) {
var (
resp Response
)
httpresp, err := h.client.Do(req)
if err != nil {
return nil, err
}
rawresp, err := httputil.DumpResponse(httpresp, true)
if err != nil {
return nil, err
}
resp.Raw = string(rawresp)
respbody, err := ioutil.ReadAll(httpresp.Body)
if err != nil {
return nil, err
}
respbodystr := string(respbody)
// check if we need to strip html
if h.Options.VHostStripHTML {
respbodystr = h.htmlPolicy.Sanitize(respbodystr)
}
resp.ContentLength = utf8.RuneCountInString(respbodystr)
resp.Data = respbody
// fill metrics
resp.StatusCode = httpresp.StatusCode
// number of words
resp.Words = len(strings.Split(respbodystr, " "))
// number of lines
resp.Lines = len(strings.Split(respbodystr, "\n"))
return &resp, nil
}
// Verify the http calls and apply-cascade all the filters, as soon as one matches it returns true
func (h *HTTPX) Verify(req *retryablehttp.Request) (bool, error) {
resp, err := h.Do(req)
if err != nil {
return false, err
}
// apply all filters
for _, f := range h.Filters {
ok, err := f.Filter(resp)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}
// AddFilter cascade
func (h *HTTPX) AddFilter(f Filter) {
h.Filters = append(h.Filters, f)
}
// NewRequest from url
func (h *HTTPX) NewRequest(method, URL string) (req *retryablehttp.Request, err error) {
req, err = retryablehttp.NewRequest(method, URL, nil)
// set default user agent
req.Header.Set("User-Agent", h.Options.DefaultUserAgent)
return
}
// SetCustomHeaders on the provided request
func (h *HTTPX) SetCustomHeaders(r *retryablehttp.Request, headers map[string]string) {
for name, value := range headers {
r.Header.Set(name, value)
}
}

47
common/httpx/option.go Normal file
View File

@ -0,0 +1,47 @@
package httpx
import (
"time"
)
// Options contains configuration options for the client
type Options struct {
Threads int
// Timeout is the maximum time to wait for the request
Timeout time.Duration
// RetryMax is the maximum number of retries
RetryMax int
CustomHeaders map[string]string
FollowRedirects bool
DefaultUserAgent string
HttpProxy string
SocksProxy string
// VHOSTs options
VHostIgnoreStatusCode bool
VHostIgnoreContentLength bool
VHostIgnoreNumberOfWords bool
VHostIgnoreNumberOfLines bool
VHostStripHTML bool
// VHostimilarityRatio 1 - 100
VHostSimilarityRatio int
}
// DefaultOptions contains the default options
var DefaultOptions = Options{
Threads: 25,
Timeout: 30 * time.Second,
RetryMax: 5,
// VHOSTs options
VHostIgnoreStatusCode: false,
VHostIgnoreContentLength: true,
VHostIgnoreNumberOfWords: false,
VHostIgnoreNumberOfLines: false,
VHostStripHTML: false,
VHostSimilarityRatio: 85,
DefaultUserAgent: "httpx - Open-source project (github.com/projectdiscovery/httpx)",
// Smuggling Options
}

12
common/httpx/response.go Normal file
View File

@ -0,0 +1,12 @@
package httpx
// Response contains the response to a server
type Response struct {
StatusCode int
Headers map[string][]string
Data []byte
ContentLength int
Raw string
Words int
Lines int
}

24
common/httpx/title.go Normal file
View File

@ -0,0 +1,24 @@
package httpx
import (
"regexp"
"strings"
"golang.org/x/net/html"
)
// ExtractTitle from a response
func ExtractTitle(r *Response) string {
var re = regexp.MustCompile(`(?m)<\s*title[^>]*>(.*?)<\s*/\s*title>`)
for _, match := range re.FindAllString(r.Raw, -1) {
return html.UnescapeString(trimTitleTags(match))
}
return ""
}
func trimTitleTags(title string) string {
// trim <title>*</title>
titleBegin := strings.Index(title, ">")
titleEnd := strings.Index(title, "</")
return title[titleBegin+1 : titleEnd]
}

View File

@ -0,0 +1,52 @@
package httpx
import (
"fmt"
"github.com/hbakhtiyor/strsim"
retryablehttp "github.com/projectdiscovery/retryablehttp-go"
"github.com/rs/xid"
)
// IsVirtualHost checks if the target endpoint is a virtual host
func (h *HTTPX) IsVirtualHost(req *retryablehttp.Request) (bool, error) {
httpresp1, err := h.Do(req)
if err != nil {
return false, err
}
// request a non-existing endpoint
req.Host = fmt.Sprintf("%s.%s", xid.New().String(), req.Host)
httpresp2, err := h.Do(req)
if err != nil {
return false, err
}
// Status Code
if !h.Options.VHostIgnoreStatusCode && httpresp1.StatusCode != httpresp2.StatusCode {
return true, nil
}
// Content - Bytes Length
if !h.Options.VHostIgnoreContentLength && httpresp1.ContentLength != httpresp2.ContentLength {
return true, nil
}
// Content - Number of words (space separated)
if !h.Options.VHostIgnoreNumberOfWords && httpresp1.Words != httpresp2.Words {
return true, nil
}
// Content - Number of lines (newline separated)
if !h.Options.VHostIgnoreNumberOfLines && httpresp1.Lines != httpresp2.Lines {
return true, nil
}
// Similarity Ratio - if similarity is under threshold we consider it a valid vHost
if int(strsim.Compare(httpresp1.Raw, httpresp2.Raw)*100) <= h.Options.VHostSimilarityRatio {
return true, nil
}
return false, nil
}

69
common/resolver/client.go Normal file
View File

@ -0,0 +1,69 @@
package dns
import (
"errors"
"math/rand"
"time"
"github.com/miekg/dns"
)
const defaultPort = "53"
// Client dns instance
type Client struct {
resolvers []string
maxRetries int
}
// Result containing ip and time to live
type Result struct {
IPs []string
TTL int
}
// New creates a new dns client
func New(baseResolvers []string, maxRetries int) (*Client, error) {
rand.Seed(time.Now().UnixNano())
client := Client{maxRetries: maxRetries}
// fails on non unix systems so we just don't care
resolvers, _ := ReadResolveConfig("/etc/resolv.conf")
client.resolvers = append(client.resolvers, resolvers...)
client.resolvers = append(client.resolvers, baseResolvers...)
return &client, nil
}
// Resolve ips for a record
func (c *Client) Resolve(host string) (Result, error) {
msg := new(dns.Msg)
msg.Id = dns.Id()
msg.RecursionDesired = true
msg.Question = make([]dns.Question, 1)
msg.Question[0] = dns.Question{
Name: dns.Fqdn(host),
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}
resolver := c.resolvers[rand.Intn(len(c.resolvers))]
var err error
var answer *dns.Msg
result := Result{}
for i := 0; i < c.maxRetries; i++ {
answer, err = dns.Exchange(msg, resolver)
if err != nil {
continue
}
if answer != nil && answer.Rcode != dns.RcodeSuccess {
return result, errors.New(dns.RcodeToString[answer.Rcode])
}
for _, record := range answer.Answer {
if t, ok := record.(*dns.A); ok {
result.IPs = append(result.IPs, t.A.String())
result.TTL = int(t.Header().Ttl)
}
}
return result, nil
}
return result, err
}

31
common/resolver/utils.go Normal file
View File

@ -0,0 +1,31 @@
package dns
import (
"net"
"github.com/miekg/dns"
)
// ReadResolveConfig retrieve resolvers from os
func ReadResolveConfig(configFile string) ([]string, error) {
var servers []string
conf, err := dns.ClientConfigFromFile(configFile)
if err != nil {
return servers, err
}
for _, nameserver := range conf.Servers {
if nameserver[0] == '[' && nameserver[len(nameserver)-1] == ']' {
nameserver = nameserver[1 : len(nameserver)-1]
}
if ip := net.ParseIP(nameserver); ip != nil {
nameserver = net.JoinHostPort(nameserver, defaultPort)
} else {
nameserver = dns.Fqdn(nameserver) + ":" + defaultPort
}
servers = append(servers, nameserver)
}
return servers, nil
}

12
common/stringz/stringz.go Normal file
View File

@ -0,0 +1,12 @@
package stringz
import "strings"
func TrimProtocol(URL string) string {
URL = strings.TrimSpace(URL)
if strings.HasPrefix(strings.ToLower(URL), "http://") || strings.HasPrefix(strings.ToLower(URL), "https://") {
URL = URL[strings.Index(URL, "//")+2:]
}
return URL
}

16
go.mod Normal file
View File

@ -0,0 +1,16 @@
module github.com/projectdiscovery/httpx
go 1.14
require (
github.com/cespare/xxhash v1.1.0 // indirect
github.com/coocood/freecache v1.1.0
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf
github.com/microcosm-cc/bluemonday v1.0.2
github.com/miekg/dns v1.1.29
github.com/projectdiscovery/gologger v1.0.0
github.com/projectdiscovery/retryablehttp-go v1.0.1
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.2.1
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2
)

47
go.sum Normal file
View File

@ -0,0 +1,47 @@
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/coocood/freecache v1.1.0 h1:ENiHOsWdj1BrrlPwblhbn4GdAsMymK3pZORJ+bJGAjA=
github.com/coocood/freecache v1.1.0/go.mod h1:ePwxCDzOYvARfHdr1pByNct1at3CoKnsipOHwKlNbzI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf h1:umfGUaWdFP2s6457fz1+xXYIWDxdGc7HdkLS9aJ1skk=
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf/go.mod h1:V99KdStnMHZsvVOwIvhfcUzYgYkRZeQWUtumtL+SKxA=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/projectdiscovery/gologger v1.0.0 h1:XAQ8kHeVKXMjY4rLGh7eT5+oHU077BNEvs7X6n+vu1s=
github.com/projectdiscovery/gologger v1.0.0/go.mod h1:Ok+axMqK53bWNwDSU1nTNwITLYMXMdZtRc8/y1c7sWE=
github.com/projectdiscovery/retryablehttp-go v1.0.1 h1:V7wUvsZNq1Rcz7+IlcyoyQlNwshuwptuBVYWw9lx8RE=
github.com/projectdiscovery/retryablehttp-go v1.0.1/go.mod h1:SrN6iLZilNG1X4neq1D+SBxoqfAF4nyzvmevkTkWsek=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

BIN
static/httpx-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB