mirror of
https://github.com/gophish/gophish
synced 2024-11-14 16:27:23 +00:00
e3352f481e
Initial commit of SSRF mitigations. This fixes #1908 by creating a *net.Dialer which restricts outbound connections to only allowed IP ranges. This implementation is based on the blog post at https://www.agwa.name/blog/post/preventing_server_side_request_forgery_in_golang To keep things backwards compatible, by default we'll only block connections to 169.254.169.254, the link-local IP address commonly used in cloud environments to retrieve metadata about the running instance. For other internal addresses (e.g. localhost or RFC 1918 addresses), it's assumed that those are available to Gophish. To support more secure environments, we introduce the `allowed_internal_hosts` configuration option where an admin can set one or more IP ranges in CIDR format. If addresses are specified here, then all internal connections will be blocked except to these hosts. There are various bits about this approach I don't really like. For example, since various packages all need this functionality, I had to make the RestrictedDialer a global singleton rather than a dependency off of, say, the admin server. Additionally, since webhooks are implemented via a singleton, I had to introduce a new function, `SetTransport`. Finally, I had to make an update in the gomail package to support a custom net.Dialer.
123 lines
2.8 KiB
Go
123 lines
2.8 KiB
Go
package webhook
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
log "github.com/gophish/gophish/logger"
|
|
)
|
|
|
|
const (
|
|
|
|
// DefaultTimeoutSeconds is the number of seconds before a timeout occurs
|
|
// when sending a webhook
|
|
DefaultTimeoutSeconds = 10
|
|
|
|
// MinHTTPStatusErrorCode is the lower bound of HTTP status codes which
|
|
// indicate an error occurred
|
|
MinHTTPStatusErrorCode = 400
|
|
|
|
// SignatureHeader is the name of the HTTP header which contains the
|
|
// webhook signature
|
|
SignatureHeader = "X-Gophish-Signature"
|
|
|
|
// Sha256Prefix is the prefix that specifies the hashing algorithm used
|
|
// for the signature
|
|
Sha256Prefix = "sha256"
|
|
)
|
|
|
|
// Sender represents a type which can send webhooks to an EndPoint
|
|
type Sender interface {
|
|
Send(endPoint EndPoint, data interface{}) error
|
|
}
|
|
|
|
type defaultSender struct {
|
|
client *http.Client
|
|
}
|
|
|
|
var senderInstance = &defaultSender{
|
|
client: &http.Client{
|
|
Timeout: time.Second * DefaultTimeoutSeconds,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
},
|
|
}
|
|
|
|
// SetTransport sets the underlying transport for the default webhook client.
|
|
func SetTransport(tr *http.Transport) {
|
|
senderInstance.client.Transport = tr
|
|
}
|
|
|
|
// EndPoint represents a URL to send the webhook to, as well as a secret used
|
|
// to sign the event
|
|
type EndPoint struct {
|
|
URL string
|
|
Secret string
|
|
}
|
|
|
|
// Send sends data to a single EndPoint
|
|
func Send(endPoint EndPoint, data interface{}) error {
|
|
return senderInstance.Send(endPoint, data)
|
|
}
|
|
|
|
// SendAll sends data to multiple EndPoints
|
|
func SendAll(endPoints []EndPoint, data interface{}) {
|
|
for _, e := range endPoints {
|
|
go func(e EndPoint) {
|
|
senderInstance.Send(e, data)
|
|
}(e)
|
|
}
|
|
}
|
|
|
|
// Send contains the implementation of sending webhook to an EndPoint
|
|
func (ds defaultSender) Send(endPoint EndPoint, data interface{}) error {
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", endPoint.URL, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
signat, err := sign(endPoint.Secret, jsonData)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
req.Header.Set(SignatureHeader, fmt.Sprintf("%s=%s", Sha256Prefix, signat))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := ds.client.Do(req)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= MinHTTPStatusErrorCode {
|
|
errMsg := fmt.Sprintf("http status of response: %s", resp.Status)
|
|
log.Error(errMsg)
|
|
return errors.New(errMsg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sign(secret string, data []byte) (string, error) {
|
|
hash1 := hmac.New(sha256.New, []byte(secret))
|
|
_, err := hash1.Write(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hexStr := hex.EncodeToString(hash1.Sum(nil))
|
|
return hexStr, nil
|
|
}
|