zoukankan      html  css  js  c++  java
  • java 文件读取

    show me she shell

    这是一道tomato师傅出的不完整的java题,java…,java…我恨java┑( ̄Д  ̄)┍

    这是一个题目一是列目录+任意文件读取,

    二是垂直越权+CLRF配SSRF打redis+反序列化命令执行

    题目的难度在于代码本身的不完整和java,没办法实际测试,所以只能强行阅读源码,幸运的是代码结构是spring完成的,和python的flask/django结构很强,这为我们阅读源码提供了可能。

    1

    整个代码中,控制器只有5个,其中

    1
    2
    3
    4
    5
    index 首页
    login 登陆、注册
    manager 管理员管理
    post 用户发送post
    user 用户功能,包括上传头像和删除自己发送的post

    entity是python中类似于model的定义,其中包括了User、Post

    interceptor主要负责路由以及权限设置,核心代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    public boolean preHandle(HttpServletRequest request,
    HttpServletResponse response, Object handler) throws Exception {

    String requestUri = request.getRequestURI();
    for (String s : excludedUrls) {
    if (requestUri.endsWith(s)) {
    return true;
    }
    }
    User user = (User) request.getSession().getAttribute("user");
    if(user == null){
    request.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(request, response);
    return false;
    }else{
    return true;
    }
    }

    通过request.getRequestURL获取连接,其中后缀在excludedUrls的不需要登陆,其他都需要登陆才能访问。

    关于excludedUrls的设置在配置文件中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <mvc:interceptors>
    <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <bean class="com.tctf.interceptor.AuthInterceptor">
    <property name="excludedUrls">
    <list>
    <value>/register.do</value>
    <value>/login.do</value>
    <value>/doregister.do</value>
    </list>
    </property>
    </bean>
    </mvc:interceptor>
    </mvc:interceptors>

    mapper其中包含了部分核心函数,但只有函数定义,没有代码

    service中包含了关于user操作和post操作的核心函数

    utiles是一些其余的核心函数

    第一个漏洞点其实比较容易发现,在user的控制器中我们可以看到关于更换头像的函数

    1
    2
    3
    4
    5
    6
    7
    8
    @RequestMapping(value = "/headimg.do",method = RequestMethod.GET)
    public void UpdateHead(@RequestParam("url")String url){
    String downloadPath = request.getSession().getServletContext().getRealPath("/")+"/headimg/";
    String headurl = "/headimg/"+ HttpReq.Download(url,downloadPath);
    User user = (User) session.getAttribute("user");
    Integer uid = user.getId();
    userMapper.UpdateHeadurl(headurl,uid);
    }

    关于获取头像的地方调用了HttpReq.Download函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public static String Download(String urlString,String path){
    String filename = "default.jpg";
    if(endWithImg(urlString)) {
    try {
    URL url = new URL(urlString);
    URLConnection urlConnection = url.openConnection();
    urlConnection.setReadTimeout(5*1000);
    InputStream is = urlConnection.getInputStream();
    byte[] bs = new byte[1024];
    int len;
    filename = generateRamdonFilename(getFileSufix(urlString));
    String outfilename = path + filename;
    OutputStream os = new FileOutputStream(outfilename);
    while ((len = is.read(bs)) != -1) {
    os.write(bs, 0, len);
    }
    os.close();
    is.close();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return filename;
    }

    这里调用URL类来获取返回

    1
    2
    URL url = new URL(urlString);
    URLConnection urlConnection = url.openConnection();

    但这之前我们需要绕过endWithImg的判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private static boolean endWithImg(String imgUrl){
    if(StringUtils.isNotBlank(imgUrl)&&(imgUrl.endsWith(".bmp")||imgUrl.endsWith(".gif")
    ||imgUrl.endsWith(".jpeg")||imgUrl.endsWith(".jpg")
    ||imgUrl.endsWith(".png"))){
    return true;
    }else{
    return false;
    }
    }

    函数比较清楚,对图片链接的结尾做了判断,也很好绕过,我们可以用形似

    1
    http://11111/111.php?a=1.jpg

    就可以直接绕过判断了,这里还算比较明白,我们可以直接用file协议去读本地文件,形似file:///etc/passwd?a=1.jpg就可以获取文件内容了。

    唯一的问题是,我们如何找到flag位置了,这就涉及到一个小trick了

    在java中,我们可以用file:///或netdoc:///来列目录

    通过这种方式,我们可以获取到服务器上的第一个flag

    2

    当然这里的第一题是当时的非预期,因为这种列目录方式只在java中才有,我们回到题目继续分析。

    在第一题中我们找到了一个SSRF漏洞,在第二题中,修复了headimg使用file协议读文件的漏洞,但我们可以用CRLF向Redis写入数据。

    1
    headimg.do?url=http://127.0.0.1%0a%0dSET%20A%20A:6379

    –>

    1
    redis set A A

    但是有什么用呢?

    让我们再回到题目代码

    在managercontroller中,我们可以发现所有关于redis的操作都在这里,但这里有一个限制是要求当前用户的isadmin必须为1,但整个代码中并没有任何关于这部分的操作,所以我们顺着回顾代码中可能接触到设置isadmin的位置。

    跟入注册代码controller.LoginController中,关于注册的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RequestMapping(value = "/doregister.do",method = RequestMethod.POST)
    public String DoRegister(User user, String repassword, Model model){
    String result = userService.register(user,repassword);
    if(result.equals("ok")){
    return "login";
    }else{
    model.addAttribute("message",result);
    return "register";
    }
    }

    @RequestMapping(value = "/register.do",method = RequestMethod.GET)
    public String Register(){
    return "register";
    }

    跟入userService.register函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public String register(User user,String repassword) {
    String username = user.getUsername();
    String password = user.getPassword();

    if(StringUtils.isBlank(username.trim()) || StringUtils.isBlank(password.trim())){
    return "You need set username and password";
    }

    int uid = userMapper.SelectIdByUsername(username);

    if(uid>0){
    return "This username has been registered!";
    }

    if(!password.equals(repassword)){
    return "repassword";
    }

    userMapper.InsertUser(user);

    return "ok";
    }

    仔细观察我们可以发现,虽然函数中从user中获取了username和password并进入userMapper.SelectIdByUsername验证,但在插入数据的时候仍然直接传入了user类。

    这里我们看看user类的定义(这应该是类似于python中model的定义方式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class User{
    private Integer id;
    private String username;
    private String password;
    private String headurl;
    private Boolean isadmin;

    public User(Integer id, String username, String password, String headurl, Boolean isadmin) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.headurl = headurl;
    this.isadmin = isadmin;
    }
    ...

    我们可以注意到这个函数在初始化时接受了isadmin,而在控制器中路由接收到这个参数时也没有做任何的处理,所以这里存在AutoBuilding漏洞

    当我们在注册的时候,原post参数为

    1
    username=test&password=test&repassword=test

    我们只要加入isadmin即可

    1
    username=test&password=test&repassword=test&isadmin=1

    我们成功给当前用户加入了管理员权限

    在获得了manager权限后,我们就可以执行manager控制器下的操作了,让我们来看看代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @RequestMapping(value = "/audit.do")
    public String AuditPost(@RequestParam("pid") Integer pid,HttpSession session) {
    User user = (User) session.getAttribute("user");
    try {
    if (user.getIsadmin()) {
    postMapper.AuditPost(pid);
    Post post = postMapper.GetOne(pid);
    redisClient.set(pid,post);
    return "manager";
    }
    }catch (Exception e){
    return "redirect:/";
    }
    return "redirect:/";
    }

    @RequestMapping(value = "/check.do")
    public String CheckPost(@RequestParam("pid") Integer pid, HttpSession session, Model model){
    User user = (User) session.getAttribute("user");
    try {
    if (user.getIsadmin()) {
    Post post = redisClient.getObject(pid);
    model.addAttribute("post", post);
    return "manager";
    }
    }catch(Exception e){
    return "redirect:/";
    }
    return "redirect:/";
    }

    这其中有一个特殊的操作就是对于redis的操作,关于redis的代码在utils.RedisClient中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public <T> void set(Integer id, T t) {
    byte[] key = getKey(id);
    RedisSerializer serializer = redisTemplate.getValueSerializer();
    byte[] val = serializer.serialize(t);
    getConnection().set(key, val);

    }

    public <T> T getObject(Integer id) {
    byte[] key = getKey(id);
    byte[] result = getConnection().get(key);
    return (T) redisTemplate.getValueSerializer().deserialize(result);
    }

    很明显其中的getObject函数有反序列化的操作,如果我们想要通过反序列化来构造RCE的话,我们需要一个gadget.

    这里tomato用了SpringAbstractBeanFactoryPointcutAdvisor
    https://github.com/mbechler/marshalsec

    这下思路就非常清晰了,整个利用链如下

    注册->使用AutoBuilding越权登陆->使用headimg的ssrf配合crlf向redis中写入序列化数据->check.do反序列化->RCE

    完整exp如下

    https://gist.github.com/Tom4t0/97708be968cc3623c74ef860ae031574

  • 相关阅读:
    CloudNotes之桌面客户端篇:将笔记发布到博客园
    CloudNotes之桌面客户端篇:笔记撰写样式的支持
    Wizard Framework:一个自己开发的基于Windows Forms的向导开发框架
    CloudNotes之桌面客户端篇:插件系统的实现
    CloudNotes之领域建模篇:领域模型简介
    CloudNotes云端个人笔记系统系列文章汇总
    将Json数据转换为ADO.NET DataSet对象
    CloudNotes之桌面客户端篇:增强的笔记列表
    CloudNotes:一个云端个人笔记系统
    一种通用查询语言的定义与实践
  • 原文地址:https://www.cnblogs.com/hackering/p/14260204.html
Copyright © 2011-2022 走看看