热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

基于AST抽象语法树的SQL注入检测(2)每周小结(0102~0108)

本周继续学习AST的SQL语法检测原理的学习,文章的接下来部分准备分为2部分进行学习:1.SQL注入语法防御规则2.druid中SQL注入防御模块sql-wall1.相关学

本周继续学习AST的SQL语法检测原理的学习,文章的接下来部分准备分为2部分进行学习:


1. SQL注入语法防御规则
2. druid中SQL注入防御模块sql-wall

 

 

 

1. 相关学习资料

http://code.alibabatech.com/wiki/display/Druid/WallFilter

http://code.alibabatech.com/wiki/display/Druid/WallFilterConfig

http://code.alibabatech.com/wiki/display/Druid/Get+Druid

 

 

2. 我对数据库防火墙的理解

bubuko.com,布布扣

数据库防火墙位于前端应用层之后,前端的应用层可以是PHP、ASP、Java等,这些语言通过一些统一的访问接口(ODBC、JDBC)对数据库系统发起访问请求

所以到了数据库这一层的都是纯的SQL请求,所以在数据库这一层面要考虑的不是一些应用系统的ODAY、本地变量覆盖的漏洞,而应该明确我们所处的防御层面,我们要防御的是黑客针对数据库发起的攻击。



1. 针对数据库的缓冲区溢出攻击: 这个是实战中很少见
http:
//www.yesky.com/94/1828594.shtml
2. 针对数据库底层代码的极限领域的攻击,例如,这是在一个CTF中出现过的Mysql Attack Topic:
php
# GOAL: dump the info for the secret id
require
db.inc.php;
$id
= @(float)$_GET[id];
die(var_dump($id));
$secretId
= 1;
if($id == $secretId)
{
echo
Invalid id (.$id.).;
}
else
{
$query
= SELECT * FROM users WHERE id = \‘.$id.\‘;;
$result
= mysql_query($query);
$row
= mysql_fetch_assoc($result);

echo
"id: ".$row[id]."
";
echo
"name:".$row[name]."
";
}
?>
http:
//localhost/php4fun/index.php?id=1.0000000000001
攻击者的目标的是要查出id为1的admin的数据,这里的绕过思路是利用了Mysql的精度范围和PHP的精度范围不同,精度小的会忽略不能支持的位数。也就是说,浮点型的精度有上限和下限
3. 纯粹的拼接SQL语法对数据进行注入攻击: 这是最常见的,我们接下来重点分析这方面内容

 

 

 

 

3. SQL注入语法防御规则

目 前,druid的防御重点主要放在拼接型的SQL注入攻击,即利用注入点在原始的SQL语句的中间或后面"插入"、"拼接"上攻击性的SQL
Payload,从而达到提取非法数据等目的,缓冲区溢出和特殊情况的攻击druid暂时没有实现,将放到未来的版本中逐渐完善,下面根据温少的文档、并
配合druid的源代码进行学习进行具体规则的学习:



0x1 只允许执行增删改查基本语句
\druid\src\main\java\com\alibaba\druid\wall\WallConfig.java(druid的源码和整体架构放在文章的后半部分)
....
//是否允许非以上基本语句的其他语句,缺省关闭,通过这个选项就能够屏蔽DDL。
private boolean nOneBaseStatementAllow= false;
....
这是最严格模式,但是也最缺乏灵活性,基本上是不能开启的,在正常的用户业务需求中,必不可少会用到除了CRUD(增删改查)之外的需求,开启这条规则会导致大量的误报,
故druid默认关闭这个开关

 

 



0x2 不允许一次执行多条语句
每次只允许执行一条SQL,一次执行多条SQL,是被认为可能正被SQL注入攻击。
1. sql server 6.0在其架构中引入了服务端游标,从而允许在同一连接句柄上执行包含多条语句的字符串。所有6.0之后的sql server版本均支持该功能且允许执行下列语句:
select id from users;select name from users;
客户端连接到sql服务器并依次执行每条语句,数据库服务器向客户端返回每条语句发送的结果集。
http:
//database.51cto.com/art/201007/213806.htm
2. mysql在4.1及之后的版本中也引入了该功能,但是PHP自身限制了这种用法。
php
$con = mysql_connect("127.0.0.1", "root" , "111");
mysql_select_db(
"php4fun_", $con);
$sql
= "update users set level=2;update users set pass=3;";
$result
= mysql_query($sql, $con);
echo mysql_error();
if($result)
{
$result_array
= mysql_fetch_array($result);
var_dump($result_array);
}
?>
result:
You have an error
in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to
use near select 1,2,3,4 from dual at line 1
而如果使用的PDO方式操作数据库
php
$db = new PDO("mysql:host=localhost:3306;dbname=php4fun_", root, 111);
$sql
= "update users set level=2;update users set pass=3;";
try
{
$db
->query($sql);
}
catch(PDOException $e)
{
echo $e
->getMessage();
die();
}
?>
result: ok
3. oracle不支持多条语句,除非使用PL/SQL
\druid\src\main\java\com\alibaba\druid\wall\WallConfig.java
....
private boolean multiStatementAllow = false;
....
druid默认是禁止这种格式的SQL语句的,也即如果在传入的SQL语句中解析出了2条及以上的SQLStatement(一个SQLStatement抽象了一条SQL语句)就判断为注入攻击

 

 



0x3 不允许访问系统表
在之前的学习笔记中,有总结过,从攻击者渠道的角度去理解,攻击者最终的目的是要获取信息
http:
//www.cnblogs.com/LittleHann/p/3495602.html
"访问系统表"就是获取信息的渠道之一,故需要拦截之
但是druid对这种规则的判断更加细化,druid只拦截在子句中出现的连接系统表查询,举例说明:
1. select * from information_schema.COLUMNS;
这条语句druid认为是合法的,因为这条语句没有注入点的存在,SQL语句本身的唯一目的就是查询系统表,说明用户在进行正常的业务操作
2. SELECT id
FROM admin
WHERE id = 1
    AND 5 = 6
UNION
SELECT concat(0x5E252421, COUNT(8), 0x2A5B7D2F)
FROM (SELECT `column_name`, `data_type`, `character_set_name`
    FROM `information_schema`.`COLUMNS`
    WHERE TABLE_NAME = 0x73696E6765725F616C62756D
        AND TABLE_SCHEMA = 0x796971696C61695F757466
    ) t
这条语句druid认为是非法的注入攻击,因为SQL在子句(可能是注入点的地方)采取了union拼接,进行了连接系统表的查询的操作
druid通过判断information_schema在AST层次结构中的位置,具体来说就是判断它的父节点是否为
"SQL表达式"(例如union select)、以及它的左节点是否为"From节点"
即满足子句拼接的模式。以此来判断这条SQL语句是否有攻击性,在代码中的体现就是
druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
.....
boolean sameToTopSelectSchema
= false;
if (parent instanceof SQLSelectStatement)
{
SQLSelectStatement selectStmt
= (SQLSelectStatement) parent;
SQLSelectQuery query
= selectStmt.getSelect().getQuery();
if (query instanceof SQLSelectQueryBlock)
{
SQLSelectQueryBlock queryBlock
= (SQLSelectQueryBlock) query;
SQLTableSource
from = queryBlock.getFrom();
while (from instanceof SQLJoinTableSource)
{
  
from = ((SQLJoinTableSource) from).getLeft();
}
if (from instanceof SQLExprTableSource)
{
SQLExpr expr
= ((SQLExprTableSource) from).getExpr();
if (expr instanceof SQLPropertyExpr)
{
SQLExpr schemaExpr
= ((SQLPropertyExpr) expr).getOwner();
if (schemaExpr instanceof SQLIdentifierExpr)
{
  String schema
= ((SQLIdentifierExpr) schemaExpr).getName();
  schema
= form(schema);
  
if (schema.equalsIgnoreCase(owner))
  {
  sameToTopSelectSchema
= true;
  }
}
 }
}
}
}
if (!sameToTopSelectSchema)
{
addViolation(visitor, ErrorCode.SCHEMA_DENY,
"deny schema : " + owner, x);
}
而代码中的owner是从配置文件中读取的:
String owner
= ((SQLName) x).getSimleName();
owner
= WallVisitorUtils.form(owner);
if (isInTableSource(x) && !visitor.getProvider().checkDenySchema(owner))
{
...
配置文件被统一放在了:
\druid\src\main\resources\META
-INF\druid\wall\mysql\deny-schema.txt
information_schema
mysql
performance_schema
这样,druid就完成了对SQL中的对系统敏感表的注入的智能检测

 

 


0x4 不允许访问系统对象
在sqlserver中有系统对象的概念。对敏感系统对象
"sysobject"的检测也是同样的原理,即只检测子句的非法连接,并从配置文件中读取拦截列表,代码和对系统表的检测是类似的

 

 



0x5 不允许访问系统变量
系统敏感变量同样也是攻击者获取非法数据的一种渠道,druid采取智能判断的做法,举例说明:
1. select @@basedir;
这条语句druid不做拦截,因为这里没有注入点的存在,也就不可能是黑客的注入攻击,应该归类于业务的正常需要
2. SELECT * FROM cnp_news where id=23 and len(@@version)>0 and 1=1
这条语句druid会做拦截,攻击者在子句中利用逻辑表达式进行非法的探测注入,目前druid的检测机制是
"黑名单机制",把需要禁止的系统变量写在了配置文件中:
druid\src\main\resources\META
-INF\druid\wall\mysql\deny-variant.txt
basedir
version_compile_os
version
datadir
druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
...
if (!checkVar(x.getParent(), x.getName()))
{
boolean isTop
= WallVisitorUtils.isTopNoneFromSelect(this, x);
if (!isTop)
{
boolean allow
= true;
if (WallVisitorUtils.isWhereOrHaving(x) && isDeny(varName))
{
allow
= false;
}
if (!allow)
{
violations.add(
new IllegalSQLObjectViolation(ErrorCode.VARIANT_DENY, "variable not allow : " + x.getName(), toSQL(x)));
}
}
}
...

 

 



0x6 不允许访问系统函数
"系统敏感表""系统敏感对象""系统敏感变量"一样,系统敏感函数也是攻击者用来获取非法信息的一种手段之一
druid中和禁用系统函数的配置文件:
druid\src\main\resources\META
-INF\druid\wall\mysql\deny-function.txt
version
load_file
database
schema
user
system_user
session_user
benchmark
current_user
sleep
xmltype
receive_message
对于系统敏感函数的禁用,这里要注意一下,和系统表的防御思想类型,druid会智能地判断敏感函数在SQL语句中出现的位置,例如:
1. select load_file(\\etc\\passwd);
druid不会拦截这条语句,还是同样的道理,SQL注入的关键在于注入点,这条语句没有注入点的存在,所以只能是用户正常的业务需求
2. select * from admin where id =(SELECT 1 FROM (SELECT SLEEP(0))A);
druid会智能地检测出这个敏感函数出现在
"where子句节点"中,而"where子句节点"经常被黑客用来当作一个SQL注入点,故druid拦截之
代码如下:
druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
public static void checkFunction(WallVisitor visitor, SQLMethodInvokeExpr x)
{
final WallTopStatementContext topStatementContext
= wallTopStatementContextLocal.get();
if (topStatementContext != null && (topStatementContext.fromSysSchema || topStatementContext.fromSysTable))
{
return;
}
checkSchema(visitor, x.getOwner());
if (!visitor.getConfig().isFunctionCheck())
{
return;
}
String methodName
= x.getMethodName().toLowerCase();
WallContext context
= WallContext.current();
if (context != null)
{
context.incrementFunctionInvoke(methodName);
}
if (!visitor.getProvider().checkDenyFunction(methodName))
{
boolean isTopNoneFrom
= isTopNoneFromSelect(visitor, x);
if (isTopNoneFrom)
{
return;
}
boolean isShow
= x.getParent() instanceof MySqlShowGrantsStatement;
if (isShow)
{
return;
}
if (isWhereOrHaving(x))
{
addViolation(visitor, ErrorCode.FUNCTION_DENY,
"deny function : " + methodName, x);
}
}
}

 

 



0x7 不允许出现注释
正常执行的SQL是不应该附带注释的,有注释的SQL都会被认为是危险操作。druid是默认
"禁止"单行注释和多行注释。这里所谓的"禁止"是值druid会在解析前自动地去除原始SQL语句中的注释。
例如攻击者常用的绕过方式:
1) sel/**/ect us/**/er() from dual;  (黑客常用来绕过基于正则前端WAF)
2) select * from admin where no=4 and 1=2 /!40001+union/ select 1,concat(database(),0x5c,user(),0x5c,version()),3,4,5,6,7
(Mysql的comment dynamic execution bypass)
http:
//www.freebuf.com/articles/web/22041.html

这里druid采取的防御思路是
"规范化",代码自动会将注释的部分删除,重新拼接SQL语句后,对"规范化"后的语句再进行注入检测,删除注释的代码逻辑在词法解析器中:
druid\src\main\java\com\alibaba\druid\sql\parser\Lexer.java
..
protected boolean skipComment = true;
..
public final void nextToken()
{
      ....
      
/*
解析‘#‘注释符
判断‘#‘解析出的节点是‘单行注释‘、或‘多行注释‘
*/
case #:
scanSharp();
if ((token() == Token.LINE_COMMENT || token() == Token.MULTI_LINE_COMMENT) && skipComment)
{
bufPos
= 0;
continue;
}
return;
      ....
      
/*
检测是否是‘--‘这种单行注释符
*/
if (subNextChar == -)
{
scanComment();
if ((token() == Token.LINE_COMMENT || token() == Token.MULTI_LINE_COMMENT) && skipComment)
{
bufPos
= 0;
continue;
}
}
      ...
      
/*
判断当前节点是否是 /*
*/ 这种类型的多行注释
*/
if (nextChar == / || nextChar == *)
{
scanComment();
if ((token() == Token.LINE_COMMENT || token() == Token.MULTI_LINE_COMMENT) && skipComment)
{
bufPos
= 0;
continue;
}
}
...
在对SQL的词法解析的过程中,druid就会自动地对各种形式的注释符进行删除,删除了注释后,druid再去解析SQL语句,这个时候会出现两个情况:
1) 解析失败抛异常,说明原本的SQL语句很有可能是攻击型的SQL语句,黑客使用了注释绕过或者注释执行技术
2) 解析正常,说明这是正常的SQL语句,不排除有的程序猿会把一些简短的注释写在SQL语句中,但是这个注释的删除对原本的执行没有影响,所以也就判定为合理SQL语句
Oracle Hints的语法是
/* + */,druid能够区分注释和Hints

 

 



0x8 禁止永真条件
永真的注入是黑客在攻击中最常见的攻击手段,黑客通过注入
"永真表达式"来探测当前"用户可控的输入点"是否可以转化为"可以导致注入的注入点"
但是druid的永真检测并不是简单的
"等式匹配",而是对真正黑客可能采用的攻击模式进行结果化的匹配。
例如:
1) 正常的业务语句
SELECT F1, F2 FROM ADMIN WHERE
1 = 1; -- 允许
SELECT F1, F2 FROM ADMIN WHERE
0 = 0; -- 允许
SELECT F1, F2 FROM ADMIN WHERE
1 != 0; -- 允许
SELECT F1, F2 FROM ADMIN WHERE
1 != 2; -- 允许
这里允许的理由是,在正常的业务中有可能有这样的语句:
php
...
  $sql = "SELECT F1, F2 FROM ADMIN WHERE 1 = $id";
..
//这是很常见的业务语句,当外部系统传入的$id=1的时候,到了数据库驱动层这里看到的语句就是: SELECT F1, F2 FROM ADMIN WHERE 1 = 1 了。但这并不能算是一条永真注入探测语句。
所以,druid目前的规则允许的判断方式是,在where子句(where节点的子节点)中只有一个"等于""不等于"的二元操作表达式(上面给出的例子),druid会判断为合法。
druid对永真注入探测的防御重点是针对where子句(where节点的子节点)后面的永真逻辑的判断,对where子句中超过2个及以上的永真逻辑表达式进行拦截,例如:
select * from admin where id =-1 OR 17-7=10;      -- 拦截
select * from admin where id =-1 and 1=2       -- 拦截
select * from admin where id =-1 and 2>1       -- 拦截
select * from admin where id =-1 and a!=b      -- 拦截
select * from admin where id =-1 and char(32)>char(31) -- 拦截
select * from admin where id =-1 and 1 like 1     -- 拦截
select * from admin where id =-1 and 17-1=10    -- 拦截
select * from admin where id =-1 and NOT (1 != 2 AND 2 != 2) --拦截
select * from admin where id =-1 and id like %% -- 拦截
select * from admin where id =-1 and length(abcde) >= 5 -- 拦截
druid的实现核心代码如下:
druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
public static void checkSelelct(WallVisitor visitor, SQLSelectQueryBlock x)
{
...
/*
目前druid只针对where节点进行判断,下一版本会提供对order by和group by类型节点的判断
*/
if (Boolean.TRUE == whereValue)
{
if (queryBlockFromIsNull(visitor, x, false))
{
addViolation(visitor, ErrorCode.EMPTY_QUERY_HAS_CONDITION,
"empty select has condition", x);
}
if (!isSimpleConstExpr(where))
{
// 简单表达式
addViolation(visitor, ErrorCode.ALWAY_TRUE, "select alway true condition not allow", x);
}
}
..

 

 



0x9 Getshell
1) into outfile
黑客常常使用这个技术利用注入点进行磁盘写入。进而getshell,获得目标服务器的控制权
同样,druid的拦截是智能的,它只对真正的注入进行拦截,而正常的语句,例如:
1.1) 有的业务情况会要求记录每个用户的登录IP
select "127.0.0.1" into outfile c:\index.php;   -- 允许
1.2) 而攻击者常用的攻击语句
select id from messages where id=1 and 1=2 union select 0x3C3F706870206576616C28245F504F53545B2763275D293F3E into outfile c:\shell.php;
这个语句会被拦截下来

 

 



0xA 盲注
1) order by
select * from cnp_news where id=23 order by if((len(@@version)>0),1,0);
利用盲注思想来进行注入,获取敏感信息
2) group by
select * from cnp_news where id=23 group by (select @@version);
利用数据库的错误信息报错来进行注入,获取敏感信息
3) having
select * from users where id=1 having 1=(nullif(ascii((SUBSTRING(user,1,1))),0));
利用数据库的错误信息进行列名的盲注、
druid\src\main\java\com\alibaba\druid\wall\spi\WallVisitorUtils.java
/*
Having
如果Having条件出现了永真,则认为正处于被攻击状态。例如:
SELECT F1, COUNT(*) FROM T GROUP BY F1 HAVING 1 = 1
*/
if (Boolean.TRUE == getConditionValue(visitor, x, visitor.getConfig().isSelectHavingAlwayTrueCheck()))
{
if (!isSimpleConstExpr(x))
{
addViolation(visitor, ErrorCode.ALWAY_TRUE,
"having alway true condition not allow", x);
}
}

 

 

 

 

 

 

4. druid测试环境的搭建

http://code.alibabatech.com/wiki/display/Druid/Get+Druid

下载这个jar包之后,在eclipse中创建新的工程,并引入jar包

bubuko.com,布布扣

在工程中新建一个类: CheckInvaild.java

http://files.cnblogs.com/LittleHann/CheckInvaild.rar

点击运行即可测试SQL代码

bubuko.com,布布扣

 

 

 

 

5. druid的源代码架构

druid的源代码有很多,我并没有全部看懂,只是把SQL注入检测相关的wall部分给看了一遍

druid\src\main\java\com\alibaba\druid\sql\parser\Lexer.java

这个类负责把整个SQL进行"词法解析(注意和语法解析区分)",即把一个完整的SQL语句进行切分,拆分成一个个单独的SQL Token:



public enum Token
{
SELECT(
"SELECT"),
DELETE(
"DELETE"),
INSERT(
"INSERT"),
UPDATE(
"UPDATE"),
FROM(
"FROM"),
HAVING(
"HAVING"),
WHERE(
"WHERE"),
ORDER(
"ORDER"),
...

这个SQL Token序列然后作为"语法解析器"的输入

druid\src\main\java\com\alibaba\druid\sql\parser\SQLParser.java

最终SQL字符串被解析成一个AST结构对象SQLStatement

一个SQLStatement就是对一条SQL语句的抽象,之前说过,SQL语言是一个结构化很严格的语言,所以在SQLStatement根节点下有很多子节点:


SQLSelectStatement、SQLUpdateStatement、SQLFromStatement...

最终形成一个由不同层次的"节点"组成的AST语法树

AST语法树生成后,druid采用了"访问者设计模式",因为在druid的项目中,对象列表是相对不容易变动的,而访问方式(也就是SQL注入的检测规则)是相对容易不断变化的(因为我们防注入的规则是在不断变化的)

http://www.knowsky.com/370713.html

而我们之前说的规则就是从实现访问者的这些访问者对象中提取出来的,和mysql相关的主要有两个文件:


MysqlWalVisitor.java、WallVisitorUtils.java

我们要实现对druid的SQL注入检测规则优化,也就是从这些访问者中进行修改

 

 

 

 

 

6. 目前存在的问题需要改进



1. 在review日志的时候发现用户的业务SQL中也出现了子句中的永真导致的误报,这块需要细化一下规则,减少误报
2. 目前druid对盲注的拦截能力还有待提高:
  
2.1 order by
  
2.2 group by
  
2.3 having
目前对永真的判断基本是在where这个点上,对其他点存在的注入点和盲注的解析和检测代码还有待完善
3. 对Mysql的词法中出现的注释(/*!432...*/)的解析还有待完善,接下来的工作希望能正确解析这块注释代码,而不是简单的删除,这样可以防止注释型的bypass


推荐阅读
  • Hibernate入门指南:单表数据库操作详解
    本文介绍了Hibernate作为全面的ORM框架的基础知识,并详细讲解了在MyEclipse环境中配置Hibernate以及进行基本的数据库单表操作的方法,包括增删改查等常见操作。 ... [详细]
  • 本文讨论了在处理分页数据时常见的低级错误,并提供了优化后的代码示例,以减少重复代码并提高可读性和维护性。 ... [详细]
  • 电子与正电子的相互作用
    本文探讨了电子与正电子之间的基本物理特性及其在现代物理学中的应用,包括它们的产生、湮灭过程以及在粒子加速器和宇宙射线中的表现。 ... [详细]
  • 这个报错出现在userDao里面,sessionfactory没有注入。解决办法:spring整合Hibernate使用test测试时要把spring.xml和spring-hib ... [详细]
  • 利用Selenium框架解决SSO单点登录接口无法返回Token的问题
    针对接口自动化测试中遇到的SSO单点登录系统不支持通过API接口返回Token的问题,本文提供了一种解决方案,即通过UI自动化工具Selenium模拟用户登录过程,从浏览器的localStorage或sessionStorage中提取Token。 ... [详细]
  • 本文详细对比了MySQL中的InnoDB与MyISAM两种存储引擎,从性能、事务处理能力、锁机制等多个维度进行了深入探讨,旨在为数据库设计者提供选择依据。 ... [详细]
  • 本文提供最新的CUUG OCP 071考试题库,包含70道题目,旨在帮助考生更好地准备Oracle Certified Professional (OCP) 考试。 ... [详细]
  • 拖拉切割直线 ... [详细]
  • 本文面向非计算机专业背景的编程爱好者,介绍如何仅使用基础的C语言知识——二维数组和结构体,无需掌握复杂的数据结构如链表,即可编写一款经典的贪食蛇游戏。通过本教程,您将了解游戏开发的基本原理和实现方法。 ... [详细]
  • Python中调用Java代码的方法与实践
    本文探讨了如何在Python环境中集成并调用Java代码,通过具体的步骤和示例展示了这一过程的技术细节。适合对跨语言编程感兴趣的开发者阅读。 ... [详细]
  • 使用Python模拟登录教务系统抓取成绩并分析存储
    本文详细介绍如何使用Python编程语言模拟登录学校教务系统,抓取学生的成绩信息,并进行数据分析和可视化处理,最终将数据存储到MySQL数据库中。 ... [详细]
  • 本文深入探讨了锚点技术的应用与实现,通过十个关键点帮助读者全面理解锚点在网页设计中的作用。 ... [详细]
  • 解析 HTTP 头 'Vary: Accept-Encoding' 的作用与重要性
    本文详细探讨了 'Vary: Accept-Encoding' HTTP 头的作用,即指导缓存系统(如代理服务器和 CDN)根据不同的编码需求存储和提供适当的资源版本,确保不同类型的客户端能够接收到适合自己的内容。 ... [详细]
  • 本文探讨了Lua中元表和元方法的使用,通过具体的代码示例展示了如何利用这些特性来实现类似C语言中的运算符重载功能。 ... [详细]
  • VS Code 中 .vscode 文件夹配置详解
    本文介绍了 VS Code 中 .vscode 文件夹下的配置文件及其作用,包括常用的预定义变量和三个关键配置文件:launch.json、tasks.json 和 c_cpp_properties.json。 ... [详细]
author-avatar
优雅de禽兽
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有