0x00 條件競爭
一、漏洞概念
競爭條件指多個線程或者進程在讀寫一個共享數據時結果依賴於它們執行的相對時間的情形。例如:考慮下面的例子:
假設兩個進程P1和P2共享了變量a。在某一執行時刻,P1更新a爲1,在另一時刻,P2更新a爲2。因此兩個任務競爭地寫變量a。在這個例子中,競爭的“失敗者”(最後更新的進程)決定了變量a的最終值。多個進程併發訪問和操作同一數據且執行結果與訪問的特定順序有關,稱爲競爭條件。
如果程序未做或者爲做好線程同步,會造成最終共享變量異常,引起其他安全問題,例如無限制購買、繞過某些次數限制等等。
二、漏洞案例
案例1:辣條購買(2018護網杯題目)
題目如上所示,用戶只有20元,只能買4包大辣條,此處我們不要求獲取flag,但如何換取一個辣條之王呢?肯定是需要想辦法獲取另外的一包。(該題目解析詳情請見:https://blog.csdn.net/iamsongyu/article/details/83346029)
從開發者角度梳理下購買流程:
- 點擊購買大辣條
- 啓動購買線程,從數據庫中獲取用戶餘額
- 查詢餘額是否大於商品價格,若大於購買成功
- 餘額減5
- 將剩餘餘額寫入數據庫。下次購買再從1開始循環。
若在某時刻購買多個辣條,如果按照上述順序依次循環執行,是沒有問題的。然而如果開發者沒有做好線程同步就會產生條件競爭。
問題發生在2-3-5這一過程,餘額減5後如果沒有及時更新到數據庫,存在其它線程此刻獲取數據庫中的餘額數據,使得下次獲取的餘額不正確。
從條件競爭角度看,其中數據庫中的餘額爲共享變量,多次購買發生在同一時刻可觸發多個線程訪問共享變量餘額。所以可以通過Berpsuit工具同一時刻多線程發送大量購買報文,期望變更後的餘額在沒有更新入數據庫前,而被後續的購買操作獲取了非法餘額,造成下一次實際餘額已經不足但是購買成功。而實際操作是成功的。
所以開發時必須保持2到5這一過程的原子性。
案例2:TOCTOU條件競爭
Linux環境中有如下運行的代碼:
int i;
for(i=1;i<argc;i++)
{
if(!access(argv[i],O_RDONLY))
{
fd = open(argv[i],O_RDONLY)
}
printf(fd);
}
一般地進程ruid=euid或者在windows 程序中,即使通過上述間隙,字符串路徑被軟鏈接到其它特權文件,open打開是也會報無法訪問的錯誤碼。
出現問題的是在具有suid的進程中,此時ruid爲真實用戶的uid,ruid的用戶運行具備suid的程序後,euid被設置爲該程序擁有者的權限,如果該程序擁有者爲root,則當前ruid用戶對應的euid將會變成0,也就是root權限。所以問題就來了,如果程序中對於訪問文件的權限希望是通過真實用戶的ruid進行判斷,這樣也是正常業務符合邏輯的,如果符合權限就可以open文件。但是open使用的是euid來判斷是否有權限打開文件,所以open的範圍將比access的範圍要大,如果存在euid能夠訪問的敏感文件但是不能被ruid訪問,導致了前後防禦的不一致,也就是違反了韌性中的層層防禦原則,沒有做到一致的協同保護。(ruid、euid和suid詳情請見:https://blog.csdn.net/charles_neil/article/details/79762334),所以會有如下的攻擊場景。
如果運行的程序是有suid的root權限,且打開文件時希望使用真實用戶的id判斷權限。攻擊者可以通過在access()和open()之間的間隙替換掉原來的文件,如下所示:
程序運行流程 |
攻擊者 |
access(“/tmp/attack”) |
|
|
unlink("/tmp/attack") |
|
symlink("/etc/shadow","/tmp/attack") |
open(“/tmp/attack”) |
|
上述漏洞就是著名的TOCTOU條件競爭漏洞,英文爲:Time of Check,Time of Use。總結該漏洞的原因本質就是:檢查和使用之間存在間隙,在此間隙內改變檢查所指的對象,是的使用時造成未授權訪問。從英文全稱也是可以看出的。但是一般需要檢查和使用時的權限有差別可能纔會造成越權事故。
修正方法思想就是要做到協同保護,確保不同保護機制之間的協調一致, 最大程度減少相互之間干擾、連鎖故障與防守空檔。
- 所以直接使用open去訪問,利用open的錯誤碼進行判斷是否有權限。這就打破了TOCTOU的流程。
- 又因爲open使用euid,權限過大,所以通過setresuid(運行條件是當前進程的euid是root)臨時將程序降權到ruid。如果此時suid是root
代碼如下所示
int i;
int caller_uid = getuid();
int owner_uid = geteuid();
for(i=1;i<argc;i++)
{
if (setresuid(-1,caller_uid,owner_uid)!= 0)//是euid爲ruid
{
exit(-1);
}
fd = open(argv[i],O_RDONLY);
if (setresuid(-1,owner_uid,caller_uid)!= 0)//回到之前的各id狀態
{
exit(-1)
}
if (fd != -1)
{
printf(fd);
}
}
0x01 Python中的條件競爭
一、模擬條件競爭場景
模擬以下場景:假設有個在線計數器,用於統計用戶投票或者點擊情況。如下代碼所示,通過訪問http:// 10.164.XXX.XXX:8000/add/模擬計數功能。代碼如下所示(基於Django框架),每次訪問開啓一個線程對用戶的請求做處理。
count = 10
def increment():
time.sleep(0.1)
global count
new_count = count + 1
count = new_count
print str(count) + ' end\n'
return
def incorrectThread(request):
t1 = threading.Thread(target=increment)
t1.start()
return HttpResponse(str(count))
urlpatterns = [
url(r'^add/$', incorrectThread),
url(r'^printCount/$', printCount),
]
上述代碼increment函數每次給全局計數變量count加1,從代碼可以看出沒有爲count的訪問進行線程同步保護,在大量同步訪問設置值時會產生條件競爭問題。最終可能造成最後技術小於實際訪問的人數。
爲了模擬多人同時點擊,通過Berpsuit工具首先攔截了請求報文,然後設置999線程同時訪問上述URL,若沒有條件競爭問題,理論上在999次訪問後並加上攔截報文的1次後,count最後的值應爲1010。然而實際結果如下所示:
最終結果爲1009,與理論值少1。通過上圖日誌發現在649時發生了條件競爭,count少加了一次。通過查詢count的結果發現,count值爲非預期值:
二、問題修復
上節中的案例可通過python內置threading模塊的互斥鎖進行防禦,代碼如下所示:
count = 10
#互斥鎖實例
lock = threading.Lock()
def increment():
global lock
#acquire請求鎖
if lock.acquire():
time.sleep(0.1)
global count
new_count = count + 1
count = new_count
print str(count) + ' end\n'
# release釋放鎖,使得其他線程可獲取鎖
lock.release()
return
threading模塊中有如下兩個方法:
acquire([timeout]): 使線程進入同步阻塞狀態,嘗試獲得鎖定。
release(): 釋放鎖。使用前線程必須已獲得鎖定,否則將拋出異常。
修改上述代碼後,保證了線程同步。進行多次測試,最終的計數結果均是正確的。
三、Python線程同步模塊
解決條件競爭的方法就是要讓存取公共變量的線程進行同步,也就是線程安全,目的是讓過程實現原子性。例如讀和寫某一變量的過程是原子性的,不可分割的,在讀寫的過程中,其他指令無法插入,等待設置完值後纔可以讀其中的數據,這就使得每次讀取的值就是最新值。一般線程安全可通過鎖(互斥所、讀寫鎖等)、條件變量、信號量、事件等方式實現。
Python中進行線程同步的方法見:
https://www.cnblogs.com/huxi/archive/2010/06/26/1765808.html
該文給出了通過python內置庫實現線程同步的方法:thread在python3中改名爲_thread和threading。
thread和threading模塊都可以用來創建和管理線程,而thread模塊提供了基本的線程和鎖支持。threading提供的是更高級的完全的線程管理。低級別的thread模塊是推薦給高手用,一般應用程序推薦使用更高級的threading模塊:
1.它更先進,有完善的線程管理支持,此外,在thread模塊的一些屬性會和threading模塊的這些屬性衝突。
2.thread模塊有很少的(實際上是一個)同步原語,而threading卻有很多。
3.thread模塊沒有很好的控制,特別當你的進程退出時,
比如:當主線程執行完退出時,其他的線程都會無警告,無保存的死亡,
而threading會允許默認,重要的子線程完成後再退出,它可以特別指定daemon類型的線程。
官方文檔見:
Threading:
https://docs.python.org/3/library/threading.html
https://docs.python.org/2.7/library/threading.html
Thread(python2):
https://docs.python.org/2.7/library/thread.html#module-thread
_Thread(python3)
https://docs.python.org/3/library/_thread.html#module-_thread
四、文件訪問TOCTOU
Python中也有類似的方法編寫和1.2.3小節相同的代碼。
1.2.3小節已經講述了問題的原因,所以根據官方文檔分析下這兩個函數是否有防守空擋。
官方文檔對os.access函數解釋如下:
已經說明了存在TOCTOU漏洞,所以Python中的Access和open使用的權限分別也是ruid和euid。
案例可以做成如下:
例如代碼文件test.py,在linux中設置改程序suid爲root,然後轉到普通用戶運行改程序,最後會輸出密碼文件信息。此處在代碼插入symlink爲模擬有其他程序一直嘗試在修改字符串鏈接到敏感文件,如果運氣好就可能執行該流程。讀者可自行嘗試下
import os
def fileCompetion():
path = "/root/ZZYDIR/1.txt"
if os.access(path, os.R_OK):
#在access判斷權限後變更路徑爲敏感文件的軟鏈接
os.unlink(path)
os.symlink('/etc/passwd', path)
f = open(path, 'r')
for i in f:
print i
print "success"
print "error"
fileCompetion()
同時也存在和1.2.3中相同的對於suid爲root下臨時變更權限的函數 os.setresuid(ruid, euid, suid) 。所以suid爲root下也可以使用該函數進行防禦。
0x02 安全測試建議
一、發現條件競爭場景
條件競爭從黑盒測試方法較容易發現,代碼白盒審計較難發現。所以需要敏感嗅覺發現可能開發者編碼不當造成條件競爭的場景。條件競爭問題常見於如下場景,滲透測試這需要關注:
- 購買場景:付款/購買/積分/訂單操縱(例如1.2.1案例1)
- 兌換:積分/優惠券/註冊邀請碼
- 繞過次數、同名限制
- 多過程處理,如文件上傳處理
- 以及其他識別出共享同一資源進行操作的場景
特點總結來說就是——共享同一資源,生成其他結果。該漏洞具有偶現性,很受環境因素的影響,比如網絡延遲、服務器的處理能力等,所以只執行一次可能並不會成功,儘量多嘗試幾次。
二、挖掘方法
如前文大量案例所示,可使用Berpsuit軟件的Inturder模塊,設置多個線程進行異步發包。通過查看多個異步請求返回的不同結果,比如11個測試中有10個相同,那一個包可能就是攻擊成功的請求。
0x03 開發建議
一、對於一般的共享變量保護
- 開發者需要識別線程中訪問的共享變量
- 判斷該共享變量是否需要保持同步性
- 若需要保持同步,使用Python內置的threading或者thread(python3爲_thread)模塊的線程同步的相關方法保護共享變量的讀寫。這些模塊包含了大量的同步方法,例如鎖(互斥所、讀寫鎖等)、條件變量、信號量、事件等。
- 可能由於同步設計和實現的不合理,仍然造成條件競爭,建議開發者通過上述安全測試方法多次進行驗證。
二、文件訪問TOCTOU
本質思想是檢查和使用時採取同樣的策略(一般是相同的權限校驗機制)。常見的對於suid爲root的程序採用上述的os.setresuid(ruid, euid, suid)臨時降權。