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

از سری مطالب آموزش کد نویسی امن در وب، این قسمت آسیب پذیری 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(&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" 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" 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" 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" } } 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" 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" } } 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" 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" 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" System.out.println(tmpStr); } if (p.waitFor() != 0) { if (p.exitValue() == 1) return &quotcommand exec failed" } inBr.close(); in.close(); } catch (Exception e) { e.printStackTrace(); return &quotExcept" } 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" 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" System.out.println(tmpStr); } if (p.waitFor() != 0) { if (p.exitValue() == 1) return &quotcommand exec failed" } inBr.close(); in.close(); } catch (Exception e) { e.printStackTrace(); return &quotExcept" } } 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" 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" System.out.println(tmpStr); } if (p.waitFor() != 0) { if (p.exitValue() == 1) return &quotcommand exec failed" } inBr.close(); in.close(); } catch (Exception e) { e.printStackTrace(); return &quotExcept" } } 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?)$" 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منابعmemoryleaks.irsecurecoding.irbawetech.comمعذرت بابت کم و کاستی های فراوان این مطلبخیلی خوشحال میشوم نظرات خود را اینجا بگذاریدبا تشکر از شما