运维审计

使用paramiko实现运维审计

当你下载paramiko源码的时候,解压完成之后有个demos文件夹,下面有个demo.py程序,在linux运行它,就可以登录远程主机。
我们在看他的源码的时候发现,他在登录后是调用interactive.py中的interactive_shell这个函数,而这个函数有调用了posix_shell()这个函数,我们对这个函数做简单的处理就可以实现简单的堡垒机实现命令的记录,下面是修改实例:

def posix_shell(chan):
    import select
        oldtty = termios.tcgetattr(sys.stdin)
    f = file('record.txt','a+b')
    try:
        tty.setraw(sys.stdin.fileno())
        tty.setcbreak(sys.stdin.fileno())
        chan.settimeout(0.0)
        records = []

        while True:
            r, w, e = select.select([chan, sys.stdin], [], [])
            if chan in r:
                try:
                    x = chan.recv(1024)
                    if len(x) == 0:
                        print '\r\n*** EOF\r\n',
                        break
                    sys.stdout.write(x)
                    sys.stdout.flush()
                except socket.timeout:
                    pass
            if sys.stdin in r:
                x = sys.stdin.read(1)
                records.append(x)
                if x== '\r':
                    c_time = time.strftime('%Y-%m-%d %H:%M:%S') 
                    cmd = ''.join(records).replace('\r', '\n')
                    log = '%s     %s' %(c_time,cmd)
                    f.write(log)
                    records = []
                if len(x) == 0:
                    break
                chan.send(x)

    finally:
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
        f.close()

下面我们使用数据库实现对数据的记录

类似于简单的堡垒机,只能选择你自己管理的ip登陆,不能输入ip,并且你不知道你管理的主机密码。

#我自己写了一个简单的去数据库查数据的mysql.py文件
#!/usr/bin/env python
# encoding: utf-8
"""
@author: feitian
@file: mysql.py
@time: 12/27/17 5:17 PM
"""
import  MySQLdb
from _ast import  Param
class Audit(object):
    def __init__(self,username,passwd):
        self.__username = username
        self.__passwd = passwd
    def CheckHostManager(self):
        params = (self.__username,self.__passwd)
        conn = MySQLdb.Connect(host='localhost',user='root',passwd='westos',db='AuditUser' )
        cur = conn.cursor(cursorclass=MySQLdb.cursors.DictCursor)
        sql = 'select * from manager where username=%s and  passwwd = %s'
        reCount = cur.execute(sql,params)
        data = cur.fetchall()
        cur.close()
        conn.close()
        return  data
    def CheckHost(self):
        params = (self.__username,)
        conn = MySQLdb.Connect(host='localhost',user='root',passwd='westos',db='AuditUser' )
        cur = conn.cursor(cursorclass=MySQLdb.cursors.DictCursor)
        sql = 'select * from ManagerHost where username=%s'
        reCount = cur.execute(sql,params)
        data = cur.fetchall()
        cur.close()
        conn.close()
        return  data
  • 修改demo.py文件
#!/usr/bin/env python
import base64
from binascii import hexlify
import getpass
import os
import select
import socket
import sys
import threading
import time
import traceback

import paramiko
import interactive
import  mysql
USER = []
def agent_auth(transport, username):
    """
    Attempt to authenticate to the given transport using any of the private
    keys available from an SSH agent.
    """

    agent = paramiko.Agent()
    agent_keys = agent.get_keys()
    if len(agent_keys) == 0:
        return

    for key in agent_keys:
        print 'Trying ssh-agent key %s' % hexlify(key.get_fingerprint()),
        try:
            transport.auth_publickey(username, key)
            print '... success!'
            return
        except paramiko.SSHException:
            print '... nope.'

def manual_auth(username, hostname):
    pw = "westos"
    t.auth_password(username, pw)
if __name__ == '__main__':
    paramiko.util.log_to_file('demo.log')
    username = raw_input('please input the HostManagers name you went login:')
    passwd = raw_input('please input HostManagers passwwd:')
    USER.append(username)
    print USER[0]
    j = 0
    feitian = mysql.Audit(username,passwd)
    if feitian.CheckHostManager():
        print "login successed"
        data = feitian.CheckHost()
        for i in data:
            if 'ip' in i:
                j += 1
                print j,i['ip']
        option = input("please select the host you want to login: ")
        hostname = data[option-1]['ip']
        port = 22
        # now connect
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect((hostname, port))
        except Exception, e:
            print '*** Connect failed: ' + str(e)
            traceback.print_exc()
            sys.exit(1)

        try:
            t = paramiko.Transport(sock)
            try:
                t.start_client()
            except paramiko.SSHException:
                print '*** SSH negotiation failed.'
                sys.exit(1)
            try:
                keys = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
            except IOError:
                try:
                    keys = paramiko.util.load_host_keys(os.path.expanduser('~/ssh/known_hosts'))
                except IOError:
                    print '*** Unable to open host keys file'
                    keys = {}

            # check server's host key -- this is important.

            key = t.get_remote_server_key()
            try:
                if not keys.has_key(hostname):
                    print '*** WARNING: Unknown host key!'
                elif not keys[hostname].has_key(key.get_name()):
                    print '*** WARNING: Unknown host key!'
                elif keys[hostname][key.get_name()] != key:
                    print '*** WARNING: Host key has changed!!!'
                    sys.exit(1)
                else:
                    print '*** Host key OK.'
            except Exception:
                print '*** WARNING: Unknown host key!'

            username = raw_input("Please input the username to login  the remote host:")
            agent_auth(t, username)
            if not t.is_authenticated():
                manual_auth(username, hostname)
            if not t.is_authenticated():
                print '*** Authentication failed. :('
                t.close()
                sys.exit(1)

            chan = t.open_session()
            chan.get_pty()
            chan.invoke_shell()
            print '*** Here we go!'
            interactive.interactive_shell(chan,USER[0],hostname)
            chan.close()
            t.close()

        except Exception, e:
            print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
            traceback.print_exc()
            try:
                t.close()
            except:
                pass
            sys.exit(1)
    else:
        print "fail"
        exit()
  • 修改interactive.py文件
import socket
import sys
import time
import MySQLdb
# windows does not have termios...
try:
    import termios
    import tty
    has_termios = True
except ImportError:
    has_termios = False


def interactive_shell(chan,hostname,ip):
    if has_termios:
        posix_shell(chan,hostname,ip)
    else:
        windows_shell(chan)


def posix_shell(chan,hostname,ip):
    import select
    oldtty = termios.tcgetattr(sys.stdin)
    try:
        tty.setraw(sys.stdin.fileno())
        tty.setcbreak(sys.stdin.fileno())
        chan.settimeout(0.0)
        records = []

        while True:
            r, w, e = select.select([chan, sys.stdin], [], [])
            if chan in r:
                try:
                    x = chan.recv(1024)
                    if len(x) == 0:
                        print '\r\n*** EOF\r\n',
                        break
                    sys.stdout.write(x)
                    sys.stdout.flush()
                except socket.timeout:
                    pass
            if sys.stdin in r:
                x = sys.stdin.read(1)
                records.append(x)
                if x== '\r':
                    c_time = time.strftime('%Y-%m-%d %H:%M:%S')
                    cmd = ''.join(records).replace('\r', '\n')
                    params = (hostname, c_time,ip,cmd)
                    conn = MySQLdb.Connect(host='localhost', user='root', passwd='westos', db='AuditUser')
                    cur = conn.cursor(cursorclass=MySQLdb.cursors.DictCursor)
                    sql = 'insert into log values(%s,%s,%s,%s)'
                    reCount = cur.execute(sql, params)
                    conn.commit()
                    cur.close()
                    conn.close()
                    records = []
                if len(x) == 0:
                    break
                chan.send(x)

    finally:
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)

# thanks to Mike Looijmans for this code
def windows_shell(chan):
    import threading

    sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")

    def writeall(sock):
        while True:
            data = sock.recv(256)
            if not data:
                sys.stdout.write('\r\n*** EOF ***\r\n\r\n')
                sys.stdout.flush()
                break
            sys.stdout.write(data)
            sys.stdout.flush()

    writer = threading.Thread(target=writeall, args=(chan,))
    writer.start()

    try:
        while True:
            d = sys.stdin.read(1)
            if not d:
                break
            chan.send(d)
    except EOFError:
        # user hit ^Z or F6
        pass

运维审计_第1张图片
运维审计_第2张图片
将堡垒机用户的环境变量的shell终端,替换成这个脚本,就可以简单模拟这个主机登录之后必须执行这个脚本,在下面在加一行logout

shellinabox web界面的shell

-t 是不要证书,默认启动4200端口

select,poll,epoll三种异步IO类型

select异步IO模型

他是单进程的,和socket不同的是socket使用多进程实现并发,而select是使用单进程实现并发(因为多线程对资源消耗比较到,如cpu之间线程的切换),所以它使用异步。
运维审计_第3张图片
绿色代表主程序(相当于死循环),3个进程(蓝色的)如果有消息直接放入readable这个列表中,而不用一直等待server端回复,如果主程序要给蓝色的进程发送消息也放入writeable中。主程序不断循环3个列表。

缺点

  • 单进程对开启文件句柄的数量有限
  • select 是一个不断循环监视的文件句柄,cpu需要将其从内核态转化为用户态,如果文件句柄很多,响应速度明显变慢。

改进版的select IO类型poll

他没有文件描述符的限制,它采用水平触发(当他将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,他下次会在此报告这些就绪的文件描述符)

缺点

包含大量文件描述符的数组被整体复制于用户态和内核态之间,他随着文件描述符数量的增加而线程增大。

epoll IO类型

在linux2.6才出现了由内核直接支持的实现方法,epoll可以同时处理水平触发和边缘触发(只告诉进程哪些文件描述符准备就绪,他只说一遍如果我们没有采取行动,那么他不会在此告知)。而且当我们调用epoll_wait()时,返回的不是实际的文件描述符,而是一个代表就绪描述符数量的值,只需要去epoll指定的数组中依次去等相应的文件描述符,这里使用了(mmap)技术(快速复制内存),省掉了这些文件描述符在系统调用时复制的开销。

https://pymotw.com/2/select/这里是使用select实现socket并发连接

  • server.py文件
#!/usr/bin/env python
#coding:utf-8
import select
import socket
import sys
import Queue

# Create a TCP/IP socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#socket.AF_INET用于服务器之间的通信,SOCK_STREAM用于tcp协议进行通信
server.setblocking(0)
#相当于异步,可以连接,但是依然是阻塞的相当于在后面排队。
# Bind the socket to the port
server_address = ('localhost', 10000)
print >>sys.stderr, 'starting up on %s port %s' % server_address
server.bind(server_address)

# Listen for incoming connections
server.listen(5)
#最大的连接数


#select()方法接收3个通信列表,第一个是所有输入的data(指外部发过来的数据),第二个是要监控所有要发出去的data(),第3个监控错误信息,接下来我们创建两个列表包含输入和输出信息来传给select()

# Sockets from which we expect to read
inputs = [ server ]

# Sockets to which we expect to write
outputs = [ ]
'''
所有客户端进来的链接和数据会被server的主循环程序放在上面的列表中处理我们现在的server端需要等待连接可写(writeable)之后才能连接,然后接受数据并返回(不是立刻返回,因为每个连接要把输入或输出的数据先缓存到queue中,然后select取出来在发出去)
'''
# Outgoing message queues (socket:Queue)
message_queues = {}

while inputs:
    # Wait for at least one of the sockets to be ready for processing
    print >>sys.stderr, '\nwaiting for the next event'
    readable, writable, exceptional = select.select(inputs, outputs, inputs) 
    #这里如果没有文件描述符是就绪的就在这里阻塞。
    #当你把inputs,outputs,execptional(这里跟inputs共用)传给select()后他会不断循环这3个列表,结果返回3个新的list,所有在readablelist中的socket代表有数据可以接收(recv),所有在writable list中存放着你可以对其发送(send)操作的socket连接,当通信出现error时,会把error写到execptional list中

    # Handle inputs
    for s in readable:
        if s is server:
        #这里是接收一个新的连接,放到列表中。
            # A "readable" server socket is ready to accept a connection
            connection, client_address = s.accept()
            print >>sys.stderr, 'new connection from', client_address
            connection.setblocking(0)
            inputs.append(connection)

            # Give the connection a queue for data we want to send
            message_queues[connection] = Queue.Queue()
        else:
        #这里就是一个 已经连接过等待发数据的链接
            data = s.recv(1024)
            if data:
                # A readable client socket has data
                print >>sys.stderr, 'received "%s" from %s' % (data, s.getpeername())
                message_queues[s].put(data)
                # Add output channel for response
                if s not in outputs:
                    outputs.append(s)     
            else:
                # Interpret empty result as closed connection
                print >>sys.stderr, 'closing', client_address, 'after reading no data'
                # Stop listening for input on the connection
                if s in outputs:
                    outputs.remove(s)
                inputs.remove(s)
                s.close()

                # Remove message queue
                del message_queues[s]
    #发送数据            
    # Handle outputs
    for s in writable:
        try:
            next_msg = message_queues[s].get_nowait()
        except Queue.Empty:
        #这个程序是和客户端只交流一次,如果为空说明已经交流过了。   
            # No messages waiting so stop checking for writability.
            print >>sys.stderr, 'output queue for', s.getpeername(), 'is empty'
            outputs.remove(s)
        else:
        #取到数据,直接发送
            print >>sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername())
            s.send(next_msg)

    #这里就是错误那个列表
    # Handle "exceptional conditions"
    for s in exceptional:
        #如果发生通信错误,直接删除连接。
        print >>sys.stderr, 'handling exceptional condition for', s.getpeername()
        # Stop listening for input on the connection
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()

        # Remove message queue
        del message_queues[s]
  • client.py 文件
#!/usr/bin/env python
#coding:utf-8
import socket
import sys

messages = [ 'This is the message. ',
             'It will be sent ',
             'in parts.',
             ]
server_address = ('localhost', 10000)

# Create a TCP/IP socket
socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
          socket.socket(socket.AF_INET, socket.SOCK_STREAM),
          ]

# Connect the socket to the port where the server is listening
print >>sys.stderr, 'connecting to %s port %s' % server_address
for s in socks:
    s.connect(server_address) 
for message in messages:

    # Send messages on both sockets
    for s in socks:
        print >>sys.stderr, '%s: sending "%s"' % (s.getsockname(), message)
        s.send(message)

    # Read responses on both sockets
    for s in socks:
        data = s.recv(1024)
        print >>sys.stderr, '%s: received "%s"' % (s.getsockname(), data)
        if not data:
            print >>sys.stderr, 'closing socket', s.getsockname()
            s.close()

你可能感兴趣的:(python运维)