https://support.sonatype.com/hc/en-us/articles/360017310793-CVE-2019-7238-Nexus-Repository-Manager-3-Missing-Access-Controls-and-Remote-Code-Execution-February-5th-2019
膜 Rico 和 voidfyoo. orz
定位到如下位置 plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/ComponentComponent.groovy:185
@Named @Singleton @DirectAction(action = 'coreui_Component') class ComponentComponent extends DirectComponentSupport { ... @DirectMethod @Timed @ExceptionMetered PagedResponse<AssetXO> previewAssets(final StoreLoadParameters parameters) { String repositoryName = parameters.getFilter('repositoryName') String expression = parameters.getFilter('expression') String type = parameters.getFilter('type') // 接收三个参数 repositoryName 、 expression 、 type if (!expression || !type || !repositoryName) { return null } // 设置 repositoryName RepositorySelector repositorySelector = RepositorySelector.fromSelector(repositoryName) // 根据 type 分别调用不同的 validate if (type == JexlSelector.TYPE) { jexlExpressionValidator.validate(expression) } else if (type == CselSelector.TYPE) { cselExpressionValidator.validate(expression) } List<Repository> selectedRepositories = getPreviewRepositories(repositorySelector) if (!selectedRepositories.size()) { return null } def result = browseService.previewAssets( repositorySelector, selectedRepositories, expression, toQueryOptions(parameters)) return new PagedResponse<AssetXO>( result.total, result.results.collect(ASSET_CONVERTER.rcurry(null, null, [:], 0)) // buckets not needed for asset preview screen ) } ... }
Nexus为了查询方便,特地在jexl的基础上引入了csel表达式。简单起见,这里不做展开。接着我们跟入 browseService.previewAssets
,接口定义在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/BrowseService.java:59
/** * Returns a {@link BrowseResult} for previewing the specified repository based on an arbitrary content selector. */ BrowseResult<Asset> previewAssets(final RepositorySelector selectedRepository, final List<Repository> repositories, final String jexlExpression, final QueryOptions queryOptions);
具体实现在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/internal/BrowseServiceImpl.java:233
@Named @Singleton public class BrowseServiceImpl extends ComponentSupport implements BrowseService { ... @Override public BrowseResult<Asset> previewAssets(final RepositorySelector repositorySelector, final List<Repository> repositories, final String jexlExpression, final QueryOptions queryOptions) { checkNotNull(repositories); checkNotNull(jexlExpression); final Repository repository = repositories.get(0); try (StorageTx storageTx = repository.facet(StorageFacet.class).txSupplier().get()) { storageTx.begin(); List<Repository> previewRepositories; if (repositories.size() == 1 && groupType.equals(repository.getType())) { previewRepositories = repository.facet(GroupFacet.class).leafMembers(); } else { previewRepositories = repositories; } PreviewAssetsSqlBuilder builder = new PreviewAssetsSqlBuilder( repositorySelector, jexlExpression, queryOptions, getRepoToContainedGroupMap(repositories)); String whereClause = String.format("and (%s)", builder.buildWhereClause()); //The whereClause is passed in as the querySuffix so that contentExpression will run after repository filtering return new BrowseResult<>( storageTx.countAssets(null, builder.buildSqlParams(), previewRepositories, whereClause), Lists.newArrayList(storageTx.findAssets(null, builder.buildSqlParams(), previewRepositories, whereClause + builder.buildQuerySuffix())) ); } } ... }
注意上面代码中的英文注释,大意为 whereClause
条件在完成 repository filtering
后将会进行 contentExpression
。而 whereClause
是通过前面一系列Builder构建的。可以跟入 builder.buildWhereClause()
,在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/internal/PreviewAssetsSqlBuilder.java:51
, 这里最终引入了contentExpression和jexlExpression:
public class PreviewAssetsSqlBuilder { ... public String buildWhereClause() { return whereClause("contentExpression(@this, :jexlExpression, :repositorySelector, " + ":repoToContainedGroupMap) == true", queryOptions.getFilter() != null); } ... }
接下来即考虑如何进一步执行 contentExpression
。在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/selector/internal/ContentExpressionFunction.java
。当 contentExpression
执行时,会调用 execute
方法:
public class ContentExpressionFunction extends OSQLFunctionAbstract { public static final String NAME = "contentExpression"; ... @Inject public ContentExpressionFunction(final VariableResolverAdapterManager variableResolverAdapterManager, final SelectorManager selectorManager, final ContentAuthHelper contentAuthHelper) { super(NAME, 4, 4); this.variableResolverAdapterManager = checkNotNull(variableResolverAdapterManager); this.selectorManager = checkNotNull(selectorManager); this.contentAuthHelper = checkNotNull(contentAuthHelper); } @Override public Object execute(final Object iThis, final OIdentifiable iCurrentRecord, final Object iCurrentResult, final Object[] iParams, final OCommandContext iContext) { OIdentifiable identifiable = (OIdentifiable) iParams[0]; // asset ODocument asset = identifiable.getRecord(); RepositorySelector repositorySelector = RepositorySelector.fromSelector((String) iParams[2]); // jexlExpression 即 iParams[1] String jexlExpression = (String) iParams[1]; List<String> membersForAuth; ... return contentAuthHelper.checkAssetPermissions(asset, membersForAuth.toArray(new String[membersForAuth.size()])) && checkJexlExpression(asset, jexlExpression, asset.field(AssetEntityAdapter.P_FORMAT, String.class)); }
其中的 iParams
即可对应传入的参数。 contentExpression(@this, :jexlExpression, :repositorySelector, " +":repoToContainedGroupMap) == true
iParams[0]
即 @this
, iParams[1]
即 jexlExpression
, iParams[2]
即 repositorySelector
。在完成初步筛选出 asset
后进入最后的 checkJexlExpression
... private boolean checkJexlExpression(final ODocument asset, final String jexlExpression, final String format) { VariableResolverAdapter variableResolverAdapter = variableResolverAdapterManager.get(format); // variableSource 从 asset 中来 VariableSource variableSource = variableResolverAdapter.fromDocument(asset); SelectorConfiguration selectorConfiguration = new SelectorConfiguration(); selectorConfiguration.setAttributes(ImmutableMap.of("expression", jexlExpression)); // JexlSelector.TYPE 是常量 定义为 'jexl' selectorConfiguration.setType(JexlSelector.TYPE); selectorConfiguration.setName("preview"); try { // 解析表达式 return selectorManager.evaluate(selectorConfiguration, variableSource); } catch (SelectorEvaluationException e) { log.debug("Unable to evaluate expression {}.", jexlExpression, e); return false; } } }
selectorConfiguration
保存要生成的表达式config。 jexlExpression
即前面传入的参数。跟入 selectorManager.evaluate
,在 components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorManagerImpl.java:156
,最终执行了表达式
@Override @Guarded(by = STARTED) public boolean evaluate(final SelectorConfiguration selectorConfiguration, final VariableSource variableSource) throws SelectorEvaluationException { // 根据传入的 selectorConfiguration 生成对应的 selector // 前面指定了 JexlSelector.TYPE ,这里将生成 JexlSelector Selector selector = createSelector(selectorConfiguration); try { // 调用 selector 的 evaluate 方法 return selector.evaluate(variableSource); } catch (Exception e) { throw new SelectorEvaluationException("Selector '" + selectorConfiguration.getName() + "' evaluation in error", e); } }
参考官方文档:
https://help.sonatype.com/repomanager3/configuration/repository-management#RepositoryManagement-CreatingaQuery其对应接口位置如下图
如果是新搭建的环境,为复现成功,还需要先往现有的Repository添加asset。这样在查询确实存在asset后,才会进一步根据 whereClause
对查询结果asset进行筛选,也才会对 whereClause
进行表达式解析。不过在实际环境中,Repository中早就各种asset了。下面随便选了一个logging.jar上传。
POC如下:
增加了权限要求 @RequiresPermissions('nexus:selectors:*')