Linux Cap: Cara Elevasi Privilege tanpa Menjadi Root

ยท

5 min read

Jadi kemarin aku coba bikin-bikin aplikasi yang harus listen di restricted port. Restricted port ini yang biasanya ada di rentang 1 - 1024. Di atas itu baru bisa pakai user biasa buat listen port, misal mau jalanin aplikasi di port 8000 ya tidak ada masalah. Sebagai contoh, aku coba tulis program simpel biar kebayang.

package main

import (
    "log"
    "net/http"
)

func main() {
    handler := http.NewServeMux()

    handler.HandleFunc("/", HandleHealthCheck)

    log.Printf("listening app in localhost:80")
    if err := http.ListenAndServe("localhost:80", handler); err != nil {
        log.Panic(err)
    }
}

func HandleHealthCheck(rw http.ResponseWriter, r *http.Request) {
    rw.Write([]byte("service is healthy"))
}

Potongan kode di atas adalah aplikasi web sederhana yang jalan di port 80. Port 80 ini kan termasuk di port yang restricted, jadi butuh privilege untuk dapat menjalankannya. Beberapa hal yang bisa dilakukan untuk jalankan aplikasi ini adalah dengan menjalankannya dengan menjadi root terlebih dahulu.

Untuk Menjalankannya, aku juga bikin bash script untuk build dan menjalankan binary yang telah dibuat.

#!/bin/bash
go build -o main app.go

if [[ $1 == '--run' ]]; then
  ./main
fi

Untuk menjalankannya tinggal panggil script-nya aja seperti ini

โžœ  example-linux-cap$ sh build.sh --run

Maka hasil keluarannya akan kurang lebih seperti di bawah.

2023/07/29 22:10:23 listening app in localhost:80
2023/07/29 22:10:23 listen tcp 127.0.0.1:80: bind: permission denied
panic: listen tcp 127.0.0.1:80: bind: permission denied

goroutine 1 [running]:
log.Panic({0xc0000bff50?, 0x67e1bb?, 0x0?})
        /usr/lib/go/src/log/log.go:384 +0x65
main.main()
        /home/rendy/Workspace/private/example-linux-cap/app.go:15 +0x105

Menjadi Root ๐Ÿฅš

Cara yang paling mudah adalah dengan menjadi root. Menjalankannya hanya cukup dengan prefix sudo. Untuk awalan mari gunakan cara bodoh untuk menjalankan aplikasi tersebut. Kenapa cara bodoh, karena dapat mengakibatkan peretas mendapatkan bug yang ada di aplikasi dan mengeksploitasinya. Tapi tidak apa-apa karena ini bagian dari belajar. Nanti kita juga akan belajar cara yang lebih baik.

Pertama-tama ganti user ke root terlebih dahulu.

โžœ  example-linux-cap$ sudo su
โžœ  example-linux-cap sudo su
[sudo] password for rendy: 
[root@canvas-mobile example-linux-cap]# whoami
root

Setelah menjadi root, mari dicoba kembali untuk menjalankan aplikasi.

[root@canvas-mobile example-linux-cap]# sh build.sh --run
2023/07/29 22:21:38 listening app in localhost:80

Aplikasi sukses berjalan. Untuk memastikan, bisa menggunakan perintah curl ke localhost:80. Hasilnya akan seperti di bawah.

โžœ  example-linux-cap curl localhost:80
service is healthy%

Cara ini juga bisa diraih dengan menggunakan sudo, tanpa mengganti user ke root. Caranya adalah seperti ini.

[root@canvas-mobile example-linux-cap]$ sudo sh build.sh --run
2023/07/29 22:21:38 listening app in localhost:80

Untuk memastikan, dapat menggunakan perintah curl seperti yang sebelumnya.

โžœ  example-linux-cap curl localhost:80
service is healthy%

Cara ini sukses, tapi sangat tidak dianjurkan demi keamanan server, karena ketika sekali saja terkena serangan exploit, seluruh akses di server akan juga dapat diambil alih oleh penyerang (hacker). Fatal banget pengaruhnya.

Menggunakan Linux Cap ๐ŸŽฉ

Sekarang, menuju ke hidangan utama yaitu ke Linux capabilities. Sebenarnya capabilities ini sudah lama dirilis, sejak kernel versi 2.2, tapi dokumentasinya cukup minim. Kegunaannya juga cukup low level, jadi ini jarang digunakan oleh end-user. Tapi sebenarnya fitur ini banyak digunakan untuk menjalankan aplikasi yang rootless, seperti podman yang merupakan tool untuk memanajemen container yang dapat berjalan secara rootless.

Kembali ke linux capabilities, dilansir dari laman linux man-pages (Halaman manual linux), capabilities ini dipecah menjadi lebih kecil-kecil sesuai dengan aksi yang ingin dilakukan. Untuk lengkapnya, bisa dilihat langsung di halaman dokumentasinya, namun sebagai contoh, ini aku lampirkan sedikit di bawah.

CAP_NET_BIND_SERVICE
    Bind a socket to Internet domain privileged ports (port
    numbers less than 1024).

CAP_NET_BROADCAST
    (Unused)  Make socket broadcasts, and listen to
    multicasts.

CAP_NET_RAW
    โ€ข  Use RAW and PACKET sockets;
    โ€ข  bind to any address for transparent proxying.

Pada kasus ini, yang diperlukan untuk listen di restricted port, berarti memerlukan capabilityCAP_NET_BIND_SERVICE. Untuk memberikan capabilities pada sebuah file atau binary, diperlukan pengetahuan juga terkait tipe capability yang akan dilampirkan. Dikutip dari laman linux man, ada 3 tipe capability yang tersedia.

Permitted (formerly known as forced):
    These capabilities are automatically permitted to the
    thread, regardless of the thread's inheritable
    capabilities.

Inheritable (formerly known as allowed):
    This set is ANDed with the thread's inheritable set to
    determine which inheritable capabilities are enabled in
    the permitted set of the thread after the execve(2).

Effective:
    This is not a set, but rather just a single bit.  If this
    bit is set, then during an execve(2) all of the new
    permitted capabilities for the thread are also raised in
    the effective set.  If this bit is not set, then after an
    execve(2), none of the new permitted capabilities is in
    the new effective set.

Berdasarkan penjelasan tersebut, kita tidak bisa menggunakan tipe Inheritable karena dia tipenya diwariskan, jadi belum parent process thread yang akan dijalankan akan memiliki capabilityCAP_NET_BIND_SERVICE. Sehingga, kita perlu menambahkan capability CAP_NET_BIND_SERVICE dengan tipe Permitted (untuk memastikan) dan Effective. Setelah mengetahui, berarti build script yang telah dibuat tadi perlu dilakukan penambahan. Untuk memberikan capability pada sebuah file, terdapaat perintah program setcap.

Untuk menggunakannya, diperlukan minimal 2 argumen yang pertama adalah capability nya dalam bentuk string dan target file nya

# setcap <capabilities> <target-file>

Berdasarkan dokumentasi, format capabilities string berbentuk <capability>=type. Untuk capability yang dibutuhkan yaitu CAP_NET_BIND_SERVICE dan tipenya disingkat, e untuk effective dan p untuk permitted. Sehingga formatnya menjadi seperti ini cap_net_bind_service=ep. Setelah itu, build script yang tadi diubah menjadi seperti ini.

#!/bin/bash
go build -o main app.go
sudo setcap 'cap_net_bind_service=ep' main

if [[ $1 == '--run' ]]; then
  ./main
fi

The Moment of truth ๐Ÿฅ

Sekarang waktunya membuktikan apakah berhasil menggunakan linux capabilities.

โžœ  example-linux-cap sh build.sh 
โžœ  example-linux-cap ./main 
2023/07/29 22:55:19 listening app in localhost:80

Hasil keluaran terminal di atas menandakan bahwa tanpa user root, menjalankan aplikasi web di restricted port tetap bisa dicapai menggunakan linux capabilities.

Jadi apa yang bisa disimpulkan terkait percobaan ini? Ya tidak semuanya harus menjadi root. Linux capabilities memberikan kenyamanan untuk mengatur dan mengerucutkan permission dengan lebih detail.

Agar aman dalam menggunakan linux cap adalah, ekspektasinya memberikan capabilities pada sebuah binary ketika installation. Karena di saat itu-lah membutuhkan akses root untuk menambahkan capability. Di luar itu, linux kernel yang akan melakukan pengecekan capability pada sebuah binary. Sehingga scopenya benar-benar kecil, yaitu di level capability pada sebuah thread process, tidak di level user. Dengan terbatasnya permission yang diset pada sebuah binary, dapat memungkinkan kita memperkecil kemungkinan untuk diretas. Istilah kerennya sih Hardening -- ๐Ÿšง.

ย