Jika Anda telah mengikuti unix.stackexchange.com untuk sementara waktu, Anda
diharapkan sudah tahu sekarang bahwa membiarkan variabel
tidak dikutip dalam konteks daftar (seperti dalam echo $var
) di Bourne/POSIX
shells (zsh menjadi pengecualian) memiliki arti yang sangat khusus dan
tidak boleh dilakukan kecuali Anda memiliki alasan yang sangat baik untuk melakukannya.
Ini dibahas panjang lebar dalam sejumlah T&J di sini (Contoh:Mengapa skrip shell saya tersedak spasi putih atau karakter khusus lainnya?, Kapan kutipan ganda diperlukan?, Perluasan variabel shell dan efek glob dan split di atasnya, Dikutip vs ekspansi string yang tidak dikutip )
Itulah yang terjadi sejak rilis awal shell
Bourne di akhir tahun 70-an dan belum diubah oleh shell Korn
(salah satu penyesalan
terbesar David Korn (pertanyaan #7 )) atau bash
yang sebagian besar
menyalin shell Korn, dan begitulah yang telah ditentukan oleh POSIX/Unix.
Sekarang, kami masih melihat sejumlah jawaban di sini dan bahkan
terkadang merilis kode shell untuk publik di mana
variabel tidak dikutip. Anda akan mengira orang akan
belajar sekarang.
Dalam pengalaman saya, ada 3 tipe orang yang mengabaikan
mengutip variabel mereka:
-
pemula. Itu dapat dimaafkan karena ini adalah
sintaks yang sama sekali tidak intuitif. Dan itu adalah peran kami di situs ini
untuk mendidik mereka. -
orang yang pelupa.
-
orang-orang yang tidak yakin bahkan setelah dipalu berulang kali,
yang berpikir bahwa pasti pembuat cangkang Bourne tidak
bermaksud agar kami mengutip semua variabel kami .
Mungkin kita bisa meyakinkan mereka jika kita mengungkap risiko yang terkait dengan
perilaku semacam ini.
Hal terburuk apa yang mungkin terjadi jika Anda
lupa mengutip variabel Anda. Apakah benar-benar itu buruk?
Jenis kerentanan apa yang kita bicarakan di sini?
Dalam konteks apa itu bisa menjadi masalah?
Jawaban yang Diterima:
Pembukaan
Pertama, saya akan mengatakan itu bukan cara yang tepat untuk mengatasi masalah.
Ini seperti mengatakan “Anda tidak boleh membunuh orang karena
jika tidak, Anda akan masuk penjara “.
Demikian pula, Anda tidak mengutip variabel Anda karena jika tidak
Anda memperkenalkan kerentanan keamanan. Anda mengutip
variabel Anda karena itu salah untuk tidak (tetapi jika takut penjara dapat membantu, mengapa tidak).
Sedikit ringkasan untuk mereka yang baru saja naik kereta.
Di sebagian besar shell, membiarkan ekspansi variabel tidak dikutip (meskipun
itu (dan sisa jawaban ini) juga berlaku untuk substitusi perintah
(`...`
atau $(...)
) dan ekspansi aritmatika ($((...))
atau $[...]
)) memiliki arti
yang sangat istimewa. Cara terbaik untuk menggambarkannya adalah seperti
memanggil semacam split+glob implisit operator¹.
cmd $var
dalam bahasa lain akan ditulis sesuatu seperti:
cmd(glob(split($var)))
$var
pertama-tama dipecah menjadi daftar kata menurut aturan
kompleks yang melibatkan $IFS
parameter khusus (split part)
dan kemudian setiap kata yang dihasilkan dari pemisahan tersebut dianggap sebagai
sebuah pola yang diperluas ke daftar file yang cocok dengannya
(glob bagian).
Sebagai contoh, jika $var
berisi *.txt,/var/*.xml
dan $IFS
berisi ,
, cmd
akan dipanggil dengan sejumlah argumen,
yang pertama adalah cmd
dan yang berikutnya adalah txt
file di direktori saat ini dan xml
file dalam /var
.
Jika Anda ingin memanggil cmd
hanya dengan dua argumen literal cmd
dan *.txt,/var/*.xml
, Anda akan menulis:
cmd "$var"
yang akan menggunakan bahasa lain yang lebih Anda kenal:
cmd($var)
Apa yang kami maksud dengan kerentanan dalam cangkang ?
Lagi pula, sudah diketahui sejak awal waktu bahwa skrip
shell tidak boleh digunakan dalam konteks yang sensitif terhadap keamanan.
Tentu, OK, membiarkan variabel tidak dikutip adalah bug, tetapi itu tidak bisa
sangat merugikan bukan?
Yah, terlepas dari kenyataan bahwa siapa pun akan memberi tahu Anda bahwa skrip shell
tidak boleh digunakan untuk CGI web, atau untungnya
sebagian besar sistem tidak mengizinkan skrip shell setuid/setgid saat ini,
satu hal yang diungkapkan shellshock (bug bash yang dapat dieksploitasi dari jarak jauh
yang menjadi berita utama pada bulan September 2014) adalah bahwa
shell masih banyak digunakan di tempat yang seharusnya tidak:
di CGI, di klien DHCP skrip kait, dalam perintah sudoers,
dipanggil oleh (jika bukan sebagai ) setuid perintah…
Terkadang tanpa disadari. Misalnya system('cmd $PATH_INFO')
dalam php
/perl
/python
Skrip CGI tidak memanggil shell untuk menafsirkan baris perintah itu (tidak untuk
menyebutkan fakta bahwa cmd
itu sendiri mungkin merupakan skrip shell dan pembuatnya
mungkin tidak pernah mengharapkannya dipanggil dari CGI).
Anda memiliki kerentanan ketika ada jalur untuk eskalasi
hak istimewa, yaitu ketika seseorang (sebut saja dia penyerang )
mampu melakukan sesuatu yang tidak seharusnya.
Itu selalu berarti penyerang menyediakan data, bahwa data
sedang diproses oleh pengguna/proses yang memiliki hak istimewa yang secara tidak sengaja
melakukan sesuatu yang seharusnya tidak dilakukan, dalam sebagian besar kasus karena
bug.
Pada dasarnya, Anda mendapat masalah ketika kode buggy Anda memproses
data di bawah kendali penyerang .
Sekarang, tidak selalu jelas di mana data itu mungkin berasal dari,
dan seringkali sulit untuk mengetahui apakah kode Anda akan pernah
memproses data yang tidak tepercaya.
Sejauh menyangkut variabel, Dalam kasus skrip CGI,
cukup jelas, datanya adalah parameter CGI GET/POST dan
hal-hal seperti cookie, path, host… parameter.
Untuk skrip setuid (berjalan sebagai satu pengguna saat dipanggil oleh
yang lain), itu adalah argumen atau variabel lingkungan.
Vektor lain yang sangat umum adalah nama file. Jika Anda mendapatkan
daftar file dari direktori, kemungkinan file tersebut telah
ditanam di sana oleh penyerang .
Dalam hal itu, bahkan pada prompt shell interaktif, Anda
dapat menjadi rentan (saat memproses file dalam /tmp
atau ~/tmp
misalnya).
Bahkan ~/.bashrc
dapat menjadi rentan (misalnya, bash
akan
menafsirkannya saat dipanggil melalui ssh
untuk menjalankan ForcedCommand
seperti di git
penerapan server dengan beberapa variabel di bawah kendali
klien).
Sekarang, skrip mungkin tidak dipanggil secara langsung untuk memproses data
yang tidak dipercaya, tetapi mungkin dipanggil dengan perintah lain yang melakukannya. Atau
kode Anda yang salah mungkin disalin-tempel ke skrip yang melakukannya (oleh Anda 3
tahun ke depan atau salah satu kolega Anda). Satu tempat di mana
sangat kritis ada dalam jawaban di situs Tanya Jawab karena Anda
tidak akan pernah tahu di mana salinan kode Anda akan berakhir.
Turun ke bisnis; seberapa parah?
Membiarkan variabel (atau substitusi perintah) tidak dikutip sejauh ini
merupakan sumber kerentanan keamanan nomor satu yang terkait
dengan kode shell. Sebagian karena bug tersebut sering diterjemahkan ke
kerentanan tetapi juga karena sangat umum untuk melihat variabel
yang tidak dikutip.
Sebenarnya, ketika mencari kerentanan dalam kode shell,
hal pertama yang harus dilakukan adalah mencari variabel yang tidak dikutip. Sangat mudah untuk
dikenali, seringkali merupakan kandidat yang baik, umumnya mudah dilacak kembali ke
data yang dikendalikan penyerang.
Ada banyak cara variabel yang tidak dikutip dapat mengubah
menjadi kerentanan. Saya hanya akan memberikan beberapa tren umum di sini.
Pengungkapan informasi
Kebanyakan orang akan menemukan bug yang terkait dengan variabel
yang tidak dikutip karena split bagian (misalnya, sekarang ini
umum untuk file memiliki spasi di namanya dan spasi
dalam nilai default IFS). Banyak orang akan mengabaikan glob bagian. bola bagian setidaknya sama berbahayanya dengan split bagian.
Globbing dilakukan pada input eksternal yang tidak bersih berarti
penyerang dapat membuat Anda membaca konten direktori mana pun.
Dalam:
echo You entered: $unsanitised_external_input
jika $unsanitised_external_input
berisi /*
, artinya
penyerang dapat melihat isi /
. Bukan masalah besar. Ini menjadi
lebih menarik meskipun dengan /home/*
yang memberi Anda daftar
nama pengguna di mesin, /tmp/*
, /home/*/.forward
untuk
petunjuk tentang praktik berbahaya lainnya, /etc/rc*/*
untuk layanan
yang diaktifkan… Tidak perlu memberi nama satu per satu. Nilai /* /*/* /*/*/*...
hanya akan mencantumkan seluruh sistem file.
Penolakan kerentanan layanan.
Mengambil kasus sebelumnya agak terlalu jauh dan kami mendapatkan DoS.
Sebenarnya, setiap variabel yang tidak dikutip dalam konteks daftar dengan input
yang tidak bersih adalah setidaknya kerentanan DoS.
Bahkan ahli skrip shell biasanya lupa mengutip hal-hal
seperti:
#! /bin/sh -
: ${QUERYSTRING=$1}
:
adalah perintah tanpa operasi. Apa yang mungkin salah?
Itu dimaksudkan untuk menetapkan $1
ke $QUERYSTRING
jika $QUERYSTRING
tidak disetel. Itulah cara cepat untuk membuat skrip CGI dapat dipanggil dari
baris perintah juga.
$QUERYSTRING
. itu masih diperluas dan karena
tidak dikutip, split+glob operator dipanggil.
Sekarang, ada beberapa gumpalan yang sangat mahal untuk
dikembangkan. /*/*/*/*
satu sudah cukup buruk karena itu berarti mendaftar
direktori hingga 4 tingkat ke bawah. Selain aktivitas disk dan CPU
, itu berarti menyimpan puluhan ribu jalur file
(40rb di sini pada server VM minimal, 10rb di antaranya direktori).
Sekarang /*/*/*/*/../../../../*/*/*/*
berarti 40k x 10k dan /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*
sudah cukup untuk
membuat mesin terkuat sekalipun bertekuk lutut.
Cobalah sendiri (meskipun bersiaplah untuk mesin Anda
crash atau hang):
a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'
Tentu saja, jika kodenya adalah:
echo $QUERYSTRING > /some/file
Kemudian Anda dapat mengisi disk.
Cukup lakukan pencarian google di shell
cgi atau bash
cgi atau ksh
cgi, dan Anda akan menemukan
beberapa halaman yang menunjukkan cara menulis CGI di shell. Perhatikan
bagaimana setengah dari parameter proses itu rentan.
Bahkan milik
milik David Korn
rentan (lihat penanganan cookie).
hingga kerentanan eksekusi kode arbitrer
Eksekusi kode arbitrer adalah jenis kerentanan terburuk,
karena jika penyerang dapat menjalankan perintah apa pun, tidak ada batasan
apa yang boleh dia lakukan.
Itu umumnya perpecahan bagian yang mengarah ke itu. Pemisahan
itu menghasilkan beberapa argumen untuk diteruskan ke perintah
ketika hanya satu yang diharapkan. Sementara yang pertama akan digunakan
dalam konteks yang diharapkan, yang lain akan berada dalam konteks yang berbeda
sehingga berpotensi ditafsirkan secara berbeda. Lebih baik dengan contoh:
awk -v foo=$external_input '$2 == foo'
Di sini, tujuannya adalah untuk menetapkan konten $external_input
variabel shell ke foo
awk
variabel.
Sekarang:
$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux
Kata kedua yang dihasilkan dari pemecahan $external_input
tidak ditugaskan ke foo
tetapi dianggap sebagai awk
kode (di sini yang
mengeksekusi perintah arbitrer:uname
).
Itu terutama masalah untuk perintah yang dapat mengeksekusi perintah
lainnya (awk
, env
, sed
(GNU satu), perl
, find
…) terutama
dengan varian GNU (yang menerima opsi setelah argumen).
Terkadang, Anda tidak akan menduga perintah untuk dapat mengeksekusi
perintah lain seperti ksh
, bash
atau zsh
[
atau printf
…
for file in *; do
[ -f $file ] || continue
something-that-would-be-dangerous-if-$file-were-a-directory
done
Jika kita membuat direktori bernama x -o yes
, maka pengujian
menjadi positif, karena ini adalah ekspresi kondisional
yang sama sekali berbeda yang kami evaluasi.
Lebih buruk lagi, jika kita membuat file bernama x -a a[0$(uname>&2)] -gt 1
,
dengan setidaknya semua implementasi ksh (termasuk sh
dari sebagian besar Unice komersial dan beberapa BSD), yang mengeksekusi uname
karena shell tersebut melakukan evaluasi aritmatika pada
operator perbandingan numerik dari [
perintah.
$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux
Sama dengan bash
untuk nama file seperti x -a -v a[0$(uname>&2)]
.
Tentu saja, jika mereka tidak bisa mendapatkan eksekusi sewenang-wenang, penyerang mungkin
menerima kerusakan yang lebih kecil (yang dapat membantu mendapatkan eksekusi
yang sewenang-wenang). Perintah apa pun yang dapat menulis file atau mengubah
izin, kepemilikan, atau memiliki efek utama atau sampingan dapat dieksploitasi.
Segala macam hal dapat dilakukan dengan nama file.
$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done
Dan Anda akhirnya membuat ..
dapat ditulisi (secara rekursif dengan GNU chmod
).
Skrip yang melakukan pemrosesan file secara otomatis di area yang dapat ditulisi publik seperti /tmp
harus ditulis dengan sangat hati-hati.
Bagaimana dengan [ $# -gt 1 ]
Itu sesuatu yang menurut saya menjengkelkan. Beberapa orang mengalami
kesulitan bertanya-tanya apakah ekspansi tertentu mungkin
bermasalah untuk memutuskan apakah mereka dapat menghilangkan tanda kutip.
Ini seperti mengatakan. Hei, sepertinya $#
tidak dapat tunduk pada
operator split+glob, mari kita minta shell untuk membagi+globnya .
Atau Hei, mari kita menulis kode yang salah hanya karena bug
tidak mungkin terkena .
Sekarang seberapa kecil kemungkinannya? Oke, $#
(atau $!
, $?
atau substitusi aritmatika
apa pun) hanya boleh berisi angka (atau -
untuk
beberapa²) jadi glob bagian keluar. Untuk perpecahan bagian untuk melakukan
sesuatu, yang kita butuhkan hanyalah $IFS
berisi angka (atau -
).
Dengan beberapa shell, $IFS
mungkin diwarisi dari lingkungan,
tetapi jika lingkungan tidak aman, permainan akan berakhir.
Sekarang jika Anda menulis fungsi seperti:
my_function() {
[ $# -eq 2 ] || return
...
}
Artinya, perilaku fungsi Anda
bergantung pada konteks pemanggilannya. Atau dengan kata lain, $IFS
menjadi salah satu masukan untuk itu. Sebenarnya, ketika Anda
menulis dokumentasi API untuk fungsi Anda, seharusnya
seperti:
# my_function
# inputs:
# $1: source directory
# $2: destination directory
# $IFS: used to split $#, expected not to contain digits...
Dan kode yang memanggil fungsi Anda perlu memastikan $IFS
tidak
mengandung angka. Semua itu karena Anda tidak ingin mengetik
2 karakter tanda kutip ganda itu.
Nah, untuk itu [ $# -eq 2 ]
bug untuk menjadi kerentanan,
entah bagaimana Anda akan membutuhkan nilai $IFS
menjadi di bawah
kendali penyerang . Bisa dibayangkan, itu tidak akan terjadi
kecuali penyerang berhasil mengeksploitasi bug lain.
Itu tidak pernah terdengar. Kasus umum adalah ketika orang
lupa membersihkan data sebelum menggunakannya dalam ekspresi
aritmatika. Kita telah melihat di atas bahwa itu dapat mengizinkan
eksekusi kode arbitrer di beberapa shell, tetapi di semua shell, ini memungkinkan penyerang untuk memberikan variabel apapun nilai integer.
Misalnya:
n=$(($1 + 1))
if [ $# -gt 2 ]; then
echo >&2 "Too many arguments"
exit 1
fi
Dan dengan $1
dengan nilai (IFS=-1234567890)
, aritmatika
evaluasi memiliki efek samping dari pengaturan IFS dan [
berikutnya perintah gagal yang berarti memeriksa terlalu banyak argumen
dilewati.
Bagaimana jika split+glob operator tidak dipanggil?
Ada kasus lain di mana kutipan diperlukan di sekitar variabel dan ekspansi lainnya:saat digunakan sebagai pola.
[[ $a = $b ]] # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac
jangan uji apakah $a
dan $b
adalah sama (kecuali dengan zsh
) tetapi jika $a
cocok dengan pola di $b
. Dan Anda perlu mengutip $b
jika Anda ingin membandingkan sebagai string (hal yang sama di "${a#$b}"
atau "${a%$b}"
atau "${a##*$b*}"
dimana $b
harus dikutip jika tidak diambil sebagai pola).
Artinya [[ $a = $b ]]
dapat mengembalikan nilai true jika $a
berbeda dari $b
(misalnya ketika $a
adalah anything
dan $b
adalah *
) atau dapat menghasilkan false jika keduanya identik (misalnya jika keduanya $a
dan $b
adalah [a]
).
Bisakah itu membuat kerentanan keamanan? Ya, seperti bug apa pun. Di sini, penyerang dapat mengubah alur kode logis skrip Anda dan/atau mematahkan asumsi yang dibuat skrip Anda. Misalnya dengan kode seperti:
if [[ $1 = $2 ]]; then
echo >&2 '$1 and $2 cannot be the same or damage will incur'
exit 1
fi
Penyerang dapat melewati pemeriksaan dengan melewati '[a]' '[a]'
.
Sekarang, jika pola itu tidak cocok atau split+glob operator berlaku, apa bahayanya meninggalkan variabel tanpa tanda kutip?
Saya harus mengakui bahwa saya memang menulis:
a=$b
case $a in...
Di sana, mengutip tidak merugikan tetapi tidak sepenuhnya diperlukan.
Namun, satu efek samping dari menghilangkan tanda kutip dalam kasus tersebut (misalnya dalam jawaban T&J) adalah dapat mengirim pesan yang salah kepada pemula:bahwa tidak apa-apa untuk tidak mengutip variabel .
Misalnya, mereka mungkin mulai berpikir bahwa jika a=$b
OK, lalu akan juga (yang tidak dalam banyak shell seperti dalam argumen ke export a=$b
export
perintah so dalam konteks daftar) atau .env a=$b
Bagaimana dengan zsh
?
zsh
memang memperbaiki sebagian besar kecanggungan desain itu. Dalam zsh
(setidaknya ketika tidak dalam mode emulasi sh/ksh), jika Anda ingin membelah , atau globbing , atau pencocokan pola , Anda harus memintanya secara eksplisit:$=var
untuk membagi, dan $~var
untuk glob atau agar konten variabel diperlakukan sebagai pola.
Namun, pemisahan (tetapi tidak globbing) masih dilakukan secara implisit pada substitusi perintah yang tidak dikutip (seperti dalam echo $(cmd)
).
Juga, efek samping yang terkadang tidak diinginkan dari tidak mengutip variabel adalah mengosongkan penghapusan . zsh
perilaku ini mirip dengan apa yang dapat Anda capai di shell lain dengan menonaktifkan globbing sama sekali (dengan set -f
) dan pemisahan (dengan IFS=''
). Namun, dalam:
cmd $var
Tidak akan ada split+glob , tetapi jika $var
kosong, alih-alih menerima satu argumen kosong, cmd
tidak akan menerima argumen sama sekali.
Itu dapat menyebabkan bug (seperti [ -n $var ]
. yang jelas ). Itu mungkin dapat mematahkan ekspektasi dan asumsi skrip dan menyebabkan kerentanan.
Karena variabel kosong dapat menyebabkan argumen hanya dihapus , itu berarti argumen berikutnya dapat ditafsirkan dalam konteks yang salah.
Sebagai contoh,
printf '[%d] <%s>n' 1 $attacker_supplied1 2 $attacker_supplied2
Jika $attacker_supplied1
kosong, lalu $attacker_supplied2
akan ditafsirkan sebagai ekspresi aritmatika (untuk %d
) sebagai ganti string (untuk %s
) dan semua data tidak bersih yang digunakan dalam ekspresi aritmatika merupakan kerentanan injeksi perintah di shell mirip Korn seperti zsh.
$ attacker_supplied1='x y' attacker_supplied2='*'
$ printf '[%d] <%s>n' 1 $attacker_supplied1 2 $attacker_supplied2
[1] <x y>
[2] <*>
baik, tapi:
$ attacker_supplied1='' attacker_supplied2='psvar[$(uname>&2)0]'
$ printf '[%d] <%s>n' 1 $attacker_supplied1 2 $attacker_supplied2
Linux
[1] <2>
[0] <>
uname
perintah sewenang-wenang dijalankan.
Bagaimana jika Anda melakukannya membutuhkan split+glob operator?
Ya, itu biasanya ketika Anda ingin membiarkan variabel Anda tidak dikutip. Namun kemudian Anda perlu memastikan bahwa Anda menyetel split dan glob operator dengan benar sebelum menggunakannya. Jika Anda hanya ingin berpisah bagian dan bukan glob part (yang sering terjadi), maka Anda perlu menonaktifkan globbing (set -o noglob
/set -f
) dan perbaiki $IFS
. Jika tidak, Anda juga akan menyebabkan kerentanan (seperti contoh CGI David Korn yang disebutkan di atas).
Kesimpulan
Singkatnya, meninggalkan variabel (atau substitusi perintah atau
ekspansi aritmatika) yang tidak dikutip dalam shell bisa sangat berbahaya
memang terutama jika dilakukan dalam konteks yang salah, dan sangat
sulit untuk mengetahui mana yang konteks yang salah itu.
Itulah salah satu alasan mengapa ini dianggap praktik buruk .
Terima kasih telah membaca sejauh ini. Jika itu terjadi di atas kepala Anda, jangan
khawatir. Seseorang tidak dapat mengharapkan semua orang untuk memahami semua implikasi dari
menulis kode mereka dengan cara mereka menulisnya. Itulah mengapa kami memiliki rekomendasi praktik yang baik , sehingga mereka dapat diikuti tanpa
harus memahami alasannya.
(dan jika itu belum jelas, harap hindari menulis
kode sensitif keamanan di shell).
Dan harap kutip variabel Anda pada jawaban Anda di situs ini!