Bảo mật Java Web Application trước lỗ hổng Cross-Site Request Forgery (CSRF)
Chào các bạn,
Trong bài viết này mình sẽ nói về một lỗ hỏng website tương đối nghiêm trọng và cách phòng chống nó trên ứng dụng Java Web.
Tấn công Cross-Site Request Forgery được thực thi bên trình duyệt web (client-side), nó có thể đánh cắp thông tin của người dùng, thực hiện hành vi không đúng đắn hoặc thực thi những đoạn code không hợp lệ đối với máy chủ back-end (Java Web).
Chỉ cần thông qua một số thủ thuật như là tấn công phi vật lý (social engineering) để có thể chèn một đoạn code nào đó vào máy của nạn nhân để đánh lừa nạn nhân tiếp tay thực thi giúp đoạn code không hợp lệ đó.
Mình sẽ ví dụ bạn thông qua chức năng thay đổi tài khoản và mật khẩu của người dùng bằng ứng dụng Java Web.
Những đoạn code dưới được code chỉ để demo cho lỗ hỏng CSRF, các bạn có thể tham khảo để phòng tránh
Chúng ta sẽ cùng tạo một Database trong SQL Server và một ứng dụng Java Web quản lý người dùng có chức năng đăng nhập và thay đổi mật khẩu của tài khoản.
1 2 3 4 5 6 7 8 9 10 | CREATE DATABASE UserManagement USE UserManagement CREATE TABLE Users ( Username varchar(100) NOT NULL PRIMARY KEY, Password varchar(100) NOT NULL, Role varchar(100) NOT NULL ) INSERT INTO Users VALUES ('bang','1234','Administrator') INSERT INTO Users VALUES ('admin','4567', 'Administrator') |
Dữ liệu sẽ trông như sau:
Hình 2 - Dữ liệu ở bảng Users trong DB UserManagement sau khi thực hiện câu truy vấn |
Chúng ta đăng nhập ứng dụng Web với tài khoản của admin.
Hình 4 - Thực hiện chức năng thay đổi mật khẩu |
Hình 5 - Thao tác thay đổi mật khẩu |
Hình 6 - Thay đổi mật khẩu thành công |
Kiểm chứng lại trong cơ sở dữ liệu.
Hình 7 - Dữ liệu trong cơ sở dữ liệu sau khi thay đổi mật khẩu của admin |
Ta có thể dễ dàng kết luận địa chỉ đầy đủ sẽ là http://abcxyz.com/DemoCSRF/ChangePasswordController?txtUsername=admin&txtPasswordChange=1234&btnAction=Change password now!
Hãy cùng xem source code mẫu của ChangePasswordController hiện tại nhé!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | package bangmaple.controllers; import bangmaple.daos.UsersDAO; import bangmaple.utils.AntiCSRFToken; import java.io.IOException; import java.sql.SQLException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet(name = "ChangePasswordController", urlPatterns = {"/ChangePasswordController"}) public class ChangePasswordController extends HttpServlet { private static final String ERROR = "error.jsp"; private static final String SUCCESS = "success.jsp"; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); String url = ERROR; try { HttpSession session = request.getSession(); if (request.getParameter("btnAction") == null) { request.getRequestDispatcher("changePassword.jsp").forward(request, response); } else { UsersDAO dao = new UsersDAO(); if (dao.changePassword(request.getParameter("txtUsername"), request.getParameter("txtPasswordChange"))) { url = SUCCESS; } } } catch (ClassNotFoundException | SQLException e) { log("ERROR at ChangePasswordController: " + e.getMessage()); } finally { if (url.equals(SUCCESS)) { request.getSession().invalidate(); response.sendRedirect("success.jsp"); } else { request.getRequestDispatcher(url).forward(request, response); } } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } } |
Dựa vào một số kĩ năng tấn công phi vật lí (social engineering) hoặc SQL Injection, kẻ xấu có thể thu được (các) account cùng chung bảng trong cơ sở dữ liệu. Ví dụ kẻ xấu đó biết được có tồn tại một tài khoản tên là bang và ta sẽ thực hiện tấn công CSRF bằng cách thay đổi tham số txtUsername để thay đổi mật khẩu của người dùng khác mà không phải của mình.
http://abcxyz.com/DemoCSRF/ChangePasswordController?txtUsername=bang&txtPasswordChange=aaaa&btnAction=Change password now!
Hình 8 - Thay đổi tham số của địa chỉ |
Hình 9 - Thành công trong việc thay đổi mật khẩu của người dùng khác |
Hình 10 - Mật khẩu của người dùng bang đã bị thay đổi |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | package bangmaple.controllers; import bangmaple.daos.UsersDAO; import bangmaple.utils.AntiCSRFToken; import java.io.IOException; import java.sql.SQLException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * * @author bangmaple */ @WebServlet(name = "ChangePasswordController", urlPatterns = {"/ChangePasswordController"}) public class ChangePasswordController extends HttpServlet { private static final String ERROR = "error.jsp"; private static final String SUCCESS = "success.jsp"; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); String url = ERROR; try { HttpSession session = request.getSession(); String antiCSRFToken; if (request.getParameter("btnAction") == null) { antiCSRFToken = AntiCSRFToken.getToken(); session.setAttribute("csrfToken", antiCSRFToken); request.getRequestDispatcher("changePassword.jsp").forward(request, response); } else { antiCSRFToken = String.valueOf(session.getAttribute("csrfToken")); if (antiCSRFToken.equals(request.getParameter("csrfToken"))) { UsersDAO dao = new UsersDAO(); if (dao.changePassword(request.getParameter("txtUsername"), request.getParameter("txtPasswordChange"))) { url = SUCCESS; } } else { request.setAttribute("ERROR", "Invalid CSRF Token! Well done, hacker!"); } } } catch (ClassNotFoundException | SQLException e) { log("ERROR at ChangePasswordController: " + e.getMessage()); } finally { if (url.equals(SUCCESS)) { request.getSession().invalidate(); response.sendRedirect("success.jsp"); } else { request.getRequestDispatcher(url).forward(request, response); } } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } } |
Vậy bạn sẽ thắc mắc là Anti-CSRF Token đào ở đâu ra? Token chúng ta có thể sinh ra bằng hàm tuyến tính ví dụ đơn giản là f[session](x) = x + 1, x>0 nhưng mà không ai làm đơn giản như vậy vì dễ bị đoán mò. Nên là mình sẽ sử dụng sự trợ giúp của java.security.SecureRandom và bộ thư viện chữ và số.
Ta tạo một class mới có tên là AntiCSRFToken có chứa hàm static để cho việc tái sử dụng hàm được dễ dàng hơn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package bangmaple.utils; import java.security.SecureRandom; public class AntiCSRFToken { private static final String DICT = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int DICT_SIZE = DICT.length(); public static String getToken() { final SecureRandom random = new SecureRandom(); final StringBuilder sb = new StringBuilder(); for (int i = 0; i < DICT_SIZE / 2; i++) { sb.append(DICT.charAt(random.nextInt(DICT_SIZE))); } return sb.toString(); } } |
Ta cũng sửa cho bên view là changePassword.jsp để lúc từ ChangePasswordController trước khi thay đổi thì nó phải trả về thêm một hidden field là csrfToken được generate từ nó, gán vào session và sau đó lúc người dùng submit form thay đổi mật khẩu thì dùng hidden field csrfToken đó chứng thực lại với csrfToken bên back-end server ở session (Java Web).
Nếu người dùng có chứa đúng csrfToken thì mới thực thi hành động thay đổi mật khẩu.
Ở bên trang changePassword.jsp ta thêm một dòng hidden field chứa csrfToken như dòng 15 ở đoạn code dưới.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>Hello ${sessionScope.USER_NAME}!</h1><br/> <h2>Change your password!</h2><br/> Please input your new password!<br/> <form action="ChangePasswordController" method="POST"> Current username: <input type="text" name="txtUsername" value="${sessionScope.USER_NAME}" readonly="true"/><br/> New password: <input type="password" name="txtPasswordChange"/><br/> <input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}"/> <input type="submit" name="btnAction" value="Change password now!"/> </form> </body> </html> |
Đăng nhập với tài khoản tên bang và thực hiện chức năng thay đổi mật khẩu, khi Inspect và ta thấy được hidden field csrfToken được generate với một chuỗi số chữ ngẫu nhiên.
Vậy khi chúng ta submit form, nó sẽ được gửi đi dạng http://abcxyz.com/DemoCSRF/ChangePasswordController?txtUsername=bang?txtPasswordChange=...&csrfToken=wSE3fY7Mc2SuAXozqPlqFxNKRjc1drX
Trông khá là rườm rà nhỉ, đương nhiên là để tránh dài dòng và lộ thông tin của csrfToken, ta luôn thực hiện khi gửi request đi phải gửi dưới dạng POST và để mã hoá nội dung của request dạng POST, ta nên thêm lớp TLS (HTTPS) cho nó.
Nếu là người dùng thông thường gửi lệnh thay đổi mật khẩu một cách chính đáng, ta sẽ nhận dược thông báo thay đổi mật khẩu thành công.
Hình 11 - Form đã được thêm csrfToken |
Hình 12 - Mật khẩu được thay đổi thành công cho tài khoản bang |
Chúng ta sẽ cố gắng tấn công bằng cách thay đổi tài khoản admin không thông qua tài khoản chính chủ.
Hình 13 - Tiếp tục tấn công CSRF |
Ta nhận được kết quả trả về thất bại vì Anti-CSRF Token không hợp lệ.
Hình 14 - Cố gắng thay đổi mật khẩu của tài khoản khác thất bại |
Từ giờ trở đi, chúng ta nên generate Anti-CSRF Token mọi lúc trước khi thực hiện gửi dữ liệu trong form để tránh bị hack. Để tối ưu nhất, ta nên để thời gian timeout cho session càng ngắn càng tốt!
Để cho việc thử nghiệm dễ dàng, bạn nên sử dụng trình duyệt FireFox hoặc Google Chrome bản cũ để tránh bị lỗi Cross-Origin Read Blocking.
Lỗ hỏng bảo mật CSRF thường được dùng chung với XSS, cho nên bảo mật trước CSRF không phải là bảo mật được tất cả. Bạn nên xem xét bảo mật những thành phần còn lại, validate các nội dung trong headers, review code kĩ là được.
Cảm ơn các bạn đã dành thời gian ra đọc bài viết.
Chúc các bạn học tốt!
------
Source-code Java Web DemoCSRF: https://github.com/bangmaple/BangMapleBlogResources/tree/master/DemoCSRF
Quản trị Windows Server cho mọi người - Phần 2
Trong bài viết này mình sẽ tiếp tục đi với series Quản trị Windows Server cho mọi người về phần 2.
Sau khi cài đặt và tinh chỉnh cơ bản hoàn tất với Windows Server 2012 trên máy ảo có tên là SRV1. Hãy cùng tiếp tục đi với những bước tiếp theo.
Chúng ta có tên dễ nhớ, máy tính cũng vậy cho nên để nhận diện được máy tính dễ dàng hơn thì chúng ta hãy đặt một cái tên cho nó nhé!
Trước khi định danh cho máy tính, chúng ta hãy cùng nghiên cứu tổng quát về Windows Server đã nhé!
Như bạn có thể thấy bên dưới là thời hạn sử dụng Windows Server 2012 R2, chúng ta sẽ có 180 ngày sử dụng nó trước khi hết hạn, nếu hết hạn thì bạn có thể dễ dàng kích hoạt lại được nên không cần phải lo lắng về vấn đề này nhé!
Hình 1 - Thông báo thời hạn sử dụng Windows Server |
Hình 2 - Các icon của Windows Server |
Icon thứ 2 sẽ là Server Manager, giúp chúng ta quản lý các tài nguyên của Server hiện hành dễ dàng.
Icon thứ 3 sẽ là Windows PowerShell, giúp chúng ta gõ lệnh hoặc lập trình trên đó, cao cấp hơn Windows Command Line mà chúng ta hay sử dụng thường ngày.
Icon thứ 4 thì rất quen thuộc, giúp chúng ta truy cập vào This PC để quản lý tài nguyên của bộ nhớ của từng phân vùng thuộc (các) ổ cứng.
Khi nhấn vào biểu tượng Windows, chúng ta sẽ được dẫn đến Start quen thuộc mà chúng ta đã và đang sử dụng những bản Windows trước đó, cụ thể là Windows 8 - 8.1.
Hình 3 - Các icon taskbar chính của Windows Server |
Hình 4 - Start sau khi nhấn icon Windows |
Hình 5 - Truy cập Internet Explorer |
Bạn sẽ có được một Start ưng ý và quen thuộc.
Hình 7 - Start giả lập bởi StartIsBack |
Hình 8 - Cửa sổ chính của Server Manager |
Hình 9 - Cửa sổ chính của Windows PowerShell |
Hình 10 - Cửa sổ chính của This PC |
Hình 11 - Thông tin của phân vùng |
Hình 12 - Kiểm tra thông tin của máy ảo hiện hành |
Proccessor: Tên vi xử lý trên máy thật của bạn
Installed Memory (RAM): Bộ nhớ bạn cấp phát cho máy ảo hiện hành
System Type: Phiên bản hệ điều hành
Hình 13 - Thông tin chi tiết cấu hình máy ảo hiện hành |
Hình 14 - Chỉnh sửa chi tiết thông tin máy ảo hiện hành |
Hình 15 - Đổi tên máy ảo hiện hành |
Hình 16 - Đổi tên máy ảo hiện hành |
Hình 17 - Đổi tên máy ảo hiện hành thành công |
Hình 18 - Ngày và giờ hệ thống |
Hình 20 - Xác nhận đổi múi giờ thành công |
Trong menu bar của VirtualBox, chọn File. Sau đó chọn Host Network Manager. Sau đó nhấn Create rồi nhấn Properties, trong phần Adapter, nhập như sau:
IPv4 Address: 172.16.0.1
IPv4 Network Mask: 255.255.0.0
Còn lại để mặc định.
Sau khi tạo xong network thì chúng ta vào Settings của máy ảo hiện hành, sau đó qua tab Network. Chọn tên của network giống như lúc mới tạo trong Host Network Manager.
Sau khi chọn, hãy xác nhận chính xác rồi nhấn OK. Sau đó quay lại màn hình máy ảo.
Hình 21 - Tạo host network |
Sau khi chọn, hãy xác nhận chính xác rồi nhấn OK. Sau đó quay lại màn hình máy ảo.
Chọn icon Server Manager, sau đó chọn Local Server rồi chọn dòng Ethernet IPv4 address assigned by DHCP, IPv6 enabled.
Sau đó nhấn chuột phải vào biểu tượng Ethernet rồi chọn Properties, sau đó một cửa sổ mới hiện rồi chọn dòng Internet Protocol Version 4 (TCP/IPv4) rồi chọn nút Properties.
Trong cửa sổ mới, ta chọn Use the following IP address, sau đó nhập:
Hình 23 - Cấu hình địa chỉ IPv4 |
Trong cửa sổ mới, ta chọn Use the following IP address, sau đó nhập:
IP address: 172.16.0.101
Subnet mask: 255.255.0.0
Default gateway: 172.16.0.1
Sau khi hoàn tất thì hãy nhấn nút OK là xong.
Xác nhận địa chỉ IPv4 bằng cách mở Windows Command Line, sau đó gõ ipconfig
Hình 25 - Cấu hình địa chỉ IPv4 |
Hình 26 - Xác nhận địa chỉ IPv4 |
Đây là một thử thách cho các bạn.
Tạo một snapshot có Snapshot Description là SRV1 - Phan 2
Hình 28 - Xác nhận snapshot đã được tạo |
Lưu trạng thái máy ảo hiện hành bằng cách tắt thông thường rồi chọn Save the machine state.
Hình 29 - Lưu trạng thái hiện hành của máy ảo |
Phần 2 đến đây là kết thúc. Hẹn các bạn ở phần 3!
Chúc các bạn học tốt!
--------------------------------------------------
Tài liệu tham khảo thêm (nên đọc):
Quản trị Windows Server cho mọi người - Phần 1
Chào các bạn,
Mình có mong muốn làm một series về các bài viết quản trị Windows Server cho mọi người, mục đích của mình muốn hướng đến là làm sao để cho các bạn có cái nhìn tổng quan về các dịch vụ của Windows Server nói chung và cái nhìn riêng là làm được và hiểu được.
Không nói nhiều, mình sẽ bắt tay vào hướng dẫn các bạn làm và sẽ giải thích từng phần.
Trước khi bước vào nội dung chính thì mình cũng mong là các bạn đã từng có kinh nghiệm thao tác và sử dụng Windows thông thường.
Thời gian không có nhiều và thiết bị máy tính cũng vậy, nên chúng ta sẽ thực hành trên máy ảo.
Vậy máy ảo sẽ giúp các bạn tiết kiệm chi phí triển khai phần cứng hơn, bạn không cần phải băn khoăn nếu bạn chưa từng dùng máy ảo bao giờ vì mình sẽ đi từng bước cụ thể nhé!
Đầu tiên bạn sẽ phải cần cài một máy ảo. Có rất nhiều dạng máy ảo hiện nay: Oracle VirtualBox, VMWare Player, VMWare Workstation, Parallels Desktop, ... Các cách cấu hình tương tự nhau, bạn có thể dễ dàng tìm hiểu cách sử dụng máy ảo bạn chọn trên Google Search.
Từ giờ trở đi mình sẽ sử dụng máy ảo Oracle VirtualBox cho việc triển khai Windows Server.
Phiên bản Windows Server 2012 cũng được sử dụng cho series này.
Bạn có thể tải xuống (các) tài nguyên dưới đây:
- Windows Server 2012 R2 64-bit ISO (bản cài đặt):
Hình 1 - Tải xuống Windows Server 2012 R2 |
Sau khi tải các tài nguyên cần thiết về, hãy thiết lập môi trường để cài đặt Windows Server 2012 nhé! Dưới đây là hình ảnh sau khi cài đặt VirtualBox và khởi động lên hoàn tất.
Hình 2 - Giao diện của VirtualBox |
Sau đó sẽ có một cửa sổ mới được hiện ra, ta nhập:
Name: SRV1
Machine Folder: Để ở nơi nào bạn có nhiều dung lượng sẽ là nơi chứa máy ảo của bạn
Type: Microsoft Windows
Version: Windows 2012 (64-bit)
Memory Size: 1500MB
Hard disk: Create a existing virtual hard disk file
Hình 3 - Giao diện tạo máy ảo |
Đây là cửa sổ tạo ổ cứng mới giống như lúc bạn gắn thêm ổ cứng vào máy tính thật vậy đó.
File location: Hãy để mặc định chung với thư mục của máy ảo để tránh bị nhầm lẫn
File size: Khuyến cáo 50.00GB
Hard disk file type: Hãy để VDI để có sự tương thích giữa các phần mềm quản lý máy ảo tương thích nhất.
Storage on physical hard disk: Nên để Dynamically allocated để cấp phát vùng bộ nhớ động có nghĩa là bạn dùng bao nhiêu dung lượng trên máy ảo thì bên máy thật mới tốn chừng đó, còn Fixed size thì bạn sẽ tạo sẵn 50GB dung lượng trên ổ cứng của máy thật.
Hình 4 - Tạo ổ cứng cho máy ảo |
Hình 5 - Cửa sổ chính của VirtualBox sau khi máy ảo được tạo |
Hình 6 - Thiết lập máy ảo |
Hình 7 - Thiết lập bộ nhớ lưu trữ |
Hình 8 - Chọn đĩa ISO cho ổ đĩa |
Hình 9 - Xác nhận đã chèn đĩa ISO vào ổ đĩa |
Hình 10 - Cửa sổ chính của VirtualBox sau khi import ISO |
Hình 11 - Màn hình cài đặt chính của Windows Server 2012 |
Hình 12 - Cài đặt Windows Server |
Hình 13 - Chọn phiên bản cài đặt Windows Server |
Hình 14 - Chọn phương thức cài đặt. |
Hình 15 - Tạo phân vừng ổ cứng
|
Sau khi tạo phân vùng mới, bạn sẽ thấy có 2 phân vùng được tạo ra, phân vùng System Reserved là để dành riêng cho hệ thống, phân vùng này không nên được chỉnh sửa/xoá nếu bạn không biết rõ về nó, nó chứa các thành phần khởi động hệ điều hành và khôi phục.
Chúng ta sẽ chọn Drive 0 Partition 2 để cài đặt Windows Server lên nó và nhấn Next.
Windows Server 2012 đang được cài đặt trên máy ảo của bạn, hãy đợi đến khi nó cài đặt xong rồi khởi động lại.
Sau khi cài đặt hoàn tất và khởi động lại, bạn sẽ được yêu cầu là nhập mật khẩu và bạn chỉ được cho phép nhập mật khẩu tương đối mạnh như là mình dùng mật khẩu BangMaple1 để làm mật khẩu cho tài khoản Administrator sau đó nhấn Finish để hoàn tất.
Sau khi nhấn Finish, bạn sẽ được đưa về màn hình khoá cũng như mỗi lúc khởi động lại, bạn cần nhấn tổ hợp phím Ctrl + Alt + Delete để có thể đăng nhập vào màn hình Desktop.
Sau khi mở màn hình khoá, bạn sẽ nhận được yêu cầu đăng nhập vào màn hình Desktop, bạn sẽ phải sử dụng lại mật khẩu lúc bạn tạo trước đó rồi nhấn Enter để hoàn tất việc xác thực.
Sau khi đăng nhập thành công, bạn sẽ được chuyển sang giao diện Windows chính như những Windows thông thường của bạn.
Vậy là bạn đã hoàn tất phần 1 của Quản trị Windows Server cho mọi người rồi đó.
Hình 16 - Phân vùng ổ cứng sau khi tạo mới
|
Hình 17 - Cài đặt Windows Server
|
Hình 18 - Sau khi cài đặt hoàn tất và khởi động lại
|
Hình 19 - Màn hình khoá của Windows Server 2012
|
Hình 20 - Đăng nhập vào Windows Server
|
Hình 21 - Cửa sổ giao diện chính của Windows Server
|
Mình sẽ có thử thách nho nhỏ là hãy cài đặt VirtualBox Guest Addition Tool CD lên Windows Server, sau khi cài đặt xong để biết bạn đã hoàn thành hay chưa thì icon mới sẽ được hiển thị trên thanh taskbar là hoàn tất!
-------------------------------------------
Tài liệu tham khảo thêm nên đọc:
Đăng ký:
Bài đăng
(
Atom
)