N1CTF Do not touch my localhost & dom cloberring
src
proxy_blocker
这是实现了一个chrome的插件,然后他装到了自己的chromedp上。用于充当proxy.
启动的时候会执行js:
window.addEventListener("message", function (event) {
if (event.origin === window.origin) {
if (event.data.type === "setProxy") {
let { hostname, port = "1080" } = window.proxy_options ?? event.data.options;
chrome.runtime.sendMessage({
type: "setProxy",
options: { host: hostname, port: parseInt(port) },
});
}
}
});
持续监听。并且收到消息且满足条件的时候就会调用发送消息来setproxy,我们可以发现一个有趣的东西: let { hostname, port = "1080" } = window.proxy_options ?? event.data.options;
这里他在没有获取到window.proxy_option 情况下才会去取event.data.options 也就是message中的东西。
后续来讲相关利用。
发送setproxy消息。但是这里只是用于初始化的。他真正使用的proxy在后续代码中。
去看一下setproxy方法:
function setProxy(options) {
const config = {
mode: "fixed_servers",
rules: {
singleProxy: options,
},
};
try{
console.log(options)
chrome.proxy.settings.set({ value: config, scope: "regular" });
}catch{}
}
我们继续往下看。
html渲染
主要的js文件都在/static/js/文件目录下。
window.proxy_options = null;
window.postMessage({
type: "setProxy",
options: {"hostname":"1.2.3.4","port":12345}
}, "*")
这个blocker.js会被加载到用户发过消息的
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>View</title>
<script src="/static/js/blocker.js"></script>
</head>
<body>
{{.}}
</body>
</html>
html中。其中{{.}}是通过go模板渲染加载的,是用户输入的消息。下面说明。
go逻辑
func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
secret := make([]byte, 8)
if _, err := rand.Read(secret); err != nil {
panic(err)
}
store := cookie.NewStore(secret)
r.Use(sessions.Sessions("session", store))
r.Use(gin.Recovery())
r.Static("/static", "./static")
r.LoadHTMLGlob("./template/*")
r.POST("/api/login", login)
r.POST("/api/post", AuthRequired(), postNotes)
r.POST("/api/sendToAdmin", AuthRequired(), sendToAdmin)
r.GET("/", withCSP(), AuthRequired(), indexPage)
r.GET("/login", withCSP(), loginPage)
r.GET("/view/:id", withCSP(), AuthRequired(), viewPage)
err := r.Run(":8080")
if err != nil {
fmt.Printf("start server failed: %s", err.Error())
}
}
这个gin框架中实现的最敏感的路由就必然是sendToAdmin了。
func sendToAdmin(c *gin.Context) {
lastRequestTimeLock.Lock()
if time.Now().Unix()-lastRequestTime < 20 {
lastRequestTimeLock.Unlock()
c.JSON(http.StatusOK, gin.H{"ok": false, "message": "please try again after 20 seconds"})
return
}
lastRequestTime = time.Now().Unix()
lastRequestTimeLock.Unlock()
id := c.PostForm("id")
user := c.MustGet("user").(*User)
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", "chrome"),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-setuid-sandbox", true),
chromedp.Flag("js-flags", "--noexpose_wasm,--jitless"),
chromedp.Flag("disable-extensions", false),
chromedp.Flag("enable-features", "BlockInsecurePrivateNetworkRequests,PrivateNetworkAccessRespectPreflightResults"),
chromedp.Flag("load-extension", "./proxy_blocker"),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := chromedp.Run(ctx,
SetCookie("admin_token", adminToken, networkAddr, "/", true),
chromedp.Navigate("http://"+networkAddr+":8888/view/"+id+"?username="+user.username),
chromedp.Sleep(3*time.Second),
); err != nil {
c.JSON(http.StatusOK, gin.H{"ok": false, "message": err.Error()})
fmt.Printf("chromedp error: %v\n", err)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "message": "success"})
}
有CSP 无法实现简单的xss。 它会将我们传入的html页面id 用他自己的chrome来发送给Admin去看。
if err := chromedp.Run(ctx,
SetCookie("admin_token", adminToken, networkAddr, "/", true),
chromedp.Navigate("http://"+networkAddr+":8888/view/"+id+"?username="+user.username),
chromedp.Sleep(3*time.Second),
); err != nil {
c.JSON(http.StatusOK, gin.H{"ok": false, "message": err.Error()})
fmt.Printf("chromedp error: %v\n", err)
return
}
这里进行chrome发起的请求。利用admin_token. 去请求了/view/${id} 这个模板页面。那么很显然是一个xss攻击的题。
但我们很难绕过csp。
ATTACK
首先第一个点就是我们如何去XSS了,刚才在最上面讲的就是 let { hostname, port = "1080" } = window.proxy_options ?? event.data.options;
这很明显出现了一个 window页面中的可控参数,我们考虑去覆盖他。利用DOM CLOBERRING
可以写出如下POC:
<img name=proxy_options href="http://hackIP:PORT/">
这样我们就可控第一个参数了。那么接下来就是希望我们能够进行xss 来让他请求自己的一些东西了。
下一步:
明天补。。
参考资料:
https://blog.zeddyu.info/2020/03/04/Dom-Clobbering/
https://nese.team/writeup/n1ctf2022.pdf