一種程式設計語言而言,在設計這種語言的時候,一般是不會產生安全隱患的,事實上,這種隱患是由程式員引入的。幾乎每一種程式設計語言都有一定這樣的漏洞,這種漏洞將會在某種程度上導致不安全軟體的產生,但是一個如軟體整體的安全性仍然大部分依賴於這個軟體製造者的知識面、理解能力和他的安全意識。Perl也有它安全上令人擔憂的部分,然而大多數程式員完全沒有意識到這些方面。
在這篇文章裡,我們將會看一下Perl中一些最普遍被誤用和忽視的屬性。我們將會看到它們的誤用將會怎樣對運行它們的系統的安全以及它們的使用者造成威脅。我們將會示範怎樣把這些弱點挖掘出來以及如何去修改、避免它們。
使用者輸入上的弱點
Perl指令碼中產生安全問題的一個很大的來源是沒有經過正確確認(或根本就沒有確認)的使用者的輸入。每次當你的程式要從一個不信任使用者那裡擷取輸入資訊的時候,即使採用的是非直接的方式,你都應該小心。舉個例子來說吧,如果你在Perl中寫CGI指令碼,你要預期到惡意的使用者將會發送給你假的輸入。 不正確的使用者輸入,如果沒有經過確認就被認可並使用了,將會導致許多方面出錯。最常見和明顯的錯誤是,沒有經過確認就去執行有使用者自訂參數的其他程式。
syetem()和exec()函數
Perl以能被用作一種“粘合”語言而著稱——它能夠通過如下方式完成一個出色的工作:在調用其他程式來為它工作的時候,通過採集一個程式的輸出,將它重新格式成一種特定的方式後傳遞到其他程式的輸入的方式仔細的協調它們的運行。這樣各個程式就能很好的運行了。
正如Perl發布標語告許我們的,我們有不止一種方法可以做同樣的事。一種執行一個外部程式和一個系統命令的方法事通過調用exec()函數。當Perl遇到一個exec()語句的時候,它審視exec()被調用處的參數,然後啟動一個新的進程來執行這條特定的命令。Perl從不會返回控制給調用exec()的原來的那個進程。
另一個相似的函數是system()。system()的運行方式非常象exec()。它們之間的唯一的大的區別是Perl會首先從父進程中分叉出一個子進程,子進程作為提供給system()的一個參數。父進程等到子進程結束運行後再接著運行程式的其餘部分。我們將會在下面更詳細的討論system()調用,但這些討論大部分也適用於exec()。
傳遞給system()的參數是一個列表——列表裡的第一個元素是要被執行的這個程式的程式名,其他元素是傳給這個程式的參數。然而,如果只有一個參數的的話,system()的執行方式會發生差異。在那種情形下,Perl將會掃描這個參數看它是不是包含任何shell轉換字元。如果有的話,它就要把這些字元通過shell來解釋。所以產生一個shell命令列來工作。不然,Perl會降字串拆成單詞然後調用效率更高的c庫函數execvp(),這個函數不能理解特殊的shell字元。
現在假設我們有一張CGI表單,它要詢問使用者名稱,然後顯示包含這個使用者統計資訊的一個檔案。我名可以如下使用system()來調用’cat’實現那種要求:
system ("cat /usr/stats/$username");
使用者名稱來自這樣的一個表單:
$username = param ("username");
. 舉個例子,當使用者在表單裡添上username = jdimov,然後提交後。Perl在字串``cat /usr/stats/jdimov''中沒有找到任何轉換字元創,所以它就調用execvp()函數運行”cat”後返回到我們的指令碼中。這個指令碼也許看起來沒有害處可言,但是它容易被一個惡意的攻擊者所利用。
問題是這樣的,通過在表單的”username”域內使用特殊的字元,一個攻擊者可以通過 shell來執行任何命令。舉個例子,我們可以這樣說,如果攻擊者傳遞這樣的字串"jdimov; cat /etc/passwd",Perl會把分號當作一個轉換字元,然後把它傳遞到shell中:
cat /usr/stats/jdimov; cat /etc/passwd
攻擊者既可以獲得亞元檔案,又可以獲得密碼檔案。如果攻擊者想要搞破壞的話,他只要發送"; rm rf /*"就可以了。
我們在前面提到system()有一個參數表,並且將第一個元素看作命令來執行,而將其餘的元素作為參數來傳遞。所以我們可以稍微改變一下我們的指令碼,使只有我們想讓執行的程式能夠被執行:
system ("cat", "/usr/stats/$username");
既然我們分開來指定程式的參數,那麼shell就永遠也不會被調用了。所以發送";rm -rf /*"也就不會起作用了,因為攻擊字串將只會被解釋成一個檔案名稱而已。
這種方法比單個參數的版本要好多了,因為它避免了使用shell命令,但是仍然有潛在的缺陷。特別的,我們要考慮到$username的值會不會被利用產生程式中能被執行的弱點。舉例來說,一個攻擊者仍然可以利用我們重寫的代碼版本,通過把$username設定成字串"../../etc/passwd"來獲得系統的密碼檔案。
使用那樣的程式的時候很多地方會出錯,舉例來說,一些應用程式將特殊的字元序列解釋成執行一條shell命令的請求。一個普遍的問題是有些版本的Unix郵件工具當它們在一定的上下文背景下看到有”~!…”等字元序列的時候將會執行一個shell命令。所以在一個訊息體中的空白行中包含"~!rm -rf *"的使用者輸入將會在某種情形下產生問題。
只要是談及安全的,上面論及system()函數的任何內容也適用於exec().
Open()函數
在Perl中open()函數被用來開啟檔案。在最為常見的形式中,它是這樣使用的:
open (FILEHANDLE, "filename");
這樣使用的時候,’filename”是以唯讀方式開啟的。如果”filename”是含有”>”標誌的首碼,那麼它是為輸出而開啟的,並且在檔案已經存在的時候覆蓋原檔案;如果含有”>>”首碼,那麼是為追加開啟的;首碼”<”開啟檔案來進行輸入操作,但這也是不含首碼的時候的預設。用未經確認的使用者輸入作為檔案名稱的一部分所產生的一些問題應該總是比較明顯的。舉例來說,向後回溯瀏覽目錄的騙招在這裡仍然能用。還有其他值得擔憂的問題。現在我們使用open()替換”cat”來修改我們的指令檔。我們象這樣的命令:
open (STATFILE, "/usr/stats/$username");
然後我們從檔案中讀取代碼並顯示它。Perl文檔告許我們:如果檔案名稱是以”│”開始的,檔案名稱將會被解釋成一個輸出管道命令;反之,如果檔案名稱以”│”結束的話,檔案名稱將會被解釋成將讓我們進行輸出的管道。
於是,只要加上一個”│”首碼,使用者就可以在/usr/stats目錄下運行任何命令了。向後回溯目錄的操作能夠讓使用者在這個系統裡執行任何程式。
一種解決這個問題打方法是:對於你想要開啟並向其中輸入的檔案總是要求通過加”<”標識顯式的指明.
有時我們確實要調用一個外部的程式,比如,我們想要改表我們的指令檔以讓他能夠讀取舊的純文字檔案/usr/stats/username,但是在顯示給使用者之前要先通過一個HTML過濾器。我們有一個馬上就可以使用的便利的方法來實現這個意圖。一種方法可以這樣做:
open (HTML, "/usr/bin/txt2html /usr/stats/$username│");
print while <HTML>;
不幸的是,這依然要通過shell層。然而我們可以採用open()調用的另一個形式來避免牽涉到shell:
open (HTML, "-│")
or exec ("/usr/bin/txt2html", "/usr/stats/$username");
print while <HTML>;
當我們開啟一個管道命令,或者是為了讀(“-│”),或者是為了寫(”│-“)的時候,Perl在當前進程中產生分支,並且返回子進程的PID給父進程,返回0給子進程。”or”語句用來決定我們是在父進程還是在子進程。如果我們在父進程(傳回值為非零),我們繼續執行print()語句。否則我們在子進程中,就執行txt2html程式,使用多於一個參數的exec()的安全版本來避免傳遞任何命令到shell層。所發生的是,子進程答應txt2html產生的STDOUT輸出,然後就默默的消亡了(記住:exec()從不返回),同時父進程從STDIN中讀取結果。象這樣的技術可以被用來通過管道將輸出輸到一個外部程式的技術:
open (PROGRAM, "│-")
or exec ("/usr/bin/progname", "$userinput");
print PROGRAM, "This is piped to /usr/bin/progname";
在我們需要管道的時候,open()的以上這些形式應該總是比直接的管道open()命令優先採用,因為它們不通過shell層。現在讓我們設想我們要將靜態文本轉化成格式化很好的HTML頁面,並且,基於方便考慮,要存放在顯示這些頁面的Perl指令碼相同的目錄下。那麼我們的open語句看起來可能是如下形式:
open (STATFILE, "<$username.html");
當使用者通過表單中傳遞username=jdimo的時候,指令碼顯示jdimov.html。這裡仍然有被攻擊的可能。不同於c++和c ,perl不用空位元組來結束字串,這樣的話字串jdimov/”jdimov/lo/bah在絕大數c庫調用中解釋為”jdimo”,但是在Perl中卻是”jdimov/0blah”。當perl傳遞一個含Null 字元的字串給用c寫的程式的時候,這個問題就突出了。UNIX核心以及絕大多數UNIX 和shell 都 是純c 語言的。Perl自身也主要是且c編寫,當使用者如下調用我們的指令碼:
statscrit.plusername=jdimov/%00
會發生什麼呢?我們的程式傳遞字串”jdimov/%。html”到對應的系統調用裡以開啟它,但是因為那些系統調用是用c編寫,接受的是空位元組的字串方式。結果怎樣呢?如果有檔案”jdimov”的話就會顯示這個檔案,可能並沒有這個檔案,即使有也不是很有用。但是如果用"statscript./pusername=statscript。p/%"來呼叫指令碼,會發生什麼呢?如果指令碼和我們的html檔案在同一個目錄下的話,這樣我們可以用這個輸入來期騙指令碼,來顯示給我們所有的內容。在這種情況下或許不是什麼大的安全危險,但是它肯定能被其它的程式使用,因為它允許攻擊者分析其他可利用的缺陷的來源。
單引號
在perl中,另一種讀取外部程式的輸出的方法是把命令放在單引號裡。所以如果我們想在分等級的$stats的檔案中儲存我們stats檔案的內容的話,我們可以這樣做:
$stats=’cat/user/stats/$username’;
這確實要通過shell層來實現。任何把使用者輸入包含在一對單引號內的指令碼都有發生前面討論的所有的安全問題的危險。有很多方法可試圖使shell不要誤解一席可能的轉換字元。但是最安全的事就是不要用但引號。取而代之的是,開啟一個通到STDIN的管道,然後分叉執行外部程式,就像我們在前一節open()所做的一樣。
Eval()和/e 修飾符
函數eval()可以在已耗用時間執行一個Perl代碼塊,並返回上一次經評估語句的值。這種函數功能經常用於諸如設定檔,它可以寫成perl代碼,除非你絕對相信輸進eval()的原始碼,否則不要做諸如eval/$userinput,之類的事,這也適用於一個常規表述中的/e 修飾符,用來使perl在執行之前解釋該表述。
過濾使用者輸入
用於本節我們所討論的所有問題的過濾使用者輸入的一個通常方法(FU In OCA )就是過濾任何不需要的轉換字元和有問題的資料。例如我們可以在任何時段過濾來避免向後的目錄查看。類似的,我們一旦看見非法的字元,就讓程式運行失敗,這種策略被稱為”黑名單”這種哲學就是如果某東西沒有明確禁止,那它肯定是好的。一個更好的策略就”白名單”,它指如果某東西沒有被明確認可,那麼它必須禁止。黑明單的最重要的問題是它非常難保持完整性並得到更。你也許會忘掉過濾某一特定字元,或者你的程式或許不得不隨不同的轉換字元集合轉到一個不同shell中。不過濾掉不需要的轉換字元和其他危險輸入,相反,只過濾進合法的輸入。下面的片段就是一個例子,它會停止執行一個安全性有問題的操作,如果有戶輸入中包含了除字母,數字,點和@符號外任何東西(@經常用於使用者的電子郵件地址)
unless($useradress=~/^[-@/w。]+)$/)
{print”secrity error。/n”exit(1);
}
基本的思想是不去編譯一個特定值的列表來保護,而是產生一個安全清單來接受可接受的輸入值的列表。可接受的輸入輸入值的選擇當然會隨著不同的應用程式而變化。可接受的值應該採用某種能將破壞的可能性降到最小的方式來選擇。
避免shell
當然,你也必須儘可能的避免shell,然而這種技術可被廣泛地應用。如果你調用一個有特定序列的編輯器。你必須確認這些特定序列是不被允許使用的。一般,通過使用現存perl模組,你能避免使用外部程式來執行一個外部函數,CPAN是一個能完成幾乎所有標準UNIX工具集能做的任何事的經測試的函數的模組來源,然而它或許會費點勁來包含一個模組,並且調用它,而不是調用一個外部程式,模組方法一般來說更安全和靈活,為解釋清楚這一點,使用Net::SMTP,而不用exec()’ing sendmail/--T會幫你少一些使用shell的麻煩,並能防止你的使用者在sendmail代理中尋求已知的弱點。
其它安全問題的來源(不安全環鏡變數)
使用者輸入實際上是perl程式的主要安全問題的來源,但是還有其它因素是在寫安全的perl原始碼時所必須考濾的,經常 在shell下啟動並執行指令碼的易受攻擊的弱點或者通過網路伺服器是不安全的環鏡變數,最通常的是PATH /變數。當你從你代碼內部中使用一個外部應用程式或功能,而僅僅指明了一條相對路徑的時候,你就使 你這個程式和運行它的系統處於危險中。如果你有如下一個system()調用:
system(“txt2html”,”user/stats/jdiov”);
對於這種調用,你假設txt2htm檔案是包含在PATH變數某處的目錄下,但是假如發生這種情況,一個攻擊者改變你的路徑指向含有相同的名字的其他帶惡意的程式中,你的系統的安全性就得不到保證。為了避免例似的事情發生,每個需要含有遠程安全意識的程式都應該這樣開始寫:
#!/usr/bin/perl -wT
require 5.001;
use strict;
$ENV{PATH} = join ':' => split (" ", << '__EOPATH__');
/usr/bin
/bin
/maybe/something/else
__EOPATH__
如果程式依賴於其他環境變數,它們也要在它們使用前明確的定義出來。
另一個危險變數(這個更是針對perl的)是@INC距陣變數,它非常像PATH,只不過它 指明Perl到哪裡去找要包含在程式中的模組。有關@INCR 的問題和PATH是非常相似的。有人可能會把你的perl指向一個具有相同的名字模組,而且如你所料的做同樣的事,但是它也背地裡做一些壞事,因此@INC同PATH都不值的信任。在包括任何外部模組之前都 必須完全重新定義。
Setuid指令碼
通常一個Perl程式是以執行它的使用者的許可權來啟動並執行。通過產生一個setuid指令碼,它的有效使用者的ID可以設定成更高的許可權,這個許可權使這個使用者可以訪問他實際沒有存取權限的資源,比如passwd程式使用setuid來擷取對系統password檔案的寫的許可權,這樣允許使用者來更改自已的密碼,因為執行程式是通過CGI界而來執行的,該介面是在使用網路伺服器的使用者權限下啟動並執行,CGI程式員經常 試圖使用setuid技巧來讓他們的指令碼執行一些惡作劇。這有可能用,但也可能十分危險,對一個事情,如果一個攻擊者發現一各方法可以利用指令碼的弱點他們不僅是獲得訪誤問系統的許可權,但是他們會用有效UID的特權來獲得存在著另外幾種類似的種族狀況,在一個程式當中,這類缺陷是比較容易監控的,尤其是對有經驗的程式員來說,目前相關方面的工作正在積極的探索著。關於這個問題,目前還沒有一個既容易又完全有效解決方案,常常在種族狀況存在的可能情況下,用到的最好的方法是使用原子操作方式來進行,這就意味著僅僅使用一種系統來同時檢查和生存檔案。而不必使用處理器,在二者之間進行切換。當然,這不可能是常有的。
另外我們所作的一種標準模式是使用SYSOPEN來確定一種唯讀模式,不必再設定刪減標誌。
通過這種方法,即使我們的檔案名稱確實已經形成,當我們開啟檔案進行寫操作的時候 ,我們也不會破壞檔案。注意:Fcnt1模快必須包含進來,以便讓sysopen()函數起作用,因為這個模組是如下常數,O_RDONLY, O_WRONLY, O_CREAT,等被定義的地方。
緩衝區溢位和perl
一般來說,perl指令碼是不容易發生緩衝區溢位的,因為perl能在需要的時候動態擴充它的資料結構。Perl跟蹤為每個字串分配的大小。在一個字串每次被賦值之前,perl保證有足夠的空間可以利用,如果需要的話,也可以為那個字串分配更多的空間。
然而在一些較老的perl實現方法中有幾種熟為人知的緩衝區溢位情形。一個明顯的例子是5.003版本會因為緩衝區溢位而崩潰。所有的suidperl版本(一種設計用來工作在為某些核心)都是在早於5。004版本的perl的不同分類的基礎上建立起來的。
結論:
在我們以後的文章中,我們將會化一定的時間來熟悉perl提供給我們的安全特性,尤其是perl的”taint 模式“,並且,我們將會證明,如果我們不小心的話,即使在如此堅固的安全機制下,依然可能會出現的一些問題。在學習perl的這些方面以及一些典型的例子的時候。我們的目標是為了培養我們的一種直覺,這種直覺能協助我們在看perl指令碼的第一眼時就能夠意識到其中的安全問題,以避免在我們的程式中犯類似的錯誤。