9月24日,廣泛存在于linux的bash漏洞曝光。因為此漏洞可以執行遠端指令,是以極為危。危害程度可能超過前段時間的心髒流血漏洞。 漏洞編号cve-2014-6271,以及打了更新檔之後被繞過的cve-2014-7169,又稱shellshock漏洞。
要是用一句話概括這個漏洞,就是代碼和資料沒有正确區分。
此次漏洞很像sql注入,通過特别設計的參數使得解析器錯誤地執行了參數中的指令。這其實是所有解析性語言都可能存在的問題。
<code>1</code>
<code>env</code> <code>x=</code><code>'() { :;}; echo vulnerable'</code> <code>bash</code> <code>-c </code><code>"echo this is a test"</code>
<code>2</code>
<code>#env [option]... [name=value]... [command [args]...]</code>
這是網上最早流傳出來驗證漏洞的代碼。如果漏洞存在,那個”echo vulnerable”會被執行,螢幕上會輸出“vulnerable”。我們分析這句shell指令的文法。
<code>+</code><code>env</code> <code>'x=() { :;}; echo vulnerable'</code> <code>bash</code> <code>-c </code><code>'echo this is a test'</code>
<code>#這是在bash -x下執行,執行指令時把指令和它們的參數顯示出來。</code>
可以看出,這個語句原本的意圖是使用env指令建立一個臨時環境,然後在裡面執行一個bash指令。
從解析上看,bash解析并沒有問題,文法是正常的。是以應該是env指令處理變量名時的漏洞。
bash可以将shell變量導出為環境變量,還可以将shell函數導出為環境變量!目前版本的bash通過以函數名作為環境變量名,以“(){”開頭的字串作為環境變量的值來将函數定義導出為環境變量。此次爆出的漏洞在于bash處理這樣的“函數環境變量”的時候,并沒有以函數結尾“}”為結束,而是一直執行其後的shell指令。
是以,在某種環境,bash會在給導出的函數定義處理環境時執行使用者代碼。
黑客定義了這樣的環境變量(注:() 和 { 間的空格不能少):
<code>export</code> <code>x=</code><code>'() { echo "inside x"; }; echo "outside x";'</code>
<code>#可以用export看到這個函數變量。</code>
<code>3</code>
<code>declare</code> <code>-x x=</code><code>"() { echo \"inside x\"; }; echo \"outside x\";"</code>
當我們開啟一個子bash時。
<code>[sean@localhost ~]$ </code><code>export</code> <code>x=</code><code>'() { echo "inside x"; }; echo "outside x";'</code>
<code>[sean@localhost ~]$ </code><code>bash</code>
<code>outside x</code>
變量中的代碼被執行了。(以上參考coolshell.cn)
漏洞就在于建立子bash時,注入代碼被執行。是以,回憶一下那個漏洞驗證代碼,env後有一個bash,我試過多次,不直接跟bash都不會觸發注入代碼。上面的例子很好的解釋了這一點,注入代碼是在子bash載入使用者環境變量時執行的,env後直接跟bash就是為了在env建立的臨時環境中建立子bash以觸發漏洞。
<code>01</code>
<code>/*</code>
<code>02</code>
<code>initialize the shell variables from the current environment. if privmode is nonzero,</code>
<code>03</code>
<code> </code><code>don't import functions from env or parse $shellopts.</code>
<code>04</code>
<code>從目前環境安裝shell變量,如果privmode非零,不要從env或者prase $shellopts導入函數。</code>
<code>05</code>
<code>*/</code>
<code>06</code>
<code>void</code> <code>initialize_shell_variables (env, privmode) </code><code>char</code> <code>**env; </code><code>int</code> <code>privmode;</code>
<code>07</code>
<code>{</code>
<code>08</code>
<code> </code><code>...</code>
<code>09</code>
<code> </code><code>create_variable_tables ();</code>
<code>10</code>
<code> </code>
<code>11</code>
<code> </code><code>/*</code>
<code>12</code>
<code> </code><code>從env環境變量中擷取參數</code>
<code>13</code>
<code> </code><code>*/</code>
<code>14</code>
<code> </code><code>for</code> <code>(string_index = 0; string = env[string_index++]; )</code>
<code>15</code>
<code> </code><code>{</code>
<code>16</code>
<code> </code><code>char_index = 0;</code>
<code>17</code>
<code> </code><code>name = string;</code>
<code>18</code>
<code> </code><code>while</code> <code>((c = *string++) && c != </code><code>'='</code><code>) ; </code><code>//查找等号'='</code>
<code>19</code>
<code> </code><code>if</code> <code>(string[-1] == </code><code>'='</code><code>)</code>
<code>20</code>
<code> </code><code>char_index = string - name - 1;</code>
<code>21</code>
<code>22</code>
<code> </code><code>/* if there are weird things in the environment, like `=xxx' or a</code>
<code>23</code>
<code> </code><code>string without an `=', just skip them.</code>
<code>24</code>
<code> </code><code>如果這裡環境中有詭異的事情,像'=xxx'或者一個沒有'='的字元串,就忽略它們。</code>
<code>25</code>
<code> </code><code>*/</code>
<code>26</code>
<code> </code><code>if</code> <code>(char_index == 0)</code>
<code>27</code>
<code> </code><code>continue</code><code>;</code>
<code>28</code>
<code>29</code>
<code> </code><code>/* assert(name[char_index] == '=') */</code>
<code>30</code>
<code> </code><code>name[char_index] = </code><code>'\0'</code><code>;</code>
<code>31</code>
<code> </code><code>/*</code>
<code>32</code>
<code> </code><code>now, name = env variable name, string = env variable value, and char_index == strlen (name)</code>
<code>33</code>
<code> </code><code>name=env變量名,string=env變量值,char_index= name長度</code>
<code>34</code>
<code>35</code>
<code>36</code>
<code>37</code>
<code> </code><code>if exported function, define it now. don't import functions from the environment in privi</code>
<code>38</code>
<code>leged mode.</code>
<code>39</code>
<code> </code><code>如果導出函數,先定義。不要在特權模式下把函數導入到環境。</code>
<code>40</code>
<code>41</code>
<code> </code><code>if</code> <code>(privmode == 0 && read_but_dont_execute == 0 && streqn (</code><code>"() {"</code><code>, string, 4)) </code><code>//比對</code>
<code>42</code>
<code>string中的</code><code>"(){"</code><code>用于判斷這是一個函數</code>
<code>43</code>
<code> </code><code>{</code>
<code>44</code>
<code> </code><code>string_length = </code><code>strlen</code> <code>(string);</code>
<code>45</code>
<code> </code><code>temp_string = (</code><code>char</code> <code>*)xmalloc (3 + string_length + char_index);</code>
<code>46</code>
<code> </code>
<code>47</code>
<code> </code><code>strcpy</code> <code>(temp_string, name);</code>
<code>48</code>
<code> </code><code>temp_string[char_index] = </code><code>' '</code><code>;</code>
<code>49</code>
<code>50</code>
<code> </code><code>strcpy</code> <code>(temp_string + char_index + 1, string); </code><code>//字元串拼接 </code>
<code>51</code>
<code>52</code>
<code> </code><code>/*</code>
<code>53</code>
<code> </code><code>這句是關鍵,initialize_shell_variables對環境變量中的代碼進行了執行,由于它錯誤的信任的外部發送的</code>
<code>54</code>
<code>資料,形成了和sql注入類似的場景,這句代碼和php中的eval是類似的,黑客隻要滿足2個條件</code>
<code>55</code>
<code> </code><code>1. 控制發送的參數,并在其中拼接代碼</code>
<code>56</code>
<code> </code><code>2. 黑客發送的包含使用者代碼的參數會被無條件的執行,而執行方不進行任何的邊界檢查</code>
<code>57</code>
<code>58</code>
<code> </code><code>這就是典型的資料和代碼沒有進行正确區分導緻的漏洞</code>
<code>59</code>
<code> </code><code>*/</code>
<code>60</code>
<code> </code><code>parse_and_execute (temp_string, name, seval_nonint|seval_nohist); </code><code>//執行函數</code>
<code>61</code>
<code>62</code>
<code> </code><code>// ancient backwards compatibility. old versions of bash exported functions like name()=() {...}</code>
<code>63</code>
<code> </code><code>// 古老的向下相容,老版本的bash導入函數類似name()=(){...}</code>
<code>64</code>
<code> </code><code>if</code> <code>(name[char_index - 1] == </code><code>')'</code> <code>&& name[char_index - 2] == </code><code>'('</code><code>)</code>
<code>65</code>
<code> </code><code>name[char_index - 2] = </code><code>'\0'</code><code>;</code>
<code>66</code>
<code>67</code>
<code> </code><code>if</code> <code>(temp_var = find_function (name))</code>
<code>68</code>
<code> </code><code>{</code>
<code>69</code>
<code> </code><code>vsetattr (temp_var, (att_exported|att_imported));</code>
<code>70</code>
<code> </code><code>array_needs_making = 1;</code>
<code>71</code>
<code> </code><code>}</code>
<code>72</code>
<code> </code><code>else</code>
<code>73</code>
<code> </code><code>report_error (_(</code><code>"error importing function definition for `%s'"</code><code>), name);</code>
<code>74</code>
<code>75</code>
<code> </code><code>/* ( */</code>
<code>76</code>
<code> </code><code>if</code> <code>(name[char_index - 1] == </code><code>')'</code> <code>&& name[char_index - 2] == </code><code>'\0'</code><code>)</code>
<code>77</code>
<code> </code><code>name[char_index - 2] = </code><code>'('</code><code>; </code><code>/* ) */</code>
<code>78</code>
<code> </code><code>}</code>
<code>79</code>
<code> </code><code>}</code>
<code>80</code>
<code>}</code>
代碼中,直接把變量值傳入parse_and_execute(),網上不少分析都是說這個就是漏洞所在。但是表示略有疑問,為何定義要直接傳進去執行呢?真正分離開資料和代碼可能才是堵上漏洞的根本辦法。
其實,一開始知道這個漏洞,還是覺得奇怪,語句本來就是在shell上執行的。後來發現,安全問題在于遠端通路時,某些協定正好會組成類似測試代碼的語句,觸發此漏洞。
因為cgi會把post包中的變量導入成使用者變量,并在裡面啟動子bash,這樣就觸發了漏洞。
[遠端]服務會調用bash。(建立bash子程序)
[遠端]服務允許使用者定義環境變量。
[遠端]服務調用子bash時加載了使用者定義的環境變量。
看上去,目前cgi已經基本不用了。但是隻要符合上面所說的三個條件,還是會觸發漏洞。是以,此漏洞危害巨大,又因為是源碼級的漏洞,影響廣泛。
在爆出漏洞第二天,更新檔就出來了。
第一個更新檔對傳入的變量做了一個判斷。類似于防sql注入的過濾方法。第一個更新檔判斷如果不是函數定義,或者指令(command)超過一個就判為不合法。自然,這種方法很可能就被繞過。
很快,繞過漏洞的測試代碼也跟着出來。
<code>[sean@localhost ~]$ </code><code>env</code> <code>x='() { (a)=>\' sh -c </code><code>"echo date"</code><code>; </code><code>cat</code> <code>echo</code>
<code>sh: x:行1: 未預期的符号 `=' 附近有文法錯誤</code>
<code>sh: x:行1: `'</code>
<code>4</code>
<code>sh: `x' 函數定義導入錯誤</code>
<code>5</code>
<code>2014年 09月 28日 星期日 21:33:50 cst</code>
當時發現了代碼執行後,所在目錄莫名其妙出了個echo檔案,echo檔案存的就是那個日期。
x='() { (a)=>\’ 這個不用說了,定義一個x的環境變量。但是,這個函數不完整啊,是的,這是故意的。另外你一定要注意,\’不是為了單引号的轉義,x這個變量的值就是 () { (a)=>\
其中的 (a)=這個東西目的就是為了讓bash的解釋器出錯(文法錯誤)。
文法出錯後,在緩沖區中就會隻剩下了 “>\”這兩個字元。
于是,這個神奇的bash會把後面的指令echo date換個行放到這個緩沖區中,然後執行。
相當于在shell 下執行了下面這個指令:
<code>[sean@localhost ~]$ >\</code>
<code>> </code><code>echo</code> <code>date</code>
bash中“\”用于指令上下換行,是以,實際執行的是:
<code>[sean@localhost ~]$ ></code><code>echo</code> <code>date</code>
bash中“>”符号是輸出重定向,這裡是把标準輸出重定向到echo檔案。date是我們費勁心思讓它執行的指令。執行結果就是執行了date指令,輸出重定向到echo檔案。
更新檔判斷如果不是函數定義,或者指令(command)超過一個。
在代碼中,”(){“表示了這是函數定義,指令隻有一個,因為分号隻有一個。這就繞過更新檔的檢測。
能想到這個攻擊的真的是個變态,連文法出錯都用上了。