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.
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:
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
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:
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 (
Patching this would have been easy: merely change the parameter order and be done :)
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
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.
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
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.
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).
Even without looking at the actual binary,
strings showed a number of interesting queries right away, especially those ones containing a
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
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
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.
Adding a challenge
One interesting approach which was followed by another team is shown in this gist. Although there is no way for us to get the private key used for signing a challenge, if you managed to learn a valid combination of challenge and signature used by the gameserver, you could just replay it. For the sake of readability, here is a slightly modifed version of the solution in the gist.
Although we did not exploit this flaw, we found it useful in the whole discussion of all discovered flaws in the service.
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 :-)