1. 程式人生 > 實用技巧 >web安全之快速反彈 POST 請求

web安全之快速反彈 POST 請求

在 CTF Web 的基礎題中,經常出現一類題型:在 HTTP 響應頭獲取了一段有效期很短的 key 值後,需要將經過處理後的 key 值快速 POST 給伺服器,若 key 值還在有效期內,則伺服器返回最終的 flag,否則繼續提示“請再加快速度!!!”

如果還執著於手動地獲取 key 值,複製下來對其進行處理,最後用相應的工具把 key 值 POST 給伺服器,那麼對不起,因為 key 值的有效期一般都在 1 秒左右,除非有單身一百年的手速,否則不要輕易嘗試。顯然,這類題不是通過純手工完成的,幸好 Python 提供了簡單易用、功能強大的 HTTP第三方開源庫Requests,幫助我們輕鬆解決關於 HTTP 的大部分問題。

程式碼如下:

只需要再用時把url改一下就好

import requests
import re

url = "http://xuenixiang.cn:22496/"
s = requests.Session()#保持客戶端與伺服器之間的回話
r = s.get(url)
expression = re.search(r'(\d+[\+\-\*])+(\d+)', r.text).group()#用正則表示式提出要計算的表示式
expr_value = eval(expression)#計算表示式的值
data = {'value': expr_value}
print(s.post(url,data).text)#傳送post請求

0x01 Python Requests

關於 Requests 庫的詳細功能請見官方文件,本文只列出解題中需要用到的部分功能。

安裝並匯入 requests 模組

在安裝了 Python 的終端下輸入以下命令安裝 requests:

$ pip install requests

安裝完使用以下命令匯入 requests:

>>> import requests

傳送 GET 請求與 POST 請求

以 Github 官網為例,對其發起 GET 請求;

>>> r = requests.get('https://github.com/')

對其發起 POST 請求:

>>> r = requests.post('https://github.com/')

檢視請求頭

對 Github 官網發起請求,以檢視 GET 請求的請求頭為例,POST 請求同理:

  1. >>> r = requests.get('https://github.com/')
  2. >>> r.request.headers
  3. {'Connection': 'keep-alive', 'Accept-Encoding': 'gzip, deflate',...

檢視請求頭的某一屬性:

  1. >>> r.request.headers['Accept-Encoding']
  2. 'gzip, deflate'

檢視響應頭

對 Github 官網發起請求,以檢視 GET 請求的響應頭為例,POST 請求同理:

  1. >>> r = requests.get('https://github.com/')
  2. >>> r.headers
  3. {'Status': '200 OK', 'Expect-CT': 'max-age=2592000, report-uri=...

檢視響應頭的某一屬性:

  1. >>> r.headers['Status']
  2. '200 OK'

檢視響應內容

對 Github 官網發起請求,檢視伺服器返回頁面的內容,以檢視 GET 請求的響應內容為例,POST 請求同理:

  1. >>> r = requests.get('https://github.com/')
  2. >>> r.text
  3. u'\n\n\n\n\n\n<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="utf-8">\n...

傳遞 GET 請求引數

GET 請求引數作為查詢字串附加在 URL 末尾,可以通過requests.get()方法中的params引數完成。例如,我要構建的 URL 為https://github.com/?username=ciphersaw&id=1,則可以通過以下程式碼傳遞 GET 請求引數:

  1. >>> args = {'username': 'ciphersaw', 'id': 1}
  2. >>> r = requests.get('https://github.com/', params = args)
  3. >>> print(r.url)
  4. https://github.com/?username=ciphersaw&id=1

其中params引數是dict型別變數。可以看到,帶有請求引數的 URL 確實構造好了,不過注意,這裡的usernameid是為了說明問題任意構造的,傳入 Github 官網後不起作用,下同。

傳遞 POST 請求引數

POST 請求引數以表單資料的形式傳遞,可以通過requests.post()方法中的data引數完成,具體程式碼如下:

  1. >>> args = {'username': 'ciphersaw', 'id': 1}
  2. >>> r = requests.post('https://github.com/', data = args)

其中data引數也是dict型別變數。由於 POST 請求引數不以明文展現,在此省略驗證步驟。

如果想傳遞自定義 Cookie 到伺服器,可以使用cookies引數。以 POST 請求為例向 Github 官網提交自定義 Cookie(cookies引數同樣適用於 GET 請求):

  1. >>> mycookie = {'userid': '123456'}
  2. >>> r = requests.post('https://github.com/', cookies = mycookie)
  3. >>> r.request.headers
  4. ...'Cookie': 'userid=123456',...

其中cookies引數也是dict型別變數。可以看到,POST 請求的請求頭中確實包含了自定義 Cookie。

會話物件 Session()

Session 是儲存在伺服器上的相關使用者資訊,用於在有效期內保持客戶端與伺服器之間的狀態。Session 與 Cookie 配合使用,當 Session 或 Cookie 失效時,客戶端與伺服器之間的狀態也隨之失效。

有關 Session 的原理可參見以下文章:

session的根本原理及安全性
Session原理

requests 模組中的 會話物件 Session() 能夠在多次請求中保持某些引數,使得底層的 TCP 連線將被重用,提高了 HTTP 連線的效能。

Session() 的建立過程如下:

>>> s = requests.Session()

在有效期內,同一個會話物件發出的所有請求都保持著相同的 Cookie,可以看出,會話物件也可以通過getpost方法傳送請求,以傳送 GET 請求為例:

>>> r = s.get('https://github.com/')

0x02 writeups

介紹完 requests 模組的基本使用方法,下面藉助幾道題來分析講解。另外,在 HTTP 響應頭中獲取的 key 值通常是經過 base64 編碼的,所以還需要引入內建模組base64用於解碼。以下程式碼均在 Python 3.6 環境下執行。

【實驗吧 CTF】 Web —— 天下武功唯快不破

此題是 Web 型別快速反彈 POST 請求的基礎題,結合 requests 模組與 base64 模組寫一個 Python 指令碼即可實現快速反彈 POST 請求。相關連結如下:

進入解題連結,發現如下提示:

“沒有一種武術是不可擊敗的,擁有最快的速度才能保持長勝,你必須竭盡所能做到最快。” 換句話說,如果我們沒有天下第一的手速,還是藉助工具來解題吧。再看看原始碼有沒什麼新發現:

提示說請用 POST 請求提交你發現的資訊,請求引數的鍵值是 key。最後按照常規思路看看響應頭:

結果發現有一個 FLAG 屬性,其值是一段 base64 編碼。在用 Python 指令碼解題之前,為了打消部分同學的疑慮,先看看純手工解碼再提交 POST 請求會有什麼效果:

將 FLAG 值進行 base64 解碼後,在 Firefox 下用New Hackbar工具提交 POST 請求:

提示需要你再快些,顯然必須要用程式語言輔助完成了。下面直接上 Python 指令碼解題:


1

2

3

4

5

6

7

8

9

10


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import base64

url = 'http://ctf5.shiyanbar.com/web/10/10.php'

headers = requests.get(url).headers

key = base64.b64decode(headers['FLAG']).decode().split(':')[1]

post = {'key': key}

print(requests.post(url, data = post).text)

  • Line 6:URL 地址的字串;
  • Line 7:獲得 GET 請求的響應頭;
  • Line 8:先將響應頭中 FLAG 屬性的值 用base64 解碼,得到的結果為bytes-like objects型別,再用decode()解碼得到字串,最後用split(':')分離冒號兩邊的值,返回的list物件中的第二個元素即為要提交的 key 值;
  • Linr 9:構造 POST 請求中data引數的dict型別變數;
  • Line 10:提交帶有data引數的 POST 請求,最終列印響應頁面的內容。

執行完指令碼後,即可看到返回的最終 flag:

【Bugku CTF】 Web —— Web6

此題是上一題的升級版,除了要求快速反彈 POST 請求,還要求所有的請求必須在同一個 Session 內完成,因此會話物件 Session() 就派上用場了。相關連結如下:

進入解題連結,直接檢視原始碼:

發現 POST 請求引數的鍵值為 margin,最後看看響應頭:

發現 flag 屬性,其值同樣是一段 base64 編碼。這裡就不手工解碼再提交 POST 請求了,直接用上一題的 Python 指令碼試試:

此處注意第 8 行的 base64 解碼,因為經過第一次 base64 解碼後,仍然還是一段 base64 編碼,所以要再解碼一次。解題過程中,要自行動手檢視每一次解碼後的值,才能選擇合適的方法去獲得最終 key 值。


1

2

3

4

5

6

7

8

9

10


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import base64

url = 'http://120.24.86.145:8002/web6/'

headers = requests.get(url).headers

key = base64.b64decode(base64.b64decode(headers['flag']).decode().split(":")[1])

post = {'margin': key}

print(requests.post(url, data = post).text)

結果如下,果然沒那麼容易得到 flag:

嗯,眉頭一緊,發現事情並不簡單。下面看看 GET 請求與 POST 請求的請求頭與響應頭是否內有玄機:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import base64

url = 'http://120.24.86.145:8002/web6/'

get_response = requests.get(url)

print('GET Request Headers:\n', get_response.request.headers, '\n')

print('GET Response Headers:\n', get_response.headers, '\n')

key = base64.b64decode(base64.b64decode(get_response.headers['flag']).decode().split(":")[1])

post = {'margin': key}

post_responese = requests.post(url, data = post)

print('POST Request Headers:\n', post_responese.request.headers, '\n')

print('POST Response Headers:\n', post_responese.headers, '\n')

不出所料,結果如下,原來是 GET 請求和 POST 請求的響應頭都有 Set-Cookie 屬性,並且值不相同,即不在同一個會話中,各自響應頭中的 flag 值也不等:

接下來引入會話物件 Session(),稍作修改就能保證 GET 請求與 POST 請求在同一個會話中了:


1

2

3

4

5

6

7

8

9

10

11


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import base64

url = 'http://120.24.86.145:8002/web6/'

s = requests.Session()

headers = s.get(url).headers

key = base64.b64decode(base64.b64decode(headers['flag']).decode().split(":")[1])

post = {"margin":key}

print(s.post(url, data = post).text)

與上一題程式碼的區別是:此處用會話物件 Session() 的getpost方法,而不是直接用 requests 模組裡的,這樣可以保持 GET 請求與 POST 請求在同一個會話中。將同一會話中的 key 值作為 POST 請求引數提交,最終得到 flag:

雖然到此即可結束,但為了驗證以上兩次請求真的在同一會話內,我們再次檢視請求頭與響應頭:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import base64

url = 'http://120.24.86.145:8002/web6/'

s = requests.Session()

get_response = s.get(url)

print('GET Request Headers:\n', get_response.request.headers, '\n')

print('GET Response Headers:\n', get_response.headers, '\n')

key = base64.b64decode(base64.b64decode(get_response.headers['flag']).decode().split(":")[1])

post = {'margin': key}

post_responese = s.post(url, data = post)

print('POST Request Headers:\n', post_responese.request.headers, '\n')

print('POST Response Headers:\n', post_responese.headers, '\n')

結果如下,GET 請求中響應頭的 Set-Cookie 屬性與 POST 請求中請求頭的 Cookie 屬性相同,表明兩次請求確實在同一會話中。

既然只需要保持兩次請求中 Cookie 屬性相同,那能不能構造 Cookie 屬性通過普通的getpost方法完成呢?答案是可以的。請見如下程式碼:


1

2

3

4

5

6

7

8

9

10

11

12


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import base64

url = 'http://120.24.86.145:8002/web6/'

headers = requests.get(url).headers

key = base64.b64decode(base64.b64decode(headers['flag']).decode().split(":")[1])

post = {"margin": key}

PHPSESSID = headers["Set-Cookie"].split(";")[0].split("=")[1]

cookie = {"PHPSESSID": PHPSESSID}

print(requests.post(url, data = post, cookies = cookie).text)

  • Line 10:獲得 GET 請求響應頭中 Set-Cookie 屬性的 PHPSESSID 值,該語句如何構造請自行分析 Set-Cookie 屬性字串值的結構;
  • Line 11:構造 POST 請求中cookies引數的dict型別變數;
  • Line 12:提交帶有data引數與cookies引數的 POST 請求,最終列印響應頁面的內容。

毫無疑問,以上程式碼的結果也是最終的 flag。

【Bugku CTF】 Web —— 秋名山老司機

前面兩題均是對響應頭中與flag相關的屬性做解碼處理,然後快速反彈一個 POST 請求得到 flag 值。而本題要求計算響應內容中的表示式,將結果用 POST 請求反彈回伺服器換取 flag 值。實際上換湯不換藥,依舊用 Python 寫個指令碼即可解決。

開啟解題連線,老規矩先看原始碼:

題意很明確,要求在 2 秒內計算給出表示式的值…呃,然後呢?重新整理頁面再看看,噢噢,然後再將計算結果用 POST 請求反彈回伺服器,請求引數的 key 值為value

從頁面內容中擷取表示式,可以用 string 自帶的split()函式,但必須先要知道表示式兩邊的字串,以其作為分隔符;也可以用正則表示式,僅需知道表示式本身的特徵即可。此處用正則表示式更佳。先放上題解指令碼,再來慢慢解析:


1

2

3

4

5

6

7

8

9

10

11

12

13


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

import requests

import re

url = 'http://120.24.86.145:8002/qiumingshan/'

s = requests.Session()

source = s.get(url)

expression = re.search(r'(\d+[+\-*])+(\d+)', source.text).group()

result = eval(expression)

post = {'value': result}

print(s.post(url, data = post).text)

有關 requests 的部分此處不細講,唯一要注意的是,與上一篇 writeup 一樣,要利用會話物件 Session(),否則提交結果的時候,重新生成了一個新的表示式,結果自然錯誤。

  • Line 9:是利用正則表示式擷取響應內容中的算術表示式。首先引入 re 模組,其次用search()匹配算術表示式,匹配成功後用group()返回算術表示式的字串。(想掌握正則表示式,還是要多看、多想、多練,畢竟應用場合非常之廣)

search() 的第一個引數是匹配的正則表示式,第二個引數是要匹配的字串。其中\d+代表一個或多個數字;[+\-*]匹配一個加號,或一個減號,或一個乘號,注意減號在中括號內是特殊字元,要用反斜槓轉義;(\d+[+\-*])+代表一個或多個由數字與運算子組成的匹配組;最後再加上剩下的一個數字(\d+)

  • Line 11:在獲得算術表示式的字串後,直接利用 Python 的內建方法eval()來計算出結果,簡單、暴力、快捷。

執行完上述指令碼,就有一定的概率可以獲得 flag 了:

為什麼說是一定概率呢?讀者們自行嘗試便知,據我觀察,當計算結果超出一定長度時,伺服器就不響應了。在此猜想:可能客戶端 Python 指令碼計算錯誤,也可能伺服器端 PHP 指令碼對大數計算有誤差,還可能在 POST 請求過程中令大整數發生改變。至於是哪種,還請高手解答。