Task xss02: Tweeter
Tl;dr
Use a simple stored XSS in the username to make the admin fetching JS code from a storage.google domain, as the CSP doesn’t allow inline js, but is misconfigured and allows user controlled urls to load js from. There you can store JS code, that leaks the admins DMs once he accesses the website.
Description
I found this Twitter (sometimes also known as X) clone. It seems like only a few people are using it to post their (sometimes out-of-date) jokes.
It’s not really something for me, but I hope you can get some nice entertainment from it!
Hint 1: The admin uses a recent version of the Chromium browser.
Hint 2: The goal of this challenge is to leak the admin’s direct messages.
Overview
The website consists of a register + login page. Additionally you can select the feed page to see some random tweets from people you follow (randomly selected) and another page where you can tweet and see (only!) your own tweets.
All tweets can be reported anytime. Even if you can’t see them in your tweet and you can also report your own tweets. You can also see easily, that a report requires the id of the tweet, which is incremented through all users for all tweets. So the tweet that was created after the tweet with id $n$ is obviously $n+1$.
Code
The code reveals, that the admin is actually visiting the users profile containing all tweets, once a tweet is reported.
Also we can see, that many api endpoints, are only accessible for the admin. For example looking at others dms is “of course” ( nice privacy ) only possible for admins:
@RequestMapping(value = "/user/{username}/dms/{other}", method = RequestMethod.GET)
public List<MessageInfo> dmhistory(@PathVariable(value="username") String username, @PathVariable(value="other") String other, Authentication auth, HttpServletRequest request, HttpServletResponse response) {
if(auth == null || !auth.isAuthenticated())
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
/* Only admins can use the API to look at other feeds */
if(!request.isUserInRole("ADMIN") && !request.getRemoteUser().equals(username))
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
if(request.isUserInRole("ADMIN"))
tweeterInitializer.updateFlagDm();
return dmRepository.getDmHistory(username, other).stream().map(DirectMessage::getInfo).collect(Collectors.toList());
}
But this really is, what we want to achieve in the end. As the only thing we can input in this website are the following three things, we need to check them:
- id of the reported tweet
- username + password
- tweet content
WebSecurityConfig
Last but not least let’s check out this very important file for WebSecurity: WebSecurityConfig.java:
/* We want to allow users with slashes and our API encodes the usernames in the URI */
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
DefaultHttpFirewall firewall = new DefaultHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
Ok, weird: slashes are allowed in usernames.
Here we can see the defined CSP, which is obviously flawed for several reasons.
/* Allow googleapis so we can use jQuery */
.and().headers().contentSecurityPolicy("script-src 'self' https://*.googleapis.com");
Read more about this here and this post, as well as some others proposed some exploit for the missing object-src policy, but this attack vector didn’t work out for me (I would be grateful, if someone has insights here).
Still wilcards are usually if used unthoughtful a bad idead in those whitelisting cases. So in this case.
Vulnerability
Report
Through the fact that usernames can include slashes we are able to let the admin visit any page we want relative to the original and with the condition that it has to end with /tweets. This vulnerability isn’t actually needed in the end, which will turn out to be a common pattern in this challenge
XSS
We can input Username, Password and Tweet content. As Passwords are naturally never shown but only saved in the database we only have to check username and tweet content for XSS. As the username is pasted directly as a string into the html code, this should give us an obvious xss: This is how the username is included:
<meta name="user" content="${sessionScope.user}"/>
And this is a conceptual XSS vector:
{username2}">{XSS}
While username2 will be now interpreted as the username you could use here some other actually existing username to make the page still working, but you don’t need that as we will only work with the api.
This closes the current meta tag and everything that comes after will be interpreted as html tags/anchors.
Another XSS vulnerability can also be found in the tweet content. It seems like it is sanitized.
sanitized_content = DOMPurify.sanitize(tweet.content);
But looking into the sanitizer or specifically in its version that is shipped with, we can see that DomPurify 2.2.8 is used, which is very old by may 2025 and several xss attack vectors have been found in this version. So we could also use some of these to get xss, if the one with the Username was not here. For example this creates a working XSS in this version:
def generate_mxss_payload():
html = ""
html += "<div>" * 506
html += """<table>
<caption>
<svg>
<title>
<table><caption></caption></table>
</title>
<style><a id="</style><img src=https://requestbin.kanbanbox.com/14987us1 onerror=alert(1)>"></a></style>
</svg>
</caption>
</table>"""
return html
CSP
The problem is that even though we have “XSS”, we don’t. The CSP is effectively forbidding executing inline js and hence we cannot execute anything. So now let’s discuss the wildcard mentioned above. In this case every service from googles api is allowed to be used, to load js code from. This includes:
- the storage api
- JSONP (legacy) endpoints
Storage API
The storage api offers to upload arbitrary content and load it from there. So we can just create a so called google bucket upload our xss code there and retrieve it with <script src={url_to_xss}></script>. Now this will be executed as the CSP allows laoding js from this source.
JSONP
Another solution includes using JSONP endpoints. Basically they allow through the callback url parameter arbitrary js code execution. Their existence is outdated and was introduced before CORS header replaced them, to “bypass” (from a developer POV) the same-origin policy.
Unfortunately even google still has some of them open as listed here.