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.
Prompt:
- Enter the base64-encoded value of the key-encrypting-key
tok
value you generated in Task 7server.py
and util.py
files you collected in Task 4. Use the script at the end of the Task 4 page if you need to download the files.When we look at app/server.py
, we can see the key encryption function being called:
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})
It looks like a binary file named keyMaster
is fed several arguments to generate an plaintext key using the following data:
str(cid)
= random.randrange(10000,100000)
demand
argument (A.K.A. the payment demand)username
We can also see that a keygeneration.log
is used to store additional information.
What we get from the log file:
username
str(cid)
demand
argumentSince we’re logged in as an Admin, we can access the log feed. Let’s take a look at the contents of the log file:
INPUT:
cat keygeneration.log
OUTPUT:
2021-01-18T22:35:18-05:00 SadRope 24040 0.677
2021-01-27T08:52:23-05:00 GutturalHops 13442 1.635
2021-01-31T13:24:57-05:00 WomanlyStarboard 30499 8.203
2021-02-13T04:08:24-05:00 MotionlessFuneral 15158 0.886
2021-02-16T00:25:59-05:00 WickedPrescription 26170 8.977
2021-02-18T11:30:34-05:00 WickedPrescription 48869 4.793
2021-02-22T12:00:53-05:00 AromaticSolidarity 10957 2.618
2021-03-04T02:46:57-05:00 RoomyFoodstuffs 38914 7.7379999999999995
2021-03-06T10:24:34-05:00 RoomyFoodstuffs 34087 6.555
2021-03-10T07:33:00-05:00 FeignedCornet 33955 8.9
2021-03-17T06:18:57-04:00 WomanlyStarboard 21027 4.552
2021-04-07T01:38:23-04:00 WomanlyStarboard 43363 5.591
2021-04-10T16:13:52-04:00 WickedPrescription 24286 7.7620000000000005
2021-04-10T17:43:22-04:00 HungrySpecialty 30278 4.218
2021-04-21T02:53:38-04:00 BillowyLadder 23839 1.026
2021-04-23T21:34:25-04:00 UnaccountableBolero 17084 9.485
2021-05-02T07:14:26-04:00 QuizzicalWashcloth 28038 4.948
2021-05-09T08:54:11-04:00 RoomyFoodstuffs 43140 4.823
2021-06-20T08:00:18-04:00 UnaccountableBolero 17727 3.9
2021-06-30T14:19:55-04:00 KnowingConnection 18680 6.526
2021-07-02T10:31:45-04:00 AttractiveWhorl 44861 4.822
2021-07-03T17:00:11-04:00 WomanlyStarboard 41374 1.9889999999999999
2021-07-15T01:23:33-04:00 WomanlyStarboard 26227 3.5709999999999997
2021-07-22T16:56:46-04:00 KnowingConnection 39091 4.662
2021-07-23T14:28:33-04:00 QuizzicalWashcloth 39828 7.083
2021-08-04T15:45:25-04:00 QuizzicalWashcloth 15276 6.376
2021-08-07T05:48:05-04:00 GruesomeEngine 36680 4.35
2021-08-07T07:47:40-04:00 WomanlyStarboard 48839 4.853
2021-08-13T08:19:49-04:00 KnowingConnection 25780 0.442
2021-09-13T18:46:22-04:00 GutturalHops 13602 1.429
2021-09-16T04:24:02-04:00 KnowingConnection 26197 2.67
2021-10-02T07:34:53-04:00 WickedPrescription 36838 0.416
2021-10-08T06:03:01-04:00 AromaticSolidarity 28849 0.553
2021-10-10T19:06:20-04:00 GruesomeEngine 20772 0.17
2021-10-13T12:30:23-04:00 KnowingConnection 49990 9.518
2021-10-19T06:18:25-04:00 BillowyLadder 30455 5.68
2021-11-03T18:58:27-04:00 QuizzicalWashcloth 23491 1.7650000000000001
2021-11-12T01:31:00-05:00 RoomyFoodstuffs 23457 9.086
2021-11-12T02:18:17-05:00 HungrySpecialty 10388 1.189
2021-11-12T08:35:58-05:00 WomanlyStarboard 28303 9.958
2021-11-20T22:05:45-05:00 BillowyLadder 46612 8.074
2021-11-25T09:37:45-05:00 AromaticSolidarity 25096 6.106
2021-11-29T04:21:45-05:00 RoomyFoodstuffs 35955 4.318
2021-11-29T11:00:36-05:00 WickedPrescription 19909 3.318
2021-12-06T04:55:58-05:00 BillowyLadder 32545 0.372
2021-12-12T16:32:14-05:00 MotionlessFuneral 44971 5.373
2021-12-12T21:44:54-05:00 UnaccountableBolero 10823 3.782
2021-12-14T22:18:43-05:00 SadRope 49207 7.572
2021-12-16T09:03:34-05:00 WomanlyStarboard 43084 0.279
2021-12-22T23:52:13-05:00 AromaticSolidarity 23359 9.345
2022-01-02T02:19:16-05:00 SadRope 31498 3.344
2022-01-10T14:19:18-05:00 SadRope 18686 8.018
2022-01-15T23:50:45-05:00 MotionlessFuneral 21232 1.077
2022-01-21T08:31:40-05:00 QuizzicalWashcloth 11236 7.946
2022-01-23T21:06:56-05:00 GruesomeEngine 27440 9.129
2022-01-25T06:24:21-05:00 KnowingConnection 44933 4.5600000000000005
2022-01-27T16:15:18-05:00 KnowingConnection 33263 9.235
2022-01-29T04:46:03-05:00 SadRope 36206 7.029
2022-01-31T12:54:21-05:00 AttractiveWhorl 26520 7.786
2022-01-31T17:50:49-05:00 UnaccountableBolero 41276 8.474
2022-01-31T22:56:01-05:00 AromaticSolidarity 15514 5.093
2022-02-02T08:13:58-05:00 AttractiveWhorl 95137 1.194
2022-02-08T08:15:25-05:00 WickedPrescription 48513 8.686
2022-03-12T21:27:55-05:00 AromaticSolidarity 47856 4.165
2022-04-20T18:40:05-05:00 SadRope 20868 5.871
2022-06-19T17:50:36-05:00 HungrySpecialty 38090 3.006
2022-06-20T12:42:53-05:00 AromaticSolidarity 33609 1.6400000000000001
2022-07-04T11:10:23-05:00 QuizzicalWashcloth 45496 0.48
This is all great, but we still don’t have the plainKey
. Since the binary keyMaster
seems to be a critical part of the key generation process, we need to find a way to download and reverse engineer it.
Upon inspecting the app/server.py
file again, we discover a very helpful local file inclusion vulnerability we could exploit to download the keymaster
file:
def fetchlog():
log = request.args.get('log')
return send_file("/opt/ransommethis/log/" + log)
The log page seems to create a query argument for log
and then provide a value for the file location. With this knowledge, we could redirect to the /opt/keyMaster/
folder with a new path of ../../keyMaster/keyMaster
.
Here’s what that URL looks like in total:
https://wrfbgtsocesalacv.ransommethis.net/fwjvwewwfiqfvfgp/fetchlog?log=../../keyMaster/keyMaster
Success!!! Looks like we can download and run the binary.
NOTE: Don’t forget to use this same technique to download other important files like victim.db
, user.db
, and keygeneration.log
. These files are important later on in Task 9.
Let’s try running it now with some dummy values.
INPUT:
./keyMaster "lock" "10000" "65" "AttractiveWhorl"
OUTPUT:
{"error":"no such table: hackers"}
This is a bit of a problem, since we don’t have a database. Just by chance, I looked to see what was in the folder:
INPUT:
ls -al
OUTPUT:
total 4576
drwxr-xr-x 2 kali kali 4096 Oct 8 22:19 .
drwxr-xr-x 28 kali kali 4096 Oct 8 22:19 ..
-rwxr-x--- 1 kali kali 4677512 Oct 8 22:19 keyMaster
-rw-r--r-- 1 kali kali 0 Oct 8 22:19 keyMaster.db
When running the binary with arguments keyMaster "lock" "10000" "65" "username"
, an empty file called keyMaster.db
was created. It is quite possible that this database already exists on the server. We can use the same local file inclusion exploit as ealier to download the keyMaster.db
file.
https://wrfbgtsocesalacv.ransommethis.net/fwjvwewwfiqfvfgp/fetchlog?log=../../keyMaster/keyMaster.db
Success!!! We now have the keyMaster database downloaded to our folder. Let’s open it!
sqlitebrowser keyMaster.db
When examining the customer
table of the keyMaster.db
, we find values for the customerId
, encryptedKey
, expectedPayment
, hackerName
, and creationDate
fields.
Let’s run the binary again and see what happens now: INPUT:
./keyMaster "lock" "10000" "65" "AttractiveWhorl"
OUTPUT:
{"error":"Insufficient credit. Please contact an administrator to reload."}
It is very likely that the “Insufficient credit” prompt is linked to the database we just downloaded. Let’s open it up again and look at some of the other tables.
In order to get our script to work, it looks like we need to modify the credit
value in the database to a value greater than zero. Let’s change it to 100
just to be safe.
INPUT:
./keyMaster "lock" "10000" "65" "AttractiveWhorl"
OUTPUT:
{"plainKey":"fafafd46-477a-11ed-8082-08002722","result":"ok"}
Success!!! Let’s see what happened in the database after a couple of runs:
This is fantastic, but we still don’t understand what the binary is doing.
Let’s try looking for strings inside the binary file first. Sometimes keys are left in the binary as plain text. We can do this with the strings
command:
INPUT:
strings keyMaster
We get a lot of data, but a very interesting set of strings catches my eye: OUTPUT:
path ransommethis.net/keymaster/cmd/keyMaster
mod ransommethis.net/keymaster (devel)
dep github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
dep github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
dep github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
dep golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
dep pkg/receipt v1.0.0
=> ./pkg/receipt (devel)
build -compiler=gc
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
This tells me several critical pieces of information:
jwt
and sqlite3
URLsFor our next step, let’s run the binary using strace -i
, which shows us the various system calls as well as their addresses:
INPUT:
rjamison@WindowsXP:~/keymaster$ strace -i ./keyMaster "lock" "10000" "1" "AttractiveWhorl"
OUTPUT:
[00007f0c8c37a1ab] execve("./keyMaster", ["./keyMaster", "lock", "10000", "1", "AttractiveWhorl"], 0x7ffd90701598 /* 49 vars */) = 0
...
[00000000004ae04a] openat(AT_FDCWD, "/etc/localtime", O_RDONLY) = 6
...
[00000000004adfdb] getrandom("\x5f\xbc\x1e\xf5\x25\x14\xe1\x67\xe7\x85\x5d\x4e\xe8\x09\x74\xec", 16, 0) = 16
...
[00007fe03066dad4] openat(AT_FDCWD, "/dev/urandom", O_RDONLY|O_CLOEXEC) = 9
...
[00007fe0305754ea] stat("/home/rjamison/keymaster/keyMaster.db-wal", 0x7ffd82177cb0) = -1 ENOENT (No such file or directory)
...
[00007fe03066dcbf] pwrite64(7, "SQLite format 3\0\20\0\1\1\0@ \0\0\0\5\0\0\0\5"..., 4096, 0) = 4096
...
[00007fe030577d0b] unlink("/home/rjamison/keymaster/keyMaster.db-journal") = 0
...
[00007fe0305754ea] stat("/home/rjamison/keymaster/keyMaster.db", {st_mode=S_IFREG|0750, st_size=20480, ...}) = 0
...
[00000000004adfdb] write(1, "{\"plainKey\":\"60781192-4430-11ed-"..., 62{"plainKey":"60781192-4430-11ed-b521-08002716","result":"ok"}
) = 62
[000000000046c0cb] exit_group(0) = ?
[????????????????] +++ exited with 0 +++
Several key things pop out at me:
keyMaster.db-wal
and keyMaster.db-journal
files being temporarily created during runtimesqlite3
callsplainKey
print is the last thing that happens before exit.Let’s open the binary in Ghidra
and see what pops out:
IMPORTANT
If you skip this instruction, all you will see are
trampoline
andsqlite3
functions.You will need special tools to reverse engineer this GO compiled binary in Ghidra. Golang binaries are notoriously massive and difficult to read. I found some excellent research and tools by Dorka Palotay. You will need to install the tools in your Ghidra plugin folder. After you run the initial decompile, you should run all of Dorka’s plugins in order.
After reviewing the source code, I determined that sqlite3 was a critical feature. Rather than stumbling through the decompiled output, I found the source code for sqlite3 and crypto online.
On the README.md
file for sqlite3
, we catch our first big break:
Password Encoding
The passwords within the user authentication module of SQLite are encoded with the SQLite function sqlite_cryp. This function uses a ceasar-cypher which is quite insecure. This library provides several additional password encoders which can be configured through the connection string.
The password cypher can be configured with the key _auth_crypt. And if the configured password encoder also requires an salt this can be configured with _auth_salt.
From here forward, I’m going to assume that sqlite3 for Go
on github is the library used to obfuscate the key-encrypting-key. If it is stored using a ceasar-cipher, it can easily be decrypted (or even read from the buffer). The only question is how many rounds?
A Caeser-Cipher is a type of substitution cipher in which each letter in the plaintext is replaced by a letter some fixed number of positions down the alphabet.
- Wikipedia
Because the key encrypts and decrypts, I had a hunch that there would be a related string in the binary.
DISCLAIMER
I am compressing days worth of reverse engineering, decompilation, and analysis for ease of reading. By no means did I know what I was doing until the very end of this process. I stepped through thousands of instructions in
pwngdb
just to understand how the functions worked.Do not be disillusioned. You need to take the time to read every binary you analyze. The path of discovery is never linear.
Now that I understand that I’m looking for an obscured string, I begin searching for hard-coded strings in functions. I began my search in the main
group. It didn’t take long until I discovered main.p4hsJ3KeOvw
, which had both a hard-coded string of q1xLzW588Stz+R/BJWj490sSeMj0om0D++pcndUBpww=
and a unique crypto function called golang.org/x/crypto/pbkdf2.Key
Here is the main.p4hsJ3KeOvw
function:
undefined8 main.p4hsJ3KeOvw(long param_1)
{
long lVar1;
undefined8 uVar2;
long unaff_R14;
undefined auStack144 [72];
ulong local_48;
char *local_40;
undefined8 local_38;
long local_30;
undefined *local_28;
undefined8 local_20;
undefined *local_18;
long local_10;
if (*(long **)(unaff_R14 + 0x10) <= &local_10 && &local_10 != *(long **)(unaff_R14 + 0x10)) {
register0x00000020 = (BADSPACEBASE *)auStack144;
local_38 = 0x2c;
local_20 = encoding/base64.(*Encoding).DecodeString();
if (param_1 != 0) {
return 0;
}
local_40 =
"q1xLzW588Stz+R/BJWj490sSeMj0om0D++pcndUBpww=" /* TRUNCATED STRING LITERAL */
;
local_48 = DAT_00852398;
local_30 = DAT_00852378;
local_28 = PTR_DAT_00852390;
local_18 = PTR_DAT_00852370;
local_10 = runtime.mallocgc();
runtime.memmove();
lVar1 = 0;
while( true ) {
if (local_30 <= lVar1) {
uVar2 = golang.org/x/crypto/pbkdf2.Key(local_20,local_40,local_10,local_30,local_38,0x1000);
return uVar2;
}
if (local_48 == 0) break;
if (local_48 <= (ulong)(lVar1 % (long)local_48)) {
runtime.panicIndex();
break;
}
*(byte *)(local_10 + lVar1) = *(byte *)(lVar1 + local_10) ^ local_28[lVar1 % (long)local_48];
lVar1 = lVar1 + 1;
}
runtime.panicdivide();
}
*(undefined8 *)((long)register0x00000020 + -8) = 0x5b85cb;
runtime.morestack_noctxt();
uVar2 = main.p4hsJ3KeOvw();
return uVar2;
}
pbkdf2
?The Password-Based Key Derivation Function 2 (pbkdf2)
standard is a simple way repeating a process or function multiple times to create a key. This technique is really good at protecting readable passwords from rainbow tables and brute force attacks, however, it is really BAD if you hard-coded your password into a file. The key
function used in the keymaster
binary includes the following description:
###func Key
func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte
Key derives a key from the password, salt and iteration count, returning a []byte of length keylen that can be used as cryptographic key. The key is derived based on the method described as PBKDF2 with the HMAC variant using the supplied hash function.For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you can get a derived key for e.g. AES-256 (which needs a 32-byte key) by doing:
dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
I wanted to see if this function was being called when I run keymaster
, so I used GDB to set some breakpoints. I noticed that 0x005b8598
is a good stopping point after the golang.org/x/crypto/pbkdf2.Key
function runs, so I tried that first.
INPUT:
gdb --args ./keyMaster "lock" "10000" "1" "AttractiveWhorl"
break *0x005b859d
run
OUTPUT:
It looks like 0x20
bytes were returned in the RAX
register after the function ran. Let’s print them out:
pwndbg> x/4xg $rax
0xc0000c2100: 0x76109d8604cee045 0x30bc3b5b4830af64
0xc0000c2110: 0xa1be8637b1023d16 0xd216bad3f5b06daf
When I convert these bytes in CyberChef, I get random binary data. If I encode it into Base64, then I’m looking at 44 characters of data.
After the conversion, we get this string: ReDOBIadEHZkrzBIWzu8MBY9ArE3hr6hr22w9dO6FtI=
So what are we looking at? The Answer.
When we enter it into the NSA Codebreaker, we get the green banner – success!!!
In the case of keymaster
, the pbkdf2
function is executed as follows:
/* password = "Jk1jQ3MZrOC2aDSPhDJJKgXC5JuXdBlxJ4A5EJQZDplQesvwBCW22zirwPwnuTr6JtkK2jZOEG+BmSWXs2ceGg==" */
password = "Jk1jQ3MZrOC2aDSPhDJJKgXC5JuXdBlxJ4A5EJQZDplQesvwBCW22zirwPwnuTr6JtkK2jZOEG+BmSWXs2ceGg==";
salt = encoding/base64.(*Encoding).DecodeString("q1xLzW588Stz+R/BJWj490sSeMj0om0D++pcndUBpww=");
iter = 0x1000;
keyLen = 0x20;
key = pbkdf2.key(password, salt, iter, keyLen, crypto/sha256.New);
In short, the password and salt are hashed 4096 times using SHA-256. The result is 32 bytes long.
Technically, the sqlite3
instructions are stale and no longer correct. The string is not being obfuscated using a caeser-cipher, but it is still vulnerable for similar reasons. The string of q1xLzW588Stz+R/BJWj490sSeMj0om0D++pcndUBpww=
could be extracted and the key recreated with little to no effort.