DiscuzCMS 代码审计(3.17更新ing)

文章目录
  1. 1. Discuz中的sql防御机制
  2. 2. Discuz中的文件上传检测机制
    1. 2.1. 检测上传文件的目录
    2. 2.2. 检测上传文件的大小以及名称
    3. 2.3. 获取文件的后缀名
    4. 2.4. 判断上传文件是否是图片
    5. 2.5. 获取上传文件的扩展名
    6. 2.6. 获取目标文件的存储路径
    7. 2.7. 完整存储路径
    8. 2.8. 关于保存
    9. 2.9. 关于报错信息
  3. 3. Discuz function模块下的function_member.php文件
    1. 3.1. 用户登录函数userlogin()
      1. 3.1.1. /uc_client/client.php
        1. 3.1.1.1. function uc_addslashes()
        2. 3.1.1.2. function dhtmlspecialchars()
        3. 3.1.1.3. function fsocketopen()
        4. 3.1.1.4. function uc_stripslashes()
        5. 3.1.1.5. function uc_api_post()
        6. 3.1.1.6. function uc_fopen2()
        7. 3.1.1.7. function uc_fopen()
        8. 3.1.1.8. function uc_api_mysql()

选择Discuz作为第一个审计的CMS大概是因为某天(具体日期我忘了)我下了个源码放在根目录下,that’s it。
Here we go~
审计环境:本地搭建的Discuz源码+phpStudy+MySQL+Windows

Discuz中的sql防御机制

DiscuzCMS配置文件/config/config_global.php文件下有一行代码是sql注入的过滤机制。

1
$_config['security']['querysafe']['status'] = 1;

往下看的参数都是过滤机制,直接过滤了sql注入需要用到很多关键字以及绕过字符。
关键字过滤

1
2
3
4
5
6
7
$_config['security']['querysafe']['daction']['0'] = '@';
$_config['security']['querysafe']['daction']['1'] = 'intooutfile';
$_config['security']['querysafe']['daction']['2'] = 'intodumpfile';
$_config['security']['querysafe']['daction']['3'] = 'unionselect';
$_config['security']['querysafe']['daction']['4'] = '(select';
$_config['security']['querysafe']['daction']['5'] = 'unionall';
$_config['security']['querysafe']['daction']['6'] = 'uniondistinct';

绕过字符过滤

1
2
3
4
5
$_config['security']['querysafe']['dnote']['0'] = '/*';
$_config['security']['querysafe']['dnote']['1'] = '*/';
$_config['security']['querysafe']['dnote']['2'] = '#';
$_config['security']['querysafe']['dnote']['3'] = '--';
$_config['security']['querysafe']['dnote']['4'] = '"';

Discuz执行SQL语句之前会调用\source\class\discuz_database.php文件下的discuz_database_safecheck类下面的checkquery($sql)函数进行过滤,跟踪该函数。

函数首先加载了config/security/querysafe这个文件,$config[‘status’]为真后$cmd的值由经过trim(),substr(),strtoupper()过滤后的$sql赋予。

1
2
3
4
相关php函数科普:
trim():取出php首尾的空格;
substr():substr($str1,num1,num2),从限定字符串中取出从num1到num2长度的内容;
strtoupper():将字符串转换为大写。

继续跟踪代码,检查到$cmd被被作为$checkcmd的一个键值被带入查询,接着调用了_do_query_safe($sql)这个函数,切换到这个函数定义。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
private static function _do_query_safe($sql) {
$sql = str_replace(array('\\\\', '\\\'', '\\"', '\'\''), '', $sql);
$mark = $clean = '';
if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false && strpos($sql, '@') === false && strpos($sql, '`') === false) {
$clean = preg_replace("/'(.+?)'/s", '', $sql);
} else {
$len = strlen($sql);
$mark = $clean = '';
for ($i = 0; $i < $len; $i++) {
$str = $sql[$i];
switch ($str) {
case '`':
if(!$mark) {
$mark = '`';
$clean .= $str;
} elseif ($mark == '`') {
$mark = '';
}
break;
case '\'':
if (!$mark) {
$mark = '\'';
$clean .= $str;
} elseif ($mark == '\'') {
$mark = '';
}
break;
case '/':
if (empty($mark) && $sql[$i + 1] == '*') {
$mark = '/*';
$clean .= $mark;
$i++;
} elseif ($mark == '/*' && $sql[$i - 1] == '*') {
$mark = '';
$clean .= '*';
}
break;
case '#':
if (empty($mark)) {
$mark = $str;
$clean .= $str;
}
break;
case "\n":
if ($mark == '#' || $mark == '--') {
$mark = '';
}
break;
case '-':
if (empty($mark) && substr($sql, $i, 3) == '-- ') {
$mark = '-- ';
$clean .= $mark;
}
break;

default:

break;
}
$clean .= $mark ? '' : $str;
}
}

if(strpos($clean, '@') !== false) {
return '-3';
}

$clean = preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]+/is", "", strtolower($clean));

if (self::$config['afullnote']) {
$clean = str_replace('/**/', '', $clean);
}

if (is_array(self::$config['dfunction'])) {
foreach (self::$config['dfunction'] as $fun) {
if (strpos($clean, $fun . '(') !== false)
return '-1';
}
}

if (is_array(self::$config['daction'])) {
foreach (self::$config['daction'] as $action) {
if (strpos($clean, $action) !== false)
return '-3';
}
}

if (self::$config['dlikehex'] && strpos($clean, 'like0x')) {
return '-2';
}

if (is_array(self::$config['dnote'])) {
foreach (self::$config['dnote'] as $note) {
if (strpos($clean, $note) !== false)
return '-4';
}
}

return 1;
}

if语句里面首先限定了不能存在的字符(/,#,–,@,’`’),满足条件后再次进行正则匹配来过滤,被preg_match()过滤后的字符被保存在$clean字符串里。

1
2
php函数科普
preg_match($pattern,$replacement,$str):依次是,正则表达式,符合正则表达式后需要被替换的词,需要进行正则替换的字符串。

代码继续向后看,使用了switch()语句对相关字符进行字符串添加,将字符添加到$clean里面再进行下面的操作。
首先检查‘@’,后面加上一个可怕的正则表达式。

1
$clean = preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]+/is", "", strtolower($clean));

直接过滤掉逗号等,往后看有一个细节:

$config[‘afullnote’]这个参数,回到之前的config_global.php文件:

查了一下资料发现这里的值是必须设置为0的,如果没有被设置为’0’,那么提交/sql/,中间的sql会被替换为空,后面有一个dnote检测,如果检测到’/‘或’/‘就直接返回‘-4’,说明检测到sql注入攻击,找到一个有趣的东西,通过报错注入来利用这里这个漏洞。

1
http://www.freebuf.com/articles/web/8038.html

Discuz中的文件上传检测机制

相关文件目录:

1
2
3
source\class\discuz\discuz_upload.php
source\function\function_core.php
static\js\home.js

Discuz下的文件过滤机制是定义了一个discuz_upload类来进行。
首先在初始化函数中对上传文件的参数(大小,名字,扩展名,判断是否是图片,其他拓展,目录)进行初始化。

检测上传文件的目录

1
2
3
function check_dir_type($type) {
return !in_array($type, array('forum', 'group', 'album', 'portal', 'common', 'temp', 'category', 'profile')) ? 'temp' : $type;
}

查看文件是从哪个目录下上传的。

检测上传文件的大小以及名称

使用函数intval()来对上传文件的大小进行过滤,使用trim()来删除文件名首尾的空格。

1
2
$attach['size'] = intval($attach['size']);
$attach['name'] = trim($attach['name']);

关于文件名那里有一个Discuz自带的一个函数:dhtmlspecialchars()。
我查了一下资料,可以参考这个:

1
http://m.blog.sina.com.cn/s/blog_8f9418df0100ugx1.html#page=1

获取文件的后缀名

1
$attach['ext'] = $this->fileext($attach['name']);

直接调用类内函数,跟踪ext判断函数

1
2
3
function fileext($filename) {
return addslashes(strtolower(substr(strrchr($filename, '.'), 1, 10)));
}

-w-这个有点点凶残,strrchr()函数找到上传文件名中’.’的最后位置,然后取出1到10位,进行小写转换后直接addslashes()过滤。
-tips:PHP相关函数科普

1
2
strrchr():两个参数,第一个字符串,第二个是待查找的子串,函数返回的结果是子串在字符串中出现的最后位置(从左往右)。
addslashes():对字符(单引号,双引号等)进行转义。

判断上传文件是否是图片

在初始化函数里只有一行代码

1
$attach['isimage'] = $this->is_image_ext($attach['ext']);

但是后面调用了一个类里面定义的函数,跟踪这个函数。

1
2
3
4
function is_image_ext($ext) {
static $imgext = array('jpg', 'jpeg', 'gif', 'png', 'bmp');
return in_array($ext, $imgext) ? 1 : 0;
}

定义一个静态数组后使用in_array()函数来进行判断后缀名是否属于其中的图片后缀,是一种基于白名单的过滤方式。
-idea:这里有一个小想法但是还没实践,Discuz是通过判断后缀名来识别图片,是否可以%00绕过或者写入一句话?有待证明。

获取上传文件的扩展名

well,操作同上,直接跟踪代码。

1
$attach['extension'] = $this->get_target_extension($attach['ext']);

同样也是白名单限制,和判断图片那里大同小异

1
2
3
4
function get_target_extension($ext) {
static $safeext = array('attach', 'jpg', 'jpeg', 'gif', 'png', 'swf', 'bmp', 'txt', 'zip', 'rar', 'mp3');
return strtolower(!in_array(strtolower($ext), $safeext) ? 'attach' : $ext);
}

获取目标文件的存储路径

1
$attach['attachdir'] = $this->get_target_dir($this->type, $extid);

直接跟踪函数好了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function get_target_dir($type, $extid = '', $check_exists = true) {

$subdir = $subdir1 = $subdir2 = '';
if($type == 'album' || $type == 'forum' || $type == 'portal' || $type == 'category' || $type == 'profile') {
$subdir1 = date('Ym');
$subdir2 = date('d');
$subdir = $subdir1.'/'.$subdir2.'/';
} elseif($type == 'group' || $type == 'common') {
$subdir = $subdir1 = substr(md5($extid), 0, 2).'/';
}

$check_exists && discuz_upload::check_dir_exists($type, $subdir1, $subdir2);

return $subdir;
}

如果是相册,论坛或者是简介等地方上传上来的,则是通过上传日期来作为目录并存储上传图像。
如果是group或者是common下则是通过md5加密后取前3位来命名文件目录。
最后检查目录是否存在,存在则返回目录~

完整存储路径

1
$attach['attachment'] = $attach['attachdir'].$this->get_target_filename($this->type, $this->extid, $this->forcename).'.'.$attach['extension'];

还是根据上传图片所在目录位置的不同来进行存储√

1
2
3
4
5
6
7
8
function get_target_filename($type, $extid = 0, $forcename = '') {
if($type == 'group' || ($type == 'common' && $forcename != '')) {
$filename = $type.'_'.intval($extid).($forcename != '' ? "_$forcename" : '');
} else {
$filename = date('His').strtolower(random(16));
}
return $filename;
}

关于保存

当前文件上传类下面一个save()函数用来存储上传的东西,这里还是直接用函数跟踪法好了。
追溯到save_to_local()这个函数,有两个参数,文件名和路径,在save()函数里面,参数$ignore的初始值是0,所以不会直接调用到save_to_local()这个函数,but还是追踪一下看看,save_to_local()里面有一个函数是is_upload_file()。

1
2
3
function is_upload_file($source) {
return $source && ($source != 'none') && (is_uploaded_file($source) || is_uploaded_file(str_replace('\\\\', '\\', $source)));
}

对上传的文件进行了判空和过滤操作,继续往下看,如果copy()函数调用成功,则表示保存成功,定义到copy函数的目录:

1
uc_server\lib\upload.class.php

1
2
3
4
function copy($sourcefile, $destfile) {
move_uploaded_file($sourcefile, $destfile);
@unlink($sourcefile);
}

tips:PHP相关函数科普:

1
2
3
move_uploaded_file():本函数检查并确保由 filename 指定的文件是合法的上传文件(即通过 PHP 的 HTTP POST 上传机制所上传的)。如果文件合法,则将其移动为由 destination 指定的文件。

unlink():删除 filename。和 Unix C 的 unlink() 函数相似。 发生错误时会产生一个 E_WARNING 级别的错误。所以使用的时候加了一个@符号来禁止报错。

继续往下看save_to_local()下面的代码

1
elseif(function_exists('move_uploaded_file') && @move_uploaded_file($source, $target))

它通过判断是否存在move_uploaded_file函数和是否已经执行成功该函数来判断是否上传成功,继续往下推进。

1
2
3
4
5
6
7
8
elseif (@is_readable($source) && (@$fp_s = fopen($source, 'rb')) && (@$fp_t = fopen($target, 'wb'))) {
while (!feof($fp_s)) {
$s = @fread($fp_s, 1024 * 512);
@fwrite($fp_t, $s);
}
fclose($fp_s); fclose($fp_t);
$succeed = true;
}

tips:PHP相关函数科普:

1
2
3
is_readable():判断给定文件名是否存在并且可读。
feof():测试文件指针是否到了文件结束的位置。
fread():从文件指针 handle 读取最多 length 个字节。

成功读写完毕后正常保存。
关于save()调用的类内定义函数,还有一个函数值得注意:get_image_info()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function get_image_info($target, $allowswf = false) {
$ext = discuz_upload::fileext($target);
$isimage = discuz_upload::is_image_ext($ext);
if(!$isimage && ($ext != 'swf' || !$allowswf)) {
return false;
} elseif(!is_readable($target)) {
return false;
} elseif($imageinfo = @getimagesize($target)) {
list($width, $height, $type) = !empty($imageinfo) ? $imageinfo : array('', '', '');
$size = $width * $height;
if($size > 16777216 || $size < 16 ) {
return false;
} elseif($ext == 'swf' && $type != 4 && $type != 13) {
return false;
} elseif($isimage && !in_array($type, array(1,2,3,6,13))) {
return false;
} elseif(!$allowswf && ($ext == 'swf' || $type == 4 || $type == 13)) {
return false;
}
return $imageinfo;
} else {
return false;
}
}

不是图像或者不可读直接false,继续往下看代码。
$imageinfo那里通过getimgaesize()函数来获取图像宽,高,类型等信息后,如果图像信息不为空,则使用list()来进行赋值,后面限制了图片的大小,后缀等等来判断上传。

关于报错信息

这个倒是很简单,两个类内定义的函数就搞定了。

1
2
3
4
5
6
7
function error() {
return $this->errorcode;
}

function errormessage() {
return lang('error', 'file_upload_error_'.$this->errorcode);
}

Discuz function模块下的function_member.php文件

所以,还是从头开始分析…

用户登录函数userlogin()

根据登录的位置来设定一个初始的isuid值,判断是否加载了uc_user_login这个函数,如果没有,就先加载function_core.php下的loaducenter()函数。

1
2
3
4
function loaducenter() {
require_once DISCUZ_ROOT.'./config/config_ucenter.php';
require_once DISCUZ_ROOT.'./uc_client/client.php';
}

先查看一下/config/目录和/uc_client/的两个PHP文件。
config_ucenter.php下是该CMS框架相关数据库信息,转回到client.php文件目录下,这个php文件有641行代码,我准备从这里开始过一下这个文件。

/uc_client/client.php

function uc_addslashes()

这是一个基于魔术引号过滤处理的函数。

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

首先对传入的变量进行相关判断,如果传入的参数是一个数组,则取出数组对应的值调用自身进行过滤,反之,则先直接去掉反斜杠,再调用addslashes()函数。

function dhtmlspecialchars()
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
if(!function_exists('dhtmlspecialchars')) {
function dhtmlspecialchars($string, $flags = null) {
if(is_array($string)) {
foreach($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val, $flags);
}
} else {
if($flags === null) {
$string = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $string);
if(strpos($string, '&amp;#') !== false) {
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
} else {
if(PHP_VERSION < '5.4.0') {
$string = htmlspecialchars($string, $flags);
} else {
if(strtolower(CHARSET) == 'utf-8') {
$charset = 'UTF-8';
} else {
$charset = 'ISO-8859-1';
}
$string = htmlspecialchars($string, $flags, $charset);
}
}
}
return $string;
}
}

基本操作和uc_addslashes()是一样的,根据是不是数组来进行第一次操作,然后把穿进来的字符串中的(‘&’, ‘“‘, ‘<’, ‘>’)等替换成对应字符经过html编码后的版本。
继续推进,如果’&#’在字符串中,则直接对字符串进行正则匹配,反之,如果PHP的版本低于5.4.0则直接调用内置htmlspecialchars()将相关字符转码为html实体。
如果PHP的版本高于5.4.0,则把字符串转化为UTF-8或者ISO-8859-1再调用htmlspecialchars(),最后返回成功过滤的$string。

function fsocketopen()
1
2
3
4
5
6
7
8
9
10
11
12
13
if(!function_exists('fsocketopen')) {
function fsocketopen($hostname, $port = 80, &$errno, &$errstr, $timeout = 15) {
$fp = '';
if(function_exists('fsockopen')) {
$fp = @fsockopen($hostname, $port, $errno, $errstr, $timeout);
} elseif(function_exists('pfsockopen')) {
$fp = @pfsockopen($hostname, $port, $errno, $errstr, $timeout);
} elseif(function_exists('stream_socket_client')) {
$fp = @stream_socket_client($hostname.':'.$port, $errno, $errstr, $timeout);
}
return $fp;
}
}

一个文件函数,限制了端口,错误号,错误信息以及时延。

function uc_stripslashes()
1
2
3
4
5
6
7
8
function uc_stripslashes($string) {
!defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
if(MAGIC_QUOTES_GPC) {
return stripslashes($string);
} else {
return $string;
}
}

如果开启了魔术引号,则去除转义的反斜杠。

function uc_api_post()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function uc_api_post($module, $action, $arg = array()) {
$s = $sep = '';
foreach($arg as $k => $v) {
$k = urlencode($k);
if(is_array($v)) {
$s2 = $sep2 = '';
foreach($v as $k2 => $v2) {
$k2 = urlencode($k2);
$s2 .= "$sep2{$k}[$k2]=".urlencode(uc_stripslashes($v2));
//这里需要注意,{$k}外面的花括号是明确变量的意思,在这里其实就是$sep2和$k[$k2]合在一起。
$sep2 = '&';
}
$s .= $sep.$s2;
} else {
$s .= "$sep$k=".urlencode(uc_stripslashes($v));
}
$sep = '&';
}
$postdata = uc_api_requestdata($module, $action, $s);
return uc_fopen2(UC_API.'/index.php', 500000, $postdata, '', TRUE, UC_IP, 20);
}

对函数进行功能分析,传入的参数$arg是一个数组,使用键值的方式对数组进行循环,如果键值里面还包含数组,键值对应的数组取出来,对该数组的单个值分别进行魔术引号判断过滤再进行url加密后拼接到$s2上,最后拼接到$s串上。
同时,如果不是键值对应的不是数组,则是直接进行魔术引号过滤后直接进行url加密。
字符串拼接完成后调用了uc_api_requestdata()后,追踪这个函数,在该函数的第一个语句又调用了另外一个函数,uc_api_input(),all right,继续跟踪这个函数,在进行url加密前却调用了另外一个函数:uc_authcode(),这个函数用来对传入的参数进行加密。
整个的函数调用关系如下:

数据进行加密后重新返回原函数function uc_api_post(),返回值那里调用到了另外一个函数uc_fopen2()…

function uc_fopen2()

函数里面有8个参数,url,限制,发送的数据,cookie,socket,ip,时限,模块这几个参数。

1
2
3
4
5
6
7
8
function uc_fopen2($url, $limit = 0, $post = '', $cookie = '', $bysocket = FALSE, $ip = '', $timeout = 15, $block = TRUE) {
$__times__ = isset($_GET['__times__']) ? intval($_GET['__times__']) + 1 : 1;
if($__times__ > 2) {
return '';
}
$url .= (strpos($url, '?') === FALSE ? '?' : '&')."__times__=$__times__";
return uc_fopen($url, $limit, $post, $cookie, $bysocket, $ip, $timeout, $block);
}

判断是否通过GET方式获取了time的值,如果$time大于2,直接返回空,反之,根据是否在url后面有参数来添加这个参数time,然后调用uc_fopen()。

function uc_fopen()

这个函数的参数构造和uc_fopen2()的参数构造是一样的,直接看它的内容好了。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
function uc_fopen($url, $limit = 0, $post = '', $cookie = '', $bysocket = FALSE, $ip = '', $timeout = 15, $block = TRUE) {
$return = '';
$matches = parse_url($url);
!isset($matches['scheme']) && $matches['scheme'] = '';
!isset($matches['host']) && $matches['host'] = '';
!isset($matches['path']) && $matches['path'] = '';
!isset($matches['query']) && $matches['query'] = '';
!isset($matches['port']) && $matches['port'] = '';
$scheme = $matches['scheme'];
$host = $matches['host'];
$path = $matches['path'] ? $matches['path'].($matches['query'] ? '?'.$matches['query'] : '') : '/';
$port = !empty($matches['port']) ? $matches['port'] : 80;
if($post) {
$out = "POST $path HTTP/1.0\r\n";
$header = "Accept: */*\r\n";
$header .= "Accept-Language: zh-cn\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$header .= "Host: $host\r\n";
$header .= 'Content-Length: '.strlen($post)."\r\n";
$header .= "Connection: Close\r\n";
$header .= "Cache-Control: no-cache\r\n";
$header .= "Cookie: $cookie\r\n\r\n";
$out .= $header.$post;
} else {
$out = "GET $path HTTP/1.0\r\n";
$header = "Accept: */*\r\n";
$header .= "Accept-Language: zh-cn\r\n";
$header .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$header .= "Host: $host\r\n";
$header .= "Connection: Close\r\n";
$header .= "Cookie: $cookie\r\n\r\n";
$out .= $header;
}

$fpflag = 0;
if(!$fp = @fsocketopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout)) {
$context = array(
'http' => array(
'method' => $post ? 'POST' : 'GET',
'header' => $header,
'content' => $post,
'timeout' => $timeout,
),
);
$context = stream_context_create($context);
$fp = @fopen($scheme.'://'.($ip ? $ip : $host).':'.$port.$path, 'b', false, $context);
$fpflag = 1;
}

if(!$fp) {
return '';
} else {
stream_set_blocking($fp, $block);
stream_set_timeout($fp, $timeout);
@fwrite($fp, $out);
$status = stream_get_meta_data($fp);
if(!$status['timed_out']) {
while (!feof($fp) && !$fpflag) {
if(($header = @fgets($fp)) && ($header == "\r\n" || $header == "\n")) {
break;
}
}

$stop = false;
while(!feof($fp) && !$stop) {
$data = fread($fp, ($limit == 0 || $limit > 8192 ? 8192 : $limit));
$return .= $data;
if($limit) {
$limit -= strlen($data);
$stop = $limit <= 0;
}
}
}
@fclose($fp);
return $return;
}
}

–tips PHP相关函数科普

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
parse_url():返回url的组成部分。

比如:
<?php
print_r(parse_url('http://php.net/manual/zh/function.parse-url.php'));
?>

Array
(
[scheme] => http
[host] => php.net
[path] => /manual/zh/function.parse-url.php
)

---
stream_set_blocking():为PHP资源流设置阻塞;
stream_set_timeout():当流超时时,由stream_get_meta_data()返回的数组的'timed_out'键 被设置为TRUE,尽管没有生成错误/警告。

首先通过url_parse()函数获取相关信息,然后调用fsocketopenz()这个函数来进行初步文件写入操作($fp)。
如果$fp为空,直接返回空,反之,对网页进行写入然后返回。

function uc_api_mysql()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function uc_api_mysql($model, $action, $args=array()) {
global $uc_controls;
if(empty($uc_controls[$model])) {
if(function_exists("mysql_connect")) {
include_once UC_ROOT.'./lib/db.class.php';
} else {
include_once UC_ROOT.'./lib/dbi.class.php';
}
include_once UC_ROOT.'./model/base.php';
include_once UC_ROOT."./control/$model.php";
eval("\$uc_controls['$model'] = new {$model}control();");
}
if($action{0} != '_') {
$args = uc_addslashes($args, 1, TRUE);
$action = 'on'.$action;
$uc_controls[$model]->input = $args;
return $uc_controls[$model]->$action($args);
} else {
return '';
}
}

这个实际上是一个数据库函数。