[
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.21-alpine AS build-env\n\nWORKDIR /build\nRUN go mod init ssrf-sheriff\nCOPY . .\nRUN go get -d -v ./...\nRUN go build -o ssrf-sheriff .\n\nFROM alpine:3.19\n\nWORKDIR /app\nCOPY --from=build-env /build/ssrf-sheriff /usr/local/bin/ssrf-sheriff\nCOPY config/base.example.yaml config/base.yaml\n\nENTRYPOINT [\"ssrf-sheriff\"]\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2019 Joel Margolis\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# SSRF Sheriff\n\nThis is an SSRF testing sheriff written in Go. It was originally created for the [Uber H1-4420 2019 London Live Hacking Event](https://www.hackerone.com/blog/london-called-hackers-answered-recapping-h1-4420), but it is now being open-sourced for other organizations to implement and contribute back to.\n\n\n## Features\n\n- Respond to any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.)\n- Configurable secret token (see [base.example.yaml](config/base.example.yaml))\n- Content-specific responses\n  - With secret token in response body\n    - JSON\n    - XML\n    - HTML\n    - CSV\n    - TXT\n    - PNG\n    - JPEG\n  - Without token in response body\n    - GIF\n    - MP3\n    - MP4\n\n## Usage\n\n```bash\ngo get github.com/teknogeek/ssrf-sheriff\ncd $GOPATH/src/github.com/teknogeek/ssrf-sheriff\ncp config/base.example.yaml config/base.yaml\n\n# ... configure ...\n\ngo run main.go\n```\n\n### Example Requests:\n\n**Plaintext**\n```\n$ curl -sSD- http://127.0.0.1:8000/foobar\nHTTP/1.1 200 OK\nContent-Type: text/plain\nX-Secret-Token: SUP3R_S3cret_1337_K3y\nDate: Mon, 14 Oct 2019 16:37:36 GMT\nContent-Length: 21\n\nSUP3R_S3cret_1337_K3y\n```\n\n**XML**\n```\n$ curl -sSD- http://127.0.0.1:8000/foobar.xml\nHTTP/1.1 200 OK\nContent-Type: application/xml\nX-Secret-Token: SUP3R_S3cret_1337_K3y\nDate: Mon, 14 Oct 2019 16:37:41 GMT\nContent-Length: 81\n\n<SerializableResponse><token>SUP3R_S3cret_1337_K3y</token></SerializableResponse>\n```\n\n## TODO\n\n- Dynamically generate valid responses with the secret token visible for\n  - GIF\n  - MP3\n  - MP4\n- Secrets in HTTP response generated/created/signed per-request, instead of returning a single secret for all requests\n- TLS support\n\n## Credit\n\nInspired (and requested) by [Frans Rosén](https://twitter.com/fransrosen) during his [talk at BountyCon '19 Singapore](https://speakerdeck.com/fransrosen/live-hacking-like-a-mvh-a-walkthrough-on-methodology-and-strategies-to-win-big?slide=49)\n\n\n-----\n\nReleased under the [MIT License](LICENSE.txt).\n\n\n"
  },
  {
    "path": "config/base.example.yaml",
    "content": "http:\n  address: \":8000\"\n\nssrf_token: \"REPLACE_THIS_WITH_YOUR_SECRET_VALUE\"\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  ssrf_sheriff:\n    build: .\n    ports:\n      - \"8000:8000\"\n"
  },
  {
    "path": "generators/images.go",
    "content": "package generators\n\nimport (\n\t\"github.com/fogleman/gg\"\n\t\"github.com/golang/freetype/truetype\"\n\t\"golang.org/x/image/font/gofont/goregular\"\n)\n\n// function that generates JPG and PNG images with the provided text\n// and save them into \"/templates\" directory\nfunc GenerateJPGAndPNG(ssrfToken string) {\n\tconst W = 1024\n\tconst H = 768\n\n\tdc := gg.NewContext(W, H)\n\tdc.SetRGB(0, 0, 0)\n\tdc.Clear()\n\tdc.SetRGB(1, 1, 1)\n\tfont, err := truetype.Parse(goregular.TTF)\n\tif err != nil {\n\t\tpanic(\"\")\n\t}\n\tface := truetype.NewFace(font, &truetype.Options{\n\t\tSize: 14,\n\t})\n\tdc.SetFontFace(face)\n\tdc.DrawStringAnchored(ssrfToken,  W/2, H/2, 0.5, 0.5)\n\n\n\tdc.SaveJPG(\"./templates/jpeg.jpg\", 80)\n\tdc.SavePNG(\"./templates/png.png\")\n}"
  },
  {
    "path": "generators/init.go",
    "content": "package generators\n\n// function that run all media files generators with the provided text\nfunc InitMediaGenerators(ssrfToken string)  {\n\tGenerateJPGAndPNG(ssrfToken)\n}\n"
  },
  {
    "path": "handler/handler.go",
    "content": "package handler\n\nimport (\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"mime\"\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/teknogeek/ssrf-sheriff/generators\"\n\t\"github.com/teknogeek/ssrf-sheriff/httpserver\"\n\t\"go.uber.org/config\"\n\t\"go.uber.org/fx\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// SerializableResponse is a generic type which both can be safely serialized to both XML and JSON\ntype SerializableResponse struct {\n\tSecretToken string `json:\"token\" xml:\"token\"`\n}\n\n// SSRFSheriffRouter is a wrapper around mux.Router to handle HTTP requests to the sheriff, with logging\ntype SSRFSheriffRouter struct {\n\tlogger    *zap.Logger\n\tssrfToken string\n}\n\n// NewHTTPServer provides a new HTTP server listener\nfunc NewHTTPServer(\n\tmux *mux.Router,\n\tcfg config.Provider,\n) *http.Server {\n\n\treturn &http.Server{\n\t\tAddr:    cfg.Get(\"http.address\").String(),\n\t\tHandler: mux,\n\t}\n}\n\n// NewSSRFSheriffRouter returns a new SSRFSheriffRouter which is used to route and handle all HTTP requests\nfunc NewSSRFSheriffRouter(\n\tlogger *zap.Logger,\n\tcfg config.Provider,\n) *SSRFSheriffRouter {\n\treturn &SSRFSheriffRouter{\n\t\tlogger:    logger,\n\t\tssrfToken: cfg.Get(\"ssrf_token\").String(),\n\t}\n}\n\n// StartFilesGenerator starts the function which is dynamically generating JPG/PNG formats\n// with the secret token rendered in the media\nfunc StartFilesGenerator(cfg config.Provider) {\n\tgenerators.InitMediaGenerators(cfg.Get(\"ssrf_token\").String())\n}\n\n// StartServer starts the HTTP server\nfunc StartServer(server *http.Server, lc fx.Lifecycle) {\n\th := httpserver.NewHandle(server)\n\tlc.Append(fx.Hook{\n\t\tOnStart: h.Start,\n\t\tOnStop:  h.Shutdown,\n\t})\n}\n\n// PathHandler is the main handler for all inbound requests\nfunc (s *SSRFSheriffRouter) PathHandler(w http.ResponseWriter, r *http.Request) {\n\tfileExtension := filepath.Ext(r.URL.Path)\n\tcontentType := mime.TypeByExtension(fileExtension)\n\tvar response string\n\n\tswitch fileExtension {\n\tcase \".json\":\n\t\tres, _ := json.Marshal(SerializableResponse{SecretToken: s.ssrfToken})\n\t\tresponse = string(res)\n\tcase \".xml\":\n\t\tres, _ := xml.Marshal(SerializableResponse{SecretToken: s.ssrfToken})\n\t\tresponse = string(res)\n\tcase \".html\":\n\t\ttmpl := readTemplateFile(\"html.html\")\n\t\tresponse = fmt.Sprintf(tmpl, s.ssrfToken, s.ssrfToken)\n\tcase \".csv\":\n\t\ttmpl := readTemplateFile(\"csv.csv\")\n\t\tresponse = fmt.Sprintf(tmpl, s.ssrfToken)\n\tcase \".txt\":\n\t\tresponse = fmt.Sprintf(\"token=%s\", s.ssrfToken)\n\tcase \".png\":\n\t\tresponse = readTemplateFile(\"png.png\")\n\tcase \".jpg\", \".jpeg\":\n\t\tresponse = readTemplateFile(\"jpeg.jpg\")\n\t// TODO: dynamically generate these formats with the secret token rendered in the media\n\tcase \".gif\":\n\t\tresponse = readTemplateFile(\"gif.gif\")\n\tcase \".mp3\":\n\t\tresponse = readTemplateFile(\"mp3.mp3\")\n\tcase \".mp4\":\n\t\tresponse = readTemplateFile(\"mp4.mp4\")\n\tdefault:\n\t\tresponse = s.ssrfToken\n\t}\n\n\tif contentType == \"\" {\n\t\tcontentType = \"text/plain\"\n\t}\n\n\ts.logger.Info(\"New inbound HTTP request\",\n\t\tzap.String(\"IP\", r.RemoteAddr),\n\t\tzap.String(\"Path\", r.URL.Path),\n\t\tzap.String(\"Response Content-Type\", contentType),\n\t\tzap.Any(\"Request Headers\", r.Header),\n\t)\n\n\tresponseBytes := []byte(response)\n\tw.Header().Set(\"Content-Type\", contentType)\n\tw.Header().Set(\"X-Secret-Token\", s.ssrfToken)\n\tw.WriteHeader(http.StatusOK)\n\tw.Write(responseBytes)\n}\n\nfunc readTemplateFile(templateFileName string) string {\n\tdata, err := ioutil.ReadFile(path.Join(\"templates\", path.Clean(templateFileName)))\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(data)\n}\n\n// NewServerRouter returns a new mux.Router for handling any HTTP request to /.*\nfunc NewServerRouter(s *SSRFSheriffRouter) *mux.Router {\n\trouter := mux.NewRouter()\n\trouter.PathPrefix(\"/\").HandlerFunc(s.PathHandler)\n\treturn router\n}\n\n// NewConfigProvider returns a config.Provider for YAML configuration\nfunc NewConfigProvider() (config.Provider, error) {\n\treturn config.NewYAMLProviderFromFiles(\"config/base.yaml\")\n}\n\n// NewLogger returns a new *zap.Logger\nfunc NewLogger() (*zap.Logger, error) {\n\tzapConfig := zap.NewProductionConfig()\n\tzapConfig.Encoding = \"console\"\n\tzapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder\n\tzapConfig.DisableStacktrace = true\n\n\treturn zapConfig.Build()\n}\n"
  },
  {
    "path": "httpserver/handle.go",
    "content": "package httpserver\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n)\n\n// HandleOption customizes the behavior of a Handle.\ntype HandleOption interface {\n\tapply(*Handle)\n}\n\ntype handleOptionFunc func(*Handle)\n\nfunc (f handleOptionFunc) apply(h *Handle) { f(h) }\n\n// ListenFunc is an option for Handle that allows changing how it listens for\n// incoming connections.\nfunc ListenFunc(f func(string, string) (net.Listener, error)) HandleOption {\n\treturn handleOptionFunc(func(h *Handle) {\n\t\th.listenFunc = f\n\t})\n}\n\n// DefaultListenFunc builds a net.Listener with the given network and address.\n// This function is the default value for ListenFunc.\nfunc DefaultListenFunc(network, address string) (net.Listener, error) {\n\tln, err := net.Listen(network, address)\n\n\t// keep-alive on all TCP connections. net/http's ListenAndServe and\n\t// ListenAndServeTLS do this by default but not Server.Serve(..).\n\tif tcpListener, ok := ln.(*net.TCPListener); ok {\n\t\tln = tcpKeepAliveListener{tcpListener}\n\t}\n\n\treturn ln, err\n}\n\nfunc newDialer() dialer { return new(net.Dialer) }\n\n// Changes how we build dialers.\n//\n// This is an unexported option used for testing only.\nfunc newDialerFunc(f func() dialer) HandleOption {\n\treturn handleOptionFunc(func(h *Handle) {\n\t\th.newDialerFunc = f\n\t})\n}\n\n// Handle is a reference to an HTTP server. It provides clean startup and\n// shutdown for net/http HTTP servers.\ntype Handle struct {\n\t// HTTP server provided by the user.\n\tsrv *http.Server\n\n\t// Listener we're listening on (if any). This is nil if Start hasn't been\n\t// called yet.\n\tln net.Listener\n\n\t// errCh will be filled with the error returned by http.Server.Serve.\n\terrCh chan error\n\n\t// Function used to create net.Listeners. Defaults to net.Listen.\n\tlistenFunc func(string, string) (net.Listener, error)\n\n\t// Function used to build dialers. Defaults to newDialer.\n\tnewDialerFunc func() dialer\n}\n\n// NewHandle builds a Handle to the given HTTP server. You can use the\n// returned Handle to start the server and access information about the\n// running server.\n//\n// Handle must be used for all server operations from this point onwards.\n// Starting or stopping the http.Server directly will lead to undefined\n// behavior.\n//\n// Note that Handle is not thread-safe. You must not call procedures on Handle\n// concurrently.\nfunc NewHandle(srv *http.Server, opts ...HandleOption) *Handle {\n\th := &Handle{\n\t\tsrv:           srv,\n\t\tlistenFunc:    DefaultListenFunc,\n\t\tnewDialerFunc: newDialer,\n\t}\n\n\tfor _, opt := range opts {\n\t\topt.apply(h)\n\t}\n\n\treturn h\n}\n\n// Addr returns the address on which the HTTP server is listening. This can be\n// used to determine the address of the server if it was started on an\n// OS-assigned port (\":0\").\n//\n// Returns nil if the server hasn't been started yet.\nfunc (h *Handle) Addr() net.Addr {\n\tif h.ln == nil {\n\t\treturn nil\n\t}\n\treturn h.ln.Addr()\n}\n\n// Start starts the HTTP server for this Handle in a separate goroutine and\n// blocks until the server is ready to accept requests or the provided context\n// finishes.\n//\n// The server is started on the address defined on Server.Addr, defaulting to\n// an OS-assigned port (\":0\") if Server.Addr is empty.\n//\n//   h := httpserver.NewHandle(&http.Server{Handler: myHandler})\n//   err := h.Start(ctx)\n//\n// Note that because the server is started in a separate goroutine, this\n// method is safe to use as-is inside Fx Lifecycle hooks.\n//\n//   fx.Hook{\n//     OnStart: handle.Start,\n//     OnStop: handle.Shutdown,\n//   }\nfunc (h *Handle) Start(ctx context.Context) error {\n\tif h.ln != nil {\n\t\treturn errors.New(\"server is already running\")\n\t}\n\n\t// http.Server defaults to \":http\" if Addr is empty. For our purposes,\n\t// \":0\" is more desirable since we almost never listen on port 80.\n\taddr := h.srv.Addr\n\tif addr == \"\" {\n\t\taddr = \":0\"\n\t}\n\n\t// Most errors that occur when starting an http.Server are actually Listen\n\t// errors. If we encounter one of those, we can abort immediately.\n\tln, err := h.listenFunc(\"tcp\", addr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error starting HTTP server on %q: %v\", addr, err)\n\t}\n\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\t// Serve blocks until it encounters an error or until the server shuts\n\t\t// down, so we need to call it in a separate goroutine. Errors here\n\t\t// (apart from http.ErrServerClosed) are rare.\n\t\terr := h.srv.Serve(ln)\n\t\terrCh <- err\n\n\t\t// Close the channel so that if shutdown is called on this Handle\n\t\t// again, it doesn't wait on the channel indefinitely.\n\t\tclose(errCh)\n\t}()\n\n\t// We wait until the server is ready to process requests.\n\t//\n\t// We would normally be able to return after starting the listener but\n\t// that introduces a very annoying race condition:\n\t//\n\t// Consider,\n\t//\n\t//   err := h.Start(..)\n\t//   h.Shutdown(ctx)\n\t//\n\t// If srv.Shutdown gets invoked before the goroutine that is calling\n\t// srv.Serve has transitioned the server to the running state,\n\t// srv.Shutdown will return right away but srv.Serve will run forever.\n\td := h.newDialerFunc()\n\tif err := waitUntilAvailable(ctx, d, ln.Addr().String()); err != nil {\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\t// If the server failed to start up, errCh probably has a more\n\t\t\t// helpful error.\n\t\t\treturn fmt.Errorf(\"error starting HTTP server: %v\", err)\n\t\tdefault:\n\t\t\t// Kill the listener if we failed to start the server up.\n\t\t\t//\n\t\t\t// We don't need to do this for the errCh path because having a\n\t\t\t// value in errCh indicates that Serve finished running, and Serve\n\t\t\t// always closes the listener.\n\t\t\tln.Close()\n\t\t\treturn wrapNetErr(err, \"error waiting for server to start up\")\n\t\t}\n\t}\n\n\th.errCh = errCh\n\th.ln = ln\n\treturn nil\n}\n\n// Shutdown initiates a graceful shutdown of the HTTP server. The provided\n// context controls how long we are willing to wait for the server to shut\n// down. Shutdown will block until the server has shut down completely or\n// until the context finishes.\nfunc (h *Handle) Shutdown(ctx context.Context) error {\n\tif err := h.srv.Shutdown(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tif err := <-h.errCh; err != http.ErrServerClosed {\n\t\treturn err\n\t}\n\n\th.ln = nil\n\treturn nil\n}\n"
  },
  {
    "path": "httpserver/tcp_listener.go",
    "content": "package httpserver\n\nimport (\n\t\"net\"\n\t\"time\"\n)\n\n// Copied from https://github.com/golang/go/blob/fcee1897767c0cfa6e13a843fe5ee5d1deb8081b/src/net/http/server.go#L3156-L3172\n\n// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted\n// connections. It's used by ListenAndServe and ListenAndServeTLS so\n// dead TCP connections (e.g. closing laptop mid-download) eventually\n// go away.\ntype tcpKeepAliveListener struct {\n\t*net.TCPListener\n}\n\nfunc (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {\n\ttc, err := ln.AcceptTCP()\n\tif err != nil {\n\t\treturn\n\t}\n\ttc.SetKeepAlive(true)\n\ttc.SetKeepAlivePeriod(3 * time.Minute)\n\treturn tc, nil\n}\n"
  },
  {
    "path": "httpserver/wait.go",
    "content": "package httpserver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n)\n\nvar _invalidHTTPRequestLine = []byte(\"INVALID\\n\\n\")\n\n// Subset of the net.Dialer API that we care about.\ntype dialer interface {\n\tDialContext(context.Context, string, string) (net.Conn, error)\n}\n\nvar _ dialer = (*net.Dialer)(nil)\n\n// waitUntilAvailable uses the given dialer to connect to the HTTP server at\n// the provided address and waits until the server is ready to accept requests\n// or the given context times out.\n//\n// This works by sending an invalid request line to the server and waiting for\n// a response. The request line is the \"GET /index.html HTTP/1.1\" part of an\n// HTTP request. Instead of sending a valid one which could end up calling the\n// user-provided request handler, we send one that will be rejected by the\n// HTTP server implementation without crashing.\nfunc waitUntilAvailable(ctx context.Context, d dialer, addr string) error {\n\tconn, err := d.DialContext(ctx, \"tcp\", addr)\n\tif err != nil {\n\t\treturn wrapNetErr(err, \"failed to dial to %q\", addr)\n\t}\n\tdefer conn.Close()\n\n\tif deadline, ok := ctx.Deadline(); ok {\n\t\t// DialContext applies the timeout only to establishing the\n\t\t// connection. Here we're applying the same deadline to the rest of\n\t\t// this TCP conversation.\n\t\tif err := conn.SetDeadline(deadline); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set connection deadline to %v: %v\", deadline, err)\n\t\t}\n\t}\n\n\tif _, err := conn.Write(_invalidHTTPRequestLine); err != nil {\n\t\treturn wrapNetErr(err, \"failed to write request to server\")\n\t}\n\n\t// Once we receive a single byte from the server, we know that the server\n\t// is processing HTTP requests.\n\tvar out [1]byte\n\tif _, err := conn.Read(out[:]); err != nil {\n\t\treturn wrapNetErr(err, \"failed to read response from server\")\n\t}\n\n\treturn nil\n}\n\n// Similar to fmt.Errorf except net.Error timeouts are translated to\n// context.DeadlineExceeded.\nfunc wrapNetErr(err error, msg string, args ...interface{}) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tif ne, ok := err.(net.Error); ok && ne.Timeout() {\n\t\treturn context.DeadlineExceeded\n\t}\n\n\tif len(args) > 0 {\n\t\tmsg = fmt.Sprintf(msg, args...)\n\t}\n\n\treturn fmt.Errorf(\"%s: %v\", msg, err)\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"github.com/teknogeek/ssrf-sheriff/handler\"\n\t\"go.uber.org/fx\"\n)\n\nfunc main() {\n\tfx.New(opts()).Run()\n}\n\nfunc opts() fx.Option {\n\treturn fx.Options(\n\t\tfx.Provide(\n\t\t\thandler.NewLogger,\n\t\t\thandler.NewConfigProvider,\n\t\t\thandler.NewSSRFSheriffRouter,\n\t\t\thandler.NewServerRouter,\n\t\t\thandler.NewHTTPServer,\n\t\t),\n\t\tfx.Invoke(handler.StartFilesGenerator, handler.StartServer),\n\t)\n}\n"
  },
  {
    "path": "templates/csv.csv",
    "content": "key,value\ntoken,%s\n"
  },
  {
    "path": "templates/html.html",
    "content": "<!DOCTYPE html><html><head><title>token=%s</title></head><body>token=%s</body></html>\n"
  }
]