تزریق SQL

از Secure Coding

نسخهٔ تاریخ ‏۱۹ دسامبر ۲۰۱۹، ساعت ۰۷:۵۴ توسط Admin (بحث | مشارکت‌ها)

در این نوع از آسیب پذیری های مهاجم می تواند payload خود را در قسمت های مختلف برنامه تزریق کند. از انواع آسیب پذیری های injection می توان به تزریق sql و یا تزریق command اشاره کرد.

تزریق sql

زبان پرس و جو sql راهی برای مدیریت داده های ذخیره شده در پایگاه داده های رابطه ای است. به این معنا که می توانیم در هر بستری از جمله وب داده هارا ذخیره، ویرایش و یا تغییر دهیم. گاهی اوقات احراز هویت کاربران، نمایش محتوای وبسایت و یا عکس های کاربران در پایگاه داده ذخیره می شود و توسط sql در مسیر های مختلف به کار برده می شود. حال فرض کنید attacker در پرس و جوی sql، دستکاری و یا Inject کند. در اینجا می گوییم sql injection رخ داده است.

Sql.png

برای مثال در تصویر بالا attacker به جای مقدار دسته بندی محصولات(Gift)این payload را وارد می کند.

' UNION SELECT username, password FROM users--

در نتیحه علاوه بر اطلاعات محصول دسته بندی Gifts تمام username و password های جدول users را هم در خروجی نمایش می دهد. حملات sql injection نوع های مختلفی دارد. چون به ازای هر query نوشته شده ممکن است حمله جدیدی رخ دهد. با این حال به صورت کلی 3 دسته بندی زیر را می توان نام برد.

بر مبنای خطا یا Error Based

مثال معروفی برای این روش وجود دارد. صفحه ای از وبسایتی برای نمایش جزییات خبر از id آن خبر استفاده می کند؛

https://web.tld/news?id=100

با توجه به مقدار id، داده های دیگری از پایگاه داده خوانده و نمایش داده می شود.

SELECT title,context FROM tbl_news WHERE id ='$id'

بر مبنای اجتماع یا Union Based

در این روش علاوه بر اطلاعات دریافت شده توسط پرس و جوی توسعه دهنده با دستور Union اطلاعات دیگری از پایگاه داده استخراج می شود.

http://fakesite.com/report.php?id=23

برای مثال فرض کنید صفحه ای وجود دارد که براساس id موجودیت های سیستم گزارشی از وضیعت آن موجودیت چاپ می کند.

اگر مقدار id را به همراه دستور زیر وارد کنیم، می توانیم تعداد ستون های جدولی که با توجه به id، اطلاعات از آن خوانده می شود را بدست آوریم.(ممکن است مقدار 5 به ازای هر جدول متفاوت باشد)

http://fakesite.com/report.php?id=23 order by 5--+

بعد از دانستن تعداد ستون های جدول با دستور Union، ستونی که از آن اطلاعات خوانده می شود و در صفحه چاپ می شود را بدست می آوریم.

http://fakesite.com/report.php?id=-23 union select 'hello1','hello2','hello3','hello4','hello5'--+

هر کدام از hello ها که در صفحه چاپ شود به این معنی است که می توانیم به جای آن hello پرس و جوی خود را بنویسیم.

برای مثال نام دیتابیس را با database() بدست می آوریم.

http://fakesite.com/report.php?id=-23 union select 1,2,database(),4,5--+

بر مبنای تاریکی Blind Based

شاید اسم این روش عجیب باشد ولی با توجه به الگو "بر مبنای" تاریکی کلمه مناسب و هکر پسندانه تری است!

در این روش با استفاده از and و دستورات زمانی مانند sleep در بارگذاری اطلاعات از پایگاه داده تاخییر و یا تغییر ایجاد می کنیم.

برای مثال می دانیم 2=1 نیست پس آن را در مقدار id قرار می دهیم تا شرط False شود و بدین ترتیب اطلاعات به درستی از پایگاه داده استخراج نشود.

http://www.shop.local/item.php?id=34 and 1=2

در مرحله بعد شرط True می کنیم.

http://www.shop.local/item.php?id=34 and 1=1

حال با توجه به روش های بالا به سراغ روش های جلوگیری از این حمله می رویم.

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

کنترل ورودی ها

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

مثال 1)

تکه کد زیر دو مقدار username و password را با درخواست از نوع POST دریافت می کند و سپس در پرس و جو با رکورد های موجود در پایگاه داده مقایسه می کند.


<?php
$userName=$_POST['userName'];
$password=$_POST['password'];
$sqlQuery="SELECT * FROM users WHERE user_name='".$username."' AND user_password='".$password"';"
?>

اگر password، مقدار ‘ or ‘a’=’a ‘or’ داشته باشد؛ شرط WHERE به ازای کاربر با کلمه عبور نامرتبط درست خواهد بود.

مثال 2)

اطلاعات کاربر را با توجه به id او توسط پرس و جو sql دریافت می کنیم.

$expected_data = 1;
$query = "SELECT * FROM users where id=$expected_data"

اگر id، مقدار1; DROP TABLE users; داشته باشد علاوه بر نمایش اطلاعات کاربر جدول کاربران هم پاک می شود! و پرس و جو در نهایت این شکلی می شود.

SELECT * FROM users where id=1; DROP TABLE users;

برای جلوگیری دو روش Block Malicious و Prepared Statements را بررسی می کنیم.

راه حل 1)

یکی از بهترین روش ها برای جلوگیری از آسیب پذیری، تشخص کاراکتر های غیر مجاز درون رشته و تبدیل آن ها به کاراکتر های مجاز است.

با این توضیح ابتدا تابعی تعریف می کنیم تا ورودی کاربر را بگیرد و کاراکتر های برهم زننده رشته پرس و جو را به کاراکتر های مجاز در رشته در قالب آرایه تبدیل کند.

<?php
function BlockSQLInjection($str)
{
return str_replace(array("'",""","'",'"'),array("'","""'",""",$str));
}
?>

حال درخواست های ارسال شده را به تابع پاس دهیم.

<?php 
$userName=BlockSQLInjection($_POST['userName']);
$password=BlockSQLInjection($_POST['password']);
?>

راه حل 2)

یکی از روش های بسیار خوب استفاده از Prepared Statements ها در زبان های مختلف است.

برای مثال در زبان php می توان از PDO برای کار با قسمت های مختلف پایگاه داده در قالب api استفاده کرد.

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

<?php
$stmt=$conn->prepare(INSERT INTO MyGuests(firstname,lastname,email)VALUES(?,?,?)");
$stmt->bind_param("sss",$firstname,$lastname,$email);
//set paramters and execute
$firstname="John";
$lastname="Doe";
$email="[email protected]";
$stmt->execute();
$firstname="Mary";
$lastname="Moe";
$email="[email protected]";
$stmt->execute();

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

مثال 1) کد زیر را در نظر بگیرید.


protected void Button1_Click(object sender, EventArgs e)
{
  string connect = "MyConnString"
    string query = "Select Count(*) From Users Where Username = '" + UserName.Text + "' And Password = '" + Password.Text + "'"
      int result = 0;
        using (var conn = new SqlConnection(connect))
          {
              using (var cmd = new SqlCommand(query, conn))
                  {
      conn.Open();
      result = (int)cmd.ExecuteScalar();
      }
      }
  if (result > 0)
  {
    Response.Redirect("LoggedIn.aspx");
  }
  else
  {
    Literal1.Text = "Invalid credentials"
}

همانطور که در رشته پرس و جو مشاهده می کنید، هیچ کنترلی بر روی درخواست ارسال شده نمی شود.


اگر مقدار ' or '1' = '1 ارسال شود شرط WHERE درست می شود.

مثال 2)

در کد زیر هم درخواست ارسال شده بدون هیچ کنترلی در رشته پرس و جو استفاده شده است.

string connect = "MyConnString"
string query = "Select * From Products Where ProductID = " + Request["ID"];

using (var conn = new SqlConnection(connect))
{
  using (var cmd = new SqlCommand(query, conn))
  {
    conn.Open();
    //Process results
  }
}

بنابر این اگر مقدار ;Drop Table Admin-- ارسال شود علاوه بر پرس و جوی قبل پرس و جوی ارسال شده اجرا می شود. برای جلوگیری دو روش زیر را بررسی می کنم.

راه حل 1)

با توجه به نوع ستون جدول در پایگاه داده، ورودی کاربر را تبدیل به آن نوع می کنم.

برای مثال id محصول از نوع عدد و int است پس درخواست ارسالی را تبدیل به عدد می کنم.

protected void Page_Load(object sender, EventArgs e)
{
  var connect = ConfigurationManager.ConnectionStrings["NorthWind"].ToString();
  var query = "Select * From Products Where ProductID = @ProductID"
  using (var conn = new SqlConnection(connect))
  {
    using (var cmd = new SqlCommand(query, conn))
    {
      cmd.Parameters.Add("@ProductID", SqlDbType.Int);
      cmd.Parameters["@ProductID"].Value = Convert.ToInt32(Request["ProductID"]);
      conn.Open();
      //Process results
    }
  }
}

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

protected void Page_Load(object sender, EventArgs e)
{
  var connect = ConfigurationManager.ConnectionStrings["NorthWind"].ToString();
  var query = "Select * From Products Where ProductID = @ProductID"
  using (var conn = new SqlConnection(connect))
  {
    using (var cmd = new SqlCommand(query, conn))
    {
      cmd.Parameters.AddWithValue("@ProductID", Convert.ToInt32(Request["ProductID"]);

      conn.Open();
      //Process results
    }
  }
}

راه حل 2)

روش دیگر استفاده از Store Procedure است.

مزیت این روش نسبت به روش قبل این است که query حتما با توجه به نوع ستون اجرا می شود.

var connect = ConfigurationManager.ConnectionStrings["NorthWind"].ToString();
var query = "GetProductByID"

using (var conn = new SqlConnection(connect))
{
  using (var cmd = new SqlCommand(query, conn))
  {
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@ProductID", SqlDbType.Int).Value = Convert.ToInt32(Request["ProductID"]);
    conn.Open();
    //Process results
  }
}

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

مثال 1)

protected void processRequest(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
        response.setContentType('text/html;charset=UTF-8');
        PrintWriter out = response.getWriter();
        try {
            String user = request.getParameter('user');
            Connection conn = null;
            String url = 'jdbc:mysql://192.168.2.128:3306/';
            String dbName = 'anvayaV2';
            String driver = 'com.mysql.jdbc.Driver';
            String userName = 'root';
            String password = '';
            try {
                Class.forName(driver).newInstance();
                conn = DriverManager.getConnection(url + dbName, userName, password);
                Statement st = conn.createStatement();
                String query = 'SELECT * FROM  User where userId='' + user + ''';
                out.println('Query : ' + query);
                System.out.printf(query);
                ResultSet res = st.executeQuery(query);
 
                out.println('Results');
                while (res.next()) {
                    String s = res.getString('username');
                    out.println('\t\t' + s);
                }
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } finally {
            out.close();
        }

با توجه به کد بالا اگر در userId مقدار admin’ or ‘1’=’1 وارد کنیم رشته پرس و جو به شکل زیر می شود.

SELECT * FROM User where userId ='admin' or '1'='1'

راه حل 1)

با استفاده Prepared Statement نوع درخواست ارسال شده را تعیین می کنیم.

PreparedStatement  preparedStatement=conn.prepareStatement('SELECT * FROM  usercheck where username=?') ;
preparedStatement.setString(1, user);

راه حل 2)

با استفاده از NamedQuery در جاوا می توانیم درخواست را به صورت Prepared Statement دریافت و پرس و جو مرتبط ایجاد کنیم.

در حالت عادی query به شکل زیر ساخته می شود.

String q='SELECT r FROM  User r where r.userId=''+user+''';
Query query=em.createQuery(q);
List users=query.getResultList();

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

Query query=em.createNamedQuery('User.findByUserId');
query.setParameter('userId', user);
List users=query.getResultList();