
User Enumeration Attacks adalah teknik hacking dengan cara memilah akun mana yang benar-benar terdaftar dan mana yang tidak terdaftar. Dengan ditemukannya akun-akun yang terdaftar, maka akan memberikan kemudahan pada hacker untuk melakukan serangan lanjutan👨💻. Dia bisa dengan secara spesifik menentukan akun mana yang perlu dibobol lebih jauh. Misalnya akun orang kaya, banyak simpanan, orang terkenal, memiliki value tertentu, atau sejenisnya. Itu tentu lebih bernilai untuk dibobol daripada akun orang random yang ga sesuai target mereka. Mereka bisa skip akun dummy dan akun-akun yang menurut mereka ga worth it dibobol.
Specific Brute Force
Misalnya, John Wick terdaftar di sebuah aplikasi keuangan. Jika si hacker menemukan email atau nomor hp John Wick yang terdaftar di aplikasi itu, maka dia akan mencoba melakukan brute force login dengan email atau nomor hp itu. Biasanya hacker punya list password yang umum digunakan, mereka bisa brute force dengan list itu. Beberapa orang menggunakan tanggal lahir atau nomor hp sebagai password. Si hacker juga bisa dengan spesifik mencoba itu. Tanpa mengetahui email atau nomor hp, si hacker akan kesulitan hacking akun John Wick🤔.
Social Engineering
Bisa juga dengan memanfaatkan email atau nomor hp John Wick sebagai target phishing. Si hacker akan mengirimkan John Wick email phishing berpura-pura mengamankan akun dengan cara mengirimkan link berbahaya. Atau menelepon nomor hp John Wick dengan berpura-pura terjadi sesuatu lalu minta tebusan atau sejenisnya. Hal kayak gini udah banyak korbannya dan masih sering terjadi di dunia. Apalagi pada orang-orang gaptek dan orang-orang tua🫣.
User Impersonate
Hacker juga bisa impersonate email John Wick kepada orang lain yang mengenal John Wick. Misalnya John Wick kenal Mad Dog. Si hacker bisa saja membuat email baru dengan dimirip-miripin dengan email John Wick yang ia dapatkan dari aplikasi tadi untuk menipu Mad Dog. Misalkan email John Wick adalah johnny.wick01@mail.com
. Si hacker akan membuat email dengan format mirip seperti johnny_wick01@mail.com
. Sekilas ini mirip. Kalau Mad Dog tidak teliti dia akan dengan gampang percaya bahwa si pengirim adalah beneran John Wick. Si hacker bisa menipu Mad Dog atas nama John Wick untuk transfer uang atau hal lainnya.
Spam Target
Email dan nomor hp yang bocor bisa jadi target spam seperti untuk mengiklankan sesuatu. Misalkan John Wick suka sepatu, hacker bisa menjual informasi email dan nomor hp John Wick ke para penjual sepatu👟. Si penjual sepatu bisa spam email dan nomor hp John Wick untuk mengiklankan sepatunya meskipun dia sendiri ga pernah subscribe. Ini tentu akan sangat mengganggu.
Privacy Violation
Misalkan John Wick terdaftar di aplikasi forum anonim dengan username fafufafu
. Orang-orang ga ada yang tahu itu siapa. Kalau si hacker berhasil mengetahui bahwa email dari user tersebut adalah johnny.wick01@mail.com
, tentu dia bisa membocorkan identitas fafufafu
itu adalah akun John Wick. Identitas dia di forum anonim itu jadi terkuak🤭.
Attacks on Login
Hacker melakukan serangan ini untuk mendapatkan email dan nomor hp user yang valid dengan memanfaatkan fitur dari aplikasi. Misalnya saat login hacker akan brute force berbagai kemungkinan kombinasi email terkait John Wick kayak john.wick@mail.com
, john.wick01@mail.com
, johnny.wick01@mail.com
, dan lain sebagainya. Jika response dari halaman login tersebut email doesn't exist
, maka si hacker berkesimpulan bahwa ini bukan email john wick di aplikasi tersebut. Jika response dari halaman tersebut invalid password
, maka si hacker berkesimpulan bahwa ini emailnya valid dan mereka dapat menentukan langkah selanjutnya.
Attacks on Registration
Saat pendaftaran, hacker juga bisa menggunakan kombinasi email dengan nama John Wick untuk mencari tahu email mana yang terdaftar di aplikasi tersebut. Jika responsenya berhasil, artinya email tersebut ga terdaftar. Sebaliknya, jika gagal berarti emailnya terdaftar.
Attacks on Forgotten Password
Fitur ini termasuk yang sering dimanfaatkan hacker karena cukup simple. Saat membuka halaman ini biasanya user akan diminta memasukkan username, email, atau nomor hp. Jika ga valid maka server akan mengirimkan response account doesn't exist
atau status code 4xx. Jika valid maka server akan mengirimkan status code 200 dan OTP ke email atau nomor hp untuk validasi. Ada juga aplikasi yang menampilkan email atau nomor hp pemilik username tersebut setelah submit username seperti OTP has been sent to johnny.wick01@mail.com
atau OTP has been sent to +62 811 1234 5678
. Ini lebih bahaya lagi. Secara terang-terangan hacker dapat informasi🫣. Identitas si pemilik username bisa ketahuan. Hal-hal kayak gini perlu dicegah dari sisi aplikasi untuk tidak sembarangan menampilkan informasi sensitif.
Preventable Action
Untuk mencegahnya ada beberapa langkah yang bisa dilakukan:
Generic Response
Kita harus memberikan response umum terkait kegagalan. Pada saat login ketika password salah ataupun username, email, atau nomor hp salah, sebaiknya tetap berikan response yang sama seperti invalid password
. Saat lupa password, sebaiknya email dan nomor hp yang ditampilkan disensor karakternya. Misalkan username John Wick yang asli adalah john.wick
, maka yang muncul saat submit di halaman lupa password jadi kayak gini: OTP has been sent to j*****1@m*****.com
atau OTP has been sent to ********5678
. Begitu juga saat username tersebut tidak ditemukan sebaiknya tetap tampilkan pesan yang sama, tapi dengan email dummy dan ga beneran ngirim OTP ke email dummy tersebut. Misalkan username john_wick
tidak terdaftar, maka di halaman lupa password response yang tampil kayak gini OTP has been sent to w*****i@g*****.com
. Ketika hacker mencoba lagi hal yang sama dengan username berbeda, maka dia akan mendapatkan email dummy yang berbeda. Username palsu lainnya seperti john-wick
akan memberikan email dummy berbeda seperti j*****o@y*****.com
di menu lupa password. Pastikan juga status code-nya tetap sama. Jika emailnya terdaftar maupun tidak terdaftar akan tetap memberikan status code 200. Status 4xx hanya diberikan untuk hal teknis misalnya kolom username tidak diisi atau sejenisnya.
Consistent Message
Pastikan tiap email dummy konsisten dengan tiap username yang di-input agar tidak mencurigakan. Jadi ketika hacker mencoba lagi username palsu yang sama seperti sebelumnya dia akan mendapatkan email dummy yang juga sama. Username palsu john_wick
tetap akan mendapatkan response email dummy w*****i@g*****.com
seperti sebelumnya. Termasuk jumlah karakter yang disensor. Misalnya jumlah karakter yang disensor *
adalah 5. Ini berlaku untuk semua username baik yang terdaftar maupun tidak. Meskipun email John Wick yang terdaftar adalah johnny.wick01@mail.com
maka jumlah karakter yang disensor jangan sesuai jumlah karakter email asli tersebut, tapi tetap dengan 5 karakter seperti dummy jadi j*****1@m*****.com
. Ini agar hacker ga bisa menebak jumlah karakter yang disensor. Dengan begini hacker akan susah bedain mana email dummy dan mana email beneran😎.
Similar Performance
Meskipun sudah menggunakan status code dan pesan yang konsisten, hacker tetap bisa curiga ketika performa saat megirimkan email dummy dengan email beneran berbeda. Misalkan kalau email dummy performanya 30ms, sedangkan kalau emailnya beneran performanya 300ms. Ini jomplang banget walaupun ga terlalu terasa karena dapat dideteksi lewat tools. Biasanya karena kalau emailnya terdaftar akan ada beberapa step tambahan seperti menjalankan beberapa validasi lainnya ke database, mengirimkan OTP beneran pada halaman lupa password, dan hal sejenisnya. Sedangkan kalau emailnya ga terdaftar, prosesnya sesederhana generate message dan kirim ke response sehingga prosesnya jauh lebih cepat. Ini juga perlu diperhatikan dan perlu dibandingkan performanya. Jika jomplang maka bisa dengan menambahkan mode sleep selama sekian ms secara random mengikuti rerata performa saat menggunakan email asli agar performanya ga mencurigakan💤. Terkadang performa “lambat” itu ada gunanya😅.
Captcha
Ini juga perlu dipertimbangkan. Dengan menambahkan Captcha, kita menambah layer untuk mengonfirmasi bahwa yang melakukan aktivitas ini bukan bot. Tapi, dari sisi UX ini menjengkelkan user. Jalan tengahnya, ini digunakan pada fitur yang ga rutin dipake user seperti saat pendaftaran atau lupa password. Kita bisa bikin algoritma captcha sendiri atau menggunakan ReCaptcha dari Google.
Monitoring Alert
Ini berfungsi untuk memantau user mana aja yang berkali-kali melakukan lupa password dalam waktu dekat. Ini merupakan aktivitas yang ga wajar. Ga mungkin orang yang sama akan terus-terusan request OTP lupa password. Kita bisa melakukan banned sementara terhadap IP yang mencurigakan selama beberapa jam atau bahkan hari. Atau bisa juga melakukan pembatasan kesalahan saat login. Misalkan maksimal 3x salah masukin password. Jika lebih dari itu maka perlu kunci percobaan login sementara waktu dan mewajibkan konfirmasi lewat email dan nomor hp🔒. Kita bisa memberikan notifikasi pada user aslinya lewat email bahwa ada pecobaan login yang gagal terhadap akun tersebut.
Sample Forgotten Password Code
Untuk login flownya cukup jelas, tinggal handle kalau username, email, atau nomor hp ga ada di database disamain aja response errornya kayak salah password. Kalau untuk pendaftaran menurut gw secara UX memang dibutuhkan informasi apakah emailnya sudah ada yang make atau belum. Alternatifnya lebih baik dihandle dengan cara menambahkan Captcha dan gunakan rate limiter agar fitur pendaftaran ga di-abuse. Sedangkan untuk lupa password kurang lebih kayak gini:
@Value
public class User{
String username;
String password;
String email;
String phone;
}
public interface UserGateway{
User findByUsername(String username);
}
public interface InMemoryGateway{
void store(String username, int otp, Duration duration);
}
public interface NotificationClient{
void sendEmail(String email, String message);
void sendSms(String phone, String message);
}
public interface ForgottenPasswordPresenter{
void present(ForgottenPasswordResponse response);
}
@Value
public class ForgottenPasswordResponse{
String message;
}
@RequiredArgsConstructor
public class ForgottenPasswordUseCase {
private static final String ALPHABET = "qwertyuiopasdfghjklzxcvbnm";
private static final List<String> EMAIL_DOMAIN = List.of("gmail","yahoo","mail");
private static final SecureRandom OTP_GENERATOR = new SecureRandom();
private final UserGateway userGateway;
private final InMemoryGateway inMemoryGateway;
private final NotificationClient notificationClient;
public void execute(String usernameRaw, ForgottenPasswordPresenter presenter) {
if(usernameRaw == null || usernameRaw.isBlank()){
throw new IllegalArgumentException("username cannot be null or blank");
}
String username = usernameRaw.trim().toLowerCase();
User user = userGateway.findByUsername(username);
String maskedContact = sendOtpToMaskedContact(username, user);
presenter.present(new ForgottenPasswordResponse("OTP has been sent to " + maskedContact));
}
private String sendOtpToMaskedContact(String username, User user){
if(user != null){
int otp = OTP_GENERATOR.nextInt(100_000, 1000_000);
inMemoryGateway.store(username, otp, Duration.ofMinutes(5));
if(user.getEmail() != null && !user.getEmail().isBlank()){
String trimmed = user.getEmail().trim();
notificationClient.sendEmail(trimmed, "this is your OTP: " + otp);
return maskEmail(trimmed);
} else if(user.getPhone() != null && !user.getPhone().isBlank()){
String trimmed = user.getPhone().trim();
notificationClient.sendSms(trimmed, "this is your OTP: " + otp);
return maskPhone(trimmed);
}
}
return maskFakeEmail(username);
}
private String maskFakeEmail(String username) {
byte[] hash = hash(username);
BigInteger hashedInt = new BigInteger(1, hash);
BigInteger alphabetLength = BigInteger.valueOf(ALPHABET.length());
int firstIndex = hashedInt.mod(alphabetLength).intValue();
hashedInt = hashedInt.divide(alphabetLength);
int lastIndex = hashedInt.mod(alphabetLength).intValue();
hashedInt = hashedInt.divide(alphabetLength);
int domainIndex = hashedInt.mod(BigInteger.valueOf(EMAIL_DOMAIN.size())).intValue();
StringBuilder result = new StringBuilder();
result.append(ALPHABET.charAt(firstIndex));
for (int i = 0; i < 5; i++){
result.append('*');
}
result.append(ALPHABET.charAt(lastIndex))
.append('@')
.append(EMAIL_DOMAIN.get(domainIndex).charAt(0));
for (int i = 0; i < 5; i++){
result.append('*');
}
result.append(".com");
return result.toString();
}
@SneakyThrows
private byte[] hash(String input) {
TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(150, 250));
MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(input.getBytes(StandardCharsets.UTF_8));
}
private String maskEmail(String email){
int atIndex = email.indexOf('@');
if(atIndex < 0){
return maskFakeEmail(email);
}
String localName = email.substring(0, atIndex);
String domainName = email.substring(atIndex + 1);
StringBuilder result = new StringBuilder();
if(!localName.isEmpty()){
result.append(localName.charAt(0));
}
for (int i = 0; i < 5; i++){
result.append('*');
}
if(localName.length() > 1){
result.append(localName.charAt(localName.length() - 1));
}
result.append('@');
if(!domainName.isEmpty()){
result.append(domainName.charAt(0));
}
for (int i = 0; i < 5; i++){
result.append('*');
}
int dotPos = domainName.lastIndexOf(".");
if(dotPos >= 0){
result.append(domainName.substring(dotPos));
}
return result.toString();
}
private String maskPhone(String phone){
StringBuilder result = new StringBuilder();
for (int i = 0; i < 8; i++){
result.append('*');
}
if(phone.length() <= 4){
return result.append(phone.charAt(phone.length() - 1)).toString();
}
result.append(phone.substring(phone.length() - 4));
return result.toString();
}
}
Flow dari code di atas kurang lebih seperti ini:
- Kita akan mengecek username terlebih dahulu di database;
- Jika ada maka kita akan generate OTP 6 digit. OTP tersebut kita store ke memory dan akan expire dalam 5 menit;
- Jika user punya email, maka OTP dikirim ke email. Selain itu OTP akan dikirim ke nomor hp;
- Jika menggunakan email, maka selain huruf pertama dan huruf terakhir nama email harus disensor;
- Jumlah karakter yang disensor adalah 5 walaupun jumlah karakter aslinya bukan 5;
- Domain email juga disensor dan hanya menampilkan karakter pertama dari domain saja;
- Jika menggunakan nomor hp maka hanya 4 digit belakang aja yang ga disensor dan 8 digit awal akan disensor;
- Jika username salah atau user ga punya dan nomor hp maka kita akan generate hash dari lowercase username menggunakan SHA-256 agar outputnya konsisten tiap username;
- Kita manfaatkan modulasi dari hash yang dikonversi ke Big Integer untuk mendapatkan karakter dummy yang konsisten tiap username sebagai karakter pertama email, karakter terakhir email, dan karakter pertama domain;
- Saat hash kita tambahkan juga time sleep secara random kisaran 150ms hingga 250ms agar tidak ada perbedaan performa yang jomplang antara generate email dummy dengan yang bukan. Ini dengan asumsi performa aslinya kisaran 200ms hingga 300ms dan generate email dummy di kisaran 30ms hingga 50ms. Untuk hal ini perlu benchmark masing-masing aplikasi untuk menentukan nilainya;
Verdict
User Enumeration Attacks dapat membocorkan data sensitif user. Email pribadi dan nomor hp pribadi adalah hal sensitif user yang wajib kita jaga. Dengan serangan ini hacker bisa melakukan penipuan yang dapat merugikan user. Penipuan lewat social engineering, impersonate, hingga phishing masih banyak kejadian dan banyak pula yang jadi korbannya. Dengan melakukan pencegahan terhadap serangan ini kita sebagai pemilik aplikasi bisa melindungi data user kita. Di lain sisi tentu juga ada kekurangan dengan pendekatan ini. Misalnya saat user aslinya beneran lupa email dan password. Dengan hint email asli yang disensor bisa saja dia tetap ga ingat emailnya apa. Untuk hal begini biasanya akan ditangani oleh customer service. Ini tugas mereka untuk verifikasi apakah ini pemilik akun atau bukan.