Nếu bạn xử lý dữ liệu người dùng, bạn cần đảm bảo nó an toàn. Tuy nhiên, nếu bạn mới bắt đầu với bảo mật, nó có thể vẻ rắc rối, nhàm chán và phức tạp.
Bài viết này là bài đầu tiên trong một series sẽ dạy bạn về các loại lỗ hổng bảo mật phổ biến và cách chúng ảnh hưởng đến phát triển Rails. Chúng ta sẽ dùng OWASP Top 10 Web Application Security Risks làm bản đồ để đi qua khu vực này.
OWASP là viết tắt của Open Web Application Security Project. Đây là một nhóm chuyên gia làm việc để giáo dục thế giới về các vấn đề bảo mật quan trọng trên web. Danh sách Top 10 của họ liệt kê các lỗ hổng phổ biến nhất trong ứng dụng web:
- Injection (Tiêm mã)
- Broken authentication (Xác thực bị phá vỡ)
- Sensitive data exposure (Tiết lộ dữ liệu nhạy cảm)
- XML external entities (XXE)
- Broken access control (Kiểm soát truy cập bị phá vỡ)
- Security misconfigurations (Cấu hình bảo mật sai)
- Cross-site scripting (XSS)
- Insecure deserialization (Deserialization không an toàn)
- Using components with known vulnerabilities (Sử dụng thành phần có lỗ hổng đã biết)
- Insufficient logging and monitoring (Ghi log và giám sát không đủ)
Mặc dù họ cập nhật danh sách thường xuyên, nhưng nó thay đổi ít hơn bạn nghĩ. Công nghệ mới kế thừa các vấn đề cũ. Trong bài này, cụ thể, chúng ta sẽ bao phủ ba chủ đề liên quan đến injection:
- JavaScript Injection - khi ứng dụng chấp nhận dữ liệu độc hại từ client, không validate/sanitize dữ liệu, và gửi nó trở lại trình duyệt.
- SQL Injection - khi các mảnh SQL được gửi عمد như một phần của truy vấn cơ sở dữ liệu đến trình thông dịch SQL không an toàn, lừa trình thông dịch chạy các lệnh nguy hiểm hoặc truy cập thông tin nhạy cảm.
- OS Injection - khi kẻ tấn công nhằm chạy lệnh hệ thống trên ứng dụng không được bảo vệ mà nhập dữ liệu từ lệnh hệ thống.
Chúng ta sẽ đi từ lý thuyết đến thực hành để hoàn toàn chứng minh cách mỗi loại hoạt động. Vậy, hãy đi sâu vào!
Các Uy Hiếp Injection
Nếu bạn quản lý một nguồn dữ liệu trong ứng dụng của mình, thì bạn có một vector injection tiềm ẩn bên trong nó. Khi nói đến các cách sáng tạo và đổi mới để hack ứng dụng của bạn, hacker tiếp tục phát minh ra những thứ mới sẽ làm bạn ngạc nhiên.
Hãy bắt đầu với thế giới của các tấn công injection. Nếu bạn nghĩ ứng dụng của mình đã được sanitize và bảo vệ, hãy suy nghĩ lại.
JavaScript Injection (Cross-Site Scripting - XSS)
JavaScript injection, thường được gọi là cross-site scripting (XSS), là việc lừa ứng dụng backend (mà client tin tưởng) gửi dữ liệu và/hoặc script độc hại trở lại trình duyệt.
Khi điều này xảy ra, kẻ tấn công có thể chạy script trong trình duyệt của người dùng để đánh cắp session, yêu cầu thông tin nhạy cảm "nhân danh" ứng dụng, hoặc chuyển hướng người dùng đến các trang web nguy hiểm.
Hãy lấy phần bình luận blog nổi tiếng làm ví dụ. Hãy tưởng tượng ứng dụng của bạn hoàn toàn dễ bị tấn công và nhận một POST cho bình luận mới trong blog của bạn, và giá trị đi thẳng vào database mà không có sanitization:
POST https://myblog.com/comments data:
Khi website của bạn tải lại phần bình luận, nó sẽ lấy bình luận mới, sẽ thực thi script đã cho trên trình duyệt.
Script, chạy trong trang ứng dụng (hoàn toàn chấp nhận được), lấy thông tin cookie của người dùng và gửi trực tiếp đến trang của kẻ tấn công.
SQL Injection
SQL injection xảy ra khi một ứng dụng xử lý database SQL không sanitize an toàn đầu vào của người dùng bất cứ khi nào đầu vào này được nối (hoặc interpolate) vào bất kỳ truy vấn nào của bạn.
Có hai mối đe dọa chính liên quan đến SQL injection mà bạn có thể biết trong thế giới Rails: injection concatenation (nối chuỗi) và interpolation (nội suy). Hãy xem sự khác biệt.
SQL injection concatenation là nổi tiếng nhất; nó xảy ra khi kẻ tấn công gửi các mảnh SQL nguy hiểm như một phần của HTTP query params hoặc request body. Đây là thủ đoạn hoạt động với hầu hết database nếu các lớp ứng dụng của bạn không thể nhận diện loại nội dung này và sanitize nó.
Làm ví dụ, hãy tưởng tượng bạn đang truy vấn người dùng bằng username để lấy thông tin nhạy cảm:
User.where("name = '#{userName}'")
Xem xét userName là đầu vào người dùng không được sanitize, kẻ tấn công có thể thay đổi giá trị param thành:
' OR '1'='1' --
Kết quả, truy vấn của bạn sẽ được biến đổi thành:
SELECT * FROM users WHERE username = '' OR '1'='1';
Vì điều kiện mới thêm luôn bằng true, truy vấn này sẽ luôn được thực thi, tiết lộ hàng trăm dữ liệu nhạy cảm từ người dùng của bạn.
SQL interpolation có thể dẫn đến injection. Làm thế nào? Bạn nhớ tính năng scoping của ActiveRecord trong Rails? Nó cho phép chúng ta chỉ định các truy vấn bạn thường dùng để tham chiếu dưới dạng lời gọi phương thức (như where, joins, và includes) trên các đối tượng association hoặc model. Lấy ví dụ này:
class User < ApplicationRecord
scope :filtered_name, -> { where(name: interpolated_string) }
end
Bạn có thể đoán phần còn lại. Nếu có mệnh đề where để nhà phát triển có thể nối giá trị đầu vào không được sanitize, nó sẽ dẫn đến, khéo, y hệt SQL injection trong ví dụ trước.
OS Injection
OS injection xảy ra khi ứng dụng cho phép người dùng nhập lệnh cấp hệ thống và không lọc chúng.
Hậu quả có thể rất nguy hiểm vì kẻ tấn công sẽ có một đường hầm tự do đến OS mà ứng dụng đang chạy. Tùy thuộc vào cách các lớp bảo mật cơ sở của OS được thiết lập, nó có thể tiết lộ dữ liệu và tệp từ các ứng dụng khác chạy đó.
Bất cứ khi nào bạn thấy một trong các dòng mã sau trong codebase Rails, hãy cảnh giác rằng OS injection có thể xảy ra ở đó:
%x[...]
system()
exec()
`my command` // backticks
Xét triển khai Rails sau:
new_path = "/root/public/images/#{some_path}"
system("ls #{new_path}")
Cho dù some_path đến từ client, kẻ tấn công có thể gửi giá trị như sau:
some_path = 'some/path; cat ./config/database.yml'
Bạn hiểu rồi, đúng không? Tất cả dữ liệu database, bao gồm cả credentials (nếu chúng không được lưu trữ an toàn trong config cloud), sẽ bị tiết lộ cho kẻ tấn công.
Dự Án RailsGoat
Để tiết kiệm thời gian và tránh phải phát triển một đống ví dụ dễ bị tấn công từ đầu, may mắn thay, chúng ta có dự án RailsGoat. Đây là một trong hàng loạt dự án mã nguồn mở (754 dự án) do kho GitHub chính thức của OWASP cung cấp, được tạo cho Rails với hầu hết các lỗ hổng Top 10 được lập trình có mục đích để giáo dục nhà phát triển về các mối đe dọa bảo mật.
Trong series này, chúng ta sẽ sử dụng các mẫu dự án để khám phá các rủi ro sâu hơn và, thậm chí tốt hơn, trong hành động!
Thiết Lập (Setup)
Trước khi bạn đi xa hơn, dự án này có một số phụ thuộc bắt buộc: Ruby, Git, MySQL, và Postgres. Hãy chắc chắn cài đặt tất cả chúng trên máy của bạn trước khi tiến hành.
Để thiết lập dự án, trước tiên, clone nó về máy:
git clone https://github.com/OWASP/railsgoat.git
Nó được nhắm mục tiêu mặc định ở Ruby 2.6.5, vì vậy hãy chắc chắn cài đặt phiên bản thích hợp nếu bạn vẫn chưa có:
rvm install "ruby-2.6.5"
Tiếp theo, chạy các lệnh sau trong thư mục gốc của app:
bundle install
rails db:setup
rails s
Chúng sẽ tải xuống và cài đặt các phụ thuộc của dự án Rails, thiết lập database, và khởi động Rails server, tương ứng.
Một Vài Điều Chỉnh
Tùy thuộc vào OS của bạn, và vì RailsGoat hơi cũ (bản phát hành mới nhất là tháng 3 năm 2018), lệnh install có thể sinh ra một số lỗi. Nếu bạn đang dùng Mac, ví dụ, và gặp lỗi sau ở console:
Lỗi console về libv8
thì, chỉ cần cài đặt gem bắt buộc:
gem install libv8 -v '3.16.14.19' -- --with-system-v8
Một gem khác có thể gây lỗi là therubyracer do bug liên quan đến phiên bản libv8 của Ruby này. Hãy chắc chắn chạy các lệnh sau:
brew install v8-315
gem install therubyracer -v '0.12.3' -- --with-v8-dir=/usr/local/opt/v8@3.15
Mặc định, cài đặt hiện tại sẽ coi SQLite là database mặc định. Tuy nhiên, cho các ví dụ tiếp theo, chúng ta sẽ cần một database thật. Chúng ta sẽ dùng MySQL.
Trước tiên, mở tệp config/database.yml, tìm node mysql, và thay đổi cấu hình theo credentials MySQL của bạn. Bạn có thể để tên database như cũ.
Dừng Rails server, và chắc chắn MySQL đang chạy; sau đó, chạy các lệnh sau:
# Tạo database MySQL
RAILS_ENV=mysql rails db:create
# Chạy migration trên database
RAILS_ENV=mysql rails db:migrate
# Seed database với bản ghi ban đầu
RAILS_ENV=mysql rails db:seed
# Khởi động Rails dùng MySQL
RAILS_ENV=mysql rails s
Thay vào đó, bạn có thể khởi động RailsGoat qua Docker. Tùy bạn!
Xong! Bây giờ, bạn có thể mở trình duyệt và đăng nhập vào ứng dụng RailsGoat. Bạn có thể tìm thấy credentials tự động tạo ở nút "Tutorial Credentials" trên thanh trên. Chọn một, nhưng chắc chắn rằng nó không phải user admin.
Ứng dụng Rails MetaCorp
Thiết Lập HTTP Proxy
Hacker làm việc bằng cách khám phá xung quanh, chủ yếu là HTTP request và response. Họ sử dụng ứng dụng HTTP proxy chạy giữa trình duyệt và server bằng cách chặn, hình dung, và sửa đổi request và response.
Cho series này, chúng ta cũng cần một cái, và công cụ hoàn hảo cho việc này là Burp. Nó là công cụ enterprise trả phí, nhưng phiên bản community miễn phí đầy đủ cho mục đích của chúng ta. Vì vậy, tiến hành, tải xuống và cài đặt nó theo hướng dẫn chính thức.
Nhớ rằng Burp được làm bằng Java, vì vậy bạn cũng cần cài đặt Java.
Hãy chắc chắn làm theo các hướng dẫn này để nó hoạt động đúng. Khi công cụ mở, vào tab Proxy > Intercept, bật nút "Intercept is on" và sau đó "Open Browser". Điều này sẽ mở Google Chromium kết nối trực tiếp với Burp và cho phép chúng ta theo dõi request/response.
Gõ gì đó đó và xem Burp theo dõi mọi thứ như thế nào.
Các Uy Hiếp Trong Hành Động
Hãy xem bây giờ cách mỗi mối đe dọa chúng ta đã thảo luận trước đó diễn ra trong kịch bản thực tế. Chúng ta sẽ bắt đầu với JavaScript injection.
JavaScript Injection Trong Hành Động
Trong ứng dụng RailsGoat, mở tệp _header.html.erb nằm trong thư mục views/layouts/shared. Bạn có thể gặp đoạn HTML sau:
Welcome, <%= current_user.first_name.html_safe %>
Thực ra, phương thức Rails này gọi cho một tên an toàn, mà thực ra không an toàn. Nó cho biết chuỗi có được tin cậy là an toàn hay không nhưng không sanitize đầu vào người dùng.
Hãy chắc chắn Burp không chạy, sau đó vào trang đăng ký và gõ sau vào trường "First Name":
Hoàn tất đăng ký và đăng nhập với user mới tạo. Bạn có thể thấy thanh điều hướng hiển thị "Welcome " + mã script.
Cách Giải Quyết Vấn Đề Này
Đây là một hiểu lầm phổ biến. Nhà phát triển thường dùng phương thức này, nhưng nó không bảo mật dữ liệu. Thay vào đó, bạn phải dùng sanitize bất cứ khi nào bạn cần render HTML một cách rõ ràng.
Trong ví dụ, chỉ cần xóa .html_safe, và cuộc tấn công sẽ bị loại bỏ.
Một mẹo hay là kết hợp các công cụ như SonarQube vào dự án. Các công cụ này nhận diện các mối đe dọa phổ biến như trên và cảnh báo nhà phát triển về nguy hiểm và cách khắc phục.
Không phải ý tưởng tốt để dựa hoàn toàn vào trí nhớ của nhà phát triển.
SQL Injection: Ví Dụ Concatenation
Ví dụ SQL injection của RailsGoat nằm trong users_controller.rb, bên trong thư mục app/controllers. Mở nó và xem nội dung.
Bạn có thể thấy hai phương thức chính để tạo và cập nhật dữ liệu user trong database. Bạn có thể phát hiện gì sai với phương thức update của chúng ta? Đi, thử đi!
Đây nó:
user = User.where("id = '#{params[:user][:id]}'")[0]
Bạn biết rằng không đúng khi nối chuỗi trong mệnh đề where. Nhưng, hãy test khả năng hack trước khi sửa nó.
Quay lại ứng dụng đang chạy trên trình duyệt Chromium của Burp và điều hướng đến menu Account Settings:

Truy cập cài đặt tài khoản
Một khi ở đó, mở công cụ Burp và chắc chắn nút "Intercept is on" được bật. Sau đó, điền các trường mật khẩu với một vài giá trị và nhấn Submit.
Burp sẽ chặn request, và trong tab Params, bạn có thể thấy gì đó tương tự như dưới đây.
Burp Suite Tool - Tab Params
Vâng, tất cả request params của bạn đều nhìn thấy ở đây. Chúng không chỉ nhìn thấy mà còn có thể chỉnh sửa. Burp sẽ giữ request của bạn cho đến khi bạn hoàn tất chỉnh sửa và sau đó giải phóng nó đến server.
Bạn có thể làm tương tự cho response.
Tuyệt, vậy hãy lừa ứng dụng. Mục tiêu của chúng ta là cập nhật mật khẩu của user admin thay vì user hiện tại đang đăng nhập.
Trước tiên, bạn cần xóa params email, first_name, và last_name của user vì chúng ta không nhằm thay đổi giá trị này cho user admin.
Thứ hai, bạn có thể chỉnh sửa giá trị param user[id] thành sau:
0') OR admin = true -- '
Đang xảy ra gì ở đây? Giá trị hiển thị ở trên 6 tham chiếu đến id của user hiện tại đăng nhập. Tuy nhiên, chúng ta không muốn thay đổi bất cứ điều gì liên quan đến user này, chỉ user admin. Số không liên quan đến không ai, điều này tốt vì điều kiện sau OR là cái quan trọng với chúng ta.
Xem xét chúng ta không biết id của admin (nếu bạn biết, sẽ tiết kiệm thời gian), chúng ta phải lừa database chọn nó qua cột role admin.
Một khi bạn hoàn tất chỉnh sửa, nhấn nút Forward để request được giải phóng, và mật khẩu của admin được cập nhật.
Đây là SQL Rails sẽ tạo ra:
SELECT `users`.* FROM `users` WHERE (id = '0') OR admin = true -- '')
Bây giờ, tiến hành đăng nhập vào tài khoản admin với mật khẩu mới của bạn.
Cách Giải Quyết Vấn Đề Này
Có một vài cách an toàn để giải quyết. Bạn luôn có thể lấy user từ database trước khi nó được cập nhật, bất kể cái gì đến từ request client.
Tuy nhiên, nó phụ thuộc vào phong cách code của nhà phát triển, mà không luôn được đảm bảo.
Vì vậy, parameterized database queries đến để cứu vãn! Hãy xem:
user = User.where("id = ?", params[:user][:id])[0]
Đơn giản như vậy! Không còn hack nữa.
SQL Injection: Ví Dụ Interpolation
Trong RailsGoat, mỗi request được lưu trong database như một tính năng audit. Cho ví dụ này, hãy phân tích lớp analytics.rb, lưu trữ một scope tên hits_by_ip. Đây là tính năng admin liệt kê dữ liệu request từ database.
Xem cách model này nội suy chuỗi trong scope của nó:
scope :hits_by_ip, ->(ip, col = "*") { select("#{col}").where(ip_address: ip).order("id DESC") }
Tuy nhiên, cách tiếp cận này nguy hiểm! Hãy xem tại sao. Vì bạn đăng nhập như user bình thường, một vài menu sẽ không hiển thị, nhưng không có nghĩa là endpoint của chúng không khả dụng. Vì vậy, tiến hành truy cập https://localhost:3000/admin/1/analytics.
Vì chúng ta đang làm việc ở cấp localhost, bạn sẽ chỉ tìm thấy dữ liệu dưới IP 127.0.0.1. Tuy nhiên, trong production, bạn sẽ tìm kiếm IP client của mình.
Vì vậy, gõ 127.0.0.1 vào hộp văn bản "Search by IP" và nhấn enter. Đừng quên bật nút intercept trên công cụ Burp của bạn.
Một khi bạn ở tab Params, bạn có thể nhấn nút Add để thêm param mới kiểu URL và cho nó tên sau:
field[(select+group_concat(password)+from+users+where+admin=true)]
Vì scope nhận chuỗi nội suy, bạn có thể đơn giản thêm bao nhiêu quy tắc cho truy vấn select tùy thích. Truy vấn cụ thể này sẽ biến thành:
SELECT (select group_concat(password) from users where admin = true) FROM analytics WHERE ip_address = "127.0.0.1" ORDER BY id DESC;
Nghĩa là chúng ta đang lấy hashed password của admin từ database và hiển thị nó trực tiếp trong view:
Truy vấn hashed password của admin
Cách Giải Quyết Vấn Đề Này?
Trước hết, hãy chắc chắn user của bạn chỉ có quyền truy cập vào những gì họ phải có. Những endpoint như vậy không nên khả dụng hoặc không được bảo vệ.
Là một cách phòng ngừa khác, bạn có thể whitelist các giá trị nên được chấp nhận và hạn chế những giá trị không nên. Xem phương thức parse_field trong cùng lớp model. Nó kiểm tra xem trường đã cho có nằm trong mảng whitelist không.
Vì vậy, trước khi gọi scope của model, bạn có thể lặp qua params và kiểm tra xem chúng có ổn để đi không. Hãy làm bằng cách cập nhật dòng 18 của admin_controller.rb (gọi scope):
fields = params[:field].map {|k,v| Analytics.parse_field(k) }.join(",")
OS Injection Trong Hành Động
Hãy khám phá ví dụ OS injection trong RailsGoat. Mở model benefits.rb dưới thư mục app/models và xem phương thức make_backup của nó.
Phương thức này tạo bản sao backup của tệp đang được tải lên qua phần “Benefit Forms” của ứng dụng. Không có vấn đề gì ở đây, trừ việc phương thức sử dụng lệnh system:
silence_streams(STDERR) { system("cp #{full_file_name} #{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}") }
Có vẻ đúng ở nhìn đầu tiên, nhưng nhìn lại. Chúng ta có thể hoàn toàn nối các lệnh hệ thống khác từ đầu vào người dùng, và chúng sẽ chạy tốt, như việc tạo một tệp.
Đợi, đây là tính năng upload tệp; làm sao chúng ta có thể cập nhật đầu vào của một tệp? Hãy xem nó trong hành động.
Quay lại ứng dụng RailsGoat, nhấn menu "Benefit Forms", chọn một tệp tùy thích, và bật nút intercept Burp. Sau đó, nhấn Start Upload.
Khi request bị chặn, bạn sẽ thấy nội dung header của nó, như dưới đây.
File uploading bị chặn
Trong hình, bạn có thể thấy hai param được highlight: benefits[backup] và benefits[upload].
Chúng ta cần thay đổi giá trị của cái đầu tiên thành true vì chúng ta muốn kích hoạt luồng tạo backup của tệp, và do đó, nơi lỗ hổng tồn tại.
Tiếp theo, thay đổi thuộc tính filename của param thứ hai thành:
filename="kid-2.png;+touch+abc.txt"
Sau đó, thả nút intercept. Điều này sẽ chuyển thành một lệnh mới ở cuối thực thi, tạo một tệp mới tên abc.txt. Ngoài việc đơn giản, đây là ví dụ tốt về việc luồng của bạn có thể dễ bị tấn công như thế nào và có phải là sân chơi hoàn hảo cho hacker không.
Bảo Vệ Đối Phòng OS Injection
Có vẻ hơi rõ ràng; tại sao ai đó copy tệp qua lệnh hệ thống? Bạn sẽ sốc về số lượng ứng dụng legacy đang chạy. Nhiều trong số chúng bao gồm codebase khổng lồ, có thể biến việc phát hiện các lỗ hổng như vậy thành nhiệm vụ Hercules.
Vì vậy, đúng, chỉ cần dùng thư viện nội bộ chính thức, như FileUtils của Ruby:
FileUtils.cp "#{full_file_name}", "#{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}"
Tổng Kết
Hôm nay, chúng ta đã đi qua những nước động荡 của các mối đe dọa bảo mật injection. Mặc dù bài viết này không bao phủ tất cả các vấn đề liên quan đến injection, nó khám phá những nổi tiếng nhất, như được OWASP xác định.
Là một bonus, tôi sẽ cung cấp một vài link quan trọng giúp cải thiện kiến thức của bạn về chủ đề. Cái đầu tiên, tất nhiên, là bài viết OWASP Top Ten; nó có nhiều link ngoài đến bài viết khác bao gồm ví dụ và kịch bản khác nhau.
Rails SQL Injection là tài liệu được biên soạn bởi một vài thành viên cộng đồng giải quyết các SQL injection phổ biến qua ví dụ thực tế. Đây là bài bắt buộc đọc sau những gì chúng ta đã bao phủ.
Cuối cùng nhưng không kém phần quan trọng, có tài liệu Security Rails chính thức. Chúng bao phủ mọi thứ về bảo mật trong ứng dụng Rails, bao gồm injection. Vì vậy, hãy chắc chắn đọc kỹ nó. Tiếp tục học tập, và chúng ta sẽ gặp nhau ở trạm tiếp theo!