strpos

1
strpos(string $haystack, string $needle, int $offset = 0): int|false

参数

  • haystack

    在该字符串中进行查找。

  • needle

    要搜索的字符串。

  • offset

    如果提供了此参数,搜索会从字符串该字符数的起始位置开始统计。 如果是负数,搜索会从字符串结尾指定字符数开始。

返回值

返回 needle 存在于 haystack 字符串起始的位置(独立于 offset)。 同时注意字符串位置是从0开始,而不是从1开始的。如果没找到 needle,将返回 false

警告

此函数可能返回布尔值 false,但也可能返回等同于 false 的非布尔值。应使用 [=== 运算符]来测试此函数的返回值。

Demo分析

1

很简单的例子。本次漏洞是开发者对 strpos 函数理解不够,或者说是开发者考虑不周,导致过滤方法可被绕过。

Demo接收用户输入的user和pass然后嵌入<xml>里解析,为了防止用户XML注入闭合掉标签,于是开发使用了strpos函数匹配指定字符>和<的位置,如果匹配到,就不进入if。但是开发没有使用===强比较,导致strpos返回0时明明表示待查找的字符串就在第一个位置,但是却因为使用了弱比较的原因直接使得if为真了导致XML注入的绕过

实例分析

本次案例,我们选取 DeDecms V5.7SP2正式版 进行分析,该CMS存在未修复的任意用户密码重置漏洞。漏洞的触发点在 member/resetpassword.php 文件中,由于对接收的参数 safeanswer 没有进行严格的类型判断,导致可以使用弱类型比较绕过。我们来看看相关代码:

6

针对上面的代码做个分析,当 $dopost 等于 safequestion 的时候,通过传入的 $mid 对应的 id 值来查询对应用户的安全问题、安全答案、用户id、电子邮件等信息。跟进到 第11行 ,当我们传入的问题和答案非空,而且等于之前设置的问题和答案,则进入 sn 函数。然而这里使用的是 == 而不是 === 来判断,所以是可以绕过的。假设用户没有设置安全问题和答案,那么默认情况下安全问题的值为 0 ,答案的值为 null (这里是数据库中的值,即 $row[‘safequestion’]=”0”$row[‘safeanswer’]=null )。当没有设置 safequestionsafeanswer 的值时,它们的值均为空字符串。第11行的if表达式也就变成了 if(‘0’ == ‘’ && null == ‘’) ,即 if(false && true) ,所以我们只要让表达式 $row[‘safequestion’] == $safequestiontrue 即可。下图是 null == ‘’ 的判断结果:

7

我们可以利用 php弱类型 的特点,来绕过这里 $row[‘safequestion’] == $safequestion 的判断,如下:

9

通过测试找到了三个的payload,分别是 0.00.0e1 ,这三种类型payload均能使得 $row[‘safequestion’] == $safequestiontrue ,即成功进入 sn 函数。跟进 sn 函数,相关代码在 member/inc/inc_pwd_functions.php 文件中,具体代码如下:

10

sn 函数内部,会根据id到pwd_tmp表中判断是否存在对应的临时密码记录,根据结果确定分支,走向 newmail 函数。假设当前我们第一次进行忘记密码操作,那么此时的 $row 应该为空,所以进入第一个 if(!is_array($row)) 分支,在 newmail 函数中执行 INSERT 操作,相关操作代码位置在 member/inc/inc_pwd_functions.php 文件中,关键代码如下:

11

该代码主要功能是发送邮件至相关邮箱,并且插入一条记录至 dede_pwd_tmp 表中。而恰好漏洞的触发点就在这里,我们看看 第13行第18行 的代码,如果 ($send == ‘N’) 这个条件为真,通过 ShowMsg 打印出修改密码功能的链接。 第17行 修改密码链接中的 $mid 参数对应的值是用户id,而 $randval 是在第一次 insert 操作的时候将其 md5 加密之后插入到 dede_pwd_tmp 表中,并且在这里已经直接回显给用户。那么这里拼接的url其实是

1
http://127.0.0.1/member/resetpassword.php?dopost=getpasswd&id=$mid&key=$randval

继续跟进一下 dopost=getpasswd 的操作,相关代码位置在 member/resetpassword.php 中,

12

在重置密码的时候判断输入的用户id是否执行过重置密码,如果id为空则退出;如果 $row 不为空,则会执行以下操作内容,相关代码在 member/resetpassword.php 中。

13

上图代码会先判断是否超时,如果没有超时,则进入密码修改页面。在密码修改页面会将 $setp 赋值为2。

14

由于现在的数据包中 $setp=2 ,因此这部分功能代码实现又回到了 member/resetpassword.php 文件中。

15

上图代码 第6行 判断传入的 $key 是否等于数据库中的 $row[‘pwd’] ,如果相等就完成重置密码操作,至此也就完成了整个攻击的分析过程。

简易流程图

image-20250125083434320

漏洞验证

我们分别注册 test1test2 两个账号

第一步访问 payload 中的 url

1
http://127.0.0.1/dedecms/member/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=9

这里 test2 的id是9

19

16

通过抓包获取到 key 值。

17

去掉多余的字符访问修改密码链接

1
http://192.168.31.240/dedecms/member/resetpassword.php?dopost=getpasswd&id=9&key=OTyEGJtg

18

最后成功修改密码,我将密码修改成 123456 ,数据库中 test2 的密码字段也变成了 123456 加密之后的值。

20

修复建议

针对上面 DeDecms任意用户密码重置 漏洞,我们只需要使用 === 来代替 == 就行了。因为 === 操作会同时判断左右两边的值和数据类型是否相等,若有一个不等,即返回 false 。具体修复代码如下:

21