11 minute read

Badge 8

Task 8 - Raiding the Vault - (Reverse Engineering, Golang)

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

Pre-Requisites

  • Ghidra
  • GDB
  • Complete all tasks up to Task 7
  • The cookie tok value you generated in Task 7
  • The server.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.
  • A database viewer, like DB Browser for SQLite

How do we get the unencrypted key-encrypting-key?

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 argument

Since 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.

Running the binary

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

SQLite View

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.

Change Credits

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:

Database After

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:

  1. This is a Go Language binary, based on the jwt and sqlite3 URLs
  2. This binary was compiled with C for Go, which includes native C libraries
  3. The version numbers of each open-source library, some of which may be outdated and vulnerable

For 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:

  1. There are keyMaster.db-wal and keyMaster.db-journal files being temporarily created during runtime
  2. The binary is executing obscured sqlite3 calls
  3. random numbers are being generated for an unknown purpose
  4. The plainKey print is the last thing that happens before exit.

Reverse Engineering

Let’s open the binary in Ghidra and see what pops out:

IMPORTANT

If you skip this instruction, all you will see are trampoline and sqlite3 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.

main list

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?

What is a Caeser-Cipher?

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

Needle in a Toothpick Box

Because the key encrypts and decrypts, I had a hunch that there would be a related string in the binary.

Finding the Function

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

main list

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;
}

What is 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)

Finding the Answer

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:

after pbkdf2

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.

  • https://gchq.github.io/CyberChef/#recipe=Find_/_Replace(%7B’option’:’Regex’,’string’:’%5E(.%7B0,16%7D)’%7D,’‘,true,false,true,false)Remove_whitespace(true,true,true,true,true,false)From_Hex(‘0x’)To_Hex(‘None’,0)Swap_endianness(‘Hex’,8,false)Remove_whitespace(true,true,true,true,true,false)From_Hex(‘Auto’)To_Base64(‘A-Za-z0-9%2B/%3D’)&input=MHhjMDAwMGMyMTAwOiAgIDB4NzYxMDlkODYwNGNlZTA0NSAgICAgIDB4MzBiYzNiNWI0ODMwYWY2NAoweGMwMDAwYzIxMTA6ICAgMHhhMWJlODYzN2IxMDIzZDE2ICAgICAgMHhkMjE2YmFkM2Y1YjA2ZGFm

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!!!

Task 8 Success

Go over that one more time

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.

But that’s not a Caeser Cipher

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.