LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

如何有效防范SQL注入-来自微软

admin
2011年1月31日 1:7 本文热度 2778

SQL 注入是一种攻击方式,在这种攻击方式中,恶意代码被插入到字符串中,然后将该字符串传递到 SQL Server 的实例以进行分析和执行。任何构成 SQL 语句的过程都应进行注入漏洞检查,因为 SQL Server 将执行其接收到的所有语法有效的查询。一个有经验的、坚定的攻击者甚至可以操作参数化数据。

SQL 注入的主要形式包括直接将代码插入到与 SQL 命令串联在一起并使其得以执行的用户输入变量。一种间接的攻击会将恶意代码注入要在表中存储或作为元数据存储的字符串。在存储的字符串随后串连到一个动态 SQL 命令中时,将执行该恶意代码。

注入过程的工作方式是提前终止文本字符串,然后追加一个新的命令。由于插入的命令可能在执行前追加其他字符串,因此攻击者将用注释标记“--”来终止注入的字符串。执行时,此后的文本将被忽略。

以下脚本显示了一个简单的 SQL 注入。此脚本通过串联硬编码字符串和用户输入的字符串而生成一个 SQL 查询:

var Shipcity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

用户将被提示输入一个市县名称。如果用户输入 Redmond,则查询将由与下面内容相似的脚本组成:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond'

但是,假定用户输入以下内容:

Redmond'; drop table OrdersTable--

此时,脚本将组成以下查询:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

分号 (;) 表示一个查询的结束和另一个查询的开始。双连字符 (--) 指示当前行余下的部分是一个注释,应该忽略。如果修改后的代码语法正确,则服务器将执行该代码。SQL Server 处理该语句时,SQL Server 将首先选择 OrdersTable 中的所有记录(其中 ShipCityRedmond)。然后,SQL Server 将删除 OrdersTable

只要注入的 SQL 代码语法正确,便无法采用编程方式来检测篡改。因此,必须验证所有用户输入,并仔细检查在您所用的服务器中执行构造 SQL 命令的代码。本主题中的以下各部分说明了编写代码的最佳做法。

始终通过测试类型、长度、格式和范围来验证用户输入。实现对恶意输入的预防时,请注意应用程序的体系结构和部署方案。请记住,为在安全环境下运行而设计的程序可能被复制到不安全的环境中。以下建议应被视为最佳做法:

  • 对应用程序接收的数据不做任何有关大小、类型或内容的假设。例如,您应该进行以下评估:
    • 如果一个用户在需要邮政编码的位置无意中或恶意地输入了一个 10 MB 的 MPEG 文件,应用程序会做出什么反应?
    • 如果在文本字段中嵌入了一个 DROP TABLE 语句,应用程序会做出什么反应?
  • 测试输入的大小和数据类型,强制执行适当的限制。这有助于防止有意造成的缓冲区溢出。
  • 测试字符串变量的内容,只接受所需的值。拒绝包含二进制数据、转义序列和注释字符的输入内容。这有助于防止脚本注入,防止某些缓冲区溢出攻击。
  • 使用 XML 文档时,根据数据的架构对输入的所有数据进行验证。
  • 绝不直接使用用户输入内容来生成 Transact-SQL 语句。
  • 使用存储过程来验证用户输入。
  • 在多层环境中,所有数据都应该在验证之后才允许进入可信区域。未通过验证过程的数据应被拒绝,并向前一层返回一个错误。
  • 实现多层验证。对无目的的恶意用户采取的预防措施对坚定的攻击者可能无效。更好的做法是在用户界面和所有跨信任边界的后续点上验证输入。
    例如,在客户端应用程序中验证数据可以防止简单的脚本注入。但是,如果下一层假设其输入已被验证,则任何可以跳过客户端的恶意用户就可能不受限制地访问系统。
  • 绝不串联未验证的用户输入。字符串串联是脚本注入的主要输入点。
  • 在可能据以构造文件名的字段中,不接受下列字符串:AUX、CLOCK$、COM1 到 COM8、CON、CONFIG$、LPT1 到 LPT8、NUL 以及 PRN。

如果可能,拒绝包含以下字符的输入。

输入字符 在 Transact-SQL 中的含义

;

查询分隔符。

'

字符数据字符串分隔符。

--

注释分隔符。

/* ... */

注释分隔符。服务器不对 /* 和 */ 之间的注释进行处理。

Xp_

用于目录扩展存储过程的名称的开头,如 xp_cmdshell

使用类型安全的 SQL 参数

SQL Server 中的 Parameters 集合提供了类型检查和长度验证。如果使用 Parameters 集合,则输入将被视为文字值而不是可执行代码。使用 Parameters 集合的另一个好处是可以强制执行类型和长度检查。范围以外的值将触发异常。以下代码段显示了如何使用 Parameters 集合:

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
SqlDbType.VarChar, 11);
parm.Value = Login.Text;

在此示例中,@au_id 参数被视为文字值而不是可执行代码。将对此值进行类型和长度检查。如果 @au_id 值不符合指定的类型和长度约束,则将引发异常。

在存储过程中使用参数化输入

存储过程如果使用未筛选的输入,则可能容易受 SQL 注入攻击。例如,以下代码容易受到攻击:

SqlDataAdapter myCommand =
new SqlDataAdapter("LoginStoredProcedure '" +
Login.Text + "'", conn);

如果使用存储过程,则应使用参数作为存储过程的输入。

在动态 SQL 中使用参数集合

如果不能使用存储过程,您仍可使用参数,如以下代码示例所示:

SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SQLParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
SqlDbType.VarChar, 11);
Parm.Value = Login.Text;

筛选输入

筛选输入可以删除转义符,这也可能有助于防止 SQL 注入。但由于可引起问题的字符数量很大,因此这并不是一种可靠的防护方法。以下示例可搜索字符串分隔符。

private string SafeSqlLiteral(string inputSQL)
{
return inputSQL.Replace("'", "''");
}

LIKE 子句

请注意,如果要使用 LIKE 子句,还必须对通配符字符进行转义:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

应检查所有调用 EXECUTE、EXEC 或 sp_executesql 的代码。可以使用类似如下的查询来帮助您标识包含这些语句的过程。

SELECT object_Name(id) FROM syscomments

WHERE UPPER(text) LIKE '%EXECUTE (%'

OR UPPER(text) LIKE '%EXECUTE (%'

OR UPPER(text) LIKE '%EXECUTE (%'

OR UPPER(text) LIKE '%EXECUTE (%'

OR UPPER(text) LIKE '%EXEC (%'

OR UPPER(text) LIKE '%EXEC (%'

OR UPPER(text) LIKE '%EXEC (%'

OR UPPER(text) LIKE '%EXEC (%'

OR UPPER(text) LIKE '%SP_EXECUTESQL%'

使用 QUOTENAME() 和 REPLACE() 包装参数

在选择的每个存储过程中,验证是否对动态 Transact-SQL 中使用的所有变量都进行了正确处理。来自存储过程的输入参数的数据或从表中读取的数据应包装在 QUOTENAME() 或 REPLACE() 中。请记住,传递给 QUOTENAME() 的 @variable 值的数据类型为 sysname,且最大长度为 128 个字符。

@variable 建议的包装

安全对象的名称

QUOTENAME(@variable)

字符串 ≤ 128 个字符

QUOTENAME(@variable, '''')

字符串 > 128 个字符

REPLACE(@variable,'''', '''''')

使用此方法时,可对 SET 语句进行如下修改:

--Before:

SET @temp = N'select * from authors where au_lname='''

+ @au_lname + N''''

--After:

SET @temp = N'select * from authors where au_lname='''

+ REPLACE(@au_lname,'''','''''') + N''''

由数据截断启用的注入

如果分配给变量的任何动态 Transact-SQL 比为该变量分配的缓冲区大,那么它将被截断。如果攻击者能够通过将意外长度的字符串传递给存储过程来强制执行语句截断,则该攻击者可以操作该结果。例如,以下脚本创建的存储过程容易受到由截断启用的注入攻击。

CREATE PROCEDURE sp_MySetPassword

@loginname sysname,

@old sysname,

@new sysname

AS

-- Declare variable.

-- Note that the buffer here is only 200 characters long.

DECLARE @command varchar(200)

-- Construct the dynamic Transact-SQL.

-- In the following statement, we need a total of 154 characters

-- to set the password of 'sa'.

-- 26 for UPDATE statement, 16 for WHERE clause, 4 for 'sa', and 2 for

-- quotation marks surrounded by QUOTENAME(@loginname):

-- 200 – 26 – 16 – 4 – 2 = 154.

-- But because @new is declared as a sysname, this variable can only hold

-- 128 characters.

-- We can overcome this by passing some single quotation marks in @new.

SET @command= 'update Users set password=' + QUOTENAME(@new, '''') + ' where username=' + QUOTENAME(@loginname, '''') + ' AND password = ' + QUOTENAME(@old, '''')

 

-- Execute the command.

EXEC (@command)

GO

通过向 128 个字符的缓冲区传递 154 个字符,攻击者便可以在不知道旧密码的情况下为 sa 设置新密码。

EXEC sp_MySetPassword 'sa', 'dummy', '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

因此,应对命令变量使用较大的缓冲区,或直接在 EXECUTE 语句内执行动态 Transact-SQL。

使用 QUOTENAME(@variable, '''') 和 REPLACE() 时的截断

如果 QUOTENAME() 和 REPLACE() 返回的字符串超过了分配的空间,该字符串将被自动截断。以下示例中创建的存储过程显示了可能出现的情况。

CREATE PROCEDURE sp_MySetPassword

@loginname sysname,

@old sysname,

@new sysname

AS

 

-- Declare variables.

DECLARE @login sysname

DECLARE @newpassword sysname

DECLARE @oldpassword sysname

DECLARE @command varchar(2000)

 

-- In the following statements, the data stored in temp variables

-- will be truncated because the buffer size of @login, @oldpassword,

-- and @newpassword is only 128 characters, but QUOTENAME() can return

-- up to 258 characters.

 

SET @login = QUOTENAME(@loginname, '''')

SET @oldpassword = QUOTENAME(@old, '''')

SET @newpassword = QUOTENAME(@new, '''')

 

-- Construct the dynamic Transact-SQL.

-- If @new contains 128 characters, then @newpassword will be '123... n

-- where n is the 127th character.

-- Because the string returned by QUOTENAME() will be truncated,

-- it can be made to look like the following statement:

-- UPDATE Users SET password ='1234. . .[127] WHERE username=' -- other stuff here

SET @command = 'UPDATE Users set password = ' + @newpassword

+ ' where username =' + @login + ' AND password = ' + @oldpassword;

 

-- Execute the command.

EXEC (@command)

GO

因此,以下语句将把所有用户的密码都设置为在前面的代码中传递的值。

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

使用 REPLACE() 时,可以通过超出分配的缓冲区空间来强迫字符串截断。以下示例中创建的存储过程显示了可能出现的情况。

CREATE PROCEDURE sp_MySetPassword

@loginname sysname,

@old sysname,

@new sysname

AS

-- Declare variables.

DECLARE @login sysname

DECLARE @newpassword sysname

DECLARE @oldpassword sysname

DECLARE @command varchar(2000)

-- In the following statements, data will be truncated because

-- the buffers allocated for @login, @oldpassword and @newpassword

-- can hold only 128 characters, but QUOTENAME() can return

-- up to 258 characters.

 

SET @login = REPLACE(@loginname, '''', '''''')

SET @oldpassword = REPLACE(@old, '''', '''''')

SET @newpassword = REPLACE(@new, '''', '''''')

 

-- Construct the dynamic Transact-SQL.

-- If @new contains 128 characters, @newpassword will be '123...n

-- where n is the 127th character.

-- Because the string returned by QUOTENAME() will be truncated, it

-- can be made to look like the following statement:

-- UPDATE Users SET password='1234…[127] WHERE username=' -- other stuff here

 

SET @command= 'update Users set password = ''' + @newpassword + ''' where username='''

+ @login + ''' AND password = ''' + @oldpassword + '''';

 

-- Execute the command.

EXEC (@command)

GO

与 QUOTENAME() 一样,可以通过声明对所有情况都足够大的临时变量来避免由 REPLACE() 引起的字符串截断。应尽可能直接在动态 Transact-SQL 内调用 QUOTENAME() 或 REPLACE()。或者,也可以按如下方式计算所需的缓冲区大小。对于 @outbuffer = QUOTENAME(@input)@outbuffer 的大小应为 2*(len(@input)+1). 。使用 REPLACE() 和双引号时(如上一示例),大小为 2*len(@input) 的缓冲区便已足够。

以下计算涵盖所有情况:

While len(@find_string) > 0, required buffer size =

round(len(@input)/len(@find_string),0) * len(@new_string)

+ (len(@input) % len(@find_string))

使用 QUOTENAME(@variable, ']') 时的截断

当 SQL Server 安全对象的名称被传递给使用 QUOTENAME(@variable, ']') 形式的语句时,可能发生截断。以下代码显示了这一可能性。

CREATE PROCEDURE sp_MyProc

@schemaname sysname,

@tablename sysname,

AS

 

-- Declare a variable as sysname. The variable will be 128 characters.

-- But @objectname actually must accommodate 2*258+1 characters.

DECLARE @objectname sysname

SET @objectname = QUOTENAME(@schemaname)+'.'+ QUOTENAME(@tablename)

 

-- Do some operations.

GO

当您串联 sysname 类型的值时,应使用足够大的临时变量来保存每个值的最多 128 个字符。应尽可能直接在动态 Transact-SQL 内调用 QUOTENAME()。或者,也可以按上一部分所述来计算所需的缓冲区大小。


该文章在 2011/1/31 1:07:43 编辑过

全部评论6

admin
2011年1月31日 1:8
所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。在某些表单中,用户输入的内容直接用来构造(或者影响)动态SQL命令,或作为存储过程的输入参数,这类表单特别容易受到SQL注入式攻击。常见的SQL注入式攻击过程类如:   ⑴ 某个ASP.NET Web应用有一个登录页面,这个登录页面控制着用户是否有权访问应用,它要求用户输入一个名称和密码。   ⑵ 登录页面中输入的内容将直接用来构造动态的SQL命令,或者直接用作存储过程的参数。下面是ASP.NET应用构造查询的一个例子:   System.Text.StringBuilder query = new System.Text.StringBuilder("SELECT * from Users WHERE login = ’")。Append(txtLogin.Text)。Append("’ AND password=’")。Append(txtPassword.Text)。Append("’");   ⑶ 攻击者在用户名字和密码输入框中输入"’或’1’=’1"之类的内容。   ⑷ 用户输入的内容提交给服务器之后,服务器运行上面的ASP.NET代码构造出查询用户的SQL命令,但由于攻击者输入的内容非常特殊,所以最后得到的SQL命令变成:SELECT * from Users WHERE login = ’’ or ’1’=’1’ AND password = ’’ or ’1’=’1’.   ⑸ 服务器执行查询或存储过程,将用户输入的身份信息和服务器中保存的身份信息进行对比。   ⑹ 由于SQL命令实际上已被注入式攻击修改,已经不能真正验证用户身份,所以系统会错误地授权给攻击者。   如果攻击者知道应用会将表单中输入的内容直接用于验证身份的查询,他就会尝试输入某些特殊的SQL字符串篡改查询改变其原来的功能,欺骗系统授予访问权限。   系统环境不同,攻击者可能造成的损害也不同,这主要由应用访问数据库的安全权限决定。如果用户的帐户具有管理员或其他比较高级的权限,攻击者就可能对数据库的表执行各种他想要做的操作,包括添加、删除或更新数据,甚至可能直接删除表。   二、如何防范?   好在要防止ASP.NET应用被SQL注入式攻击闯入并不是一件特别困难的事情,只要在利用表单输入的内容构造SQL命令之前,把所有输入内容过滤一番就可以了。过滤输入内容可以按多种方式进行。   ⑴ 对于动态构造SQL查询的场合,可以使用下面的技术:   第一:替换单引号,即把所有单独出现的单引号改成两个单引号,防止攻击者修改SQL命令的含义。再来看前面的例子,“SELECT * from Users WHERE login = ’’’ or ’’1’’=’’1’ AND password = ’’’ or ’’1’’=’’1’”显然会得到与“SELECT * from Users WHERE login = ’’ or ’1’=’1’ AND password = ’’ or ’1’=’1’”不同的结果。   第二:删除用户输入内容中的所有连字符,防止攻击者构造出类如“SELECT * from Users WHERE login = ’mas’ —— AND password =’’”之类的查询,因为这类查询的后半部分已经被注释掉,不再有效,攻击者只要知道一个合法的用户登录名称,根本不需要知道用户的密码就可以顺利获得访问权限。   第三:对于用来执行查询的数据库帐户,限制其权限。用不同的用户帐户执行查询、插入、更新、删除操作。由于隔离了不同帐户可执行的操作,因而也就防止了原本用于执行SELECT命令的地方却被用于执行INSERT、UPDATE或DELETE命令。   ⑵ 用存储过程来执行所有的查询。SQL参数的传递方式将防止攻击者利用单引号和连字符实施攻击。此外,它还使得数据库权限可以限制到只允许特定的存储过程执行,所有的用户输入必须遵从被调用的存储过程的安全上下文,这样就很难再发生注入式攻击了。   ⑶ 限制表单或查询字符串输入的长度。如果用户的登录名字最多只有10个字符,那么不要认可表单中输入的10个以上的字符,这将大大增加攻击者在SQL命令中插入有害代码的难度。   ⑷ 检查用户输入的合法性,确信输入的内容只包含合法的数据。数据检查应当在客户端和服务器端都执行——之所以要执行服务器端验证,是为了弥补客户端验证机制脆弱的安全性。   在客户端,攻击者完全有可能获得网页的源代码,修改验证合法性的脚本(或者直接删除脚本),然后将非法内容通过修改后的表单提交给服务器。因此,要保证验证操作确实已经执行,唯一的办法就是在服务器端也执行验证。你可以使用许多内建的验证对象,例如RegularExpressionValidator,它们能够自动生成验证用的客户端脚本,当然你也可以插入服务器端的方法调用。如果找不到现成的验证对象,你可以通过CustomValidator自己创建一个。   ⑸ 将用户登录名称、密码等数据加密保存。加密用户输入的数据,然后再将它与数据库中保存的数据比较,这相当于对用户输入   的数据进行了“消毒”处理,用户输入的数据不再对数据库有任何特殊的意义,从而也就防止了攻击者注入SQL命令。 System.Web.Security.FormsAuthentication类有一个 HashPasswordForStoringInConfigFile,非常适合于对输入数据进行消毒处理。   ⑹ 检查提取数据的查询所返回的记录数量。如果程序只要求返回一个记录,但实际返回的记录却超过一行,那就当作出错处理。

该评论在 2011/1/31 1:08:09 编辑过
admin
2011年1月31日 1:8
sql注入就是,通过语句的连接做一些不是你想要的操作.. 举个例子你就懂了 例如你要查询id=1的记录,直接连接就是这样"select * from tableName where id=1" 别人可以写成"select * from tableName where id=1;delete from tableName" 这样就把你的表数据全部删除了.就是加个;继续写脚本,当然,这只是个例子..还能做其他操作, 比如获取你数据库的用户名,密码什么的,那就惨了,,传参的方式可以防止注入 "select * from tableName where id=@id" 然后给@id赋值,就ok啦.. 这是我的个人看法,,期待更好的解答

该评论在 2011/1/31 1:08:27 编辑过
admin
2011年1月31日 1:9
在conn.asp中添加代码: 
<%
dim sql_injdata,SQL_inj,SQL_Get,SQL_Data,Sql_Post
SQL_injdata = "'│and│exec│insert│select│delete│update│count│*│%│chr│mid│master│truncate│char│declare"
SQL_inj = split(SQL_Injdata,"│")
If Request.QueryString<>"" Then
For Each SQL_Get In Request.QueryString
For SQL_Data=0 To Ubound(SQL_inj)
if instr(Request.QueryString(SQL_Get),Sql_Inj(Sql_DATA))>0 Then
Response.Write "<Script Language=javascript>alert('注意:请不要提交非法请求!');history.back(-1)</Script>"
Response.end
end if
next
Next
End If
If Request.Form<>"" Then
For Each Sql_Post In Request.Form
For SQL_Data=0 To Ubound(SQL_inj)
if instr(Request.Form(Sql_Post),Sql_Inj(Sql_DATA))>0 Then
Response.Write "<Script Language=javascript>alert('注意:请不要提交非法请求!');history.back(-1)</Script>"
Response.end
end if
next
next
end if
%>

该评论在 2011/1/31 1:10:09 编辑过
admin
2011年1月31日 1:28

防止SQL病毒注入都有那些好方法呢?附加点代码,谢谢了!

在你接收url参数的时候 过滤特殊字符就可以了 veryeasy~~ 
给你一个函数
'_______________________________________________________________
'函数名:SetRequest
'作 用:防止SQL注入
'ParaName:参数名称-字符型
'ParaType:参数类型-数字型(1表示是数字,0表示为字符)
'RequestType:请求方式(0:直接请求,1:Request请求,2:post请求,3:get请求,4:Cookies请求,5:WEB请求)
'_______________________________________________________________
Public Function SetRequest(ParaName,RequestType,ParaType)
Dim ParaValue
Select Case RequestType
Case 0
ParaValue=ParaName
Case 1
ParaValue=Request(ParaName)
Case 2
ParaValue=Request.Form(ParaName)
Case 3
ParaValue=Request.QueryString(ParaName)
Case 4
ParaValue=Request.Cookies(ParaName)
Case 5
ParaValue=Request.ServerVariables(ParaName)
End Select

If ParaType=1 Then
If instr(ParaValue,",")>0 Then
If not isNumeric(Replace(Replace(ParaValue,",","")," ","")) Then
Response.Redirect("/")
End If
Else
If not isNumeric(ParaValue) Then
Response.Redirect("/")
End If
End If
Else
ParaValue=Replace(Replace(ParaValue,Chr(0),""),"'","")
End If
SetRequest=ParaValue
End function


接收参数的时候 全部用 SetRequest(参数) 就可以了

该评论在 2011/1/31 1:29:22 编辑过
admin
2011年1月31日 1:35
在.NET中,如何防止sql注入了? 最好的方法是什么了 首先 你需要在客户端尽量的限制非法字符 可以用正则表达式 再次 你需要在sql语句中限制 如果是C#的话 可以再SQL字符串前面加@ 最后 尽量使用存储过程而少用SQL语句 这样基本就差不多了 再能被注入就是高手了 那个防不胜防

该评论在 2011/1/31 1:35:05 编辑过
admin
2011年1月31日 1:39
Chr(0)是什么意思? 后面加chr(0)是标准化格式,也是为了安全,防止类似“注入”之类的让你的原路径c:\123被更改而被它人利用, 你可以看一下这三句就知道区别了 SHFileOp.pFrom = "c:\123" + Chr(0) (这里的路径为 c:\123 ) SHFileOp.pFrom = "c:\123" + Chr(0) + Chr(65) (这里的路径仍为 c:\123 ) SHFileOp.pFrom = "c:\123" + Chr(65) + Chr(65) (这里的路径就为 c:\123AA )

该评论在 2011/1/31 1:39:53 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved