打开题目
毫无思绪,先用御剑扫描一下
只能扫出index.php
我们尝试能不能用php伪协议读取flag
php://filter/read=convert.base64-encode/resource=index.php
php://filter/read=convert.base64-encode/resource=flag.php
但是页面都回显了429
怀疑是不是源码泄露
用githack看看
在其目录下发现了被我们扒下来的源码
源码如下
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>
我们代码审计一下
get传参exp,用isset函数检查exp不为null,其中传入的exp里面不能包含php伪协议,data协议,filter协议。
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
这里是将get传入的exp的内容,通过正则表达式对 $_GET['exp']
中的内容进行替换,并最终与字符串 ';'
进行比较。
-
preg_replace
函数:preg_replace
用于执行正则表达式的搜索和替换。/[a-z,_]+((?R)?)/
是要搜索的模式。NULL
是替换的内容,这里表示删除匹配的部分。$_GET['exp']
是输入的字符串。
-
';' === preg_replace(...)
: 将preg_replace
的结果与字符串';'
进行比较
(?R)? : (?R)代表当前表达式,就是这个(/[a-z,_]+((?R)?)/),所以会一直递归,?表示递归当前表达式0次或1次(若是(?R)*则表示递归当前表达式0次或多次,例如它可以匹配a(b(c()d())))
接着又进行了一次黑名单过滤
直接构造payload:
exp=highlight_file(next(array_reverse(scandir(pos(localeconv())))));
highlight_file() 函数对文件进行语法高亮显示,本函数是show_source() 的别名
next() 输出数组中的当前元素和下一个元素的值。
array_reverse() 函数以相反的元素顺序返回数组。(主要是能返回值)
scandir() 函数返回指定目录中的文件和目录的数组。
pos() 输出数组中的当前元素的值。
localeconv() 函数返回一个包含本地数字及货币格式信息的数组,该数组的第一个元素就是"."。
得到flag
或
?exp=highlight_file( session_id(session_start()));
知识点:
-
isset函数
isset () 函数用于检测变量是否已设置并且非 NULL
-
什么是无参数RCE
无参rce,就是说在无法传入参数的情况下,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果。
核心代码:
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']); }
代码的核心就是只允许函数而不允许函数中的参数,就是说传进去的值是一个字符串接一个()
,那么这个字符串就会被替换为空,如果替换后只剩下;
,那么这段代码就会被eval
执行。而且因为这个正则表达式是递归调用的,所以说像a(b(c()));
第一次匹配后就还剩
下a(b());
,第二次匹配后就还剩a();
,第三次匹配后就还剩;
了,所以说这一串a(b(c())),
就会被eval
执行,但相反,像a(b('111'));
这种存在参数的就不行,因为无论正则匹配多少次它的参数总是存在的。那假如遇到这种情况,我们就只能使用没有参数的php函数
常见的绕过姿势:
1.getallheaders()
获取
http
所有的头部信息,也就是headers
,然后我们可以用var_dump
把它打印出来,但这个有个限制条件就是必须在apache
的环境下可以使用,其它环境都是用不了的
2.get_defined_vars()
原理和getallheaders()差不多,但是它并不是获取的
headers
,而是获取的四个全局变量$_GET $_POST $_FILES $_COOKIE。
而它的返回值是一个二维数组。这种方法和第一种方法几乎是一样的,就多了一步,就是利用
current()
函数将二维数组转换为一维数组
3.session_id()
简单来说就是把恶意代码写到
COOKIE
的PHPSESSID
中,然后利用session_id()
这个函数去读取它,返回一个字符串,然后我们就可以用eval
去直接执行了,这里有一点要注意的就是session_id()
要开启session
才能用,所以说要先session_start()
但这里要注意的是,
PHPSESSIID
中只能有A-Z a-z 0-9
,-
,所以说我们要先将恶意代码16进制编码以后再插入进去,而在php中,将16进制转换为字符串的函数为hex2bin
4.php函数直接读取文件
<1> localeconv
localeconv函数返回一个包含本地数字及货币格式信息的数组。
<2>scandir
scandir列出目录中的文件和目录
<3>current(pos)
pos()
函数是current()
函数的别名,他们俩是完全一样
作用就是输出数组中当前元素的值,只输出值而忽略掉键,默认是数组中的第一个值
<4>chdir()
函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()
会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir
切就好了,如果要向上跳就要构造chdir('..')
<5>array_reverse()
将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()
<6>
highlight_file()
-
什么是二维数组?
二维数组本质上是以数组作为数组元素
的数组,即数组的数组。
定义类型:类型名 数组名[ 行表达式 ][ 列表达式];行与列用常量表达式
详情见:C 语言之二维数组(详细版)_c语言二维数组-CSDN博客
知识点源于:https://www.cnblogs.com/pursue-security/p/15406272.html