iT邦幫忙

2022 iThome 鐵人賽

DAY 14
1
影片教學

從建立環境、驗證漏洞、感受漏洞來學習資安系列 第 16

Day14 - Pwnkit 如果沒有 SUID,那跟鹹魚有什麼分別? (作業5)

  • 分享至 

  • xImage
  •  

做人如果沒夢想,那跟鹹魚有什麼分別? - 周星馳 《少林足球》

Yes

  • 第14天要針對 Pwnkit 的POC程式以及漏洞原理做說明,但放心啦真的不會太難(跟後面的dirtypipe相比的話)。/images/emoticon/emoticon07.gif

  • 首先,我要先介紹一個工具叫做 GTFOBins,這個工具收集了 Linux 中可以被濫用的正常指令,但前提是這些正常指令設定本身是錯誤的。其中有一類叫做 Limited SUID,指得是高權限的程式被設定 SUID 後,如果誘發它去執行 shell 指令,就可以在該 shell 取得高權限,那究竟甚麼是 SUID 呢?

  • SUID 可以參考鳥哥的 Liunx 教學文章 檔案特殊權限: SUID, SGID, SBIT,裡面有提到使用者可以透過 passwd 指令去改密碼,但修改密碼會修改 /etc/shadow 的檔案,而該檔案我們並沒有權限可以讀取跟修改,那為何改得動呢? 原因就在於 SUID 的神奇功能,它可以讓使用者執行程式時擁有跟程式 owner 的執行權限,所以當使用者執行 passwd 的時候,執行期間本身是 root 權限,等待程式執行完成後才會恢復原本的權限。但我們可以自己刻一隻程式進行測試 :

步驟如下 :

  1. mkdir SUID_Test #隨意創建資料夾 SUID_Test
  2. cd SUID_Test
  3. vim suid_test.c #貼上以下程式
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    system("export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ; /bin/sh") ;
    return 0;
}
  1. gcc suid_test.c -o suid_test ; ./suid_test ; # 編譯及執行後,進入 sh
  2. whoami #發現身分沒變
  3. exit
  4. sudo chown root:root suid_test
  5. sudo chmod 4755 suid_test
  6. ls -al # 會發現該檔案的顏色變了
  7. ./suid_test
  8. whoami #發現身分沒變
  • 做到這邊會發現明明設定了 SUID ,但是執行出來的 shell 仍然沒有高權限,這邊並不是我們的設定錯誤,而是程式內部少了去指定 UID、GID,參考這次 pwnkit 的 POC 程式,修改程式碼如下所示 :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    setuid(0); setgid(0);
    seteuid(0); setegid(0);
    system("export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ; /bin/sh") ;
    return 0;
}
  1. gcc suid_test.c -o suid_test
  2. sudo chown root:root suid_test; sudo chmod 4755 suid_test;
  3. ./suid_test
  4. whoami #發現變成 root 權限
  • 上面的例子說明了當程式的 owner 為 root,而且有設定 SUID 屬性,且程式內部有正確設定 UID、GUID 的情況下,執行該程式就會拿到 root 權限。而這次的 pwnkit 的漏洞就是利用這樣的原理達成,利用 pkexec 這隻程式本身的屬性設定加上程式設計上的疏失導致執行 shell,最終達到提權的效果。

https://ithelp.ithome.com.tw/upload/images/20220929/20148308Jj34oBqxq5.jpg

  • 那究竟 pwnkit 有甚麼樣的設計疏失,這就不得不提到程式語言的記憶體管理觀念,在 C/C++ 語言的記憶宣告中是沒有邊界的概念,也就是說程式設計師可以宣告一塊記憶體,但是讀寫的時候卻不小心超出讀取或是超出寫入,是不會發生問題的。以下列例子為例 :
#include <stdio.h>
int main(int argc, char *argv[]) {
    int a[3] = {1,2,3} ;
    a[3] = 87;
    printf("%d, %d, %d.\n",a[2],a[3],a[4]);
    return 0;
}
  • 上面的例子我宣告一個陣列其長度為 3,理論上我只能讀到 index 為 2 的部分,但可發現我寫入index 3 以及讀取 index 4 的時候,程式並不會出錯,也把當時記憶體的執行狀況顯示出來。雖然 C/C++ 這樣的設計省略了邊界檢查可以讓使用保有設計上的彈性以及增加效能,但萬一發生記憶體超出邊界的寫入時就有可能改變程式的執行流程,導致漏洞發生。那到底 pwnkit 的漏洞是怎麼發生的呢? 可以參考來源資料PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034)以及下列資訊。
435 main (int argc, char *argv[])
436 {
...
534   for (n = 1; n < (guint) argc; n++)
535     {
...
568     }
...
610   path = g_strdup (argv[n]);
...
629   if (path[0] != '/')
630     {
...
632       s = g_find_program_in_path (path);
...
639       argv[n] = path = s;
640     }
|---------+---------+-----+------------|---------+---------+-----+------------| 
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] | 
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------| 
V         V                V           V         V                V 
"program" "-option"           NULL      "value" "PATH=name"          NULL 
  • 從上面可以發現當今天 argc=0 時第 534 行的邏輯並不會執行,但會留下變數 n=1,這邊是個伏筆,因為 argv 其實是空的,所以n=1會造成後續超越邊界的讀取或是寫入。接著第 610 行會將 argv[1] 的數值讀入,但因為程式執行時argv 下個鄰近位置是 envp,所以 argv[1] 的讀入等同於 envp[0]。接著 632 行會把該變數當成是程式名稱拿去搜尋執行變數,然後 s 就會找到該程式的完整路徑位址,格式為"name=./value",並於 639 行將該數值寫回 argv[1] 中,並等同於蓋掉第一個環境變數 envp[0]。細節部分一樣可以參考來源資料 PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034)

  • 所以我們可以解讀一下POC程式,並且把關鍵的部分作成兩隻程式,一支是 call.c,另一支則是 show.c,相關程式部分如下所示 :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
    char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
    execve("./show", (char*[]){NULL}, env);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[], char *envp[]) {
    printf("argc:%d.\n",argc);
    int i , j;
    for(i=0; i<argc; i++) {
        printf("argv[%d]=%s.\n",i,argv[i]);
    }
    printf("------------------------------------------\n");
    for(i=0; i<5; i++) {
        printf("argv[%d]=%s.\n",i,argv[i]);
    }
    for(j=0; j<5; j++) {
        printf("envp[%d]=%s.\n",j,envp[j]);
    }
    return 0;
}

步驟如下 :

  1. gcc call.c -o call
  2. gcc show.c -o show
  3. ./show 1234 #先觀察正常的執行流程
    https://ithelp.ithome.com.tw/upload/images/20220929/20148308mwMdtxU9NC.jpg
  4. ./call #觀察 pwnkit 的執行流程
    https://ithelp.ithome.com.tw/upload/images/20220929/20148308kOEujy30tU.jpg
  • 套用到剛剛上面的描敘,接著 632 行會把該 pwnkit 當成是程式名稱拿去搜尋執行變數,然後 s 就會找到該程式的完整路徑位址,其數值為 GCONV_PATH=.\pwnkit ,並於 639 行將該數值寫回 argv[1] 中,並等同於蓋掉第一個環境變數 envp[0],因此會多出一組環境變數 GCONV_PATH=.\pwnkit。
    參考來源資料 7.利用iconv 我們可以知道當環境變數設定 GCONV_PATH=路徑 時會載入底下的 .so 檔內的定義的 gconv_init() 函式,也因此等同於透過 pkexec 去觸發一個攻擊者可控的函式,在該函式內寫入呼叫 /bin/sh 的行為,搭配 pkexec 的 SUID 設定,就可以拿下 root 權限惹。

  • 呼,終於大概講說完這個漏洞80%的概念了,剩下一些細節像是後續環境變數為何要那樣設定,還有 iconv 的觸發原理,就要靠有興趣的各位往下鑽研,我這邊就只能帶個頭而已~~ 至於修補的方法一種是拿掉 SUID 的設定,另一種則是升級 pkexec 的版本,就看各位要選擇哪一種啦...

Pwnkit 回家作業 : /images/emoticon/emoticon13.gif

  • 目前該 POC 程式會在執行期間在進行 compile 一次,但萬一目標機器沒裝 gcc 的話就無法使用,請想辦法把它改成不需要執行中編譯的版本,並在目標機器上進行測試執行。

參考資料 :

  1. Linux 編譯 shared library 的方法和注意事項

上一篇
Day13 - Pwnkit 可不可以讓我提權一下下就好
下一篇
Day15 - DirtyPipe 只有在這個骯髒的管線,才能義正嚴詞的說那些分頁是無辜的。 (作業5解答)
系列文
從建立環境、驗證漏洞、感受漏洞來學習資安37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言