question2answer之旅

又是项目中的一个,从中get一个cve,github有800+的star,还算人在用,也有一定用户量,CVE-2017-12775
目前最新是1.7.5,审计的是1.7.4

分享一下分析过程(不涉及漏洞),总结一下写法以及思维。
比较小巧的一个程序,功能也是比较少,单纯的为了实现一下q&a吧

先观察目录
804631-20170810120603355-2107066362.png

比较直观,可以看到核心东西都放在qa-include里面

接手程序,按顺序先分析,路由 -> 数据库操作 -> 具体危害函数

路由分析

先进入index.php

qa-include/qa-index.php

if (isset($_POST['qa']) && $_POST['qa'] == 'ajax')
  require 'qa-ajax.php';

elseif (isset($_GET['qa']) && $_GET['qa'] == 'image')
  require 'qa-image.php';

elseif (isset($_GET['qa']) && $_GET['qa'] == 'blob')
  require 'qa-blob.php';

else {
  xxx
}

有一些特别的操作,跟进一个

qa-include/qa-ajax.php

require 'qa-base.php';
qa_report_process_stage('init_ajax');
qa_set_request(qa_post_text('qa_request'), qa_post_text('qa_root'));
$_GET=array(); // for qa_self_html()

function qa_ajax_db_fail_handler(){
  echo "QA_AJAX_RESPONSE\n0\nA database error occurred.";
  qa_exit('error');
}

$routing=array(
  'notice' => 'notice.php',
  'favorite' => 'favorite.php',
  'vote' => 'vote.php',
  'recalc' => 'recalc.php',
  'mailing' => 'mailing.php',
  'version' => 'version.php',
  'category' => 'category.php',
  'asktitle' => 'asktitle.php',
  'answer' => 'answer.php',
  'comment' => 'comment.php',
  'click_a' => 'click-answer.php',
  'click_c' => 'click-comment.php',
  'click_admin' => 'click-admin.php',
  'show_cs' => 'show-comments.php',
  'wallpost' => 'wallpost.php',
  'click_wall' => 'click-wall.php',
  'click_pm' => 'click-pm.php',
);

$operation=qa_post_text('qa_operation');

if (isset($routing[$operation])) {
  qa_db_connect('qa_ajax_db_fail_handler');
  require QA_INCLUDE_DIR.'ajax/'.$routing[$operation];
  qa_db_disconnect();
}

qa-base.php里面就是一些程序初始化,全是函数,可以跳过。

1、参数接收

qa_set_request(qa_post_text('qa_request'), qa_post_text('qa_root'));

很显眼的是接受GETPOST参数用的,跟进一下qa_post_text函数

function qa_post_text($field){
  if (qa_to_override(__FUNCTION__)) { $args=func_get_args(); return qa_call_override(__FUNCTION__, $args); }
  return isset($_POST[$field]) ? preg_replace('/\r\n?/', "\n", trim(qa_gpc_to_string($_POST[$field]))) : null;
}

这个支持函数覆盖,但是一般默认是没有的,所以前面的qa_to_override也是利用不上
$_POST数组先经过qa_gpc_to_string处理,这个是会针对GPC,把转义字符去掉

function qa_gpc_to_string($string){
  return get_magic_quotes_gpc() ? stripslashes($string) : $string;
}

可以看到后面是会把$_POST数组中的一些\r\n字符给替换为\n
get或者其他类似

2、路由分配
返回qa-include/qa-ajax.php接着继续跟

它的路由都是硬编码,而且是限定在数组里面,里面很多进入requireinclude都进入这样限定的数组去操作,能一定程度避免问题

ajax.php就到此为止,现在回到qa-include/qa-index.php的条件判断里面,最后else中,这个才是其他操作的路由

先看看怎么样要进入其他的操作,比如install

$requestlower = strtolower(qa_request());

if ($requestlower == 'install')
  require QA_INCLUDE_DIR.'qa-install.php';
elseif ($requestlower == 'url/test/'.QA_URL_TEST_STRING)
  require QA_INCLUDE_DIR.'qa-url-test.php';
else {
  xxx
}

通过qa_request函数去获取

function qa_request(){
    global $qa_request;
    return $qa_request;
}

所以,$requestlower就是全局变量$qa_request经过小写化
跟踪一下,全局变量$qa_request是在哪有赋值

qa-include/qa-base.php
function qa_set_request($request, $relativeroot, $usedformat=null) {
  global $qa_request, $qa_root_url_relative, $qa_used_url_format;

  $qa_request=$request;
  $qa_root_url_relative=$relativeroot;
  $qa_used_url_format=$usedformat;
}

那就再看下调用的地方
qa-include/qa-index.php

function qa_index_set_request(){
  $relativedepth = 0;

  if (isset($_GET['qa-rewrite'])) {
     /* URLs rewritten by .htaccess or Nginx */
  }
  elseif (isset($_GET['qa'])) {
  
    if (strpos($_GET['qa'], '/') === false) {
      $urlformat = ( empty($_SERVER['REQUEST_URI']) || strpos($_SERVER['REQUEST_URI'], '/index.php') !== false )
        ? QA_URL_FORMAT_SAFEST : QA_URL_FORMAT_PARAMS;
      $requestparts = array(qa_gpc_to_string($_GET['qa']));

      for ($part = 1; $part < 10; $part++) {
        if (isset($_GET['qa_'.$part])) {
          $requestparts[] = qa_gpc_to_string($_GET['qa_'.$part]);
          unset($_GET['qa_'.$part]);
        }
      }
    }
    else {
      $urlformat = QA_URL_FORMAT_PARAM;
      $requestparts = explode('/', qa_gpc_to_string($_GET['qa']));
    }

    unset($_GET['qa']);
  }
  else {
      /* index.php/aaa/bbb */
  }

  foreach ($requestparts as $part => $requestpart) { // remove any blank parts
    if (!strlen($requestpart))
      unset($requestparts[$part]);
  }

  reset($requestparts);
  $key = key($requestparts);

  $requestkey = isset($requestparts[$key]) ? $requestparts[$key] : '';
  $replacement = array_search($requestkey, qa_get_request_map());
  if ($replacement !== false)
    $requestparts[$key] = $replacement;

  qa_set_request(
    implode('/', $requestparts),
    ($relativedepth > 1 ? str_repeat('../', $relativedepth - 1) : './'),
    $urlformat
  );
}

最后的进入qa_set_request函数

qa_index_set_request这个函数实现的功能大概有这三种
第一个是url重写的匹配,对nginx、apache的差异做了变化
第二个是常见的匹配,index.php?qa=xxx
第三个是self模式匹配,index.php/aaa/bbb

数据库操作

以这个为例

qa-include/db/admin.php

function qa_db_category_rename($categoryid, $title, $tags){
  qa_db_query_sub(
    'UPDATE ^categories SET title=$, tags=$ WHERE categoryid=#',
    $title, $tags, $categoryid
  );
  qa_db_categories_recalc_backpaths($categoryid);
}

可以看到有一些字符,^$#

重点是qa_db_query_sub函数

function qa_db_query_sub($query){
  $funcargs=func_get_args();
  return qa_db_query_raw(qa_db_apply_sub($query, array_slice($funcargs, 1)));
}

继续跟进

function qa_db_apply_sub($query, $arguments)
{
  $query = preg_replace_callback('/\^([A-Za-z_0-9]+)/', 'qa_db_prefix_callback', $query);

  if (!is_array($arguments))
    return $query;

  $countargs = count($arguments);
  $offset = 0;

  for ($argument = 0; $argument < $countargs; $argument++) {
    $stringpos = strpos($query, '$', $offset);
    $numberpos = strpos($query, '#', $offset);

    if ($stringpos === false || ($numberpos !== false && $numberpos < $stringpos)) {
      $alwaysquote = false;
      $position = $numberpos;
    }
    else {
      $alwaysquote = true;
      $position = $stringpos;
    }

    if (!is_numeric($position))
      qa_fatal_error('Insufficient parameters in query: '.$query);

    $value = qa_db_argument_to_mysql($arguments[$argument], $alwaysquote);
    $query = substr_replace($query, $value, $position, 1);
    $offset = $position + strlen($value); // allows inserting strings which contain #/$ character
  }

  return $query;
}

preg_replace_callback的调用将^替换为表前缀
后面就是在查找$#的位置

再进入qa_db_argument_to_mysql看看最后是怎么样操作的

function qa_db_argument_to_mysql($argument, $alwaysquote, $arraybrackets=false)
{
  if (is_array($argument)) {
    $parts=array();

    foreach ($argument as $subargument)
      $parts[] = qa_db_argument_to_mysql($subargument, $alwaysquote, true);

    if ($arraybrackets)
      $result = '('.implode(',', $parts).')';
    else
      $result = implode(',', $parts);

  }
  elseif (isset($argument)) {
    if ($alwaysquote || !is_numeric($argument))
      $result = "'".qa_db_escape_string($argument)."'";
    else
      $result = qa_db_escape_string($argument);
  }
  else
    $result = 'NULL';

  return $result;
}

可以看到

if ($alwaysquote || !is_numeric($argument))
  $result = "'".qa_db_escape_string($argument)."'";
else
  $result = qa_db_escape_string($argument);

如果是$占位的话,表示是字符串,这时候会加上单引号,然后再经过qa_db_escape_string处理,也就是real_escape_string的处理
如果是#占位的话,表示是数字,但是,如果#占位也有非数字出现,也是会进行单引号里面。

这种写法,也就只能找两个地方的点
1、直接去查询qa_db_query_raw函数(直接调用mysql的query查询)
2、变量拼接进入sql

很可惜还没找到这个利用点

转载于:https://www.cnblogs.com/iamstudy/articles/question2answer_1-7-4_code.html

这段代码用于计算得分并更新答题记录的相关字段。 首先,使用 `json.loads()` 方法将 `record.answer` 字段的 JSON 字符串解析为 Python 对象,并将结果赋值给 `raw` 变量。 接下来,初始化 `score` 变量为 0,用于记录得分。 然后,遍历 `raw` 中的每个元素,使用 `QuestionBank.objects.filter(id=i["id"]).first()` 查询与当前题目 ID 匹配的题目对象,并将结果赋值给 `question` 变量。 接着,判断竞赛是否为非随机生成题目的情况,如果是,则执行相应的逻辑。首先,使用 `CompetitionToQuestionBank.objects.filter(competition_id=competition, question_id_id=i["id"]).first()` 查询与当前题目 ID 和竞赛 ID 匹配的竞赛题目关联对象,并将结果赋值给 `c_question` 变量。然后,递增 `question.answer_num` 和 `c_question.answer_num` 字段的值,表示答题次数加一。接着,判断用户答案是否与正确答案相匹配,如果匹配,则将 `score` 加一,并递增 `question.correct_answer_num` 和 `c_question.correct_answer_num` 字段的值,表示正确答案次数加一。最后,将正确答案赋值给当前题目的 `right_answer` 字段,并保存更新后的 `question` 和 `c_question` 对象。 如果竞赛为随机生成题目的情况,则执行相应逻辑。首先,递增 `question.answer_num` 字段的值,表示答题次数加一。接着,判断用户答案是否与正确答案相匹配,如果匹配,则将 `score` 加一,并递增 `question.correct_answer_num` 字段的值,表示正确答案次数加一。最后,将正确答案赋值给当前题目的 `right_answer` 字段,并保存更新后的 `question` 对象。 接下来,将计算得到的 `score` 的值赋值给 `record.score` 字段。 最后,使用 `json.dumps()` 方法将更新后的 `raw` 对象转换为 JSON 格式的字符串,并将结果赋值给 `record.answer` 字段。 这段代码的作用是根据用户的答题情况计算得分,并更新相应的题目信息和答题记录的字段。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值