Skip to content

Instantly share code, notes, and snippets.

@wilfreddv
Created March 1, 2022 12:55
Show Gist options
  • Save wilfreddv/7377892645979c213842655c63c1300f to your computer and use it in GitHub Desktop.
Save wilfreddv/7377892645979c213842655c63c1300f to your computer and use it in GitHub Desktop.

STB 2018 Writeup

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++ file: quick overview

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.

The PHP file: quick overview

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.

The bug

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.

Side quest: 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.

The attack

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment