Description

You’ve found a website that lets you input remote templates for rendering. Your task is to exploit this system’s vulnerabilities to access and retrieve a hidden flag. Good luck!

Reconnaissance

We had the program code written in Golang.

package main
 
import (
 "encoding/json"
 "fmt"
 "html/template"
 "io"
 "net/http"
 "os"
 "os/exec"
 "path/filepath"
 "strings"
)
 
const WEB_PORT = "1337"
const TEMPLATE_DIR = "./templates"
 
type LocationInfo struct {
 IpVersion     int     `json:"ipVersion"`
 IpAddress     string  `json:"ipAddress"`
 Latitude      float64 `json:"latitude"`
 Longitude     float64 `json:"longitude"`
 CountryName   string  `json:"countryName"`
 CountryCode   string  `json:"countryCode"`
 TimeZone      string  `json:"timeZone"`
 ZipCode       string  `json:"zipCode"`
 CityName      string  `json:"cityName"`
 RegionName    string  `json:"regionName"`
 Continent     string  `json:"continent"`
 ContinentCode string  `json:"continentCode"`
}
 
type MachineInfo struct {
 Hostname      string
 OS            string
 KernelVersion string
 Memory        string
}
 
type RequestData struct {
 ClientIP     string
 ClientUA     string
 ServerInfo   MachineInfo
 ClientIpInfo LocationInfo `json:"location"`
}
 
func (p RequestData) FetchServerInfo(command string) string {
 out, err := exec.Command("sh", "-c", command).Output()
 if err != nil {
  return ""
 }
 return string(out)
}
 
func (p RequestData) GetLocationInfo(endpointURL string) (*LocationInfo, error) {
 resp, err := http.Get(endpointURL)
 if err != nil {
  return nil, err
 }
 
 defer resp.Body.Close()
 
 if resp.StatusCode != http.StatusOK {
  return nil, fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode)
 }
 
 body, err := io.ReadAll(resp.Body)
 if err != nil {
  return nil, err
 }
 
 var locationInfo LocationInfo
 if err := json.Unmarshal(body, &locationInfo); err != nil {
  return nil, err
 }
 
 return &locationInfo, nil
}
 
func isSubdirectory(basePath, path string) bool {
 rel, err := filepath.Rel(basePath, path)
 if err != nil {
  return false
 }
 return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
 
func readFile(filepath string, basePath string) (string, error) {
 if !isSubdirectory(basePath, filepath) {
  return "", fmt.Errorf("Invalid filepath")
 }
 
 data, err := os.ReadFile(filepath)
 if err != nil {
  return "", err
 }
 return string(data), nil
}
 
func readRemoteFile(url string) (string, error) {
 response, err := http.Get(url)
 if err != nil {
  return "", err
 }
 
 defer response.Body.Close()
 
 if response.StatusCode != http.StatusOK {
  return "", fmt.Errorf("HTTP request failed with status code: %d", response.StatusCode)
 }
 
 content, err := io.ReadAll(response.Body)
 if err != nil {
  return "", err
 }
 
 return string(content), nil
}
 
func getIndex(w http.ResponseWriter, r *http.Request) {
 http.Redirect(w, r, "/render?page=index.tpl", http.StatusMovedPermanently)
}
 
func getTpl(w http.ResponseWriter, r *http.Request) {
 var page string = r.URL.Query().Get("page")
 var remote string = r.URL.Query().Get("use_remote")
 
 if page == "" {
  http.Error(w, "Missing required parameters", http.StatusBadRequest)
  return
 }
 
 reqData := &RequestData{}
 
 userIPCookie, err := r.Cookie("user_ip")
 clientIP := ""
 
 if err == nil {
  clientIP = userIPCookie.Value
 } else {
  clientIP = strings.Split(r.RemoteAddr, ":")[0]
 }
 
 userAgent := r.Header.Get("User-Agent")
 
 locationInfo, err := reqData.GetLocationInfo("https://freeipapi.com/api/json/" + clientIP)
 
 if err != nil {
  fmt.Println(err)
  http.Error(w, "Could not fetch IP location info", http.StatusInternalServerError)
  return
 }
 
 reqData.ClientIP = clientIP
 reqData.ClientUA = userAgent
 reqData.ClientIpInfo = *locationInfo
 reqData.ServerInfo.Hostname = reqData.FetchServerInfo("hostname")
 reqData.ServerInfo.OS = reqData.FetchServerInfo("cat /etc/os-release | grep PRETTY_NAME | cut -d '\"' -f 2")
 reqData.ServerInfo.KernelVersion = reqData.FetchServerInfo("uname -r")
 reqData.ServerInfo.Memory = reqData.FetchServerInfo("free -h | awk '/^Mem/{print $2}'")
 
 var tmplFile string
 
 if remote == "true" {
  tmplFile, err = readRemoteFile(page)
 
  if err != nil {
   http.Error(w, "Internal Server Error", http.StatusInternalServerError)
   return
  }
 } else {
  tmplFile, err = readFile(TEMPLATE_DIR+"/"+page, "./")
 
  if err != nil {
   http.Error(w, "Internal Server Error", http.StatusInternalServerError)
   return
  }
 }
 
 tmpl, err := template.New("page").Parse(tmplFile)
 if err != nil {
  http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  return
 }
 
 err = tmpl.Execute(w, reqData)
 if err != nil {
  http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  return
 }
}
 
func main() {
 mux := http.NewServeMux()
 
 mux.HandleFunc("/", getIndex)
 mux.HandleFunc("/render", getTpl)
 mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
 
 fmt.Println("Server started at port " + WEB_PORT)
 http.ListenAndServe(":"+WEB_PORT, mux)
}

Pay closer look to the highlighted code snippet.

This function allows us to execute shell commands. The program also has a function to read files from the local filesystem and from remote URLs.

Exploitation

We can create a template that executes commands on the server by using go templating code.

But we cannot type directly into the input field. We need to host the template on a remote server. So I decided to use Webhook.site to host the template.

Replace the command to find the flag:

Conclusion

This challenge was a good exercise in exploiting SSRF vulnerabilities. I learned how to use go templating code to execute commands on the server. I also learned how to host templates on remote servers and use them to exploit SSRF vulnerabilities. I hope you enjoyed this write-up. Thanks for reading!