CVE-2023-22515 - Broken Access Control Vulnerability in Confluence Data Center and Server
I saw the below tweet by TheGrandPew on my feed, and I wanted to give it a try along with Rahul Maini. Me, Rahul and few other friends were attending BSides Ahmedabad Conference. So thought, it would be fun exercise to reverse it while attending some talks.
The tweet sounded like the vulnerablity is very easy to reproduce. But, to be honest, I spent so much time like 10-12 hours every day for 2 days straight, maybe because I don't have experience in Java exploitation( I always hated Java challenges in CTF).
It is my first time looking at Apache struts application, never got chance to work on it and I had zero knowledge on how it works which made it really time consuming as I have to search for "What is struts.xml", "What is ONGL?" etc.
Initial Diffing between 8.5.1 and 8.5.2
After reading the advisory, it was apparent that the vulnerability exist in /setup/* actions.
Downloaded the vulnerable and fixed version from here and started working on it.
Rabbit Hole / Mistake 1
The confluence archieve is very big and there are lot of jar files in it. So, we mistakenly decided to only look at diff for the the JAR files which has "confluence" name in it. Using, the below command you can extract the source code from JAR files.
find ./atlassian-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
Using, VSCode Diff plugin and looking at the changed files and modified code.
There are few other files which has code changed, but they didn't look interesting. However, The above file caught the eye, as you can see they are wrapping ApplicationConfig and SetupPersister with ReadOnly*** classes and these new ReadOnly** class files were newly added in the 8.5.2 fixed version.
If you notice the overridden functions of these classes it can be noticed that functions such as setSetupComplete, setSetupType, and setProperty were all disallowed.
The Goal
Looking at the advisory and patches made one thing clear, which is we need to somehow make the setup to incomplete which restarts the confluence setup; thereby allowing us to add new admin user or add new database configuration.
Having this in mind, started looking at ways to modify the application config in only the JARs with "confluence" name in it.
Looking at the struts.xml and specifically namespace="/setup" which is responsible for all the setup related endpoints/actions, started reviewing the source code of these Actions. None of them has anything interesting which can help us modify the application config.
struts.xml is a configuration file used in the Apache Struts framework for defining the mapping between web requests and corresponding actions or resources, facilitating the creation of Java web applications with predefined conventions and configurations.
All of the requests /setup/** endpoint are intercepted by below struts.xml interceptor. Interceptors are just middle-wares which intercept request and responses.
<interceptor name="setup" class="com.atlassian.confluence.setup.actions.SetupCheckInterceptor"/>
[...]
<package name="setup" extends="default" namespace="/setup">
<default-interceptor-ref name="validatingSetupStack"/>
[...]
By looking at the source code of SetupCheckInterceptor, we can deduce that we need to return isSetupComplete to be false, so that we can hit the /setup/ endpoints.
#Archive2/8.5.1/sources/com/atlassian/confluence/setup/actions/SetupCheckInterceptor.java
public String intercept(ActionInvocation actionInvocation) throws Exception {
if (BootstrapUtils.getBootstrapManager().isSetupComplete() && ContainerManager.isContainerSetup()) {
return ALREADY_SETUP;
}
return actionInvocation.invoke();
}
#Archive2/8.5.1/sources/com/atlassian/config/ApplicationConfig.java
public class ApplicationConfig implements ApplicationConfiguration {
@Override // com.atlassian.config.ApplicationConfiguration
public synchronized boolean isSetupComplete() {
return this.setupComplete;
}
Deadlock???
Our task is to somehow make setupComplete to false, and the calls to setSetupComplete(false) are made inside /setup/** actions. There is no way to reach setSetupComplete calls without bypassing SetupCheckInterceptor. So, I started looking at other conditions in interceptor which is ContainerManager.isContainerSetup(). I attached debugger and see what it does and realized that its not possible to make it return false.
Our task is to somehow make setupComplete to false, and the calls to setSetupComplete(false) are made inside /setup/** actions. There is no way to reach setSetupComplete calls without bypassing SetupCheckInterceptor. So, I started looking at other conditions in interceptor which is ContainerManager.isContainerSetup(). I attached debugger and see what it does and realized that its not possible to make it return false.
I was stuck here for several hours debugging, and trying random stuff but not going anywhere and I was getting frustrated as there is no way I can set setupComplete to false. Then Pew told me to look at other interceptors, so I started decompiling all the other JAR files and see the differences. Upto my surprise, there is one interesting JAR which has lot of changes which is /atlassian-confluence-8.5.2/confluence/WEB-INF/lib/com.atlassian.struts2_struts-support-1.2.0.jar. At this moment, I felt so frusrtated because of not diffing whole source code which costed me several hours on doing random shit.
SafeParametersInterceptor.java
In com.atlassian.struts2_struts-support-1.2.0.jar, there is one file that got modified which is sources/com/atlassian/xwork/interceptors/SafeParametersInterceptor.java. This file is an one more interceptor in struts.xml, which turned out to be responsible for parsing the query parameters in the URL, parsing the POST body parameters.
<interceptor name="params" class="com.atlassian.xwork.interceptors.SafeParametersInterceptor"/>
I started reading the code of SafeParameterInterceptors and it looked very interesting, and little bit surprised on the way they handling the request parameters. So what it does is it intercepts incoming HTTP requests, extracts parameters, and sets them as properties of the action being invoked.
For example if you are calling /person.action?name=s1r1us, it parses the query parameter name=s1r1us, and on the PersonAction.class object it calls the method of PersonAction, which is PersonAction.setName('s1r1us').
Interesting enough, you can also have nested parameters like /person.action?address.city=asdf which will call PersonAction.getAddress().setCity('asdf'). The parsing of the query parameters and executing them on the Action objects are performed using OGNL.
In the first look, this looked so surprising and insecure but this is how struts also does in ParamInterceptor which SafeParameterInterceptor inherits. And obviously as expected, there was an RCE in struts on this ParamInterceptor once? here?.
My initial idea is to used nested parameters to access and set setupcomplete to false on AppliccationConfig. This can be done as
((LoginAction) action).getBootstrapStatusProvider().getApplicationConfig().setSetupComplete(false)
When its translated to query parameter this would look something like
https://vuln.com/some.action?bootstrapStatusProvider.applicationConfig.setupComplete=0
or
https://vuln.com/some.action?bootstrapStatusProvider['applicationConfig']['setupComplete']=false
This would be turned to Action.getBlah().getApplicationConfig().setSetupComplete('false')
But, again I fell in another Rabbit hole which again costed me so many hours.
Rabbit Hole 2 - Trying to bypass isSafeComplexParameterName
We can't just use nested parameters and modify ApplicationConfig because, SafeParameterInterceptor parses the given parameters and uses regular expression to determine if its of form test['asd'], test[0], and test.asd, if it determines its a complex parameter, then it checks if the @ParameterSafe annotation is set on the current action/endpoint we are calling. If only the said annotation exist then only we can use complex nested parameters. If ParameterSafe annotation is not set, we can only use basic parameters (?a=b&c=d) and not complex parameters (?a.b=1&c['d']=1)
I checked what all other actions has @ParameterSafe annotation, but most of them need authentication.
One interesting action has this annotation @ParameterSafe which is /setup/setupdbtype.action, sadly we can't reach this cuz we need isSetupComplete to be true which is what we are trying xD.
The Rabbit hole I fell is trying to bypass isSafeComplexParameterName. If we can craft a nested URL parameter which can fool isSafeParameterName to think its a basic/safe parameter then the annotation check won't be performed. I spent lot of time spending trying ways to craft a URL parameter.
One URL which bypasses the check is, notice [0] the Pattern.compile(".*\\['[a-zA-Z0-9_]+'\\]").matcher(key).matches() regex fails recognising this as complex parameter and returns false, which is what we needed.
https://vuln.com/some.action?bootstrapStatusProvider['applicationConfig'][0]=0
I spent lot of time spending ways to find gadget where I can modify Array/List which will trigger the vulnerability. Sadly, I failed in it.
Funny enough, the URL I crafted 8 hours back which is enough to trigger the vulnerability and make the setupComplete to false. I know why it works, check the above code clearly and try to figure it out. And share the solution in DMs with me :)
https://vuln.com/some.action?bootstrapStatusProvider.applicationConfig.setupComplete=0
Once we make setupComplete to false, you can just create an admin user and get access to Confluence.
Conclusions
I had great fun working along with Rahul Maini, and suffering while working on this. Learned a lot about Struts, and JAVA. I hope you learn something with the blog, with that I will leave two tasks to the reader. And Thanks to TheGrandPew for the inspiration(I still dont understand how he was so quick xd)
Task #1
Task #1
Figure out why the above URL works without needing to bypass isSafeComplexParameterName. Reply in the tweet please.
Solvers:
you?
Task #2
In the latest version, the above bypass I found with array index accessor still works, I couldn't find a way to exploit this if you can find a gadget that would modify some array index, which can change some config, or something impactful, then it would lead to one more 0-day.
https://vuln.com/some.action?bootstrapStatusProvider['applicationConfig'][0]=0
Solvers:
you?