├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | webhook -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jordan Wright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software ("Gophish Community Edition") and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webhook 2 | 3 | A simple webhook server for use with Gophish events. 4 | 5 | # Installation 6 | 7 | ``` 8 | go get github.com/gophish/gophish 9 | ``` 10 | 11 | # Usage 12 | 13 | ``` 14 | ./webhook --help 15 | usage: webhook [] 16 | 17 | Flags: 18 | --help Show context-sensitive help (also try --help-long and --help-man). 19 | -u, --path="/webhook" Webhook server path 20 | -p, --port="9999" Webhook server port 21 | -h, --server=127.0.0.1 Server address 22 | -s, --secret=SECRET Webhook secret 23 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gophish/webhook 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 7 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 8 | github.com/sirupsen/logrus v1.4.2 9 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 4 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 10 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 14 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 15 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 16 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 18 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "os" 15 | "strings" 16 | 17 | log "github.com/sirupsen/logrus" 18 | "gopkg.in/alecthomas/kingpin.v2" 19 | ) 20 | 21 | var ( 22 | serverPath = kingpin.Flag("path", "Webhook server path").Default("/webhook").Short('u').String() 23 | serverPort = kingpin.Flag("port", "Webhook server port").Default("9999").Short('p').String() 24 | serverIP = kingpin.Flag("server", "Server address").Default("127.0.0.1").Short('h').IP() 25 | secret = kingpin.Flag("secret", "Webhook secret").Short('s').String() 26 | 27 | errNoSignature = errors.New("No X-Gophish-Signature header provided") 28 | errInvalidSignature = errors.New("Invalid signature provided") 29 | ) 30 | 31 | func webhookHandler(w http.ResponseWriter, r *http.Request) { 32 | defer r.Body.Close() 33 | 34 | // Get the provided signature 35 | signatureHeader := r.Header.Get("X-Gophish-Signature") 36 | if signatureHeader == "" { 37 | log.Errorf("no signature provided in ruest from %s", r.RemoteAddr) 38 | http.Error(w, errNoSignature.Error(), http.StatusBadRequest) 39 | return 40 | } 41 | 42 | signatureParts := strings.SplitN(signatureHeader, "=", 2) 43 | if len(signatureParts) != 2 { 44 | log.Errorf("invalid signature: %s", signatureHeader) 45 | http.Error(w, errInvalidSignature.Error(), http.StatusBadRequest) 46 | return 47 | } 48 | signature := signatureParts[1] 49 | 50 | gotHash, err := hex.DecodeString(signature) 51 | if err != nil { 52 | http.Error(w, err.Error(), http.StatusBadRequest) 53 | } 54 | 55 | // Copy out the ruest body so we can validate the signature 56 | body, err := ioutil.ReadAll(r.Body) 57 | if err != nil { 58 | http.Error(w, err.Error(), http.StatusInternalServerError) 59 | return 60 | } 61 | 62 | // Validate the signature 63 | expectedMAC := hmac.New(sha256.New, []byte(*secret)) 64 | expectedMAC.Write(body) 65 | expectedHash := expectedMAC.Sum(nil) 66 | 67 | if !hmac.Equal(gotHash, expectedHash) { 68 | log.Errorf("invalid signature provided. expected %s got %s", hex.EncodeToString(expectedHash), signature) 69 | http.Error(w, errInvalidSignature.Error(), http.StatusBadRequest) 70 | return 71 | } 72 | 73 | // Print the request header information(taken from 74 | // net/http/httputil.DumpRequest) 75 | buf := &bytes.Buffer{} 76 | rURI := r.RequestURI 77 | if rURI == "" { 78 | rURI = r.URL.RequestURI() 79 | } 80 | 81 | fmt.Fprintf(buf, "%s %s HTTP/%d.%d\r\n", r.Method, 82 | rURI, r.ProtoMajor, r.ProtoMinor) 83 | 84 | absRequestURI := strings.HasPrefix(r.RequestURI, "http://") || strings.HasPrefix(r.RequestURI, "https://") 85 | if !absRequestURI { 86 | host := r.Host 87 | if host == "" && r.URL != nil { 88 | host = r.URL.Host 89 | } 90 | if host != "" { 91 | fmt.Fprintf(buf, "Host: %s\r\n", host) 92 | } 93 | } 94 | 95 | // Print out the payload 96 | r.Header.Write(buf) 97 | err = json.Indent(buf, body, "", " ") 98 | if err != nil { 99 | log.Error("error indenting json body: %v", err) 100 | http.Error(w, err.Error(), http.StatusInternalServerError) 101 | return 102 | } 103 | buf.WriteTo(os.Stdout) 104 | fmt.Print("\n") 105 | 106 | w.WriteHeader(http.StatusNoContent) 107 | } 108 | 109 | func main() { 110 | kingpin.Parse() 111 | addr := net.JoinHostPort(serverIP.String(), *serverPort) 112 | log.Infof("Webhook server started at %s%s", addr, *serverPath) 113 | http.ListenAndServe(addr, http.HandlerFunc(webhookHandler)) 114 | } 115 | --------------------------------------------------------------------------------