天天看點

ShellShock漏洞原理分析

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++) &amp;&amp; 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 &amp;&amp; read_but_dont_execute == 0 &amp;&amp; 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>&amp;&amp; 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>&amp;&amp; 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時加載了使用者定義的環境變量。

ShellShock漏洞原理分析

看上去,目前cgi已經基本不用了。但是隻要符合上面所說的三個條件,還是會觸發漏洞。是以,此漏洞危害巨大,又因為是源碼級的漏洞,影響廣泛。

在爆出漏洞第二天,更新檔就出來了。

第一個更新檔對傳入的變量做了一個判斷。類似于防sql注入的過濾方法。第一個更新檔判斷如果不是函數定義,或者指令(command)超過一個就判為不合法。自然,這種方法很可能就被繞過。

很快,繞過漏洞的測試代碼也跟着出來。

<code>[sean@localhost ~]$ </code><code>env</code> <code>x='() { (a)=&gt;\' 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)=&gt;\’ 這個不用說了,定義一個x的環境變量。但是,這個函數不完整啊,是的,這是故意的。另外你一定要注意,\’不是為了單引号的轉義,x這個變量的值就是 () { (a)=&gt;\

其中的 (a)=這個東西目的就是為了讓bash的解釋器出錯(文法錯誤)。

文法出錯後,在緩沖區中就會隻剩下了 “&gt;\”這兩個字元。

于是,這個神奇的bash會把後面的指令echo date換個行放到這個緩沖區中,然後執行。

相當于在shell 下執行了下面這個指令:

<code>[sean@localhost ~]$ &gt;\</code>

<code>&gt; </code><code>echo</code> <code>date</code>

bash中“\”用于指令上下換行,是以,實際執行的是:

<code>[sean@localhost ~]$ &gt;</code><code>echo</code> <code>date</code>

bash中“&gt;”符号是輸出重定向,這裡是把标準輸出重定向到echo檔案。date是我們費勁心思讓它執行的指令。執行結果就是執行了date指令,輸出重定向到echo檔案。

更新檔判斷如果不是函數定義,或者指令(command)超過一個。

在代碼中,”(){“表示了這是函數定義,指令隻有一個,因為分号隻有一個。這就繞過更新檔的檢測。

能想到這個攻擊的真的是個變态,連文法出錯都用上了。