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; } }
|
函数比较清楚,对图片链接的结尾做了判断,也很好绕过,我们可以用形似
就可以直接绕过判断了,这里还算比较明白,我们可以直接用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
|
–>
但是有什么用呢?
让我们再回到题目代码
在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