1 min read
Access Control Facades and Hardcoded Secrets: A Sage 300 Case Study (Part 3)
This is a continuation of the Sage 300 case study series where we explore the process of discovering and developing exploits for six (6) different...
28 min read
Konrad Haase : Jun 27, 2023 5:04:15 PM
This is a continuation of the Sage 300 case study series where we explore the process of discovering and developing exploits for six (6) different vulnerabilities we found in Sage 300 that would have allowed users to bypass authentication, decrypt sensitive data including stored passwords, and obtain direct database access
In the first part of our Sage 300 case study, we showed how the Sage 300 authentication was a façade that could allow a low privileged TEST user to gain unrestricted access to the database, and (in some configurations) even the underlying host. We also showed how it was possible to exploit these issues by copying and pasting a password in an ISM file and then unmasking a password textbox. While simple, this method involved overwriting the ADMIN password, which wasn’t particularly elegant.
In this part of the case study, we will aim to completely subvert this application’s access controls by figuring out how the Database Setup utility is retrieving and decrypting the SQL password, which would allow us to simply read and decrypt the SQL password from the exposed ISM files, and probably all the other user passwords too. To accomplish this, we’ll need to get our hands dirty with some reverse engineering
Note that this portion of the case study will get quite technical. If you can’t make it through, we suggest you skip to Part 3.
Since we know the Sage 300 Database Setup utility performs both reads and writes on passwords stored in the “browse.ism” and “orgs.ism” files, we’ll start by analyzing it in an attempt to discover the program’s logic for encrypting and decrypting the passwords stored in those files. Technically, we could use the main Sage 300 binary since it probably has the same logic, but it’s much bigger and does a lot more than the more focused Database Setup utility. targeting a smaller binary makes finding the decryption function easier because there’s less noise.
To kick things off, we’re going to open the Database Setup utility binary (C:\Sage\Sage300\runtime\a4wsetup.exe) in a tool called PeStudio. This tool was designed to accelerate the initial assessment or triage of malware samples, but we’re going to use it because it nicely presents all of the key executable details.
Sage300-2-33: The Sage 300 Database Setup utility (a4wsetup.exe) open in PeStudio.
From the first page of PeStudio’s output we can learn that the Database Setup utility is a 32-bit executable, likely written in C++ using Microsoft Visual Studio 14.5. PeStudio, like all other executable inspection tools, retrieves this information from various Portable Executable headers. The Portable Executable (“PE”) file format is a data structure containing all the information necessary for the Windows operating system to load and run the executable code contained within executable files (.exe files) and code libraries (.dll files). A diagram depicting the structure of a 32-bit PE file has been included below:
Sage300-2-34: The PE data structure, courtesy of Wikipedia.
Looking at the structure of a PE file depicted in the diagram above, it can be easy to be overwhelmed by the amount of information available to us. The good news is that at this stage the only sections we’re interested in are the import directory table and the import address table, which contain information about the various code libraries and functions used by the program.
PeStudio will automatically parse these sections out of our “a4wsetup.exe” PE file to present us with a list of code libraries being imported in the “libraries” tab, and a list of all the functions being imported from those code libraries in the “functions” tab. For readers unfamiliar with code libraries, these are just collections of code that are designed to be used by other applications to avoid duplicating work, avoid taking up disk space, and increasing maintainability (modify code in one library instead of in every application that uses it). For example, if Sage developed a set of applications (different executables) that all had to access our “browse.ism” and “orgs.ism” files to read and write various credentials, it would make the most sense for them to write that functionality once, package it in a code library, and have all of their executables like Sage 300 and our database setup utility interface with that code library to call the various functions stored within; instead of some poor programmer having to copy and paste code between them. When we’re looking at our database setup utility, “a4wsetup.exe”, what we expect to see is much of the ISM file interfacing is being done through a code library.
Looking at the various code libraries being imported by our database setup utility, we see three Sage code libraries (prefixed by “a4w”) among the other Windows library files.
Sage300-2-35: PeStudio showing the libraries imported by the Sage 300 Database Setup utility (a4wsetup.exe).
Based on their names, we can guess these files are related to API (“a4wapi.dll”), memory (“a4wmem.dll”), and monitoring (“a4wmtr.dll”) functionalities. The apparent API code library, “a4wapi.dll”, seems to import 74 different functions. When we use the “functions” tab to inspect the parsed Import Address Table (the section of the executable that stores the mapping of functions used by the executable), we notice something strange:
Sage300-2-36: PeStudio showing the functions imported by the Sage 300 Database Setup utility (a4wsetup.exe).
The imported functions from the “a4wapi.dll” code library we’re interested in are being displayed as numbers instead of names. These numbers are function ordinals. When functions are imported by a program, they can be imported by name or number (an ordinal). It seems our database setup utility opted to import all of the API functions by ordinal, making our analysis a bit more complicated.
Ideally, we want to see the function names of the API exports because those names would likely give us hints as to what the functions do and may allow us to pick out a handful of functions we’re interested in from the 70+ imports. One way we could go about getting names for these imported functions is by opening the “a4wapi.dll” file and looking at the export table to map all of the exported functions names back to our API library based on the ordinal number. This method would be manual and tedious so instead we’re going to use a tool to perform that mapping for us: Ghidra.
Ghidra is a suite of software reverse engineering tools developed and released as open source by the United States National Security Agency (“NSA”) in 2019. We’ll import our “a4wsetup.exe” database setup binary into Ghidra to have it disassemble the binary and iteratively process its dependencies to retrieve symbols (symbols refer to names of methods, variables, data structures etc.). After the file has been processed, Ghidra will present the view pictured below:
Sage300-2-37: Ghidra showing the disassembled Sage 300 Database Setup utility (a4wsetup.exe).
In the two main windows we see the heavily commented assembly instructions for the program (left) and Ghidra’s attempt at translating those instructions back to C code (right). As we mentioned previously, the executable we’re examining is comprised of compiled code, which means the high-level language was translated to assembly. This translation process heavily optimizes and rearranges the code, so Ghidra will be unable to recover/produce what the original code looked like. Despite being unable to recover the original source code, Ghidra can usually perform a translation from assembly back to C code that is accurate enough to accelerate our analysis. We will be primarily showing Ghidra’s “Decompile” window in this article instead of the “Listing” window (that shows the x86 assembly instructions) because it will be good enough for our purposes, and to avoid turning this article into an x86 assembly tutorial.
If you’re looking for a tutorial to get familiar with Ghidra, consider using this article.
We’re going to start our analysis in Ghidra by looking at the Symbol Tree. This is a view of all the symbols (again, symbols refer to names of methods, variables, data structures, etc.) that were iteratively recovered from the executable’s dependencies. As pictured below, Ghidra nicely mapped the “a4wapi.dll” imports to symbols recovered from the code library to give us an easy to process view of the API functions used by the program (right):
Sage300-2-38: The Ghidra Symbol Tree for the disassembled Sage 300 Database Setup utility (a4wsetup.exe).
The two function imports from the API code library that immediately stand out are the “usrVerify” and “usrSetPW” functions. These functions sound like they’re related to the user authentication and user password change functionalities. We can confirm our hypothesis by using Ghidra to show us the references in the program to these functions.
Sage300-2-39: Ghidra showing us the references (usages of) the “usrVerify” function, which is imported from the a4wapi.dll.
From the dialogue box pictured above, we can see that Ghidra found two different references to the “usrVerify” function. This means that there are two instances where the Sage 300 Database Setup utility calls this function. These two functions have been included below, as they appear in the in the “Decompile” window of Ghidra:
Sage300-2-40: The two functions containing calls to the “usrVerify” function in the Decompile window of Ghidra.
As we can see from the code above, both references to “usrVerify” reside in unnamed functions. These functions have been assigned a name by Ghidra based on their memory address, so “FUN_00406f43” is a function that is located at the memory address 00406f43. Our job as reverse engineers is to try to figure out what these functions do and give them more appropriate names so that as we move through the program the functionality starts becoming apparent, much like a puzzle. Figuring out functionality is just making a series of educated guesses based on what we see. Looking at the second use of “usrVerify” (left in Sage300-2-40 screenshot above), we can guess this unnamed function might be part of some password change function since we see a call to “usrSetPW” shortly after the call to “use Verify”. In this case though, we can make guessing easier by simply moving up a level in the program flow by locating references to the functions that contain our “usrVerify” calls.
The first use of “usrVerify” appears in “FUN_00406f43”, which is only called as part of the “SignOnDlgProc” exported function, likely corresponds to the main sign-on popup pictured below:
Sage300-2-41: The “SignOnDlgProc” function in the Ghidra Decompile view (left) and the corresponding Sage 300 Database Setup utility Sign-On dialog prompt (right).
Now that we have a good idea of where this function resides and what its role roughly is, we’ll rename “FUN_00406f43” to something more informative, like “UserSignOnLogic".
The other function that calls “usrVerify” is “FUN_0040277d”, which we find is only called by the “ChangePwDlgProc” function, likely corresponding to the change password functionality (triggered when logging in with an admin password that is not compliant with security settings) pictured below:
Sage300-2-42: The “ChangePwDlgProc” function in the Ghidra Decompile view (left) and the corresponding Sage 300 Database Setup utility Change Password dialog prompt (right).
We’ll rename “FUN_0040277d” to something more informative, like “UserPasswordResetLogic”.
Based on what we saw within our “UserSignOnLogic” and “UserPasswordResetLogic” functions, we have good reason to believe that the “usrVerify” function does, in fact, handle user verification during the two authentication flows (sign on and password change). Our next step is to figure out how that function performs user verification/authentication. We can guess that the function would be passed some kind of reference to the value of the password field input (the password string we enter when logging in) and the credentials stored in the “browse.ism” file so that they can be compared. Since we know that the password in the “browse.ism” file is stored as an encrypted byte block, we know that to compare the passwords the program must either decrypt the stored password or encrypt the provided one. Since we know that the stored password was encrypted with a block cipher (symmetric encryption) we know that the encryption and decryption keys will be the same. This means we can identify either the encryption or decryption loop to figure out where the key is.
To continue our reverse engineering efforts, we could target either the “UserSignOnLogic” or “UserPasswordResetLogic” function. We’ll chose the former, as the “usrVerify” function call in the “UserSignOnLogic” function is easier to trigger; all we have to do is open our target program and attempt to sign in. This being easy to trigger will simplify our debugging efforts later on.
Looking at the call to the “usrVerify” function within the “UserSignOnLogic” function, it seems that two data item references (DAT_*) are passed as arguments, followed by a local variable (local_*) and a stack address (stack0x*), as pictured below:
Sage300-2-43: Our “UserSignOnLogic” function in the Ghidra Decompile view with the call to the “usrVerify” function highlighted to show the parameters being passed.
If our hypothesis is correct, one of these data item references (DAT_0040e048 or DAT_0040f4d0) is going to be our submitted password, and one is going to be the real password stored in the “browse.ism” file. To confirm our theory and figure out if the program is encrypting the submitted password or decrypting the stored password, we’ll use a debugger.
A debugger is a tool that allows us to follow along with the program code as it is being executed. Debuggers allow us to step through execution one instruction at a time and set break points that allow us to pause program execution so that we can inspect the contents of memory after specific instructions. We’ll be using the x64dbg debugger to set a breakpoint (a pause) on the “usrVerify” function so that we can see what data references are being passed as arguments in the hopes it will point us towards an encryption or decryption function that we can extract an encryption key from.
Before we begin debugging, we’ll go back to PeStudio and disable Address Space Layout Randomization (ASLR), as pictured below:
Sage300-2-44: The ASLR option being disabled on the Sage 300 Database Setup utility (a4wsetup.exe) via PeStudio.
ASLR is a security mechanism that randomizes the location of memory regions to make exploit development for memory corruption vulnerabilities more difficult. What ASLR means for us is that the memory addresses of program functions that we saw in Ghidra would not be the same in a debugger, because they’d be placed randomly. ASLR also means that breakpoints we set on certain memory addresses may change from execution to execution, making our job needlessly difficult. Thankfully, we can opt-out of ASLR by modifying the appropriate header on our target “a4wsetup.exe” binary via PeStudio. Once we opt-out, Windows won’t use ASLR, and we can perform our debugging in a reliable and repeatable manner.
Since our “a4wsetup.exe” file is a 32-bit executable, we’ll actually open it in x32dbg, the 32-bit version of x64dbg.
Sage300-2-45: The Sage 300 Database Setup utility (a4wsetup.exe) open in the x32dbg debugger.
The debugger has several windows, including the main window (upper left) that contains the assembly instructions that are being stepped through, the register window (upper right) that contains information about the various CPU registers, memory dump window (lower left) that contains the contents of memory at a certain address, and the stack window (lower right) containing the contents of the stack.
If you’re looking for a tutorial on getting started with x64dbg, consider starting with this article from Varonis.
Once the program is loaded in the debugger, we can set a break point on the “usrVerify” function in the “a4wapi.dll” code library and then run the program. When presented with the “Sign-On” window, we’ll enter a distinctly incorrect password like “wrongpassword”. We do this so that we can distinguish our real password being read from the “browse.ism” file from the one we are entering in the input field when reviewing program memory.
After entering the password and clicking “OK” to submit, we see that the debugger has hit the breakpoint we set on the “usrVerify” function. We can now look at the stack (bottom right) to see what arguments (highlighted in grey) were passed to the function.
Sage300-2-46: The breakpoint on the “usrVerify” function being hit in x32dbg with the function arguments on the stack (bottom right window) highlighted in grey.
Since the stack is last-in-first-out, we’ll have to work backwards to figure out what each of the four arguments is. Looking at the stack what we see is the stack address in the left column, the value at that address in the middle column, and then any additional information the debugger can provide on the right. Our first argument (at 0019F2AC) seems to be the value 0x000007FF, which does not seem to be a memory address. Our second argument (moving down to 0019F2B0) seems to be a memory address 0040F4D0 (in program memory, not the stack). Inspecting that memory address reveals that is storing the string “ADMIN”, which has been padded out with spaces (0x20 represents a space in ASCII):
Sage300-2-47: The contents of the 0040F4D0 address (second “usrVerify” argument) (left) and the stack (right) in x32dbg.
The third argument seems to be another memory address (this time in stack memory) that contains an encrypted password based on the tell-tale repeating pattern we see at the end of the string. This encrypted string does not match the one in the “browse.ism” file, so we can deduce this is probably our submitted password that has been encrypted.
Sage300-2-48: The contents of the 0019F36C address (third “usrVerify” argument) (left) and the stack (right) in x32dbg. This shows that the third “usrVerify” argument is the encrypted user-submitted password.
The final argument simply appears to be a stack address holding the value 0x55.
We can leverage the information we’ve gathered during debugging to rename some of our variables in Ghidra. Since we know that the second argument stores the username, and the third argument stores the encrypted submitted password, we’ll rename the second and third arguments to reflect that.
Sage300-2-49: The original Ghidra Decompile view of the “usrVerify” function call in the “UserSignOnLogic” function (left) next to the same view with renamed “usrVerify” arguments (right).
Since we’re not entirely sure what the first and last arguments represent, we’re not going to rename them at this point. Even though we don’t know what all of the arguments do, we figured out enough to know that the function takes a username and the encrypted version of the submitted password. The fact that the submitted password is already encrypted at this point means that the password encryption must be happening between the reading of the submitted password from the form field and the call to the “usrVerify” function. Let’s take a closer look at the decompiled code to see if we can’t figure out where the encryption is happening.
We see our “UserSignOnLogic” function starts with a call to the “GetDlgItem” function. This function returns a handle to a control (like a button or form field) in a specific dialogue box. The handle to that control (“hWnd”) is then used in the “GetWindowTextA” function to read text from that control up to a max of 65 characters (0x41 = 65). Then up to 64 of those characters (0x40 = 64) are copied to a new buffer using the “strCopyBZ” function (educated guess based on name since this is a “a4wapi.dll” function and not the standard C strcpy function). This new buffer is then passed to “FUN_004073f0” along with the buffer that ends up holding the encrypted password (“encryptedSubmitPassword” variable).
Sage300-2-50: The original Ghidra Decompile view of the “UserSignOnLogic” function (left) next to the same view with various renamed function call arguments (right).
To validate our assumptions about what the code here is doing we can set a new breakpoint in our debugger at the “GetWindowTextA”, restart our database setup utility, login again, and see what variables we get back.
Sage300-2-51: The “UserSignOnLogic” function being stepped through in x32dbg, currently at the call to the “GetWindowTextA” function.
We find that the call to “GetWindowTextA” returns a reference to the “WRONGPASSWORD” string. While this is the password we submitted to the sign-on page, it has been capitalized, indicating that the Sage 300 login process may not be case sensitive.
Sage300-2-52: The “UserSignOnLogic” function being stepped through in x32dbg, currently at the call to the “strCopyBZ” function.
This string reference is then passed to the “strCopyBZ” function, which copies our string to a new buffer and pads it out with spaces (0x20). This confirms our assumption about encrypted passwords we see in the “browse.ism” file being padded to produce the distinct ECB repeating pattern.
Sage300-2-53: The “UserSignOnLogic” function being stepped through in x32dbg, currently showing the contents of the memory buffer produced by the “strCopyBZ” function.
Finally, this copied buffer is passed to the “FUN_004073F0” function to produce the encrypted password in yet another new buffer that is ultimately passed to our “usrVerify” function, as we saw earlier.
Sage300-2-54: The “UserSignOnLogic” function being stepped through in x32dbg, currently showing the contents of the memory buffer produced by the “FUN_004073F0” function.
We’ve identified the “FUN_004073F0” function as being responsible for producing the encrypted version of our password. We’ll rename the “FUN_004073F0” function to “EncryptPassword” to clarify things in Ghidra. Next well dive into the function to investigate what it’s doing under the hood.
Sage300-2-55: The “EncryptPassword” function (formerly FUN_004073F0) in the Ghidra Decompile view (right), next to the Decompile view of the mystery “FUN_004075b0” function it calls.
We’ve renamed some of the function parameters based on what the function was passed when it was called to help us along. We’re pleasantly surprised to find it does not call too many other functions. It begins by calling the “memset” and “memcpy” functions to create a new buffer to store our encrypted password and copy our space-padded password string in (this is only clear because we renamed the function parameters). Then it enters a loop of calling a second mystery function, “FUN_004075b0”, passing in the variable “local_8” which seems to be a reference to our new encrypted password buffer.
Let’s go back to our debugger. We’ll rerun our program, login again, and step through the program until we get to our “EncryptPassword” function. Once there, we’re going to step into the “EncryptPassword” function and set a breakpoint on our new function of interest, “FUN_004075b0”. To get to this function of interest we need to step past the “memset” and “memcpy” operations, which we can confirm copy our password string to a new buffer.
Sage300-2-56: The “EncryptFunction” function being stepped through in x32dbg, currently showing the contents of the password memory buffer produced by the calls to “memset” and “memcpy”.
Allowing the “FUN_004075b0” function to execute once shows us that this is the function responsible for the actual encryption functionality. We see that it encrypted the first 8 bytes of our plaintext (highlighted in grey in the bottom memory window). Since this function is in a loop, we can expect the program to continue to call this function until the entire password buffer is encrypted.
Sage300-2-57: The “EncryptFunction” function being stepped through in x32dbg, currently showing that a portion of the password memory buffer was modified after a single call to the “FUN_004075b0” function.
Now that the “FUN_004075b0” function is confirmed to be the encryption function, let’s open it up in Ghidra and see if we can figure out what kind of encryption algorithm we’re dealing with.
Sage300-2-58: The Ghidra’s Decompile view for the “FUN_004075b0” encryption function.
While it is possible that Sage opted to develop their own proprietary encryption algorithm (which would be complicated and easy to get wrong), it is far more likely that they implemented a well-known encryption algorithm and that’s what we’re looking at above. Knowing that the encryption used for passwords is a block-cipher with a block size of 8-bytes (based on what we saw in the browse.ism file and again when we ran the FUN_004075b0 function) narrows down the obvious choices to DES, 3DES, or Blowfish.
Common Block Ciphers | Supports 8-byte Blocks |
DES | Yes |
3DES | Yes |
AES | No |
Blowfish | Yes |
Twofish | No |
To figure out if any of these ciphers are what we’re looking at in “FUN_004075b0”, we can review publicly available C and C++ implementations of the most probable algorithms to see if any are a close match. We can also encrypt our known passwords with the most probable algorithms and observe if the output is similar to what we saw in the ISAM files. After performing both activities, we conclude that our “FUN_004075b0” function is most likely an implementation of Blowfish. A comparison of an open-source implementation of Blowfish in ECB mode (left) to our encryption function (right) is pictured below:
Sage300-2-59: An open-source implementation of Blowfish in ECB mode (left) next to the Ghidra Decompile view of our encryption function (right).
To make our conclusion a bit more obvious, let’s rename the local variables and parameters in Ghidra to closer match the open-source Blowfish implementation on the left.
Sage300-2-60: An open-source implementation of Blowfish in ECB mode (left) next to the Ghidra Decompile view of our encryption function with renamed variables and function parameters (right).
This comparison is a good example of how code gets optimized during compilation and how Ghidra can struggle with unknown data structures. While the open-source Blowfish implementation in C is likely close to what the Sage developers authored, we see in the Ghidra translation that the compiler optimized away certain control-flows like the “if” statement that it turned into a “while” loop. Ghidra also mangled the BLOWFISH_CTX structure, which is a pair of arrays (a 1d and 2d array). Retyping it from an integer to an autostruct cleaned it up a bit, but we’d have to go in and manually build the structure if we wanted it to better match the original source code.
Returning to the “EncryptPassword” function, we can rename the various variables and functions to reflect our current understanding:
Sage300-2-61: The Ghidra Decompile view of our encryption function with renamed variables and functions.
In summary, our “EncryptPassword” function is creating a new buffer in memory, copying our padded password, then encrypting it 8 bytes at a time with Blowfish until the entire buffer is encrypted. The big question now is where is the encryption key?
Reviewing the open-source Blowfish implementation, we see in the usage example we have to call an initialization function with our encryption key to initialize a Blowfish context (“BLOWFISH_CTX”) object before passing it to the encryption function to encrypt strings.
Sage300-2-62: The usage example included in an open-source implementation of Blowfish.
Looking back at our “UserSignOnLogic” function, we see that the Blowfish initialization is likely “FUN_004074c0”, which happens just before the call to the “EncryptPassword” function. It seems likely that the “DAT_0040f46c” object is the “BLOWFISH_CTX” object given it is created from the return of the initialization function and then passed to the “EncryptPassword” function, similar to the usage example above.
Sage300-2-63: The Ghidra Decompile view of our “UserSignOnLogic” function where the “DAT-0040f46c” object, likely “BLOWFISH_CTX”, is highlighted.
We’ll rename our “FUN_004074c0” to “BlowfishInit”, and then open it up. Inside we find another function with multiple hardcoded parameters.
Sage300-2-64: The Ghidra Decompile view of our new “BlowfishInit” function (formerly “FUN_004074c0”).
Diving into this new “FUN_004071d0” function and renaming a few variables (right) to match the open-source implementation of the Blowfish initialization function (left) verifies our assumption that this is the initialization function we were looking for.
Sage300-2-65: An open-source C implementation of the Blowfish initialization function (left) next to the Ghidra Decompile view of the “FUN_004071d0” function (inside our “BlowfishInit” function) (right).
We’ll rename the “FUN_004071d0” function “BlowfishInitInner”. Its function signature differed from the open-source implementation in that instead of being passed as a reference to the initialization function, the Blowfish context object is created inline by the function and returned for later use. We can compare the open-source implementation to the decompiled function we see to figure out what each of the function parameters are (one of them should be the encryption key). Doing so reveals that the first parameter, which is a memory address, is the encryption key and the second parameter the length of the key (0x28 = 40).
Sage300-2-66: The Ghidra Decompile view of the “BlowfishInit” function with parameters to the “BlowfishInitInner” function commented above.
Now that we’ve identified a reference to our encryption key (a memory address), we have to extract the actual key. Since Ghidra has shown us the reference to the key is a memory address that lies in the Sage 300 Database Setup Utility binary itself (instead of something constructed during runtime), we can just open navigate to that memory address and extract the key from the executable. We’ll open up Ghidra’s “Bytes” view, navigate to the key’s memory address (0x00409cf0), and select the correct key size (0x28 = 40).
Sage300-2-67: The Ghidra “Bytes” view with the 40-byte Blowfish key highlighted (left) next to the Ghidra Decompile view of the “BlowfishInit” function.
At this point we want to validate that we’ve correctly identified the encryption method, mode of operation, and encryption key. The easiest way to do that is to try using online tools and/or third-party cryptography libraries to decrypt one of the passwords stored in the “browse.ism” file. Unfortunately, after a few attempts we found that our extracted key and encrypted text were not resulting in the expected plaintext when using open-source implementations of Blowfish in ECB mode, leading us to conclude that either we made a mistake in our analysis, or the Sage developers added a twist to their Blowfish implementation.
If the Sage developers did add a twist to their Blowfish implementation, we would need to reverse engineer several functions to figure out where they deviated from the standard implementation, which would be time-consuming. Alternatively, we could work towards finding a way to “plug-in” to their implementation such that we can use their functions to perform decryption; remember, our goal here isn’t to figure out how everything works, we just want to be able to decrypt the ISM file secrets on demand. So instead of continuing with our analysis of the Sage 300 Database Setup Utility (“a4wsetup.exe”), we’ll take a step back and look for an easier target; after all, Sage 300 is a big platform with a lot of surface area.
One peculiarity about our analysis so far is that Sage seems to have embedded both the encryption key and the Blowfish implementation in the “a4wsetup.exe” binary. This is a software anti-pattern because this binary can’t possibly be the only Sage executable that needs these functions (for example, the main Sage 300 program will need to perform some of these functions when changing passwords or verifying users). If multiple executables rely on a set of functionalities, most developers would opt to implement those functions once in a code library so that they could be reused by the various executables that need them. Since Sage 300 has existed under one name or another for over 30 years, it may be the case that the functions were originally implemented inline and then never updated, while other more modern parts of the program were built to use a code library as development best-practices progressed.
To see if this was the case, we can leverage PowerShell to do a simple search in the runtime folder for all files containing the strings “blowfish”, “decrypt” and, “encrypt”. The query returns five code libraries:
Sage300-2-68: PowerShell being used to query all of the binaries in the Sage 300 “runtime” folder for the terms “blowfish”, “decrypt”, and “encrypt”.
Unlike our “a4wsetup.exe” binary, these code libraries returned in this search must have the symbols related to blowfish intact, so (hopefully) minimal reverse engineering will be required to understand what’s happening in them. Looking at the returned code libraries, we can identify the three Alchemex libraries (“ALWEBCOM.dll” is an Alchemex library) and the BouncyCastle library as being third-party components for business intelligence reporting and cryptography, respectively. The only Sage proprietary binary here is the “Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll” library. A proprietary Sage cryptography binary is exactly what we were looking for!
We’ll triage the cryptography library in PeStudio and discover that instead of being a compiled binary like our “a4wsetup.exe” file was, this is a Microsoft .NET assembly.
Sage300-2-69: The Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll code library open in PeStudio.
When programs are compiled in intermediate-level languages like C and C++, the compiler will optimize and translate them to assembly (machine code). We’ve spent a bit of time looking through that machine code in Ghidra (though technically we avoided the assembly and opted for the rough “Decompile” translation instead) and have found that it takes considerable effort to reverse engineer these binaries. In contrast, .NET assemblies do not get compiled the same way. Instead, they get “compiled” into an intermediate language and then translated to machine code by the Common Language Runtime (“CLR”) (which you can think of like an execution environment, like the Java runtime environment) when the assembly is executed. Since .NET assemblies are not translated to machine code when they’re compiled into executables or code libraries, we can often recover code that is very close to what developers originally wrote.
To decompile this .NET assembly, we’ll leverage the free dotPeek tool from Jet Brains. Opening the “Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll” assembly, we see that we’ve hit the jackpot; this code library contains a full implementation for Blowfish and even exposes the functions we need so that we can easily make use of them. The decompiled code that dotPeek produced also looks quite legible, which may allow us to later figure out where the Sage Blowfish implementation differed from the open-source implementations.
Sage300-2-70: The Blowfish functions located in the Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll code library open in dotPeek.
To verify that this library is going to work for us we’ll create a new .NET Framework project in Visual Studio (version of .NET must match that of the cryptography code library) and import the “Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll” assembly as a dependency. We can then use the public methods the library exposes to decrypt one of the passwords stored in the “browse.ism” file. We’ll use the “Blowfish.Blowfish_Decrypt” function, which takes an encryption key (as a byte array) and a ciphertext string to perform our decryption, which is pictured below:
Sage300-2-71: The Blowfish functions in the Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll code library being leveraged to successfully decrypt a password from the “browse.ism” file.
Running our proof-of-concept code results in the plaintext password for the “Admin” user being printed! We finally have a way to decrypt the protected contents of the “browse.ism” files. Before we celebrate, let’s turn our attention to the “orgs.ism” file which contains the encrypted password of the SQL database user for the various system and company databases. Earlier we noticed that the padding was identical to that in the “browse.ism” file, so there is a very high probability the same key was used.
Sage300-2-72: The encrypted password for the SAMSYS database stored in the “orgs.ism” file open in HxD.
We’ll update our proof-of-concept code to decrypt both the encrypted admin password string and the encrypted SQL password string. Running the updated code successfully prints the SQL password!
Sage300-2-73: The Blowfish functions in the Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll code library being leveraged to successfully decrypt passwords from “browse.ism” and “orgs.ism”.
At this point we have a working proof-of-concept that would allow us to read encrypted user and SQL database account passwords stored in the Sage 300 “Shared Data” directory. This means that if we had access to a Sage 300 workstation, we wouldn’t need to know any passwords or modify any files to gain access to the administrator account and complete control over the underlying SQL database. We could edit financial information, modify user permissions, copy sensitive data, or even delete all data in the Sage 300 database. Furthermore, if the SQL server has not been hardened, it may be possible for us to even execute code on the underlying server, which would allow us to begin attacking the wider corporate environment. For attackers, this access control façade, compounded by the hardcoded Blowfish key, represents an avenue to escalate privileges and to do significant and potentially irreparable damage to an organization’s ERP system.
Before we move on to weaponizing our findings to decrypt all stored passwords, let's explore some unanswered questions.
One unanswered question remaining is how does Sage’s Blowfish implementation differ from the typical ones? Since we have full insight into the Sage Blowfish implementation through the “Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll” assembly, we should be able to quickly figure this out. Going back to the decompiled code in dotPeek and doing a brief code review reveals that the initial P-Array and S-Box arrays are using different values than the standard pi-derived ones that all other implementations use (different even after accounting for the conversion from decimal to hex).
Sage300-2-74: The Blowfish initialization arrays in the open-source implementation (left) compared to those in the Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll code library (right).
This change would make Sage’s Blowfish output incompatible with the open-source implementations, which all use the pi-derived P-Array and S-Box arrays, explaining our earlier decryption issues.
Another unanswered question is that if our “a4wsetup.exe” program and presumably the main Sage binary, “accpac.exe”, contain inline implementations of the Blowfish decrypt function, why is this function implemented in the “Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll” code library? Unlike the binaries we were looking at earlier, this is a .NET assembly. Earlier we hypothesized that modern developments built on top of Sage might opt to use a code library instead of inline functions. Specifically what other parts of the Sage 300 program are relying on this code library?
We can leverage dotPeek to find out by importing the entire “C:\Sage\Sage300” folder into the program, and then searching all of the Sage 300 .NET assemblies for usages of our “Blowfish_Decrypt” function.
Sage300-2-75: Leveraging dotPeek to search all of the Sage 300 binaries for uses of the “Blowfish_Decrypt” function.
Our first search result is from the “GlobalSearchWindowService” assembly, which is part of the Global Search optional feature. Digging into this first search result reveals that something called the “LandlordPassKey” is being used as the decryption key for the “userPassword” parameter in a “SetBasicAuthHeader” function.
Sage300-2-76: First usage of the “Blowfish_Decrypt” function in the “GlobalSearchWindowService” binary.
Based on the function name and content we can assume this function is building a Basic Authentication header using a user password that was provided in an encrypted format using an encrypted password. Interestingly, it seems to be decrypting the password using the “LandlordPassKey” key. The “LandlordPassKey” seems to be located in the Blowfish class of our “Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll” assembly, pictured below:
Sage300-2-77: The “LandlordPassKey” Blowfish encryption key.
This key appears to be different than the one used to decrypt passwords in the ISM files, making it the second hardcoded Blowfish key we recovered from a Sage 300 binary. We have another key, now we need to figure out what it unlocks.
We saw that the “LandlordPassKey” was being used by the “GlobalSearchWindowService” to decrypt the “userPassword” value. To determine what the “userPassword” value is, why it’s being decrypted, and what it’s being used for, we’ll have to follow the code. By searching for all the calls to the “SetBasicAuthHeader” function, we can learn how the function is being used and where the user password parameter is coming from. We can leverage dotPeek to search for references to our “SetBasicAuthHeader” function and investigate those.
Sage300-2-78: First usage of the “SetBasicAuthHeader” function.
Looking at our first search result for uses of the “SetBasicAuthHeader” header, we see that the “MakeWebRequest” function calls the “SetBasicAuthHeader” function by passing on the password parameter. To learn where this password originates from, we keep working our way back by searching for references to the “MakeWebRequest” function. Doing so leads us to a function called “GetAllCompanyCore”, which is again just passing on its password parameter argument. Following the “GetAllCompanyCore” function leads us to the “TimerCallBack” function where we see that the username and password arguments are being taken from the “solrusername” and “solrpassword” fields of the “AppSettings” property of the “ConfigurationManager” object.
Sage300-2-79: The origin of the “userPassword” parameter (found to be “solrpassword” from the app settings) in the “TimerCallBack” function.
The ConfigurationManager class exists to expose configuration files for client applications. This means that there exists a configuration file (.config) for the “GlobalSearchWindowService” that contains encrypted credentials that we can decrypt and read using the “LandlordKeyPass”. We can navigate to the folder containing our “GlobalSearchWindowService” assembly using dotPeek (“C:\Sage\Sage300\GS\GlobalSearchWindowService”) and look for the associated config file, which we find is “GlobalSearchWindowService.exe.config”.
Opening the “GlobalSearchWindowService.exe.config” file and looking for the “appSettings” section confirms that the “solrusername” and “solrpassword” fields are stored here.
Sage300-2-80: The “appSettings” section of the “GlobalSearchWindowService.exe.config” file containing the “solrusername” and “solrpassword” entries.
In addition to the username and password fields, we also see application settings related to the Apache Solr instance that seems to have been installed with the Sage Global Search service, like the Solr instance URL (“https://localhost:8984/solr/”).
Importing the “LandlordPassKey” and the value of the stored “solrpassword” to our proof-of-concept code allows us to craft a quick decryption loop to reveal the plaintext.
Sage300-2-81: The Blowfish functions in the Sage.CA.SBS.ERP.Sage300.Common.Cryptography.dll code library being leveraged in conjunction with the “LandlordPassKey” to successfully decrypt the Apache Solr password stored in the“GlobalSearchWindowService.exe.config” file.
With credentials in hand for the Apache Solr instance (solr:S@ge300Se@rch), we can test them against the SOLR instance running on port 8984 (“https://localhost:8984/solr/”).
Sage300-2-82: The Apache Solr instance that is installed with the Global Search Sage 300 functionality being accessed with the recovered credentials.
The credentials worked and provided us with Administrator access to the SOLR instance!
Sage300-2-83: The “Core Admin” panel for the Apache Solr instance that is installed with the Global Search Sage 300 functionality.
Based on the documentation from Sage regarding the Global Search, it seems that the Apache Solr instance runs on the various workstations and servers that have the feature installed. Since the credentials we found were hardcoded in the configuration file as an encrypted string (something that end-users have no obvious way of changing), we could use our recovered password on any Sage 300 Global Search installation to get admin access to the Solr instance.
Apache Solr is a product that has been affected by a fair number of critical vulnerabilities that were later weaponized, like CVE-2019-17558, which was an issue in the handling of custom Velocity templates that could be exploited by an authenticated attacker to obtain remote code execution. The Apache Solr installations associated with Sage 300 are likely only going to be updated by system administrators when the Sage 300 installations are updated, which is probably once a year when the new Sage 300 version is released. Yearly releases provide ample time for Solr vulnerabilities to be discovered, published, and weaponized by attackers.
Returning to our search results for usages of the “Blowfish_Decrypt” function and selecting the second result introduces us to the “GetConnectionString” function in the “Sage.CA.SBS.ERP.Sage300.Common.BusinessRepository.dll” code library. This function seems to read an XML file called “SITE \dbconfig.xml” in the shared data directory to extract and assemble a SQL connection string.
Sage300-2-84: Usage of the “Blowfish_Decrypt” function in the “GetConnectionString” function.
Interestingly, this function relies on yet another new encryption key called “PASS_KEY”, which is stored in the same binary. We can quickly locate the value of this key using dotPeek:
Sage300-2-85: The “PASS_KEY” Blowfish encryption key.
This is the third hardcoded encryption key we’ve recovered from the Sage 300 binaries. We can extract this new “PASS_KEY” for use in our proof-of-concept script, along with the encrypted password string from the “dbconfig.xml” file in the “SharedData\SITE” folder.
Sage300-2-86: The contents of the “dbconfig.xml” file in the Sage 300 Shared Data folder.
Decrypting this string provides us with valid credentials to the “PORTAL” database on the SQL server that Sage 300 uses. Note that unlike the Apache Solr credentials, it is possible to change this password through the “a4wsetup.exe” utility.
Sage300-2-87: Decrypting the “PORTAL” database password with the “PASS_KEY” Blowfish key.
The “PORTAL” database is associated with the Web Screens functionality, as described in Section 5-3 of the Sage 300 Administration and Installation guide. If an organization had Sage 300 deployed in a workstation configuration as well as a Web Screens configuration, then a workstation user could decrypt the SQL connection password in the “dbconfig.xml” file in the Shared Data folder to access the Sage 300 Web Screens “PORTAL” database.
We’ll access the “PORTAL” database to investigate the potential damage an attacker could do with access. The “PORTAL” database supposedly stores configuration used exclusively by the Web Screens feature and, as such, has far fewer database tables than the company data. By dumping data for the interesting tables, we eventually come across the “UserTenant” table, which contains user data and corresponding “ApplicationPasswords”, as pictured below:
Sage300-2-88: The contents of the “UserTenant” table in the “PORTAL” database.
These “ApplicationPasswords” stored in the database look quite similar to the SQL password we just looked at in the “dbconfig.xml”. It seems probable that these have been encrypted with the same Blowfish function as everything else we’ve been looking at. We could dive back into the code to try to figure out how these passwords are encrypted, or we could just try all of the keys we’ve collected so far and see if something works.
Sage300-2-89: Attempting to decrypt the stored passwords using each of the recovered Blowfish keys.
Attempting to decrypt the encrypted “ApplicationPassword” for the “Admin” user with the three different keys we’ve collected so far reveals that the “LandlordPassKey” can be used to successfully decrypt these passwords. This means that an attacker who could gain access to the “PORTAL” database (as we showed was possible for workstation users via the “dbconfig.xml” file) could recover all the passwords for Sage 300 Web Screen users. These passwords could then be tested for other accounts in an organization’s environment. For example, perhaps a user reused their Sage 300 Web Screen password for their corporate email account.
After answering some of our outstanding questions and discovering other vulnerabilities along the way, our next step is to weaponize the discovered vulnerabilities.
In the second part of our Sage 300 case study, we walked through the process of reverse engineering the encryption algorithm used by Sage 300 so we could decrypt passwords stored in the ISM files and, in the process, discovered a pattern of hardcoded secrets being used through the application binaries. We explored each vulnerability we discovered to figure out the impact and then developed proof-of-concept code snippets to exploit each one. In the end we had developed the capability to extract plaintext user credentials and SQL strings from the ISM files, decrypt passwords stored in the PORTAL database (Web Screens functionality), obtain administrator access to the Apache Solr instance associated with the Global Search feature, and retrieve other secrets stored in configuration files.
In the next part of our case study, we’ll discuss the weaponization process, outlined available mitigations, discuss the lessons learned for administrators and developers, and provide the high-level summary of our communications with the vendor as we worked to get these issues fixed.
Read Access Control Facades and Hardcoded Secrets: A Sage 300 Case Study (Part 3).
1 min read
This is a continuation of the Sage 300 case study series where we explore the process of discovering and developing exploits for six (6) different...
In 2022 Konrad Haase, a member of the Control Gap Offensive Security team, discovered a series of vulnerabilities in Sage 300, a well-established...
1 min read
In modern cyberattacks, threat actors will often begin their attacks against enterprises by obtaining low-privileged access to a single system in the...