Bash

忘記在 bash/POSIX shell 中引用變數的安全隱患

  • April 1, 2022

如果您已經關注 unix.stackexchange.com 有一段時間了,那麼您現在應該知道echo $var在 Bourne/POSIX shell(zsh 是例外)中的列表上下文(如 )中不加引號的變數具有非常特殊的含義,並且除非您有充分的理由,否則不應這樣做。

在這裡的一些問答中詳細討論了它(範例:為什麼我的 shell 腳本會因空白或其他特殊字元而窒息?何時需要雙引號?擴展 shell 變數以及 glob 的效果並對其進行拆分引用vs 不帶引號的字元串擴展

自 70 年代後期首次發布 Bourne shell 以來就是這種情況,並且沒有被 Korn shell(David Korn 最大的遺憾之一(問題 #7))改變,或者bash主要複製了 Korn shell,那就是POSIX/Unix 是如何指定的。

現在,我們仍然在這裡看到許多答案,甚至偶爾會公開發布未引用變數的 shell 程式碼。你會認為人們現在已經學會了。

根據我的經驗,主要有 3 種類型的人會省略引用他們的變數:

  • 初學者。誠然,這是一種完全不直覺的語法,因此可以原諒這些。我們在這個網站上的職責是教育他們。
  • 健忘的人。
  • 即使經過反复錘擊也不相信的人,他們認為Bourne shell 作者肯定不打算讓我們引用所有變數

如果我們揭露與這種行為相關的風險,也許我們可以說服他們。

如果您忘記引用變數,可能發生的最糟糕的事情是什麼。真的有那麼糟糕嗎?

我們在這裡談論什麼樣的漏洞?

在什麼情況下會出現問題?

**前言 –**首先,我想說這不是解決問題的正確方法。這有點像說“你不應該殺人,否則你會進監獄”。

同樣,您不要引用您的變數,因為否則您將引入安全漏洞。您引用變數是因為不這樣做是錯誤的(但如果對監獄的恐懼可以提供幫助,為什麼不這樣做)。

給剛上火車的小伙伴們做個小總結。

在大多數 shell 中,不加引號的變數擴展(儘管(以及此答案的其餘部分)也適用於命令替換(...$(...))和算術擴展($((...))$[...]))具有非常特殊的含義。描述它的最佳方式是它就像呼叫某種隱式split+glob運算符¹。

cmd $var

用另一種語言會寫成:

cmd(glob(split($var)))

$var首先根據涉及$IFS特殊參數的複雜規則拆分為單詞列表(拆分部分),然後將該拆分產生的每個單詞視為一種模式,該模式被擴展為與其匹配的文件列表(glob部分) .

例如,如果$varcontains*.txt,/var/*.xml$IFS contains ,cmd將使用多個參數呼叫,第一個是目前目錄cmd中的文件,下一個是txt 目前目錄中的xml文件和/var.

如果您只想cmd使用兩個文字參數cmd 和呼叫*.txt,/var/*.xml,您可以編寫:

cmd "$var"

這將是您更熟悉的其他語言:

cmd($var)

shell 中的漏洞是什麼意思?

畢竟,從一開始就知道 shell 腳本不應該在安全敏感的環境中使用。當然,好的,不引用變數是一個錯誤,但這不會造成太大的傷害,不是嗎?

好吧,儘管有人會告訴你 shell 腳本永遠不應該用於 Web CGI,或者幸好現在大多數係統都不允許 setuid/setgid shell 腳本,但 shellshock 的一件事(可遠端利用的 bash 錯誤使2014 年 9 月的頭條新聞)揭示的是,shell 仍然在它們可能不應該被廣泛使用的地方:在 CGI、DHCP 客戶端掛鉤腳本、sudoers 命令中,(如果不是作為)setuid 命令呼叫……

有時不知不覺。例如,system('cmd $PATH_INFO')php// CGI 腳本中確實呼叫了一個 shell 來解釋該命令行(更不用說它本身可能是一個 shell 腳本並且它的作者可能從未期望它會從 CGI 呼叫它)。perl``python``cmd

當存在提升權限的路徑時,您就有了一個漏洞,也就是說,當某人(我們稱他為攻擊者)能夠做他不應該做的事情時。

這總是意味著攻擊者提供數據,該數據由特權使用者/程序處理,在大多數情況下,由於錯誤,該數據無意中做了不應該做的事情。

基本上,當您的錯誤程式碼在攻擊者的控制下處理數據時,您就會遇到問題。

現在,這些數據可能來自哪裡並不總是很明顯,而且通常很難判斷您的程式碼是否會處理不受信任的數據。

就變數而言,在 CGI 腳本的情況下,很明顯,數據是 CGI GET/POST 參數以及諸如 cookie、路徑、主機…參數之類的東西。

對於 setuid 腳本(在被另一個使用者呼叫時作為一個使用者執行),它是參數或環境變數。

另一個非常常見的向量是文件名。如果您從目錄中獲取文件列表,則攻擊者可能已將文件植入其中。

在這方面,即使在互動式 shell 的提示下,您也可能容易受到攻擊(例如在處理文件時/tmp~/tmp

甚至 a~/.bashrc也可能是易受攻擊的(例如,當呼叫它以在伺服器部署中執行 類似bash時會解釋它,其中一些變數在客戶端的控制下)。ssh``ForcedCommand``git

現在,可能不會直接呼叫腳本來處理不受信任的數據,但它可能會被另一個命令呼叫。或者您的錯誤程式碼可能會被複製粘貼到腳本中(由您或您的一位同事在 3 年後)。一個特別重要的地方是問答網站中的答案,因為您永遠不知道您的程式碼副本可能會在哪裡結束。

言歸正傳;有多糟糕?

到目前為止,不加引號的變數(或命令替換)是與 shell 程式碼相關的安全漏洞的第一大來源。部分原因是這些錯誤通常會轉化為漏洞,但也因為看到未引用的變數很常見。

實際上,在查找 shell 程式碼中的漏洞時,首先要做的是查找未加引號的變數。它很容易被發現,通常是一個很好的候選者,通常很容易追溯到攻擊者控制的數據。

未引用的變數可以通過無數種方式變成漏洞。我在這裡只給出一些常見的趨勢。

資訊披露

由於拆分部分,大多數人會遇到與未引用變數相關的錯誤(例如,現在文件名稱中通常有空格,而空格在 IFS 的預設值中)。很多人會忽略 glob部分。glob部分至少與 split部分一樣危險。

對未經處理的外部輸入進行通配意味著攻擊者可以讓您讀取任何目錄的內容。

在:

echo You entered: $unsanitised_external_input

如果$unsanitised_external_input包含/*,則表示攻擊者可以看到/. 沒什麼大不了的。它會變得更有趣,但/home/*它會為您提供機器上的使用者名列表、/tmp/*其他 /home/*/.forward危險行為的提示、/etc/rc*/*啟用的服務……無需單獨命名。的值/* /*/* /*/*/*...將僅列出整個文件系統。

拒絕服務漏洞。

把前面的案例看得太遠了,我們得到了 DoS。

實際上,列表上下文中任何未引用且輸入未經處理的變數至少是 DoS 漏洞。

即使是專業的 shell 腳本編寫者也經常忘記引用以下內容:

#! /bin/sh -
: ${QUERYSTRING=$1}

:是無操作命令。什麼可能出錯?

這意味著分配$1$QUERYSTRING如果$QUERYSTRING 未設置。這也是使 CGI 腳本可從命令行呼叫的一種快速方法。

雖然它$QUERYSTRING仍然被擴展並且因為它沒有被引用,所以呼叫了split+glob運算符。

現在,有些 glob 的擴展成本特別高。/*/*/*/*一個已經夠糟糕了,因為它意味著最多列出 4 個級別的目錄。除了磁碟和 CPU 活動之外,這還意味著儲存數以萬計的文件路徑(在最小的伺服器虛擬機上為 40k,其中目錄為 10k)。

現在/*/*/*/*/../../../../*/*/*/*意味著 40k x 10k, /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*足以讓最強大的機器屈服。

自己嘗試一下(儘管準備好讓您的機器崩潰或掛起):

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

當然,如果程式碼是:

echo $QUERYSTRING > /some/file

然後你可以填滿磁碟。

只需在shell cgibash cgiksh cgi上進行Google搜尋,您就會發現一些頁面向您展示瞭如何在 shell 中編寫 CGI。注意那些處理參數的人中有一半是脆弱的。

甚至David Korn 自己的 也很脆弱(查看 cookie 處理)。

最多任意程式碼執行漏洞

任意程式碼執行是最嚴重的漏洞類型,因為如果攻擊者可以執行任何命令,那麼他可以做的事情是沒有限制的。

這通常是導致這些的*分裂部分。*當只需要一個參數時,這種拆分會導致將多個參數傳遞給命令。雖然其中第一個將在預期的上下文中使用,但其他的將在不同的上下文中,因此可能會有不同的解釋。舉個例子更好:

awk -v foo=$external_input '$2 == foo'

這裡的目的是將 $external_inputshell變數的內容分配給foo awk變數。

現在:

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

拆分的第二個單詞$external_input 沒有分配給foo但被視為awk程式碼(這裡執行任意命令:uname)。

對於可以執行其他命令(awk, env, sed(GNU one), perl, find…)的命令尤其是對於 GNU 變體(在參數後接受選項),這尤其是一個問題。有時,您不會懷疑命令能夠執行其他命令,例如ksh,bashzsh’s[printf

for file in *; do
 [ -f $file ] || continue
 something-that-would-be-dangerous-if-$file-were-a-directory
done

如果我們創建一個名為 的目錄x -o yes,那麼測試就會變成肯定的,因為它是我們正在評估的完全不同的條件表達式。

更糟糕的是,如果我們創建一個名為 的文件x -a a[0$(uname>&2)] -gt 1,其中至少包含所有 ksh 實現(包括sh 大多數商業 Unices 和一些 BSD 的實現),該文件的執行uname 是因為這些 shell 對命令的數值比較運算符執行算術評估[

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

bash類似的文件名相同x -a -v a[0$(uname>&2)]

當然,如果他們不能任意執行,攻擊者可能會接受較小的損害(這可能有助於任意執行)。任何可以寫入文件或更改權限、所有權或具有任何主要或副作用的命令都可能被利用。

各種事情都可以用文件名來完成。

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

你最終使..可寫(使用 GNU 遞歸 chmod)。

在公共可寫區域中自動處理文件的腳本/tmp需要非常仔細地編寫。

關於什麼[ $# -gt 1 ]

這是我覺得惱火的事情。有些人不厭其煩地想知道一個特定的擴展是否有問題,以確定他們是否可以省略引號。

就像是說。哎,好像$#不能用split+glob操作符,讓shell來split+glob吧。或者嘿,讓我們編寫錯誤的程式碼只是因為該錯誤不太可能被擊中

現在可能性有多大?好的,$#(或$!$?或任何算術替換)可能只包含數字(或-對於某些²),因此glob部分已出。但是,要讓拆分部分做某事,我們所需要的只是$IFS包含數字(或-)。

使用一些shell,$IFS可能是從環境中繼承的,但是如果環境不安全,那麼遊戲就結束了。

現在,如果您編寫如下函式:

my_function() {
 [ $# -eq 2 ] || return
 ...
}

這意味著您的函式的行為取決於呼叫它的上下文。或者換句話說,$IFS 成為它的輸入之一。嚴格來說,當你為你的函式編寫 API 文件時,它應該是這樣的:

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

呼叫您的函式的程式碼需要確保$IFS不包含數字。所有這一切都是因為您不想輸入這 2 個雙引號字元。

現在,要使該[ $# -eq 2 ]錯誤成為漏洞,您需要以某種方式使 的值受到攻擊者$IFS的控制。可以想像,除非攻擊者設法利用另一個漏洞,否則這種情況通常不會發生。

不過,這並非聞所未聞。一個常見的情況是人們忘記在算術表達式中使用數據之前對其進行清理。我們已經在上面看到它可以允許在某些 shell 中執行任意程式碼,但在所有這些 shell 中,它允許 攻擊者給任何變數一個整數值。

例如:

n=$(($1 + 1))
if [ $# -gt 2 ]; then
 echo >&2 "Too many arguments"
 exit 1
fi

並且使用$1with value (IFS=-1234567890),該算術評估具有設置 IFS 的副作用,並且下一個[ 命令失敗,這意味著繞過了對太多 args的檢查。

不呼叫split+glob運算符時怎麼辦?

還有另一種情況,需要在變數和其他擴展周圍加上引號:當它用作模式時。

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

不要測試$a和是否$b相同(除了 with zsh)但如果$a匹配$b. $b如果要作為字元串進行比較,則需要引用(如果不將其視為模式,則應在or或"${a#$b}"where"${a%$b}""${a##*$b*}"引用相同的內容)。$b

這意味著在不同[[ $a = $b ]]的情況下可能返回 true (例如當is和is時),或者在它們相同時可能返回 false(例如當兩者和are時)。$a``$b``$a``anything``$b``*``$a``$b``[a]

這會導致安全漏洞嗎?是的,就像任何錯誤一樣。在這裡,攻擊者可以更改您腳本的邏輯程式碼流和/或破壞您的腳本所做的假設。例如,使用如下程式碼:

if [[ $1 = $2 ]]; then
  echo >&2 '$1 and $2 cannot be the same or damage will incur'
  exit 1
fi

攻擊者可以通過傳遞繞過檢查'[a]' '[a]'

現在,如果模式匹配和split+glob運算符都不適用,那麼不加引號的變數有什麼危險?

我不得不承認我確實寫過:

a=$b
case $a in...

在那裡,引用沒有害處,但不是絕對必要的。

但是,在這些情況下(例如在問答答案中)省略引號的一個副作用是它可能向初學者發送錯誤資訊:不引用 variables 可能是可以的

例如,他們可能會開始認為如果a=$b可以,那麼export a=$b也可以(在許多 shell中,它不是在命令的參數中,export所以在列表上下文中)或env a=$b.

怎麼樣zsh

zsh確實解決了大部分設計上的尷尬。在zsh(至少在不處於 sh/ksh 仿真模式時),如果您想要split 、globbingpattern matching,您必須明確請求:拆分和glob 或將變數的內容視為一種模式。$=var``$~var

但是,拆分(但不是萬用字元)仍然在不帶引號的命令替換時隱式完成(如echo $(cmd))。

此外,不引用變數的有時不需要的副作用是清空刪除。該zsh行為類似於您可以通過完全禁用萬用字元(with set -f)和拆分(with IFS='')在其他 shell 中實現的行為。還在:

cmd $var

不會有split+glob,但如果$var是空的,而不是接收一個空參數,cmd將根本不接收任何參數。

這可能會導致錯誤(如明顯的[ -n $var ])。這可能會破壞腳本的期望和假設並導致漏洞。

由於空變數可能導致一個參數被刪除,這意味著下一個參數可能在錯誤的上下文中被解釋。

舉個例子,

printf '[%d] <%s>\n' 1 $attacker_supplied1 2 $attacker_supplied2

如果$attacker_supplied1為空,$attacker_supplied2則將被解釋為算術表達式 (for %d) 而不是字元串 (for %s),並且在算術表達式中使用的任何未經處理的數據都是類似 Korn 的 shell(例如 zsh)中的命令注入漏洞

$ attacker_supplied1='x y' attacker_supplied2='*'
$ printf '[%d] <%s>\n' 1 $attacker_supplied1 2 $attacker_supplied2
[1] <x y>
[2] <*>

很好,但是:

$ attacker_supplied1='' attacker_supplied2='psvar[$(uname>&2)0]'
$ printf '[%d] <%s>\n' 1 $attacker_supplied1 2 $attacker_supplied2
Linux
[1] <2>
[0] <>

執行了uname 任意命令

另請注意,雖然zsh預設情況下不會對替換進行 globbing,但由於 zsh 中的 glob 比其他 shell 中的 glob 更強大,這意味著如果您globsubst同時啟用該選項,它們會造成更大的損害extendedglob,或者沒有禁用bareglobqual並且無意中沒有引用一些變數。

例如,甚至:

set -o globsubst
echo $attacker_controlled

將是一個任意命令執行漏洞,因為命令可以作為 glob 擴展的一部分執行,例如使用e評估 glob 限定符:

$ set -o globsubst
$ attacker_controlled='.(e[uname])'
$ echo $attacker_controlled
Linux
.
emulate sh # or ksh
echo $attacker_controlled

不會導致 ACE 漏洞(儘管它仍然是 sh 中的 DoS 漏洞),因為bareglobqual在 sh/ksh 仿真中被禁用。globsubst當想要解釋 sh/ksh 程式碼時,除了在那些 sh/ksh 仿真中啟用之外,沒有充分的理由啟用。

當您確實需要split+glob運算符時怎麼辦?

是的,這通常是您確實希望不引用變數的時候。但是,您需要確保在使用它之前正確調整您的splitglob運算符。如果您只想要拆分部分而不是glob部分(大多數情況下都是這種情況),那麼您確實需要禁用 globbing ( set -o noglob/ set -f) 並修復$IFS. 否則,您也會導致漏洞(如上面提到的 David Korn 的 CGI 範例)。

結論

簡而言之,在 shell 中不加引號的變數(或命令替換或算術擴展)確實非常危險,尤其是在錯誤的上下文中完成時,而且很難知道哪些是錯誤的上下文。

這就是為什麼它被認為是不好的做法的原因之一。

感謝您到目前為止的閱讀。如果它超出您的想像,請不要擔心。不能指望每個人都理解以他們編寫程式碼的方式編寫程式碼的所有含義。這就是我們提供 良好實踐建議的原因,因此無需了解原因就可以遵循它們。

(如果這還不明顯,請避免在 shell 中編寫安全敏感程式碼)。

請在本網站上的答案中引用您的變數!


¹在ksh93pdksh及其衍生物中,除非禁用萬用字元,否則也會執行大括號擴展ksh93(對於 ksh93u+ 以上的版本,即使該braceexpand選項被禁用)。

² 在ksh93andyash中,算術擴展還可以包括1,2, 1e+66, inf, nan. 中還有更多zsh,包括#哪個是 glob 運算符extendedglob,但zsh在算術擴展時從不拆分 + glob,即使在sh仿真中

引用自:https://unix.stackexchange.com/questions/171346