Masalah Plugin SM - Change LDAP Password

Kamas Muhammad <kamas@lc.vlsm.org>


Tidak ada copyright apapun dalam dukumen ini, anda bebas menyalin, mencetak, maupun memodifikasi. Saran, koreksi, kritik, kesalahan ketik, maupun ucapan silakan dikirimkan ke email tersebut diatas. Terima Kasih.

Asal Usulnya

Salah satu pekerjaan yang saya lakukan di salah satu site beberapa waktu yang lalu adalah melakukan migrasi pengguna email dari yang semula menggunakan unix passwd user ke virtual domain user. User backend yang saya gunakan adalah LDAP menggunakan implementasi OpenLDAP.

Saya menggunakan squirrelmail untuk menyediakan akses email via web, dan memasang plugin change_ldappass (v2.2)[1] agar para pengguna dapat mengubah passwordnya sendiri.

Data password pada sistem sebelumnya saya ekstrak dari /etc/shadow yang kemudian saya pasangkan dengan entri masing-masing pengguna. Sederhananya, contoh bentuk ldif adalah seperti ini:

dn: uid=sokam,o=users,.............
uid: sokam
accountStatus: active
mail: sokam@.............
mailQuotaSize: 0
userPassword: {crypt}$1$B8RKHiNI$XPXIhmMR0W6PpfpWOwKY3.


Yang dicetak tebal adalah yang didapat dari /etc/shadow

Teks password di atas saya beri tambahan '{crypt}' agar OpenLDAP dapat melakukan proses hashing sesuai dengan algoritma yang digunakan, yaitu Crypt-MD5. Kenapa menggunakan algoritma itu? Ya karena format itulah yang saya peroleh dari file /etc/shadow di server asalnya.

Masalah Terjadi Ketika...

Pada awalnya—sebagai bahan percobaan—saya membuat sebuah account di situ. Kemudian account itu ikut saya migrasikan bersama dengan seluruh account yang sebelumnya sudah ada di situ. Pasa saat awal instalasi, saya mencoba mengganti password saya beberapa kali, dan beres-beres saja. Beberapa tim saya juga melakukan hal yang sama, dan untuk sementara hasilnya baik.

Beberapa minggu setelah itu, saya mendengar bahwa ada beberapa pengguna yang bisa login, bisa membaca email, bisa kirim email, tapi tidak bisa melakukan pengubahan password. Awalnya agak ragu juga, jadi saya minta account yang bisa saya pakai untuk coba ini coba itu.

Benar, ternyata memang account yang saya pakai itu bisa login, tapi tidak bisa mengubah password. Hasil debug plugin change_ldappass menunjukkan bahwa plugin itu menyimpulkan bahwa "password salah". Anehnya, pesan itu muncul setelah dn yang bersangkutan berhasil melakukan proses bind ke LDAP. Dari situ saya berprasangka bahwa ada suatu mekanisme pemeriksaan password yang tidak seberapa beres.

Bongkar Source Code

Pada file functions.php, pada fungsi change_ldappass_go saya menemukan kode berikut:

245     //lets try to determine the encrytion method of the stored password
246     $p=split("}",$storedpass); //split the password
247     $ctype=strtolower($p[0]); //into the {crypttype}
248     $lpass=$p[1]; //and the password
249     //if the stored password is {crypt} then its salted, but which sub-type?
250     switch ($ctype) {
251        case "{crypt":
252           $pl=strlen($lpass); // We'll look at the length and salt of what's already stored to determine the crypt sub-type
253           $stype="DES"; //sensible default if not detected
254           if ($pl>=34 and substr($lpass,0,3)=="$1$") $stype="MD5";
255           if ($pl>=34 and substr($lpass,0,3)=="$2$") $stype="BLOWFISH";
256           if ($debug) array_push($Messages, _("Password type is") . " {crypt}, sub-type $stype");
257           $cpass=ldap_crypt_passwd($cp_oldpass,$lpass,$stype); //crypt up our old password so we can check it again
258           break;

Ah, ternyata memang benar. Plugin ini memeriksa panjang teks password hashed, yang bila panjangnya adalah 34 karakter, maka akan diputuskan bahwa tipe yang digunakan adalah MD5. Password yang ada pada saya adalah $1$2j$XYe926aEZflKvDflj49ZF. yang panjangnya hanya 28 karakter.

Format string password Crypt-MD5 adalah $1$[salt]$[cryptedText][2]. Di situ tidak tertera apakah panjang karakter yang digunakan harus 34 atau lebih. Karena itulah, tampaknya password saya masuk ke kategori yang bukan MD5 dan bukan BLOWFISH. Kemudian saya lanjutkan ke fungsi ldap_crypt_passwd.

270    function ldap_crypt_passwd($password,$salt,$stype) {
271        if ($stype=="MD5") return crypt($password,substr($salt,0,12)); //MD5 uses 12 chr salt
272        if ($stype=="BLOWFISH") return crypt($password,substr($salt,0,16)); //BLOWFISH uses 16 chr salt
273        // DES or something else
274        return crypt($password,substr($salt,0,2)); //crypt uses 2 chr salt
275    }

Nah, inilah yang membuat saya tidak bisa mengubah password. Password saya di-crypt menggunakan algoritma DES yang kemudian hasilnya dibandingkan dengan string password saya yang tersimpan di LDAP. Inilah data password saya dan yang seharusnya muncul bila di-crypt menggunakan algoritma Crypt-MD5.

Password saya: imutdanlucu
Salt: 2j
Hasil hashing: XYe926aEZflKvDflj49ZF.
String lengkap: $1$2j$XYe926aEZflKvDflj49ZF.

Bila dimasukkan pada fungsi ldap_crypt_passwd, maka fungsi tersebut akan dieksekusi dengan parameter seperti yang ditunjukkan di bawah ini. Ternyata hasilnya memang tidak seperti yang diharapkan.

ldap_crypt_passwd( 'imutdanlucu', '$1$2j$XYe926aEZflKvDflj49ZF.', 'MD5' ) // hasil: $1$$o2ee/jyow01P4XXgWAEDW1

Siapa pun memang bisa melihat bahwa $1$2j$XYe926aEZflKvDflj49ZF. tidak sama dengan $1$$o2ee/jyow01P4XXgWAEDW1. Inilah yang menyebabkan bahwa password lama yang saya masukkan divonis "salah".

Perbaikan?

Saya sebenarnya ragu apakah ini merupakan perbaikan. Tapi kalau saya mengacu pada referensi yang disediakan oleh wikipedia[3], bolehlah saya merasa bahwa ada sesuatu yang tidak beres dengan script itu. Format varian crypt adalah:

Format Awalan
DES -
MD5 $1$
Blowfish $2$ atau $2a$
SHA $5$ atau $6$

Berbekal referensi itu, saya mengubah 2 hal, yaitu pemeriksaan panjang teks (pemeriksaan 34 karakter saya hilangkan) dan perbaikan penentuan salt yang digunakan. Walau begitu, perbaikan ini hanya saya lakukan untuk tipe MD5, bukan yang lain. Patch file functions.php adalah sebagai berikut:

--- old/functions.php   2009-01-19 09:06:05.000000000 +0700
+++ new/functions.php   2009-01-19 09:06:17.000000000 +0700
@@ -251,7 +251,7 @@
     case "{crypt":
         $pl=strlen($lpass); // We'll look at the length and salt of what's already stored to determine the crypt sub-type
         $stype="DES";       //sensible default if not detected
-        if ($pl>=34 and substr($lpass,0,3)=="$1$") $stype="MD5";
+        if (substr($lpass,0,3)=="$1$") $stype="MD5";
         if ($pl>=34 and substr($lpass,0,3)=="$2$") $stype="BLOWFISH";
         if ($debug) array_push($Messages, _("Password type is") . " {crypt}, sub-type $stype");
         $cpass=ldap_crypt_passwd($cp_oldpass,$lpass,$stype); //crypt up our old password so we can check it again
@@ -492,7 +492,11 @@
  * @return string
  */
 function ldap_crypt_passwd($password,$salt,$stype) {
-    if ($stype=="MD5")      return crypt($password,substr($salt,0,12)); //MD5 uses 12 chr salt
+    if ($stype=="MD5") {
+       $pecah = explode( "$", $salt );
+       $uyah = sprintf( "$1$%s$", $pecah[2] );
+       return crypt( $password, $uyah );
+    }
     if ($stype=="BLOWFISH") return crypt($password,substr($salt,0,16)); //BLOWFISH uses 16 chr salt
     // DES or something else
     return crypt($password,substr($salt,0,2));      //crypt uses 2 chr salt

Yah, semoga perbaikan kecil ini bisa memperlancar kerjaan orang-orang yang mengalami masalah serupa dengan saya. Seperti biasa, kalo ada yang ndak beres kirim email saja ke alamat di atas he..he..he..

Referensi

  1. Plugin Squirrelmail - Change LDAP Password
  2. Usenix - MD5 Crypt
  3. Wikipedia - Crypt Unix

Lain-lain

Ditulis pada hari senin yang cukup panas, tanggal 19 Januari 2009 jam 11an siang. Ada yang lagi nyeruput kopi, ada yang lagi mbenerin konfig Xorg, ada yang lagi ngobrol, yang bobo juga ada :D


Info tambahan

Sebenernya untuk mencapai keperluan auth, patch di atas tidak sepenuhnya diperlukan. Hapus saja length >= 34, mustinya udah jalan. Patch di atas lebih bermain pada misi "iseng" sekedar untuk memperjelas cara kerjanya.

Entah kenapa, tapi fungsi crypt() memperlakukan "$1$[8chars]$" sebagai salt, bukan hanya 8 karakter seperti yang disebutkan pada spesifikasinya. Perilaku ini jauh berbeda dengan ketika oprasi ini dilakukan menggunakan perl, seperti potongan kode di bawah:

use Crypt::PasswdMD5;
...
$salt = $storedPass;
$salt =~ s/^\$1\$//;
$salt =~ s/^(.*)\$.*$/$1/;

$encryptedpass = unix_md5_crypt( $passYangMauDicek, $salt );
...

Di situ bisa kita lihat bahwa yang dimasukkan sebagai salt pada fungsi unix_md5_crypt hanyalah (max) 8 karakter itu sendiri, tanpa identifier dan delimiter.