易思espCMS v5 后台SQL注入漏洞

君叹   ·   发表于 2022-09-09 22:01:58   ·   代码审计

易思espCMS v5 后台SQL注入漏洞


1. 前言

漏洞是在看seay大大的书里发现的,看书的时候,看到里面很多代码搞不懂,上网搜,发现大部分文章都和书里写的差不多,没有那种保姆级教程,
就比如说这段

  1. $this->fun->accept('parentid', 'R');

这段代码的原理不明白,为什么要这么取,而且搜索accept函数能搜到两个,那么到底调用的哪个(虽然可以通过倒推法,分别在两个函数中加入一句 die() 函数测试,虽然说搞明白这种代码运行的机制对找漏洞可能没什么帮助,但是还是想要搞明白其中的原理,所以写下这篇文章。

文章分为四个部分
1 前言
2 代码审计过程
3 靶场测试

代码在附件中


2. 代码审计

  1. 这里用的方法是从敏感函数回溯参数

此处的sql语句内变量无单引号保护


向上追朔
$parentid
被处理了两次

从下往上,第一次是

  1. $parentid = empty($parentid) ? 1 : $parentid;

这里用到了 PHP 3元运算符
php三元运算符:
变量 = 条件 ? 值1 : 值2
如果条件成立,那么变量等于值1
反之等于值2

empty() 。判断内容是否为空值
所以这里的代码就可以理解为
如果$parentid为空值,那么给$parentid赋值为1
反之等于它本身


所以上面那句没什么作用
我们继续追溯

  1. $parentid = $this->fun->accept('parentid', 'R');

这里的代码,对于我这种PHP小白理解起来就比较困难了
首先这是在类中
$this 的含义,是对象本身,也就是自己
从自己这里要取一个模块 fun
再从模块 fun 中,拿到accept函数
但是从这个文件里并不能看到 fun 函数
不过我们可以看到这里的这句

创建类 important 继承自 connector

  1. class important extends connector

这里用到了类的继承
我们再到 connector 类中找找


全局搜索
在这里能看到类的定义

在文件中查找 fun
可以在181行找到对 fun 的赋值
在方法 commandinc 中被定义

而 commandinc 又在这个类的第一个方法
也就是13行的softbase()函数中被调用

那么现在问题来了,softbase()函数又是在哪里被调用的
这个问题我查了有一会

我在查看刚才的 important 类的时候
这个类的第一个方法中调用了 softbase()方法
但是我始终找不到这个方法在哪里有被调用
魔术方法前面一般会有下划线
但是这里没有
我试着全文搜索这个函数的时候
发现这个函数和类名相同
这也给了我新的灵感
于是我上网搜,类名和方法名相同

百度百科:
php构造函数是类中的一个特殊函数,当使用 new 操作符创建一个类的实例时,构造函数将会自动调用。
构造函数
当函数与类同名时,这个函数将成为构造函数。如果一个类没有构造函数,则调用基类的构造函数,如果有的话,则调用该构造函数。

也就是说,当类实例化的时候,会自动执行softbase()方法
softbase()方法中又会执行commandinc()方法
commandinc()方法中定义了 fun 模块
fun 模块就是实例化的 functioninc() 类

  1. $this->fun = new functioninc();

所以刚刚的那句

  1. $parentid = $this->fun->accept('parentid', 'R');

意思就是从 functioninc() 类中取到 accept() 函数

这样这里的疑问就迎刃而解了
继续追踪 accept 函数

查看函数代码
刚刚传入了两个值
parentid和R
所以现在
$k = ‘parentid’
$var = ‘R’

  1. function accept($k, $var='R', $htmlcode=true, $rehtml=false) {
  2. switch ($var) {
  3. case 'G':
  4. $var = &$_GET;
  5. break;
  6. case 'P':
  7. $var = &$_POST;
  8. break;
  9. case 'C':
  10. $var = &$_COOKIE;
  11. break;
  12. case 'R':
  13. $var = &$_GET;
  14. if (empty($var[$k])) {
  15. $var = &$_POST;
  16. }
  17. break;
  18. }
  19. $putvalue = isset($var[$k]) ? $this->daddslashes($var[$k], 0) : NULL;
  20. return $htmlcode ? ($rehtml ? $this->preg_htmldecode($putvalue) : $this->htmldecode($putvalue)) : $putvalue;
  21. }

switch语句
当 case 后面的内容和 switch() 括号里的内容相等的时候
就执行这个case下的代码,反之则不执行


代码往下看
当 $var=R 时
将 $_GET 的值赋给$var
然后检查 $var[$k] 是否为空
刚刚的$k是parentid,也就是说get传参中要传入这么一个参数
如果为空,就改变$var的值为$_POST的内容
这里可以理解为,找找GET传参里面有没有这个参数
GET传参里没有,就从POST传参里面找找


再下面这句
这里也用了三元运算符
先检查是否存在 parentid ,如果不存在
就会将 NULL 赋值给 $putvalue
如果存在,就用 daddslashes 方法处理

  1. $putvalue = isset($var[$k]) ? $this->daddslashes($var[$k], 0) : NULL;

daddslashes方法其实就是 addslashes 的一个小变种

ps:这段可以直接跳过,这里就相当于一个魔术引号函数

来查看一下函数内容

  1. function daddslashes($string, $force=0, $strip=FALSE) {
  2. if (!get_magic_quotes_gpc() || $force == 1) {
  3. if (is_array($string)) {
  4. foreach ($string as $key => $val) {
  5. $string[$key] = addslashes($strip ? stripslashes($val) : $val);
  6. }
  7. } else {
  8. $string = addslashes($strip ? stripslashes($string) : $string);
  9. }
  10. }
  11. return $string;
  12. }

查看最外层的if语句的条件
!get_magic_quotes_gpc() || $force == 1
|| 左右两边任意一个值为 True 整条语句为 True
get_magic_quotes_gpc() 函数是查看是否开启魔术引号
也就是 $force 的值为1
或者没有开启魔术引号,都会进入这个函数
再下一个 if 语句
如果传入的需要被魔术引号处理的函数是一个数组
那就用for循环,一个变量一个变量的处理

这里的三元运算符可以直接无视
因为$strip的值并没有被传入
缺省值为 FALSE
这里可以直接看做

  1. $string[$key] = addslashes($val)

或者

  1. $string = addslashes($string)

stripslashes() 函数删除由 addslashes() 函数添加的反斜杠。
addslashes() 函数在指定的预定义字符前添加反斜杠。这些字符是单引号(’)、双引号(”)、反斜线(\)与NUL(NULL字符


然后返回accept()函数
查看最后一句 return 的内容

  1. return $htmlcode ? ($rehtml ? $this->preg_htmldecode($putvalue) : $this->htmldecode($putvalue)) : $putvalue;

这里用到了两重三元运算符
$htmlcode 是布尔值
如果为1
那么就是 return ($rehtml ? $this->preg_htmldecode($putvalue) : $this->htmldecode($putvalue))
如果为 0 ,那么就是 return $putvalue
这个值并没有被传入
而默认为 true

true <==> 1
false <==> 0

这里继续往里看
$rehtml ? $this->preg_htmldecode($putvalue) : $this->htmldecode($putvalue)

$rehtml 的值也并没有被传入
而默认为 false
那么这里最后返回的值其实就是
$this->htmldecode($putvalue)


这里再分别看一下 preg_htmldecode() 和 htmldecode()

ps:这里的preg_htmldecode()是防止XSS攻击的,将<>之类的转义
htmldecode() 也是防止XSS攻击的

这段也可以直接跳过

这里可以看到,如果$string是数组
则使用递归的方式,将其中的内容一个一个处理
如果不是数组
就替换预定义字符 array(‘&’, ‘“‘, ‘<’, ‘>’)
然后再用一个正则表达式进行替换(这里正则表达式要过滤的是什么东西我确实不知道,有知道的师傅还望评论区指点一下)
将其替换为 html 实体

  1. function preg_htmldecode($string) {
  2. if (is_array($string)) {
  3. foreach ($string as $key => $val) {
  4. $string[$key] = $this->preg_htmldecode($val);
  5. }
  6. } else {
  7. $string = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $string);
  8. $string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
  9. }
  10. return $string;
  11. }

再看另一个函数
这里先检查传入的值是否为空值,为空直接返回这个值
然后再检查是不是数组,如果不是数组,就首尾去空

trim() 函数移除字符串两侧的空白字符或其他预定义字符。

htmlspecialchars() 函数把一些预定义的字符转换为 HTML 实体。语法为:htmlspecialchars(string,quotestyle,character-set).
预定义的字符是:

  1. &(和号) 成为&amp;
  2. " (双引号) 成为 &quot;
  3. ' (单引号) 成为 &apos;
  4. < (小于) 成为 &lt;
  5. > (大于) 成为 &gt;

str_ireplace() 函数使用一个字符串替换字符串中的另一些字符。
也就是检查内容里有没有 Xss 这个字符串,有的话替换为空,,,
这里我属实搞不懂这么写的意义是什么,,,

  1. function htmldecode($str) {
  2. if (empty($str)) return $str;
  3. if (!is_array($str)) {
  4. $str = htmlspecialchars(trim($str));
  5. $str = str_ireplace("Xss", "", $str);
  6. } else {
  7. foreach ($str as $key => $val) {
  8. $str[$key] = htmlspecialchars($val);
  9. $str[$key] = $this->htmldecode($val);
  10. }
  11. }
  12. return $str;
  13. }

所以这里并没有去针对SQL注入有什么防御
接下来我们就可以查找一下,
在哪里调用了这个类

同时我们也可以看到
这个类被定义了很多次
随便点进去一个
都可以发现这和刚刚的这个类都完全不一样

我们进入类实例化的文件
可以看到,在实例化之前
包含了一个文件
这里包含哪个文件
就说明调用的是哪个 important 类

  1. include admin_ROOT . adminfile . "/control/$archive.php";
  2. $control = new important();

我们追踪一下 $archive

先看从下往上数第一个
这里三元运算符
如果$archive为空值,赋值为 adminuser
否则就等于原本的值
再往上经过了 indexget() 函数的处理
追踪一下这个函数

  1. function indexget($k, $var='R', $htmlcode=true) {
  2. switch ($var) {
  3. case 'G':
  4. $var = &$_GET;
  5. break;
  6. case 'P':
  7. $var = &$_POST;
  8. break;
  9. case 'C':
  10. $var = &$_COOKIE;
  11. break;
  12. case 'R':
  13. $var = &$_GET;
  14. if (empty($var[$k])) {
  15. $var = &$_POST;
  16. }
  17. break;
  18. }
  19. $putvalue = isset($var[$k]) ? indexdaddslashes($var[$k], 0) : NULL;
  20. return $htmlcode ? indexhtmldecode($putvalue) : $putvalue;
  21. }

效果和刚刚说的accept()函数差不多
这里就不多赘述了

所以只要get传参传入
archive=citylist
即可包含我们想要包含的那个文件
然后继续
我们同时也需要执行oncitylist()方法才行


method_exists() 检查类方法是否存在

我们可以看到这里
如果$control中存在$action方法,那么就会执行这个方法
我们要执行的方法是 oncitylist

我们向上追溯,看看$action如何被定义

从下往上第一句
将原本的$action前面拼接上 on
那么我们需要原本的 $action 的值为 citylist 即可
再往上,又是那个三元运算符
如果为空,赋值为 ‘login’
否则还是原本的值
再往上
用indexget()函数处理
也就是说,get传参传入 action=citylist 即可


然后构造POC还需要一步,我们需要知道回显点和字段数
字段数有两种方法
白盒方法是(得先安装好CMS)
打开phpstudy,然后打开mysql管理器,找到这个表,数一数(点赞)(简单实用)
或者命令行登录进去
这里的 esp_ 是当时安装cms设置的数据表前缀
得到字段数为5


3. 靶场测试

另一种方法就是实测


然后查找回显点

最后构造 POC:
/adminsoft/index.php?archive=citylist&action=citylist&parentid=-1 union select 1,2,user(),4,5


总结

我也不知道这种刨根问底找到关于漏洞的每一句代码的意义对自己和对看文章的各位有没有什么帮助,还是只是浪费自己的时间,一句一句的追查回溯,总感觉好累,但是如果不知道那些东西干嘛用的,又不甘心,不过我也才刚刚起步,以后的路还很长,走到后面,就知道自己有没有走错路了(要是师傅们能指点一下就更好了)
代码在附件里

用户名金币积分时间理由
Track-JARVIS 31.00 0 2022-09-13 20:08:56 一个受益终生的帖子~~
Track-劲夫 100.00 0 2022-09-13 12:12:43 一个受益终生的帖子~~

打赏我,让我更有动力~

4 条回复   |  直到 7个月前 | 1106 次浏览

xiyue
发表于 2022-9-25

1

评论列表

  • 加载数据中...

编写评论内容

常长老
发表于 2023-4-5

师傅,一起学吗,看了你的文章很有感触,我学的时候也有过和你一样的疑问

评论列表

  • 加载数据中...

编写评论内容

beixiao
发表于 2023-4-22

2

评论列表

  • 加载数据中...

编写评论内容

xuejiuhan
发表于 7个月前

1

评论列表

  • 加载数据中...

编写评论内容
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.