概述
命令执行漏洞概念:当应用需要调用一些外部程序去处理内容的情况下,就会用到一些执行系统命令的函数。如PHP中的system,exec,shell_exec等,当用户可以控制命令执行函数中的参数时,将可注入恶意系统命令到正常命令中,造成命令执行攻击。
漏洞成因:脚本语言优点是简洁,方便,但也伴随着一些问题,如速度慢,无法解除系统底层,如果我们开发的应用需要一些除去web的特殊功能时,就需要调用一些外部程序。
敲黑板!敲黑板!敲黑板!提到命令执行漏洞,大家难免会想到代码执行漏洞,两者确实很像,没有太大的区别,提交漏洞时可能没有分的那么详细,不过我们在学习时还是要学会区分不同的概念。
(1)命令执行漏洞:直接调用操作系统命令。
其原理为:在操作系统中,“&、|、||”都可以作为命令连接符使用,用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。
(2)代码执行漏洞:靠执行脚本代码调用操作系统命令。
其原理为:应用有时需要调用一些执行系统命令的函数,如PHP中的system、exec、shell_exec、passthru、popen、proc_popen等,当用户能控制这些函数中的参数时,就可以将恶意系统命令拼接到正常命令中,从而造成命令执行攻击,这就是命令执行漏洞。
漏洞示例
入门示例
命令执行漏洞是指攻击者可以随意执行系统命令。
进阶示例
注意:下面的例子可能需要通读全文后才能更好的理解。
使用phpstudy搭建环境,新建一个test.php文件,其中输入代码:
<?php
$arg =$_GET['cmd'];
if ($arg) {
system(“$arg”);
}
?>
1
2
3
4
5
6
在浏览器中访问:
我们没有加任何东西,当然会报错啦。接下来我们给它加上参数:
这样的操作就是相当于我们在命令行(cmd)下进行ipconfig的查询:
同样我们来做一个代码执行的页面。新建hetian.php,输入代码:
在浏览器中打开:
接下来我们加入自己的参数,在挖掘漏洞时我们最常见的就是使用phpinfo():
我们这里也使用phpinfo();这样执行的效果和我们直接去phpinfo.php查看是一样的:
命令执行模型
任何脚本语言都可以调用操作系统命令,而各个脚本语言的实现方式都不一样,接下来以PHP和Java两种程序语言为例分析。当程序员明白了这写函数存在的问题后,才能真正做到安全开发。
PHP命令执行
实例1、命令执行
实例2、代码执行
注意:phpinfo()函数用于显示PHP的版本信息页面。
实例3、动态函数调用
PHP支持动态函数调用,代码如下:
Java命令执行
在JavaSE中,存在Runtime类,在该类中提供了exec方法用以在单独的进程中执行指定的字符串命令。像JSP、Servlet、Spring、Hibernate等技术一般执行外部程序都会调用此方法。下面以Runtime类为例进行说明,模型代码如下:
Java反序列化漏洞
1、什么是序列化和反序列化
Java描述的是一个‘世界’,程序运行开始时,这个‘世界’也开始运作,但‘世界’中的对象不是一成不变的,它的属性会随着程序的运行而改变。
但很多情况下,我们需要保存某一刻某个对象的信息,来进行一些操作。比如利用反序列化将程序运行的对象状态以二进制形式储存与文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。可以有效地实现多平台之间的通信、对象持久化存储。
一个类的对象要想序列化成功,必须满足两个条件:
(1)该类必须实现 java.io.Serializable 接口。
(2)该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
如果你想知道一个 Java 标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现 java.io.Serializable接口。
下面书写一个简单的demo,为了节省文章篇幅,这里把序列化操作和反序列化操作弄得简单一些,并省去了传递过程。
对象所属类:
public class Employee implements java.io.Serializable
{
public String name;
public String identify;
public void mailCheck()
{
System.out.println("This is the "+this.identify+" of our company");
}
}
1
2
3
4
5
6
7
8
9
将对象序列化为二进制文件:
//反序列化所需类在io包中
import java.io.*;
public class SerializeDemo
{
public static void main(String [] args)
{
Employee e = new Employee();
e.name = "员工甲";
e.identify = "General staff";
try
{
// 打开一个文件输入流
FileOutputStream fileOut =
new FileOutputStream("D:\Task\employee1.db");
// 建立对象输入流
ObjectOutputStream out = new ObjectOutputStream(fileOut);
//输出反序列化对象
out.writeObject(e);
out.close();
fileOut.close();
System.out.printf("Serialized data is saved in D:\Task\employee1.db");
}catch(IOException i)
{
i.printStackTrace();
}
}
}
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
一个Identity属性为Visitors的对象被储存进了employee1.db,而反序列化操作就是从二进制文件中提取对象:
import java.io.*;
public class SerializeDemo
{
public static void main(String [] args)
{
Employee e = null;
try
{
// 打开一个文件输入流
FileInputStream fileIn = new FileInputStream("D:\Task\employee1.db");
// 建立对象输入流
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取对象
e = (Employee) in.readObject();
in.close();
fileIn.close();
}catch(IOException i)
{
i.printStackTrace();
return;
}catch(ClassNotFoundException c)
{
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
System.out.println("Deserialized Employee...");
System.out.println("Name: " + e.name);
System.out.println("This is the "+e.identify+" of our company");
}
}
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
就这样,一个完整的序列化周期就完成了,其实实际应用中的序列化无非就是传输的方式和传输机制稍微复杂一点,和这个demo没有太大区别。
2、简单的反序列化漏洞demo
在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法书写不当时就会引发漏洞。
PS:有时也会使用readUnshared()方法来读取对象,readUnshared()不允许后续的readObject和readUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。
//反序列化所需类在io包中
import java.io.*;
public class test{
public static void main(String args[]) throws Exception{
UnsafeClass Unsafe = new UnsafeClass();
Unsafe.name = "hacked by ph0rse";
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将Unsafe对象写入object文件
os.writeObject(Unsafe);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
UnsafeClass objectFromDisk = (UnsafeClass)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
class UnsafeClass implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行命令
Runtime.getRuntime().exec("calc.exe");
}
}
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
程序运行逻辑为:
(1)UnsafeClass类被序列化进object文件
(2)从object文件中恢复对象
(3)调用被恢复对象的readObject方法
(4)命令执行
了解更多Java反序列化漏洞的信息:https://xz.aliyun.com/t/2041#toc-3
框架执行漏洞
至今,框架技术已经被广泛应用,框架让开发变得更简单、更省时、更高效,越来越多的开发者喜欢用框架。使用框架对开发者来说是好事,但是一旦出现了安全漏洞,则是致命的。框架的用户群体越多,危害就越大。像之前爆出的Struts2代码执行漏洞,就让国内的网站消失了一大批。
Struts2代码执行漏洞
这就是Struts2<2.2.0的命令执行漏洞,让国内外的一些金融、教育、电子商城网站死了一大批,罪魁祸首不是网站自身的漏洞,而是Struts2框架。
Struts2漏洞重现与原理分析:
简单说一下Struts2基于“commons-fileupload”组件实现文件上传的漏洞,漏洞编号CVE-2017-5638,S02-45。
漏洞重现:
通过发包模拟器或其它你能修改请求头Content-Type字段的客户端,可以把 Content-Type 修改成诸如以下的形式:
haha~multipart/form-data %{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};
自己搭个web服务器,MVC框架使用struts2 (使用有问题的版本),实现一个文件上传的功能。
然后发包模拟器发包,结果发现在服务端电脑上弹出一个计算器的程序,谁让你动我电脑上的东西了吗?但就是动了,流氓会武术谁也档不住~~ OMG!!!
漏洞形成原理:
由于头字段Content-Type的内容是非法的,不符合格式的,所以上传肯定是会出错的,commons-fileupload组件的ServletFileUpload#parseRequest(RequestContext)方法会抛出异常,其中异常信息就包含ognl格式的字符串%{#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec('calc')};
重点来了,struts2会对异常信息执行国际化的功能,它会使用ognl去解析这个符合ognl语法格式的字符串,然后就悲剧了,成功弹出一个计算器。
漏洞解决方法
a. 使用官方补丁.
b. 对于懒得替换struts2版本的朋友,可以自己在StrutsPrepareAndExecuteFilter过滤器前增加一个过滤器,如果发现Content-Type有非法字符,就不再调用struts2的过滤器就行.
补充一句,为什么不能使用struts2拦截器来过滤这种请求呢,因为漏洞执行的时候还没到strus2的拦截器,在StrutsPrepareAndExecuteFilter类的doFilter方法中,request = prepare.wrapRequest(request);包装请求对象的时候漏洞就执行了,也就是说如果是文件上传请求,在包装请求对象的时候文件就已被保存到服务器了。
ThinkPHP命令执行漏洞
防范命令执行漏洞
了解了代码的执行原理后,对其防范就比较简单了,根据语言的相似点,可以得到以下总结。
(1)尽量不要使用系统执行命令;
(2)在进入执行命令函数/方法之前,变量一定要做好过滤,对敏感字符进行转义;
(3)在使用动态函数之前,确保使用的函数是指定函数之一;
(4)对PHP语言来说,不能完全控制的危险函数最好不要使用。
在进行防范之前,确保输入是否可控,如果外部不可输入,代码只有程序开发人员可以控制,这样,即使你写的代码漏洞百出,也不会有危害。这一点适用于所有的安全漏洞的防范。当然,不建议你这么做,隐藏起来并不是真的安全,要永远假定攻击者知晓系统内情,这样才能做到代码阶段的漏洞防范。
相对而言,只要输入可控,就可能存在安全漏洞,这也验证了一句老话:有输入的地方就有可能存在漏洞!
DVWA
命令执行漏洞:程序中因为某些功能需要执行系统命令,并通过网页传递参数到后台执行,然而最根本的原因是没有对输入框中的内容做代码过滤,正常情况下输入框只能接收指定类型的数据。命令注入漏洞可以使攻击在受攻击的服务器上执行任意的系统命令。
Low
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到,服务器通过判断操作系统执行不同ping命令,但是对IP参数并未做任何的过滤,导致了严重的命令注入漏洞。
【漏洞利用】
如果程序没有进行过滤,那么我们就可以通过连接符执行多条系统命令。 比如可以用&&来执行多条命令,命令连接符:&&,|,&……
Medium
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
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
相比Low级别的代码,服务器端对ip参数做了一定过滤,即把”&&” ,”;”删除,依旧存在安全问题。
【漏洞利用】
因为被过滤的只有”&&”与”;”,所以”&”不会受影响。由于使用的是str_replace把”&&”,”;”替换为空字符,因此可以采用以下方式绕过: 127.0.0.1&;&ipconfig,” ;”会被替换为空字符,这样一来就变成了”127.0.0.1&& ipconfig” ,会成功执行。
High
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);
// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "
{$cmd}";
}
?>
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
相比Medium级别的代码,High级别的代码进一步完善了黑名单,看似过滤了所有的非法字符,但仔细观察到是把”| ”(注意这里|后有一个空格)替换为空字符,于是”| ” 就有用了。我们依然可以绕过。
Impossible
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );
// Split the IP into 4 octects
$octet = explode( ".", $target );
// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
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
可以看到,Impossible级别的代码加入了Anti-CSRF token,同时对参数ip进行了严格的限制,只有诸如“数字.数字.数字.数字”的输入才会被接收执行,因此不存在命令注入漏洞。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_39190897/article/details/86761882