此篇教大家如何利用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,没人告诉我错误就寄了。
祝大家越来越好,不用关注我(疯狂暗示)