Microservice as a Service 3
Part 3: User manager
https://maas.rars.win/
Challenge
TL;DR: Different programming languages processes HTTP
request.bodydata differently. When the same request is parsed by different languages, assumptions may break.
Once again, we begin by analyzing the docker network layout. We will be able to access manager from the main application. We also see that there are 2 additional hosts, manager_users and manager_updater, which we cannot reach from the main application directly as they reside in manager-net*:
manager:
build: manager
depends_on: ["manager_users", "manager_updater"]
networks:
- level-1
- manager-net
manager_users:
image: library/redis:latest
networks:
- manager-net
manager_updater:
build: manager/updater
networks:
- level-1
- manager-net
* Looks like there is a configuration mistake as
manager_updateris accessible vialevel-1network (which can be pivoted from previous MaaS challengescalculatorandnotes)
Analysing the main application routing for manager, we see that we are able to register and afterwards we are able to request a password update.
Source: app/app.py
@app.route("/manager/login", methods=["POST"])
def manager_login():
username = request.form.get("username")
password = request.form.get("password")
r = requests.post("http://manager:5000/login", json={
"username": username,
"password": password
})
response = r.json()
if response.get('error'):
return response['error'], 403
session['managerid'] = response['uid']
session['managername'] = response['name']
if response.get('flag'):
session['flag'] = response['flag']
return redirect("/manager")
@app.route("/manager/update", methods=["POST"])
def manager_update():
uid = int(request.args['id'])
schema = {"type": "object",
"properties": {
"id": {
"type": "number",
"minimum": int(session['managerid'])
},
"password": {
"type": "string",
"minLength": 10
}
}}
payload = json.loads(request.get_data())
try:
jsonschema.validate(payload, schema)
except jsonschema.exceptions.ValidationError:
return jsonify({"error": f"Invalid data provided"})
return jsonify(requests.post("http://manager:5000/update",
data=request.get_data()).json())
Post authentication, we can request a password update for a specified user id. However, we see that the input values are passed through a jsonschema.validate() check. According to the schema, the specified uid can only be greater or equal than the current session user’s uid. Also, the new password must be 10 characters long.
If we pass the validation, a request is made to manager to update the password for the specified id.
Source: app/manager/app.py
@app.route("/update", methods=["POST"])
def update():
return jsonify(requests.post("http://manager_updater:8080/",
data=request.get_data()).json())
A request is made to the manager_updater host where a Redis database is used to store the user credentials.
Source: app/manager/updater/main.go
func update(w http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
uid, err := jsonparser.GetInt(body, "id")
Well, since the id parameter can only be equal or greater than our current session, and the admin id is 0, what if we specified 2 id in our request? We will set the first id to 0 and the second id to 1 (our current id).

Which works, giving us the message that the password has been changed.

Logging in as admin gives us the flag:

Flag: rarctf{rfc8259_15_4_b1t_v4gu3_1a97a3d3}
Reference
- https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities