نگاره‌هایی پیرامون امنیت، شبکه و رمزنگاری

03 نوامبر 2019

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


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

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

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

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("\$code;");

برای مثال در برنامه زیر ورودی دستور 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="" method="get">
    Ping address: <input type="text" name="addr">
    <input type="submit">
</form>
</body>
</html>
<?php
#Excute Command
echo shell_exec("ping ".$_GET['addr']);
?>

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

مثال 2)

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

<html>
<head>
    <title>Command Injection</title>
</head>
<body>
<form action="" method="get">
    Ping address: <input type="text" name="addr">
    <input type="submit">
</form>
</body>
</html>
<?php
#Excute Command
shell_exec("ping ".$_GET['addr']);
?>

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

راه حل 1)

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

<html>
<head>
    <title>Command Injection</title>
</head>
<body>
<form action="" method="get">
    Ping address: <input type="text" name="addr">
    <input type="submit">
</form>
</body>
</html>
<?php
#Excute Command
echo shell_exec(escapeshellcmd("ping ".$_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("ping ".$_GET['addr']);
}
?>

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

مثال 1)

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

<%@ Page Language="C#" Debug="true" Trace="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.IO" %>
<script Language="c#" runat="server">
void Page_Load(object sender, EventArgs e){
}
string ExcuteCmd(string arg){
	ProcessStartInfo psi = new ProcessStartInfo();
	psi.FileName = "cmd.exe";
	psi.Arguments = "/c ping -n 2 " + 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)));
}
</script>

<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id="cmd" method="post" runat="server">
<asp:Label id="lblText" runat="server">Command:</asp:Label>
<asp:TextBox id="addr" runat="server" Width="250px">
</asp:TextBox>
<asp:Button id="testing" runat="server" Text="excute" OnClick="cmdExe_Click">
</asp:Button>
</form>
</body>
</HTML>

مثال 2)

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

<%@ Page Language="C#" Debug="true" Trace="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.IO" %>
<script Language="C#" runat="server">
string ExcuteCmd(string arg){
  ProcessStartInfo psi = new ProcessStartInfo();
  psi.FileName = "cmd.exe";
  psi.Arguments = "/c ping -n 2 " + 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["addr"];
  Server.HtmlEncode(ExcuteCmd(addr));
}
</script>

<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id="cmd" method="GET" runat="server">
</form>
</body>
</HTML>

راه حل 1)

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

<%@ Page Language="C#" Debug="true" Trace="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.IO" %>
<script Language="c#" runat="server">
    void Page_Load(object sender, EventArgs e){
    }
    Boolean Blacklist(string address)
    {
        string[] black_array = { "192.168.1.1", "127.0.0.1" };
        Match match = Regex.Match(address, @"^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$");
        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 = "cmd.exe";
            psi.Arguments = "/c ping -n 2 " + 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 "Access Denied";
        }

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

<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id="cmd" method="post" runat="server">
<asp:Label id="lblText" runat="server">Command:</asp:Label>
<asp:TextBox id="addr" runat="server" Width="250px">
</asp:TextBox>
<asp:Button id="testing" runat="server" Text="excute" OnClick="cmdExe_Click">
</asp:Button>
</form>
</body>
</HTML>

راه حل 2)

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

<%@ Page Language="C#" Debug="true" Trace="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.IO" %>
<script Language="c#" runat="server">
    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("\n", temp);
        return address;
    }
    Boolean Blacklist(string address)
    {
        address = SafeString(address); 
        string[] black_array = { "192.168.1.1", "127.0.0.1" };
        if (black_array.Contains(address))
        {
            return false;
        }
        else
        {

            return true;
        }
    }
    string ExcuteCmd(string arg){
        if (Blacklist(arg)) {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "cmd.exe";
            psi.Arguments = "/c ping -n 2 " + 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 "Access Denied";
        }

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

<HTML>
<HEAD>
<title>ASP.NET Ping Application</title>
</HEAD>
<body>
<form id="cmd" method="post" runat="server">
<asp:Label id="lblText" runat="server">Command:</asp:Label>
<asp:TextBox id="addr" runat="server" Width="250px">
</asp:TextBox>
<asp:Button id="testing" runat="server" Text="excute" OnClick="cmdExe_Click">
</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("serial")
@WebServlet(urlPatterns = { "/ognleijc" })
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 = "";
        OgnlContext ctx = new OgnlContext();
        String expression = req.getParameter("expression");
        if (!StringUtils.isBlank(expression)) {
            try {
                Object expr = Ognl.parseExpression(expression.replaceAll("Math\\.", "@Math@"));
                value = Ognl.getValue(expr, ctx);
            } catch (OgnlException e) {
                if (e.getReason() != null) {
                    errMessage = e.getReason().getMessage();
                }
                log.debug("OgnlException occurs: ", e);
            } catch (Exception e) {
                log.debug("Exception occurs: ", e);
            } catch (Error e) {
                log.debug("Error occurs: ", e);
            }
        }

        bodyHtml.append("<form action=\"ognleijc\" method=\"post\">");
        bodyHtml.append(getMsg("msg.enter.math.expression", locale));
        bodyHtml.append("<br><br>");
        if (expression == null) {
            bodyHtml.append("<input type=\"text\" name=\"expression\" size=\"80\" maxlength=\"300\">");
        } else {
            bodyHtml.append("<input type=\"text\" name=\"expression\" size=\"80\" maxlength=\"300\" value=\""
                    + encodeForHTML(expression) + "\">");
        }
        bodyHtml.append(" = ");
        if (value != null && NumberUtils.isNumber(value.toString())) {
            bodyHtml.append(value);
        }
        bodyHtml.append("<br><br>");
        bodyHtml.append("<input type=\"submit\" value=\"" + getMsg("label.calculate", locale) + "\">");
        bodyHtml.append("<br><br>");
        if (value == null && expression != null) {
            bodyHtml.append(getErrMsg("msg.invalid.expression", new String[] { errMessage }, locale));
        }
        bodyHtml.append(getInfoMsg("msg.note.commandinjection", locale));
        bodyHtml.append("</form>");

        responseToClient(req, res, getMsg("title.commandinjection.page", 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("/rce")
public class Rce {

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

        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 + "\n";
                System.out.println(tmpStr);
            }

            if (p.waitFor() != 0) {
                if (p.exitValue() == 1)
                    return "command exec failed";
            }

            inBr.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
            return "Except";
        }
        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("/rce")
public class Rce {

    @RequestMapping("/exec")
    @ResponseBody
    public String CommandExec(HttpServletRequest request) {
    	
    	
    	String lineStr = "";
        String cmd = request.getParameter("cmd").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 + "\n";
	                System.out.println(tmpStr);
	            }
	
	            if (p.waitFor() != 0) {
	                if (p.exitValue() == 1)
	                    return "command exec failed";
	            }
	
	            inBr.close();
	            in.close();
	        } catch (Exception e) {
	            e.printStackTrace();
	            return "Except";
	        }
	       
        }
        return lineStr;
    }
    public Boolean WhiteCommand(String cmd) {
    	String[] splited = cmd.split("\\s+");
    	String [] whitelist = {"echo","whoami" };
    	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("/rce")
public class Rce {

    @RequestMapping("/exec")
    @ResponseBody
    public String CommandExec(HttpServletRequest request) {
    	
    	
    	String lineStr = "";
        String ip = request.getParameter("address").toString();
        
	        Runtime run = Runtime.getRuntime();
	        
	    	if(validate(ip) && WhiteAddress(ip)) {
		        try {
		            Process p = run.exec("ping -n 1 "+ip);
		            BufferedInputStream in = new BufferedInputStream(p.getInputStream());
		            BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
		            String tmpStr;
		
		            while ((tmpStr = inBr.readLine()) != null) {
		                lineStr += tmpStr + "\n";
		                System.out.println(tmpStr);
		            }
		
		            if (p.waitFor() != 0) {
		                if (p.exitValue() == 1)
		                    return "command exec failed";
		            }
		
		            inBr.close();
		            in.close();
		        } catch (Exception e) {
		            e.printStackTrace();
		            return "Except";
		        }
		} 
	        return lineStr;
		
	    }
	    public static boolean validate(final String ip) {
	        String PATTERN = "^((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?)$";

	        return ip.matches(PATTERN);
	    }
    public Boolean WhiteAddress(String ip) {
    	String [] whitelist = {"127.0.0.1","192.168.1.1" };
    	if(!Arrays.asList(whitelist).contains(ip)){
    	    return true;
    	}else {
    		return false;
    	}
    }
}

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

نمونه Source Code با PHP

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

نمونه Source Code با Java

Avatar
2 پست نوشته شده
دسته‌ها: امنیت وب
  • به اشتراک بگذارید:
  1. Avatar محمدحسین گفت:

    سلام خیلی مفید بود ممنون

  2. Avatar علی گفت:

    باز هم سپاس. یک پرسش اینکه چرا باید امکان فرستادن Command را به کاربر روی یک پردازش بدهیم؟