This is a write-up of the 2018 STB challenge: https://github.com/securifybv/spotthebug/tree/master/STB_2018
When first opening the repository, I see two interesting files; a PHP file, and a C++ file.
The C++ has a BaseCGIClass (which has no functionality), and two children classes of the BaseCGIClass. I immediately notice the std::system
call in the Executor
class. The Logger
class seems to not have any potential side effects. In the main
function, I assume the Cgicc
is an interface for the CGI protocol. This means formData("encryptedData")
likely returns user input. We can control this.
Right after writing the user input into some shared memory, we call exec
with the PHPCOMMAND
. However, it doesn't look like we can control this. After that, we log some stuff and reply to the user.
On first glance, it looks like this file contains a pretty standard set of database-connectivity functions. The first thing that comes to mind when dealing with databases is SQL-injection. Skimming the code, I see $db->escapeString
around all the presumably user generated values, so injection is probably not the angle we're looking for.
On line 51, we can see how the PHP script communicates with the C++ file; the shared memory. We know we have control over that (the formData("encryptedData)
gets written into this memory).
The following comment caught my eye: // We use RSA for encryption. Even if you can bypass our high-grade security, you have to know a user's cash token!
. At the beginning of the file, we encountered $d
and $n
, which are probably used for the decryption, which is confirmed by line 60. On line 62, we can see what kind of format the decrypted data should have. Since we don't know the token
ahead of time, we cannot craft a payload that transfers a ton of money to some other account without authorization. On line 72 we see our input gets used as parameters for the queries, and on 77 we get the result back. On line 80, it writes any error message back.
After looking at it for a while, trying to figure out how to manipulate either PHPCOMMAND
or break the logger, I noticed the weird static_cast
on line 37 of stb.cpp
. Notice how it casts base
, which is an Executor
, to Logger
. At first, I thought this was C++ magic, but, after running a test on my own, I found the bug. logData
is never called, since base
is of type Executor
. Instead, exec
is called! My guess is that C++ resolves the offset of logData
to the beginning of the Logging
class, and calls base
's method at that offset. This happens to be exec
.
class Base {};
class Foo: public Base {
public:
virtual void exec(const char* prg) { printf("exec: %s\n", prg); }
};
class Bar: public Base {
public:
virtual void log(const char* data) { printf("log: %s\n", data); }
};
int main(void) {
Base *base = new Foo();
static_cast<Bar*>(base)->log("encryptedData");
}
This outputs exec: encryptedData
, and not log: encryptedData
as you would expect on first glance. This is great! We log the memorySpace
, which we know we can control. Even better, since errors start with a #
, it is most likely seen as a comment by the shell which std::system
invokes. Going back to subprocess.php
, we can find 5 new Exception()
calls. Somehow, we need to trigger one of these in order to get the "logger" to run.
The two exceptions on lines 65 and 69, are interesting, though. In those exceptions, a null byte is appended. This means that they're useless to us; any data after the null byte won't be read by std:system
.
The exception on line 57 gets thrown when there was an error connecting to the database. I don't think we have much influence on that.
That means only two exceptions remain. The first one is thrown in checkRow
, on line 19. It will be thrown if fetchArray()
returns false
. According to the docs, that happens when no matching rows are found. Since we control the parameters, this shouldn't be hard. In fact, the comment we saw earlier hinted that we don't know token
. But that works in our favour. Passing in an invalid token will cause the exception to be thrown.
The second is thrown in checkUpdate
, on line 26. This will be thrown if the UPDATE
queries on lines 43 or 46 fail. However, to reach these lines, we do have to know the correct token
. We could maybe use our own? Reading the query more carefully, I note that $parsed["amount"]
is not escaped here. It might look like a possibility for SQL-injection.
Would passing a string here cause anything to break? After all, we expect an integer. Turns out, in PHP, "123" + 123
results in 246
! This means line 33 would not break. For the injection, we would need to pass a delimiter inside the parsed["amount"]
variable. In SQLite3, commands are delimited by a semicolon. Unfortunately for use, a semicolon is also used as a delimiter for the payload format (line 63). So it seems the possibility to inject SQL really ends here.
Okay, what do we know? We know we can make the PHP script throw an exception by passing bogus values, causing the SQL to fail. This error will be written to the shared memory starting at the beginning (offset=0). The first character will be a #
. Due to a bad cast in the C++ file, exec
is called with the sharedMemory
instead of logData
. Since the first character is a #
, this will be ignored by the shell.
We should also keep in mind that the data will be decrypted before being parsed. This is easily reversed, since we know all the components. We don't want to encrypt the whole payload, though. Otherwise std:system
would receive garbage. We just need to encrypt the part we need to pass the check on line 64. To split the comment from the command, we will need a newline. This can be easily put together using some Python.
n = 37662123018614894363000505295713618793576071522786088024797791265545848021381210845377436711475574095578204570155391624887547302667908836835675664304233389970978806204091543317544485506272376395108063018443343946143277267975714261855561020081290532392743652531864719705264768528667954781074780935036249053507555769459
d = 3
MALICIOUS_CODE = "..."
PADDING = "PADDINGPADDINGPADDING" # Just to be sure
PATTERN = "1;2;3;4;5"
PATTERN = int(PATTERN.encode().hex(), 16)
# TODO: Reverse RSA
PAYLOAD = f"{PATTERN}{PADDING}\n{MALICIOUS_CODE}".encode()
Passing this should allow us to have code execution in the shell.