Vulnerability Overview
Confluence is a collaboration product developed by Atlassian, widely used for knowledge sharing, document collaboration, and centralized information storage.
CVE-2023-22515 is a privilege escalation vulnerability that allows an attacker to create a new administrator account in the following affected versions:
- 8.0.0 < Confluence < 8.2.3
- Confluence < 8.3.3
- Confluence < 8.4.3
- Confluence < 8.5.2
Analysis
Since this issue is already patched, I followed the usual workflow and diffed the changes. I used the 8.5.1 and 8.5.2 JARs for comparison:

The notable change is that several classes were removed/added.
Removed:
ServerInfoActionServerInfoFilter
Added:
ReadOnlyApplicationConfigReadOnlySetupPersister
The two ReadOnly* classes extend existing classes and act as “hardened” wrappers: they throw UnsupportedOperationException on setters to prevent modifications, effectively making the objects read-only to reduce risk.
This is reflected in the BootstrapStatusProviderImpl change: the original this.delegate.getApplicationConfig(); becomes return new ReadOnlyApplicationConfig(this.delegate.getApplicationConfig());

Next question: where are these properties being set? Looking at interceptors and the official description, SafeParametersInterceptor immediately stood out:


It injects form parameters into action properties, so this is very likely the relevant path. The interceptor contains a number of safety checks (especially around the @ParameterSafe annotation), and there is a doInterceptor implementation:

I tried jumping to the parent class but my IDE couldn’t resolve it (Cannot find declaration to go to). I was lazy, so I searched the documentation and then checked the source on GitHub:


After reading the code, the issue becomes clear: after obtaining the action it only checks NoParameters and then calls setParameters:

For Struts-style vulnerabilities, the usual processing chain is: interceptor → Action.
This XML defines an interceptor named params and binds it to com.atlassian.xwork.interceptors.SafeParametersInterceptor:

Then we look for reachable trigger points. Searching for params yields something promising:



After checking the stacks, it appears that any action not using setupStack is potentially usable (in my tests, only validatingStack or the default stack worked; other stacks were reachable but didn’t create the user, not sure why):

That’s the end of the analysis.
Reproduction
Set up a test environment:


The sqlserver version was too old, so I gave up on that path (just kidding).
I ended up using PostgreSQL instead:

Once the initial setup page was reachable, I opened Burp and visited /setup/setupadministrator-start.action. It could no longer enter the registration flow:

After overwriting the relevant properties and refreshing, the admin setup flow became available again:

After the new admin is created, confirm the result:

RCE has already been discussed by others. I plan to look for a more convenient route; if I find anything, I’ll write a follow-up.