文章总结: 该文档分析了0CTF2016的Web题目piapiapia,通过代码审计发现用户资料更新功能存在反序列化漏洞。关键发现是filter函数对单引号和SQL关键字的过滤存在缺陷,结合序列化操作可构造恶意payload读取数据库中的flag。解题步骤包括下载源码、分析class.php中的过滤逻辑、利用update.php的序列化点进行漏洞利用。 综合评分: 85 文章分类: CTF,WEB安全,代码审计,漏洞分析,实战经验
[0CTF 2016]piapiapia
原创
devildollking devildollking
熔城Sec
2026年6月15日 09:00 辽宁
在小说阅读器读本章
去阅读
01
题目简介
- 题目名称:[0CTF 2016]piapiapia
- 题目平台:CTF²
- 题目类型:Web
02
解题步骤
启动并访问靶机
使用dirseach扫描,发现存在压缩包www.zip
访问下载进行解压操作
接下来就是对代码进行解读了,config.php内容如下,发现全局变量flag
<?php$config['hostname'] = '127.0.0.1';$config['username'] = 'root';$config['password'] = '';$config['database'] = '';$flag = '';?>
接下来看一下class.php,这里我认为最主要的就是filter方法,过滤了单引号、反斜线及SQL关键字。
<?phprequire('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)序列化操作。
<?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字符,域名部分类似 // 格式:[email protected],不严格但有一定限制 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主要是将字符串还原为数组直接输出。
<?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,一个简单的登录
<?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
<?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后面需要序列化的内容为
;}s:5:"photo";s:10:"config.php";}
长度为34,所以挤出34位长度,这时候利用class.php中的filter代码,每次过滤where关键字就会将字符串的长度添加1位,所以我们需要在$nickname中输入34个where,然后将;}s:5:”photo”;s:10:”config.php”;}添加到关键字后面
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
然后接下来注册一个账户,注册完成后登录,会让你更新信息
然后修改下图标红的两个参数,修改$nickename为nickename[]进行数组绕过和$nickename的值
然后访问/profile.php
然后右键查看源代码
这里就是base64加密的config.php的内容,进行解密即可
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:熔城Sec devildollking devildollking《[0CTF 2016]piapiapia》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。




![[0CTF2016]piapiapia](/images/random/titlepic/14.jpg)





评论