# FaustCTF’17: Smartmeter Writeup

Another service we successfully exploited during the CTF was Smartmeter. Although we only exploited a single flaw, we nevertheless share all findings we discovered also after the CTF.

# Introduction

Smartmeter was a web service built using kore.io. To annoy anybody trying to look at traffic, it ran via HTTPS (including forward secrecy) and more so, had access logging disabled. As a backend for data storage, this service utilized a PostgreSQL database, containing four tables: challenges, devices, users and owners. Each device can be owned by exactly one user. The flags were stored in the reason field for a device ownership.

## The Obfuscated Binary

Firing up IDA, we note something interesting. While the call to libc_start_main looks just like in any other binary, the main function ends quite abruptly. We find that this pattern happens all the time throughout the code:

Let’s dissect what is happening here. We see that in 0x4068BE, the address of the function printf_chk is loaded into rax. Next, the address is pushed onto the stack, i.e., is now the top of the stack. Next, another address is loaded into rax and then rax is exchanged with the rsp+0x8. This way, we now have the printf_chk address at rsp and the second address at rsp+0x8. Subsequently, we see a retn, which pops the stack and continues execution where the element pointed to. In essence, this is actually a call printf_chk. When a call is invoked, this automatically puts the return address on the stack. Since retn pops the stack, the next element now is loc_4068D3, which points to the instruction right after the retn. In other words: this is merely an obfuscated call, which annoys IDA nevertheless. So, let’s get rid of all these things right away:

Since we don’t want to re-align all calls in the whole binary, this merely replaces the 21 byte sequence used throughout the binary with the matching call instruction.

Next up are the actual returns from functions. Rather using retn, the binary uses pop rsi; jmp rsi. This can be easily replaced just as well d = d.replace("\x5e\xff\xe6", "\xc3\x90\x90"). Now, thankfully IDA and our decompiler work ;-) Actually, there was another obfuscation applied (full script), but it did not hinder the analysis.

## Understanding the service and finding the vulns

kore.io always ships a configuration file, in which the endpoints and the functions are listed.

On the frontend, only register and static were available. Let’s take a look a serve_static, which boils down to something like this:

### Path Traversal

It appears that the filename is checked to not contain any .., so as to stop a path traversal. However, the parameter order is wrong: this merely checks if the filename is contained within .., not the other way around. This way, we have a path traversal vulnerability, which could be used to retrieve flags from other services, e.g., toaster (https://target:2443/static?file=../../../srv/toaster/toasts.db).

Patching this would have been easy: merely change the parameter order and be done :)

### Buffer Overflow

To access the stored flag information, the gameserver uses a Schnorr signature of a challenge provided to it by our service. In essence, our service only knows the public key, which is sufficient to verify a signature. To ensure safe transmission of the data, it is Base64 encoded and subsequently decoded by our service. Let’s have a look at that function, which is used to decode the signature and the challenge.

Basically, this function uses scanf in a loop to convert two hex characters into one byte, copying the result into decoded. The pointer to decoded actually points to memory in the stack frame of the calling function. Looking at that function, we find that the buffer is merely 56 bytes away from the RIP. As there is no bounds checking performed here and no restriction on the length of the challenge sent to our service. So, in order to overwrite the RIP, we just need to send 56 bytes and our desired return address. Although system is already in the binary, this is where it gets a bit more complicated: in x64, to call system(str), we need to have the address of str the rdi register. When we reach our overwritten RIP, however, rdi points to something we cannot control. Easy enough, let’s have it point to something we control!

Sadly, that is also not as easy. Since the binary is run using ASLR, we can’t predict any stack addresses and we did not find a memory leak. What we do know, however, is that the BSS segment is not randomized. Hence, all we need to do is write a string to BSS and then call system with that address.

#### Exploit

When looking through the gadgets in our binary, we see that at 0x40e5c7 there is mov qword ptr [rbx], rax ; pop rbx ; pop rsi ; jmp rsi. If we can control rbx to point into the BSS and we fill rax with the string we want to write there, we can put an arbitrary command in a known memory region. We find another useful gadget at 0x41cb57: pop rax ; pop rbx ; pop rbp ; pop rsi ; jmp rsi. This allows us to fill rax and rbx at once. While typically in a CTF, you can just run system('/bin/sh') as both STDOUT and STDIN are piped via the network, our service is a HTTP(S) binary. So, to allow for any arbitrary command to be executed, let’s be generic about what we want to execute (and how long the command can be).

In the example, we merely dump the PostgreSQL database into a file and leak that file using the path traversal vulnerability. Similarly, this could have been used to leak the flags via nc or abuse the privileges of the smartmeter user in any way you might see fit.

#### Patch

If you have made it all the way through here and looked at every specific detail, you could have noticed that it seems that a regular expression in the config of kore.io should prevent this (line 7). However, although this regular expression does check that 32 hex digits are contained in the challenge, since no beginning of line and end of line markers are used around it, it does not limit the length at all. Most likely, this would have been the easist fix (given that there are lot of spaces in the configuration after the validator, you would not have to modify too much in the binary).

### SQL Injection

Even without looking at the actual binary, strings showed a number of interesting queries right away, especially those ones containing a %s:

The most promising one appears to be the query in line 4: we have two parameters which are controllable by the user and we directly select the reason, i.e., the flag. However, looking at the binary, we find that the query is only executed when a single quote is neither contained in the email nor password. Similarly, most of the functions check to ensure that no single quote is contained. There is, however, one exemption: registering users.

We note that while the password is checked to ensure that no single quote can be injected, the email address is checked for double quotes (which are not necessary for a successful injection). Also, looking at the call to the function which contains the query, we find that if inserting a user is not successful, a HTTP 500 response is generated, whereas a 200 OK is emitted if the query worked.

#### The Blind SQL Injection Exploit

We can inject SQL code into the query insert into users (email, password) values('%s', crypt('%s', gen_salt('bf')));. Unfortunately, we can’t just select the flags with a sub-query and let the insertstatement store them in the users table, as we can’t read the email or the password column later. With every sql injection we get only one bit of information back - registration worked (status 200) or registration failed (status 500). That’s enough to mount a blind sql injection attack.

Our new email looks basically like '||(case when (<sql condition>) then '<random mail>' else '<existing mail>' end),'<random password hash>')--. The full (injected) statement is insert into users (email, password) values(''||(case when (<sql condition>) then '<random mail>' else '<existing mail>' end),'<random password hash>')-- .... If the condition we give in <sql condition> is true, a new user is created and we get a status 200 response back. If the condition is false, the insert will fail (as we use an existing mail), and we get a status 500 response back.

Let’s pack this information leak into a python script:

We want to leak all flags, so we build a statement that contains them all: SELECT string_agg(reason,'_' order by device_id) FROM owners gives us a string of the format FAUST_WS..._FAUST_WS..._.... The order by is important, otherwise the string could be different for each probe. Next, we extract the string character by character, using binary search on the ascii code of each character:

The length of the string is always known - there are 8 devices, each one contains an 38 character long flag, joined by 7 separators. All in all that’s 311 characters. We can use extract_char and retrieve one char after another.

We could improve this exploit by ignoring the fixed characters in the flags. As all flags started with FAUST_WS and all separators were known, we don’t have to extract 71 / 331 characters (~20%). Next we could print out intermediate results of our string extraction, to get partial results in case of a connection error. Our actual exploit did both, but still the attack was quite slow: It often took over 60 seconds to extract all 8 flags from another team.

The binary basically already had everything in place to stop the SQL injection. The only change necessary was to exchange the double quote in the strstr call with a single quote.
Holy cow, this service had a bit of everything. HTTPS to ensure that traffic could not be easily anaylzed. An obfuscation scheme, which while not hard to reverse, made things a little more tough. Tiny issues like the improper ordering of parameters for strstr and checking the email address for double instead of single quotes when registering were combined with a buffer overflow vulnerability, which was not trivial to exploit. All in all a cool service which during the CTF, I spent much too little time on :-)