通過寫入其父腳本程序的標準輸入,在提升的 bash 程序中執行命令
我有一個簡單的 bash 腳本
bash.sh
,它使用pkexec
.#!/bin/bash bash -c 'pkexec bash'
執行時會提示使用者輸入密碼。主腳本
bash.sh
以普通使用者身份執行,但由它啟動的 bash 實例以具有提升權限的 root 身份執行。當我打開終端視窗並嘗試將一些命令寫入提升的 bash 程序的標準輸入時,它會引發權限錯誤(如預期的那樣)。
echo 'echo hello' > /proc/<child-bash-pid>/fd/0
問題是,當我寫入父程序 (
bash.sh
) 時,它會被傳遞給子 bash 程序,然後由子 bash 程序執行命令。echo 'echo hello' > /proc/<parent-bash.sh-pid>/fd/0
我無法理解這怎麼可能?由於父程序以普通使用者身份執行,為什麼我(普通使用者)允許將命令傳遞給以更高權限執行的子程序?
我理解子程序的標準輸入連接到父腳本的標準輸入這一事實,但如果允許這樣做,那麼任何普通程序都可以通過寫入根 bash 程序的父程序來執行根命令。
這似乎不合邏輯。我錯過了什麼?
/usr/share
注意:我通過刪除只有 root 有權執行的文件來驗證子級正在執行傳遞給父級的命令。sudo touch /usr/share/testfile echo 'rm -f /usr/share/testfile' > /proc/<parent-bash.sh-pid>/fd/0
文件已成功刪除。
這很正常。為了理解它,讓我們看看文件描述符是如何工作的,以及它們是如何在程序之間傳遞的。
您提到您正在使用
GLib.spawn_async()
生成 shell 腳本。據推測,該函式創建了一個管道,用於將數據發送到孩子的標準輸入(或者您自己創建管道並將其傳遞給函式)。要生成子程序,該函式將fork()
關閉一個新程序,重新排列其文件描述符,使 stdin 管道變為 fd0
,然後是exec()
您的腳本。由於腳本以 開頭#!/bin/bash
,核心通過exec()
bash shell 解釋它,然後執行你的 shell 腳本。該 shell 腳本派生並執行了另一個 bash(順便說一下,這是多餘的;你並不真的需要bash -c
在那裡)。沒有重新排列文件描述符,因此新程序繼承了與其標準輸入文件描述符相同的管道。請注意,這本身並沒有“連接”到其父程序 - 事實上,文件描述符引用一個相同的管道,即由GLib.spawn_async()
. 實際上,我們只是為管道創建別名:這些程序中的 fd 0 都引用了管道。該過程在
pkexec
被呼叫時重複 - 但pkexec
它是一個 suid 根二進製文件。這意味著,當該二進製文件被exec()
編輯時,它以 root 身份執行,但其標準輸入仍連接到原始管道。pkexec
然後進行權限檢查(包括提示輸入密碼),最後exec()
是 bash。現在我們有一個根 shell,它從管道獲取輸入,而您的使用者擁有的許多其他程序也有對該管道的引用。要理解的重要一點是,在 POSIX 語義下,文件描述符沒有權限。文件具有權限,但文件描述符代表訪問文件(或管道之類的抽象緩衝區)的權限。您可以將文件描述符傳遞給新程序,甚至是現有程序(通過 UNIX 套接字),並且訪問文件的權限隨文件描述符一起傳遞。您甚至可以打開一個文件,然後將其所有者更改為另一個使用者,但仍然可以通過原始 fd 作為以前的所有者訪問該文件,因為僅在打開文件時檢查權限。通過這種方式,文件描述符允許跨權限邊界進行通信。通過讓您的使用者擁有的程序和 root 擁有的程序共享相同的文件描述符,您授予兩個程序對該文件描述符的相同權限。而且,由於 fd 是一個管道,並且 root 程序正在從該管道獲取命令,因此您的使用者擁有的其他程序可以以 root 身份發出命令。管道本身沒有所有者的概念,只是一系列恰好具有打開文件描述符的程序。
此外,由於基本的 Linux 安全模型假定使用者可以完全控制他們的所有程序,這意味著您可以像您所做的那樣窺視
/proc
以獲取對 fd 的訪問權限。您不能通過/proc
以 root 身份執行的 bash 程序的條目來執行此操作(因為您不是 root),但您可以為自己的程序執行此操作,並且獲得的結果管道文件描述符與您可以執行的操作完全相同它直接以root身份執行的子程序。因此,將數據回顯到管道會導致核心將其反彈回從管道讀取的程序 - 在這種情況下,只有子 root shell 正在主動從管道讀取命令。如果從終端呼叫 shell 腳本,那麼將數據回顯到其標準輸入文件描述符實際上最終會將數據寫入終端,並將顯示給使用者(但不由 shell 執行)。這是因為終端設備是雙向的,事實上,終端將連接到標準輸入和標準輸出(以及標準錯誤)。但是,終端具有用於注入輸入數據的特殊 ioctl 方法,因此仍然可以以使用者身份將命令注入 root shell(它只需要一個簡單的
echo
.總的來說,您發現了一個關於權限提升的不幸事實:當您允許使用者以任何方式提升到 root shell 時,實際上,應該假定由該使用者執行的任何應用程序都能夠濫用該提升(而它存在)。出於安全目的和目的,使用者成為 root。即使這種標準輸入註入是不可能的,例如,如果您在終端下執行腳本,您也可以簡單地使用 X 伺服器鍵盤注入支持在圖形級別直接發送命令。或者你可以使用
gdb
使用打開的管道附加到程序並將寫入註入其中。關閉這個漏洞的唯一方法是讓 root shell 直接連接到一個安全的 I/O 通道到(物理)使用者,該通道不能被非特權程序篡改。如果不嚴格限制可用性,這是很難做到的。最後一件事值得注意:通常,(匿名)管道有一個讀端和一個寫端,即兩個獨立的文件描述符。作為標準輸入傳遞給子程序的一端是讀取端,而寫入端將保留在呼叫
GLib.spawn_async()
. 這意味著子程序實際上不能寫入標準輸入以將數據發送回它們自己或以bash
root 身份執行(當然,程序通常不會寫入標準輸入,儘管沒有什麼說你不能 - 但在這種情況下當 stdin 是管道的讀取端時,它不起作用)。但是,核心/proc
用於從另一個程序訪問文件描述符的機制顛覆了這一點:如果一個程序有一個打開的 fd 到管道的讀取端,但您嘗試打開其各自的/proc
fd 文件進行寫入,那麼核心實際上會為您提供同一管道的寫入端。或者,您可以查找/proc
與呼叫 的原始程序相對應的條目GLib.spawn_async()
,找到可寫入的管道末端,然後寫入其中,這將不依賴於這種特殊的核心行為;這主要是一種好奇心,但並沒有真正改變安全問題。