In Securitum, we also perform security audits of desktop applications – everything ranging from a simple interface to complex systems with custom network protocols. Some of these apps are simply a wrapper around a database connection. It means that we can directly log in to the DB server and just issue SQL commands in there, all within the permissions granted to our role. This approach is called 2 tier architecture (client < - > DB) and is not recommended for security reasons, unless you can implement business logic and permission validation entirely on a database level, through grants, functions, procedures and such…..
A significant number of apps that we test are 2-tier apps. This is a problem for our customers, because most often separation of privileges in them is non-existent and they may not be aware of it. On the upside, they are really easy to test.
Scenario
In this episode we will show an example of a 2-tier application encountered during a pentest, that:
has two user roles with different permission sets – Administrator and User,
is 2-tier, so as we said before, we can directly log in to the database.
The application is a type of “Business Process Management” software, running internally within financial institutions, controlling flow of financial documents – a target that has access to potentially sensitive data.
How does the app validate user permissions? That’s the question – it is not entirely impossible that they are checked on the database level and that grants to the tables are issued in a least privilege manner. However, I have yet to see a completely secure 2-tier application.
The app is built on top of a not-deprecated-but-feels-like Microsoft Foundation Class (MFC) framework, connecting to a neighbour MS SQL server. It was delivered to the pentest as an executable (.exe) with accompanying DLL files.
Permission check
The first step is to find a functionality that we would like to bypass or work on. We have one – closing a “case” file in the system has an option to export the data to disk:

When clicked, the app throws an error “Only administrator can export the case”:

Step two – let’s disassemble the binary and find the piece of code responsible for this particular permission check. To do this, I start Ghidra and import the executable. On the initial analysis window, it shows which referenced DLLs are missing – then I import them too. Next, I perform the full automated analysis of binaries, so the tool can find entry points, references to data, function boundaries and such.
To find the piece of code, the first step is to search for strings within the application and then see where the code references these strings. For example, a string “Invalid data.” Is referenced at four addresses, within two functions (see right-hand side):

To find the string we are interested in, we have to search for it within all binaries – the main one and its DLLs. It can also happen that the string is in a resource file but is translated to a common label – then we of course have to search for the label, not the actual, translated string.
It turns out that there is an “IfAdministrator” (“CzyAdministrator”) function in one of the auxiliary modules – Flow.dll. Below is a reconstructed C-code in the Decompile window in Ghidra:

Let’s see what happens inside this function:

The function checks whether “Administrator” string is within role set of the current user. If it is not, the function returns “0” (failure) and displays the message box. Otherwise, it continues with the operation further into the code part not shown here.
If the app checks whether the current role set contains “Administrator”, this is an indicator that it might validate permissions on the client-side. In other words, the role is declarative and the app trusts us that we are who we claim to be.
There are three ways to quickly test this hypothesis:
connect to the DB server, try to modify data in there and see if we are allowed to (boring),
patch the binary (modify the condition, export modified program as a new executable and run it),
run unmodified program through a debugger and change the “if” condition on the fly – this is what we will do now.
So, we open a debugger, in this case it’s x32dbg. Import the main binary, run it and wait until it breaks on the entry point (default behaviour), ensuring that auxiliary DLLs have also been loaded already.
Now we have to set a breakpoint at the moment the “IfAdministrator” check is performed. How to know what address is it? Ghidra uses a default offset of 0x00400000. In x32dbg, when a program is actually run, we have to check “modules” list and find the base address and add our offset to it. Or, we can go shorter, by simply invoking “Go to address” prompt with Ctrl+G, and then just type “Flow.dll+offset”. In our case the “if” statement in Ghidra was at 0x470578, so we navigate to “Flow.dll+70578”. We set a breakpoint here, then continue execution. Within the app’s UI, we navigate to the target functionality – click “Export the case” checkbox. The breakpoint is hit on a jne (jump if not equal) instruction:

This instruction will jump to the specified address (Flow.dll+705D1) if the ZF (zero flag) register is set to 1. Because just one instruction before we have test eax, eax – which, when provided two equal registers, sets the aforementioned ZF to 1 – we will not jump, and we will try to change this behaviour.
The opposite function to jne is je (jump if equal), essentially reversing the behaviour: jumps to the given label if ZF is set. We can patch this instruction while hanging on the breakpoint, by changing jne (0x75) to je (0x74):

There are endless methods to alter the program’s flow – for example, we could also modify the ZF registry on the fly. Now let’s continue execution and see if we can bypass the admin-check this way: we get a success message “Information about the closed case have been archived”!

That’s it, we confirmed that the application indeed validates the role client-side. Apart from permissions, we can assume that also business logic or data validation is done the same way.
Key takeaways
If your desktop app connects directly to the database – it is likely that it has the same issues as the one described in this scenario. It’s a good idea to confirm whether low-privileged users don’t actually have administrative privileges within your sensitive systems – be it HR, finance or other internal software. Apart from compliance, this opens up the door for data exfiltration or unauthorized modifications.
It’s a must to delegate business and security logic outside the control of the users. A middle API-layer is recommended, although in certain scenarios (low-complexity environments) it may be enough to program it within the database engine.
If unsure – give us a message and we will be happy to test the security of your apps, providing recommendations on any weaknesses they may have.





