Fixing File Access Vulnerabilities in Ruby/Rails

brakeman, rails, ruby, security

Following up on a previous post about Command Injection Vulnerabilities, this post is going to look at File Access Vulnerabilities.

File Access vulnerabilities fall under the category of Insecure Direct Object Reference vulnerabilities in the OWASP top 10 lists. And for 2010 and 2013, Insecure Direct Object vulnerabilities were number 3 for both years. tada confetti_ball fireworks

What is a File Access Vulnerability?

A File Access vulnerability is when an attacker can use various calls to create, modify, or delete files on your server’s file system or a remote file system (eg: S3) that they shouldn’t have permission to modify. Here’s an example of a call that would allow an attacker to link your database file into the public directory of a Rails server:

1
2
3
4
5
6
7
8
9
# http://domain.com?payload=config/database.yml
payload = params[:payload]

path = Rails.root.join(payload)
id = SecureRandom.uuid

File.link(path, "public/#{id}")

redirect_to "/#{id}"

While this example is contrived and code in the wild is not likely to look this obvious, the above is a perfect example of how this type of attack functions. The attacker is able to manipulate your code into linking (and therefore exposing) a file that you wouldn’t want leaked.

Now the difficult thing is that there are an enormous number of methods that are vulnerable to File Access attacks. Pulling from the Brakeman source code, we can create a list of methods where a File Access vulnerability could occur:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# As of Oct 25, 2015
# From: https://github.com/presidentbeef/brakeman/blob/d2d49bd61f2d77919df17fd8dce6193cf1d1ada2/lib/brakeman/checks/check_file_access.rb#L11-L27

# Dir:
Dir[]
Dir.chdir
Dir.chroot
Dir.delete
Dir.entries
Dir.foreach
Dir.glob
Dir.new
Dir.open
Dir.rmdir
Dir.unlink

# File
File.delete
File.foreach
File.lchmod
File.lchown
File.link
File.new
File.open
File.read
File.readlines
File.rename
File.symlink
File.sysopen
File.truncate
File.unlink

# FileUtils
FileUtils.cd
FileUtils.chdir
FileUtils.chmod
FileUtils.chmod_R
FileUtils.chown
FileUtils.chown_R
FileUtils.cmp
FileUtils.compare_file
FileUtils.compare_stream
FileUtils.copy
FileUtils.copy_entry
FileUtils.copy_file
FileUtils.copy_stream
FileUtils.cp
FileUtils.cp_r
FileUtils.getwd
FileUtils.install
FileUtils.link
FileUtils.ln
FileUtils.ln_s
FileUtils.ln_sf
FileUtils.makedirs
FileUtils.mkdir
FileUtils.mkdir_p
FileUtils.mkpath
FileUtils.move
FileUtils.mv
FileUtils.pwd
FileUtils.remove
FileUtils.remove_dir
FileUtils.remove_entry
FileUtils.remove_entry_secure
FileUtils.remove_file
FileUtils.rm
FileUtils.rm_f
FileUtils.rm_r
FileUtils.rm_rf
FileUtils.rmdir
FileUtils.rmtree
FileUtils.safe_unlink
FileUtils.symlink
FileUtils.touch

# IO
IO.foreach
IO.new
IO.open
IO.read
IO.readlines
IO.sysopen

# Kernel
Kernel.load
Kernel.open
Kernel.readlines

# Net::FTP
Net::FTP.new
Net::FTP.open

# Net::HTTP
Net::HTTP.new

# PStore
PStore.new

# Pathname
Pathname.glob
Pathname.new

# Shell
Shell.new

# YAML
YAML.load_file
YAML.parse_file

That’s a nasty long list. And what it means is when you make one of these calls, if you’re using input a user controls then they can attack your system!

To top it all off, there are numerous different types of attacks that could performed. They’re all dangerous and slightly different:

1
2
3
4
5
6
7
8
9
Filling up disk space:                         FileUtils.copy, FileUtils.cp, File.new, IO.new, PStore.new
Move a file to a downloadable location:        File.rename, FileUtils.move
Linking a file to a downloadable location:     File.link, File.symlink, FileUtils.link, FileUtils.ln
Bricking your server (DoS):                    Dir.delete, FileUtils.rm
Changing permissions to directories (DoS):     File.chmod, File.chown, FileUtils.chmod, FileUtils.chown
Renaming key files:                            File.rename, FileUtils.move, FileUtils.mv
Leaking paths:                                 FileUtils.pwd
Downloading malicious files onto your server:  Net::FTP.new, Net::HTTP.new
Launch an attack against another website:      Net::FTP.new, Net::HTTP.new

Some are more harmful than others and typically an attacker is going to leverage one or more of these vulnerabilities to escalate their privileges to own your system. From wikipedia:

Privilege escalation is the act of exploiting a bug, design flaw or configuration oversight in an operating system or software application to gain elevated access to resources that are normally protected from an application or user.

How do you Fix File Access Vulnerabilities?

The best technique for preventing File Access vulnerabilities is not allowing them happen in the first place and avoiding unnecessary system level operations.

Thanks Captain Obvious! facepalm

While that advice is correct, it’s not necessarily good or helpful, so let’s look at the techniques you can use to keep File Access attacks from happening when you do need to work with your system.

Restriction via Identifier

The first way to do that is by using an identifier to refer to files on disk. This identifier will take the form of an id, hash, or GUID.

1
2
3
4
5
6
7
8
9
10
# HTML
<select name="file_guid">
  <option value="690e1597-de8d-4912-ac04-d0e626f806f4">file1.log</option>
  <option value="2e157fa3-ea1e-4b46-931e-c0f8b10bfcb2">file2.log</option>
  <option value="fffb938b-07bc-472c-a48f-383123a9f04d">file3.log</option>
</select>

# Controller
download = FileDownload.find_by(file_guid: params[:file_guid])
send_file(download.path, filename: download.name, type: "text/plain")

Notice in the above code that a GUID is used as the value that gets submitted to the server, and not the actual file name. This makes it impossible for an attacker to download a file they’re not allowed to, and also keeps you safe from any manipulation of the file name or path. This technique will work for moving, deleting, renaming, and sending files as long as you know files names and paths ahead of time. It is the best way to secure your app.

Partial Restriction

Ideally you wouldn’t have to resort to any other techniques for protection, however the real world is a bit messier. And sometimes you don’t have all the information you need in order to use an identifier. In cases like this you want to “sandbox” your users as much as possible by limiting access within the file system:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# HTML
<select name="file_name">
  <option value="file1.log">file1.log</option>
  <option value="file2.log">file2.log</option>
  <option value="file3.log">file3.log</option>
</select>

# Controller
file_name = sanitize(params[:file_name])

# if possible current_user.download_directory should be an identifier
# and controlled 100% by the server.
download_path = "downloads/#{current_user.download_directory}/#{file_name}"

if File.exists?(download_path)
  send_file download_path, filename: file_name, type: "text/plain"
else
  # return an error message
end

Here you can use a sanitize function to clear params[:file_name] of any dangerous characters. In this way you’re accessing the file system in a controlled manner.

Filtered Restriction

The next technique to limit file access trouble is by restricting to specific file types. Here you want to whitelist the types of files that a user can access, such as only .pdf files on the server:

1
2
3
4
5
6
payload = sanitize(params[:filename])
if payload =~ /.pdf$/
  send_file("downloads/#{payload}", filename: 'report.pdf', type: "application/pdf")
else
  raise "Unknown file format requested"
end

This is a line of defense that makes sure that you’re not leaking any sensitive information like a database.yml file. And again make sure to use a sanitize function!

The place you have to be careful here is that whitelisted file extensions can be exploited if an attacker is able to move or rename files. Specifically if they are able to add a .pdf extension to database.yml then they’re able to download the database.yml.pdf file. That’s where multiple vulnerabilities come in as mentioned before. An attacker uses one File Access vulnerability to rename the file, and another to download it.

Store User Files on a Different Server

These days disk space is cheap. One great way to avoid opening your web server up to compromise is to limit data stored on the system. This means leveraging tools like Amazon S3, or DreamHost’s Dream Objects to store user files, generated reports, etc. on a server that is loosely coupled to your app.

As I mentioned in the opening paragraph of this post, you can still shoot yourself in the foot and have an attacker gain access to files they shouldn’t with external storage. Storing your files externally simply separates systems (called a boundary) so that a compromise of your data storage system, doesn’t also compromise your web server.

The added benefit of storing data on other servers is that it will help you scale the load your servers can handle and can reduce processing cycles for those files.

Use an Intermediary

One of the great tools to come out of the “dev ops revolution” is Chef. I use Chef on a regular basis and within the apps that I develop our team uses Chef to manage server configurations. With Chef you can create a boundary between your Ruby/Rails app and your configuration code. Then when you’re passing information between the web app and chef you can ask yourself: “Is this data dangerous?” It’s a subtle distinction and if you’re doing enough system calls it’s worth the investment.

But before you jump on the Chef bandwagon, having an intermediary isn’t going to solve the File Access problem. At the end of the day you’re going to need to pay attention to what you’re doing. The nice bit about Chef is that you can come up with ground rules on your team like:

  • No system calls in main app, only in Chef
  • Heavily sanitized user input, used sparingly in Chef
  • Code Review by two or more people for Chef changes
  • Quarterly review of chef code for vulnerabilities

You get the idea, create a separation of concerns between safe code and hazardous code!

Use Dangerous Methods Sparingly

There’s a good chance that a lot of the methods listed above won’t be useful for you. And really that’s the best case scenario. At the end of the day, not using a dangerous method is the #1 technique for keeping your app safe.

When you’re being asked to implement the amazing new feature that involves file access, you can provide constructive feedback on potential harms that these types of features can bring to the table.

Fixing SQL Injection Vulnerabilities in Ruby/Rails

activerecord, brakeman, rails, ruby, security, sql

In a previous post on Fixing Command Injection Vulnerabilities you saw the damage that can be caused when an attacker gets access to your system. It’s basically Game Over!

Nelson from the simpsons laughing at a Game Over screen

The same is true of SQL Injection also known as SQLi. The dangers of SQL Injection have been talked about for a long time, but for many developers they’ve never seen it in practice. This post is going to explore what a SQL Injection is, why you need to fix it, and how to fix it!

What is a SQL Injection Vulnerability?

SQL Injection falls into the Injection category of vulnerabilities detailed in the OWASP Top 10. SQL Injection is easy to exploit, occurs commonly, and the impact is severe. As a professional software developer it is your job to recognize and fix these vulnerabilities!

This is what SQL Injection looks like:

1
User.where("email = #{payload}").first

That’s all that’s required for an attacker to gain access to your entire database. Don’t believe me? Let’s see how an attacker could own your database.

Basic Exploitation

Since an attacker has full control of payload (for example sake let’s say via params[:email]) they can insert whatever they’d like into your where query. Here’s an example:

1
2
3
4
5
6
7
# http://domain.com/query?email=') or 1=1--
payload = "') or 1=1--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "a@a.com", name: "A", admin: false, created_at: "2015-10-02 13:14:38", updated_at: "2015-10-02 13:14:38">

Above the attacker is sending a payload of ') or 1=1--. It works like this:

  1. The first part of the payload ') sets the query to return zero results; email is blank: email=''.
  2. The second part, 1=1 always equals true, which results in the first entry in the users table being returned.
  3. The final part, -- is a SQL comment. This is a technique to cancel out any further query modifications that could occur server side. Essentially, this reduces the fine tuning to make a payload work.

Simplified, most SQL Injections will follow this type of payload format:

  1. Close the query
  2. Insert the attack
  3. Prevent server modifications

While this seems trivial, an attacker can now manipulate payloads to get access to juicier information. Let’s see another example:

1
2
3
4
5
6
7
# http://domain.com/query?email=') or admin='t'--
payload = "') or admin='t'--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 193, email: "admin1@email.com", name: "Admin1", admin: true, created_at: "2015-09-28 01:33:39", updated_at: "2015-09-28 01:58:35">

Using the payload ') or admin='t'-- the attacker has gotten the system to return an admin user. They now have knowledge about an admin in your database.

Enumerating

In order to get a full dump of admin accounts the attacker needs to be able to enumerate through your admin table. It turns out that this is trivial to accomplish using an id filter:

1
2
3
4
5
6
7
# http://domain.com/query?email=') or admin='t' and id > 193--
payload = "') or admin='t' and id > 193--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 291, email: "admin2@email.com", name: "Admin2", admin: true, created_at: "2015-09-28 01:33:39", updated_at: "2015-09-28 01:58:35">

Here the attacker adds and id > 193 to get the next admin user. At this point, they keep incrementing id until they dump every admin out of your database.

In the back of your mind maybe you’re thinking:

“My user table gets owned, but I encrypt my passwords so at least the damage is just limited to a single table. Big deal if someone gets access to all my user’s … that’s not too bad … right?”

Now you’re smart so I’m sure you didn’t say that to yourself. Because this is bad. And an attacker can do worse!

Discovering Other Tables

How can an attacker find out what other tables exist in the application? Via the sqlite_master table. This table lists the entire database’s schema including tables and indexes.

In order to access this information a couple of new techniques will be required. Let’s see the payload first and then look at the techniques:

1
2
3
4
5
6
7
# http://domain.com/query?email=') union select 1,name,1,1,1,1 from sqlite_master--
payload = "') union select 1,name,1,1,1,1 from sqlite_master--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "schema_migrations", name: "1", admin: true, created_at: 1, updated_at: 1>

The first new technique is the addition of the union operator. This is a SQL operator (not limited to sqlite3) that combines the result of two select statements.

This payload also introduces a new technique of querying a system table:

1
select 1,name,1,1,1,1 from sqlite_master--

What’s happening here is that the attacker is selecting the name column from the sqlite_master table, and then inserting 1’s to fill out the remaining columns. Without those 1’s the database would throw an exception:

1
2
3
SELECTs to the left and right of UNION do not have the same number of result columns:
SELECT "users".* FROM "users" WHERE (email = '') union select
name, 1, 1, 1, 1 FROM sqlite_master--') ORDER BY "users"."id" ASC LIMIT 1

The end query that gets sent to the database looks like this:

1
2
3
SELECT "users".* FROM "users" WHERE (email = '')
  UNION
SELECT 1,name,1,1,1,1 FROM sqlite_master--')  ORDER BY "users"."id" ASC LIMIT 1

Remember that the first query to users doesn’t return a result so the result of the second query is interpreted as a User and fills a User object with the sqlite_master information. Specifically, the payload is crafted so that the name field corresponds with the email field in User.

In this particular example the result was email: "schema_migration" which isn’t helpful. Of course an attacker could use the enumeratation technique from eariler to traverse the entires in the sqlite_master table, but that’s slow. Instead the payload can be modified to use a function and get all the tables in the database at once!

1
2
3
4
5
6
7
# http://domain.com/query?email=') union select 1,group_concat(name, ','),1,1,1,1 from sqlite_master--
payload = "') union select 1,group_concat(name, ','),1,1,1,1 from sqlite_master--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "users,credit_cards,schema_migrations,unique_schema_migrations,sqlite_sequence", name: "1", admin: true, created_at: 1, updated_at: 1>

Above the payload is using the group_concat function provided by sqlite3 to pull together all of the tables into a single value: users,credit_cards,schema_migrations,unique_schema_migrations,sqlite_sequence And viola, the attacker now has knowledge of every table in your database, including the credit_cards table!

Accessing Other Tables

Now that the attacker has discovered the credit_cards table in the application, they’re going to pull as much out of it as they can. Using the same union technique from above:

1
2
3
4
5
6
7
# http://domain.com/query?email=') union select 1,number, 1, 1, 1, 1 FROM credit_cards--
payload = "') union select 1,number,1,1,1,1 FROM credit_cards--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "4242 4242 4242 4242", name: "1", admin: true, created_at: 1, updated_at: 1>

The output of User ought to scare you! An attacker has managed to populate the email field with a credit card number.

For our attacker this is where the party really starts. They have a toe hold into your system, and it’s a matter of time and a simple script to dump all your database. allthethings

How to fix SQL Injection Vulnerabilities

By now it should be crystal clear why you must fix SQL Injection vulnerabilities. In order to fix your SQL queries you’ll need to use parameterization. Parameterization, in a nutshell, is the safest way to handle unsafe user input. And whether you’re using ActiveRecord, Sequel, ROM, or some other ORM they’re all going to have facilities for parameterizing queries.

Let’s look at some common unsafe queries that frequently occur and how to fix them (these examples are ActiveRecord based.)

Single Parameter Queries

The most common use case for Ruby queries is a single parameter.

1
2
3
4
5
6
7
8
# Unsafe
User.where("email = '#{email}'")
User.where("email = '%{email}'" % { email: email })

# Safe
User.where(email: email)
User.where("email = ?", email)
User.where("email = :email", email: email)

While line 3 above looks very similar to line 8, they are different in that line 3 uses string formatting instead of parameterization which is unsafe for protecting against SQL injection.

Looking at the Unsafe vs Safe examples above you can extrapolate a rule of thumb: If you have to add surrounding quotes to your query, you’re vulnerable to SQL Injection.

Compounding Queries

Sometimes you need to chain together a series of queries, usually that’s with an AND statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Unsafe
def unsafe_query
  sql = []
  sql << "email = #{email}" if condition1?
  sql << "name = #{name}"   if condition2?
  # ... etc

  User.where(sql.join(' and '))
end

# Safe
def safe_query
  User.all.tap do |query|
    query.where(email: email) if condition1?
    query.where(name: name)   if condition2?
    # ... etc
  end
end

ActiveRecord is great because it allows you to easily chain together multiple pieces of a query and because they’re evaluated lazily.

One of the real tricky places I’ve seen people struggle with is OR statements. This is in the process of changing but right now the common pattern is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Unsafe
def unsafe_query
  sql = []
  sql << "email = #{email}" if condition1?
  sql << "name = #{name}"   if condition2?
  # ... etc

  User.where(sql.join(' OR '))
end

# Safe
def safe_query
  sql   = []
  param = []

  if condition1?
    sql << "email = ?"
    param << email
  end

  if condition2?
    sql << "name = ?"
    param << name
  end

  User.where(sql.join(' OR '), *param)
end

While not very pretty, notice that the parameters are passed in separately from the query. This way parameterization can still occur keeping you safe from SQL Injection. There are ways to pretty this up which I’d encourage you to use if this type of code is in your code base.

LIKE Query

Another common scenario is doing a starts with/ends with filter using LIKE. This query is more apt to introduce SQL injection because many people don’t understand how it works!

1
2
3
4
5
# Unsafe
User.where("email LIKE '%#{partial_email}%'")

# Safe
User.where("email LIKE ?", "%#{partial_email}%")

Notice that with both queries, you’re going to have to do some string interpolation to insert the % signs. You’ll want to make sure that this occurs inside the value that will be parameterized.

Raw Queries

The final common scenario is raw queries. These are queries where you need to get right into the SQL itself without using ActiveRecord or any other type of framework.

1
2
3
4
5
6
7
8
9
10
11
# Unsafe
st = ActiveRecord::Base.connection.raw_connection.prepare(
  "select * from users where email = '#{email}'")
results = st.execute
st.close

# Safe
st = ActiveRecord::Base.connection.raw_connection.prepare(
  "select * from users where email = ?")
results = st.execute(email)
st.close

The above query is too simple for a raw query, you’d normally be doing a complex query, but at least now you can see proper parameterization. Raw queries follow in the same footsteps as previous examples of query parameterization.

That wraps up this post on SQL Injection. I hope that you learned something new. If there’s a Ruby or Rails security topic that you’d like me to touch on send me a tweet or an email.

Fixing Command Injection Vulnerabilities in Ruby/Rails

brakeman, rails, ruby, security

This post details what a Command Injection vulnerabilitiy is, why you need to fix them, and how to fix them!

What is a Command Injection Vulnerability?

Command Injection is one of the worst types of security vulnerabilities that you can have within your system. It’s one part of a larger umbrella of vulnerabilities known as Injection vulnerabilities. Injection vulnerabilities have taken the #1 spot on the OWASP Top 10 in both 2010 and 2013.

*Using my best John Oliver voice*: Not something to be proud of Injection!

John Oliver yelling at an image that reads: Injection #1 in 2010 and 2013

Command Injection looks like this:

1
2
path = "#{Rails.root}/public/downloads/#{user_supplied_path}"
`ls #{path}`

That’s all that is required for an attacker to gain full access to your system. In such a case all of your data could be stolen, modified, or deleted. Not code that you want to be responsible for writing!

So how does an attacker leverage this code? Since the user is able to provide user_supplied_path they could do this:

1
2
3
4
user_supplied_path = '; cat ./config/database.yml'

path = "#{Rails.root}/public/downloads/#{user_supplied_path}"
`ls #{path}`  #=> ls /webserver/public/downloads; cat ./config/database.yml

This gives the attacker full access to the details of your database. At this point, your company goes bankrupt, you lose your job, and you stub your toe on the way out of your office. It’s a terrible day!

Let’s fix it.

How to Fix Command Injection Vulnerabilities in Rails/Ruby

There are multiple formats that a Command Injection vulnerability can come in based upon the system call that you’re making. Here’s some examples:

1
2
3
4
5
6
7
8
9
10
11
# All the below commands are unsafe!!

system("ls -a -l -@ -1 #{path}")                  # System
`ls #{params[:dir]} &2>1`                         # Backtick
%x(ls #{params[:dir]})                            # %x
exec("md5sum #{params[:input]}")                  # Exec

# Open3
Open3.capture2("ls #{params[:file]}")             # capture2
Open3.capture2e("curl -fsSL #{url}")              # capture2e
Open3.capture3("curl -fsSL #{url}")               # capture3

There are tons of commands that you could be using and thankfully they all follow the same pattern for properly protecting them from Command Injection.

To safely protect from Command Injection, call your command by breaking each piece into a seperate string. Here’s an example:

1
2
3
4
system("ls -a -l -@ -1 #{path}")

# Fixed
system("ls", "-a", "-l", "-@", "-1", path)

That’s it. Really simple! Now there are a few special cases that you’re going to run into, so let’s look at those.

Backtick (`) method

The backtick method is handled in a different way when writing system commands. Namely you cannot provide it multiple arguments. So instead when faced with command injection for backtick, you’ll need to use another method like system.

1
2
3
4
`ls -a -l -@ -1 #{path}`

# Fixed
system("ls", "-a", "-l", "-@", "-1", path)

Output Redirection

Output redirection is another special cases. Let’s say you’ve got a command like this:

1
`ls -a -l -@ -1 #{path} 2>&1`

Simple enough you think, and you change the backtick method to system:

1
system("ls", "-a", "-l", "-@", "-1", path, "2>&1")

The problem is you’re going to run into an error like this:

ls 2>&1: No such file or directory

This is because the system command now thinks that 2>&1 is a part of the arguments to ls and not an output redirection.

To fix this, you’re going to have to use a command from the Open3 library to properly handle output redirection. Because you’re using 2>&1 – which means redirecting stderr to stdout – you’re going to use capture2e:

1
2
require 'open3'
stdout_and_stderr, status = Open3.capture2e("ls", "-a", "-l", "-@", "-1", path)

And don’t forget to require 'open3' otherwise you’ll get:

NameError: uninitialized constant Open3

Logging

One pattern that I’ve seen is logging before running a system command like so:

1
2
3
cmd = "ls -a -l -@ -1 #{path}"
Rails.logger.debug("Running: #{cmd}")
`cmd`

I like to use the splat operator to simplfy the above to:

1
2
3
cmd = ["ls", "-a", "-l", "-@", "-1", path]
Rails.logger.debug("Running: #{cmd.join(" ")}")
system(*cmd)

File Access Vulnerabilities

One big caveat with all of the above is that there is still potential for abuse. Let’s say you’ve got the following command:

1
system("cat", user_supplied_path)

The above is a type of File Access vulnerability and brakeman isn’t going to recognize it!

In this case you’d want to switch to a Ruby built in command like File.read to grab the contents of the file, and make sure to sanitize the payload. Just remember to be careful that you don’t create a new vulnerability when fixing an old one!

Bad URI(is Not URI?): (URI::InvalidURIError)

gems, ruby, rubygems

In this post, I examine a bad URI(is not URI?) error that I received while running gem install and dive into the rubygems source code to diagnose the issue.

1
bad URI(is not URI?):  (URI::InvalidURIError)

I stumbled across the above error a number of times at work when running gem install. It was nothing short of frustrating since every time I did a search for the string I came up with nothing related to rubygems or the gem command. Here’s the full stacktrace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.../uri/common.rb:176:in `split': bad URI(is not URI?):  (URI::InvalidURIError)
.../uri/common.rb:212:in `parse'
.../uri/common.rb:748:in `parse'
.../rubygems/source_list.rb:58:in `<<'
.../rubygems/source_list.rb:73:in `block in replace'
.../rubygems/source_list.rb:72:in `each'
.../rubygems/source_list.rb:72:in `replace'
.../rubygems/source_list.rb:38:in `from'
.../rubygems.rb:867:in `sources='
.../rubygems/config_file.rb:225:in `initialize'
.../rubygems/gem_runner.rb:74:in `new'
.../rubygems/gem_runner.rb:74:in `do_configuration'
.../rubygems/gem_runner.rb:39:in `run'
.../bin/gem:22:in `<main>'

Since Google wasn’t going to solve the problem for me, I had to jump into the source code myself to address the issue. It took running through the rubygems code to actually discover that problem was how sources was defined.

How does rubygems define sources?

In ruby gems there are multiple ways to define sources. The first is the most basic, the default gem source is https://rubygems.org.

The second way to set your gem sources is via a gemrc file. The gemrc file can be defined in 3 different locations:

  • system wide /etc/gemrc
  • per user ~/.gemrc
  • per environment (gemrc files listed in GEMRC environment variable)

The final place where gem sources can be defined is in bundler:

1
2
3
4
5
6
7
source "http://rubygems.org"
source "http://my-private-gemstore.com"

gem 'sinatra'
gem 'rake'

# etc ...

By going through the various locations for defining sources above, I was able to find a gemrc file that was missing an entry for sources:

1
2
3
---
:sources:
-          # should be https://rubygems.org

By filling in the blank item under sources to https://rubygems.org, my problem was resolved.

Why not use RubyGems as the only source?

If you’re newer to ruby, or have never used a private gem repo you’re probably asking why allow multiple source definitions?

Let’s look at the historical side. Back in the day there used to multiple sources of gems. As the ruby community aged these sites either migrated to rubygems.org or shutdown altogether. In cronological order:

Prior to consolidation, you’d need to define all the sources that you were pulling your gems from. This is one reason why you’re able to define multiple sources today.

The second reason that you can define multiple sources for ruby gems is private gem repos. Private repos are a way to privately host gems without giving access to the outside world. There are various use cases for this: ip restrictions, security considerations, etc. which is why you or your company might do this.

If you’re interested, there are a couple of ways to host your gems privately via:

Setting Up CentOS 6.2 Minimal on VMWare Fusion

centos, linux, other, vm

Below are the steps required to get CentOS 6.2 Minimal installed within a VMWare Fusion virtual machine. The end goal of this post is to have a working OS which we can ssh into from the Mac hosting the VM.

Specifically I’m doing this to be able to practice/learn/discover various pieces available in CentOS which is the environment that I now work in at Cisco. Also a hat tip to Mo Khan, whom I work with, his blog lead me to document as I go.

Spinning Up the VM

Network Configuration

The minimal version of CentOS doesn’t have the network setup, so if you ping google: ping google.com it will fail. That means you’ll have to configure your network setup. Once your machine has booted:

  • Configure eth0 by running: vi /etc/sysconfig/network-scripts/ifcfg-eth0
1
2
3
4
5
6
7
8
9
10
DEVICE="eth0"
HWADDR="01:23:45:67:89:ab" # yours will be different
NM_CONTROLLED="yes"

# Was originally no, change to yes
ONBOOT="yes"

# New Info
BOOTPROTO=dhcp
IP=192.168.1.128 # you can modify as you'd like
  • Restart the network interface: /etc/init.d/network restart
  • Then ping google.com and success

Setup SSH

  • Install the openssh packages with yum:
1
yum install openssh-server openssh-client -y
  • Now you should be able to ssh into the box locally:
1
ssh root@127.0.0.1
  • Run ifconfig | grep "inet " and pull the ip address that isn’t 127.0.0.1. It will be the same as the eth0 config from above (192.168.1.128). Then on your mac (not linux terminal) ssh into the vm:
1
ssh root@129.168.1.128