Serangan Kredensial

Serangan kredensial: credential stuffing, password spraying, dan rate limiting yang efektif.

Daftar isi

Satu akun yang diretas jarang berdiri sendiri, karena pengguna mengulang kata sandi yang sama antarsitus dan penyerang menguji pasangan yang bocor secara massal. Serangan kredensial mengeksploitasi kebiasaan itu tanpa harus menebak satu pun kata sandi dari nol. Topik ini membedakan credential stuffing dari password spraying, mensimulasikan keduanya terhadap akun lab lokal, lalu membangun rate limiting yang menahan laju percobaan tanpa menghukum pengguna yang sah.

Dua pola penyalahgunaan kredensial#

Kedua pola ini berbagi satu sumber kekuatan, yaitu basis data kata sandi yang bocor dari situs lain, tetapi cara keduanya menguji target berbeda. Tabel berikut meletakkannya berdampingan.

AspekCredential stuffingPassword spraying
Masukanpasangan username dan kata sandi dari kebocorandaftar kata sandi yang paling sering digunakan
Pola ujisetiap pasangan dicoba pada akun berusername samasatu kata sandi dicoba bergiliran ke banyak akun
Volume per akunbeberapa percobaan dalam waktu singkatsengaja rendah, satu atau dua percobaan per akun
Tanda bahayalonjakan kegagalan login pada satu akunsedikit kegagalan yang menyebar ke banyak akun

Perbedaan pola itu menentukan deteksinya. Credential stuffing terlihat sebagai lonjakan percobaan yang terkonsentrasi, sehingga penghitungan per akun dan per IP sudah menyaring sebagian besarnya. Password spraying justru dirancang agar tetap di bawah ambang setiap akun, sehingga penghitungan per akun tidak menyentuhnya dan diperlukan penghitungan yang lebih tinggi, yaitu per alamat IP dan per seluruh situs. Kedua pola sama-sama hidup dari kebiasaan mengulang kata sandi yang sama, sehingga kebijakan pendaftaran yang menolak kata sandi yang pernah bocor menutup sumbernya sejak awal.

Simulasi pada akun lab#

Skrip berikut menjalankan kedua pola terhadap empat akun lab yang didefinisikan di dalam berkasnya sendiri, tanpa satu pun permintaan yang keluar dari mesin lokal. Bagian pertama menguji lima pasangan kredensial bocoran, bagian kedua menguji kata sandi umum password123 bergiliran ke seluruh akun dengan pembatas laju per IP, dan bagian ketiga membangun pembatas laju jendela geser yang dibahas pada bagian berikutnya.

python serangan-kredensial.py
"""Simulasi credential stuffing, password spraying, dan rate limiting.

Seluruh percobaan hanya diarahkan ke akun lab yang didefinisikan di
berkas ini. Rate limiting memakai jendela geser (sliding window) per
akun dan per IP dengan penolakan sementara, bukan pemblokiran permanen.
Cap waktu berupa angka detik tetap agar keluaran deterministik.
"""

AKUN = {
    "ali": "kunci-rahasia-7",
    "budi": "matahari-pagi",
    "citra": "kopi-susu-24",
    "dewi": "sepeda-gunung",
}

# pasangan kredensial dari kebocoran situs lain
BOCORAN = [
    ("ali", "password123"),
    ("budi", "matahari-pagi"),
    ("budi", "dunia-malam"),
    ("citra", "kopi-susu-24"),
    ("dewi", "123456"),
]


class PembatasLaju:
    """Jendela geser: maksimum `maks` percobaan dalam `jendela` detik per kunci."""

    def __init__(self, maks, jendela):
        self.maks = maks
        self.jendela = jendela
        self.upaya = {}  # kunci -> daftar cap waktu percobaan yang diteruskan

    def boleh(self, kunci, waktu):
        terkini = [t for t in self.upaya.get(kunci, []) if t > waktu - self.jendela]
        self.upaya[kunci] = terkini
        if len(terkini) >= self.maks:
            return False
        terkini.append(waktu)
        return True


print("== Credential stuffing: kredensial bocoran diuji ke satu situs ==")
for pengguna, sandi in BOCORAN:
    hasil = "BERHASIL MASUK" if AKUN.get(pengguna) == sandi else "gagal"
    print(f"  {pengguna} / {sandi}: {hasil}")

print()
print("== Password spraying: satu kata sandi umum diuji ke banyak akun ==")
pembatas_ip = PembatasLaju(maks=3, jendela=60.0)
for urutan, pengguna in enumerate(AKUN, start=1):
    if pembatas_ip.boleh("10.0.0.9", 2000.0 + urutan):
        hasil = "BERHASIL MASUK" if AKUN[pengguna] == "password123" else "gagal"
        print(f"  percobaan ke-{urutan}, akun {pengguna}: {hasil}")
    else:
        print(f"  percobaan ke-{urutan}, akun {pengguna}: DITOLAK pembatas laju per IP")

print()
print("== Rate limiting per akun+IP: maks 5 permintaan dalam 60 detik ==")
pembatas = PembatasLaju(maks=5, jendela=60.0)
kunci = ("ali", "10.0.0.9")
for urutan in range(1, 8):
    waktu = 1000.0 + urutan  # satu percobaan tiap detik
    if pembatas.boleh(kunci, waktu):
        print(f"  t+{urutan} s : percobaan ke-{urutan} diteruskan ke pemeriksaan kata sandi")
    else:
        print(f"  t+{urutan} s : percobaan ke-{urutan} DITOLAK sementara")
if pembatas.boleh(kunci, 1000.0 + 61):
    print("  t+61 s : percobaan ke-8 diteruskan kembali (penolakan bersifat sementara)")
text Keluaran
== Credential stuffing: kredensial bocoran diuji ke satu situs ==
  ali / password123: gagal
  budi / matahari-pagi: BERHASIL MASUK
  budi / dunia-malam: gagal
  citra / kopi-susu-24: BERHASIL MASUK
  dewi / 123456: gagal

== Password spraying: satu kata sandi umum diuji ke banyak akun ==
  percobaan ke-1, akun ali: gagal
  percobaan ke-2, akun budi: gagal
  percobaan ke-3, akun citra: gagal
  percobaan ke-4, akun dewi: DITOLAK pembatas laju per IP

== Rate limiting per akun+IP: maks 5 permintaan dalam 60 detik ==
  t+1 s : percobaan ke-1 diteruskan ke pemeriksaan kata sandi
  t+2 s : percobaan ke-2 diteruskan ke pemeriksaan kata sandi
  t+3 s : percobaan ke-3 diteruskan ke pemeriksaan kata sandi
  t+4 s : percobaan ke-4 diteruskan ke pemeriksaan kata sandi
  t+5 s : percobaan ke-5 diteruskan ke pemeriksaan kata sandi
  t+6 s : percobaan ke-6 DITOLAK sementara
  t+7 s : percobaan ke-7 DITOLAK sementara
  t+61 s : percobaan ke-8 diteruskan kembali (penolakan bersifat sementara)

Bagian stuffing memperlihatkan dua dari lima pasangan bocoran berhasil, yaitu budi dan citra yang menggunakan kata sandi yang sama seperti di situs tempat kebocoran berasal. Tidak ada satu pun penebakan dari nol, dan justru kebiasaan mengulang kata sandi itulah yang membuka pintunya. Bagian spraying memperlihatkan nasib yang berbeda. Semua akun menolak password123, sedangkan percobaan keempat bahkan tidak pernah sampai ke pemeriksaan kata sandi karena pembatas laju per IP sudah menahannya. Pola ini sama sekali tidak terlihat bila penghitungan hanya dilakukan per akun.

Rate limiting yang benar-benar bekerja#

Pembatas laju pada skrip menggunakan jendela geser (sliding window), yaitu menyimpan cap waktu setiap percobaan yang diteruskan lalu menghitung berapa yang masih berada dalam 60 detik terakhir pada saat permintaan baru datang. Kuncinya adalah pasangan akun dan IP, sehingga penyerang yang menghabiskan jatah pada satu kombinasi tidak ikut mengunci kombinasi lain milik pengguna yang sama dari jaringan berbeda. Keluaran memperlihatkan lima percobaan pertama diteruskan, percobaan keenam dan ketujuh ditolak, lalu percobaan pada t+61 s diteruskan kembali karena jendelanya sudah bergeser.

Ciri yang paling penting dari rancangan ini adalah penolakan yang bersifat sementara. Pemblokiran permanen terhadap akun yang diserang justru menjadi senjata bagi penyerang, karena siapa pun yang mengirim kata sandi salah cukup banyak dapat mengunci pemilik akun dari akunnya sendiri. Pertahanan yang sehat menaikkan tingkat penolakannya secara bertahap, misalnya penundaan respons yang makin panjang, tantangan captcha, verifikasi ulang faktor kedua, dan notifikasi kepada pemilik akun, sementara penolakan penuh hanya berlaku dalam jeda waktu yang terukur.

Penghitungan berlapis menutup celah yang tersisa, yaitu lapisan akun+IP untuk stuffing, lapisan per IP untuk spraying dari satu sumber, dan lapisan global untuk spraying dari banyak sumber yang terdistribusi. Faktor kedua menurunkan nilai setiap kata sandi yang berhasil ditebak, sehingga kedua pola kehilangan sebagian besar keuntungannya pada situs yang mewajibkannya.

Mencoba jendela geser di browser#

Skrip menilai percobaan pada cap waktu yang dimajukan sendiri, sedangkan demo berikut menyerahkan waktunya ke tangan pembaca. Kirimkan percobaan login berulang-ulang hingga jendelanya penuh, amati percobaan keenam dan ketujuh yang ditolak sementara, lalu lompatkan waktu 60 detik dan kirimkan satu percobaan lagi untuk melihat jendela yang sudah bergeser menerimanya kembali. Mode tanpa pembatas meneruskan seluruh percobaan tanpa menyimpan keadaan apa pun, dan perbedaannya dengan mode jendela geser tampak pada log yang terkumpul.

Kesalahan umum#

  • Memblokir permanen akun yang menjadi sasaran percobaan. Penyerang menggunakan blokir itu untuk mengunci pengguna sah dari akunnya, sehingga penolakan sementara dan verifikasi tambahan dipilih sebagai gantinya.
  • Membatasi laju hanya per akun. Password spraying menjaga volume per akun tetap rendah, sehingga penghitungan juga diperlukan pada level IP dan level situs.
  • Mengatur ulang penghitung pada jadwal yang diketahui, misalnya setiap tengah malam. Penyerang menyesuaikan lajunya pada jadwal itu, sedangkan jendela geser menilai cap waktu yang benar-benar berjalan.
  • Menolak kata sandi lemah hanya saat pendaftaran. Kata sandi pengguna lama tetap lemah, sehingga pencocokan terhadap daftar kebocoran juga dilakukan saat penggantian kata sandi dan saat login dari perangkat baru.

Ringkasan#

  • Credential stuffing menguji pasangan bocoran, sedangkan password spraying menguji kata sandi umum ke banyak akun, dan keduanya hidup dari kebiasaan mengulang kata sandi yang sama.
  • Rate limiting yang bekerja menghitung percobaan pada jendela geser dengan kunci berlapis, yaitu akun+IP, IP, dan seluruh situs.
  • Penolakan bersifat sementara dan berjenjang, karena pemblokiran permanen mengubah pertahanan menjadi alat penyerang untuk mengunci pengguna sah.
  • Deteksinya mengikuti polanya, yaitu lonjakan kegagalan pada satu akun untuk stuffing dan sedikit kegagalan yang menyebar untuk spraying.

Referensi#