默认分类

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

回复

This is just a placeholder img.