show me she shell

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

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

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

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

1

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

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

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

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

@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的设置在配置文件中

<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的控制器中我们可以看到关于更换头像的函数

@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函数

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类来获取返回

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

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

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;
    }
}

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

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写入数据。

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

–>

redis set A A

但是有什么用呢?

让我们再回到题目代码

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

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

@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函数

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的定义方式)

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参数为

username=test&password=test&repassword=test

我们只要加入isadmin即可

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

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

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

@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中

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用了SpringAbstractBeanFactoryPointcutAdvisorhttps://github.com/mbechler/marshalsec

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

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

完整exp如下

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