TCP远程命令执行

2024-09-07 13:07:07 浏览数 (1)

此篇教大家如何利用TCP进行远程命令执行。

一. 命令集

将值得信任的命令放进一个txt文件中,执行命令时,就去这个文件里面找,有就执行命令,没有就不执行。

代码语言:javascript复制
ls -a -l
pwd
tree
whoami
who
uname -a
cat
touch

注意我们是用空格隔开的。

二. 命令执行模块实现

依然封装成类,将上述命令集写进类中。

代码语言:javascript复制
class Command
{
public:
    Command(const string& cond_path)
    :_cond_path(cond_path)
    {}

    ~Command()
    {}
private:
    set<string> _safe_cmd;
    string _cond_path;
};

有着两个成员,首先是set类型,即命令集。其次是上述文件集的路径。

初始化路径之后,我们再来将命令写进集合中。

代码语言:javascript复制
const string sep=" ";

string PrefixCommand(const string& cmd)
{
    if(cmd.empty())
    {
        return string();
    }
    auto pos=cmd.find(sep);
    if(pos==string::npos)
    {
        return cmd;
    }
    else
    {
        return cmd.substr(0,pos);
    }
}

void LoadConf(const string& conf)
{
    ifstream in(conf);
    if(!in.is_open())
    {
        LOG(FATAL,"open %s errorn",conf);
        return;
    }
    string line;
    while(getline(in,line))
    {
        LOG(DEBUG,"load command [%s] successn",line.c_str());
        _safe_cmd.insert(PrefixCommand(line));
    }

    in.close();
}

LoadConf函数即初始化集合函数,利用文件读取流打开文件,一行一行读取命令集,并且只取第一个空格前面的字符插入进集合中。下面会讲为什么。

这个操作可以在构造函数的时候实现,所以可以将这个函数加入到构造函数中。

代码语言:javascript复制
Command(const string& cond_path)
:_cond_path(cond_path)
{
    LoadConf(_cond_path);
}

此处肯定需要检测输入的命令是否在命令集中。我们只检查输入命令第一个空格前的字符是否相同即可。例如"ls -a -l"只需检测ls即可。为什么呢?

例如touch指令,后面肯定要加创建的文件名字,这样就需要连着文件名一起检查在不在命令集中,这显然是不合理的,文件名有无数个,怎么能列举完呢。所以只需检测第一个空格前面相不相同即可。

来看如何检查:

代码语言:javascript复制
const string sep=" ";

string PrefixCommand(const string& cmd)
{
    if(cmd.empty())
    {
        return string();
    }
    auto pos=cmd.find(sep);
    if(pos==string::npos)
    {
        return cmd;
    }
    else
    {
        return cmd.substr(0,pos);
    }
}

bool SafeCheck(const string& cmd)
{
    string prefix=PrefixCommand(cmd);
    auto iter=_safe_cmd.find(prefix);
    if(iter==_safe_cmd.end())
    {
        return false;
    }
    return true;
}

string Excute(const string& cmd)
{
    string result;
    if(SafeCheck(cmd))
    {
        FILE* fp=popen(cmd.c_str(),"r");
        if(fp==nullptr)
        {
            return "failed";
        }

        char buffer[1024];
        while(fgets(buffer,sizeof(buffer),fp)!=NULL)
        {
            result =buffer;
        }
        pclose(fp);
    }
    else
    {
        result="坏人n";
    }

    return result;
}

Excute函数就是具体如何处理输入的正确指令,重点就是运用popen函数。

代码语言:javascript复制
FILE *popen(const char *command, const char *type);

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程

参数说明:

command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令,比如sh -c ls type: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。

返回值:

如果调用 fork() 或 pipe() 失败,或者不能分配内存将返回NULL,否则返回一个读或者打开文件的指针。

最后合起来就是命令指令模块:

代码语言:javascript复制
const string sep=" ";

class Command
{
private:
    void LoadConf(const string& conf)
    {
        ifstream in(conf);
        if(!in.is_open())
        {
            LOG(FATAL,"open %s errorn",conf);
            return;
        }
        string line;
        while(getline(in,line))
        {
            LOG(DEBUG,"load command [%s] successn",line.c_str());
            _safe_cmd.insert(PrefixCommand(line));
        }

        in.close();
    }
public:
    Command(const string& cond_path)
    :_cond_path(cond_path)
    {
        LoadConf(_cond_path);
    }

    string PrefixCommand(const string& cmd)
    {
        if(cmd.empty())
        {
            return string();
        }
        auto pos=cmd.find(sep);
        if(pos==string::npos)
        {
            return cmd;
        }
        else
        {
            return cmd.substr(0,pos);
        }
    }

    bool SafeCheck(const string& cmd)
    {
        string prefix=PrefixCommand(cmd);
        auto iter=_safe_cmd.find(prefix);
        if(iter==_safe_cmd.end())
        {
            return false;
        }
        return true;
    }

    string Excute(const string& cmd)
    {
        string result;
        if(SafeCheck(cmd))
        {
            FILE* fp=popen(cmd.c_str(),"r");
            if(fp==nullptr)
            {
                return "failed";
            }

            char buffer[1024];
            while(fgets(buffer,sizeof(buffer),fp)!=NULL)
            {
                result =buffer;
            }
            pclose(fp);
        }
        else
        {
            result="坏人n";
        }

        return result;
    }

    ~Command()
    {}
private:
    set<string> _safe_cmd;
    string _cond_path;
};

其中LOG函数是封装的日志功能:

代码语言:javascript复制
#pragma once

//日志
#include<iostream>
#include<fstream>
#include<cstdio>
#include<string>
#include<ctime>
#include<unistd.h>
#include<sys/types.h>
#include<stdarg.h>
#include<pthread.h>
#include"LockGuard.hpp"

using namespace std;

bool gIsSave=false;
const string logname="log.txt";


void SaveFile(const string& filename,const string& message)
{
    ofstream out(filename,ios::app);
    if(!out.is_open())
    {
        return;
    }
    out<<message;
    out.close();
}

//1.日志是有等级的
enum Level
{
    DEBUG=0,
    INFO,
    WARNING,
    ERROR,
    FATAL
};


string LevelToString(int level)
{
    switch(level)
    {
        case DEBUG: return "Debug";break;
        case INFO: return "Info";break;
        case WARNING: return "Warning";break;
        case ERROR: return "Error";break;
        case FATAL: return "Fatal";break;
        default: return "Unknown";break;
    }
}

string GetTimeString()
{
    time_t curr_time=time(nullptr);
    struct tm* format_time=localtime(&curr_time);
    if(format_time==nullptr) return "None";
    char time_buffer[64];
    snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
    format_time->tm_year 1900,format_time->tm_mon 1,format_time->tm_mday,
    format_time->tm_hour,format_time->tm_min,format_time->tm_sec);

    return time_buffer;
}

pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

//2.日志是由格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容...
void LogMessage(string filename,int line,bool issave,int level,const char* format,...)
{
    string levelstr=LevelToString(level);
    string timestr=GetTimeString();
    pid_t selfid=getpid();

    //可变参数部分处理
    char buffer[1024];
    va_list arg;
    va_start(arg,format);
    vsnprintf(buffer,sizeof(buffer),format,arg);
    va_end(arg);

    LockGuard lockguard(&lock);

    string message;
    message="[" timestr "]" "[" levelstr "]" "[pid: "
             to_string(selfid) "]" "[" filename "]"
             "[" to_string(line) "]" buffer "n";
    if(!issave)
    {
        cout<<message;
    }
    else
    {
        SaveFile(logname,message);
    }
}

void Test(int num,...)
{
    va_list arg;
    va_start(arg,num);

    while(true)
    {
        int data=va_arg(arg,int);
        cout<<"data: "<<data<<endl;
        num--;
    }

    va_end(arg);//arg==NULL
}

//C99新特性 __VA_ARGS__
#define LOG(level,format,...) do {LogMessage(__FILE__,__LINE__,gIsSave,level,format,##__VA_ARGS__);} while(0)
#define EnableFile() do {gIsSave=true;} while(0)
#define EnableScreen() do {gIsSave=false;} while(0)

三. 服务端模块实现

具体的实现跟前面TCP服务端模块实现一样(点此查看)。此处我们采用多线程来实现,就不过多赘述。

代码语言:javascript复制
#include<iostream>
#include<string>
#include<unistd.h>
#include<cstring>
#include<pthread.h>
#include<functional>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

#include"Log.hpp"
#include"InetAddr.hpp"

const static int defaultsockfd=-1;
const static int gbacklog=10;

class TcpServer;

struct ThreadData
{
public:
    ThreadData(int fd,InetAddr addr,TcpServer* s)
    :sockfd(fd)
    ,clientaddr(addr)
    ,self(s)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
    TcpServer* self;
};

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR
};

using func_t=function<string(const string&)>;

class TcpServer
{
public:
    TcpServer(int port,func_t func)
    :_port(port)
    ,_listensock(defaultsockfd)
    ,_isrunning(false)
    ,_func(func)
    {}

    void InitServer()
    {
        //1.创建流式套接字
        _listensock=::socket(AF_INET,SOCK_STREAM,0);
        if(_listensock<0)
        {
            LOG(FATAL,"socket errorn");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG,"socket create success, sockfd is: %dn",_listensock);

        //2.bind
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=INADDR_ANY;
        int n=::bind(_listensock,(struct sockaddr*)&local,sizeof(local));
        if(n<0)
        {
            LOG(FATAL,"bind errorn");
            exit(BIND_ERROR);
        }
        LOG(DEBUG,"bind success,sockfd is: %dn",_listensock);

        //3.tcp是面向连接的,所以通信之前,必须先建立连接,服务器是被连接的
        //tcpserver启动,未来首先要一直等待客户的连接到来
        n=::listen(_listensock,gbacklog);
        if(n<0)
        {
            LOG(FATAL,"listen errorn");
            exit(LISTEN_ERROR);
        }
        LOG(DEBUG,"listen success,sockfd is: %dn",_listensock);

    }

    void Service(int sockfd,InetAddr client)
    {
        LOG(DEBUG,"get a new link,info %s:%d,fd: %dn",client.Ip().c_str(),client.Port(),sockfd);
        string clientaddr="[" client.Ip() ":" to_string(client.Port()) "]# ";
        while(true)
        {
            char inbuffer[1024];
            ssize_t n=recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);
            if(n>0)
            {
                inbuffer[n]=0;
                cout<<clientaddr<<inbuffer<<endl;

                string result=_func(inbuffer);

                send(sockfd,result.c_str(),result.size(),0);
            }
            else if(n==0)
            {
                //client退出&&关闭连接了
                LOG(INFO,"%s quitn",clientaddr.c_str());
                break;
            }
            else
            {
                LOG(ERROR,"read errorn",clientaddr.c_str());
                break;
            }
        }

        ::close(sockfd);//不关闭会发生文件描述符泄露
    }

    static void* HandlerSock(void* args)//IO和业务解耦
    {
        pthread_detach(pthread_self());
        ThreadData* td=static_cast<ThreadData*>(args);
        td->self->Service(td->sockfd,td->clientaddr);
        delete td;
        return nullptr;
    }

    void Loop()
    {
        _isrunning=true;
        //4.不能直接接收数据,应该先获取连接
        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int sockfd=::accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sockfd<0)
            {
                LOG(WARNING,"accept errorn");
                continue;
            }

            //采用多线程
            //此处不能像多进程一样关闭文件描述符,因为多线程文件描述符表是共享的
            pthread_t t;
            ThreadData* td=new ThreadData(sockfd,InetAddr(peer),this);
            pthread_create(&t,nullptr,HandlerSock,td);//将线程分离
        }
        _isrunning=false;
    }

    ~TcpServer()
    {
        if(_listensock>defaultsockfd)
        {
            ::close(_listensock);
        }
    }
private:
    uint16_t _port;
    int _listensock;
    bool _isrunning;

    func_t _func;
};

其中InetAddr.hpp文件是我们封装的。

代码语言:javascript复制
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

using namespace std;

class InetAddr
{
private:
    void GetAddress(string* ip,uint16_t* port)
    {
        *port=ntohs(_addr.sin_port);//网络字节序转为主机字节序
        *ip=inet_ntoa(_addr.sin_addr);//将网络字节序IP转为点分式十进制IP
    }
public:
    InetAddr(const struct sockaddr_in &addr)
    :_addr(addr)
    {
        GetAddress(&_ip,&_port);
    }

    string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    ~InetAddr()
    {}
private:
    struct sockaddr_in _addr;
    string _ip;
    uint16_t _port;
};

四. 服务端调用模块实现

只需创建出服务端类的对象,依次调用InitServer和Loop函数即可。并创建出执行命令类对象。

代码语言:javascript复制
#include"CommandExcute.hpp"
#include"TcpServer.hpp"
#include<memory>

using namespace std;

void Usage(string proc)
{
    cout<<"Usage:nt"<<proc<<" local_portn"<<endl;
}

// ./tcpserver port
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        return 1;
    }
    EnableScreen();
    uint16_t port=stoi(argv[1]);
    Command cmd("./safe.txt");

    func_t cmdExec=bind(&Command::Excute,&cmd,placeholders::_1);
    unique_ptr<TcpServer> tsvr=make_unique<TcpServer>(port,cmdExec);
    tsvr->InitServer();
    tsvr->Loop();


    return 0;
}

五. 客户端模块实现

客户端还是没有变化。不懂得可看此处(点此查看)。

代码语言:javascript复制
#include<iostream>
#include<string>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

#include"Log.hpp"

using namespace std;

void Usage(string proc)
{
    cout<<"Usage:nt"<<proc<<" serverip serverportn"<<endl;
}


// ./tcpclient serverip serverport
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip=argv[1];
    uint16_t serverport=stoi(argv[2]);

    int sockfd=::socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        cerr<<"socket errorn"<<endl;
        exit(2);
    }

    //与udpclient一样,不需显式bind

    //构建目标主机的socket信息
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);
    server.sin_addr.s_addr=inet_addr(serverip.c_str());

    int n=connect(sockfd,(struct sockaddr*)&server,sizeof(server));
    if(n<0)
    {
        cerr<<"connect errorn"<<endl;
        exit(3);
    }

    while(true)
    {
        cout<<"Please Enter#";
        string outstring;
        getline(cin,outstring);

        ssize_t s=send(sockfd,outstring.c_str(),outstring.size(),0);//write
        if(s>0)
        {
            char inbuffer[1024];
            ssize_t m=recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);
            if(m>0)
            {
                inbuffer[m]=0;
                cout<<inbuffer<<endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }
}

六. 效果展示

可以看见只要在命令集中的命令都能执行。

总结:

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

0 人点赞