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 -- ๐ง.