[Python] 設定 setuid 讓 python 程式可以用 root 權限執行
最近需要讓一個 python 程式可以在 Mac/Linux 上被普通 (非 root) 的 user 呼叫,
可是它本身又需要 root 的權限來操作一些檔案,
考慮了一下,似乎只能用 setuid 來解決~
setuid 是一個檔案權限的設定,設定了之後,
被執行起來的程式,會以檔案擁有者 (file owner) 的權限執行,
而不是執行程式的用戶權限~
因此以這個例子來說,我們只要讓那個檔案 owner 是 root,
然後授與它 setuid,它就能被一般用戶執行,且能操作 root 才能碰到的東西了~
不過在實作時,接連遇到了幾個問題~
第一個問題是 setuid 設定在 python script 上無效~
雖然可以 chmod 4755 xxx.py,可是在執行 script 時會用 python 來執行,
而 python 並沒有 setuid,因此還是用戶權限執行起來的~
我們也不可能將 setuid 設定在 python 上面,
因為這樣大家都可以藉由 python 來提昇為 root 權限,非常危險~
為了解決這個問題,網路上一般的解法是寫一個小的 C/C++ 程式,
這個 C/C++ 程式設定了 setuid,讓它再去執行 python script~
下面是一個小程式 testsuid.cpp,它先試著自己寫 /etc/xxx,
接著再呼叫 python 去執行 testsuid.py:
#include <stdio.h> #include <stdlib.h> int main(int argc, char* argv[]) { FILE* pf = fopen("/etc/xxx", "w"); if (pf != NULL) { printf("Can write file /etc/xxx\n"); fclose(pf); } else { printf("Cannot write file /etc/xxx\n"); } system("python testsuid.py"); return 0; }
而 testsuid.py 的內容也很簡單,就只是試著去寫 /etc/yyy 這個檔案:
open("/etc/yyy", "w")
先不設定 setuid,編譯後執行看看:
testuser@localhost ~ $ ./testsuid Cannot write file /etc/xxx Traceback (most recent call last): File "testsuid.py", line 1, in <module> open("/etc/yyy", "w") IOError: [Errno 13] Permission denied: '/etc/yyy'
不意外的,用 fopen() 是沒有權限建立 /etc/xxx,而 python 程式也沒辦法建立 /etc/yyy。
接著把 testsuid 程式設定成 setduid 再執行看看:
testuser@localhost ~ $ sudo chmod 4755 testsuid testuser@localhost ~ $ sudo chown root testsuid testuser@localhost ~ $ ./testsuid Can write file /etc/xxx Traceback (most recent call last): File "testsuid.py", line 1, in <module> open("/etc/yyy", "w") IOError: [Errno 13] Permission denied: '/etc/yyy'
奇怪的事發生了,setuid 看來是有效果,因此 fopen() 現在有辦法寫出 /etc/xxx 了,
可是 python 程式卻還是沒有權限去寫,是怎麼回事呢?
經由同事的幫忙,這才知道有 uid 和 euid 這兩個東西~
uid 是登入的用戶 id,euid (effective uid) 則是執行中的 process 的 uid,
當我們設定 setuid bit 時,
process 本身的 euid 會被設定成 file owner 的 uid (本例中是 root),
但 uid 沒有改變,因此在接下來的 system() 呼叫時,
會以 uid 的值再產生下一個 process,導致 setuid 的效果沒有繼承至下一個 process~
要解決這個問題,就是在 setuid 的程式裡,多呼叫一個 setuid(0),
把自己的 uid 設定成 root,這樣子 uid 和 euid 都是 0,
下一個被呼叫的子 process 就也會是 root 權限了:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char* argv[]) { setuid(0); FILE* pf = fopen("/etc/xxx", "w"); if (pf != NULL) { printf("Can write file /etc/xxx\n"); fclose(pf); } else { printf("Cannot write file /etc/xxx\n"); } system("python testsuid.py"); return 0; }
執行後的結果如下,C++ 和 python 程式都沒有遇到錯誤,
可以成功的建立 /etc/xxx 和 /etc/yyy 了:
testuser@localhost ~ $ ./testsuid
Can write file /etc/xxx