1. 입력데이터 검증 및 표현
프로그램 입력값에 대한 검증 누락 또는 부적절한 검증, 데이터의 잘못된 형식 지정, 일관되지 않은 언어셋 사용 등으로 인해 발생하는 보안 약점으로 SQL 삽입, 크로스사이트 스크립트(XSS) 등의 공격을 유발할 수 있다.
1) 입력값 검증이 필요한 이유
프로그램은 입력, 처리, 출력이 순서로 반복되는 것이다.
프로그램을 안전하게 만들기 위해서는 처리 단계에서 안전한 처리가 이루어져야 한다. 안전한 처리란 개발자가 의도한 대로 동작해야 안전한 처리가 이루어진다.
처리만 개발자의 의도대로 동작한다고 안전해지는 것은 아니다. 입력 또한 신뢰할 수 있는 입력을 받아야 한다. 믿을 수 있는 시스템 내부의 값 같은 안전한 곳으로부터 전달된 입력은 신뢰할 수 있는 입력이지만, 믿을 수 없는 사용자가 입력한 값(사용자가 잘못 입력하거나 전달 과정에서 변조될 수 있음) 같은 안전하지 않은 곳으로부터 전달된 입력은 신뢰할 수 없는 입력이다.
안전한 처리를 하기 위해서는 신뢰할 수 있는 입력값을 사용하고, 신뢰할 수 없는 입력값을 사용해야 하는 경우, 입력값을 검증, 제한해서 사용해야 한다.
2. 인젝션(Injection, 삽입) 취약점
어떤 처리가 있을 때, 입력값에 처리를 조작할 수 있는 문자열 포함 여부를 확인하지 않고 처리에 사용하는 경우
처리의 구조와 의미가 변형되어 원래 의도했던 처리와 다르게 처리되는 문제점
1) 유형
- SQL Injection
- 입력값이 SQL 문을 만들고 실행하는데 사용
- XPath Injection
- 입력값이 XPath 구문을 만들고 실행하는데 사용
- XQuery Injection
- 입력값이 XQuery 구문을 만들고 실행하는데 사용
- Command Injection
- 입력값이 운영체제 명령어 또는 명령어의 일부로 사용
2) 방어
- 입력값에 처리를 조작하는 문자열 포함 여부를 확인하고 사용 - 입력값 검증, 제한
- 입력값에 처리를 조작하는 문자열이 포함된 경우
- 오류 처리
- 제거하고 사용
- 처리를 조작하는 문자열을 일반 문자열로 해석되도록 변경해서 사용 - 이스케이프 처리
- 각 기능에서 제공하는 안전한 방법을 사용
- 구조를 정의하고 정의된 구조에 입력값을 검증된 기능을 통해서 대입하는 방식으로 구현
- 구조화된 쿼리 실행, 파라미터화된 쿼리 실행
제거하고 사용하는 것은 문제가 될 수 있다. 사용자가 어떤 영향을 미치는지 모르고 실제 그 문장을 찾고자 입력했을 수도 있다 제거하고 사용하는 것은 사용자의 의도를 무시하는 수가 있다. 사용자가 100% 공격자는 아니다.
3. SQL Injection
외부 입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고, 쿼리문(SQL문)을 생성, 실행하는데 사용하는 경우
원래 의도했던 쿼리의 구조와 내용이 변경되어 실행되는 것
1) 예상되는 문제점
- 권한 밖 DB 데이터에 접근이 가능
- 쿼리 실행을 통해서 제공되는 기능을 비정상적으로 제공받는 것이 가능
- 해당 기능을 우회해서 이용하는 것이 가능
2) 예시
(1) 정상적인 입력인 경우 동작
login.do?id=abc&pw=xyz
ID : abc -----------------------------> select * from users where id = 'abc' and pw = 'xzy'
PW: xyz
- 일치하는 정보가 있는 경우 로그인에 성공하고 메인 페이지를 반환
- 일치하는 정보가 없는 경우 오류 메시지와 함께 로그인 페이지를 반환
ID 입력을 abc, PW 입력을 xyz로 했을 때 GET 방식으로 보면 위와 같이 전달되고, 서버에서 확인할 때 SQL 문이 위와 같이 생성되어 확인한다.
(2) 비정상적인 입력인 경우 동작
login.do?id=abc&pw=xyz' or 'a' = 'a
ID: abc -----------------------------------------> select * from users where id = 'abc' and pw = 'xzy' or 'a' = 'a'
PW: xyz' or 'a' = 'a
- 원래는 users 테이블에 id, pw 컬럼의 값이 일치하는 것이 있는지 조회하는 쿼리
- 조작된 입력값에 의해 항상 참인 쿼리로 변경되어서 실행
일치하는 정보가 있는 것으로 판단해서 메인 페이지를 반환하게 된다.
(3) 데이터베이스가 동작하는 서버의 제어권을 탈취해 원격에서 해당 서버를 제어하는 것이 가능
OWASP TOP10
OWASPTOP10은 웹 애플리케이션에서 가장 빈번하고 많이 발생하는 취약점들을 1~10위로 정리해서 발표하는 것이다.
위 링크를 보면 2017년에서 Injection이 1등이었고, 이전에도 항상 상위권을 지키고 있다.
3) 방어 기법
파이썬에서 SQL Injection을 방어하는 방법
- 입력값에 쿼리 조작 문자열 포함 여부를 확인하고 사용
- 오류 처리
- 제거하고 사용
- 안전한 형태로 변경해서 사용
- PreparedStatement(Query Parameters)와 같은 구조화된 쿼리 실행(파라미터화된 쿼리 실행)을 보장하는 것을 사용
- 오류 메시지에 상세한 내용(데이터베이스 및 쿼리 구조, 쿼리 실행과 관련한 프로그램 구조 등)이 포함되지 않도록 처리
- 애플리케이션에서 사용하는 DB 사용자의 권한을 필요한 만큼의 최소한으로 부여
밑에 두 개는 SQL Injection 공격을 완화하기 위해서 필요한 방어 기법
4. WebGoat 실습
Injection Flaws > String SQL Injection
- 입력한 사용자 이름과 일치하는 계좌 정보를 반환해 주는 웹 페이지
- 목표는 모든 사용자의 계좌 정보가 출력되게 하는 것
1) 정상적인 입력
Smith를 입력하면 나오는 화면이다. 정상적으로 출력이 되는데, 내부적으로 어떻게 처리가 되는지, 화면에서 입력된 값이 서버로 어떻게 전달되고 어떻게 사용되는지 유추해야 한다.
2) 개발자 도구를 이용해 서버로 전달되는 내용 분석
개발자 도구를 이용하면 action을 통해 어디로 가는지, method를 통해 어떤 방식으로 전달하는지, 어떤 내용들을 전달하는지 알 수 있다.
위 소스 코드는 POST 방식이므로 요청 본문에 들어가야 하는데 편의상 GET 방식으로 유추한다.
attack?Screen=34&menu=1100&account_name=Smith&SUBMIT=Go!
Smith가 어디에 들어가는지 봐야 한다.
3) 서버에서 처리 유추
요청 파라미터로 전달된 값이 어떻게 사용되는지 유추
특정 테이블에 데이터를 조회하는 쿼리를 만들고 실행하는 데 사용할 것이라고 유추한다.
WebGoat에서 준 힌트로 유추할 수 있다. SELECT * FROM user_data WHERE last_name = 'Smith'
4) 모든 사용자 데이터를 조회하는 쿼리 생성
입력값이 서버에서 검증 없이 그대로 쿼리를 만드는 데 사용된다면 모든 사용자 데이터를 조회하는 쿼리를 어떻게 만들어야 하는지 생각해야 한다.
SELECT * FROM user_data WHERE last_name = 'Smith' or 'a' = 'a'
위와 같이 입력하면 항상 참이 되는 조건을 추가하는 것이다. 모든 데이터를 조회할 때는 항상 참이 되도록 만들어 주면 된다.
5) 쿼리 조작 문자열 포함해서 요청
외부 입력값을 쿼리 조작 문자열(' = or) 포함 여부를 확인하지 않고 그대로 쿼리 생성 및 실행에 사용해서 쿼리의 원래 의미(이름이 일치하는 데이터를 조회해서 반환)를 변경(모든 데이터를 조회)해서 실행
5. Numeric SQL Injection
Injection Flaws > Numeric SQL Injection
1) 정상적인 동작
- Columbia를 선택하면 101이 station 파라미터 값으로 전달
2) 개발자 도구 이용해서 소스 코드 분석
- <select name="station"><option value="101">Columbia</option>
3) 서버로 전달되는 값(GET 방식으로) 유추
- attack?Screen=44&menu=1100&station=101&SUBMIT=Go!
4) 서버에서 요청으로 전달된 값을 쿼리를 생성하고 실행하는 데 사용(힌트)
- SELECT * FROM weather_data WHERE station = 101
5) 모든 데이트를 조회하는 쿼리
- SELECT * FROM weather_data WHERE station = 101 or 1 = 1
항상 참이 되는 조건을 추가하는데 원래 쿼리의 형태를 고려해서 추가해야 한다.
6) 방법
(1) 개발자 도구를 이용
- 서버로 전달되는 값을 조작
입력창이 아니므로 값을 직접 입력하는 것이 불가능하므로 개발자 도구를 이용해 Columbia가 선택되었을 때 서버로 전달되는 값을 조작한다.
(2) Proxy 이용
- Intercept 설정
- 요청 발생
- 인터셉터 된 요청의 내용(요청 파라미터의 값)을 변조한 후 인터셉터를 해제
변조된 요청이 서버로 전달
7) 모든 데이터 조회 확인
6. 입력값 검증의 중요성
인젝션을 통해 조회된 결과의 지역과 지역 선택 창에 나오는 지역이 다르다. 전체 지역은 6개지만, 서비스로 제공하는 지역은 4개만 제공하기 때문이다.
1) 입력창(text, textarea)과, 라디오 버튼, 체크 박스, 셀렉트 박스의 차이
- 입력창
- 사용자가 자유롭게 입력
- 라디오 버튼, 체크 박스, 셀렉트 박스
- 시스템에서 제공하는 범위 내에서 선택하도록 제한
2) 문제점
서비스로 제공하는 4개 지역만 선택하도록 제한하기 위해서 셀렉트 박스를 사용했지만, 서버에서는 해당 범위의 값이 전달되었는지 확인해야 하나 하지 않았기 때문에 문제가 발생
3) 보완
클라이언트 사이드에 적용된 보안 기능은 서버 사이드에도 동일하게 또는 더 높은 보안 기능을 적용해야 함
7. SQL 인젝션 유형
select * from members where id = ___
- 사용자가 입력한 ID와 일치하는 회원 정보를 조회해서 제공
이러한 구조를 가졌을 때 인젝션이 발생하는 입력의 유형을 본다.
1) 에러를 유발하는 입력
Error Based SQL Injection
- 입력값으로 에러를 유발하는 값을 전달
- 생성된 에러 메시지를 통해서 정보를 수집하고 수집한 정보를 이용해서 추가 공격을 계획
(1) 예시
select * from members where id = 123'
- ID 컬럼은 숫자 형으로 문자열 데이터를 받을 수 없고, 작은따옴표의 개수가 일치하지 않아서 오류 발생
2) 항상 참이 되는 입력
- 쿼리문의 조건식의 결과가 항상 참이 되게 만드는 입력
- 권한 밖의 데이터에 접근, 조회하는 것이 가능
- 모든 데이터 조회 가능
(1) 예시
select * from members where id = 123 or 1 = 1
- 입력값이 항상 참
- 인젝션이 걸리는 컬럼의 데이터 타입에 맞춰 작은따옴표를 추가 해야 함
- members 테이블의 모든 데이터 조회 가능
3) UNION 구문을 이용
Union Based SQL Injection
- 원래 서비스를 통해서 실행되는 쿼리에 공격자가 알고자 하는 정보를 조회하는 쿼리를 UNION 구문을 이용해서 결합하여 실행
- UNION 구문은 두 쿼리의 실행 결과를 하나로 결합함
- 공격자가 알고자 하는 정보가 함께 출력
(1) 예시
select * from members id = 123 정상 입력으로 123 회원의 데이터가 조회되어 출력
select * from members id = 123 and 1 = 2 UNION select 1, 2, 3, 4 from 어떤테이블
- 1, 2, 3, 4가 출력
- and 1 = 2는 정상 쿼리의 실행 결과가 없도록 만드는 구문
- UNION 구문은 공격자가 알고자 하는 정보를 조회하는 쿼리
- 시스템에 어떤 쿼리가 있는지 알 수 없으므로 시스템 테이블을 우선적으로 활용
4) Stored Procedure를 호출하는 입력
- 데이터베이스에서 제공하는 Stored Procedure를 실행하는 구문을 입력
- 데이터베이스의 제어권 탈취 가능
(1) 예시
select * from members id = 123 ; exec xp_cmdshell 'cmd.exe /c dir'
- ;는 쿼리문의 종결을 의미
- exec는 Stored Procedure를 실행
- 일반적으로 시스템 Stored Procedure를 우선적으로 활용
- xp_cmdshell은 MS-SQL에서 제공하는 시스템 Stored Procedure로 매개변수로 전달된 값을 DBMS의 쉘에서 실행하고 그 결과를 반환
- DBMS의 쉘에서 실행할 명령어
5) Blind SQL Injection
- 쿼리 실행 결과에 따라서 서버의 반응이 달라지는 경우
- 공격자가 원하는 내용을 조회하는 쿼리를 작성해서 전달하고 실행 결과를 보면서 정보를 수집
(1) 예시
[정상적인 실행]
select * from members where id = 123
⇒ 존재하는 ID인 경우 → ID가 123인 사용자의 정보를 제공
select * from members where id = 999
⇒ 존재하지 않는 ID인 경우 → 존재하지 않습니다. 메시지를 제공
[공격 가능 여부를 확인]
select * from members where id = 123 and 1 = 1 ⇒ ID가 123인 사용자의 정보를 제공
select * from members where id = 123 and 1 = 2 ⇒ 존재하지 않습니다. 메시지를 제공
[공격자가 알고자 하는 정보를 조회하는 쿼리를 전달]
select * from members where id = 123 and 공격자가 알고자 하는 정보를 조회하는 쿼리
- 공격 가능 여부를 확인할 때 조건에 따라 결과 화면이 달라짐
- 사용자 정보가 출력됨, 해당 쿼리가 참이고, 오류 메시지가 출력되면 해당 쿼리가 거짓인 것을 알 수 있음
8. Blind Numeric SQL Injection 실습
Injection Flaws > Blind Numeric SQL Injection
- 사용자가 입력한 계좌 번호의 유효성(있다, 없다)을 확인해 주는 웹 페이지
- 해당 계좌가 존재하는 경우, Account number is valid. 를 출력
- 목표는 pins 테이블에서 cc_number 컬럼의 값이 1111222233334444와 일치하는 pin 컬럼의 값을 찾는 것
1) 정상적인 동작
- 해당 계좌가 존재하는 경우, Account number is valid. 를 출력
- 해당 계좌가 존재하지 않는 경우, Invalid account number. 를 출력
2) 개발자 도구 이용해서 소스 코드 분석
- <input name="account_number" type="TEXT" value="999">
3) 서버로 전달되는 값(GET 방식으로) 유추
- attack?Screen=35&menu=1100&account_number=999&SUBMIT=Go!
4) 서버에서 요청으로 전달된 값을 쿼리를 생성하고 실행하는 데 사용(힌트를 이용 추측)
- select * from accounts where account_number = 999
5) 공격자가 알고자 하는 정보를 조회 하는 쿼리 (문제 힌트 이용)
- select pin from pins where cc_number = '1111222233334444'
6) 원래 서비스 쿼리에 추가
- 101 and (select pin from pins where cc_number='1111222233334444') > 2000
- 참과 거짓을 판단할 수 있도록 >을 넣어준다
102 and (select pin from pins where cc_number='1111222233334444') > 1000
조건을 만족하는 pin 값은 1000보다 크다
102 and (select pin from pins where cc_number='1111222233334444') > 5000
조건을 만족하는 pin 값은 1000보다 크고 5000보다 작음
102 and (select pin from pins where cc_number='1111222233334444') = ????
범위를 점점 줄여서 최종적으로 아래 쿼리를 만족하는 숫자를 찾음
102 and (select pin from pins where cc_number='1111222233334444') = 2364
Account number is valid가 출력
7) Burp Suite 사용
- 위에서 실습한 결과를 가지고 사용 방법을 익힌다.
- Brute Force(브루트 포스)를 사용할 수 있다.
- 여기서는 범위를 알기 때문에 Numbers를 사용해 사용법을 익힌다.
(1) intercept on
(2) Go! 눌러서 intercept
(3) account_number 매개변수에 오른쪽 클릭
- send to intruder 클릭
- intruder에 색이 변함
(4) intruder 확인
- account_number에 문자가 들어가 있는데 여기에 공격한다는 의미
(5) Payloads 설정
- Numbers 타입 설정
- 2350부터 2380까지 1씩 증가 하는 숫자 범위 설정 후 Start attack
(6) attack 확인
- account_number에 2364가 들어갔을 때만 Length가 다름
위, 아래 값들은 Invalid account number.의 값이 들어가는 것이고, 2364는 다른 값이 들어가는 것을 확인할 수 있다.
(7) WebGoat 입력
9. Blind String SQL Injection
Injection Flaws > Blind String SQL Injection
- 목표는 pins 테이블에서 cc_number 컬럼의 값이 4321432143214321과 일치하는 name 컬럼의 값을 찾은 것
- name 컬럼은 문자열 타입의 데이터를 저장하는 컬럼
- 힌트 field name, table pins, row cc_number
1) 쿼리문 유추
이전 문제와 비슷한 문제로 일치하는 결과가 있는 경우와 일치하는 결과가 없는 경우로 나누어야 한다.
이전 문제는 숫자로 나누는 것이지만, 이번 문제는 문자로 나누는 것이다.
SELECT * FROM accounts WHEREaccount_number = 102 and (SELECT name FROM pins WHERE cc_number = '4321432143214321') = ???
이렇게 했을 때, ?에 맞는 문자열이 온다면 102도 있는 값이고 and 뒤 쿼리문도 맞는 값이 되어 Account number is valid.가 나올 것이다. 여기서도 Burte Force로 공격한다면 엄청난 경우의 수가 나오기 때문에 그렇게 할 수는 없을 것이다.
2) 한 글자씩 추출
- substr()
- substr은 문자열 자르기로 substr(문자열, 시작 위치, 길이)로 사용하는 함수
- 문자열 : 원하는 문자열(대상 컬럼)
- 길이 : 시작 위치부터 마지막 위치
select * from accounts where account_number = 102 and (select substr(name, 1, 1) from pins where cc_number = '4321432143214321') = '?'
select * from accounts where account_number = 102 and (select substr(name, 2, 1) from pins where cc_number = '4321432143214321') = '?'
해당 컬럼에 문자를 하나씩 가져와서 ?에 있는 것과 맞는지 확인하는 것이다. 하지만 딱 맞는 알파벳을 찾기란 쉽지 않다.
3) 이름 데이터의 각 자리를 아스키코드로 만들어서 범위 연산 수행
문제 힌트에도 나왔듯이 문자를 숫자로 바꾸는 생각을 해야 한다. 문자를 숫자로 바꾸는 것은 ASCII 코드를 사용해 이전 문제와 같이 범위 연산을 하면 맞는 알파벳을 찾아가기에 효율성을 높일 수 있다.
select * from accounts where account_number = 102 and (select ascii(substr(name, 1, 1)) from pins where cc_number = '4321432143214321') < 46
4) 찾는 값
위 범위 연산을 해서 최종으로 찾는 값은 Jill이 된다.
select * from accounts where account_number = 102 and (select name from pins where cc_number = '4321432143214321') = 'Jill'
이 코드를 입력하면 Account number is valid가 나오는 것을 볼 수 있다.
10. 취약한 소스코드 확인
Ctrl + Shift + R (Open Resource, 열려 있는 프로젝트에서 특정 패턴의 파일을 검색해서 열어주는 도구)
이클립스에서 파일을 찾는다.
Ctrl + Shift + F를 눌러 자동으로 코드 내용을 문법 템플릿에 맞게 포맷팅(들여쓰기) 한다.
69번 줄 protected Element createContent(WebSession s) 함수 안에 있는 문구 중 77번 줄, 84번 줄, 106~108줄을 본다.
1) 77번 줄
String accountNumber = s.getParser().getRawParameter(ACCT_NUM, "101");
- 요청 파라미터 중 ACCT_NUM 파라미터의 값을 가져와서 반환
- 만약 파라미터 또는 파라미터의 값이 없는 경우 101을 반한
2) 문제점
외부 입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고 문자열 결합 방식의 쿼리문 생성하고 있음
외부 입력값에 의해 쿼리의 구조와 의미가 변형될 수 있음
(1) 84번 줄
String query = "SELECT * FROM user_data WHERE userid = " + accountNumber;
- 문자열 결합 방식으로 쿼리를 생성
(2) 106번 줄
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet results = statement.executeQuery(query);
- Statement 구문을 통해서 만들어진 쿼리를 그대로 실행
위 80번, 87번에서 쿼리 조작 문자열 포함 여부를 검증하지 않고 문자열 결합 방식으로 쿼리를 생성하면서 문제가 있었는데, 쿼리를 실행하는 부분에서도 만들어진 쿼리를 그대로 실행해서 문제가 발생한다.
4) Java Statement 객체
(1) Statement
- 만들어진 문자열 형태의 쿼리를 그대로 전달해서 실행
- 쿼리 생성 책임이 개발자에게 있음
(2) PreparedStatement
- 미리 정의한 쿼리 구조에 맞춰서 쿼리를 생성해서 DB로 전달해서 실행
- 쿼리 구조 정의는 개발자가 하고, 쿼리 생성은 해당 객체가 책임지고 수행
(3) CallableStatement
- DB에 정의되어 있는 Stored Procedure를 호출할 때 사용
5) 취약한 소스 코드 안전하게 변경
(1) 84번 줄
String query = "SELECT * FROM user_data WHERE userid = " + accountNumber;
- PreparedStatement 객체를 이용해서 미리 정의한 구조로 쿼리가 실행되는 것을 보장
- 구조화된 쿼리 실행 또는 파라미터화된 쿼리 실행
String query = "SELECT * FROM user_data WHERE userid = ? ";
- 쿼리의 구조를 정의
- 변수 부분을 ?로 표시 (데이터 타입을 고려하지 않음 = 따옴표를 포함하지 않음)
userid 문자열이면 따옴표가 있어야 하지만 PreparedStatement에서 쿼리를 정의할 때는 숫자인지, 문자열인지 구분하지 않아서 변수가 들어가는 부분에 물음표를 쓴다.
(2) 106번 줄
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
- PreparedStatement 객체 생성
- connection.prepareStatement() 메서드를 이용해서 생성
- 매매 변수의 값으로 쿼리 구조 전달
PreparedStatement statement = connection.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
- 객체 생성 시 쿼리 구조를 미리 정의
- Ctrl + Shift + O 눌러서 PreparedStatement import
ResultSet results = statement.executeQuery(query);
- 쿼리 실행에 필요한 변수를 성정하고 쿼리를 실행
- 변숫값이 할당되는 컬럼의 데이터 타입에 맞는 메서드를 사용해야 함
- 쿼리 실행 메서드에 쿼리문을 전달하지 않아야 함
statement.setInt(1, Integer.parseInt(accountNumber));
ResultSet results = statement.executeQuery();
- 데이터 타입에 맞춰서 해당하는 메서드를 선택(int)
- 변수의 값을 할당(PreparedStatement는 인덱스가 1부터 시작)
- 공격 문자가 포함되면 숫자 변환 시 오류가 발생
- 쿼리 구문이 PreparedStatement에 이미 정의되어 있으므로 executeQuery() 메서드에 쿼리문을 전달하지 않음
'SK Shieldus Rookies 19th > 애플리케이션 보안' 카테고리의 다른 글
[SK shieldus Rookies 19기][애플리케이션 보안] - WebGoat, Bee box, Command Injection (0) | 2024.03.29 |
---|---|
[SK shieldus Rookies 19기][애플리케이션 보안] - WebGoat, Bee box, SQL Injection (0) | 2024.03.27 |
[SK shieldus Rookies 19기][애플리케이션 보안] - WebGoat, Burp Suite (0) | 2024.03.24 |
[SK shieldus Rookies 19기][애플리케이션 보안] - 애플리케이션 보안 1 (1) | 2024.03.24 |
[SK shieldus Rookies 19기][애플리케이션 보안 ] - 실습 환경 구성 (0) | 2024.03.24 |
댓글