Skip to content
endr
Go back

Race Condition dan TOCTOU: Kenapa Cek Dulu, Baru Update, Itu Bahaya

ID

Banyak bug keamanan lahir dari pola yang kelihatannya wajar: cek dulu kondisinya, baru update. Cek voucher-nya udah kepake belum, kalau belum ya proses redeem. Celahnya muncul kalau dua request nyelip barengan di jeda antara “cek” dan “update”: dua-duanya baca kondisi yang sama, dua-duanya lolos. Itu race condition.

Gua ketemu contoh klasiknya pas ngerjain exam bootcamp cyber security. Ada halaman redeem voucher yang katanya cuma bisa di-redeem 1x per user. Gua tembak pake Turbo Intruder: 100 request barengan, RPS 50, 100 connections, 0 retries, 0 fails, semua balik 200 OK.

Bug-nya di mana

Riwayat redeem dengan voucher WELCOME50K yang sama muncul berkali-kali, semua timestamp-nya identik 2026-07-02 20:05:29 (domain di-blur)

Pas buka riwayat redeem, voucher WELCOME50K yang sama nongol berkali-kali dan semuanya punya timestamp yang sama persis sampai ke detik: “2026-07-02 20:05:29”. Gua sempet diem lama merhatiin itu. Kalau request-nya diproses satu-satu, timestamp-nya enggak bakal identik kayak gitu, jadi itu bukti beberapa request diproses barengan. Itu momen paling ngena dari nemuin bug ini.

Ini yang disebut race condition: server proses beberapa request barengan tanpa pengaman yang cukup (PortSwigger, Race conditions). Bentuknya di sini namanya TOCTOU, time-of-check to time-of-use: sistem ngecek kondisi suatu resource, baru pake resource itu, tapi kondisinya bisa berubah di antara cek dan pake (CWE-367). PortSwigger nyebut pola cek-lalu-tandai kayak gini limit overrun, subtype TOCTOU yang paling sering ketemu: cek dulu kode udah dipake belum, baru update record buat nandain udah dipake.

Ilustrasi umum, bukan kode aplikasi di exam:

-- ilustrasi alur rentan, dua statement terpisah
SELECT used FROM vouchers WHERE code = $1 AND user_id = $2;
-- aplikasi baca used = false, lanjut proses redeem...
UPDATE vouchers SET used = true WHERE code = $1 AND user_id = $2;

Antara SELECT dan UPDATE itu ada jeda, race window-nya di situ. Dua request yang nyelip di jeda itu, dua-duanya baca used = false, dua-duanya lolos cek.

Alat yang gua pake

Turbo Intruder itu ekstensi Burp Suite buat ngirim banyak HTTP request, pake HTTP stack yang ditulis dari nol biar cepet (GitHub, PortSwigger/turbo-intruder). Tantangannya bukan sekadar ngirim banyak request, tapi nyamain timing-nya biar race window-nya kena barengan. Ada mekanisme gate: request ditahan sampe openGate() dipanggil, terus dilepas sekaligus. Target exam ini jalan di HTTP/2, dan di HTTP/2 request paralel biasa masih bisa kepencar dikit sama jitter jaringan. Makanya single-packet attack lebih diandelin di sini: puluhan request digabung jadi satu paket TCP biar semuanya nyampe ke server nyaris bersamaan, nutup race window serapet mungkin.

Sebelum nyampe ke Turbo Intruder, gua nyoba Burp Intruder biasa duluan. Nyoba beberapa kali, tapi enggak pernah kena race window-nya. Intruder biasa ngirim request-nya satu-satu, sequential, bukan konkuren, jadi dua request enggak pernah bener-bener nyelip bareng di jeda antara SELECT dan UPDATE itu, selalu ada yang kelar duluan sebelum yang lain mulai. Makanya gua pindah ke Turbo Intruder, yang emang dirancang buat ngirim barengan pake gate mechanism itu.

Cara masangnya: Extensions → BApp Store → cari “Turbo Intruder” → Install. Abis itu klik kanan di request redeem voucher, pilih “Send to Turbo Intruder”, terus edit skrip Python-nya:

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                            concurrentConnections=30,
                            engine=Engine.BURP2)

    for i in range(25):
        engine.queue(target.req, 'WELCOME50K', learn=1)

def handleResponse(req, interesting):
    table.add(req)

engine=Engine.BURP2 sama concurrentConnections=30 itu yang bikin Turbo Intruder pake single-packet attack. Kode voucher di request-nya ke-mark otomatis jadi placeholder %s pas “Send to Turbo Intruder”, tinggal isi nilainya di argumen kedua engine.queue() (WELCOME50K, sama kayak yang nongol di riwayat redeem). Loop-nya 25x, jadi 25 request nembak dalam satu batch.

Kalau males oprek skrip, Burp Repeater juga bisa: bikin Group dari beberapa tab request redeem voucher (klik kanan tab → Add tab to group), terus klik kanan icon Group-nya → “Send group in parallel”. Burp otomatis milih teknik sesuai versi HTTP target: single-packet attack buat HTTP/2, last-byte sync buat HTTP/1 (PortSwigger, Sending grouped HTTP requests).

Ini hasil salah satu attack run-nya:

Konfigurasi Turbo Intruder: 100 request, RPS 50, 100 connections, 0 retries, 0 fails, semua response 200 OK (domain di-blur)

Konfigurasinya: 100 request, RPS 50, 100 connections, 0 retries, 0 fails, semua balik 200 OK. Enggak ada satu pun yang ditolak server.

Gimana nutupnya

Cara paling langsung, gabung cek dan update jadi satu statement, contoh aja:

UPDATE vouchers
SET used = true
WHERE code = $1 AND user_id = $2 AND used = false
RETURNING id;

Kalau RETURNING enggak ngembaliin baris, request itu kalah, voucher-nya udah keduluan dipake. Di PostgreSQL, UPDATE yang nemu baris lagi di-update transaksi lain bakal nunggu duluan, terus kondisi WHERE-nya dicek ulang ke versi baris yang udah ke-update (PostgreSQL, Read Committed Isolation Level). Kalau versinya udah enggak cocok sama used = false, UPDATE kedua enggak ngubah apa-apa. Satu statement kayak ini nutup celah check-lalu-update yang tadi.

Ada juga dua cara yang lebih defense-in-depth: unique constraint di kombinasi user_id dan kode voucher biar insert kedua ngelanggar constraint-nya, atau transaksi eksplisit pake SELECT ... FOR UPDATE yang ngunci barisnya sampe transaksi kelar. CWE-367 nyaranin kuncinya dipasang sebelum cek, bukan sesudahnya.

Buat request yang mungkin di-retry dari sisi client, ada pola idempotency key: client kirim key unik, server nyimpen hasil request pertama, request berikutnya dengan key sama enggak dieksekusi ulang.

Turbo Intruder ngirim banyak request sekaligus, jadi cuma boleh dipake di sistem yang emang boleh dites. Kasus ini gua dapetin di exam bootcamp cyber security yang emang dipakai buat belajar :D.


Sumber:


Share this post:

Previous Post
Rumus Haversine: Ngitung Jarak Dua Titik dari Lintang dan Bujur
Next Post
IDOR: Ganti Angka di URL, Buka Profil Orang Lain