چگونه از Command Injection جلوگیری کنیم؟

از سری مطالب آموزش کد نویسی امن در وب، این قسمت آسیب پذیری command injection و روش های جلوگیری از آن را بررسی می کنیم.

بعضی اوقات از دستورات سیستم عامل به صورت مستقیم و یا غیر مستقیم در بستر وب استفاده می شود.

اگر ورود های دستورات سیستم عامل به صورت صحیح و کامل بررسی نشود ممکن است مهاجم با ورودی های متفاوت دستورات مورد نظر خود را اجرا کند.

این آسیب پذیری بسیار گسترده و خطرناک است چون امکان دسترسی به کلیه منابع سرور اپلیکیشن و یا حتی سایر سرور ها را فراهم می کند.

portswigger.net
portswigger.net


در تصویر بالا مهاجم با استفاده از آسیب پذیری command injection توانسته است اسکریپت bash را سمت سرور اجرا و دسترسی به سرور اپلیکشین بگیرد.

/**
* Get the code from a GET input
* Example - http://example.com/?code=phpinfo();
*/
$code = $_GET['code'];

/**
* Unsafely evaluate the code
* Example - phpinfo();
*/
eval(&quot\$code;&quot);

برای مثال در برنامه زیر ورودی دستور eval بدون هیچ بررسی از کاربر توسط پارامتر code گرفته می شود.

http://example.com/?code=phpinfo();

یا

http://example.com/?code=system('whoami');

حال اگر مهاجم دستورات زبان و یا سیستم عامل را وارد کند می گوییم command injection رخ داده است.

روش های جلوگیری در زبان PHP

مثال 1)

تکه کد زیر آدرس کاربر را دریافت و به دستور ping می دهد.

<html>
<head>
    <title>Command Injection</title>
</head>
<body>
<form action=&quot&quot method=&quotget&quot>
    Ping address: <input type=&quottext&quot name=&quotaddr&quot>
    <input type=&quotsubmit&quot>
</form>
</body>
</html>
<?php
#Excute Command
echo shell_exec(&quotping &quot.$_GET['addr']);
?>

می توانیم در ورودی علاوه بر آدرس با استفاده از جداکننده هایی مانند ; و یا && دستورات دیگر مورد نظر خود را وارد و اجرا کنیم.

مثال 2)

در نمونه دیگر خروجی دستور به صورت مستقیم به کاربر نشان داده نمی شود.

<html>
<head>
    <title>Command Injection</title>
</head>
<body>
<form action=&quot&quot method=&quotget&quot>
    Ping address: <input type=&quottext&quot name=&quotaddr&quot>
    <input type=&quotsubmit&quot>
</form>
</body>
</html>
<?php
#Excute Command
shell_exec(&quotping &quot.$_GET['addr']);
?>

در این مورد می توانیم با استفاده از دستور های ایجاد کننده تاخییر مانند sleep آسیب پذیری را شناسایی کنیم.

راه حل 1)

در زبان php می توانیم با استفاده از تابع escapeshellcmd از ورود دستورات دیگر جلوگیری می کنیم.

<html>
<head>
    <title>Command Injection</title>
</head>
<body>
<form action=&quot&quot method=&quotget&quot>
    Ping address: <input type=&quottext&quot name=&quotaddr&quot>
    <input type=&quotsubmit&quot>
</form>
</body>
</html>
<?php
#Excute Command
echo shell_exec(escapeshellcmd(&quotping &quot.$_GET['addr']));
?>

راه حل 2)

روش دیگر جلوگیری، تعریف نوع ورودی کاربر است.

به این صورت که اگر مانند مثال بالا کاربر باید ip خود را وارد اپلیکیشن کند.

ما باید نوع ورودی کاربر را بررسی و در صورت صحیح بودن آن را به تابع مورد نظر پاس دهیم.

<?php
function isAllowed($cmd){
    // If the ip is matched, return true
    if(filter_var($cmd, FILTER_VALIDATE_IP)) {
        return true;
    }

    return false;
}
#Excute Command
if (isAllowed($_GET['addr'])) {
    echo shell_exec(&quotping &quot.$_GET['addr']);
}
?>

روش های جلوگیری در ASP.NET

مثال 1)

در برنامه زیر ورودی دستور ping از کاربر گرفته می شود. و نتیجه به کاربر نشان داده می شود.

<%@ Page Language=&quotC#&quot Debug=&quottrue&quot Trace=&quotfalse&quot %>
<%@ Import Namespace=&quotSystem.Diagnostics&quot %>
<%@ Import Namespace=&quotSystem.IO&quot %>
<script Language=&quotc#&quot runat=&quotserver&quot>
void Page_Load(object sender, EventArgs e){
}
string ExcuteCmd(string arg){
	ProcessStartInfo psi = new ProcessStartInfo();
	psi.FileName = &quotcmd.exe&quot
	psi.Arguments = &quot/c ping -n 2 &quot + arg;
	psi.RedirectStandardOutput = true;
	psi.UseShellExecute = false;
	Process p = Process.Start(psi);
	StreamReader stmrdr = p.StandardOutput;
	string s = stmrdr.ReadToEnd();
	stmrdr.Close();
	return s;
}
void cmdExe_Click(object sender, System.EventArgs e){
	Response.Write(Server.HtmlEncode(ExcuteCmd(addr.Text)));
}


<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id=&quotcmd&quot method=&quotpost&quot runat=&quotserver&quot>
<asp:Label id=&quotlblText&quot runat=&quotserver&quot>Command:</asp:Label>
<asp:TextBox id=&quotaddr&quot runat=&quotserver&quot Width=&quot250px&quot>
</asp:TextBox>
<asp:Button id=&quottesting&quot runat=&quotserver&quot Text=&quotexcute&quot =&quotcmdExe_Click&quot>
</asp:Button>
</form>
</body>
</HTML>

مثال 2)

در برنامه زیر ورودی دستور ping از کاربر گرفته می شود. و نتیجه به کاربر نشان داده نمی شود.

<%@ Page Language=&quotC#&quot Debug=&quottrue&quot Trace=&quotfalse&quot %>
<%@ Import Namespace=&quotSystem.Diagnostics&quot %>
<%@ Import Namespace=&quotSystem.IO&quot %>
<script Language=&quotC#&quot runat=&quotserver&quot>
string ExcuteCmd(string arg){
  ProcessStartInfo psi = new ProcessStartInfo();
  psi.FileName = &quotcmd.exe&quot
  psi.Arguments = &quot/c ping -n 2 &quot + arg;
  psi.RedirectStandardOutput = true;
  psi.UseShellExecute = false;
  Process p = Process.Start(psi);
  StreamReader stmrdr = p.StandardOutput;
  string s = stmrdr.ReadToEnd();
  stmrdr.Close();
  return s;
}
void Page_Load(object sender, System.EventArgs e){
  string addr = Request.QueryString[&quotaddr&quot];
  Server.HtmlEncode(ExcuteCmd(addr));
}


<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id=&quotcmd&quot method=&quotGET&quot runat=&quotserver&quot>
</form>
</body>
</HTML>

راه حل 1)

ورودی کاربر حتما باید از نوع ip باشد و به غیر از ip های داخلی مانند 127.0.0.1 باشد.

<%@ Page Language=&quotC#&quot Debug=&quottrue&quot Trace=&quotfalse&quot %>
<%@ Import Namespace=&quotSystem.Diagnostics&quot %>
<%@ Import Namespace=&quotSystem.IO&quot %>
<script Language=&quotc#&quot runat=&quotserver&quot>
    void Page_Load(object sender, EventArgs e){
    }
    Boolean Blacklist(string address)
    {
        string[] black_array = { &quot192.168.1.1&quot, &quot127.0.0.1&quot };
        Match match = Regex.Match(address, @&quot^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$&quot);
        if (match.Success)
        {
            if (black_array.Contains(address))
            {
                return false;
            }
            else
            {
                return true;
            }
        }
        return false;

    }
    string ExcuteCmd(string arg){
        if (Blacklist(arg)) {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = &quotcmd.exe&quot
            psi.Arguments = &quot/c ping -n 2 &quot + arg;
            psi.RedirectStandardOutput = true;
            psi.UseShellExecute = false;
            Process p = Process.Start(psi);
            StreamReader stmrdr = p.StandardOutput;
            string s = stmrdr.ReadToEnd();
            stmrdr.Close();
            return s;
        }
        else
        {
            return &quotAccess Denied&quot
        }

    }
    void cmdExe_Click(object sender, System.EventArgs e){
        Response.Write(Server.HtmlEncode(ExcuteCmd(addr.Text)));
    }


<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id=&quotcmd&quot method=&quotpost&quot runat=&quotserver&quot>
<asp:Label id=&quotlblText&quot runat=&quotserver&quot>Command:</asp:Label>
<asp:TextBox id=&quotaddr&quot runat=&quotserver&quot Width=&quot250px&quot>
</asp:TextBox>
<asp:Button id=&quottesting&quot runat=&quotserver&quot Text=&quotexcute&quot =&quotcmdExe_Click&quot>
</asp:Button>
</form>
</body>
</HTML>

راه حل 2)

ورودی کاربر نباید شامل برخی از کاراکتر ها مانند & نباشد.

<%@ Page Language=&quotC#&quot Debug=&quottrue&quot Trace=&quotfalse&quot %>
<%@ Import Namespace=&quotSystem.Diagnostics&quot %>
<%@ Import Namespace=&quotSystem.IO&quot %>
<script Language=&quotc#&quot runat=&quotserver&quot>
    void Page_Load(object sender, EventArgs e){
    }
    String SafeString(string address)
    {
        char[] separators = new char[]{' ',';',',','\r','\t','\n','&'};

        string[] temp = address.Split(separators, StringSplitOptions.RemoveEmptyEntries);
        address = String.Join(&quot\n&quot, temp);
        return address;
    }
    Boolean Blacklist(string address)
    {
        address = SafeString(address); 
        string[] black_array = { &quot192.168.1.1&quot, &quot127.0.0.1&quot };
        if (black_array.Contains(address))
        {
            return false;
        }
        else
        {

            return true;
        }
    }
    string ExcuteCmd(string arg){
        if (Blacklist(arg)) {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = &quotcmd.exe&quot
            psi.Arguments = &quot/c ping -n 2 &quot + arg;
            psi.RedirectStandardOutput = true;
            psi.UseShellExecute = false;
            Process p = Process.Start(psi);
            StreamReader stmrdr = p.StandardOutput;
            string s = stmrdr.ReadToEnd();
            stmrdr.Close();
            return s;
        }
        else
        {
            return &quotAccess Denied&quot
        }

    }
    void cmdExe_Click(object sender, System.EventArgs e){
        Response.Write(Server.HtmlEncode(ExcuteCmd(SafeString(addr.Text))));
    }


<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id=&quotcmd&quot method=&quotpost&quot runat=&quotserver&quot>
<asp:Label id=&quotlblText&quot runat=&quotserver&quot>Command:</asp:Label>
<asp:TextBox id=&quotaddr&quot runat=&quotserver&quot Width=&quot250px&quot>
</asp:TextBox>
<asp:Button id=&quottesting&quot runat=&quotserver&quot Text=&quotexcute&quot =&quotcmdExe_Click&quot>
</asp:Button>
</form>
</body>
</HTML>

روش های جلوگیری در JAVA

مثال 1)

در تکه کد زیر ورودی کاربر به دسترسی کنترل نمی شود

@Runtime@getRuntime().exec('rm -fr /your-important-dir/')

و می توان از دستورات سیستم عامل استفاده کرد.

package org.t246osslab.easybuggy.vulnerabilities;

import java.io.IOException;
import java.util.Locale;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.t246osslab.easybuggy.core.servlets.AbstractServlet;

@SuppressWarnings(&quotserial&quot)
@WebServlet(urlPatterns = { &quot/ognleijc&quot })
public class OGNLExpressionInjectionServlet extends AbstractServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

        Locale locale = req.getLocale();
        StringBuilder bodyHtml = new StringBuilder();
        Object value = null;
        String errMessage = &quot&quot
        OgnlContext ctx = new OgnlContext();
        String expression = req.getParameter(&quotexpression&quot);
        if (!StringUtils.isBlank(expression)) {
            try {
                Object expr = Ognl.parseexpression.replaceAll(&quotMath\\.&quot, &quot@Math@&quot));
                value = Ognl.getValue(expr, ctx);
            } catch (OgnlException e) {
                if (e.getReason() != null) {
                    errMessage = e.getReason().getMessage();
                }
                log.debug(&quotOgnlException occurs: &quot, e);
            } catch (Exception e) {
                log.debug(&quotException occurs: &quot, e);
            } catch (Error e) {
                log.debug(&quotError occurs: &quot, e);
            }
        }

        bodyHtml.append(&quot<form action=\&quotognleijc\&quot method=\&quotpost\&quot>&quot);
        bodyHtml.append(getMsg(&quotmsg.enter.math.expression&quot, locale));
        bodyHtml.append(&quot<br><br>&quot);
        if (expression == null) {
            bodyHtml.append(&quot<input type=\&quottext\&quot name=\&quotexpression\&quot size=\&quot80\&quot maxlength=\&quot300\&quot>&quot);
        } else {
            bodyHtml.append(&quot<input type=\&quottext\&quot name=\&quotexpression\&quot size=\&quot80\&quot maxlength=\&quot300\&quot value=\&quot&quot
                    + encodeForHTML(expression) + &quot\&quot>&quot);
        }
        bodyHtml.append(&quot = &quot);
        if (value != null && NumberUtils.isNumber(value.toString())) {
            bodyHtml.append(value);
        }
        bodyHtml.append(&quot<br><br>&quot);
        bodyHtml.append(&quot<input type=\&quotsubmit\&quot value=\&quot&quot + getMsg(&quotlabel.calculate&quot, locale) + &quot\&quot>&quot);
        bodyHtml.append(&quot<br><br>&quot);
        if (value == null && expression != null) {
            bodyHtml.append(getErrMsg(&quotmsg.invalid.expression&quot, new String[] { errMessage }, locale));
        }
        bodyHtml.append(getInfoMsg(&quotmsg.note.commandinjection&quot, locale));
        bodyHtml.append(&quot</form>&quot);

        responseToClient(req, res, getMsg(&quottitle.commandinjection.page&quot, locale), bodyHtml.toString());
    }
}

مثال 2)

در تکه کد زیر ورودی کاربر بدون هیچ کنترلی به run.exec پاس داده می شود.

package org.joychou.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * @author  JoyChou (joychou@joychou.org)
 * @date    2018.05.24
 * @desc    Java code execute
 * @fix     过滤造成命令执行的参数
 */

@Controller
@RequestMapping(&quot/rce&quot)
public class Rce {

    @RequestMapping(&quot/exec&quot)
    @ResponseBody
    public String CommandExec(HttpServletRequest request) {
        String cmd = request.getParameter(&quotcmd&quot).toString();
        Runtime run = Runtime.getRuntime();
        String lineStr = &quot&quot

        try {
            Process p = run.exec(cmd);
            BufferedInputStream in = new BufferedInputStream(p.getInputStream());
            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
            String tmpStr;

            while ((tmpStr = inBr.readLine()) != null) {
                lineStr += tmpStr + &quot\n&quot
                System.out.println(tmpStr);
            }

            if (p.waitFor() != 0) {
                if (p.exitValue() == 1)
                    return &quotcommand exec failed&quot
            }

            inBr.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
            return &quotExcept&quot
        }
        return lineStr;
    }
}

راه حل 1)

می توان برای ورودی های کاربر whitelist تعریف کرد تا هر دستوری قابلیت اجرا نداشته باشد.

package org.joychou.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;

/**
 * @author  JoyChou (joychou@joychou.org)
 * @fix     RezaDuty 
 */

@Controller
@RequestMapping(&quot/rce&quot)
public class Rce {

    @RequestMapping(&quot/exec&quot)
    @ResponseBody
    public String CommandExec(HttpServletRequest request) {
    	
    	
    	String lineStr = &quot&quot
        String cmd = request.getParameter(&quotcmd&quot).toString();
        if(WhiteCommand(cmd)) {
	        Runtime run = Runtime.getRuntime();
	        
	
	        try {
	            Process p = run.exec(cmd);
	            BufferedInputStream in = new BufferedInputStream(p.getInputStream());
	            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
	            String tmpStr;
	
	            while ((tmpStr = inBr.readLine()) != null) {
	                lineStr += tmpStr + &quot\n&quot
	                System.out.println(tmpStr);
	            }
	
	            if (p.waitFor() != 0) {
	                if (p.exitValue() == 1)
	                    return &quotcommand exec failed&quot
	            }
	
	            inBr.close();
	            in.close();
	        } catch (Exception e) {
	            e.printStackTrace();
	            return &quotExcept&quot
	        }
	       
        }
        return lineStr;
    }
    public Boolean WhiteCommand(String cmd) {
    	String[] splited = cmd.split(&quot\\s+&quot);
    	String [] whitelist = {&quotecho&quot,&quotwhoami&quot };
    	if(Arrays.asList(whitelist).contains(splited[0])){
    	    return true;
    	}else {
    		return false;
    	}
    }
}

راه حل 2)

می توان نوع ورودی کاربر را مشخص و کنترل کرد.

package org.joychou.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;

/**
 * @author  JoyChou (joychou@joychou.org)
 * @fix     RezaDuty 
 */

@Controller
@RequestMapping(&quot/rce&quot)
public class Rce {

    @RequestMapping(&quot/exec&quot)
    @ResponseBody
    public String CommandExec(HttpServletRequest request) {
    	
    	
    	String lineStr = &quot&quot
        String ip = request.getParameter(&quotaddress&quot).toString();
        
	        Runtime run = Runtime.getRuntime();
	        
	    	if(validate(ip) && WhiteAddress(ip)) {
		        try {
		            Process p = run.exec(&quotping -n 1 &quot+ip);
		            BufferedInputStream in = new BufferedInputStream(p.getInputStream());
		            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
		            String tmpStr;
		
		            while ((tmpStr = inBr.readLine()) != null) {
		                lineStr += tmpStr + &quot\n&quot
		                System.out.println(tmpStr);
		            }
		
		            if (p.waitFor() != 0) {
		                if (p.exitValue() == 1)
		                    return &quotcommand exec failed&quot
		            }
		
		            inBr.close();
		            in.close();
		        } catch (Exception e) {
		            e.printStackTrace();
		            return &quotExcept&quot
		        }
		} 
	        return lineStr;
		
	    }
	    public static boolean validate(final String ip) {
	        String PATTERN = &quot^((0|1\\d?\\d?|2[0-4]?\\d?|25[0-5]?|[3-9]\\d?)\\.){3}(0|1\\d?\\d?|2[0-4]?\\d?|25[0-5]?|[3-9]\\d?)$&quot

	        return ip.matches(PATTERN);
	    }
    public Boolean WhiteAddress(String ip) {
    	String [] whitelist = {&quot127.0.0.1&quot,&quot192.168.1.1&quot };
    	if(!Arrays.asList(whitelist).contains(ip)){
    	    return true;
    	}else {
    		return false;
    	}
    }
}

نسخه انگلیسی و کامل تر این مطالب در اینجا وجود دارد.

نمونه Source Code با PHP

نمونه Source Code با ASP.NET

نمونه Source Code با Java

منابع




معذرت بابت کم و کاستی های فراوان این مطلب
خیلی خوشحال میشوم نظرات خود را اینجا بگذارید

با تشکر از شما