詳解 CTF Web 中的快速反彈 POST 請求

目錄

 

0x00 前言

0x01 Python Requests

安裝並導入 requests 模塊

發送 GET 請求與 POST 請求

查看請求頭

查看響應頭

查看響應內容

傳遞 GET 請求參數

傳遞 POST 請求參數

會話對象 Session()

0x02 writeups

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

【Bugku CTF】 Web —— Web6

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


0x00 前言

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

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

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 請求同理:

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

查看請求頭的某一屬性:

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

查看響應頭

對 Github 官網發起請求,以查看 GET 請求的響應頭爲例,POST 請求同理:

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

查看響應頭的某一屬性:

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

查看響應內容

對 Github 官網發起請求,查看服務器返回頁面的內容,以查看 GET 請求的響應內容爲例,POST 請求同理:

>>> r = requests.get('https://github.com/')
>>> r.text
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 請求參數:

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

其中 params 參數是 dict 類型變量。可以看到,帶有請求參數的 URL 確實構造好了,不過注意,這裏的 username和 id 是爲了說明問題任意構造的,傳入 Github 官網後不起作用,下同。

傳遞 POST 請求參數

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

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

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

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

>>> mycookie = {'userid': '123456'}
>>> r = requests.post('https://github.com/', cookies = mycookie)
>>> r.request.headers
...'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,可以看出,會話對象也可以通過 get 與 post方法發送請求,以發送 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 請求。相關鏈接如下:

syb_fast_question

進入解題鏈接,發現如下提示:

syb_fast_page

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

syb_fast_page_source

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

syb_fast_response_header

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

syb_fast_submit_key

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

syb_fast_fail_page

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

#!/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)
  • 執行完腳本後,即可看到返回的最終 flag: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 請求,最終打印響應頁面的內容。

syb_fast_flag

【Bugku CTF】 Web —— Web6

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

bugku_fast_question

進入解題鏈接,直接查看源碼:

bugku_fast_page_source

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

bugku_fast_response_header

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

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

#!/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 請求的請求頭與響應頭是否內有玄機:

#!/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 請求在同一個會話中了:

#!/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() 的 get 和 post 方法,而不是直接用 requests 模塊裏的,這樣可以保持 GET 請求與 POST 請求在同一個會話中。將同一會話中的 key 值作爲 POST 請求參數提交,最終得到 flag:

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

#!/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 屬性通過普通的 get 與 post 方法完成呢?答案是可以的。請見如下代碼:

#!/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)
  •  
  • 毫無疑問,以上代碼的結果也是最終的 flag。Line 10:獲得 GET 請求響應頭中 Set-Cookie 屬性的 PHPSESSID 值,該語句如何構造請自行分析 Set-Cookie 屬性字符串值的結構;
  • Line 11:構造 POST 請求中 cookies 參數的 dict 類型變量;
  • Line 12:提交帶有 data 參數與 cookies 參數的 POST 請求,最終打印響應頁面的內容。

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

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

bugku_qiuming_driver

打開解題連接,老規矩先看源碼:

bugku_qiuming_page_source

題意很明確,要求在 2 秒內計算給出表達式的值…呃,然後呢?刷新頁面再看看,噢噢,然後再將計算結果用 POST 請求反彈回服務器,請求參數的 key 值爲 value

bugku_qiuming_hint

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

#!/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 了:

bugku_qiuming_flag

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

 

本文標題:詳解 CTF Web 中的快速反彈 POST 請求

文章作者:Cipher Saw

原始鏈接:https://ciphersaw.github.io/2017/12/16/詳解 CTF Web 中的快速反彈 POST 請求/ 

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