# Webpage to PDF (1) Challenge cung cấp một trang web cho phép nhận đầu vào là một URL và trả về một trang PDF chứa nội dung HTML từ URL nhập vào. ![image](https://hackmd.io/_uploads/rkyyidmzke.png) ![image](https://hackmd.io/_uploads/rJGAqd7fyx.png) Review source qua một lượt thì em thấy được đoạn xử lí tại route /process ![image](https://hackmd.io/_uploads/S1eO6u7zkg.png) Web sẽ thực hiện gửi request tới url mà user nhập vào form sau đó ghi lại response vào html_file. Sau đó, sử thực thi command `wkhtmltopdf {html_file} {pdf_file}` để tạo file pdf từ file HTML vừa ghi. Nếu không có gì lỗi thì sẽ chuyển hướng người dùng đến pdf_file vừa tạo. Thử search google: `wkhtmltopdf vulnerability`: https://www.virtuesecurity.com/kb/wkhtmltopdf-file-inclusion-vulnerability-2/ ![image](https://hackmd.io/_uploads/B1PvlKQGJl.png) Với ý tưởng này, em tạo một exploit HTML với JS Bin: ![image](https://hackmd.io/_uploads/SJU4ZYmGkx.png) ![image](https://hackmd.io/_uploads/S1KH-tQM1g.png) ![image](https://hackmd.io/_uploads/rkXvbt7fyg.png) ![image](https://hackmd.io/_uploads/ryNf4YQGkx.png) ![image](https://hackmd.io/_uploads/r1zrVFmGyg.png) Error... Ở bài viết https://makandracards.com/makandra/516576-fix-exit-code-1-due-network-error-protocolunknownerror có đề cập đến việc file:// đã không cho phép. Tuy nhiên vẫn có thế bypass được thông qua flag `--enable-local-file-access` Điều này có nghĩa là command thực thi là: ``` wkhtmltopdf --enable-local-file-access something.html something.pdf ``` Và hơn nữa session_id có thể control được nên có thể inject flag kia vào để thực thi command. ![image](https://hackmd.io/_uploads/SJX4IF7zkg.png) ![image](https://hackmd.io/_uploads/SkeUIKQzJg.png) 404 Not Found. Nhưng có vẻ command đã thực thi thành công. Điều khiến 404 error là đường dẫn file sai.Xóa đoạn `--enable-local-file-access%20` trên url là xong. ![image](https://hackmd.io/_uploads/BJs7uYmGye.png) # Webpage to PDF (2) ![image](https://hackmd.io/_uploads/HJcMfcmM1l.png) ![image](https://hackmd.io/_uploads/rkiEf9Xfyx.png) Ở challenge này web vẫn cung cấp chức năng giống hệt như trước nhưng có có điểm khác biệt trong xử lí quá trình tạo file pdf. ![image](https://hackmd.io/_uploads/S1wuM9QMke.png) Thay vì xử lí thông qua command trực tiếp dẫn đến Command Injection như challenge trên thì tại đây việc handle pdf_file ở challenge này lại thông qua: ``` # Make PDF pdfkit.from_string(response.text, pdf_file) ``` Do đó không thể inject option vào command như challage trước. Dùng Pycharm để thử đọc handle của xử lí pdf này. ![image](https://hackmd.io/_uploads/BJvxQ9mM1x.png) Vào đọc thử config thì em thấy có một hàm xử lí sau: ![image](https://hackmd.io/_uploads/HJkUXq7Mkx.png) Điểm qua xử lí ở đây thì nó sẽ tìm các thẻ `<meta>` trong content với attribute name có tiền tố `pdfkit-` ![image](https://hackmd.io/_uploads/rJmuO5QMyg.png) Kết quả trả về sẽ là một dict với các cặp giá trị `{name:value}` với name là tên đằng sau prefix và content là nội dung của content trong thẻ `<meta>`. ![image](https://hackmd.io/_uploads/Skr86qQMJe.png) Option lấy từ meta ở trước đó. ![image](https://hackmd.io/_uploads/BJgVR97zJl.png) Execute command từ option được truyền vào. => Dựa vào thẻ meta để truyền option => Có thể thực thi command với option `--enable-local-file-access`. Tạo HTML exploit: ![image](https://hackmd.io/_uploads/BJ3BbsXzke.png) ![image](https://hackmd.io/_uploads/r1DF-sQf1e.png) ![image](https://hackmd.io/_uploads/SkrcWi7zJg.png) # MiniCTF (1) (2) Đây là một source Flask viết theo mô hình MVT. ![image](https://hackmd.io/_uploads/HkO8hMVzkl.png) Nói qua một chút thì mô hình MVT là mô hình kiến trúc web được Flask hay Django sử dụng: * Model: Chịu trách nhiệm tương tác với database. Các Models thường là các class đại diện cho các bảng trong Database. * View: Xử lí logic và kết nối giữa Model và Template. View là nơi tiếp nhân request từ user, thực hiện các xử lí logic cần thiết bao gồm cả việc lấy data từ Model và trả kết quả người dùng thông qua Template. * Template: Chứa mã HTML động để hiển thị cho người dùng, trình bày dữ liệu mà View lấy được từ Model. Quay về challenge hiện tại, trước tiên em dùng thử webapp được cung cấp. ![image](https://hackmd.io/_uploads/SJCU0fEz1e.png) Đầu tiên em thấy web app có 4 chức năng cơ bản thường thấy trong các trang web chơi CTF là Login, Register, Scoreboard và Challenge. ![image](https://hackmd.io/_uploads/SkOC77Nz1g.png) ![image](https://hackmd.io/_uploads/Bk8JV7EGkl.png) ![image](https://hackmd.io/_uploads/BJdgEXEMJg.png) ![image](https://hackmd.io/_uploads/HJOzN7Vf1l.png) Bắt đầu điểm qua source. Đầu tiên có model ![image](https://hackmd.io/_uploads/rkXAVQNGJg.png) Đầu tiên có attempt.py làm việc với database khi user submit flag. ![image](https://hackmd.io/_uploads/ryyfBmVf1e.png) return về id(số chỉ của lượt submit), challenge_id, user_id, is_correct. Tiếp theo là challenge.py ![image](https://hackmd.io/_uploads/rJmRr74z1e.png)![image](https://hackmd.io/_uploads/Bk5J8XEM1x.png) Ở đây làm việc để lấy những thông tin về các challenge, nếu là admin thì có thể lấy được thông tin flag => Cần tìm ra hướng để có thể bypass trở thành admin? Tuy nhiên, thuộc tính flag đã bị hash thông qua hàm compute_hash. ![image](https://hackmd.io/_uploads/ryxlnSNGyg.png) Tiếp theo đó là user.py làm việc với database để quản lí thông tin về user. ![image](https://hackmd.io/_uploads/HkvCLXVzJx.png) ![image](https://hackmd.io/_uploads/Sy_1v7Vzyx.png) Hết models, tiếp đến là view để xem các xử lí logic. ![image](https://hackmd.io/_uploads/BygxqmVfJx.png) Trước tiên là pages.py, em check qua các route: Đăng kí user: ![image](https://hackmd.io/_uploads/S1y0KE4G1e.png) Lấy thông tin từ form sau đó commit lẳng vào database nếu user chưa tồn tại. Đăng nhập user: ![image](https://hackmd.io/_uploads/ByNuqVEGyl.png) Lấy hai trường username-password từ form thực hiện query để kiểm tra thông tin đăng nhập của user. Đăng xuất: ![image](https://hackmd.io/_uploads/rJGQj4VGke.png) redirect đến page home. ![image](https://hackmd.io/_uploads/HkgDsV4Myl.png) * /challenge/ render trang các challenge. * /scoreboard/ render trang bảng điểm. * /admin/challenge render trang quản lí challenge của admin nhưng với điều kiện user đăng nhập phải là admin nếu không sẽ nhận lỗi 403 FORBIDDEN. Tiếp theo đến với `__init__.py`, tại đây khởi tạo các API ![image](https://hackmd.io/_uploads/HJSNhVNGkx.png) ![image](https://hackmd.io/_uploads/BJAvhE4Mye.png) Điểm qua các api của user: * `__init__.py`: ![image](https://hackmd.io/_uploads/B1Jx64Nzkl.png) * `challenge.py`: ![image](https://hackmd.io/_uploads/BJ6n64EG1g.png) ![image](https://hackmd.io/_uploads/BJ-R6VEzkl.png) Ghi lại submit của user cho các challenge. Thực hiện check flag. ![image](https://hackmd.io/_uploads/rJ9cCVNM1g.png) Nếu trả về True thì thực hiện gi vào attempt là is_correct=True còn nếu trả về False thì is_correct=False. * `users.py`: ![image](https://hackmd.io/_uploads/HylkGr4fkl.png) Đọc thông tin của user hiện tại, user khác và top 10 user có điểm cao nhất. * `__init__.py`: ![image](https://hackmd.io/_uploads/ByOtXB4fJg.png) Class GroupAPI khởi tạo kế thừa Class MethodView, dùng để handle HTTP request tới model. Nếu client gửi request với tham số group, nó sẽ trả về các mục được nhóm theo thuộc tính được chỉ định.Nếu không có tham số group thì nó sẽ trả về các mục mà không theo nhóm. VD: ``` GET /api/users Response: { "users": [ {"id": 1, "name": "Alice", "role": "admin"}, {"id": 2, "name": "Bob", "role": "user"} ] } ``` ``` GET /api/users?group=role Response: { "users": { "admin": [{"id": 1, "name": "Alice", "role": "admin"}], "user": [{"id": 2, "name": "Bob", "role": "user"}] } } ``` Tiếp theo đó là các api của admin: * challenge.py: ![image](https://hackmd.io/_uploads/HJjLdBVzkx.png) ![image](https://hackmd.io/_uploads/Bkk__SVG1l.png) ![image](https://hackmd.io/_uploads/BJDsuSVGJe.png) Gồm các method list_challenge, tạo challenge, xóa challenge,... tuy nhiên chỉ có list challenge là dùng được còn lại các route khác đều "not implement". Còn tại list challenge route thì nếu là admin thì nó sẽ gọi đến method admin_marshall. ![image](https://hackmd.io/_uploads/r1BRKB4fJg.png) Vì vậy chỉ cần là admin là sẽ lấy được flag? Tiếp theo đó đọc đến các `/migration/version/__init.py` dùng để khởi tạo data cho database. ``` db.session.add(User(id=1, username='admin', is_admin=True, score=0, password=ADMIN_PASSWORD)) db.session.add(User(id=2, username='player', is_admin=False, score=500, password=PLAYER_PASSWORD, last_solved_at=datetime.fromisoformat('2024-05-11T03:05:00'))) db.session.add(Challenge(id=1, title='Hack this site!', description=f'I was told that there is <a href="/" target="_blank">an unbreakable CTF platform</a>. Can you break it?', category=Category.WEB, flag=FLAG_1, score=500, solves=1, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=2, title='cryp70 6r0s', description='<img src="/static/bitcoin.png" class="rounded mx-auto d-block">', category=Category.CRYPTO, flag='flag{cryp70_6r0s_t0_th3_m00n!}', score=500, solves=0, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=3, title='ɿɘvɘɿƨƎ ɘnϱinɘɘɿinӘ', description='?ϱniɿɘɘniϱnɘ ɘƨɿɘvɘɿ ob uoγ nɒƆ .ϱniɿɘɘniϱnɘ bɿɒwɿoʇ ob nɘɟʇo ɘlqoɘᑫ', category=Category.REVERSE, flag='flag{llew_sgnirts_reenigne_esrever_nac_i}', score=500, solves=0, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=4, title='Where is my canary?', description='<img src="/static/canary.png" class="rounded mx-auto d-block"><br />Challenge connectable at: <code>nc localhost 1337</code>', category=Category.PWN, flag='flag{i_can_see_a_canary_because_i_have_my_stack_protector_on}', score=500, solves=0, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=5, title='Memory Forensics', description='"I am thinking of the flag. Can you navigate in my memory and find what the flag is?"', category=Category.FORENSICS, flag='flag{you_need_to_keep_tuning_to_synchronise_with_me}', score=500, solves=0, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=6, title='You know the rules...', description='...and so do I!<br /><img src="/static/rickroll.gif" class="rounded mx-auto d-block">', category=Category.MISC, flag='flag{never_gonna_give_you_up_never_gonna_let_you_down}', score=500, solves=0, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=7, title='Keyless encryption', description='I encrypted the flag with MD5 and obtained the below ciphertext. Can you decrypt?<br /><code>38760ea8642cbb508964eb087f01d97b</code>', category=Category.CRYPTO, flag='flag{can_you_even_decrypt_messages_encrypted_with_md5?}', score=500, solves=0, released_at=RELEASE_TIME_NOW)) db.session.add(Challenge(id=1337, title='A placeholder challenge', description=f'Many players complained that the CTF is too guessy. We heard you. As an apology, we will give you a free flag. Enjoy - <code>{FLAG_2}</code>.', category=Category.MISC, flag=FLAG_2, score=500, solves=0, released_at=RELEASE_TIME_BACKUP)) db.session.add(Attempt(challenge_id=1, user_id=2, flag=FLAG_1, is_correct=True, submitted_at=RELEASE_TIME_NOW)) db.session.commit() ``` Phân tích ngắn gọn vậy thôi, giờ em bắt đầu exploit. Trước tiên đọc data migration thì em nhận thấy flag 2 nằm ở: ``` db.session.add(Challenge(id=1337, title='A placeholder challenge', description=f'Many players complained that the CTF is too guessy. We heard you. As an apology, we will give you a free flag. Enjoy - <code>{FLAG_2}</code>.', category=Category.MISC, flag=FLAG_2, score=500, solves=0, released_at=RELEASE_TIME_BACKUP)) ``` Nhận thấy chỉ có thuộc tính flag mới bị hash nhưng tại đây description có render FLAG_2 trực tiếp. Tuy nhiên khi call đến api /api/challenge thì không đọc được flag. ![image](https://hackmd.io/_uploads/SJFdXvEGkl.png) ... Tell me why? Vậy là trở thành admin là sẽ đọc được. Vậy bằng cách nào trở thành admin. ![image](https://hackmd.io/_uploads/SynkEvNMke.png) Nhận thấy ở đoạn xử lí đăng kí này xử lí data từ user như sau: ``` form = UserForm(request.form, obj=user) ``` Điều này dẫn đến việc UserForm sẽ tự động lấy tất cả các tham số từ request.form và gán vào thuộc tính của đối tượng user. Việc này có thể dẫn tới lỗ hổng [pollution parameters](https://portswigger.net/web-security/api-testing/server-side-parameter-pollution). Để tránh điều này có thể thêm tham số only và chỉ định các trường mà form cho phép. ``` UserForm = model_form(User, only=['username', 'password']) ``` Và giờ em đăng kí user admin dựa trên lỗ hổng trên. ![image](https://hackmd.io/_uploads/ryg58PNM1g.png) ![image](https://hackmd.io/_uploads/HJC9Iv4G1g.png) Call tới api /api/admin/challenge để xem kết quả. ![image](https://hackmd.io/_uploads/HJv-DvVfke.png) ![image](https://hackmd.io/_uploads/By7QvwEfkx.png) ![image](https://hackmd.io/_uploads/ByjUvDVMkl.png) Vậy là có được flag2. Còn flag 1 có vẻ như đã bị hash.![image](https://hackmd.io/_uploads/rkctDDEfJg.png) do hàm compute_hash. ![image](https://hackmd.io/_uploads/ByTjvvEM1g.png) Vì thế nên không đọc được. Nhận thấy ở đây có 1 solve cho web challenge. ![image](https://hackmd.io/_uploads/rJ9XFDNfJg.png) Check scoreboard thì đó là của user player (vì đã ai solve đâu). Đây phải chăng là manh mối để tìm flag1? ![image](https://hackmd.io/_uploads/SJ8ccwNMkx.png) Nhận thấy ở `__init.py` ![image](https://hackmd.io/_uploads/ByRhcPNfyx.png) `items` sẽ lấy toàn bộ thuộc tính, ở dưới có nhận một tham số đầu vào là group nếu em truyền vào group một thuộc tính nào đó thì sẽ nhận được giá trị thuộc tính đó. Vì vậy, em thử submit một challenge bất kì vào thử truyền tham số group trong api /attempts và quan sát kết quả. ![image](https://hackmd.io/_uploads/S1Phjv4G1g.png) Đây chính là flag em vừa submit. Điều này dẫn đến một ý tưởng là tìm cách login với username là player (người đã solve webchallenge chứa flag). ![image](https://hackmd.io/_uploads/SyN42wNMyg.png) Check thử password thì tất cả đều bị hash. ![image](https://hackmd.io/_uploads/r16v2vVG1g.png) Nhưng tại đây e nhận được salt từ syntax của hash value. => Thử brute-force để tìm password của player. ``` import os import hashlib def compute_hash(password, salt=None): if salt is None: salt = os.urandom(4).hex() return salt + '.' + hashlib.sha256(f'{salt}/{password}'.encode()).hexdigest() hex_values = [f"{i:06x}" for i in range(0x1000000)] for value in hex_values: if compute_hash(value, salt='4b9a50ae') == '4b9a50ae.744c75c952ef0b49cdf77383a030795ff27ad54f20af8c71e6e9d705e5abfb94': print(value) break ```