内容简介:/src/main/java/com/jeecms/cms/action/member/UeditorAct.java在接受了用户传递过来的url之后, 带入saveRemoteImage方法在saveRemoteImage方法当中, 如果通过了endWithImg方法的检测,就直接发起请求, 并且把请求到的结果输出到文件当中。
/src/main/java/com/jeecms/cms/action/member/UeditorAct.java
@RequestMapping(value = "/ueditor/getRemoteImage.jspx")
public void getRemoteImage(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String url = request.getParameter("upfile");
CmsSite site=CmsUtils.getSite(request);
JSONObject json = new JSONObject();
String[] arr = url.split(UE_SEPARATE_UE);
String[] outSrc = new String[arr.length];
for (int i = 0; i < arr.length; i++) {
outSrc[i]=saveRemoteImage(arr[i], site.getContextPath(), site.getUploadPath());
}
String outstr = "";
for (int i = 0; i < outSrc.length; i++) {
outstr += outSrc[i] + UE_SEPARATE_UE;
}
outstr = outstr.substring(0, outstr.lastIndexOf(UE_SEPARATE_UE));
json.put(URL, outstr);
json.put(SRC_URL, url);
json.put(TIP, LocalizedMessages.getRemoteImageSuccessSpecified(request));
ResponseUtils.renderJson(response, json.toString());
}
在接受了用户传递过来的url之后, 带入saveRemoteImage方法
private String saveRemoteImage(String imgUrl,String contextPath,String uploadPath){
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
CloseableHttpClient client = httpClientBuilder.build();
String outFileName="";
try{
if(endWithImg(imgUrl)){
HttpGet httpget = new HttpGet(new URI(imgUrl));
HttpResponse response = client.execute(httpget);
InputStream is = null;
OutputStream os = null;
HttpEntity entity = null;
entity = response.getEntity();
is = entity.getContent();
outFileName=UploadUtils.generateFilename(uploadPath, FileNameUtils.getFileSufix(imgUrl));
os = new FileOutputStream(realPathResolver.get(outFileName));
IOUtils.copy(is, os);
}
在saveRemoteImage方法当中, 如果通过了endWithImg方法的检测,就直接发起请求, 并且把请求到的结果输出到文件当中。
private 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;
}
}
endWithImg的检测比较简单, 绕过也比较简单加个?.jpg就可以绕过了。
不过本地测试时, 访问这个jpg文件的结果却是404.
首先来看看保存访问结果的文件的文件名生成方法, 是包含一个月份目录的。
public static String generateFilename(String path, String ext){
return path + MONTH_FORMAT.format(new Date())
+ RandomStringUtils.random(4, Num62.N36_CHARS) + "." + ext;
}
结果类似为 /u/cms/www/201902/15002619t400.jpg
而在jeecms的默认源码当中, 是不存在201902这个目录的。
并且在saveRemoteImage方法当中, 并没有”判断这个目录存不存在,如果不存在的话就创建该目录”这种逻辑。
在FileOutputStream时, 如果目录是不存在的话, 会出异常, 所以这里的文件并没有保存上。
要想保存上这个文件, 首先还是得创建这个目录。
在
@RequestMapping(value = "/ueditor/upload.jspx",method = RequestMethod.POST)
public void upload(
@RequestParam(value = "Type", required = false) String typeStr,
Boolean mark,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
responseInit(response);
if (Utils.isEmpty(typeStr)) {
typeStr = "File";
}
if(mark==null){
mark=false;
}
JSONObject json = new JSONObject();
JSONObject ob = validateUpload(request, typeStr);
if (ob == null) {
json = doUpload(request, typeStr, mark);
} else {
json = ob;
}
ResponseUtils.renderJson(response, json.toString());
}
直接查看调用的doUpload方法,
private JSONObject doUpload(HttpServletRequest request, String typeStr,Boolean mark)throws Exception {
.......
else {
fileUrl = fileRepository.storeByExt(site.getUploadPath(),
ext, uplFile);
}
继续查看storeByExt方法
public String storeByExt(String path, String ext, MultipartFile file)
throws IOException {
//String filename = UploadUtils.generateFilename(path, ext);
//File dest = new File(getRealPath(filename));
String fileName=UploadUtils.generateRamdonFilename(ext);
String fileUrl =path+fileName;
File dest = new File(getRealPath(path),fileName);
dest = UploadUtils.getUniqueFile(dest);
store(file, dest);
return fileUrl;
}
文件名和目录的生成方法和saveRemoteImage时使用的方法相同,然后调用了store方法。
private void store(MultipartFile file, File dest)throws IOException {
try {
UploadUtils.checkDirAndCreate(dest.getParentFile());
file.transferTo(dest);
} catch (IOException e) {
log.error("Transfer file error when upload file", e);
throw e;
}
}
public static void checkDirAndCreate(File dir){
if (!dir.exists())
dir.mkdirs();
}
可以看到虽然在下载远程图片的功能中, 没有”如果不存在这个日期目录就创建该目录”这个逻辑, 但是在上传的时候存在这个逻辑。 所以可以先通过上传, 创建了该目录之后, 再继续给SSRF利用。
上传这个功能, 需要登录之后才能正常使用。
因为在doupload方法之前,
JSONObject ob = validateUpload(request, typeStr);
if (ob == null) {
json = doUpload(request, typeStr, mark);
} else {
json = ob;
}
经过了validateUpload方法, 在该方法当中
CmsUser user = CmsUtils.getUser(request);
// 非允许的后缀
if (!user.isAllowSuffix(ext)) {
result.put(STATE, LocalizedMessages
.getInvalidFileSuffixSpecified(request));
return result;
}
如果是未登录状态, user为null 接下来就会出现空指针异常。
上传之后, 就成功创建了目录。
再SSRF
不过发起请求的httpClientBuilder, 仅支持HTTP/HTTPS协议。
SSTI
JEECMS中存在一些可以上传任意文件的点, 只举例一个
/src/main/java/com/jeecms/cms/action/member/SwfUploadAct.java
@RequestMapping(value = "/member/o_swfAttachsUpload.jspx", method = RequestMethod.POST)
public void swfAttachsUpload(
String root,
Integer uploadNum,
@RequestParam(value = "Filedata", required = false) MultipartFile file,
HttpServletRequest request, HttpServletResponse response,
ModelMap model) throws Exception{
super.swfAttachsUpload(root, uploadNum, file, request, response, model);
}
调用了父类的swfAttachsUpload方法,
protected void swfAttachsUpload(
String root,
Integer uploadNum,
@RequestParam(value = "Filedata", required = false) MultipartFile file,
HttpServletRequest request, HttpServletResponse response,
ModelMap model) throws Exception {
JSONObject data=new JSONObject();
WebCoreErrors errors = validateUpload( file, request);
if (errors.hasErrors()) {
data.put("error", errors.getErrors().get(0));
ResponseUtils.renderJson(response, data.toString());
}else{
CmsSite site = CmsUtils.getSite(request);
String ctx = request.getContextPath();
String origName = file.getOriginalFilename();
String ext = FilenameUtils.getExtension(origName).toLowerCase(
Locale.ENGLISH);
// TODO 检查允许上传的后缀
String fileUrl="";
try {
if (site.getConfig().getUploadToDb()) {
String dbFilePath = site.getConfig().getDbFileUri();
fileUrl = dbFileMng.storeByExt(site.getUploadPath(), ext, file
.getInputStream());
// 加上访问地址
fileUrl = request.getContextPath() + dbFilePath + fileUrl;
} else if (site.getUploadFtp() != null) {
Ftp ftp = site.getUploadFtp();
String ftpUrl = ftp.getUrl();
fileUrl = ftp.storeByExt(site.getUploadPath(), ext, file
.getInputStream());
// 加上url前缀
fileUrl = ftpUrl + fileUrl;
}else if (site.getUploadOss() != null) {
CmsOss oss = site.getUploadOss();
fileUrl = oss.storeByExt(site.getUploadPath(), ext, file.getInputStream());
} else {
fileUrl = fileRepository.storeByExt(site.getUploadPath(), ext,
file);
// 加上部署路径
fileUrl = ctx + fileUrl;
}
cmsUserMng.updateUploadSize(CmsUtils.getUserId(request), Integer.parseInt(String.valueOf(file.getSize()/1024)));
fileMng.saveFileByPath(fileUrl, origName, false);
model.addAttribute("attachmentPath", fileUrl);
} catch (IllegalStateException e) {
model.addAttribute("error", e.getMessage());
} catch (IOException e) {
model.addAttribute("error", e.getMessage());
}
data.put("attachUrl", fileUrl);
data.put("attachName", origName);
ResponseUtils.renderJson(response, data.toString());
}
}
在这个方法中, 上传时没有检查文件的后缀,
从TODO注释中也能看出来, 检查允许上传的后缀这个功能还未实现就直接上线了。
不过在jeecms中上传的jsp,jspx文件并不能被访问到。
<servlet-mapping> <servlet-name>JeeCmsFront</servlet-name> <url-pattern>*.jspx</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>JeeCmsFront</servlet-name> <url-pattern>*.jsp</url-pattern> </servlet-mapping>
jsp和jspx文件都经过了JeeCmsFront,
<servlet> <servlet-name>JeeCmsFront</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/config/jeecms-servlet-front.xml /WEB-INF/config/plug/**/*-servlet-front-action.xml </param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet>
jsp和jspx文件都会经过org.springframework.web.servlet.DispatcherServlet, 上传上去的jsp文件肯定是没有对应的映射的 就直接404了。
这里得结合一些其他的点进行利用,
/src/main/java/com/jeecms/cms/action/front/CsiCustomAct.java
@RequestMapping(value = "/csi_custom*.jspx")
public String custom(String tpl, HttpServletRequest request,
HttpServletResponse response, ModelMap model) {
log.debug("visit csi custom template: {}", tpl);
CmsSite site = CmsUtils.getSite(request);
if(StringUtils.isNotBlank(tpl)){
// 将request中所有参数保存至model中。
model.putAll(RequestUtils.getQueryParams(request));
FrontUtils.frontData(request, model, site);
FrontUtils.frontPageData(request, model);
return FrontUtils.getTplPath(site.getSolutionPath(), TPLDIR_CSI_CUSTOM,
tpl);
}else{
return FrontUtils.pageNotFound(request, response, model);
}
}
可以看到将用户传递过来的tpl变量直接带入了getTplPath方法,
public static String getTplPath(String solution, String dir, String name){
return solution + "/" + dir + "/" + name + TPL_SUFFIX;
}
可控的tpl变量直接拼接进了模板路径当中,
public static final String TPL_SUFFIX = ".html";
默认的模板后缀为.html, 高版本jdk当中已经不再能够截断, 所以这里先通过刚才的任意文件上传一个.html文件, 然后控制模板文件路径为自己上传的模板文件进行SSTI.
因为jeecms的模板引擎使用的是freemarker, 一开始以为直接用freemarker的SSTI就能rce了, 但是测试的时候失败了。
<#assignex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
在新版本freemarker中, 多了一个TemplateClassResolver.SAFER_RESOLVER配置。
TemplateClassResolver.SAFER_RESOLVER now disallows creating freemarker.template.utility.JythonRuntime and freemarker.template.utility.Execute. This change affects the behavior of the new built-in if FreeMarker was configured to use SAFER_RESOLVER, which is not the default until 2.4 and is hence improbable.
TemplateClassResolver SAFER_RESOLVER = new TemplateClassResolver() {
public Class resolve(String className, Environment env, Template template)throws TemplateException {
if (!className.equals(ObjectConstructor.class.getName()) && !className.equals(Execute.class.getName()) && !className.equals("freemarker.template.utility.JythonRuntime")) {
try {
return ClassUtil.forName(className);
} catch (ClassNotFoundException var5) {
throw new _MiscTemplateException(var5, env);
}
} else {
throw MessageUtil.newInstantiatingClassNotAllowedException(className, env);
}
}
}
如果使用了TemplateClassResolver.SAFER_RESOLVER, 就不允许再调用freemarker.template.utility.Execute, freemarker.template.utility.ObjectConstructor以及freemarker.template.utility.JythonRuntime。
public ConstructorFunction(String classname, Environment env, Template template)throws TemplateException {
this.env = env;
this.cl = env.getNewBuiltinClassResolver().resolve(classname, env, template);
if (!TemplateModel.class.isAssignableFrom(this.cl)) {
throw new _MiscTemplateException(NewBI.this, env, new Object[]{"Class ", this.cl.getName(), " does not implement freemarker.template.TemplateModel"});
}
并且允许调用的类只允许为实现了freemarker.template.TemplateModel接口的类, 大概看了下实现了该接口的类, 除了不允许使用的三个类,没有找到其他能利用的类, 就只有放弃RCE了。
从文档中可以看出, freemarker从2.4版本以后才默认打开TemplateClassResolver.SAFER_RESOLVER, jeecms使用的版本为
<freemarker.version>2.3.25-incubating</freemarker.version>
虽然没有默认打开该配置, 但是JEECMS中的freemarker手动打开了TemplateClassResolver.SAFER_RESOLVER,所以SSTI没办法RCE了。
protected void initApplicationContext()throws BeansException {
super.initApplicationContext();
if (getConfiguration() == null) {
FreeMarkerConfig config = autodetectConfiguration();
Configuration configuration=config.getConfiguration();
configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
setConfiguration(configuration);
}
checkTemplate();
}
在TemplateClassResolver.SAFER_RESOLVER的限制下, SSTI也就只能读读文件了, 并且只能读取WEB目录下的文件。
反序列
JEECMS中使用了shiro, 版本为
<shiro.version>1.4.0</shiro.version>
老版本shiro(1.2.4)曾爆过一个反序列,
看了一下maven下载的1.4.0的shiro包, 依然存在反序列的点
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext){
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}
return this.deserialize(bytes);
}
经过decrypt, aes解密之后就开始反序列了。
protected PrincipalCollection deserialize(byte[] serializedIdentity){
return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
}
public T deserialize(byte[] serialized)throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
高版本shiro只是没有在AbstractRememberMeManager中硬编码了AES的key, 但是在JEECMS当中, 又再次硬编码了AES的key
/src/main/webapp/WEB-INF/config/shiro-context.xml
<!-- rememberMe管理器 -->
<beanid="rememberMeManager"class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<propertyname="cipherKey"value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
<propertyname="cookie"ref="rememberMeCookie"/>
</bean>
直接使用这个AES key就能打反序列了。
看了下JEECMS的jar包, 打反序列版本比较合适的为C3P0的jar包。
JEECMS的C3P0包版本和ysoserial自带的C3P0包版本相同。
<c3p0.version>0.9.5.2</c3p0.version>
一开始不知道C3P0这gadget到底是咋用, 看了下代码。
/com/mchange/c3p0/0.9.5.2/c3p0-0.9.5.2.jar!/com/mchange/v2/c3p0/impl/PoolBackedDataSourceBase.class
private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch(version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}
继续调用getObject方法
public Object getObject()throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}
Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName);
}
return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
调用referenceToObject方法,
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3)throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}
Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}
Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
通过URLClassLoader获取远程jar包中的类, 然后classforname后, newInstance实例化该类, 调用构造方法。
不过在打反序列的时候, 出现了suid错误
明明yso的C3P0版本和jeecms的一样, 但是还是提示suid错误。
因为jeecms中依赖了quartz-scheduler包, 这个包又依赖了0.9.1.1的c3p0. 反序列的时候调用的是老版本的C3P0的包。(这里我也不太懂我本地为什么调用的是老版本的包, 按理maven解决依赖冲突时 优先最短路径优先, 应该调用的是0.9.5.2包。并且高版本的C3P0依赖在前,有大哥懂为啥调用老版本的jar包的麻烦教我一手。)
这时候ysoserial的C3P0版本和jeecms的版本就不相同了 suid就不同了, 这里直接修改一下ysoserial的C3P0版本,
text变量的字符串为ysoserial生成的C3P0 payload base64编码,
References
1. https://freemarker.apache.org/docs/versions_2_3_19.html
2. https://portswigger.net/blog/server-side-template-injection
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Cyberwar
Kathleen Hall Jamieson / Oxford University Press / 2018-10-3 / USD 16.96
The question of how Donald Trump won the 2016 election looms over his presidency. In particular, were the 78,000 voters who gave him an Electoral College victory affected by the Russian trolls and hac......一起来看看 《Cyberwar》 这本书的介绍吧!