June 11, 2026
No-FA — picoCTF 2026 Writeup
Dalam challenge No-FA dari picoCTF 2026, peserta diberikan sebuah aplikasi web berbasis Flask beserta sebuah database yang diduga telah…
Devspace
4 min read
Dalam challenge No-FA dari picoCTF 2026, peserta diberikan sebuah aplikasi web berbasis Flask beserta sebuah database yang diduga telah bocor. Dari deskripsi challenge, kita mengetahui bahwa terdapat data sensitif yang telah terekspos dan tujuan utama kita adalah mendapatkan flag yang hanya dapat diakses oleh akun administrator.
Challenge Information
Category:_ Web Exploitation Difficulty: Medium Author: Darkraicg492 Platform: picoCTF 2026_
Description
Seems like some data has been leaked! Can you get the flag?
Dari source code yang diberikan, diketahui bahwa flag hanya akan ditampilkan ketika pengguna yang sedang login memiliki username admin. Oleh karena itu, fokus utama analisis adalah mencari cara untuk mendapatkan akses sebagai administrator dan melewati mekanisme autentikasi yang diterapkan aplikasi.
Initial Analysis
Attachment challenge berisi dua file penting:
- Source code aplikasi Flask (
app.py) - Database SQLite (
users.db)
Karena challenge secara eksplisit menyebutkan adanya kebocoran data, langkah pertama yang masuk akal adalah melakukan analisis terhadap database yang diberikan. Jika terdapat kredensial pengguna yang bocor, maka kemungkinan besar informasi tersebut dapat dimanfaatkan untuk mendapatkan akses ke aplikasi.
Selain itu, source code aplikasi perlu dianalisis untuk memahami bagaimana proses autentikasi dan otorisasi berjalan.
Enumeration
Langkah awal dilakukan dengan mengidentifikasi jenis file database.
file users.dbfile users.dbOutput menunjukkan bahwa file tersebut merupakan database SQLite.
users.db: SQLite 3.x databaseusers.db: SQLite 3.x databaseSelanjutnya database dibuka menggunakan SQLite.
sqlite3 users.dbsqlite3 users.dbDaftar tabel yang tersedia dapat dilihat menggunakan:
.tables.tablesHasilnya hanya terdapat satu tabel:
usersusersKemudian struktur tabel diperiksa:
.schema users
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT NOT NULL,
password TEXT NOT NULL,
two_fa BOOLEAN NOT NULL DEFAULT 0
);.schema users
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT NOT NULL,
password TEXT NOT NULL,
two_fa BOOLEAN NOT NULL DEFAULT 0
);Dari isi tabel ditemukan akun administrator berikut:
5|admin|iamadmin@nfs.com|c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67|15|admin|iamadmin@nfs.com|c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67|1Menariknya, akun admin memiliki fitur 2FA yang aktif (1). Hal ini mengindikasikan bahwa meskipun password berhasil diperoleh, masih akan ada lapisan autentikasi tambahan yang harus dilewati.
Selanjutnya dilakukan analisis terhadap source code aplikasi. Pada route utama ditemukan logika berikut:
@app.route("/")
def home():
if 'username' not in session or session['logged'] == 'false':
flash('Please login to access this page', 'red')
return redirect(url_for('login'))
flag = "No flag for you!!"
if session.get('username') == 'admin':
flag = os.getenv('FLAG')
return render_template("index.html", flag=flag)@app.route("/")
def home():
if 'username' not in session or session['logged'] == 'false':
flash('Please login to access this page', 'red')
return redirect(url_for('login'))
flag = "No flag for you!!"
if session.get('username') == 'admin':
flag = os.getenv('FLAG')
return render_template("index.html", flag=flag)Kode tersebut menunjukkan bahwa flag hanya diberikan apabila session menyimpan username admin. Dengan demikian tujuan eksploitasi menjadi jelas:
- Login sebagai admin.
- Melewati mekanisme 2FA.
- Mengakses halaman utama sebagai admin.
Vulnerability Analysis
1. Weak Password Storage
Pada proses login, aplikasi melakukan hashing password menggunakan SHA-256 lalu membandingkannya dengan nilai yang tersimpan di database.
if user and hashlib.sha256(password.encode()).hexdigest() == user['password']:if user and hashlib.sha256(password.encode()).hexdigest() == user['password']:Masalah utama di sini adalah hash password disimpan tanpa salt. Ketika database bocor, hash seperti ini dapat dengan mudah dianalisis menggunakan wordlist populer ataupun database hash publik.
Hash milik admin:
c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67Setelah dianalisis, hash tersebut berhasil dipecahkan dan menghasilkan password:
apple@123apple@123Sehingga diperoleh kredensial valid:
username: admin
password: apple@123username: admin
password: apple@123
2. OTP Stored Inside Client-Side Session
Kerentanan kedua jauh lebih menarik.
Ketika pengguna berhasil login, aplikasi menghasilkan OTP empat digit:
otp = str(random.randint(1000, 9999))
session['otp_secret'] = otp
session['otp_timestamp'] = time.time()
session['username'] = username
session['logged'] = 'false'otp = str(random.randint(1000, 9999))
session['otp_secret'] = otp
session['otp_timestamp'] = time.time()
session['username'] = username
session['logged'] = 'false'Aplikasi Flask secara default menggunakan session berbasis cookie. Cookie tersebut memang ditandatangani agar tidak dapat dimodifikasi secara sembarangan, namun isinya tidak dienkripsi.
Artinya, data sensitif yang disimpan dalam session masih dapat dibaca oleh pengguna.
Pada route 2FA ditemukan proses validasi berikut:
stored_otp = session['otp_secret']stored_otp = session['otp_secret']Dengan kata lain, OTP yang digunakan untuk verifikasi justru disimpan langsung di sisi klien. Hal ini menghilangkan manfaat keamanan dari mekanisme 2FA karena attacker dapat membaca OTP tanpa harus melakukan brute force.
Exploitation
Step 1 — Login Sebagai Administrator
Menggunakan kredensial yang berhasil diperoleh sebelumnya:
username: admin
password: apple@123username: admin
password: apple@123Login berhasil dilakukan dan aplikasi kemudian meminta kode OTP.
Step 2 — Mengambil Session Cookie
Setelah login, session cookie Flask diambil melalui Browser DevTools.
Contoh cookie yang diperoleh:
.eJwty0sKgCAUAMC7vLVEiv_LhORLBH-oraK756LtwDyQagjowcLl0kAgUGc7Bp4d50ItjPxtxoxjutzAUqUp3alhYmNSGsU5gXtgLy7jSs7nWOD9AEfEHGg.ail1ZQ.ctTCijNjuEgZ87LUvUm4a7sPC0M
.eJwty0sKgCAUAMC7vLVEiv_LhORLBH-oraK756LtwDyQagjowcLl0kAgUGc7Bp4d50ItjPxtxoxjutzAUqUp3alhYmNSGsU5gXtgLy7jSs7nWOD9AEfEHGg.ail1ZQ.ctTCijNjuEgZ87LUvUm4a7sPC0M
Step 3 — Decode Cookie
Cookie kemudian didecode menggunakan flask-unsign.
flask-unsign --decode --cookie '<cookie>'flask-unsign --decode --cookie '<cookie>'Hasil decoding:
{
'logged': 'false',
'otp_secret': '8596',
'otp_timestamp': 1781101925.2669744,
'username': 'admin'
}{
'logged': 'false',
'otp_secret': '8596',
'otp_timestamp': 1781101925.2669744,
'username': 'admin'
}Dari output tersebut terlihat jelas bahwa OTP tersimpan langsung di dalam session.
otp_secret: 8596otp_secret: 8596
Step 4 — Bypass 2FA
OTP yang ditemukan kemudian dimasukkan ke halaman verifikasi.
85968596Karena nilai OTP valid dan masih berada dalam rentang waktu yang diperbolehkan, aplikasi mengubah status login menjadi valid dan mengizinkan akses ke halaman utama.
session['logged'] = 'true'session['logged'] = 'true'
Obtaining the Flag
Setelah berhasil melewati proses autentikasi sebagai administrator, pengguna diarahkan ke halaman utama aplikasi.
Karena session sekarang berisi username admin, kondisi berikut bernilai benar:
if session.get('username') == 'admin':if session.get('username') == 'admin':Akibatnya flag ditampilkan kepada pengguna.
picoCTF{n0_r4t3_n0_4uth_7bd3c284}picoCTF{n0_r4t3_n0_4uth_7bd3c284}Root Cause
Challenge ini mengandung dua kelemahan yang saling melengkapi:
- Password administrator disimpan menggunakan SHA-256 tanpa salt sehingga dapat di-crack setelah database bocor.
- OTP untuk mekanisme 2FA disimpan di dalam client-side session cookie sehingga dapat dibaca oleh pengguna.
Kombinasi kedua kelemahan tersebut memungkinkan attacker memperoleh kredensial administrator sekaligus mengetahui OTP yang digunakan untuk proses autentikasi.
Mitigation
Beberapa langkah mitigasi yang dapat diterapkan:
- Gunakan algoritma password hashing yang dirancang khusus untuk autentikasi seperti bcrypt, scrypt, atau Argon2.
- Terapkan salt unik untuk setiap password.
- Jangan menyimpan OTP atau secret autentikasi di client-side session.
- Simpan data sensitif di server-side storage.
- Terapkan desain 2FA yang memisahkan secret autentikasi dari data yang dapat diakses pengguna.
Lessons Learned
Challenge ini menunjukkan bahwa keberadaan 2FA tidak otomatis meningkatkan keamanan apabila implementasinya salah.
Beberapa poin penting yang dapat dipelajari:
- Database leak sering kali menjadi titik awal kompromi akun.
- Hash password tanpa salt sangat rentan terhadap cracking.
- Session cookie Flask bersifat signed, bukan encrypted.
- Data sensitif tidak boleh disimpan pada client-side session.
- Mekanisme keamanan yang salah implementasi dapat memberikan rasa aman yang palsu.
Conclusion
No-FA merupakan challenge Web Exploitation yang menggabungkan dua kelemahan klasik: password hashing yang lemah dan implementasi 2FA yang tidak aman. Dengan memanfaatkan database yang bocor untuk memperoleh password administrator dan membaca OTP langsung dari session cookie, proses autentikasi dapat dilewati sepenuhnya hingga akhirnya flag berhasil diperoleh.
Challenge ini menjadi pengingat bahwa keamanan tidak hanya bergantung pada fitur yang digunakan, tetapi juga pada bagaimana fitur tersebut diimplementasikan.
Writer : Muhammad Afif Nuromli