漏洞公告

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即可对应传入的参数。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:*')

源链接

Hacking more

...