[0CTF 2016] piapiapia

发布于 2021-08-06  68 次阅读


知识点:

strlen()函数 判读数组时可绕过对长度的判断

在目录扫描是 可以尝试 低线程 延时扫描 扫出 多个 只能挨个尝试。

反序列化逃逸溢出解法。

解题:

image-20201227191052919
image-20201227191100703
image-20201227191130259

通过测试不同的数据 产生两种 不同的回显,猜测 是admin 弱密码爆破

image-20201227192249815

爆破一波,并不能成功的爆破出来,接下来的思路是尝试sql注入。

image-20201227192422456
image-20201227192433239

诸如此类 尝试了 很多,一点数据库 报错 都没有,放弃SQL注入。F12和返回包也没有提示

页面也没有其他功能 只能 扫一扫目录了

image-20201228102546182

扫了一波,基本上 字典里面的 状态码 都是 200 没有 办法 也就 148条 挨个 访问。

访问之后 发现 /regiter.php 和 www.zip 是正常的。 先下载 备份文件看看。

image-20201228103016982

先看看index.php,原来是限制了长度导致的页面不同回显 误导了我。正常登陆之后 跳转到了 profile.php 并且包含了 class.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);

通过 类 完成了 用户的增删改查 并且连接了数据库,写的很死,sql注入没啥机会(也可能是我菜)

里面还包含了个config.php看看他的功能:

<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>

这里 我们能够看到flag,所以这个题 思路就很明确了,通过XX方法来读取config.php的值,然后得到里面的flag。

接下来看看 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']));
?>

调用class的用户查询函数,返回值并进行反序列化,这里我们遇到了一个

file_get_contents

这个函数,是将整个文件读取到一个字符串中。

整理一下思路 flag在config.php中,想要得到flag 就要读取 这个文件 ,正好存在 一个函数 可以 将文件内容读取 到字符串中,因此我们现在需要构造$profile的序列化格式中 $profile=config.php,因此进一步看 show_profile()这个函数的功能。就是获取username的值调用filter()对其进行过滤,是将黑名单里面的值替换成 _ 或者hacker,这里感觉像是之前unctf做的反序列化逃逸

接下来看看register.php和update.php对用户名 存不存在过滤

image-20201228110819695

register.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 {
?>

我们发现这里又多了一个nickname,啊为啥会有两个,去题目页面尝试一下

image-20201228111409871

经过尝试,发现 注册的那个相当于 你的qq号,nickname相当于你的qq名,长度问题迎刃而解。分析一下 update.php对nickname的限制吧。保证nickname都是正常大小写字符加数字 并且长度不大于10,因此结合上述分析,我们应该是利用反序列化逃逸将$profile[‘photo’]的值溢出,传入我们需要的值。并将nickname修改为数组让过strlen函数对长度的检测。

因此构造payload

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:“photo”;s:10:“config.php”;}
image-20201228113323028
image-20201228113819975
image-20201228113903818