strpos

image-20251019214951715

  • haystack

    在该字符串中进行查找。

  • needle

    要搜索的字符串。

  • offset

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

  • 返回值

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

    如果没找到 needle,将返回 **false**。

    警告:此函数可能返回布尔值 **false**,但也可能返回等同于 false 的非布尔值。请阅读 布尔类型章节以获取更多信息。应使用 === 运算符来测试此函数的返回值。

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$mystring = 'abc';
$findme = 'a';
$pos = strpos($mystring, $findme);

// 注意这里使用的是 ===。简单的 == 不能像我们期待的那样工作,
// 因为 'a' 是第 0 位置上的(第一个)字符。
if ($pos === false) {
echo "The string '$findme' was not found in the string '$mystring'";
} else {
echo "The string '$findme' was found in the string '$mystring'";
echo " and exists at position $pos";
}
?>

Demo分析

image-20251019215317907

我们看到 第11行第12行 ,程序通过格式化字符串的方式,使用 xml 结构存储用户的登录信息。实际上这样很容易造成数据注入。然后 第21行 实例化 Login 类,并在 第16行 处调用 login 方法进行登陆操作。在进行登录操作之前,代码在 第8行第9行 使用 strpos 函数来防止输入的参数含有 <** 和 **> 符号,猜测开发者应该是考虑到非法字符注入问题。

strpos 函数返回查找到的子字符串的下标。如果字符串开头就是我们要搜索的目标,则返回下标 0 ;如果搜索不到,则返回 false 。在这道题目中,开发者只考虑到 strpos 函数返回 false 的情况,却忽略了匹配到的字符在首位时会返回 0 的情况,因为 false0 的取反均为 true 。这样我们就可以在用户名和密码首字符注入 < 符号,从而注入xml数据。我们尝试使用以下 payload ,观察 strpos 函数的返回结果:

1
user=<"><injected-tag%20property="&pass=<injected-tag>

image-20251019220023977

如上图所示,很明显是可以注入xml数据的。

实例分析

环境:DeDecms V5.7SP2正式版

审计

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

image-20251019221427384

针对上面的代码做个分析,当 $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 == ‘’ 的判断结果:

image-20251019221710106

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

image-20251019221744862

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

image-20251019221813257

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

image-20251019221942740

该代码主要功能是发送邮件至相关邮箱,并且插入一条记录至 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 中:
image-20251019222320071

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

image-20251019222430345

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

image-20251019222516900

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

image-20251019222551822

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

漏洞利用

我们分别注册 test1test2 两个账号

第一步访问 payload 中的 url

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

这里 test2 的id是9

image-20251019222749751

image-20251019222800377

通过抓包获取到 key 值。

17

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

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

18

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

20

CTF例题练习

环境搭建

题目链接: https://pan.baidu.com/s/1pHjOVK0Ib-tjztkgBxe3nQ 密码: 59t2

image-20251019221134058

WP

题目为QCTF 2018中的一道题目,场景就是一个彩票系统,每位用户初始情况下有20$,由用户输入一个7位数,系统也会随机生成一个7位数。然后逐位数字进行比较,位数相同的个数越多,奖励的钱也越多。当你的钱足够买flag的时候,系统就会给你flag。

image-20251019224125847

我们来看一下后台代码是如何进行比较的,比较代码在 buy.php 文件中:

image-20251019224313114

在上图中看到表单的部分( 代码4-8行 ),调用了 js/buy.js 文件,应该是用来处理上面的表单的,我们具体看一下 js 代码:

image-20251019224531669

第10行 处看到,程序将表单数据以 json 格式提交到服务器端,提交页面为 api.php ,我们转到该文件看看:

image-20251019224836128

这里主要是对数字进行比较,注意 第13行 用的是 == 操作符对数据进行比较,这里会引发安全问题。因为用户的数据是以 json 格式传上来的,如果我们传一个数组,里面包含7个 true 元素,这样在比较的时候也是能相等的。因为 == 运算符只会判断两边数据的值是否相等,并不会判断数据的类型。而语言定义,除了 0、false、null 以外均为 true ,所以使用 true 和数字进行比较,返回的值肯定是 true 。只要后台生成的随机数没有数字0,我们传入的payload就能绕过每位数字的比较。我们发送几次payload后,就可以买到flag了。

image-20251021212439981