“Jalankan perintah apa pun yang akan meneruskan data yang tidak dipercaya ke perintah yang menafsirkan argumen sebagai perintah”

18

Dari manual findutils:

Misalnya membangun seperti dua perintah ini

# risky
find -exec sh -c "something {}" \;
find -execdir sh -c "something {}" \;

sangat berbahaya. Alasan untuk ini adalah bahwa '{}' diperluas ke nama file yang mungkin mengandung titik koma atau karakter lain yang khusus untuk shell. Jika misalnya seseorang membuat file /tmp/foo; rm -rf $HOMEmaka dua perintah di atas dapat menghapus direktori home seseorang.

Jadi untuk alasan ini, jangan jalankan perintah apa pun yang akan meneruskan data yang tidak dipercaya (seperti nama file) ke perintah yang menafsirkan argumen sebagai perintah yang akan ditafsirkan lebih lanjut (misalnya 'sh').

Dalam kasus shell, ada solusi cerdas untuk masalah ini:

# safer
find -exec sh -c 'something "$@"' sh {} \;
find -execdir sh -c 'something "$@"' sh {} \;

Pendekatan ini tidak dijamin untuk menghindari setiap masalah, tetapi jauh lebih aman daripada mengganti data pilihan penyerang ke dalam teks perintah shell.

  1. Apakah penyebab masalah find -exec sh -c "something {}" \;adalah penggantian untuk {}tidak dikutip dan karenanya tidak diperlakukan sebagai string tunggal?
  2. Dalam solusinya find -exec sh -c 'something "$@"' sh {} \;,

    • pertama {}diganti, tetapi karena {}tidak dikutip, tidak "$@"juga memiliki masalah yang sama dengan perintah aslinya? Sebagai contoh, "$@"akan diperluas untuk "/tmp/foo;", "rm", "-rf", dan "$HOME"?

    • mengapa {}tidak luput atau dikutip?

  3. Bisakah Anda memberikan contoh lain (masih dengan sh -c, atau tanpa itu jika berlaku; dengan atau tanpa findyang mungkin tidak perlu) di mana masalah dan solusi yang sama berlaku, dan yang merupakan contoh minimal sehingga kita dapat fokus pada masalah dan solusi dengan sedikit gangguan mungkin? Lihat Cara untuk memberikan argumen pada perintah yang dijalankan oleh `bash -c`

Terima kasih.

Tim
sumber

Jawaban:

29

Ini tidak benar-benar terkait dengan mengutip, melainkan pada pemrosesan argumen.

Perhatikan contoh berisiko:

find -exec sh -c "something {}" \;
  • Hal ini diurai oleh shell, dan dibagi menjadi enam kata: find, -exec, sh, -c, something {}(tanpa tanda kutip lagi), ;. Tidak ada yang berkembang. Shell berjalan finddengan enam kata tersebut sebagai argumen.

  • Ketika findtemuan sesuatu untuk proses, katakanlah foo; rm -rf $HOME, itu menggantikan {}dengan foo; rm -rf $HOME, dan berjalan shdengan argumen sh, -cdan something foo; rm -rf $HOME.

  • shsekarang melihat -c, dan sebagai hasilnya mem-parsing something foo; rm -rf $HOME( argumen non-opsi pertama ) dan mengeksekusi hasilnya.

Sekarang pertimbangkan varian yang lebih aman:

find -exec sh -c 'something "$@"' sh {} \;
  • Shell berjalan finddengan argumen find, -exec, sh, -c, something "$@", sh, {}, ;.

  • Sekarang ketika findtemuan foo; rm -rf $HOME, itu menggantikan {}lagi, dan berjalan shdengan argumen sh, -c, something "$@", sh, foo; rm -rf $HOME.

  • shmelihat -c, dan mem-parsing something "$@"sebagai perintah untuk menjalankan, dan shdan foo; rm -rf $HOMEsebagai parameter posisi ( mulai dari$0 ), diperluas "$@"menjadi foo; rm -rf $HOME sebagai nilai tunggal , dan berjalan somethingdengan argumen tunggal foo; rm -rf $HOME.

Anda dapat melihat ini dengan menggunakan printf. Buat direktori baru, masukkan, dan jalankan

touch "hello; echo pwned"

Menjalankan varian pertama sebagai berikut

find -exec sh -c "printf \"Argument: %s\n\" {}" \;

menghasilkan

Argument: .
Argument: ./hello
pwned

sedangkan varian kedua, jalankan sebagai

find -exec sh -c 'printf "Argument: %s\n" "$@"' sh {} \;

menghasilkan

Argument: .
Argument: ./hello; echo pwned
Stephen Kitt
sumber
Dalam varian yang lebih aman, mengapa {}tidak luput atau dikutip?
Tim
1
Biasanya tidak perlu dikutip; itu hanya perlu dikutip dalam shell di mana ia memiliki beberapa arti lain, dan saya tidak berpikir itu adalah kasus dengan salah satu shell utama yang digunakan saat ini.
Stephen Kitt
Dari gnu.org/software/findutils/manual/html_mono/… : "Kedua konstruksi ini ( ;dan {}) harus diloloskan (dengan '\') atau dikutip untuk melindungi mereka dari ekspansi oleh shell." Apakah maksud Anda bahwa itu salah untuk bash?
Tim
3
Maksud saya itu tidak benar untuk shell pada umumnya. Lihat POSIX . Pernyataan khusus dalam findutilsbuku pedoman itu paling tidak berasal dari tahun 1996 ... Tentu saja tidak ada salahnya mengutip {}, baik sebagai '{}'atau "{}", tetapi tidak perlu.
Stephen Kitt
@ Tim Anda sedang mengalami situasi umum di mana intuisi Anda belum memperhitungkan fakta bahwa ada dua jenis pemrosesan argumen yang sangat berbeda terjadi di sini: kapan / bagaimana shell mem-parsing argumen dari satu baris teks (yang Di sinilah masalah mengutip) berbeda dari passing argumen sistem operasi mentah (di mana kutipan hanya karakter biasa). Dalam perintah shell find some/path -exec sh -c 'something "$@"' {} \;sebenarnya ada tiga lapisan pemrosesan argumen / lewat, dua dari variasi shell dan satu dari variasi sistem operasi baku dasar.
mtraceur
3

Bagian 1:

find hanya menggunakan penggantian teks.

Ya, jika Anda tidak mengutipnya, seperti ini:

find . -type f -exec sh -c "echo {}" \;

dan seorang penyerang bisa membuat file bernama ; echo owned, maka itu akan exec

sh -c "echo ; echo owned"

yang akan menghasilkan shell berjalan echokemudian echo owned.

Tetapi jika Anda menambahkan tanda kutip, penyerang bisa mengakhiri tanda kutip Anda kemudian menempatkan perintah jahat setelah itu dengan membuat file bernama '; echo owned:

find . -type f -exec sh -c "echo '{}'" \;

yang akan menghasilkan shell berjalan echo '', echo owned.

(jika Anda menukar tanda kutip ganda untuk tanda kutip tunggal, penyerang bisa menggunakan jenis tanda kutip lainnya juga.)


Bagian 2:

Dalam find -exec sh -c 'something "$@"' sh {} \;, {}awalnya tidak ditafsirkan oleh shell, itu dieksekusi langsung dengan execve, jadi menambahkan kutipan shell tidak akan membantu.

find -exec sh -c 'something "$@"' sh "{}" \;

tidak berpengaruh, karena shell menghapus tanda kutip ganda sebelum dijalankan find.

find -exec sh -c 'something "$@"' sh "'{}'" \;

menambahkan kutipan yang shell tidak memperlakukan secara khusus, jadi dalam kebanyakan kasus itu hanya berarti perintah tidak akan melakukan apa yang Anda inginkan.

Setelah itu memperluas untuk /tmp/foo;, rm, -rf, $HOMEseharusnya tidak menjadi masalah, karena mereka adalah argumen untuk something, dan somethingmungkin tidak memperlakukan argumen sebagai perintah untuk mengeksekusi.


Bagian 3:

Saya menganggap pertimbangan serupa berlaku untuk apa pun yang mengambil input yang tidak dipercaya dan menjalankannya sebagai (bagian dari) perintah, misalnya xargsdan parallel.

Mikel
sumber
xargshanya berbahaya jika -Iatau -Jdigunakan. Dalam operasi normal itu hanya menambahkan argumen ke bagian akhir daftar, seperti -exec ... {} +halnya.
Charles Duffy
1
( parallelSebaliknya, menjalankan shell secara default tanpa permintaan eksplisit dari pengguna, meningkatkan permukaan yang rentan; ini adalah masalah yang menjadi subjek banyak diskusi; lihat unix.stackexchange.com/questions/349483/… untuk penjelasan, dan tautan yang disertakan ke lists.gnu.org/archive/html/bug-parallel/2015-05/msg00005.html ).
Charles Duffy
@CharlesDuffy Maksud Anda " mengurangi permukaan yang rentan". Serangan yang diilustrasikan oleh OP memang tidak bekerja secara default dengan GNU Parallel.
Ole Tange
1
Ya, saya tahu Anda membutuhkannya untuk paritas di SSH - jika Anda tidak menelurkan proses parallel"penerima" jarak jauh di ujung soket apa pun, dapat melakukan shell-less langsung execv. Yang persis apa yang akan saya lakukan, jika di sepatu Anda menerapkan alat. Memiliki program noninteraktif berperilaku berbeda berdasarkan nilai saat ini SHELLadalah yang paling tidak mengejutkan.
Charles Duffy
1
Ya - Saya berpendapat bahwa pipa dan pengalihan seharusnya tidak mungkin kecuali pengguna secara eksplisit memulai shell, di mana mereka hanya mendapatkan perilaku eksplisit dari shell yang mereka mulai secara eksplisit. Jika seseorang hanya dapat mengharapkan apa yang execvdisediakan, itu lebih sederhana dan tidak mengherankan, dan aturan yang tidak berubah di seluruh lokasi / lingkungan runtime.
Charles Duffy
3

1. Apakah penyebab masalah find -exec sh -c "something {}" \;adalah penggantian untuk {}tidak dikutip dan karenanya tidak diperlakukan sebagai string tunggal?

Dalam arti tertentu, tetapi mengutip tidak dapat membantu di sini . Nama file yang diganti {}bisa berisi karakter apa saja , termasuk tanda kutip . Apa pun bentuk kutipan yang digunakan, nama file dapat berisi hal yang sama, dan "keluar" dari kutipan.

2. ... tetapi karena {}tidak dikutip, tidak "$@"juga memiliki masalah yang sama dengan perintah aslinya? Misalnya, "$@"akan diperluas ke "/tmp/foo;", "rm", "-rf", and "$HOME"?

Tidak. "$@"Perluas parameter posisi, sebagai kata yang terpisah, dan tidak membaginya lebih lanjut. Di sini, {}ada argumen untuk finddirinya sendiri, dan findmeneruskan nama file saat ini juga sebagai argumen berbeda sh. Ini langsung tersedia sebagai variabel dalam skrip shell, itu tidak diproses sebagai perintah shell itu sendiri.

... mengapa {}tidak luput atau dikutip?

Tidak harus seperti itu, pada kebanyakan shell. Jika Anda menjalankan fish, itu harus: fish -c 'echo {}'mencetak baris kosong. Tetapi tidak masalah jika Anda mengutipnya, shell hanya akan menghapus tanda kutip.

3. Bisakah Anda memberi contoh lain ...

Setiap kali Anda memperluas nama file (atau string yang tidak terkontrol) apa adanya di dalam string yang diambil sebagai semacam kode (*) , ada kemungkinan eksekusi perintah sewenang-wenang.

Misalnya, ini mengembang $f secara langsung kode Perl, dan akan menyebabkan masalah jika nama file berisi kutipan ganda. Kutipan dalam nama file akan mengakhiri kutipan dalam kode Perl, dan sisa nama file dapat berisi kode Perl:

touch '"; print "HELLO";"'
for f in ./*; do
    perl -le "print \"size: \" . -s \"$f\""
done

(Nama file harus sedikit aneh karena Perl mem-parsing seluruh kode di depan, sebelum menjalankannya. Jadi kita harus menghindari kesalahan parse.)

Meskipun ini lolos dengan aman melalui argumen:

for f in ./*; do
    perl -le 'print "size: " . -s $ARGV[0]' "$f"
done

(Tidak masuk akal untuk menjalankan shell lain langsung dari shell, tetapi jika Anda melakukannya, mirip dengan find -exec sh ... case)

(* beberapa jenis kode termasuk SQL, jadi XKCD wajib: https://xkcd.com/327/ plus penjelasan: https://www.explainxkcd.com/wiki/index.php/Little_Bobby_Tables )

ilkkachu
sumber
1

Kekhawatiran Anda justru menjadi alasan mengapa input kutipan Paralel GNU:

touch "hello; echo pwned"
find . -print0 | parallel -0 printf \"Argument: %s\\n\" {}

Ini tidak akan berjalan echo pwned .

Ini akan menjalankan shell, sehingga jika Anda memperpanjang perintah Anda, Anda tidak akan tiba-tiba mendapatkan kejutan:

# This _could_ be run without spawining a shell
parallel "grep -E 'a|bc' {}" ::: foo
# This cannot
parallel "grep -E 'a|bc' {} | wc" ::: foo

Untuk perincian lebih lanjut tentang masalah pemijahan-kulit, lihat: https://www.gnu.org/software/parallel/parallel_design.html#Always-running-commands-in-a-shell

Ole Tange
sumber
1
... yang mengatakan, find . -print0 | xargs -0 printf "Argument: %s\n"sama aman (atau lebih tepatnya, lebih, karena dengan -print0satu menangani nama file dengan baris baru dengan benar); mengutip paralel adalah solusi untuk masalah yang tidak ada sama sekali ketika tidak ada shell hadir.
Charles Duffy
Tetapi segera setelah Anda menambah | wcperintah Anda harus melompat melalui lingkaran untuk membuatnya aman. GNU Parallel aman secara default.
Ole Tange
ITYM: mencoba untuk menutupi setiap kekhasan bahasa shell dengan proses rumit mengutip semuanya. Itu tidak seaman menyimpan data dengan benar dalam variabel, tidak dicampur dengan kode. Sekarang, jika ingin secara otomatis mengubahnya foo {} | wcmenjadi sh -c 'foo "$1" | wcsh {} `, itu mungkin berbeda.
ilkkachu