一、概述
服务器端参数污染(Server-Side Parameter Pollution,SSPP) 是一种因服务器对输入参数的解析逻辑存在缺陷,导致攻击者可通过构造特殊参数篡改服务器处理逻辑的安全漏洞。其核心是利用服务器对 “参数重复、参数名冲突、参数嵌套” 等场景的解析规则漏洞,使服务器错误解析参数,最终执行非预期操作(如越权、数据泄露、功能滥用等)。
1.1 核心原理
API 的参数传递通常依赖 HTTP 协议的查询字符串(Query String)、表单数据(Form Data)、JSON/XML 等格式。服务器在接收参数后,会通过后端框架(如 PHP、Java Spring、Python Flask 等)的解析机制将参数转换为可处理的变量(如键值对、数组等)。
服务器端参数污染的本质是:攻击者通过构造特殊的参数格式(如重复参数名、嵌套参数、特殊分隔符等),利用服务器解析逻辑的漏洞,使参数被错误解析为 “多值”“覆盖原有参数” 或 “注入非预期参数”,从而干扰服务器的正常处理流程。
1.2 触发场景
服务器端参数污染的触发与后端框架的参数解析规则强相关。不同框架对 “重复参数名”“参数嵌套” 的处理逻辑不同,这为攻击提供了可能性。以下是典型场景:
1.2.1 重复参数名的解析差异
当请求中包含多个同名参数时,不同框架的解析结果不同,可能被攻击者利用:
- PHP:会将同名参数解析为数组(如
?id=1&id=2
会被解析为$_GET['id'] = [1,2]
)。 - Java Spring:默认取最后一个参数值(如
?id=1&id=2
会被解析为id=2
)。 - Python Flask:默认取第一个参数值(如
?id=1&id=2
会被解析为id=1
)。
攻击案例:
假设一个删除用户的 API 端点为 /delete?user_id=123
,后端逻辑预期 user_id
是单个值,用于删除指定用户。若后端是 PHP(解析为数组),攻击者可构造 /delete?user_id=123&user_id=456
,导致服务器将 user_id
解析为数组 [123,456]
,若代码未限制 “只能删除单个用户”,则可能同时删除两个用户(越权操作)。
1.2.2 参数覆盖(Parameter Override)
服务器端可能存在 “内部参数”(如权限标识、状态值等),若后端逻辑将用户传入的参数与内部参数 “合并” 时未过滤,攻击者可通过传入同名参数覆盖内部参数。
攻击案例:
某 API 的 /update
端点用于更新用户信息,后端逻辑会自动添加 is_admin=false
(内部参数,限制普通用户权限),并将用户传入的参数(如 name=test
)与内部参数合并。若攻击者传入 /update?name=test&is_admin=true
,且后端未过滤 is_admin
参数,则会覆盖内部的 is_admin=false
,导致普通用户升级为管理员(权限提升)。
1.2.3 嵌套参数解析漏洞
部分框架支持 “嵌套参数”(如 user[name]=alice&user[age]=20
解析为 user = {name: "alice", age: 20}
)。若解析逻辑未限制嵌套层级或键名,攻击者可构造恶意嵌套参数注入非预期字段。
攻击案例:
某 API 的 /create
端点用于创建用户,预期参数为 user[name]
和 user[email]
。后端使用支持嵌套解析的框架(如 Ruby on Rails),攻击者构造 /create?user[name]=alice&user[role]=admin
,若代码未限制 user[role]
只能是 user
,则会创建一个 role=admin
的高权限用户(越权创建)。
1.2.4 多格式参数混合解析
API 可能同时支持多种参数格式(如 application/x-www-form-urlencoded
和 multipart/form-data
),若服务器对不同格式的解析逻辑不一致,可能导致参数被重复解析或错误合并。
攻击案例:
某 API 同时接收表单参数和 JSON 参数,后端在合并两种格式的参数时未去重。攻击者在表单中传入 id=1
,在 JSON 中传入 id=2
,若合并后 id=2
覆盖了 id=1
,可能导致操作对象被篡改。
1.3 危害
服务器端参数污染的危害取决于被干扰的业务逻辑,主要包括:
- 越权操作:如删除 / 修改他人数据、创建高权限账号;
- 功能滥用:如绕过验证(覆盖
is_verified=true
)、执行未授权功能; - 数据泄露:如通过注入
view=all
参数获取所有用户数据; - 辅助其他漏洞:如结合 SQL 注入(通过多值参数构造注入语句)、XSS 等。
1.4 防御措施
防御的核心是严格控制参数的解析逻辑,确保只接收预期的参数,且参数格式符合预期:
- 参数白名单:只允许预设的参数名(如
user_id
、name
),忽略其他参数(如is_admin
)。 - 限制参数类型:明确参数为单个值(如
int
、string
),拒绝数组或嵌套结构(除非业务必需)。 - 统一解析逻辑:了解框架的参数解析规则(如重复参数取第一个 / 最后一个),并在代码中显式处理(如只取第一个值)。
- 输入验证:对参数值进行合法性校验(如
user_id
必须为正整数,且属于当前用户的权限范围)。
二、 测试不同的参数污染
靶场地址:https://portswigger.net/web-security/api-testing/server-side-parameter-pollution
2.1 查询字符串中的服务器端参数污染
要测试查询字符串中的服务器端参数污染,请在输入中 放置查询语法字符(如 #
、、 &
和) ,并观察应用程序如何响应。
假设有一个存在漏洞的应用程序,它允许你根据用户名搜索其他用户。当你搜索某个用户时,你的浏览器会发出以下请求:
GET /userSearch?name=peter&back=/home
为了检索用户信息,服务器使用以下请求查询内部 API:
GET /users/search?name=peter&publicProfile=true
截断查询字符串
可以使用 URL 编码 #
字符尝试截断服务器端请求。为了帮助您解释响应,您还可以在该 #
字符后添加一个字符串。
例如,您可以将查询字符串修改为以下内容:
GET /userSearch?name=peter%23foo&back=/home
前端会尝试访问以下URL:
GET /users/search?name=peter#foo&publicProfile=true
检查响应,寻找查询是否被截断的线索。例如,如果响应返回 user peter
,则服务器端查询可能已被截断。如果 Invalid name
返回错误消息,则应用程序可能已将其视为 foo
用户名的一部分。这表明服务器端请求可能未被截断。
如果您能够截断服务器端请求,则无需将该字段 publicProfile
设置为 true。您或许可以利用这一点返回非公开的用户资料。
注入无效参数
您可以使用 URL 编码 &
字符尝试向服务器端请求添加第二个参数。
例如,您可以将查询字符串修改为以下内容:
GET /userSearch?name=peter%26foo=xyz&back=/home
这会导致服务器端向内部 API 发出以下请求:
GET /users/search?name=peter&foo=xyz&publicProfile=true
查看响应,了解附加参数的解析方式。例如,如果响应未发生改变,则可能表明该参数已成功注入,但被应用程序忽略。
注入有效参数
如果能够修改查询字符串,则可以尝试向服务器端请求添加第二个有效参数。
例如,如果您已经确定了 email
参数,则可以将其添加到查询字符串中,如下所示:
GET /userSearch?name=peter%26email=foo&back=/home
这会导致服务器端向内部 API 发出以下请求:
GET /users/search?name=peter&email=foo&publicProfile=true
查看响应以获取有关如何解析附加参数的线索。
覆盖现有参数
要确认应用程序是否容易受到服务器端参数污染的影响,您可以尝试覆盖原始参数。具体方法是注入第二个同名参数。
例如,您可以将查询字符串修改为以下内容:
GET /userSearch?name=peter%26name=carlos&back=/home
这会导致服务器端向内部 API 发出以下请求:
GET /users/search?name=peter&name=carlos&publicProfile=true
内部 API 会解析两个 name
参数。其影响取决于应用程序如何处理第二个参数。不同的 Web 技术会有所不同。例如:
- PHP 仅解析最后一个参数。这将导致用户搜索
carlos
。 - ASP.NET 会将这两个参数组合在一起。这将导致用户搜索
peter,carlos
,从而可能引发Invalid username
错误消息。 - Node.js / express 仅解析第一个参数。这会导致用户搜索
peter
,但结果不变。
如果您能够覆盖原始参数,则可能能够进行漏洞利用。例如,您可以添加 name=administrator
到请求中。这可能使您能够以管理员用户身份登录。
靶场任务:以 身份登录administrator并删除carlos
1、在忘记密码处,输入管理员的用户名,尝试重置。
2、在burp中 Proxy > HTTP history,查找相关链接。注意到POST /forgot-password 和 /static/js/forgotPassword.jsJavaScript 文件。
3、将这两个数据包发送到repeater模块中,修改username参数观察响应包中的数据。
对&符号进行URL编码后,响应包发生变化。
4、尝试截断呢,尝试使用 URL 编码#字符截断服务器端查询字符串。
返回一条Field not specified错误消息。这表明服务器端查询可能包含一个名为 的附加参数 field,该参数已被角色删除#
5、向请求中添加一个 field
值无效的参数。
6、尝试猜测field的参数值,可能是email。
7、在JavaScript文件中发现,密码重置的路径是/forgot-password?reset_token=${resetToken}
8、进行拼接。
9、token值出来之后,拼接到URL中,使用浏览器直接访问。
/forgot-password?reset_token=u3vhiuso9rekyxhs49g6uqpnkd2jv937
直接进入输入密码界面。
10、使用重置后的密码登录。
2.2 REST 路径中的服务器端参数污染
RESTful API 可以将参数名称和值放在 URL 路径中,而不是查询字符串中。例如,考虑以下路径:
/api/users/123
URL 路径可能分解如下:
/api
是根 API 端点。/users
代表一种资源,在本例中为users
。/123
表示一个参数,这里是特定用户的标识符。
假设有一个应用程序,允许根据用户名编辑用户个人资料。请求将发送到以下端点:
GET /edit_profile.php?name=peter
这会导致以下服务器端请求:
GET /api/private/users/peter
攻击者可能能够操纵服务器端 URL 路径参数来利用此 API。要测试此漏洞,请添加路径遍历序列来修改参数,并观察应用程序的响应情况。
可以提交 URL 编码 peter/../admin
作为参数的值 name
:
GET /edit_profile.php?name=peter%2f..%2fadmin
这可能会导致以下服务器端请求:
GET /api/private/users/peter/../admin
如果服务器端客户端或后端 API 规范化此路径,则可能会将其解析为 /api/private/users/admin
。
靶场任务:以 身份登录administrator并删除carlos。
1、整个网站就只有登录框可能存在与后端交互,故以administrator用户名充值密码。
3、在burp中 Proxy > HTTP history,查找相关链接。注意到POST /forgot-password 和 /static/js/forgotPassword.jsJavaScript 文件发送到repeater模块。
4、尝试对username=administrator参数进行污染。
使用?的URL编码格式%3f
请求可能访问了与原始请求相同的 URL 路径,输入可能位于 URL 路径中。
使用截断符号%23继续尝试。
猜测可能已经到达根部。
5、继续猜测可能存在的API文件,例如openapi.json。
注意观察响应包,主要到有一个查找用户的API端点:
/api/internal/v1/users/{username}/field/{field}
6、尝试做修改。
7、在JavaScript文件中,发现了密码重置的路径。
/forgot-password?passwordResetToken=${resetToken}
8、尝试拼接。
9、使用之前发现的端点:
/api/internal/v1/users/{username}/field/{field}
10、将toekn值,拼接到URL中,直接访问。
/forgot-password?passwordResetToken=v131ihzgbwzk739w3exl9pvdl2wk5ljp
11、使用重置后的密码登录,然后删除用户。
2.3 结构化数据格式中的服务器端参数污染
攻击者或许能够操纵参数,从而利用服务器处理其他结构化数据格式(例如 JSON 或 XML)时存在的漏洞。为了测试这一点,请将非预期的结构化数据注入用户输入,并观察服务器的响应情况。
假设有一个应用程序允许用户编辑个人资料,然后通过向服务器端 API 发出请求来应用更改。当您编辑姓名时,浏览器会发出以下请求:
POST /myaccount name=peter
这会导致以下服务器端请求:
PATCH /users/7312/update {"name":"peter"}
您可以尝试按如下方式将参数添加 access_level
到请求中:
POST /myaccount name=peter","access_level":"administrator
如果用户输入在没有经过充分验证或清理的情况下被添加到服务器端 JSON 数据中,则会导致以下服务器端请求:
PATCH /users/7312/update {name="peter","access_level":"administrator"}
这可能会导致用户 peter
被授予管理员访问权限。
考虑一个类似的例子,但客户端用户输入的是 JSON 数据。当你编辑姓名时,浏览器会发出以下请求:
POST /myaccount {"name": "peter"}
这会导致以下服务器端请求:
PATCH /users/7312/update {"name":"peter"}
您可以尝试按如下方式将参数添加 access_level
到请求中:
POST /myaccount {"name": "peter\",\"access_level\":\"administrator"}
如果对用户输入进行解码,然后在没有进行适当编码的情况下将其添加到服务器端 JSON 数据中,则会导致以下服务器端请求:
PATCH /users/7312/update {"name":"peter","access_level":"administrator"}
同样,这可能会导致用户 peter
被授予管理员访问权限。
结构化格式注入也可能发生在响应中。例如,如果用户输入安全地存储在数据库中,然后未经充分编码就嵌入到来自后端 API 的 JSON 响应中,就可能发生这种情况。