├── .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 |
--------------------------------------------------------------------------------